From 7a8a892efdf59925a95cdf6504f7c74c31b87eeb Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 25 Sep 2015 17:12:41 +0200 Subject: [PATCH 001/908] Add "rake gitlab:list_repos" task --- doc/raketasks/list_repos.md | 30 ++++++++++++++++++++++++++++++ lib/tasks/gitlab/list_repos.rake | 16 ++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 doc/raketasks/list_repos.md create mode 100644 lib/tasks/gitlab/list_repos.rake diff --git a/doc/raketasks/list_repos.md b/doc/raketasks/list_repos.md new file mode 100644 index 0000000000..476428eb4f --- /dev/null +++ b/doc/raketasks/list_repos.md @@ -0,0 +1,30 @@ +# Listing repository directories + +You can print a list of all Git repositories on disk managed by +GitLab with the following command: + +``` +# Omnibus +sudo gitlab-rake gitlab:list_repos + +# Source +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:list_repos RAILS_ENV=production +``` + +If you only want to list projects with recent activity you can pass +a date with the 'SINCE' environment variable. The time you specify +is parsed by the Rails [TimeZone#parse +function](http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html#method-i-parse). + +``` +# Omnibus +sudo gitlab-rake gitlab:list_repos SINCE='Sep 1 2015' + +# Source +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:list_repos RAILS_ENV=production SINCE='Sep 1 2015' +``` + +Note that the projects listed are NOT sorted by activity; they use +the default ordering of the GitLab Rails application. diff --git a/lib/tasks/gitlab/list_repos.rake b/lib/tasks/gitlab/list_repos.rake new file mode 100644 index 0000000000..1377e1ea91 --- /dev/null +++ b/lib/tasks/gitlab/list_repos.rake @@ -0,0 +1,16 @@ +namespace :gitlab do + task list_repos: :environment do + scope = Project + if ENV['SINCE'] + date = Time.parse(ENV['SINCE']) + warn "Listing repositories with activity since #{date}" + project_ids = Project.where(['last_activity_at > ?', date]).pluck(:id) + scope = scope.where(id: project_ids) + end + scope.find_each do |project| + base = File.join(Gitlab.config.gitlab_shell.repos_path, project.path_with_namespace) + puts base + '.git' + puts base + '.wiki.git' + end + end +end From 5bcd0efe3e0b1fef06147d87f843adac717d7c42 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 25 Sep 2015 18:31:54 +0200 Subject: [PATCH 002/908] Add parallel-rsync-repos script and start docs --- bin/parallel-rsync-repos | 26 ++++++++ doc/operations/rsyncing_repositories.md | 87 +++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 bin/parallel-rsync-repos create mode 100644 doc/operations/rsyncing_repositories.md diff --git a/bin/parallel-rsync-repos b/bin/parallel-rsync-repos new file mode 100644 index 0000000000..b2429f743b --- /dev/null +++ b/bin/parallel-rsync-repos @@ -0,0 +1,26 @@ +#!/bin/sh +# this script should run as the 'git' user, not root, because of mkdir +# +# Example invocation: +# find /var/opt/gitlab/git-data/repositories -maxdepth 2 | \ +# parallel-rsync-repos /var/opt/gitlab/git-data/repositories /mnt/gitlab/repositories + +SRC=$1 +DEST=$2 + +if [ -z "$JOBS" ] ; then + JOBS=10 +fi + +if [ -z "$SRC" ] || [ -z "$DEST" ] ; then + echo "Usage: $0 SRC DEST" + exit 1 +fi + +if ! cd $SRC ; then + echo "cd $SRC failed" + exit 1 +fi + +sed "s|$SRC|./|" |\ + parallel -j$JOBS --progress "mkdir -p $DEST/{} && rsync --delete -a {}/. $DEST/{}/" diff --git a/doc/operations/rsyncing_repositories.md b/doc/operations/rsyncing_repositories.md new file mode 100644 index 0000000000..231e09f046 --- /dev/null +++ b/doc/operations/rsyncing_repositories.md @@ -0,0 +1,87 @@ +# Moving repositories managed by GitLab + +Sometimes you need to move all repositories managed by GitLab to +another filesystem or another server. In this document we will look +at some of the ways you can copy all your repositories from +`/var/opt/gitlab/git-data/repositories` to `/mnt/gitlab/repositories`. + +We will look at three scenarios: the target directory is empty, the +target directory contains an outdated copy of the repositories, and +how to deal with thousands of repositories. + +**Each of the approaches we list can/will overwrite data in the +target directory `/mnt/gitlab/repositories`. Do not mix up the +source and the target.** + +## Target directory is empty: use a tar pipe + +If the target directory `/mnt/gitlab/repositories` is empty the +simplest thing to do is to use a tar pipe. + +``` +# As the git user +tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\ + tar -C /mnt/gitlab/repositories -xf - +``` + +If you want to see progress, replace `-xf` with `-xvf`. + +### Tar pipe to another server + +You can also use a tar pipe to copy data to another server. If your +'git' user has SSH access to the newserver as 'git@newserver', you +can pipe the data through SSH. + +``` +# As the git user +tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\ + ssh git@newserver tar -C /mnt/gitlab/repositories -xf - +``` + +If you want to compress the data before it goes over the network +(which will cost you CPU cycles) you can replace `ssh` with `ssh +-C`. + +## The target directory contains an outdated copy of the repositories: use rsync + +In this scenario it is better to use rsync. This utility is either +already installed on your system or easily installable via apt, yum +etc. + +``` +# As the 'git' user +rsync -a --delete /var/opt/gitlab/git-data/repositories/. \ + /mnt/gitlab/repositories +``` + +The `/.` in the command above is very important, without it you can +easily get the wrong directory structure in the target directory. +If you want to see progress, replace `-a` with `-av`. + +### Single rsync to another server + +If the 'git' user on your source system has SSH access to the target +server you can send the repositories over the network with rsync. + +``` +# As the 'git' user +rsync -a --delete /var/opt/gitlab/git-data/repositories/. \ + git@newserver:/mnt/gitlab/repositories +``` + +## Thousands of Git repositories: use one rsync per repository + +Every time you start an rsync job it has to inspect all files in +the source directory, all files in the target directory, and then +decide what files to copy or not. If the source or target directory +has many contents this startup phase of rsync can become a burden +for your GitLab server. In cases like this you can make rsync's +life easier by dividing its work in smaller pieces, and sync one +repository at a time. + +In addition to rsync we will use [GNU +Parallel](http://www.gnu.org/software/parallel/). This utility is +not included in GitLab so you need to install it yourself with apt +or yum. Also note that the GitLab scripts we used below were added +in GitLab 8.???. + From 9f3984b5e8fb261eb24be76ec548d83c43d58b96 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 25 Sep 2015 18:32:51 +0200 Subject: [PATCH 003/908] Rename doc file --- .../{rsyncing_repositories.md => moving_repositories.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/operations/{rsyncing_repositories.md => moving_repositories.md} (100%) diff --git a/doc/operations/rsyncing_repositories.md b/doc/operations/moving_repositories.md similarity index 100% rename from doc/operations/rsyncing_repositories.md rename to doc/operations/moving_repositories.md From 4dd7c2f1e0174f8de6be9c57f7296e64e1534af5 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 25 Sep 2015 18:35:41 +0200 Subject: [PATCH 004/908] Remove unwanted linebreak --- doc/operations/moving_repositories.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/operations/moving_repositories.md b/doc/operations/moving_repositories.md index 231e09f046..88b90e9131 100644 --- a/doc/operations/moving_repositories.md +++ b/doc/operations/moving_repositories.md @@ -39,8 +39,7 @@ tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\ ``` If you want to compress the data before it goes over the network -(which will cost you CPU cycles) you can replace `ssh` with `ssh --C`. +(which will cost you CPU cycles) you can replace `ssh` with `ssh -C`. ## The target directory contains an outdated copy of the repositories: use rsync From 3ef71fa4fbea9a5ecc3c094df39cf4ea2f97dc90 Mon Sep 17 00:00:00 2001 From: Jan-Gerd Tenberge Date: Sat, 26 Sep 2015 20:53:16 +0200 Subject: [PATCH 005/908] Add support for HiDPI displays in gravatar service --- app/services/gravatar_service.rb | 4 ++-- spec/helpers/application_helper_spec.rb | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/services/gravatar_service.rb b/app/services/gravatar_service.rb index 4bee0c26a6..433ecc2df3 100644 --- a/app/services/gravatar_service.rb +++ b/app/services/gravatar_service.rb @@ -1,13 +1,13 @@ class GravatarService include Gitlab::CurrentSettings - def execute(email, size = nil) + def execute(email, size = nil, scale = 2) if current_application_settings.gravatar_enabled? && email.present? size = 40 if size.nil? || size <= 0 sprintf gravatar_url, hash: Digest::MD5.hexdigest(email.strip.downcase), - size: size, + size: size * scale, email: email.strip end end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 742420f550..e6e9bc99a1 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -141,15 +141,19 @@ describe ApplicationHelper do stub_gravatar_setting(plain_url: 'http://example.local/?s=%{size}&hash=%{hash}') expect(gravatar_icon(user_email, 20)). - to eq('http://example.local/?s=20&hash=b58c6f14d292556214bd64909bcdb118') + to eq('http://example.local/?s=40&hash=b58c6f14d292556214bd64909bcdb118') end it 'accepts a custom size argument' do - expect(helper.gravatar_icon(user_email, 64)).to include '?s=64' + expect(helper.gravatar_icon(user_email, 64)).to include '?s=128' end - it 'defaults size to 40 when given an invalid size' do - expect(helper.gravatar_icon(user_email, nil)).to include '?s=40' + it 'defaults size to 40@2x when given an invalid size' do + expect(helper.gravatar_icon(user_email, nil)).to include '?s=80' + end + + it 'accepts a scaling factor' do + expect(helper.gravatar_icon(user_email, 40, 3)).to include '?s=120' end it 'ignores case and surrounding whitespace' do From f93177c6a3bee4c02f6a9a18278db13dd1c30c35 Mon Sep 17 00:00:00 2001 From: Jan-Gerd Tenberge Date: Sat, 26 Sep 2015 21:25:04 +0200 Subject: [PATCH 006/908] Add scale parameter to helper --- app/helpers/application_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 39ab83ccf1..162dae1ed3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -78,8 +78,8 @@ module ApplicationHelper end end - def gravatar_icon(user_email = '', size = nil) - GravatarService.new.execute(user_email, size) || + def gravatar_icon(user_email = '', size = nil, scale = 2) + GravatarService.new.execute(user_email, size, scale) || default_avatar end From 8d620e39c967739f99bd175449864524f05b915c Mon Sep 17 00:00:00 2001 From: Jan-Gerd Tenberge Date: Sat, 26 Sep 2015 21:51:02 +0200 Subject: [PATCH 007/908] Add scale argument in user class --- app/helpers/application_helper.rb | 4 ++-- app/models/user.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 162dae1ed3..056ffd278f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -68,13 +68,13 @@ module ApplicationHelper end end - def avatar_icon(user_email = '', size = nil) + def avatar_icon(user_email = '', size = nil, scale = 2) user = User.find_by(email: user_email) if user user.avatar_url(size) || default_avatar else - gravatar_icon(user_email, size) + gravatar_icon(user_email, size, scale) end end diff --git a/app/models/user.rb b/app/models/user.rb index 25371f9138..55c095d7d5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -633,11 +633,11 @@ class User < ActiveRecord::Base email.start_with?('temp-email-for-oauth') end - def avatar_url(size = nil) + def avatar_url(size = nil, scale = 2) if avatar.present? [gitlab_config.url, avatar.url].join else - GravatarService.new.execute(email, size) + GravatarService.new.execute(email, size, scale) end end From 6479b821ebc04d9e2ec69be451768968c06ce6a5 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 5 Oct 2015 18:02:12 +0200 Subject: [PATCH 008/908] Add RSYNC variable to parallel-rsync-repos --- bin/parallel-rsync-repos | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) mode change 100644 => 100755 bin/parallel-rsync-repos diff --git a/bin/parallel-rsync-repos b/bin/parallel-rsync-repos old mode 100644 new mode 100755 index b2429f743b..b777056c95 --- a/bin/parallel-rsync-repos +++ b/bin/parallel-rsync-repos @@ -4,6 +4,15 @@ # Example invocation: # find /var/opt/gitlab/git-data/repositories -maxdepth 2 | \ # parallel-rsync-repos /var/opt/gitlab/git-data/repositories /mnt/gitlab/repositories +# +# You can also rsync to a remote destination. +# +# parallel-rsync-repos /var/opt/gitlab/git-data/repositories user@host:/mnt/gitlab/repositories +# +# If you need to pass extra options to rsync, set the RSYNC variable +# +# env RSYNC='rsync --rsh="foo bar"' parallel-rsync-repos /src dest +# SRC=$1 DEST=$2 @@ -17,10 +26,14 @@ if [ -z "$SRC" ] || [ -z "$DEST" ] ; then exit 1 fi +if [ -z "$RSYNC" ] ; then + RSYNC=rsync +fi + if ! cd $SRC ; then echo "cd $SRC failed" exit 1 fi sed "s|$SRC|./|" |\ - parallel -j$JOBS --progress "mkdir -p $DEST/{} && rsync --delete -a {}/. $DEST/{}/" + parallel -j$JOBS --progress "mkdir -p $DEST/{} && $RSYNC --delete -a {}/. $DEST/{}/" From e0ef09d9a35bf001acbb89e4177d942f6db93e50 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 5 Oct 2015 18:02:32 +0200 Subject: [PATCH 009/908] Some more text in the doc --- doc/operations/moving_repositories.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/doc/operations/moving_repositories.md b/doc/operations/moving_repositories.md index 88b90e9131..d156f3ad77 100644 --- a/doc/operations/moving_repositories.md +++ b/doc/operations/moving_repositories.md @@ -16,7 +16,10 @@ source and the target.** ## Target directory is empty: use a tar pipe If the target directory `/mnt/gitlab/repositories` is empty the -simplest thing to do is to use a tar pipe. +simplest thing to do is to use a tar pipe. This method has low +overhead and tar is almost always already installed on your system. +However, it is not possible to resume an interrupted tar pipe: if +that happens then all data must be copied again. ``` # As the git user @@ -43,9 +46,11 @@ If you want to compress the data before it goes over the network ## The target directory contains an outdated copy of the repositories: use rsync -In this scenario it is better to use rsync. This utility is either -already installed on your system or easily installable via apt, yum -etc. +If the target directory already contains a partial / outdated copy +of the repositories it may be wasteful to copy all the data again +with tar. In this scenario it is better to use rsync. This utility +is either already installed on your system or easily installable +via apt, yum etc. ``` # As the 'git' user From ad37f58ebd97fee3c06a531e4067c8adb4c9ecc7 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 6 Oct 2015 12:42:05 +0200 Subject: [PATCH 010/908] Add explanation about parallel-rsync-repos --- doc/operations/moving_repositories.md | 55 ++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/doc/operations/moving_repositories.md b/doc/operations/moving_repositories.md index d156f3ad77..a89602b367 100644 --- a/doc/operations/moving_repositories.md +++ b/doc/operations/moving_repositories.md @@ -87,5 +87,58 @@ In addition to rsync we will use [GNU Parallel](http://www.gnu.org/software/parallel/). This utility is not included in GitLab so you need to install it yourself with apt or yum. Also note that the GitLab scripts we used below were added -in GitLab 8.???. +in GitLab 8.1. +** This process does not clean up repositories at the target location that no +longer exist at the source. ** If you start using your GitLab instance with +`/mnt/gitlab/repositories`, you need to run `gitlab-rake gitlab:cleanup:repos` +after switching to the new repository storage directory. + +### Parallel rsync for all repositories known to GitLab + +This will sync repositories with 10 rsync processes at a time. + +``` +# Omnibus +sudo gitlab-rake gitlab:list_repos |\ + sudo -u git \ + /usr/bin/env JOBS=10 \ + /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repoos \ + /var/opt/gitlab/git-data/repositories \ + /mnt/gitlab/repositories + +# Source +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:list_repos |\ + sudo -u git -H \ + /usr/bin/env JOBS=10 \ + bin/parallel-rsync-repos \ + /home/git/repositories \ + /mnt/gitlab/repositories +``` + +### Parallel rsync only for repositories with recent activity + +Suppose you have already done one sync that started after 2015-10-1 12:00 UTC. +Then you might only want to sync repositories that were changed via GitLab +_after_ that time. You can use the 'SINCE' variable to tell 'rake +gitlab:list_repos' to only print repositories with recent activity. + +``` +# Omnibus +sudo gitlab-rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\ + sudo -u git \ + /usr/bin/env JOBS=10 \ + /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repoos \ + /var/opt/gitlab/git-data/repositories \ + /mnt/gitlab/repositories + +# Source +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\ + sudo -u git -H \ + /usr/bin/env JOBS=10 \ + bin/parallel-rsync-repos \ + /home/git/repositories \ + /mnt/gitlab/repositories +``` From c5d97c7e37fbfc4ed478da24d64d54c770ed2097 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Sat, 19 Sep 2015 13:33:28 +0200 Subject: [PATCH 011/908] COPYING is now also accepted as licence file Fixes #2526 --- CHANGELOG | 2 ++ app/models/repository.rb | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 31feb115d1..f078d08d86 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -90,6 +90,8 @@ v 8.0.1 v 8.0.0 - Fix Markdown links not showing up in dashboard activity feed (Stan Hu) - Remove milestones from merge requests when milestones are deleted (Stan Hu) +v 8.0.0 (unreleased) + - Accept COPYING as licence file (Zeger-Jan van de Weg) - Fix HTML link that was improperly escaped in new user e-mail (Stan Hu) - Fix broken sort in merge request API (Stan Hu) - Bump rouge to 1.10.1 to remove warning noise and fix other syntax highlighting bugs (Stan Hu) diff --git a/app/models/repository.rb b/app/models/repository.rb index 8b51602bc2..cc46ab916c 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -210,9 +210,9 @@ class Repository def license cache.fetch(:license) do - tree(:head).blobs.find do |file| - file.name =~ /\Alicense/i - end + tree(:head).blobs.find_all do |file| + file.name =~ /\A(copying|license)/i + end.last # Prefer `LICENSE` as filename over `COPYING` end end From 9ec32b1cacdd411cbdcc262f7b327c6bfc552735 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 21 Sep 2015 10:14:38 +0200 Subject: [PATCH 012/908] Prefer Licence over Copying --- app/models/repository.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index cc46ab916c..dc7cd92674 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -210,9 +210,13 @@ class Repository def license cache.fetch(:license) do - tree(:head).blobs.find_all do |file| - file.name =~ /\A(copying|license)/i - end.last # Prefer `LICENSE` as filename over `COPYING` + licenses = tree(:head).blobs.find_all do |file| + file.name =~ /\A(copying|license)/i + end + + # If `licence`, `copying` and `copying.lesser` are found, return in the + # following order: licence, copying, copying.lesser + licenses.find { |l| l =~ /\Alicence/i } || licenses.sort.first end end From 7248566632d5de39e7b57594dcf6e021ae912282 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 23 Sep 2015 19:48:53 +0200 Subject: [PATCH 013/908] Add changelog line to right release --- CHANGELOG | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f078d08d86..3d0beb0b7a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -82,6 +82,7 @@ v 8.0.2 - Fix Reply by email for non-UTF-8 messages. - Add option to use StartTLS with Reply by email IMAP server. - Allow AWS S3 Server-Side Encryption with Amazon S3-Managed Keys for backups (Paul Beattie) + - Accept COPYING as licence file (Zeger-Jan van de Weg) v 8.0.1 - Remove git refs used internally by GitLab from network graph (Stan Hu) @@ -90,8 +91,6 @@ v 8.0.1 v 8.0.0 - Fix Markdown links not showing up in dashboard activity feed (Stan Hu) - Remove milestones from merge requests when milestones are deleted (Stan Hu) -v 8.0.0 (unreleased) - - Accept COPYING as licence file (Zeger-Jan van de Weg) - Fix HTML link that was improperly escaped in new user e-mail (Stan Hu) - Fix broken sort in merge request API (Stan Hu) - Bump rouge to 1.10.1 to remove warning noise and fix other syntax highlighting bugs (Stan Hu) From 54e6c0045bb13c05cc5478cbdf47d3246bd9fe2b Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Tue, 22 Sep 2015 14:04:14 +0200 Subject: [PATCH 014/908] Added three rake tasks for repository maintainance Tasks added: gitlab:git:repack gitlab:git:gc gitlab:git:prune --- CHANGELOG | 1 + lib/tasks/gitlab/git.rake | 52 ++++++++++++++++++++++++++++++ lib/tasks/gitlab/task_helpers.rake | 4 +++ 3 files changed, 57 insertions(+) create mode 100644 lib/tasks/gitlab/git.rake diff --git a/CHANGELOG b/CHANGELOG index 31feb115d1..2c98b092e1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -62,6 +62,7 @@ v 8.0.4 - Fix "Assign All" button on Runner admin page - Fix search in Files - Add full project namespace to payload of system webhooks (Ricardo Band) + - Add rake tasks for git repository maintainance (Zeger-Jan van de Weg) v 8.0.3 - Fix URL shown in Slack notifications diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake new file mode 100644 index 0000000000..4fbf5a9393 --- /dev/null +++ b/lib/tasks/gitlab/git.rake @@ -0,0 +1,52 @@ +namespace :gitlab do + namespace :git do + + desc "GitLab | Git | Repack" + task repack: :environment do + failures = perform_git_cmd('git repack -a --quiet', 'Git repack') + if failures.empty? + puts "Done".green + else + output_failures(failures) + end + end + + desc "GitLab | Git | Run gits garbage collection on all repo's" + task gc: :environment do + failures = perform_git_cmd('git gc --auto --quiet', "Garbage Collection") + if failures.empty? + puts "Done".green + else + output_failures(failures) + end + end + + desc "GitLab | Git | Git prune all repo's" + task prune: :environment do + failures = perform_git_cmd('git prune', 'Git Prune') + if failures.empty? + puts "Done".green + else + output_failures(failures) + end + end + + def perform_git_cmd(cmd, message) + puts "Starting #{message} on all repositories" + + failures = [] + all_repos.each do |r| + puts "Performing #{message} at #{r}" + failures << r unless system(*%w(#{cmd}), chdir: r) + end + + failures + end + + def output_failures(failures) + puts "The following repositories reported errors:".red + failures.each { |f| puts "- #{f}" } + end + + end +end diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index c95b6540eb..77c41bf61c 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -128,4 +128,8 @@ namespace :gitlab do false end end + + def all_repos + Dir.glob(File.join(Gitlab.config.gitlab_shell.repos_path, '**/*\.git')) + end end From 8db063b579322238af43f6d04b5968d9c6ea935d Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Thu, 1 Oct 2015 13:34:41 +0200 Subject: [PATCH 015/908] Scalable way of requesting all repos --- lib/tasks/gitlab/git.rake | 19 +++++++++++-------- lib/tasks/gitlab/task_helpers.rake | 6 +++++- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake index 4fbf5a9393..65ee430d55 100644 --- a/lib/tasks/gitlab/git.rake +++ b/lib/tasks/gitlab/git.rake @@ -3,7 +3,7 @@ namespace :gitlab do desc "GitLab | Git | Repack" task repack: :environment do - failures = perform_git_cmd('git repack -a --quiet', 'Git repack') + failures = perform_git_cmd(%W(git repack -a --quiet), "Repacking repo") if failures.empty? puts "Done".green else @@ -11,9 +11,9 @@ namespace :gitlab do end end - desc "GitLab | Git | Run gits garbage collection on all repo's" + desc "GitLab | Git | Run garbage collection on all repos" task gc: :environment do - failures = perform_git_cmd('git gc --auto --quiet', "Garbage Collection") + failures = perform_git_cmd(%W(git gc --auto --quiet), "Garbage Collecting") if failures.empty? puts "Done".green else @@ -21,9 +21,9 @@ namespace :gitlab do end end - desc "GitLab | Git | Git prune all repo's" + desc "GitLab | Git | Prune all repos" task prune: :environment do - failures = perform_git_cmd('git prune', 'Git Prune') + failures = perform_git_cmd(%W(git prune), "Git Prune") if failures.empty? puts "Done".green else @@ -35,9 +35,12 @@ namespace :gitlab do puts "Starting #{message} on all repositories" failures = [] - all_repos.each do |r| - puts "Performing #{message} at #{r}" - failures << r unless system(*%w(#{cmd}), chdir: r) + all_repos do |repo| + if system(*cmd, chdir: repo) + puts "Performed #{message} at #{repo}" + else + failures << repo + end end failures diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index 77c41bf61c..e35fd47c5c 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -130,6 +130,10 @@ namespace :gitlab do end def all_repos - Dir.glob(File.join(Gitlab.config.gitlab_shell.repos_path, '**/*\.git')) + IO.popen(%W(find #{Gitlab.config.gitlab_shell.repos_path} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find| + find.each_line do |path| + yield path.chomp + end + end end end From ed41333a6ecbfcc04781a47a3dc71064c92d837b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 14 Oct 2015 19:27:23 +0200 Subject: [PATCH 016/908] Use Gitlab::Markdown.render with :pipeline option rather than different methods --- app/helpers/gitlab_markdown_helper.rb | 37 +-- app/views/events/_commit.html.haml | 2 +- .../projects/commit/_commit_box.html.haml | 4 +- app/views/projects/commits/_commit.html.haml | 2 +- app/views/projects/issues/show.html.haml | 2 +- .../merge_requests/show/_mr_box.html.haml | 2 +- .../merge_requests/widget/_open.html.haml | 2 +- app/views/projects/milestones/show.html.haml | 2 +- .../projects/repositories/_feed.html.haml | 2 +- lib/gitlab/markdown.rb | 236 +++++++++--------- lib/gitlab/markdown/markdown_filter.rb | 39 +++ lib/gitlab/markdown/relative_link_filter.rb | 2 +- lib/gitlab/markdown/sanitization_filter.rb | 6 +- lib/gitlab/reference_extractor.rb | 20 +- 14 files changed, 181 insertions(+), 177 deletions(-) create mode 100644 lib/gitlab/markdown/markdown_filter.rb diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 6581348212..c6fff1b8ec 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -20,7 +20,7 @@ module GitlabMarkdownHelper end user = current_user if defined?(current_user) - gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: user) + gfm_body = Gitlab::Markdown.render(escaped_body, project: @project, current_user: user, pipeline: :single_line) fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body) if fragment.children.size == 1 && fragment.children[0].name == 'a' @@ -48,37 +48,20 @@ module GitlabMarkdownHelper def markdown(text, context = {}) return "" unless text.present? - context.reverse_merge!( - path: @path, - pipeline: :default, - project: @project, - project_wiki: @project_wiki, - ref: @ref - ) - - user = current_user if defined?(current_user) - + context[:project] ||= @project + html = Gitlab::Markdown.render(text, context) - Gitlab::Markdown.post_process(html, pipeline: context[:pipeline], project: @project, user: user) - end - # TODO (rspeicher): Remove all usages of this helper and just call `markdown` - # with a custom pipeline depending on the content being rendered - def gfm(text, options = {}) - return "" unless text.present? + context.merge!( + current_user: (current_user if defined?(current_user)), - options.reverse_merge!( - path: @path, - pipeline: :default, - project: @project, - project_wiki: @project_wiki, - ref: @ref + # RelativeLinkFilter + requested_path: @path, + project_wiki: @project_wiki, + ref: @ref ) - user = current_user if defined?(current_user) - - html = Gitlab::Markdown.gfm(text, options) - Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], project: @project, user: user) + Gitlab::Markdown.post_process(html, context) end def asciidoc(text) diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml index ad63841ccf..4ba8b84fd9 100644 --- a/app/views/events/_commit.html.haml +++ b/app/views/events/_commit.html.haml @@ -2,4 +2,4 @@ .commit-row-title = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '' · - = gfm event_commit_title(commit[:message]), project: project + = markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index fbf0a9ec0c..f3be1da842 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -50,10 +50,10 @@ .commit-box.gray-content-block.middle-block %h3.commit-title - = gfm escape_once(@commit.title) + = markdown escape_once(@commit.title), pipeline: :single_line - if @commit.description.present? %pre.commit-description - = preserve(gfm(escape_once(@commit.description))) + = preserve(markdown(escape_once(@commit.description), pipeline: :single_line)) :coffeescript $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}") diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index cddd5aa3a8..9e0b536bb4 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -32,7 +32,7 @@ - if commit.description? .commit-row-description.js-toggle-content %pre - = preserve(gfm(escape_once(commit.description))) + = preserve(markdown(escape_once(commit.description), pipeline: :single_line)) .commit-row-info = commit_author_link(commit, avatar: true, size: 24) diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 5cb814c9ea..3233c6884c 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -37,7 +37,7 @@ .gray-content-block.middle-block %h2.issue-title - = gfm escape_once(@issue.title) + = markdown escape_once(@issue.title), pipeline: :single_line %div - if @issue.description.present? .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''} diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index b4f62a7589..448230a377 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -1,6 +1,6 @@ .gray-content-block.middle-block %h2.issue-title - = gfm escape_once(@merge_request.title) + = markdown escape_once(@merge_request.title), pipeline: :single_line %div - if @merge_request.description.present? diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 0aad9bb3e8..8629129c0b 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -24,4 +24,4 @@ %i.fa.fa-check Accepting this merge request will close #{"issue".pluralize(@closes_issues.size)} = succeed '.' do - != gfm(issues_sentence(@closes_issues)) + != markdown issues_sentence(@closes_issues), pipeline: :gfm diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 4eeb0621e5..302410765f 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -31,7 +31,7 @@ %span All issues for this milestone are closed. You may close milestone now. %h3.issue-title - = gfm escape_once(@milestone.title) + = markdown escape_once(@milestone.title), pipeline: :single_line %div - if @milestone.description.present? .description diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml index f3526ad074..6ca919f7f8 100644 --- a/app/views/projects/repositories/_feed.html.haml +++ b/app/views/projects/repositories/_feed.html.haml @@ -12,7 +12,7 @@ = link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do %code= commit.short_id = image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: '' - = gfm escape_once(truncate(commit.title, length: 40)) + = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line %td %span.pull-right.cgray = time_ago_with_tooltip(commit.committed_date) diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index d5b0060dd5..773ebddba2 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -19,51 +19,15 @@ module Gitlab # context - Hash of context options passed to our HTML Pipeline # # Returns an HTML-safe String - def self.render(markdown, context = {}) - html = renderer.render(markdown) - html = gfm(html, context) + def self.render(text, context = {}) + pipeline = context[:pipeline] || :full - html.html_safe - end + html_pipeline = html_pipelines[pipeline] - # Convert a Markdown String into HTML without going through the HTML - # Pipeline. - # - # Note that because the pipeline is skipped, SanitizationFilter is as well. - # Do not output the result of this method to the user. - # - # markdown - Markdown String - # - # Returns a String - def self.render_without_gfm(markdown) - renderer.render(markdown) - end + transformers = get_context_transformers(pipeline) + context = transformers.reduce(context) { |context, transformer| transformer.call(context) } - # Perform post-processing on an HTML String - # - # This method is used to perform state-dependent changes to a String of - # HTML, such as removing references that the current user doesn't have - # permission to make (`RedactorFilter`). - # - # html - String to process - # options - Hash of options to customize output - # :pipeline - Symbol pipeline type - # :project - Project - # :user - User object - # - # Returns an HTML-safe String - def self.post_process(html, options) - context = { - project: options[:project], - current_user: options[:user] - } - doc = post_processor.to_document(html, context) - - if options[:pipeline] == :atom - doc.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML) - else - doc.to_html - end.html_safe + html_pipeline.to_html(text, context) end # Provide autoload paths for filters to prevent a circular dependency error @@ -75,6 +39,7 @@ module Gitlab autoload :ExternalLinkFilter, 'gitlab/markdown/external_link_filter' autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter' autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter' + autoload :MarkdownFilter, 'gitlab/markdown/markdown_filter' autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter' autoload :RedactorFilter, 'gitlab/markdown/redactor_filter' autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter' @@ -85,98 +50,38 @@ module Gitlab autoload :TaskListFilter, 'gitlab/markdown/task_list_filter' autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter' - # Public: Parse the provided HTML with GitLab-Flavored Markdown + # Perform post-processing on an HTML String # - # html - HTML String - # options - A Hash of options used to customize output (default: {}) - # :no_header_anchors - Disable header anchors in TableOfContentsFilter - # :path - Current path String - # :pipeline - Symbol pipeline type - # :project - Current Project object - # :project_wiki - Current ProjectWiki object - # :ref - Current ref String + # This method is used to perform state-dependent changes to a String of + # HTML, such as removing references that the current user doesn't have + # permission to make (`RedactorFilter`). + # + # html - String to process + # context - Hash of options to customize output + # :pipeline - Symbol pipeline type + # :project - Project + # :user - User object # # Returns an HTML-safe String - def self.gfm(html, options = {}) - return '' unless html.present? + def self.post_process(html, context) + doc = html_pipelines[:post_process].to_document(html, context) - @pipeline ||= HTML::Pipeline.new(filters) - - context = { - # SanitizationFilter - pipeline: options[:pipeline], - - # EmojiFilter - asset_host: Gitlab::Application.config.asset_host, - asset_root: Gitlab.config.gitlab.base_url, - - # ReferenceFilter - only_path: only_path_pipeline?(options[:pipeline]), - project: options[:project], - - # RelativeLinkFilter - project_wiki: options[:project_wiki], - ref: options[:ref], - requested_path: options[:path], - - # TableOfContentsFilter - no_header_anchors: options[:no_header_anchors] - } - - @pipeline.to_html(html, context).html_safe + if context[:xhtml] + doc.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML) + else + doc.to_html + end.html_safe end private - - # Check if a pipeline enables the `only_path` context option - # - # Returns Boolean - def self.only_path_pipeline?(pipeline) - case pipeline - when :atom, :email - false - else - true - end - end - - def self.redcarpet_options - # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use - @redcarpet_options ||= { - fenced_code_blocks: true, - footnotes: true, - lax_spacing: true, - no_intra_emphasis: true, - space_after_headers: true, - strikethrough: true, - superscript: true, - tables: true - }.freeze - end - - def self.renderer - @markdown ||= begin - renderer = Redcarpet::Render::HTML.new - Redcarpet::Markdown.new(renderer, redcarpet_options) - end - end - - def self.post_processor - @post_processor ||= HTML::Pipeline.new([Gitlab::Markdown::RedactorFilter]) - end - - # Filters used in our pipeline - # - # SanitizationFilter should come first so that all generated reference HTML - # goes through untouched. - # - # See https://github.com/jch/html-pipeline#filters for more filters. - def self.filters - [ + FILTERS = { + plain_markdown: [ + Gitlab::Markdown::MarkdownFilter + ], + gfm: [ Gitlab::Markdown::SyntaxHighlightFilter, Gitlab::Markdown::SanitizationFilter, - Gitlab::Markdown::RelativeLinkFilter, Gitlab::Markdown::EmojiFilter, Gitlab::Markdown::TableOfContentsFilter, Gitlab::Markdown::AutolinkFilter, @@ -192,7 +97,90 @@ module Gitlab Gitlab::Markdown::LabelReferenceFilter, Gitlab::Markdown::TaskListFilter + ], + + full: [:plain_markdown, :gfm], + atom: :full, + email: :full, + description: :full, + single_line: :gfm, + + post_process: [ + Gitlab::Markdown::RelativeLinkFilter, + Gitlab::Markdown::RedactorFilter ] + } + + CONTEXT_TRANSFORMERS = { + gfm: { + only_path: true, + + # EmojiFilter + asset_host: Gitlab::Application.config.asset_host, + asset_root: Gitlab.config.gitlab.base_url + }, + full: :gfm, + + atom: [ + :full, + { + only_path: false, + xhtml: true + } + ], + email: [ + :full, + { + only_path: false + } + ], + description: [ + :full, + { + # SanitizationFilter + inline_sanitization: true + } + ], + single_line: :gfm, + + post_process: { + post_process: true + } + } + + def self.html_pipelines + @html_pipelines ||= Hash.new do |hash, pipeline| + filters = get_filters(pipeline) + HTML::Pipeline.new(filters) + end + end + + def self.get_filters(pipelines) + Array.wrap(pipelines).flat_map do |pipeline| + case pipeline + when Class + pipeline + when Symbol + get_filters(FILTERS[pipeline]) + when Array + get_filters(pipeline) + end + end.compact + end + + def self.get_context_transformers(pipelines) + Array.wrap(pipelines).flat_map do |pipeline| + case pipeline + when Hash + ->(context) { context.merge(pipeline) } + when Proc + pipeline + when Symbol + get_context_transformers(CONTEXT_TRANSFORMERS[pipeline]) + when Array + get_context_transformers(pipeline) + end + end.compact end end end diff --git a/lib/gitlab/markdown/markdown_filter.rb b/lib/gitlab/markdown/markdown_filter.rb new file mode 100644 index 0000000000..921e2a0794 --- /dev/null +++ b/lib/gitlab/markdown/markdown_filter.rb @@ -0,0 +1,39 @@ +module Gitlab + module Markdown + class MarkdownFilter < HTML::Pipeline::TextFilter + def initialize(text, context = nil, result = nil) + super text, context, result + @text = @text.gsub "\r", '' + end + + def call + html = self.class.renderer.render(@text) + html.rstrip! + html + end + + private + + def self.redcarpet_options + # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use + @redcarpet_options ||= { + fenced_code_blocks: true, + footnotes: true, + lax_spacing: true, + no_intra_emphasis: true, + space_after_headers: true, + strikethrough: true, + superscript: true, + tables: true + }.freeze + end + + def self.renderer + @renderer ||= begin + renderer = Redcarpet::Render::HTML.new + Redcarpet::Markdown.new(renderer, redcarpet_options) + end + end + end + end +end diff --git a/lib/gitlab/markdown/relative_link_filter.rb b/lib/gitlab/markdown/relative_link_filter.rb index 6ee3d1ce03..3e9909d2f3 100644 --- a/lib/gitlab/markdown/relative_link_filter.rb +++ b/lib/gitlab/markdown/relative_link_filter.rb @@ -16,7 +16,7 @@ module Gitlab def call return doc unless linkable_files? - doc.search('a').each do |el| + doc.search('a:not(.gfm)').each do |el| process_link_attr el.attribute('href') end diff --git a/lib/gitlab/markdown/sanitization_filter.rb b/lib/gitlab/markdown/sanitization_filter.rb index e368de7d84..550dfafca8 100644 --- a/lib/gitlab/markdown/sanitization_filter.rb +++ b/lib/gitlab/markdown/sanitization_filter.rb @@ -11,7 +11,7 @@ module Gitlab def whitelist # Descriptions are more heavily sanitized, allowing only a few elements. # See http://git.io/vkuAN - if pipeline == :description + if context[:inline_sanitization] whitelist = LIMITED whitelist[:elements] -= %w(pre code img ol ul li) else @@ -25,10 +25,6 @@ module Gitlab private - def pipeline - context[:pipeline] || :default - end - def customized?(transformers) transformers.last.source_location[0] == __FILE__ end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 66016ecc87..633c988d02 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -13,7 +13,8 @@ module Gitlab def analyze(text) references.clear - @text = Gitlab::Markdown.render_without_gfm(text) + + @document = Gitlab::Markdown.render(text, project: project) end %i(user label issue merge_request snippet commit commit_range).each do |type| @@ -44,20 +45,13 @@ module Gitlab filter = Gitlab::Markdown.const_get(klass) context = { - project: project, - current_user: current_user, - - # We don't actually care about the links generated - only_path: true, - ignore_blockquotes: true, - - # ReferenceGathererFilter + project: project, + current_user: current_user, load_lazy_references: false, reference_filter: filter } - pipeline = HTML::Pipeline.new([filter, Gitlab::Markdown::ReferenceGathererFilter], context) - result = pipeline.call(@text) + result = self.class.pipeline.call(@document, context) values = result[:references][filter_type].uniq @@ -67,5 +61,9 @@ module Gitlab values end + + def self.pipeline + @pipeline ||= HTML::Pipeline.new([Gitlab::Markdown::ReferenceGathererFilter]) + end end end From da9746e59a46005a0948d34bfd94ae715143c3f7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 14 Oct 2015 19:28:46 +0200 Subject: [PATCH 017/908] Enable caching of Gitlab::Markdown rendered result --- lib/gitlab/markdown.rb | 35 ++++++++++++++++++++++--------- lib/gitlab/reference_extractor.rb | 4 ++-- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 773ebddba2..21416f0fa0 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -20,14 +20,16 @@ module Gitlab # # Returns an HTML-safe String def self.render(text, context = {}) - pipeline = context[:pipeline] || :full + cache_key = context.delete(:cache_key) - html_pipeline = html_pipelines[pipeline] - - transformers = get_context_transformers(pipeline) - context = transformers.reduce(context) { |context, transformer| transformer.call(context) } - - html_pipeline.to_html(text, context) + if cache_key + cache_key = full_cache_key(cache_key, context[:pipeline]) + Rails.cache.fetch(cache_key) do + cacheless_render(text, context) + end + else + cacheless_render(text, context) + end end # Provide autoload paths for filters to prevent a circular dependency error @@ -130,9 +132,7 @@ module Gitlab ], email: [ :full, - { - only_path: false - } + { only_path: false } ], description: [ :full, @@ -155,6 +155,17 @@ module Gitlab end end + def self.cacheless_render(text, context = {}) + pipeline = context[:pipeline] || :full + + html_pipeline = html_pipelines[pipeline] + + transformers = get_context_transformers(pipeline) + context = transformers.reduce(context) { |context, transformer| transformer.call(context) } + + html_pipeline.to_html(text, context) + end + def self.get_filters(pipelines) Array.wrap(pipelines).flat_map do |pipeline| case pipeline @@ -182,5 +193,9 @@ module Gitlab end end.compact end + + def self.full_cache_key(cache_key, pipeline = :full) + ["markdown", *cache_key, pipeline] + end end end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 633c988d02..2262e5a168 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -11,10 +11,10 @@ module Gitlab @load_lazy_references = load_lazy_references end - def analyze(text) + def analyze(text, cache_key: nil) references.clear - @document = Gitlab::Markdown.render(text, project: project) + @document = Gitlab::Markdown.render(text, project: project, cache_key: cache_key) end %i(user label issue merge_request snippet commit commit_range).each do |type| From b093cb35d1614a48e980c5ccfca3a8d307d732f7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 14 Oct 2015 20:18:49 +0200 Subject: [PATCH 018/908] Use Gitlab::Markdown for Asciidoc and ReferenceExtractor pipelines --- app/helpers/gitlab_markdown_helper.rb | 15 +- lib/gitlab/asciidoc.rb | 27 +-- lib/gitlab/markdown.rb | 226 ++++++++++++++++---------- lib/gitlab/reference_extractor.rb | 18 +- spec/lib/gitlab/asciidoc_spec.rb | 6 +- 5 files changed, 166 insertions(+), 126 deletions(-) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index c6fff1b8ec..2f933f4d69 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -65,13 +65,16 @@ module GitlabMarkdownHelper end def asciidoc(text) - Gitlab::Asciidoc.render(text, { - commit: @commit, - project: @project, - project_wiki: @project_wiki, + Gitlab::Asciidoc.render(text, + project: @project, + current_user: (current_user if defined?(current_user)), + + # RelativeLinkFilter + project_wiki: @project_wiki, requested_path: @path, - ref: @ref - }) + ref: @ref, + commit: @commit + ) end # Return the first line of +text+, up to +max_chars+, after parsing the line diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index bf33e5b1b1..330d3342dd 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -1,14 +1,10 @@ require 'asciidoctor' -require 'html/pipeline' module Gitlab # Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters # the resulting HTML through HTML pipeline filters. module Asciidoc - # Provide autoload paths for filters to prevent a circular dependency error - autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter' - DEFAULT_ADOC_ATTRS = [ 'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab', 'env-gitlab', 'source-highlighter=html-pipeline' @@ -24,13 +20,11 @@ module Gitlab # :requested_path # :ref # asciidoc_opts - a Hash of options to pass to the Asciidoctor converter - # html_opts - a Hash of options for HTML output: - # :xhtml - output XHTML instead of HTML # - def self.render(input, context, asciidoc_opts = {}, html_opts = {}) - asciidoc_opts = asciidoc_opts.reverse_merge( + def self.render(input, context, asciidoc_opts = {}) + asciidoc_opts.reverse_merge!( safe: :secure, - backend: html_opts[:xhtml] ? :xhtml5 : :html5, + backend: :html5, attributes: [] ) asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS) @@ -38,23 +32,10 @@ module Gitlab html = ::Asciidoctor.convert(input, asciidoc_opts) if context[:project] - result = HTML::Pipeline.new(filters).call(html, context) - - save_opts = html_opts[:xhtml] ? - Nokogiri::XML::Node::SaveOptions::AS_XHTML : 0 - - html = result[:output].to_html(save_with: save_opts) + html = Gitlab::Markdown.render(html, context.merge(pipeline: :asciidoc)) end html.html_safe end - - private - - def self.filters - [ - Gitlab::Markdown::RelativeLinkFilter - ] - end end end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 21416f0fa0..dc6751fa1c 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -21,9 +21,9 @@ module Gitlab # Returns an HTML-safe String def self.render(text, context = {}) cache_key = context.delete(:cache_key) + cache_key = full_cache_key(cache_key, context[:pipeline]) if cache_key - cache_key = full_cache_key(cache_key, context[:pipeline]) Rails.cache.fetch(cache_key) do cacheless_render(text, context) end @@ -32,6 +32,47 @@ module Gitlab end end + def self.render_result(text, context = {}) + pipeline = context[:pipeline] || :full + + html_pipeline = html_pipelines[pipeline] + + transformers = context_transformers[pipeline] + context = transformers.reduce(context) { |context, transformer| transformer.call(context) } + + html_pipeline.call(text, context) + end + + def self.cached?(cache_key, pipeline: :full) + cache_key = full_cache_key(cache_key, pipeline) + cache_key ? Rails.cache.exist?(cache_key) : false + end + + # Perform post-processing on an HTML String + # + # This method is used to perform state-dependent changes to a String of + # HTML, such as removing references that the current user doesn't have + # permission to make (`RedactorFilter`). + # + # html - String to process + # context - Hash of options to customize output + # :pipeline - Symbol pipeline type + # :project - Project + # :user - User object + # + # Returns an HTML-safe String + def self.post_process(html, context) + html_pipeline = html_pipelines[:post_process] + + if context[:xhtml] + html_pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML) + else + html_pipeline.to_html(html, context) + end.html_safe + end + + private + # Provide autoload paths for filters to prevent a circular dependency error autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter' autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter' @@ -52,35 +93,8 @@ module Gitlab autoload :TaskListFilter, 'gitlab/markdown/task_list_filter' autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter' - # Perform post-processing on an HTML String - # - # This method is used to perform state-dependent changes to a String of - # HTML, such as removing references that the current user doesn't have - # permission to make (`RedactorFilter`). - # - # html - String to process - # context - Hash of options to customize output - # :pipeline - Symbol pipeline type - # :project - Project - # :user - User object - # - # Returns an HTML-safe String - def self.post_process(html, context) - doc = html_pipelines[:post_process].to_document(html, context) - - if context[:xhtml] - doc.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML) - else - doc.to_html - end.html_safe - end - - private - FILTERS = { - plain_markdown: [ - Gitlab::Markdown::MarkdownFilter - ], - gfm: [ + def self.gfm_filters + @gfm_filters ||= [ Gitlab::Markdown::SyntaxHighlightFilter, Gitlab::Markdown::SanitizationFilter, @@ -99,71 +113,99 @@ module Gitlab Gitlab::Markdown::LabelReferenceFilter, Gitlab::Markdown::TaskListFilter - ], - - full: [:plain_markdown, :gfm], - atom: :full, - email: :full, - description: :full, - single_line: :gfm, - - post_process: [ - Gitlab::Markdown::RelativeLinkFilter, - Gitlab::Markdown::RedactorFilter ] - } + end - CONTEXT_TRANSFORMERS = { - gfm: { - only_path: true, + def self.all_filters + @all_filters ||= { + plain_markdown: [ + Gitlab::Markdown::MarkdownFilter + ], + gfm: gfm_filters, - # EmojiFilter - asset_host: Gitlab::Application.config.asset_host, - asset_root: Gitlab.config.gitlab.base_url - }, - full: :gfm, + full: [:plain_markdown, :gfm], + atom: :full, + email: :full, + description: :full, + single_line: :gfm, - atom: [ - :full, - { - only_path: false, - xhtml: true - } - ], - email: [ - :full, - { only_path: false } - ], - description: [ - :full, - { - # SanitizationFilter - inline_sanitization: true - } - ], - single_line: :gfm, + asciidoc: [ + Gitlab::Markdown::RelativeLinkFilter + ], - post_process: { - post_process: true + post_process: [ + Gitlab::Markdown::RelativeLinkFilter, + Gitlab::Markdown::RedactorFilter + ], + + reference_extraction: [ + Gitlab::Markdown::ReferenceGathererFilter + ] } - } + end + + def self.all_context_transformers + @all_context_transformers ||= { + gfm: { + only_path: true, + + # EmojiFilter + asset_host: Gitlab::Application.config.asset_host, + asset_root: Gitlab.config.gitlab.base_url + }, + full: :gfm, + + atom: [ + :full, + { + only_path: false, + xhtml: true + } + ], + email: [ + :full, + { + only_path: false + } + ], + description: [ + :full, + { + # SanitizationFilter + inline_sanitization: true + } + ], + single_line: :gfm, + + post_process: { + post_process: true + } + } + end + + def self.html_filters + @html_filters ||= Hash.new do |hash, pipeline| + filters = get_filters(pipeline) + hash[pipeline] = filters if pipeline.is_a?(Symbol) + filters + end + end def self.html_pipelines @html_pipelines ||= Hash.new do |hash, pipeline| filters = get_filters(pipeline) - HTML::Pipeline.new(filters) + html_pipeline = HTML::Pipeline.new(filters) + hash[pipeline] = html_pipeline if pipeline.is_a?(Symbol) + html_pipeline end end - def self.cacheless_render(text, context = {}) - pipeline = context[:pipeline] || :full - - html_pipeline = html_pipelines[pipeline] - - transformers = get_context_transformers(pipeline) - context = transformers.reduce(context) { |context, transformer| transformer.call(context) } - - html_pipeline.to_html(text, context) + def self.context_transformers + @context_transformers ||= Hash.new do |hash, pipeline| + transformers = get_context_transformers(pipeline) + hash[pipeline] = transformers if pipeline.is_a?(Symbol) + transformers + end end def self.get_filters(pipelines) @@ -172,9 +214,9 @@ module Gitlab when Class pipeline when Symbol - get_filters(FILTERS[pipeline]) + html_filters[all_filters[pipeline]] when Array - get_filters(pipeline) + html_filters[pipeline] end end.compact end @@ -187,14 +229,26 @@ module Gitlab when Proc pipeline when Symbol - get_context_transformers(CONTEXT_TRANSFORMERS[pipeline]) + context_transformers[all_context_transformers[pipeline]] when Array - get_context_transformers(pipeline) + context_transformers[pipeline] end end.compact end + def self.cacheless_render(text, context = {}) + result = render_result(text, context) + output = result[:output] + if output.respond_to?(:to_html) + output.to_html + else + output.to_s + end + end + def self.full_cache_key(cache_key, pipeline = :full) + return unless cache_key && pipeline.is_a?(Symbol) + ["markdown", *cache_key, pipeline] end end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 2262e5a168..f34bf7d1d0 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -14,7 +14,8 @@ module Gitlab def analyze(text, cache_key: nil) references.clear - @document = Gitlab::Markdown.render(text, project: project, cache_key: cache_key) + @pipeline = Gitlab::Markdown.cached?(cache_key, pipeline: :full) ? :full : :plain_markdown + @html = Gitlab::Markdown.render(text, project: project, cache_key: cache_key, pipeline: @pipeline) end %i(user label issue merge_request snippet commit commit_range).each do |type| @@ -45,14 +46,19 @@ module Gitlab filter = Gitlab::Markdown.const_get(klass) context = { - project: project, - current_user: current_user, + pipeline: [:reference_extraction], + + project: project, + current_user: current_user, + + # ReferenceGathererFilter load_lazy_references: false, reference_filter: filter } - result = self.class.pipeline.call(@document, context) + context[:pipeline].unshift(filter) unless @pipeline == :full + result = Gitlab::Markdown.render_result(@html, context) values = result[:references][filter_type].uniq if @load_lazy_references @@ -61,9 +67,5 @@ module Gitlab values end - - def self.pipeline - @pipeline ||= HTML::Pipeline.new([Gitlab::Markdown::ReferenceGathererFilter]) - end end end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 03e36fd355..e8c8180e1c 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -50,9 +50,9 @@ module Gitlab filtered_html = 'ASCII' allow(Asciidoctor).to receive(:convert).and_return(html) - expect_any_instance_of(HTML::Pipeline).to receive(:call) - .with(html, context) - .and_return(output: Nokogiri::HTML.fragment(filtered_html)) + expect(Gitlab::Markdown).to receive(:render) + .with(html, context.merge(pipeline: :asciidoc)) + .and_return(filtered_html) expect( render('foo', context) ).to eql filtered_html end From 925dc5925b672718ea1f0f08c5bdd492b5ab3e5d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 14 Oct 2015 21:29:35 +0200 Subject: [PATCH 019/908] Cache rendered contents of issues, MRs and notes --- app/models/concerns/issuable.rb | 3 +- app/models/concerns/mentionable.rb | 39 ++++++++++++------- app/models/note.rb | 2 +- app/views/projects/issues/show.html.haml | 2 +- .../merge_requests/show/_mr_box.html.haml | 2 +- app/views/projects/notes/_note.html.haml | 2 +- lib/gitlab/markdown.rb | 12 +++++- lib/gitlab/reference_extractor.rb | 20 +++++----- 8 files changed, 51 insertions(+), 31 deletions(-) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index feee8460b8..e260eae8a4 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -46,7 +46,8 @@ module Issuable allow_nil: true, prefix: true - attr_mentionable :title, :description + attr_mentionable :title + attr_mentionable :description, cache: true participant :author, :assignee, :notes end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 715fc6f689..ad432144bd 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -10,8 +10,9 @@ module Mentionable module ClassMethods # Indicate which attributes of the Mentionable to search for GFM references. - def attr_mentionable(*attrs) - mentionable_attrs.concat(attrs.map(&:to_s)) + def attr_mentionable(attr, options = {}) + attr = attr.to_s + mentionable_attrs << [attr, options] end # Accessor for attributes marked mentionable. @@ -37,11 +38,6 @@ module Mentionable "#{friendly_name} #{to_reference(from_project)}" end - # Construct a String that contains possible GFM references. - def mentionable_text - self.class.mentionable_attrs.map { |attr| send(attr) }.compact.join("\n\n") - end - # The GFM reference to this Mentionable, which shouldn't be included in its #references. def local_reference self @@ -54,20 +50,33 @@ module Mentionable end def mentioned_users(current_user = nil, load_lazy_references: true) - return [] if mentionable_text.blank? - + # TODO: Douwe: Will be simplified when the "Simplify ..." MR is merged. ext = Gitlab::ReferenceExtractor.new(self.project, current_user, load_lazy_references: load_lazy_references) - ext.analyze(mentionable_text) - ext.users.uniq + self.class.mentionable_attrs.each do |attr, options| + text = send(attr) + cache_key = [self, attr] if options[:cache] + ext.analyze(text, cache_key: cache_key, pipeline: options[:pipeline]) + end + ext.users end # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. - def references(p = project, current_user = self.author, text = mentionable_text, load_lazy_references: true) + def references(p = project, current_user = self.author, text = nil, load_lazy_references: true) return [] if text.blank? ext = Gitlab::ReferenceExtractor.new(p, current_user, load_lazy_references: load_lazy_references) - ext.analyze(text) - (ext.issues + ext.merge_requests + ext.commits).uniq - [local_reference] + + if text + ext.analyze(text) + else + self.class.mentionable_attrs.each do |attr, options| + text = send(attr) + cache_key = [self, attr] if options[:cache] + ext.analyze(text, cache_key: cache_key) + end + end + + (ext.issues + ext.merge_requests + ext.commits) - [local_reference] end # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. @@ -111,7 +120,7 @@ module Mentionable def detect_mentionable_changes source = (changes.present? ? changes : previous_changes).dup - mentionable = self.class.mentionable_attrs + mentionable = self.class.mentionable_attrs.map { |attr, options| attr } # Only include changed fields that are mentionable source.select { |key, val| mentionable.include?(key) } diff --git a/app/models/note.rb b/app/models/note.rb index 2fbe478415..b4d6ddba70 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -28,7 +28,7 @@ class Note < ActiveRecord::Base default_value_for :system, false - attr_mentionable :note + attr_mentionable :note, cache: true, pipeline: :note participant :author belongs_to :project diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 3233c6884c..e27f20bdfb 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -43,7 +43,7 @@ .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''} .wiki = preserve do - = markdown(@issue.description) + = markdown(@issue.description, cache_key: [@issue, "description"]) %textarea.hidden.js-task-list-field = @issue.description diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index 448230a377..9bfe202589 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -7,6 +7,6 @@ .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''} .wiki = preserve do - = markdown(@merge_request.description) + = markdown(@merge_request.description, cache_key: [@merge_request, "description"]) %textarea.hidden.js-task-list-field = @merge_request.description diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 1638ad6891..bcb42d39c9 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -58,7 +58,7 @@ .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-text = preserve do - = markdown(note.note, {no_header_anchors: true}) + = markdown(note.note, pipeline: :note, cache_key: [note, "note"]) - unless note.system? -# System notes can't be edited = render 'projects/notes/edit_form', note: note diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index d1f22070cb..b9615f9501 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -20,6 +20,8 @@ module Gitlab # # Returns an HTML-safe String def self.render(text, context = {}) + context[:pipeline] ||= :full + cache_key = context.delete(:cache_key) cache_key = full_cache_key(cache_key, context[:pipeline]) @@ -33,7 +35,7 @@ module Gitlab end def self.render_result(text, context = {}) - pipeline = context[:pipeline] || :full + pipeline = context[:pipeline] ||= :full html_pipeline = html_pipelines[pipeline] @@ -129,6 +131,7 @@ module Gitlab atom: :full, email: :full, description: :full, + note: :full, single_line: :gfm, asciidoc: [ @@ -170,6 +173,13 @@ module Gitlab only_path: false } ], + note: [ + :full, + { + # TableOfContentsFilter + no_header_anchors: true + } + ], description: [ :full, { diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index f34bf7d1d0..37141efb4c 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -9,13 +9,12 @@ module Gitlab @project = project @current_user = current_user @load_lazy_references = load_lazy_references + + @texts = [] end - def analyze(text, cache_key: nil) - references.clear - - @pipeline = Gitlab::Markdown.cached?(cache_key, pipeline: :full) ? :full : :plain_markdown - @html = Gitlab::Markdown.render(text, project: project, cache_key: cache_key, pipeline: @pipeline) + def analyze(text, options = {}) + @texts << Gitlab::Markdown.render(text, options.merge(project: project)) end %i(user label issue merge_request snippet commit commit_range).each do |type| @@ -46,7 +45,7 @@ module Gitlab filter = Gitlab::Markdown.const_get(klass) context = { - pipeline: [:reference_extraction], + pipeline: :reference_extraction, project: project, current_user: current_user, @@ -56,10 +55,11 @@ module Gitlab reference_filter: filter } - context[:pipeline].unshift(filter) unless @pipeline == :full - - result = Gitlab::Markdown.render_result(@html, context) - values = result[:references][filter_type].uniq + values = @texts.flat_map do |html| + text_context = context.dup + result = Gitlab::Markdown.render_result(html, text_context) + result[:references][filter_type] + end.uniq if @load_lazy_references values = Gitlab::Markdown::ReferenceFilter::LazyReference.load(values).uniq From 528d2823e9888ae09aa03b72c2e61cd0b9a39937 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 14 Oct 2015 21:52:41 +0200 Subject: [PATCH 020/908] Remove unused Gitlab::Markdown#cached? method --- lib/gitlab/markdown.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index b9615f9501..5c4481e649 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -45,11 +45,6 @@ module Gitlab html_pipeline.call(text, context) end - def self.cached?(cache_key, pipeline: :full) - cache_key = full_cache_key(cache_key, pipeline) - cache_key ? Rails.cache.exist?(cache_key) : false - end - # Perform post-processing on an HTML String # # This method is used to perform state-dependent changes to a String of From 9c2214f202e98d0427d86a57888574327a6607dd Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 15 Oct 2015 13:41:03 +0200 Subject: [PATCH 021/908] Remove stray conflict marker --- app/models/concerns/participable.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index f5f973c0e6..85367f89f4 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -37,7 +37,6 @@ module Participable # Be aware that this method makes a lot of sql queries. # Save result into variable if you are going to reuse it inside same request -<<<<<<< HEAD def participants(current_user = self.author, load_lazy_references: true) participants = self.class.participant_attrs.flat_map do |attr| value = From 3f08e4b186bd02b37f34ccf1bc641a95f9d865ce Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Thu, 1 Oct 2015 20:34:23 +0200 Subject: [PATCH 022/908] Add specs on #license --- CHANGELOG | 2 +- app/models/repository.rb | 6 +++++- spec/models/repository_spec.rb | 11 +++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3d0beb0b7a..f274942f7b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -62,6 +62,7 @@ v 8.0.4 - Fix "Assign All" button on Runner admin page - Fix search in Files - Add full project namespace to payload of system webhooks (Ricardo Band) + - Accept COPYING as licence file (Zeger-Jan van de Weg) v 8.0.3 - Fix URL shown in Slack notifications @@ -82,7 +83,6 @@ v 8.0.2 - Fix Reply by email for non-UTF-8 messages. - Add option to use StartTLS with Reply by email IMAP server. - Allow AWS S3 Server-Side Encryption with Amazon S3-Managed Keys for backups (Paul Beattie) - - Accept COPYING as licence file (Zeger-Jan van de Weg) v 8.0.1 - Remove git refs used internally by GitLab from network graph (Stan Hu) diff --git a/app/models/repository.rb b/app/models/repository.rb index dc7cd92674..8cd182e1b0 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -216,7 +216,11 @@ class Repository # If `licence`, `copying` and `copying.lesser` are found, return in the # following order: licence, copying, copying.lesser - licenses.find { |l| l =~ /\Alicence/i } || licenses.sort.first + regex = [/\Alicense(\..*)?\z/i, /\Acopying(\..{0,3})?\z/i, /\Acopying.lesser/i] + + regex.map do |re| + licenses.find { |l| l.name =~ re } + end.compact.first end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 05e51532eb..60b9398847 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -91,4 +91,15 @@ describe Repository do it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") } end end + + describe "#license" do + it 'test selection preference' do + repository.send(:cache).expire(:license) + TestBlob = Struct.new(:name) + files = [TestBlob.new('file'), TestBlob.new('license'), TestBlob.new('copying')] + expect(repository.tree).to receive(:blobs).and_return(files) + + expect(repository.license.name).to eq('license') + end + end end From 7d7b6dbb9f2a8c6dbee8ab6eb0ab075705abcfbd Mon Sep 17 00:00:00 2001 From: Jan-Gerd Tenberge Date: Mon, 19 Oct 2015 01:21:21 +0200 Subject: [PATCH 023/908] Update spec --- spec/helpers/application_helper_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 742420f550..b5a29346dc 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -95,9 +95,9 @@ describe ApplicationHelper do end it 'should call gravatar_icon when no User exists with the given email' do - expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20) + expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2) - helper.avatar_icon('foo@example.com', 20) + helper.avatar_icon('foo@example.com', 20, 2) end end From 34d0cd438a7173c35c687ecb37eae1f3293b84b5 Mon Sep 17 00:00:00 2001 From: Jan-Gerd Tenberge Date: Mon, 19 Oct 2015 01:22:20 +0200 Subject: [PATCH 024/908] Update spec --- spec/helpers/application_helper_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index e6e9bc99a1..bed6658ce0 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -95,7 +95,7 @@ describe ApplicationHelper do end it 'should call gravatar_icon when no User exists with the given email' do - expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20) + expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2) helper.avatar_icon('foo@example.com', 20) end From abbca6151d66ebc227c883af35cd01cf4d7e24eb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 21 Oct 2015 12:18:23 +0200 Subject: [PATCH 025/908] Make pipelines actually make sense --- lib/gitlab/markdown.rb | 216 +++--------------- lib/gitlab/markdown/asciidoc_pipeline.rb | 13 ++ lib/gitlab/markdown/atom_pipeline.rb | 14 ++ lib/gitlab/markdown/combined_pipeline.rb | 25 ++ lib/gitlab/markdown/description_pipeline.rb | 14 ++ lib/gitlab/markdown/email_pipeline.rb | 13 ++ lib/gitlab/markdown/full_pipeline.rb | 9 + lib/gitlab/markdown/gfm_pipeline.rb | 41 ++++ lib/gitlab/markdown/note_pipeline.rb | 14 ++ lib/gitlab/markdown/pipeline.rb | 29 +++ .../markdown/plain_markdown_pipeline.rb | 13 ++ lib/gitlab/markdown/post_process_pipeline.rb | 20 ++ .../markdown/reference_extraction_pipeline.rb | 13 ++ lib/gitlab/markdown/single_line_pipeline.rb | 9 + 14 files changed, 265 insertions(+), 178 deletions(-) create mode 100644 lib/gitlab/markdown/asciidoc_pipeline.rb create mode 100644 lib/gitlab/markdown/atom_pipeline.rb create mode 100644 lib/gitlab/markdown/combined_pipeline.rb create mode 100644 lib/gitlab/markdown/description_pipeline.rb create mode 100644 lib/gitlab/markdown/email_pipeline.rb create mode 100644 lib/gitlab/markdown/full_pipeline.rb create mode 100644 lib/gitlab/markdown/gfm_pipeline.rb create mode 100644 lib/gitlab/markdown/note_pipeline.rb create mode 100644 lib/gitlab/markdown/pipeline.rb create mode 100644 lib/gitlab/markdown/plain_markdown_pipeline.rb create mode 100644 lib/gitlab/markdown/post_process_pipeline.rb create mode 100644 lib/gitlab/markdown/reference_extraction_pipeline.rb create mode 100644 lib/gitlab/markdown/single_line_pipeline.rb diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 5c4481e649..3ae4ccfacb 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -35,14 +35,8 @@ module Gitlab end def self.render_result(text, context = {}) - pipeline = context[:pipeline] ||= :full - - html_pipeline = html_pipelines[pipeline] - - transformers = context_transformers[pipeline] - context = transformers.reduce(context) { |context, transformer| transformer.call(context) } - - html_pipeline.call(text, context) + pipeline_type = context[:pipeline] ||= :full + pipeline_by_type(pipeline_type).call(text, context) end # Perform post-processing on an HTML String @@ -59,17 +53,37 @@ module Gitlab # # Returns an HTML-safe String def self.post_process(html, context) - html_pipeline = html_pipelines[:post_process] + pipeline = pipeline_by_type(:post_process) if context[:xhtml] - html_pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML) + pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML) else - html_pipeline.to_html(html, context) + pipeline.to_html(html, context) end.html_safe end private + def self.cacheless_render(text, context = {}) + result = render_result(text, context) + output = result[:output] + if output.respond_to?(:to_html) + output.to_html + else + output.to_s + end + end + + def self.full_cache_key(cache_key, pipeline = :full) + return unless cache_key + + ["markdown", *cache_key, pipeline] + end + + def self.pipeline_by_type(pipeline_type) + const_get("#{pipeline_type.to_s.camelize}Pipeline") + end + # Provide autoload paths for filters to prevent a circular dependency error autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter' autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter' @@ -91,172 +105,18 @@ module Gitlab autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter' autoload :UploadLinkFilter, 'gitlab/markdown/upload_link_filter' - def self.gfm_filters - @gfm_filters ||= [ - Gitlab::Markdown::SyntaxHighlightFilter, - Gitlab::Markdown::SanitizationFilter, - - Gitlab::Markdown::UploadLinkFilter, - Gitlab::Markdown::EmojiFilter, - Gitlab::Markdown::TableOfContentsFilter, - Gitlab::Markdown::AutolinkFilter, - Gitlab::Markdown::ExternalLinkFilter, - - Gitlab::Markdown::UserReferenceFilter, - Gitlab::Markdown::IssueReferenceFilter, - Gitlab::Markdown::ExternalIssueReferenceFilter, - Gitlab::Markdown::MergeRequestReferenceFilter, - Gitlab::Markdown::SnippetReferenceFilter, - Gitlab::Markdown::CommitRangeReferenceFilter, - Gitlab::Markdown::CommitReferenceFilter, - Gitlab::Markdown::LabelReferenceFilter, - - Gitlab::Markdown::TaskListFilter - ] - end - - def self.all_filters - @all_filters ||= { - plain_markdown: [ - Gitlab::Markdown::MarkdownFilter - ], - gfm: gfm_filters, - - full: [:plain_markdown, :gfm], - atom: :full, - email: :full, - description: :full, - note: :full, - single_line: :gfm, - - asciidoc: [ - Gitlab::Markdown::RelativeLinkFilter - ], - - post_process: [ - Gitlab::Markdown::RelativeLinkFilter, - Gitlab::Markdown::RedactorFilter - ], - - reference_extraction: [ - Gitlab::Markdown::ReferenceGathererFilter - ] - } - end - - def self.all_context_transformers - @all_context_transformers ||= { - gfm: { - only_path: true, - - # EmojiFilter - asset_host: Gitlab::Application.config.asset_host, - asset_root: Gitlab.config.gitlab.base_url - }, - full: :gfm, - - atom: [ - :full, - { - only_path: false, - xhtml: true - } - ], - email: [ - :full, - { - only_path: false - } - ], - note: [ - :full, - { - # TableOfContentsFilter - no_header_anchors: true - } - ], - description: [ - :full, - { - # SanitizationFilter - inline_sanitization: true - } - ], - single_line: :gfm, - - post_process: { - post_process: true - } - } - end - - def self.html_filters - @html_filters ||= Hash.new do |hash, pipeline| - filters = get_filters(pipeline) - hash[pipeline] = filters if pipeline.is_a?(Symbol) - filters - end - end - - def self.html_pipelines - @html_pipelines ||= Hash.new do |hash, pipeline| - filters = get_filters(pipeline) - html_pipeline = HTML::Pipeline.new(filters) - hash[pipeline] = html_pipeline if pipeline.is_a?(Symbol) - html_pipeline - end - end - - def self.context_transformers - @context_transformers ||= Hash.new do |hash, pipeline| - transformers = get_context_transformers(pipeline) - hash[pipeline] = transformers if pipeline.is_a?(Symbol) - transformers - end - end - - def self.get_filters(pipelines) - Array.wrap(pipelines).flat_map do |pipeline| - case pipeline - when Class - pipeline - when Symbol - html_filters[all_filters[pipeline]] - when Array - html_filters[pipeline] - end - end.compact - end - - def self.get_context_transformers(pipelines) - Array.wrap(pipelines).flat_map do |pipeline| - case pipeline - when Hash - ->(context) { context.merge(pipeline) } - when Proc - pipeline - when Symbol - context_transformers[all_context_transformers[pipeline]] - when Array - context_transformers[pipeline] - end - end.compact - end - - def self.cacheless_render(text, context = {}) - result = render_result(text, context) - output = result[:output] - if output.respond_to?(:to_html) - output.to_html - else - output.to_s - end - end - - def self.full_cache_key(cache_key, pipeline = :full) - return unless cache_key && pipeline.is_a?(Symbol) - - ["markdown", *cache_key, pipeline] - end + autoload :AsciidocPipeline, 'gitlab/markdown/asciidoc_pipeline' + autoload :AtomPipeline, 'gitlab/markdown/atom_pipeline' + autoload :CombinedPipeline, 'gitlab/markdown/combined_pipeline' + autoload :DescriptionPipeline, 'gitlab/markdown/description_pipeline' + autoload :EmailPipeline, 'gitlab/markdown/email_pipeline' + autoload :FullPipeline, 'gitlab/markdown/full_pipeline' + autoload :GfmPipeline, 'gitlab/markdown/gfm_pipeline' + autoload :NotePipeline, 'gitlab/markdown/note_pipeline' + autoload :Pipeline, 'gitlab/markdown/pipeline' + autoload :PlainMarkdownPipeline, 'gitlab/markdown/plain_markdown_pipeline' + autoload :PostProcessPipeline, 'gitlab/markdown/post_process_pipeline' + autoload :ReferenceExtractionPipeline, 'gitlab/markdown/reference_extraction_pipeline' + autoload :SingleLinePipeline, 'gitlab/markdown/single_line_pipeline' end end diff --git a/lib/gitlab/markdown/asciidoc_pipeline.rb b/lib/gitlab/markdown/asciidoc_pipeline.rb new file mode 100644 index 0000000000..6829b4acb9 --- /dev/null +++ b/lib/gitlab/markdown/asciidoc_pipeline.rb @@ -0,0 +1,13 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class AsciidocPipeline < Pipeline + def self.filters + [ + Gitlab::Markdown::RelativeLinkFilter + ] + end + end + end +end diff --git a/lib/gitlab/markdown/atom_pipeline.rb b/lib/gitlab/markdown/atom_pipeline.rb new file mode 100644 index 0000000000..e151f8f5e5 --- /dev/null +++ b/lib/gitlab/markdown/atom_pipeline.rb @@ -0,0 +1,14 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class AtomPipeline < FullPipeline + def self.transform_context(context) + super(context).merge( + only_path: false, + xhtml: true + ) + end + end + end +end diff --git a/lib/gitlab/markdown/combined_pipeline.rb b/lib/gitlab/markdown/combined_pipeline.rb new file mode 100644 index 0000000000..a3d717550f --- /dev/null +++ b/lib/gitlab/markdown/combined_pipeline.rb @@ -0,0 +1,25 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + module CombinedPipeline + def self.new(*pipelines) + Class.new(Pipeline) do + const_set :PIPELINES, pipelines + + def self.filters + pipelines.flat_map(&:filters) + end + + def self.transform_context(context) + pipelines.reduce(context) { |context, pipeline| pipeline.transform_context(context) } + end + + def self.pipelines + self::PIPELINES + end + end + end + end + end +end diff --git a/lib/gitlab/markdown/description_pipeline.rb b/lib/gitlab/markdown/description_pipeline.rb new file mode 100644 index 0000000000..76f6948af8 --- /dev/null +++ b/lib/gitlab/markdown/description_pipeline.rb @@ -0,0 +1,14 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class DescriptionPipeline < FullPipeline + def self.transform_context(context) + super(context).merge( + # SanitizationFilter + inline_sanitization: true + ) + end + end + end +end diff --git a/lib/gitlab/markdown/email_pipeline.rb b/lib/gitlab/markdown/email_pipeline.rb new file mode 100644 index 0000000000..b88cb79027 --- /dev/null +++ b/lib/gitlab/markdown/email_pipeline.rb @@ -0,0 +1,13 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class EmailPipeline < FullPipeline + def self.transform_context(context) + super(context).merge( + only_path: false + ) + end + end + end +end diff --git a/lib/gitlab/markdown/full_pipeline.rb b/lib/gitlab/markdown/full_pipeline.rb new file mode 100644 index 0000000000..553e9367c1 --- /dev/null +++ b/lib/gitlab/markdown/full_pipeline.rb @@ -0,0 +1,9 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class FullPipeline < CombinedPipeline.new(PlainMarkdownPipeline, GfmPipeline) + + end + end +end diff --git a/lib/gitlab/markdown/gfm_pipeline.rb b/lib/gitlab/markdown/gfm_pipeline.rb new file mode 100644 index 0000000000..ca90bd75d7 --- /dev/null +++ b/lib/gitlab/markdown/gfm_pipeline.rb @@ -0,0 +1,41 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class GfmPipeline < Pipeline + def self.filters + @filters ||= [ + Gitlab::Markdown::SyntaxHighlightFilter, + Gitlab::Markdown::SanitizationFilter, + + Gitlab::Markdown::UploadLinkFilter, + Gitlab::Markdown::EmojiFilter, + Gitlab::Markdown::TableOfContentsFilter, + Gitlab::Markdown::AutolinkFilter, + Gitlab::Markdown::ExternalLinkFilter, + + Gitlab::Markdown::UserReferenceFilter, + Gitlab::Markdown::IssueReferenceFilter, + Gitlab::Markdown::ExternalIssueReferenceFilter, + Gitlab::Markdown::MergeRequestReferenceFilter, + Gitlab::Markdown::SnippetReferenceFilter, + Gitlab::Markdown::CommitRangeReferenceFilter, + Gitlab::Markdown::CommitReferenceFilter, + Gitlab::Markdown::LabelReferenceFilter, + + Gitlab::Markdown::TaskListFilter + ] + end + + def self.transform_context(context) + context.merge( + only_path: true, + + # EmojiFilter + asset_host: Gitlab::Application.config.asset_host, + asset_root: Gitlab.config.gitlab.base_url + ) + end + end + end +end diff --git a/lib/gitlab/markdown/note_pipeline.rb b/lib/gitlab/markdown/note_pipeline.rb new file mode 100644 index 0000000000..a8bf5f42d8 --- /dev/null +++ b/lib/gitlab/markdown/note_pipeline.rb @@ -0,0 +1,14 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class NotePipeline < FullPipeline + def self.transform_context(context) + super(context).merge( + # TableOfContentsFilter + no_header_anchors: true + ) + end + end + end +end diff --git a/lib/gitlab/markdown/pipeline.rb b/lib/gitlab/markdown/pipeline.rb new file mode 100644 index 0000000000..3c0b676a07 --- /dev/null +++ b/lib/gitlab/markdown/pipeline.rb @@ -0,0 +1,29 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class Pipeline + def self.filters + [] + end + + def self.transform_context(context) + context + end + + def self.html_pipeline + @html_pipeline ||= HTML::Pipeline.new(filters) + end + + class << self + %i(call to_document to_html).each do |meth| + define_method(meth) do |text, context| + context = transform_context(context) + + html_pipeline.send(meth, text, context) + end + end + end + end + end +end diff --git a/lib/gitlab/markdown/plain_markdown_pipeline.rb b/lib/gitlab/markdown/plain_markdown_pipeline.rb new file mode 100644 index 0000000000..0abb93f8a0 --- /dev/null +++ b/lib/gitlab/markdown/plain_markdown_pipeline.rb @@ -0,0 +1,13 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class PlainMarkdownPipeline < Pipeline + def self.filters + [ + Gitlab::Markdown::MarkdownFilter + ] + end + end + end +end diff --git a/lib/gitlab/markdown/post_process_pipeline.rb b/lib/gitlab/markdown/post_process_pipeline.rb new file mode 100644 index 0000000000..60cc32f490 --- /dev/null +++ b/lib/gitlab/markdown/post_process_pipeline.rb @@ -0,0 +1,20 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class PostProcessPipeline < Pipeline + def self.filters + [ + Gitlab::Markdown::RelativeLinkFilter, + Gitlab::Markdown::RedactorFilter + ] + end + + def self.transform_context(context) + context.merge( + post_process: true + ) + end + end + end +end diff --git a/lib/gitlab/markdown/reference_extraction_pipeline.rb b/lib/gitlab/markdown/reference_extraction_pipeline.rb new file mode 100644 index 0000000000..a89ab462ba --- /dev/null +++ b/lib/gitlab/markdown/reference_extraction_pipeline.rb @@ -0,0 +1,13 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class ReferenceExtractionPipeline < Pipeline + def self.filters + [ + Gitlab::Markdown::ReferenceGathererFilter + ] + end + end + end +end diff --git a/lib/gitlab/markdown/single_line_pipeline.rb b/lib/gitlab/markdown/single_line_pipeline.rb new file mode 100644 index 0000000000..2f24927b87 --- /dev/null +++ b/lib/gitlab/markdown/single_line_pipeline.rb @@ -0,0 +1,9 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + class SingleLinePipeline < GfmPipeline + + end + end +end From f65840bc842b86cd71e70af8b9acdb1a786d9054 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 22 Oct 2015 11:00:34 +0200 Subject: [PATCH 026/908] Fix Markdown XHTML context param --- lib/gitlab/markdown.rb | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 3ae4ccfacb..5f1a6c8cee 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -20,8 +20,6 @@ module Gitlab # # Returns an HTML-safe String def self.render(text, context = {}) - context[:pipeline] ||= :full - cache_key = context.delete(:cache_key) cache_key = full_cache_key(cache_key, context[:pipeline]) @@ -35,8 +33,7 @@ module Gitlab end def self.render_result(text, context = {}) - pipeline_type = context[:pipeline] ||= :full - pipeline_by_type(pipeline_type).call(text, context) + pipeline_by_name(context[:pipeline]).call(text, context) end # Perform post-processing on an HTML String @@ -53,7 +50,10 @@ module Gitlab # # Returns an HTML-safe String def self.post_process(html, context) - pipeline = pipeline_by_type(:post_process) + pipeline = pipeline_by_name(context[:pipeline]) + context = pipeline.transform_context(context) + + pipeline = pipeline_by_name(:post_process) if context[:xhtml] pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML) @@ -74,14 +74,15 @@ module Gitlab end end - def self.full_cache_key(cache_key, pipeline = :full) + def self.full_cache_key(cache_key, pipeline_name) return unless cache_key - + pipeline_name ||= :full ["markdown", *cache_key, pipeline] end - def self.pipeline_by_type(pipeline_type) - const_get("#{pipeline_type.to_s.camelize}Pipeline") + def self.pipeline_by_name(pipeline_name) + pipeline_name ||= :full + const_get("#{pipeline_name.to_s.camelize}Pipeline") end # Provide autoload paths for filters to prevent a circular dependency error From adba37f99aa6351ce7ca29055d5aab284058bc2e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 22 Oct 2015 11:02:21 +0200 Subject: [PATCH 027/908] Use markdown with gfm pipeline rather than gfm method --- app/views/projects/commits/show.atom.builder | 2 +- app/views/projects/issues/_closed_by_box.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index 3854ad5d61..7aab98e07c 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -17,7 +17,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.name commit.author_name xml.email commit.author_email end - xml.summary gfm(commit.description) + xml.summary markdown(commit.description, pipeline: :atom) end end end diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml index aef352029d..eebe36bc44 100644 --- a/app/views/projects/issues/_closed_by_box.html.haml +++ b/app/views/projects/issues/_closed_by_box.html.haml @@ -1,3 +1,3 @@ .issue-closed-by-widget = icon('check') - This issue will be closed automatically when merge request #{gfm(merge_requests_sentence(@closed_by_merge_requests.sort))} is accepted. + This issue will be closed automatically when merge request #{markdown(merge_requests_sentence(@closed_by_merge_requests.sort), pipeline: :gfm)} is accepted. From c1ecfb5de960a675888640a05c75c07217c2d293 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 22 Oct 2015 15:38:11 +0200 Subject: [PATCH 028/908] Disabling caching in test environment because it was causing issues with Markdown --- config/environments/test.rb | 2 ++ config/initializers/session_store.rb | 22 +++++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/config/environments/test.rb b/config/environments/test.rb index 2d5e7addcd..e03f54c553 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -7,6 +7,8 @@ Gitlab::Application.configure do # and recreated between test runs. Don't rely on the data there! config.cache_classes = false + config.cache_store = :null_store + # Configure static asset server for tests with Cache-Control for performance config.serve_static_assets = true config.static_cache_control = "public, max-age=3600" diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 04ed9e90df..6be21a771e 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -9,12 +9,16 @@ begin rescue end -Gitlab::Application.config.session_store( - :redis_store, # Using the cookie_store would enable session replay attacks. - servers: Gitlab::Application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store - key: '_gitlab_session', - secure: Gitlab.config.gitlab.https, - httponly: true, - expire_after: Settings.gitlab['session_expire_delay'] * 60, - path: (Gitlab::Application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root -) +if Rails.env.test? + Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session" +else + Gitlab::Application.config.session_store( + :redis_store, # Using the cookie_store would enable session replay attacks. + servers: Gitlab::Application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store + key: '_gitlab_session', + secure: Gitlab.config.gitlab.https, + httponly: true, + expire_after: Settings.gitlab['session_expire_delay'] * 60, + path: (Gitlab::Application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root + ) +end From c746a1de4ef75034c75d20e763ccfa1f73750722 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 22 Oct 2015 15:40:02 +0200 Subject: [PATCH 029/908] Use faster, more appropriate pipeline for mentionable attributes --- app/models/commit.rb | 2 +- app/models/concerns/issuable.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index 492f6be1ce..a5f70b3a35 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -7,7 +7,7 @@ class Commit include Referable include StaticModel - attr_mentionable :safe_message + attr_mentionable :safe_message, pipeline: :single_line participant :author, :committer, :notes attr_accessor :project diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 54bfd4eced..1d7cecef97 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -46,7 +46,7 @@ module Issuable allow_nil: true, prefix: true - attr_mentionable :title + attr_mentionable :title, pipeline: :single_line attr_mentionable :description, cache: true participant :author, :assignee, :notes_with_associations end From 51ed8fd706802fe0fffe7a2db0b5adc1f2196e50 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 22 Oct 2015 15:40:21 +0200 Subject: [PATCH 030/908] Use correct var name --- lib/gitlab/markdown.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 5f1a6c8cee..969607558e 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -77,7 +77,7 @@ module Gitlab def self.full_cache_key(cache_key, pipeline_name) return unless cache_key pipeline_name ||= :full - ["markdown", *cache_key, pipeline] + ["markdown", *cache_key, pipeline_name] end def self.pipeline_by_name(pipeline_name) From aa4007a597ea6c7de11a8f37de2fd889aa6e6646 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 22 Oct 2015 15:40:36 +0200 Subject: [PATCH 031/908] Slight refactoring --- lib/gitlab/reference_extractor.rb | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 69c7a0f477..e5cafebdbc 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -11,6 +11,7 @@ module Gitlab @load_lazy_references = load_lazy_references @texts = [] + @references = {} end def analyze(text, options = {}) @@ -19,21 +20,12 @@ module Gitlab %i(user label issue merge_request snippet commit commit_range).each do |type| define_method("#{type}s") do - references[type] + @references[type] ||= pipeline_result(type) end end private - def references - @references ||= Hash.new do |references, type| - type = type.to_sym - next references[type] if references.has_key?(type) - - references[type] = pipeline_result(type) - end - end - # Instantiate and call HTML::Pipeline with a single reference filter type, # returning the result # From 46025ffb8c717eedced397edd19ce8f0d67802e7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 22 Oct 2015 15:40:42 +0200 Subject: [PATCH 032/908] Fix specs --- spec/lib/gitlab/markdown/sanitization_filter_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb index e50c82d0b3..8243c99460 100644 --- a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb +++ b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb @@ -93,16 +93,16 @@ module Gitlab::Markdown end end - context 'when pipeline is :description' do + context 'when inline_sanitization is true' do it 'uses a stricter whitelist' do - doc = filter('

Description

', pipeline: :description) + doc = filter('

Description

', inline_sanitization: true) expect(doc.to_html.strip).to eq 'Description' end %w(pre code img ol ul li).each do |elem| it "removes '#{elem}' elements" do act = "<#{elem}>Description" - expect(filter(act, pipeline: :description).to_html.strip). + expect(filter(act, inline_sanitization: true).to_html.strip). to eq 'Description' end end @@ -110,7 +110,7 @@ module Gitlab::Markdown %w(b i strong em a ins del sup sub p).each do |elem| it "still allows '#{elem}' elements" do exp = act = "<#{elem}>Description" - expect(filter(act, pipeline: :description).to_html).to eq exp + expect(filter(act, inline_sanitization: true).to_html).to eq exp end end end From c5146ca1517412b0389c483f18c13072e2abc4a3 Mon Sep 17 00:00:00 2001 From: Jan-Gerd Tenberge Date: Fri, 23 Oct 2015 03:00:08 +0200 Subject: [PATCH 033/908] Fix merge error --- app/helpers/application_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 596a47938d..3fa7d2bb1c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -68,7 +68,7 @@ module ApplicationHelper end end - def avatar_icon(user_email = nil, size = nil, scale = 2) + def avatar_icon(user_or_email = nil, size = nil, scale = 2) if user_or_email.is_a?(User) user = user_or_email else From 7851a292a1fc7da3cd2d1140cd40f35009a9c082 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 23 Oct 2015 15:00:26 +0200 Subject: [PATCH 034/908] Use single_line Markdown pipeline for commit description --- app/views/projects/commits/show.atom.builder | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index 7aab98e07c..a40720ab4a 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -17,7 +17,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.name commit.author_name xml.email commit.author_email end - xml.summary markdown(commit.description, pipeline: :atom) + xml.summary markdown(commit.description, pipeline: :single_line) end end end From 2bbdeb91ca6b2c1e4fa10dbed34f9fff39e790f4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 27 Oct 2015 15:22:48 +0100 Subject: [PATCH 035/908] Reset memoized project repository when path changes --- app/models/project.rb | 1 + app/views/projects/edit.html.haml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 74b89aad49..e0b44ee9be 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -647,6 +647,7 @@ class Project < ActiveRecord::Base gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki") send_move_instructions(old_path_with_namespace) reset_events_cache + @repository = nil rescue # Returning false does not rollback after_* transaction but gives # us information about failing some of tasks diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index afbf88b550..f5bb00b061 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -24,10 +24,10 @@ .col-sm-10 = f.text_area :description, placeholder: "Awesome project", class: "form-control", rows: 3, maxlength: 250 - - if @project.repository.exists? && @project.repository.branch_names.any? + - unless @project.empty_repo? .form-group = f.label :default_branch, "Default Branch", class: 'control-label' - .col-sm-10= f.select(:default_branch, @repository.branch_names, {}, {class: 'select2 select-wide'}) + .col-sm-10= f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) = render 'shared/visibility_level', f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can_change_visibility_level?(@project, current_user), form_model: @project From 28af56dea5a88ffcaceb082cf67c9c1ab021609d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 27 Oct 2015 17:49:34 +0100 Subject: [PATCH 036/908] gfm is no longer --- app/views/shared/snippets/_header.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 0a4a790ec5..dc98d310a1 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -21,4 +21,4 @@ = render "snippets/actions" .gray-content-block.middle-block %h2.snippet-title - = gfm escape_once(@snippet.title) + = markdown escape_once(@snippet.title), pipeline: :single_line From 3b717c8a8c1e0f10bc06fd8501ce2423c98490d4 Mon Sep 17 00:00:00 2001 From: Jan-Gerd Tenberge Date: Sun, 1 Nov 2015 02:42:36 +0100 Subject: [PATCH 037/908] Fix typo --- app/helpers/application_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3fa7d2bb1c..3230ff1b00 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -78,7 +78,7 @@ module ApplicationHelper if user user.avatar_url(size) || default_avatar else - gravatar_icon(user_or_email, size, scales) + gravatar_icon(user_or_email, size, scale) end end From 77f8a1e392b64f51326df8aebdc77e97af07bfed Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 2 Nov 2015 17:27:38 +0100 Subject: [PATCH 038/908] Merge when build succeeds --- CHANGELOG | 1 + .../stylesheets/pages/merge_requests.scss | 4 +- .../projects/merge_requests_controller.rb | 35 ++++++++++++--- app/models/ci/commit.rb | 8 ++++ app/models/commit_status.rb | 33 ++++++++++++++ app/models/merge_request.rb | 13 ++++++ .../merge_when_build_succeeds_service.rb | 43 +++++++++++++++++++ .../merge_requests/refresh_service.rb | 8 +++- app/services/system_note_service.rb | 14 ++++++ .../cancel_merge_when_build_succeeds.js.haml | 2 + .../projects/merge_requests/merge.js.haml | 6 ++- .../merge_requests/widget/_open.html.haml | 2 + .../widget/open/_accept.html.haml | 18 +++++--- .../open/_merge_when_build_succeeds.html.haml | 27 ++++++++++++ config/routes.rb | 1 + ...ge_when_build_succeeds_to_merge_request.rb | 7 +++ db/schema.rb | 17 +++++--- 17 files changed, 219 insertions(+), 20 deletions(-) create mode 100644 app/services/merge_requests/merge_when_build_succeeds_service.rb create mode 100644 app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml create mode 100644 app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml create mode 100644 db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb diff --git a/CHANGELOG b/CHANGELOG index 0d89fca9fc..195e53abd8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ v 8.2.0 (unreleased) - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) - Remove deprecated CI events from project settings page - Use issue editor as cross reference comment author when issue is edited with a new mention. + - Merge when build succeeds (Zeger-Jan van de Weg) v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index f0b3667acc..b5a8c89367 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -19,18 +19,20 @@ .accept-merge-holder { .accept-action { display: inline-block; + float: left; } .accept-control { display: inline-block; + float: left; margin: 0; margin-left: 20px; padding: 5px; + padding-top: 12px; line-height: 20px; &.right { float: right; - padding-top: 12px; a { color: $gl-gray; } diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 16c4238662..2f9b8a25ed 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -2,7 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ :edit, :update, :show, :diffs, :commits, :merge, :merge_check, - :ci_status, :toggle_subscription + :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds ] before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits] before_action :validates_merge_request, only: [:show, :diffs, :commits] @@ -149,15 +149,34 @@ class Projects::MergeRequestsController < Projects::ApplicationController render partial: "projects/merge_requests/widget/show.html.haml", layout: false end + def cancel_merge_when_build_succeeds + return access_denied! unless @merge_request.can_be_merged_by?(current_user) + + if @merge_request.merge_when_build_succeeds? + @merge_request.reset_merge_when_build_succeeds + SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user) + end + end + def merge return access_denied! unless @merge_request.can_be_merged_by?(current_user) - if @merge_request.mergeable? - @merge_request.update(merge_error: nil) - MergeWorker.perform_async(@merge_request.id, current_user.id, params) - @status = true + unless @merge_request.mergeable? + @status = :failed + return + end + + @merge_request.update(merge_error: nil) + + if params[:merge_when_build_succeeds] && @merge_request.ci_commit.active? + MergeRequests::MergeWhenBuildSucceedsService.new(@project, + current_user, + merge_params: merge_params) + .execute(@merge_request) + @status = :merge_when_build_succeeds else - @status = false + MergeWorker.perform_async(@merge_request.id, current_user.id, params) + @status = :success end end @@ -282,6 +301,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController ) end + def merge_params + params.permit(:should_remove_source_branch, :commit_message) + end + # Make sure merge requests created before 8.0 # have head file in refs/merge-requests/ def ensure_ref_fetched diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 13437b2483..ebe4bace3b 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -164,6 +164,14 @@ module Ci status == 'canceled' end + def active? + running? || pending? + end + + def complete? + canceled? || success? || failed? + end + def duration duration_array = latest_statuses.map(&:duration).compact duration_array.reduce(:+).to_i diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 0b73ab6d2e..b1049fab78 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -1,3 +1,32 @@ +# == Schema Information +# +# project_id integer +# status string +# finished_at datetime +# trace text +# created_at datetime +# updated_at datetime +# started_at datetime +# runner_id integer +# coverage float +# commit_id integer +# commands text +# job_id integer +# name string +# deploy boolean default: false +# options text +# allow_failure boolean default: false, null: false +# stage string +# trigger_request_id integer +# stage_idx integer +# tag boolean +# ref string +# user_id integer +# type string +# target_url string +# description string +# + class CommitStatus < ActiveRecord::Base self.table_name = 'ci_builds' @@ -46,6 +75,10 @@ class CommitStatus < ActiveRecord::Base build.update_attributes finished_at: Time.now end + after_transition running: :success do |build, transition| + MergeRequests::MergeWhenBuildSucceedsService.new(build.commit.gl_project, nil).trigger(build) + end + state :pending, value: 'pending' state :running, value: 'running' state :failed, value: 'failed' diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 85f37e49e6..c5f04dbcf4 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -34,9 +34,12 @@ class MergeRequest < ActiveRecord::Base belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" + belongs_to :merge_user, class_name: "User" has_one :merge_request_diff, dependent: :destroy + serialize :merge_params, Hash + after_create :create_merge_request_diff after_update :update_merge_request_diff @@ -385,6 +388,16 @@ class MergeRequest < ActiveRecord::Base message end + def reset_merge_when_build_succeeds + return unless merge_when_build_succeeds? + + self.merge_when_build_succeeds = false + self.merge_user = nil + self.merge_params = nil + + self.save + end + # Return array of possible target branches # depends on target project of MR def target_branches diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb new file mode 100644 index 0000000000..a4418360b8 --- /dev/null +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -0,0 +1,43 @@ +module MergeRequests + class MergeWhenBuildSucceedsService < MergeRequests::BaseService + def execute(merge_request) + merge_request.merge_params.merge!(params[:merge_params]) + + # The service is also called when the merge params are updated. + already_approved = merge_request.merge_when_build_succeeds? + + unless already_approved + merge_request.merge_when_build_succeeds = true + merge_request.merge_user = @current_user + end + + merge_request.save + + unless already_approved + SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user) + end + end + + def trigger(build) + merge_requests = merge_request_from(build) + + merge_requests.each do |merge_request| + next unless merge_request.merge_when_build_succeeds? + + ci_commit = merge_request.ci_commit + if ci_commit && ci_commit.success? && merge_request.mergeable? + MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) + end + end + end + + private + + def merge_request_from(build) + merge_requests = @project.origin_merge_requests.opened.where(source_branch: build.ref).to_a + merge_requests += @project.fork_merge_requests.opened.where(source_branch: build.ref).to_a + + merge_requests.uniq.select(&:source_project) + end + end +end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index d68bc79ecc..335ef32abc 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -8,6 +8,7 @@ module MergeRequests find_new_commits reload_merge_requests + reset_merge_when_build_succeeds # Leave a system note if a branch was deleted/added if branch_added? || branch_removed? @@ -57,7 +58,6 @@ module MergeRequests merge_requests = filter_merge_requests(merge_requests) merge_requests.each do |merge_request| - if merge_request.source_branch == @branch_name || force_push? merge_request.reload_code merge_request.mark_as_unchecked @@ -76,6 +76,12 @@ module MergeRequests end end + def reset_merge_when_build_succeeds + merge_requests_for_source_branch.each do |merge_request| + merge_request.reset_merge_when_build_succeeds + end + end + def find_new_commits if branch_added? @commits = [] diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 708c2f0048..c9846e9f26 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -130,6 +130,20 @@ class SystemNoteService create_note(noteable: noteable, project: project, author: author, note: body) end + # Called when 'merge when build succeeds' is executed + def self.merge_when_build_succeeds(noteable, project, author) + body = "Approved this request to be merged automatically when the build succeeds" + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when 'merge when build succeeds' is canceled + def self.cancel_merge_when_build_succeeds(noteable, project, author) + body = "Canceled the automatic merge" + + create_note(noteable: noteable, project: project, author: author, note: body) + end + # Called when the title of a Noteable is changed # # noteable - Noteable object that responds to `title` diff --git a/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml b/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml new file mode 100644 index 0000000000..eab5be488b --- /dev/null +++ b/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml @@ -0,0 +1,2 @@ +:plain + $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/accept'))}"); diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml index 33321651e3..89aae17a60 100644 --- a/app/views/projects/merge_requests/merge.js.haml +++ b/app/views/projects/merge_requests/merge.js.haml @@ -1,6 +1,10 @@ -- if @status +- case @status +- when :success :plain merge_request_widget.mergeInProgress(); +- when :merge_when_build_succeeds + :plain + $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_build_succeeds'))}"); - else :plain $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}"); diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 0aad9bb3e8..e0013fb769 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -13,6 +13,8 @@ = render 'projects/merge_requests/widget/open/conflicts' - elsif @merge_request.work_in_progress? = render 'projects/merge_requests/widget/open/wip' + - elsif @merge_request.merge_when_build_succeeds? + = render 'projects/merge_requests/widget/open/merge_when_build_succeeds' - elsif !@merge_request.can_be_merged_by?(current_user) = render 'projects/merge_requests/widget/open/not_allowed' - elsif @merge_request.can_be_merged? diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 613525437a..2afc5f8125 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -2,8 +2,15 @@ = hidden_field_tag :authenticity_token, form_authenticity_token .accept-merge-holder.clearfix.js-toggle-container .accept-action - = f.button class: "btn btn-create accept_merge_request" do - Accept Merge Request + - ci_commit = @merge_request.ci_commit + - if ci_commit && ci_commit.active? + = f.button class: "btn btn-create btn-grouped merge_when_build_succeeds", name: "merge_when_build_succeeds" do + Merge when Build Succeeds + = f.button class: "btn btn-warning btn-grouped accept_merge_request" do + Accept Merge Request Now + - else + = f.button class: "btn btn-create btn-grouped accept_merge_request" do + Accept Merge Request - if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && !@merge_request.for_fork? .accept-control.checkbox = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do @@ -19,7 +26,8 @@ rows: 14, hint: true :coffeescript - $('.accept-mr-form').on 'ajax:before', -> - btn = $('.accept_merge_request') - btn.disable() + $('.accept_merge_request').on 'click', -> + btn = $(this) btn.html(" Merge in progress") + $('.accept-mr-form').on 'ajax:sen', -> + $(".accept-mr-form :input").disable() diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml new file mode 100644 index 0000000000..7e5385cf8b --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -0,0 +1,27 @@ +%h4 + Approved by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} + to be merged automatically when + #{link_to "the build", ci_status_path(@merge_request.ci_commit)} succeeds +%div + - if @merge_request.merge_params["should_remove_source_branch"] + = succeed '.' do + The changes will be merged into + %span.label-branch= @merge_request.target_branch + The source branch will be removed. + - elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) + - remove_source_branch_button = true + %p + = succeed '.' do + The changes will be merged into + %span.label-branch= @merge_request.target_branch + The source branch will not be removed. + +- if remove_source_branch_button || @merge_request.can_be_merged_by?(current_user) + .clearfix.prepend-top-10 + - if remove_source_branch_button + = link_to merge_namespace_project_merge_request_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do + = icon('times') + Remove Source Branch When Merged + - if @merge_request.can_be_merged_by?(current_user) + = link_to merge_namespace_project_merge_request_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request), remote: true, method: :delete, class: "btn btn-grouped btn-warning btn-sm" do + Cancel Automatic Merge diff --git a/config/routes.rb b/config/routes.rb index 0458f538eb..917c3d3f1e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -556,6 +556,7 @@ Gitlab::Application.routes.draw do get :diffs get :commits post :merge + delete :merge, action: :cancel_merge_when_build_succeeds get :merge_check get :ci_status post :toggle_subscription diff --git a/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb b/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb new file mode 100644 index 0000000000..ceb52f0c22 --- /dev/null +++ b/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb @@ -0,0 +1,7 @@ +class AddMergeWhenBuildSucceedsToMergeRequest < ActiveRecord::Migration + def change + add_column :merge_requests, :merge_params, :text + add_column :merge_requests, :merge_when_build_succeeds, :boolean, default: false, null: false + add_column :merge_requests, :merge_user_id, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 4bde9f0b74..825d95565b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151026182941) do +ActiveRecord::Schema.define(version: 20151028152939) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -418,6 +418,7 @@ ActiveRecord::Schema.define(version: 20151026182941) do end add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree + add_index "labels", ["title"], name: "index_labels_on_title", using: :btree create_table "members", force: true do |t| t.integer "access_level", null: false @@ -453,9 +454,9 @@ ActiveRecord::Schema.define(version: 20151026182941) do add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree create_table "merge_requests", force: true do |t| - t.string "target_branch", null: false - t.string "source_branch", null: false - t.integer "source_project_id", null: false + t.string "target_branch", null: false + t.string "source_branch", null: false + t.integer "source_project_id", null: false t.integer "author_id" t.integer "assignee_id" t.string "title" @@ -464,13 +465,16 @@ ActiveRecord::Schema.define(version: 20151026182941) do t.integer "milestone_id" t.string "state" t.string "merge_status" - t.integer "target_project_id", null: false + t.integer "target_project_id", null: false t.integer "iid" t.text "description" - t.integer "position", default: 0 + t.integer "position", default: 0 t.datetime "locked_at" t.integer "updated_by_id" t.string "merge_error" + t.text "merge_params" + t.boolean "merge_when_build_succeeds", default: false, null: false + t.integer "merge_user_id" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree @@ -499,6 +503,7 @@ ActiveRecord::Schema.define(version: 20151026182941) do add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree + add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree create_table "namespaces", force: true do |t| t.string "name", null: false From 63b234706d46f75c0c0f93bdfdc576e328981eb5 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 2 Nov 2015 20:02:51 +0100 Subject: [PATCH 039/908] MRs author can cancel automatic merge --- app/controllers/projects/merge_requests_controller.rb | 4 +++- .../widget/open/_merge_when_build_succeeds.html.haml | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 2f9b8a25ed..d58dab2d66 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -150,7 +150,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def cancel_merge_when_build_succeeds - return access_denied! unless @merge_request.can_be_merged_by?(current_user) + unless @merge_request.can_be_merged_by?(current_user) || @merge_request.author == current_user + return access_denied! + end if @merge_request.merge_when_build_succeeds? @merge_request.reset_merge_when_build_succeeds diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index 7e5385cf8b..f389433496 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -14,7 +14,7 @@ = succeed '.' do The changes will be merged into %span.label-branch= @merge_request.target_branch - The source branch will not be removed. + The source branch won't be removed. - if remove_source_branch_button || @merge_request.can_be_merged_by?(current_user) .clearfix.prepend-top-10 @@ -22,6 +22,6 @@ = link_to merge_namespace_project_merge_request_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do = icon('times') Remove Source Branch When Merged - - if @merge_request.can_be_merged_by?(current_user) + - if @merge_request.can_be_merged_by?(current_user) || @merge_request.author == current_user = link_to merge_namespace_project_merge_request_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request), remote: true, method: :delete, class: "btn btn-grouped btn-warning btn-sm" do Cancel Automatic Merge From d2846293d067cdf38d2816768e059d901a960a22 Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Tue, 3 Nov 2015 16:37:39 +0100 Subject: [PATCH 040/908] Move visibility_level check for forked projects to Project model --- app/helpers/visibility_level_helper.rb | 3 +-- app/models/project.rb | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index b52cd23aba..bc594dc53c 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -89,7 +89,6 @@ module VisibilityLevelHelper def skip_level?(form_model, level) form_model.is_a?(Project) && - form_model.forked? && - !Gitlab::VisibilityLevel.allowed_fork_levels(form_model.forked_from_project.visibility_level).include?(level) + !form_model.visibility_level_allowed?(level) end end diff --git a/app/models/project.rb b/app/models/project.rb index 74b89aad49..a4c634bdb5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -783,4 +783,9 @@ class Project < ActiveRecord::Base service.active = true service.save end + + def visibility_level_allowed?(level) + return true unless forked? + Gitlab::VisibilityLevel.allowed_fork_levels(forked_from_project.visibility_level).include?(level) + end end From 9e1db139eb1387fa9658ed68592d93eca61efb6b Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Tue, 3 Nov 2015 17:23:19 +0100 Subject: [PATCH 041/908] Move level_name resolving to Gitlan::VisibilityLevel --- app/services/base_service.rb | 5 +---- lib/gitlab/visibility_level.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/services/base_service.rb b/app/services/base_service.rb index f00ec7408b..b48ca67d4d 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -39,10 +39,7 @@ class BaseService def deny_visibility_level(model, denied_visibility_level = nil) denied_visibility_level ||= model.visibility_level - level_name = 'Unknown' - Gitlab::VisibilityLevel.options.each do |name, level| - level_name = name if level == denied_visibility_level - end + level_name = Gitlab::VisibilityLevel.level_name(denied_visibility_level) model.errors.add( :visibility_level, diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index 335dc44be1..d0ffe24f82 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -51,6 +51,15 @@ module Gitlab def allowed_fork_levels(origin_level) [PRIVATE, INTERNAL, PUBLIC].select{ |level| level <= origin_level } end + + def level_name(level) + level_name = 'Unknown' + options.each do |name, lvl| + level_name = name if lvl == level + end + + level_name + end end def private? From 7cb442eed4f488e378b3f20008ebe6ed3b53d31d Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Tue, 3 Nov 2015 18:23:48 +0100 Subject: [PATCH 042/908] Fix Project update service When project is updated and it is a fork, then visibility_level should not be less restrictive than in its parent project. --- app/models/project.rb | 2 +- app/services/projects/update_service.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index a4c634bdb5..7f2dd37a3c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -786,6 +786,6 @@ class Project < ActiveRecord::Base def visibility_level_allowed?(level) return true unless forked? - Gitlab::VisibilityLevel.allowed_fork_levels(forked_from_project.visibility_level).include?(level) + Gitlab::VisibilityLevel.allowed_fork_levels(forked_from_project.visibility_level).include?(level.to_i) end end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 69bdd045dd..0a42f3e02a 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -11,6 +11,15 @@ module Projects end end + unless project.visibility_level_allowed?(new_visibility) + level_name = Gitlab::VisibilityLevel.level_name(new_visibility) + project.errors.add( + :visibility_level, + "#{level_name} could not be set as visibility level of this project - parent project settings are more restrictive" + ) + return false + end + new_branch = params[:default_branch] if project.repository.exists? && new_branch && new_branch != project.default_branch From 3bc012db77e1b59986362d8de0660b97a15c9d1f Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Wed, 4 Nov 2015 22:13:40 +0100 Subject: [PATCH 043/908] Fix GitlabV::isibilityLevel::level_name method --- lib/gitlab/visibility_level.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index d0ffe24f82..3160a3c758 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -55,7 +55,7 @@ module Gitlab def level_name(level) level_name = 'Unknown' options.each do |name, lvl| - level_name = name if lvl == level + level_name = name if lvl == level.to_i end level_name From 89ecba5e6cec6632c2f8e3baef6604b1b8ea0d45 Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Wed, 4 Nov 2015 23:40:43 +0100 Subject: [PATCH 044/908] Update forks visibility_level after parent project visibility_level change --- app/models/project.rb | 6 +++- app/services/projects/update_service.rb | 48 ++++++++++++++++++------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 7f2dd37a3c..f287e59b6d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -731,7 +731,11 @@ class Project < ActiveRecord::Base end def forks_count - ForkedProjectLink.where(forked_from_project_id: self.id).count + forks.count + end + + def forks + ForkedProjectLink.where(forked_from_project_id: self.id) end def find_label(name) diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 0a42f3e02a..0d2665f298 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -3,21 +3,17 @@ module Projects def execute # check that user is allowed to set specified visibility_level new_visibility = params[:visibility_level] - if new_visibility && new_visibility.to_i != project.visibility_level - unless can?(current_user, :change_visibility_level, project) && - Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) - deny_visibility_level(project, new_visibility) - return project + if new_visibility + if new_visibility.to_i != project.visibility_level + unless can?(current_user, :change_visibility_level, project) && + Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + deny_visibility_level(project, new_visibility) + return project + end end - end - unless project.visibility_level_allowed?(new_visibility) - level_name = Gitlab::VisibilityLevel.level_name(new_visibility) - project.errors.add( - :visibility_level, - "#{level_name} could not be set as visibility level of this project - parent project settings are more restrictive" - ) - return false + return false unless visibility_level_allowed?(new_visibility) + update_forks_visibility_level(new_visibility) end new_branch = params[:default_branch] @@ -32,5 +28,31 @@ module Projects end end end + + private + + def visibility_level_allowed?(level) + return true if project.visibility_level_allowed?(level) + + level_name = Gitlab::VisibilityLevel.level_name(level) + project.errors.add( + :visibility_level, + "#{level_name} could not be set as visibility level of this project - parent project settings are more restrictive" + ) + + false + end + + def update_forks_visibility_level(new_level) + project.forks.each do |forked_link| + forked_project = forked_link.forked_to_project + fork_level = forked_project.visibility_level + + if fork_level > new_level.to_i + forked_project.visibility_level = new_level.to_i + forked_project.save! + end + end + end end end From a3f64b53ecb0b40c94eb0fe32b662239fff6961f Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 11 Nov 2015 13:09:23 +0100 Subject: [PATCH 045/908] Set changelog item to right release --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 36612126c6..69a293d70d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,6 +32,7 @@ v 8.2.0 (unreleased) - Fix incoming email config defaults - MR target branch is now visible on a list view when it is different from project's default one - Improve Continuous Integration graphs page + - Accept COPYING as licence file (Zeger-Jan van de Weg) v 8.1.4 - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) @@ -155,7 +156,6 @@ v 8.0.4 - Fix "Assign All" button on Runner admin page - Fix search in Files - Add full project namespace to payload of system webhooks (Ricardo Band) - - Accept COPYING as licence file (Zeger-Jan van de Weg) v 8.0.3 - Fix URL shown in Slack notifications From e189666a6d233a76a8f2e29919048e59b137823c Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 11 Nov 2015 13:31:42 +0100 Subject: [PATCH 046/908] Add CHANGELOG line to the right release --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index cc68f489d4..3439bf5d75 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,6 +32,7 @@ v 8.2.0 (unreleased) - Fix incoming email config defaults - MR target branch is now visible on a list view when it is different from project's default one - Improve Continuous Integration graphs page + - Add rake tasks for git repository maintainance (Zeger-Jan van de Weg) v 8.1.4 - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) @@ -155,7 +156,6 @@ v 8.0.4 - Fix "Assign All" button on Runner admin page - Fix search in Files - Add full project namespace to payload of system webhooks (Ricardo Band) - - Add rake tasks for git repository maintainance (Zeger-Jan van de Weg) v 8.0.3 - Fix URL shown in Slack notifications From 303f79f8046876c7a461708dee5d1f50f12737bf Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 18 Nov 2015 17:10:04 +0800 Subject: [PATCH 047/908] Make sure notify email always has author info. --- app/views/notify/_note_message.html.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml index 00cb4aa24c..27112c6745 100644 --- a/app/views/notify/_note_message.html.haml +++ b/app/views/notify/_note_message.html.haml @@ -1,2 +1,4 @@ +%div + "#{link_to @note.author_name, user_url(@note.author)} wrote:" %div = markdown(@note.note, pipeline: :email) From 2f048df4a4a83ff009d2ef2d14ee04e5a2798618 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 18 Nov 2015 11:17:41 +0100 Subject: [PATCH 048/908] API support, incorporated feedback --- .../projects/merge_requests_controller.rb | 4 +- app/models/commit_status.rb | 2 +- app/models/merge_request.rb | 6 +- app/services/merge_requests/merge_service.rb | 16 +++-- .../merge_when_build_succeeds_service.rb | 2 +- .../merge_requests/refresh_service.rb | 4 +- app/services/system_note_service.rb | 2 +- .../widget/open/_accept.html.haml | 2 +- .../open/_merge_when_build_succeeds.html.haml | 8 +-- app/workers/merge_worker.rb | 13 +--- config/routes.rb | 3 +- doc/api/merge_requests.md | 60 ++++++++++++++-- lib/api/merge_requests.rb | 68 +++++++++++++------ spec/models/merge_request_spec.rb | 14 +++- spec/services/system_note_service_spec.rb | 16 +++++ 15 files changed, 157 insertions(+), 63 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d58dab2d66..931298df5d 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -171,9 +171,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.update(merge_error: nil) if params[:merge_when_build_succeeds] && @merge_request.ci_commit.active? - MergeRequests::MergeWhenBuildSucceedsService.new(@project, - current_user, - merge_params: merge_params) + MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) .execute(@merge_request) @status = :merge_when_build_succeeds else diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index b1049fab78..acc86b1a8c 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -75,7 +75,7 @@ class CommitStatus < ActiveRecord::Base build.update_attributes finished_at: Time.now end - after_transition running: :success do |build, transition| + after_transition [:pending, :running] => :success do |build, transition| MergeRequests::MergeWhenBuildSucceedsService.new(build.commit.gl_project, nil).trigger(build) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c5f04dbcf4..7b372399a3 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -253,6 +253,10 @@ class MergeRequest < ActiveRecord::Base end end + def mergeable_by_or_author(user) + self.can_be_merged_by?(user) || self.author == user + end + def mr_and_commit_notes # Fetch comments only from last 100 commits commits_for_notes_limit = 100 @@ -390,7 +394,7 @@ class MergeRequest < ActiveRecord::Base def reset_merge_when_build_succeeds return unless merge_when_build_succeeds? - + self.merge_when_build_succeeds = false self.merge_user = nil self.merge_params = nil diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 7963af127e..db8d18a7d8 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -6,15 +6,12 @@ module MergeRequests # Executed when you do merge via GitLab UI # class MergeService < MergeRequests::BaseService - attr_reader :merge_request, :commit_message + attr_reader :merge_request - def execute(merge_request, commit_message) - @commit_message = commit_message + def execute(merge_request) @merge_request = merge_request - unless @merge_request.mergeable? - return error('Merge request is not mergeable') - end + return error('Merge request is not mergeable') unless @merge_request.mergeable? merge_request.in_locked_state do if commit @@ -32,7 +29,7 @@ module MergeRequests committer = repository.user_to_committer(current_user) options = { - message: commit_message, + message: params[:commit_message] || merge_request.merge_commit_message, author: committer, committer: committer } @@ -46,6 +43,11 @@ module MergeRequests def after_merge MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) + + if params[:should_remove_source_branch] + DeleteBranchService.new(@merge_request.source_project, current_user). + execute(merge_request.source_branch) + end end end end diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb index a4418360b8..d5cae2f98f 100644 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -1,7 +1,7 @@ module MergeRequests class MergeWhenBuildSucceedsService < MergeRequests::BaseService def execute(merge_request) - merge_request.merge_params.merge!(params[:merge_params]) + merge_request.merge_params.merge!(params) # The service is also called when the merge params are updated. already_approved = merge_request.merge_when_build_succeeds? diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 335ef32abc..e502485720 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -77,9 +77,7 @@ module MergeRequests end def reset_merge_when_build_succeeds - merge_requests_for_source_branch.each do |merge_request| - merge_request.reset_merge_when_build_succeeds - end + merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds) end def find_new_commits diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index c9846e9f26..5e8281a3fd 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -132,7 +132,7 @@ class SystemNoteService # Called when 'merge when build succeeds' is executed def self.merge_when_build_succeeds(noteable, project, author) - body = "Approved this request to be merged automatically when the build succeeds" + body = "This merge request will be automatically merged when the build for #{noteable.ci_commit.short_sha} succeeds" create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 2afc5f8125..2a51241971 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -29,5 +29,5 @@ $('.accept_merge_request').on 'click', -> btn = $(this) btn.html(" Merge in progress") - $('.accept-mr-form').on 'ajax:sen', -> + $('.accept-mr-form').on 'ajax:send', -> $(".accept-mr-form :input").disable() diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index f389433496..ddd1a7bd63 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -1,9 +1,9 @@ %h4 Approved by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} to be merged automatically when - #{link_to "the build", ci_status_path(@merge_request.ci_commit)} succeeds + #{link_to "the build", ci_status_path(@merge_request.ci_commit)} succeeds. %div - - if @merge_request.merge_params["should_remove_source_branch"] + - if @merge_request.merge_params["should_remove_source_branch"].present? = succeed '.' do The changes will be merged into %span.label-branch= @merge_request.target_branch @@ -19,9 +19,9 @@ - if remove_source_branch_button || @merge_request.can_be_merged_by?(current_user) .clearfix.prepend-top-10 - if remove_source_branch_button - = link_to merge_namespace_project_merge_request_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do + = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do = icon('times') Remove Source Branch When Merged - if @merge_request.can_be_merged_by?(current_user) || @merge_request.author == current_user - = link_to merge_namespace_project_merge_request_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request), remote: true, method: :delete, class: "btn btn-grouped btn-warning btn-sm" do + = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-warning btn-sm" do Cancel Automatic Merge diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb index 5d1a8555b7..c87c0a252b 100644 --- a/app/workers/merge_worker.rb +++ b/app/workers/merge_worker.rb @@ -8,16 +8,7 @@ class MergeWorker current_user = User.find(current_user_id) merge_request = MergeRequest.find(merge_request_id) - result = MergeRequests::MergeService.new(merge_request.target_project, current_user). - execute(merge_request, params[:commit_message]) - - if result[:status] == :success && params[:should_remove_source_branch].present? - DeleteBranchService.new(merge_request.source_project, current_user). - execute(merge_request.source_branch) - - merge_request.source_project.repository.expire_branch_names - end - - result + MergeRequests::MergeService.new(merge_request.target_project, current_user, params). + execute(merge_request) end end diff --git a/config/routes.rb b/config/routes.rb index 917c3d3f1e..993687665c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -556,8 +556,7 @@ Gitlab::Application.routes.draw do get :diffs get :commits post :merge - delete :merge, action: :cancel_merge_when_build_succeeds - get :merge_check + post :cancel_merge_when_build_succeeds get :ci_status post :toggle_subscription end diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index ffa7f2cdf1..1b2ad1caf4 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -2,8 +2,8 @@ ## List merge requests -Get all merge requests for this project. -The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`). +Get all merge requests for this project. +The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`). The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests. ``` @@ -292,9 +292,59 @@ PUT /projects/:id/merge_request/:merge_request_id/merge Parameters: -- `id` (required) - The ID of a project -- `merge_request_id` (required) - ID of MR -- `merge_commit_message` (optional) - Custom merge commit message +- `id` (required) - The ID of a project +- `merge_request_id` (required) - ID of MR +- `merge_commit_message` (optional) - Custom merge commit message +- `should_remove_source_branch` (optional) - if `true` removes the source branch +- `merge_when_build_succeeds` (optional) - if `true` the MR is merge when the build succeeds + +```json +{ + "id": 1, + "target_branch": "master", + "source_branch": "test1", + "project_id": 3, + "title": "test1", + "state": "merged", + "upvotes": 0, + "downvotes": 0, + "author": { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "name": "Administrator", + "state": "active", + "created_at": "2012-04-29T08:46:00Z" + }, + "assignee": { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "name": "Administrator", + "state": "active", + "created_at": "2012-04-29T08:46:00Z" + } +} +``` + +## Cancel Merge When Build Succeeds + +Cancels the merge when build succeeds and reset the merge parameters + +If successfull you'll get `200 OK`. + +If you don't have permissions to accept this merge request - you'll get a 401 + +If the merge request is already merged or closed - you get 405 and error message 'Method Not Allowed' + +In case the merge request is not set to be merged when the build succeeds, you'll also get a 405 with the error message 'Method Not Allowed' +``` +PUT /projects/:id/merge_request/:merge_request_id/cancel_merge_when_build_succeeds +``` +Parameters: + +- `id` (required) - The ID of a project +- `merge_request_id` (required) - ID of MR ```json { diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 6eb84baf9c..f981432db3 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -179,9 +179,10 @@ module API # Merge MR # # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - ID of MR - # merge_commit_message (optional) - Custom merge commit message + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR + # merge_commit_message (optional) - Custom merge commit message + # merge_when_build_succeeds (optional) - truethy when this MR should be merged when the build is succesfull # Example: # PUT /projects/:id/merge_request/:merge_request_id/merge # @@ -191,34 +192,57 @@ module API allowed = ::Gitlab::GitAccess.new(current_user, user_project). can_push_to_branch?(merge_request.target_branch) - if allowed - if merge_request.unchecked? - merge_request.check_if_can_be_merged - end + # Merge request can not be merged + # because user dont have permissions to push into target branch + unauthorized! unless allowed - if merge_request.open? && !merge_request.work_in_progress? - if merge_request.can_be_merged? - commit_message = params[:merge_commit_message] || merge_request.merge_commit_message + not_allowed! unless merge_request.open? - ::MergeRequests::MergeService.new(merge_request.target_project, current_user). - execute(merge_request, commit_message) + merge_request.check_if_can_be_merged if merge_request.unchecked? - present merge_request, with: Entities::MergeRequest - else - render_api_error!('Branch cannot be merged', 405) - end + merge_params = { + commit_message: params[:merge_commit_message], + should_remove_source_branch: params[:should_remove_source_branch] + } + + if !merge_request.work_in_progress? + if parse_boolean(params[:merge_when_build_succeeds]) + ::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params). + execute(merge_request) else - # Merge request can not be merged - # because it is already closed/merged or marked as WIP - not_allowed! + ::MergeRequests::MergeService.new(merge_request.target_project, current_user, merge_params). + execute(merge_request, merge_params) end else - # Merge request can not be merged - # because user dont have permissions to push into target branch - unauthorized! + render_api_error!('Branch cannot be merged', 405) end + + present merge_request, with: Entities::MergeRequest end + # Cancel Merge if Merge When build succeeds is enabled + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR + # + post ":id/merge_request/:merge_request_id/cancel_merge_when_build_succeeds" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + + allowed = ::Gitlab::GitAccess.new(current_user, user_project). + can_push_to_branch?(merge_request.target_branch) + + # Merge request can not be merged + # because user dont have permissions to push into target branch + unauthorized! unless allowed + + if merge_request.merged? || !merge_request.open? || !merge_request.merge_when_build_succeeds? + not_allowed! + end + + merge_request.reset_merge_when_build_succeeds + + present merge_request, with: Entities::MergeRequest + end # Get a merge request's comments # diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index eed2cbc541..e42534a849 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -30,7 +30,7 @@ describe MergeRequest do describe 'associations' do it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') } it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') } - + it { is_expected.to belong_to(:merge_user).class_name("User") } it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) } end @@ -53,6 +53,8 @@ describe MergeRequest do it { is_expected.to respond_to(:unchecked?) } it { is_expected.to respond_to(:can_be_merged?) } it { is_expected.to respond_to(:cannot_be_merged?) } + it { is_expected.to respond_to(:merge_params) } + it { is_expected.to respond_to(:merge_when_build_succeeds) } end describe '#to_reference' do @@ -171,6 +173,16 @@ describe MergeRequest do end end + describe "#reset_merge_when_build_succeeds" do + let(:merge_if_green) { create :merge_request, merge_when_build_succeeds: true } + it "sets the item to false" do + merge_if_green.reset_merge_when_build_succeeds + merge_if_green.reload + + expect(merge_if_green.merge_when_build_succeeds).to be_falsey + end + end + describe "#hook_attrs" do it "has all the required keys" do attrs = subject.hook_attrs diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index a45130bd47..35912ece64 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -207,6 +207,22 @@ describe SystemNoteService do end end + describe '.merge_when_build_succeeds' do + let(:ci_commit) { create :ci_commit, gl_project: project } + let(:merge_request) { create :merge_request, project: project } + + subject { described_class.merge_when_build_succeeds(merge_request, project, author) } + + it_behaves_like 'a system note' + + it "posts the Merge When Build Succeeds system note" do + allow(merge_request).to receive(:ci_commit).and_return(ci_commit) + allow(ci_commit).to receive(:short_sha).and_return('12345678') + + expect(subject.note).to eq "This merge request will be automatically merged when the build for 12345678 succeeds" + end + end + describe '.change_title' do subject { described_class.change_title(noteable, project, author, 'Old title') } From 6f41e3d9caa28b2a654fb775202be632673f2299 Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Wed, 18 Nov 2015 22:11:15 +0100 Subject: [PATCH 049/908] Change forks method to has_many relation --- app/models/project.rb | 11 +++++------ app/services/projects/update_service.rb | 3 +-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index f287e59b6d..2562c441ab 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -97,9 +97,12 @@ class Project < ActiveRecord::Base has_one :gitlab_issue_tracker_service, dependent: :destroy has_one :external_wiki_service, dependent: :destroy - has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" + has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" + has_one :forked_from_project, through: :forked_project_link + + has_many :forked_project_links, foreign_key: "forked_from_project_id" + has_many :forks, through: :forked_project_links, source: :forked_to_project - has_one :forked_from_project, through: :forked_project_link # Merge Requests for target project should be removed with it has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' # Merge requests from source project should be kept when source project was removed @@ -734,10 +737,6 @@ class Project < ActiveRecord::Base forks.count end - def forks - ForkedProjectLink.where(forked_from_project_id: self.id) - end - def find_label(name) labels.find_by(name: name) end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 0d2665f298..67e3429c0b 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -44,8 +44,7 @@ module Projects end def update_forks_visibility_level(new_level) - project.forks.each do |forked_link| - forked_project = forked_link.forked_to_project + project.forks.each do |forked_project| fork_level = forked_project.visibility_level if fork_level > new_level.to_i From e1e67d383e4b56c9e1848aebc175402a71502e82 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 19 Nov 2015 16:18:13 +0100 Subject: [PATCH 050/908] Move Markdown filters and pipelines into folders. --- lib/gitlab/markdown.rb | 80 ++++--- .../markdown/{ => filter}/autolink_filter.rb | 0 .../commit_range_reference_filter.rb | 0 .../{ => filter}/commit_reference_filter.rb | 0 .../markdown/{ => filter}/emoji_filter.rb | 0 .../external_issue_reference_filter.rb | 0 .../{ => filter}/external_link_filter.rb | 0 .../{ => filter}/issue_reference_filter.rb | 0 .../{ => filter}/label_reference_filter.rb | 0 .../markdown/{ => filter}/markdown_filter.rb | 0 .../merge_request_reference_filter.rb | 0 .../markdown/{ => filter}/redactor_filter.rb | 0 .../{ => filter}/reference_gatherer_filter.rb | 0 .../{ => filter}/relative_link_filter.rb | 0 .../{ => filter}/sanitization_filter.rb | 0 .../{ => filter}/snippet_reference_filter.rb | 0 .../{ => filter}/syntax_highlight_filter.rb | 0 .../{ => filter}/table_of_contents_filter.rb | 0 .../markdown/{ => filter}/task_list_filter.rb | 0 .../{ => filter}/upload_link_filter.rb | 0 .../{ => filter}/user_reference_filter.rb | 0 lib/gitlab/markdown/pipeline.rb | 5 + .../{ => pipeline}/asciidoc_pipeline.rb | 0 .../markdown/{ => pipeline}/atom_pipeline.rb | 0 .../{ => pipeline}/description_pipeline.rb | 0 .../markdown/{ => pipeline}/email_pipeline.rb | 0 .../markdown/{ => pipeline}/full_pipeline.rb | 0 .../markdown/{ => pipeline}/gfm_pipeline.rb | 0 .../markdown/{ => pipeline}/note_pipeline.rb | 0 .../{ => pipeline}/plain_markdown_pipeline.rb | 0 .../{ => pipeline}/post_process_pipeline.rb | 0 .../reference_extraction_pipeline.rb | 0 .../{ => pipeline}/single_line_pipeline.rb | 0 lib/gitlab/markdown/reference_filter.rb | 4 + lib/gitlab/reference_extractor.rb | 3 +- .../gitlab/markdown/autolink_filter_spec.rb | 114 ---------- .../commit_range_reference_filter_spec.rb | 145 ------------- .../markdown/commit_reference_filter_spec.rb | 135 ------------ .../markdown/cross_project_reference_spec.rb | 40 ++-- spec/lib/gitlab/markdown/emoji_filter_spec.rb | 95 --------- .../external_issue_reference_filter_spec.rb | 79 ------- .../markdown/external_link_filter_spec.rb | 31 --- .../markdown/filter/autolink_filter_spec.rb | 112 ++++++++++ .../commit_range_reference_filter_spec.rb | 143 +++++++++++++ .../filter/commit_reference_filter_spec.rb | 133 ++++++++++++ .../markdown/filter/emoji_filter_spec.rb | 98 +++++++++ .../external_issue_reference_filter_spec.rb | 77 +++++++ .../filter/external_link_filter_spec.rb | 29 +++ .../filter/issue_reference_filter_spec.rb | 137 ++++++++++++ .../filter/label_reference_filter_spec.rb | 142 +++++++++++++ .../merge_request_reference_filter_spec.rb | 118 +++++++++++ .../markdown/filter/redactor_filter_spec.rb | 89 ++++++++ .../filter/reference_gatherer_filter_spec.rb | 87 ++++++++ .../filter/relative_link_filter_spec.rb | 147 +++++++++++++ .../filter/sanitization_filter_spec.rb | 197 +++++++++++++++++ .../filter/snippet_reference_filter_spec.rb | 116 ++++++++++ .../filter/syntax_highlight_filter_spec.rb | 17 ++ .../filter/table_of_contents_filter_spec.rb | 97 +++++++++ .../markdown/filter/task_list_filter_spec.rb | 10 + .../filter/upload_link_filter_spec.rb | 73 +++++++ .../filter/user_reference_filter_spec.rb | 120 +++++++++++ .../markdown/issue_reference_filter_spec.rb | 139 ------------ .../markdown/label_reference_filter_spec.rb | 144 ------------- .../merge_request_reference_filter_spec.rb | 120 ----------- .../gitlab/markdown/redactor_filter_spec.rb | 91 -------- .../reference_gatherer_filter_spec.rb | 89 -------- .../markdown/relative_link_filter_spec.rb | 149 ------------- .../markdown/sanitization_filter_spec.rb | 199 ------------------ .../markdown/snippet_reference_filter_spec.rb | 118 ----------- .../markdown/syntax_highlight_filter_spec.rb | 19 -- .../markdown/table_of_contents_filter_spec.rb | 99 --------- .../gitlab/markdown/task_list_filter_spec.rb | 12 -- .../markdown/upload_link_filter_spec.rb | 75 ------- .../markdown/user_reference_filter_spec.rb | 122 ----------- 74 files changed, 2007 insertions(+), 2042 deletions(-) rename lib/gitlab/markdown/{ => filter}/autolink_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/commit_range_reference_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/commit_reference_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/emoji_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/external_issue_reference_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/external_link_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/issue_reference_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/label_reference_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/markdown_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/merge_request_reference_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/redactor_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/reference_gatherer_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/relative_link_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/sanitization_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/snippet_reference_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/syntax_highlight_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/table_of_contents_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/task_list_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/upload_link_filter.rb (100%) rename lib/gitlab/markdown/{ => filter}/user_reference_filter.rb (100%) rename lib/gitlab/markdown/{ => pipeline}/asciidoc_pipeline.rb (100%) rename lib/gitlab/markdown/{ => pipeline}/atom_pipeline.rb (100%) rename lib/gitlab/markdown/{ => pipeline}/description_pipeline.rb (100%) rename lib/gitlab/markdown/{ => pipeline}/email_pipeline.rb (100%) rename lib/gitlab/markdown/{ => pipeline}/full_pipeline.rb (100%) rename lib/gitlab/markdown/{ => pipeline}/gfm_pipeline.rb (100%) rename lib/gitlab/markdown/{ => pipeline}/note_pipeline.rb (100%) rename lib/gitlab/markdown/{ => pipeline}/plain_markdown_pipeline.rb (100%) rename lib/gitlab/markdown/{ => pipeline}/post_process_pipeline.rb (100%) rename lib/gitlab/markdown/{ => pipeline}/reference_extraction_pipeline.rb (100%) rename lib/gitlab/markdown/{ => pipeline}/single_line_pipeline.rb (100%) delete mode 100644 spec/lib/gitlab/markdown/autolink_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/commit_reference_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/emoji_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/external_link_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/autolink_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/commit_range_reference_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/commit_reference_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/emoji_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/external_issue_reference_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/external_link_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/issue_reference_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/label_reference_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/merge_request_reference_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/redactor_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/reference_gatherer_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/relative_link_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/sanitization_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/snippet_reference_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/syntax_highlight_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/table_of_contents_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/task_list_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/upload_link_filter_spec.rb create mode 100644 spec/lib/gitlab/markdown/filter/user_reference_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/issue_reference_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/label_reference_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/redactor_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/relative_link_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/sanitization_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/task_list_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/upload_link_filter_spec.rb delete mode 100644 spec/lib/gitlab/markdown/user_reference_filter_spec.rb diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 969607558e..f4e2cefca5 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -33,7 +33,7 @@ module Gitlab end def self.render_result(text, context = {}) - pipeline_by_name(context[:pipeline]).call(text, context) + Pipeline[context[:pipeline]].call(text, context) end # Perform post-processing on an HTML String @@ -50,11 +50,9 @@ module Gitlab # # Returns an HTML-safe String def self.post_process(html, context) - pipeline = pipeline_by_name(context[:pipeline]) - context = pipeline.transform_context(context) - - pipeline = pipeline_by_name(:post_process) + context = Pipeline[context[:pipeline]].transform_context(context) + pipeline = Pipeline[:post_process] if context[:xhtml] pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML) else @@ -66,6 +64,7 @@ module Gitlab def self.cacheless_render(text, context = {}) result = render_result(text, context) + output = result[:output] if output.respond_to?(:to_html) output.to_html @@ -76,48 +75,41 @@ module Gitlab def self.full_cache_key(cache_key, pipeline_name) return unless cache_key - pipeline_name ||= :full - ["markdown", *cache_key, pipeline_name] - end - - def self.pipeline_by_name(pipeline_name) - pipeline_name ||= :full - const_get("#{pipeline_name.to_s.camelize}Pipeline") + ["markdown", *cache_key, pipeline_name || :full] end # Provide autoload paths for filters to prevent a circular dependency error - autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter' - autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter' - autoload :CommitReferenceFilter, 'gitlab/markdown/commit_reference_filter' - autoload :EmojiFilter, 'gitlab/markdown/emoji_filter' - autoload :ExternalIssueReferenceFilter, 'gitlab/markdown/external_issue_reference_filter' - autoload :ExternalLinkFilter, 'gitlab/markdown/external_link_filter' - autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter' - autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter' - autoload :MarkdownFilter, 'gitlab/markdown/markdown_filter' - autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter' - autoload :RedactorFilter, 'gitlab/markdown/redactor_filter' - autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter' - autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter' - autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter' - autoload :SyntaxHighlightFilter, 'gitlab/markdown/syntax_highlight_filter' - autoload :TableOfContentsFilter, 'gitlab/markdown/table_of_contents_filter' - autoload :TaskListFilter, 'gitlab/markdown/task_list_filter' - autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter' - autoload :UploadLinkFilter, 'gitlab/markdown/upload_link_filter' + autoload :AutolinkFilter, 'gitlab/markdown/filter/autolink_filter' + autoload :CommitRangeReferenceFilter, 'gitlab/markdown/filter/commit_range_reference_filter' + autoload :CommitReferenceFilter, 'gitlab/markdown/filter/commit_reference_filter' + autoload :EmojiFilter, 'gitlab/markdown/filter/emoji_filter' + autoload :ExternalIssueReferenceFilter, 'gitlab/markdown/filter/external_issue_reference_filter' + autoload :ExternalLinkFilter, 'gitlab/markdown/filter/external_link_filter' + autoload :IssueReferenceFilter, 'gitlab/markdown/filter/issue_reference_filter' + autoload :LabelReferenceFilter, 'gitlab/markdown/filter/label_reference_filter' + autoload :MarkdownFilter, 'gitlab/markdown/filter/markdown_filter' + autoload :MergeRequestReferenceFilter, 'gitlab/markdown/filter/merge_request_reference_filter' + autoload :RedactorFilter, 'gitlab/markdown/filter/redactor_filter' + autoload :ReferenceGathererFilter, 'gitlab/markdown/filter/reference_gatherer_filter' + autoload :RelativeLinkFilter, 'gitlab/markdown/filter/relative_link_filter' + autoload :SanitizationFilter, 'gitlab/markdown/filter/sanitization_filter' + autoload :SnippetReferenceFilter, 'gitlab/markdown/filter/snippet_reference_filter' + autoload :SyntaxHighlightFilter, 'gitlab/markdown/filter/syntax_highlight_filter' + autoload :TableOfContentsFilter, 'gitlab/markdown/filter/table_of_contents_filter' + autoload :TaskListFilter, 'gitlab/markdown/filter/task_list_filter' + autoload :UserReferenceFilter, 'gitlab/markdown/filter/user_reference_filter' + autoload :UploadLinkFilter, 'gitlab/markdown/filter/upload_link_filter' - autoload :AsciidocPipeline, 'gitlab/markdown/asciidoc_pipeline' - autoload :AtomPipeline, 'gitlab/markdown/atom_pipeline' - autoload :CombinedPipeline, 'gitlab/markdown/combined_pipeline' - autoload :DescriptionPipeline, 'gitlab/markdown/description_pipeline' - autoload :EmailPipeline, 'gitlab/markdown/email_pipeline' - autoload :FullPipeline, 'gitlab/markdown/full_pipeline' - autoload :GfmPipeline, 'gitlab/markdown/gfm_pipeline' - autoload :NotePipeline, 'gitlab/markdown/note_pipeline' - autoload :Pipeline, 'gitlab/markdown/pipeline' - autoload :PlainMarkdownPipeline, 'gitlab/markdown/plain_markdown_pipeline' - autoload :PostProcessPipeline, 'gitlab/markdown/post_process_pipeline' - autoload :ReferenceExtractionPipeline, 'gitlab/markdown/reference_extraction_pipeline' - autoload :SingleLinePipeline, 'gitlab/markdown/single_line_pipeline' + autoload :AsciidocPipeline, 'gitlab/markdown/pipeline/asciidoc_pipeline' + autoload :AtomPipeline, 'gitlab/markdown/pipeline/atom_pipeline' + autoload :DescriptionPipeline, 'gitlab/markdown/pipeline/description_pipeline' + autoload :EmailPipeline, 'gitlab/markdown/pipeline/email_pipeline' + autoload :FullPipeline, 'gitlab/markdown/pipeline/full_pipeline' + autoload :GfmPipeline, 'gitlab/markdown/pipeline/gfm_pipeline' + autoload :NotePipeline, 'gitlab/markdown/pipeline/note_pipeline' + autoload :PlainMarkdownPipeline, 'gitlab/markdown/pipeline/plain_markdown_pipeline' + autoload :PostProcessPipeline, 'gitlab/markdown/pipeline/post_process_pipeline' + autoload :ReferenceExtractionPipeline, 'gitlab/markdown/pipeline/reference_extraction_pipeline' + autoload :SingleLinePipeline, 'gitlab/markdown/pipeline/single_line_pipeline' end end diff --git a/lib/gitlab/markdown/autolink_filter.rb b/lib/gitlab/markdown/filter/autolink_filter.rb similarity index 100% rename from lib/gitlab/markdown/autolink_filter.rb rename to lib/gitlab/markdown/filter/autolink_filter.rb diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/filter/commit_range_reference_filter.rb similarity index 100% rename from lib/gitlab/markdown/commit_range_reference_filter.rb rename to lib/gitlab/markdown/filter/commit_range_reference_filter.rb diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/filter/commit_reference_filter.rb similarity index 100% rename from lib/gitlab/markdown/commit_reference_filter.rb rename to lib/gitlab/markdown/filter/commit_reference_filter.rb diff --git a/lib/gitlab/markdown/emoji_filter.rb b/lib/gitlab/markdown/filter/emoji_filter.rb similarity index 100% rename from lib/gitlab/markdown/emoji_filter.rb rename to lib/gitlab/markdown/filter/emoji_filter.rb diff --git a/lib/gitlab/markdown/external_issue_reference_filter.rb b/lib/gitlab/markdown/filter/external_issue_reference_filter.rb similarity index 100% rename from lib/gitlab/markdown/external_issue_reference_filter.rb rename to lib/gitlab/markdown/filter/external_issue_reference_filter.rb diff --git a/lib/gitlab/markdown/external_link_filter.rb b/lib/gitlab/markdown/filter/external_link_filter.rb similarity index 100% rename from lib/gitlab/markdown/external_link_filter.rb rename to lib/gitlab/markdown/filter/external_link_filter.rb diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/filter/issue_reference_filter.rb similarity index 100% rename from lib/gitlab/markdown/issue_reference_filter.rb rename to lib/gitlab/markdown/filter/issue_reference_filter.rb diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/filter/label_reference_filter.rb similarity index 100% rename from lib/gitlab/markdown/label_reference_filter.rb rename to lib/gitlab/markdown/filter/label_reference_filter.rb diff --git a/lib/gitlab/markdown/markdown_filter.rb b/lib/gitlab/markdown/filter/markdown_filter.rb similarity index 100% rename from lib/gitlab/markdown/markdown_filter.rb rename to lib/gitlab/markdown/filter/markdown_filter.rb diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/filter/merge_request_reference_filter.rb similarity index 100% rename from lib/gitlab/markdown/merge_request_reference_filter.rb rename to lib/gitlab/markdown/filter/merge_request_reference_filter.rb diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/gitlab/markdown/filter/redactor_filter.rb similarity index 100% rename from lib/gitlab/markdown/redactor_filter.rb rename to lib/gitlab/markdown/filter/redactor_filter.rb diff --git a/lib/gitlab/markdown/reference_gatherer_filter.rb b/lib/gitlab/markdown/filter/reference_gatherer_filter.rb similarity index 100% rename from lib/gitlab/markdown/reference_gatherer_filter.rb rename to lib/gitlab/markdown/filter/reference_gatherer_filter.rb diff --git a/lib/gitlab/markdown/relative_link_filter.rb b/lib/gitlab/markdown/filter/relative_link_filter.rb similarity index 100% rename from lib/gitlab/markdown/relative_link_filter.rb rename to lib/gitlab/markdown/filter/relative_link_filter.rb diff --git a/lib/gitlab/markdown/sanitization_filter.rb b/lib/gitlab/markdown/filter/sanitization_filter.rb similarity index 100% rename from lib/gitlab/markdown/sanitization_filter.rb rename to lib/gitlab/markdown/filter/sanitization_filter.rb diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/filter/snippet_reference_filter.rb similarity index 100% rename from lib/gitlab/markdown/snippet_reference_filter.rb rename to lib/gitlab/markdown/filter/snippet_reference_filter.rb diff --git a/lib/gitlab/markdown/syntax_highlight_filter.rb b/lib/gitlab/markdown/filter/syntax_highlight_filter.rb similarity index 100% rename from lib/gitlab/markdown/syntax_highlight_filter.rb rename to lib/gitlab/markdown/filter/syntax_highlight_filter.rb diff --git a/lib/gitlab/markdown/table_of_contents_filter.rb b/lib/gitlab/markdown/filter/table_of_contents_filter.rb similarity index 100% rename from lib/gitlab/markdown/table_of_contents_filter.rb rename to lib/gitlab/markdown/filter/table_of_contents_filter.rb diff --git a/lib/gitlab/markdown/task_list_filter.rb b/lib/gitlab/markdown/filter/task_list_filter.rb similarity index 100% rename from lib/gitlab/markdown/task_list_filter.rb rename to lib/gitlab/markdown/filter/task_list_filter.rb diff --git a/lib/gitlab/markdown/upload_link_filter.rb b/lib/gitlab/markdown/filter/upload_link_filter.rb similarity index 100% rename from lib/gitlab/markdown/upload_link_filter.rb rename to lib/gitlab/markdown/filter/upload_link_filter.rb diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/filter/user_reference_filter.rb similarity index 100% rename from lib/gitlab/markdown/user_reference_filter.rb rename to lib/gitlab/markdown/filter/user_reference_filter.rb diff --git a/lib/gitlab/markdown/pipeline.rb b/lib/gitlab/markdown/pipeline.rb index 3c0b676a07..d683756f95 100644 --- a/lib/gitlab/markdown/pipeline.rb +++ b/lib/gitlab/markdown/pipeline.rb @@ -3,6 +3,11 @@ require 'gitlab/markdown' module Gitlab module Markdown class Pipeline + def self.[](name) + name ||= :full + Markdown.const_get("#{name.to_s.camelize}Pipeline") + end + def self.filters [] end diff --git a/lib/gitlab/markdown/asciidoc_pipeline.rb b/lib/gitlab/markdown/pipeline/asciidoc_pipeline.rb similarity index 100% rename from lib/gitlab/markdown/asciidoc_pipeline.rb rename to lib/gitlab/markdown/pipeline/asciidoc_pipeline.rb diff --git a/lib/gitlab/markdown/atom_pipeline.rb b/lib/gitlab/markdown/pipeline/atom_pipeline.rb similarity index 100% rename from lib/gitlab/markdown/atom_pipeline.rb rename to lib/gitlab/markdown/pipeline/atom_pipeline.rb diff --git a/lib/gitlab/markdown/description_pipeline.rb b/lib/gitlab/markdown/pipeline/description_pipeline.rb similarity index 100% rename from lib/gitlab/markdown/description_pipeline.rb rename to lib/gitlab/markdown/pipeline/description_pipeline.rb diff --git a/lib/gitlab/markdown/email_pipeline.rb b/lib/gitlab/markdown/pipeline/email_pipeline.rb similarity index 100% rename from lib/gitlab/markdown/email_pipeline.rb rename to lib/gitlab/markdown/pipeline/email_pipeline.rb diff --git a/lib/gitlab/markdown/full_pipeline.rb b/lib/gitlab/markdown/pipeline/full_pipeline.rb similarity index 100% rename from lib/gitlab/markdown/full_pipeline.rb rename to lib/gitlab/markdown/pipeline/full_pipeline.rb diff --git a/lib/gitlab/markdown/gfm_pipeline.rb b/lib/gitlab/markdown/pipeline/gfm_pipeline.rb similarity index 100% rename from lib/gitlab/markdown/gfm_pipeline.rb rename to lib/gitlab/markdown/pipeline/gfm_pipeline.rb diff --git a/lib/gitlab/markdown/note_pipeline.rb b/lib/gitlab/markdown/pipeline/note_pipeline.rb similarity index 100% rename from lib/gitlab/markdown/note_pipeline.rb rename to lib/gitlab/markdown/pipeline/note_pipeline.rb diff --git a/lib/gitlab/markdown/plain_markdown_pipeline.rb b/lib/gitlab/markdown/pipeline/plain_markdown_pipeline.rb similarity index 100% rename from lib/gitlab/markdown/plain_markdown_pipeline.rb rename to lib/gitlab/markdown/pipeline/plain_markdown_pipeline.rb diff --git a/lib/gitlab/markdown/post_process_pipeline.rb b/lib/gitlab/markdown/pipeline/post_process_pipeline.rb similarity index 100% rename from lib/gitlab/markdown/post_process_pipeline.rb rename to lib/gitlab/markdown/pipeline/post_process_pipeline.rb diff --git a/lib/gitlab/markdown/reference_extraction_pipeline.rb b/lib/gitlab/markdown/pipeline/reference_extraction_pipeline.rb similarity index 100% rename from lib/gitlab/markdown/reference_extraction_pipeline.rb rename to lib/gitlab/markdown/pipeline/reference_extraction_pipeline.rb diff --git a/lib/gitlab/markdown/single_line_pipeline.rb b/lib/gitlab/markdown/pipeline/single_line_pipeline.rb similarity index 100% rename from lib/gitlab/markdown/single_line_pipeline.rb rename to lib/gitlab/markdown/pipeline/single_line_pipeline.rb diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index a4c560f578..22fe4440be 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -29,6 +29,10 @@ module Gitlab end end + def self.[](name) + Markdown.const_get("#{name.to_s.camelize}ReferenceFilter") + end + def self.user_can_reference?(user, node, context) if node.has_attribute?('data-project') project_id = node.attr('data-project').to_i diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index e5cafebdbc..e83167fa7d 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -33,8 +33,7 @@ module Gitlab # # Returns the results Array for the requested filter type def pipeline_result(filter_type) - klass = "#{filter_type.to_s.camelize}ReferenceFilter" - filter = Gitlab::Markdown.const_get(klass) + filter = Gitlab::Markdown::ReferenceFilter[filter_type] context = { pipeline: :reference_extraction, diff --git a/spec/lib/gitlab/markdown/autolink_filter_spec.rb b/spec/lib/gitlab/markdown/autolink_filter_spec.rb deleted file mode 100644 index 26332ba521..0000000000 --- a/spec/lib/gitlab/markdown/autolink_filter_spec.rb +++ /dev/null @@ -1,114 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe AutolinkFilter do - include FilterSpecHelper - - let(:link) { 'http://about.gitlab.com/' } - - it 'does nothing when :autolink is false' do - exp = act = link - expect(filter(act, autolink: false).to_html).to eq exp - end - - it 'does nothing with non-link text' do - exp = act = 'This text contains no links to autolink' - expect(filter(act).to_html).to eq exp - end - - context 'Rinku schemes' do - it 'autolinks http' do - doc = filter("See #{link}") - expect(doc.at_css('a').text).to eq link - expect(doc.at_css('a')['href']).to eq link - end - - it 'autolinks https' do - link = 'https://google.com/' - doc = filter("See #{link}") - - expect(doc.at_css('a').text).to eq link - expect(doc.at_css('a')['href']).to eq link - end - - it 'autolinks ftp' do - link = 'ftp://ftp.us.debian.org/debian/' - doc = filter("See #{link}") - - expect(doc.at_css('a').text).to eq link - expect(doc.at_css('a')['href']).to eq link - end - - it 'autolinks short URLs' do - link = 'http://localhost:3000/' - doc = filter("See #{link}") - - expect(doc.at_css('a').text).to eq link - expect(doc.at_css('a')['href']).to eq link - end - - it 'accepts link_attr options' do - doc = filter("See #{link}", link_attr: { class: 'custom' }) - - expect(doc.at_css('a')['class']).to eq 'custom' - end - - described_class::IGNORE_PARENTS.each do |elem| - it "ignores valid links contained inside '#{elem}' element" do - exp = act = "<#{elem}>See #{link}" - expect(filter(act).to_html).to eq exp - end - end - end - - context 'other schemes' do - let(:link) { 'foo://bar.baz/' } - - it 'autolinks smb' do - link = 'smb:///Volumes/shared/foo.pdf' - doc = filter("See #{link}") - - expect(doc.at_css('a').text).to eq link - expect(doc.at_css('a')['href']).to eq link - end - - it 'autolinks irc' do - link = 'irc://irc.freenode.net/git' - doc = filter("See #{link}") - - expect(doc.at_css('a').text).to eq link - expect(doc.at_css('a')['href']).to eq link - end - - it 'does not include trailing punctuation' do - doc = filter("See #{link}.") - expect(doc.at_css('a').text).to eq link - - doc = filter("See #{link}, ok?") - expect(doc.at_css('a').text).to eq link - - doc = filter("See #{link}...") - expect(doc.at_css('a').text).to eq link - end - - it 'does not include trailing HTML entities' do - doc = filter("See <<<#{link}>>>") - - expect(doc.at_css('a')['href']).to eq link - expect(doc.text).to eq "See <<<#{link}>>>" - end - - it 'accepts link_attr options' do - doc = filter("See #{link}", link_attr: { class: 'custom' }) - expect(doc.at_css('a')['class']).to eq 'custom' - end - - described_class::IGNORE_PARENTS.each do |elem| - it "ignores valid links contained inside '#{elem}' element" do - exp = act = "<#{elem}>See #{link}" - expect(filter(act).to_html).to eq exp - end - end - end - end -end diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb deleted file mode 100644 index e5b8d723fe..0000000000 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ /dev/null @@ -1,145 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe CommitRangeReferenceFilter do - include FilterSpecHelper - - let(:project) { create(:project, :public) } - let(:commit1) { project.commit } - let(:commit2) { project.commit("HEAD~2") } - - let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}") } - let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}") } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Commit Range #{range.to_reference}" - expect(filter(act).to_html).to eq exp - end - end - - context 'internal reference' do - let(:reference) { range.to_reference } - let(:reference2) { range2.to_reference } - - it 'links to a valid two-dot reference' do - doc = filter("See #{reference2}") - - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param) - end - - it 'links to a valid three-dot reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param) - end - - it 'links to a valid short ID' do - reference = "#{commit1.short_id}...#{commit2.id}" - reference2 = "#{commit1.id}...#{commit2.short_id}" - - exp = commit1.short_id + '...' + commit2.short_id - - expect(filter("See #{reference}").css('a').first.text).to eq exp - expect(filter("See #{reference2}").css('a').first.text).to eq exp - end - - it 'links with adjacent text' do - doc = filter("See (#{reference}.)") - - exp = Regexp.escape(range.to_s) - expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) - end - - it 'ignores invalid commit IDs' do - exp = act = "See #{commit1.id.reverse}...#{commit2.id}" - - expect(project).to receive(:valid_repo?).and_return(true) - expect(project.repository).to receive(:commit).with(commit1.id.reverse) - expect(filter(act).to_html).to eq exp - end - - it 'includes a title attribute' do - doc = filter("See #{reference}") - expect(doc.css('a').first.attr('title')).to eq range.reference_title - end - - it 'includes default classes' do - doc = filter("See #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range' - end - - it 'includes a data-project attribute' do - doc = filter("See #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-project') - expect(link.attr('data-project')).to eq project.id.to_s - end - - it 'includes a data-commit-range attribute' do - doc = filter("See #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-commit-range') - expect(link.attr('data-commit-range')).to eq range.to_reference - end - - it 'supports an :only_path option' do - doc = filter("See #{reference}", only_path: true) - link = doc.css('a').first.attr('href') - - expect(link).not_to match %r(https?://) - expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit_range]).not_to be_empty - end - end - - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:reference) { range.to_reference(project) } - - before do - range.project = project2 - end - - it 'links to a valid reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) - end - - it 'links with adjacent text' do - doc = filter("Fixed (#{reference}.)") - - exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}") - expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) - end - - it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" - expect(filter(act).to_html).to eq exp - - exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" - expect(filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit_range]).not_to be_empty - end - end - end -end diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb deleted file mode 100644 index d080efbf3d..0000000000 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ /dev/null @@ -1,135 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe CommitReferenceFilter do - include FilterSpecHelper - - let(:project) { create(:project, :public) } - let(:commit) { project.commit } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Commit #{commit.id}" - expect(filter(act).to_html).to eq exp - end - end - - context 'internal reference' do - let(:reference) { commit.id } - - # Let's test a variety of commit SHA sizes just to be paranoid - [6, 8, 12, 18, 20, 32, 40].each do |size| - it "links to a valid reference of #{size} characters" do - doc = filter("See #{reference[0...size]}") - - expect(doc.css('a').first.text).to eq commit.short_id - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_commit_url(project.namespace, project, reference) - end - end - - it 'always uses the short ID as the link text' do - doc = filter("See #{commit.id}") - expect(doc.text).to eq "See #{commit.short_id}" - - doc = filter("See #{commit.id[0...6]}") - expect(doc.text).to eq "See #{commit.short_id}" - end - - it 'links with adjacent text' do - doc = filter("See (#{reference}.)") - expect(doc.to_html).to match(/\(#{commit.short_id}<\/a>\.\)/) - end - - it 'ignores invalid commit IDs' do - invalid = invalidate_reference(reference) - exp = act = "See #{invalid}" - - expect(project).to receive(:valid_repo?).and_return(true) - expect(project.repository).to receive(:commit).with(invalid) - expect(filter(act).to_html).to eq exp - end - - it 'includes a title attribute' do - doc = filter("See #{reference}") - expect(doc.css('a').first.attr('title')).to eq commit.link_title - end - - it 'escapes the title attribute' do - allow_any_instance_of(Commit).to receive(:title).and_return(%{">whatever#{exp}@#{commit.short_id}<\/a>\.\)/) - end - - it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Committed #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit]).not_to be_empty - end - end - end -end diff --git a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb index 8d4f9e403a..f86d52b9d0 100644 --- a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb +++ b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb @@ -1,35 +1,33 @@ require 'spec_helper' -module Gitlab::Markdown - describe CrossProjectReference do - include described_class +describe Gitlab::Markdown::CrossProjectReference do + include described_class - describe '#project_from_ref' do - context 'when no project was referenced' do - it 'returns the project from context' do - project = double + describe '#project_from_ref' do + context 'when no project was referenced' do + it 'returns the project from context' do + project = double - allow(self).to receive(:context).and_return({ project: project }) + allow(self).to receive(:context).and_return({ project: project }) - expect(project_from_ref(nil)).to eq project - end + expect(project_from_ref(nil)).to eq project end + end - context 'when referenced project does not exist' do - it 'returns nil' do - expect(project_from_ref('invalid/reference')).to be_nil - end + context 'when referenced project does not exist' do + it 'returns nil' do + expect(project_from_ref('invalid/reference')).to be_nil end + end - context 'when referenced project exists' do - it 'returns the referenced project' do - project2 = double('referenced project') + context 'when referenced project exists' do + it 'returns the referenced project' do + project2 = double('referenced project') - expect(Project).to receive(:find_with_namespace). - with('cross/reference').and_return(project2) + expect(Project).to receive(:find_with_namespace). + with('cross/reference').and_return(project2) - expect(project_from_ref('cross/reference')).to eq project2 - end + expect(project_from_ref('cross/reference')).to eq project2 end end end diff --git a/spec/lib/gitlab/markdown/emoji_filter_spec.rb b/spec/lib/gitlab/markdown/emoji_filter_spec.rb deleted file mode 100644 index 11efd9bb4c..0000000000 --- a/spec/lib/gitlab/markdown/emoji_filter_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe EmojiFilter do - include FilterSpecHelper - - before do - ActionController::Base.asset_host = 'https://foo.com' - end - - it 'replaces supported emoji' do - doc = filter('

:heart:

') - expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png' - end - - it 'ignores unsupported emoji' do - exp = act = '

:foo:

' - doc = filter(act) - expect(doc.to_html).to match Regexp.escape(exp) - end - - it 'correctly encodes the URL' do - doc = filter('

:+1:

') - expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/1F44D.png' - end - - it 'matches at the start of a string' do - doc = filter(':+1:') - expect(doc.css('img').size).to eq 1 - end - - it 'matches at the end of a string' do - doc = filter('This gets a :-1:') - expect(doc.css('img').size).to eq 1 - end - - it 'matches with adjacent text' do - doc = filter('+1 (:+1:)') - expect(doc.css('img').size).to eq 1 - end - - it 'matches multiple emoji in a row' do - doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:') - expect(doc.css('img').size).to eq 3 - end - - it 'has a title attribute' do - doc = filter(':-1:') - expect(doc.css('img').first.attr('title')).to eq ':-1:' - end - - it 'has an alt attribute' do - doc = filter(':-1:') - expect(doc.css('img').first.attr('alt')).to eq ':-1:' - end - - it 'has an align attribute' do - doc = filter(':8ball:') - expect(doc.css('img').first.attr('align')).to eq 'absmiddle' - end - - it 'has an emoji class' do - doc = filter(':cat:') - expect(doc.css('img').first.attr('class')).to eq 'emoji' - end - - it 'has height and width attributes' do - doc = filter(':dog:') - img = doc.css('img').first - - expect(img.attr('width')).to eq '20' - expect(img.attr('height')).to eq '20' - end - - it 'keeps whitespace intact' do - doc = filter('This deserves a :+1:, big time.') - - expect(doc.to_html).to match(/^This deserves a , big time\.\z/) - end - - it 'uses a custom asset_root context' do - root = Gitlab.config.gitlab.url + 'gitlab/root' - - doc = filter(':smile:', asset_root: root) - expect(doc.css('img').first.attr('src')).to start_with(root) - end - - it 'uses a custom asset_host context' do - ActionController::Base.asset_host = 'https://cdn.example.com' - - doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?') - expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com') - end - end -end diff --git a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb deleted file mode 100644 index d8c2970b6b..0000000000 --- a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe ExternalIssueReferenceFilter do - include FilterSpecHelper - - def helper - IssuesHelper - end - - let(:project) { create(:jira_project) } - - context 'JIRA issue references' do - let(:issue) { ExternalIssue.new('JIRA-123', project) } - let(:reference) { issue.to_reference } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Issue #{reference}" - expect(filter(act).to_html).to eq exp - end - end - - it 'ignores valid references when using default tracker' do - expect(project).to receive(:default_issues_tracker?).and_return(true) - - exp = act = "Issue #{reference}" - expect(filter(act).to_html).to eq exp - end - - it 'links to a valid reference' do - doc = filter("Issue #{reference}") - expect(doc.css('a').first.attr('href')) - .to eq helper.url_for_issue(reference, project) - end - - it 'links to the external tracker' do - doc = filter("Issue #{reference}") - link = doc.css('a').first.attr('href') - - expect(link).to eq "http://jira.example/browse/#{reference}" - end - - it 'links with adjacent text' do - doc = filter("Issue (#{reference}.)") - expect(doc.to_html).to match(/\(#{reference}<\/a>\.\)/) - end - - it 'includes a title attribute' do - doc = filter("Issue #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker" - end - - it 'escapes the title attribute' do - allow(project.external_issue_tracker).to receive(:title). - and_return(%{">
whateverIgnore Me) - expect(filter(act).to_html).to eq exp - end - - it 'ignores non-HTTP(S) links' do - exp = act = %q(IRC) - expect(filter(act).to_html).to eq exp - end - - it 'skips internal links' do - internal = Gitlab.config.gitlab.url - exp = act = %Q(Login) - expect(filter(act).to_html).to eq exp - end - - it 'adds rel="nofollow" to external links' do - act = %q(Google) - doc = filter(act) - - expect(doc.at_css('a')).to have_attribute('rel') - expect(doc.at_css('a')['rel']).to eq 'nofollow' - end - end -end diff --git a/spec/lib/gitlab/markdown/filter/autolink_filter_spec.rb b/spec/lib/gitlab/markdown/filter/autolink_filter_spec.rb new file mode 100644 index 0000000000..837bdb56f7 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/autolink_filter_spec.rb @@ -0,0 +1,112 @@ +require 'spec_helper' + +describe Gitlab::Markdown::AutolinkFilter do + include FilterSpecHelper + + let(:link) { 'http://about.gitlab.com/' } + + it 'does nothing when :autolink is false' do + exp = act = link + expect(filter(act, autolink: false).to_html).to eq exp + end + + it 'does nothing with non-link text' do + exp = act = 'This text contains no links to autolink' + expect(filter(act).to_html).to eq exp + end + + context 'Rinku schemes' do + it 'autolinks http' do + doc = filter("See #{link}") + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'autolinks https' do + link = 'https://google.com/' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'autolinks ftp' do + link = 'ftp://ftp.us.debian.org/debian/' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'autolinks short URLs' do + link = 'http://localhost:3000/' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'accepts link_attr options' do + doc = filter("See #{link}", link_attr: { class: 'custom' }) + + expect(doc.at_css('a')['class']).to eq 'custom' + end + + described_class::IGNORE_PARENTS.each do |elem| + it "ignores valid links contained inside '#{elem}' element" do + exp = act = "<#{elem}>See #{link}" + expect(filter(act).to_html).to eq exp + end + end + end + + context 'other schemes' do + let(:link) { 'foo://bar.baz/' } + + it 'autolinks smb' do + link = 'smb:///Volumes/shared/foo.pdf' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'autolinks irc' do + link = 'irc://irc.freenode.net/git' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'does not include trailing punctuation' do + doc = filter("See #{link}.") + expect(doc.at_css('a').text).to eq link + + doc = filter("See #{link}, ok?") + expect(doc.at_css('a').text).to eq link + + doc = filter("See #{link}...") + expect(doc.at_css('a').text).to eq link + end + + it 'does not include trailing HTML entities' do + doc = filter("See <<<#{link}>>>") + + expect(doc.at_css('a')['href']).to eq link + expect(doc.text).to eq "See <<<#{link}>>>" + end + + it 'accepts link_attr options' do + doc = filter("See #{link}", link_attr: { class: 'custom' }) + expect(doc.at_css('a')['class']).to eq 'custom' + end + + described_class::IGNORE_PARENTS.each do |elem| + it "ignores valid links contained inside '#{elem}' element" do + exp = act = "<#{elem}>See #{link}" + expect(filter(act).to_html).to eq exp + end + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/commit_range_reference_filter_spec.rb new file mode 100644 index 0000000000..05def97a9e --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/commit_range_reference_filter_spec.rb @@ -0,0 +1,143 @@ +require 'spec_helper' + +describe Gitlab::Markdown::CommitRangeReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:project, :public) } + let(:commit1) { project.commit } + let(:commit2) { project.commit("HEAD~2") } + + let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}") } + let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}") } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Commit Range #{range.to_reference}" + expect(filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { range.to_reference } + let(:reference2) { range2.to_reference } + + it 'links to a valid two-dot reference' do + doc = filter("See #{reference2}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param) + end + + it 'links to a valid three-dot reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param) + end + + it 'links to a valid short ID' do + reference = "#{commit1.short_id}...#{commit2.id}" + reference2 = "#{commit1.id}...#{commit2.short_id}" + + exp = commit1.short_id + '...' + commit2.short_id + + expect(filter("See #{reference}").css('a').first.text).to eq exp + expect(filter("See #{reference2}").css('a').first.text).to eq exp + end + + it 'links with adjacent text' do + doc = filter("See (#{reference}.)") + + exp = Regexp.escape(range.to_s) + expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs' do + exp = act = "See #{commit1.id.reverse}...#{commit2.id}" + + expect(project).to receive(:valid_repo?).and_return(true) + expect(project.repository).to receive(:commit).with(commit1.id.reverse) + expect(filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = filter("See #{reference}") + expect(doc.css('a').first.attr('title')).to eq range.reference_title + end + + it 'includes default classes' do + doc = filter("See #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range' + end + + it 'includes a data-project attribute' do + doc = filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-commit-range attribute' do + doc = filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-commit-range') + expect(link.attr('data-commit-range')).to eq range.to_reference + end + + it 'supports an :only_path option' do + doc = filter("See #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit_range]).not_to be_empty + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:reference) { range.to_reference(project) } + + before do + range.project = project2 + end + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + + exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}") + expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" + expect(filter(act).to_html).to eq exp + + exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit_range]).not_to be_empty + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/commit_reference_filter_spec.rb new file mode 100644 index 0000000000..0a892a12b9 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/commit_reference_filter_spec.rb @@ -0,0 +1,133 @@ +require 'spec_helper' + +describe Gitlab::Markdown::CommitReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:project, :public) } + let(:commit) { project.commit } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Commit #{commit.id}" + expect(filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { commit.id } + + # Let's test a variety of commit SHA sizes just to be paranoid + [6, 8, 12, 18, 20, 32, 40].each do |size| + it "links to a valid reference of #{size} characters" do + doc = filter("See #{reference[0...size]}") + + expect(doc.css('a').first.text).to eq commit.short_id + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_commit_url(project.namespace, project, reference) + end + end + + it 'always uses the short ID as the link text' do + doc = filter("See #{commit.id}") + expect(doc.text).to eq "See #{commit.short_id}" + + doc = filter("See #{commit.id[0...6]}") + expect(doc.text).to eq "See #{commit.short_id}" + end + + it 'links with adjacent text' do + doc = filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(#{commit.short_id}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs' do + invalid = invalidate_reference(reference) + exp = act = "See #{invalid}" + + expect(project).to receive(:valid_repo?).and_return(true) + expect(project.repository).to receive(:commit).with(invalid) + expect(filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = filter("See #{reference}") + expect(doc.css('a').first.attr('title')).to eq commit.link_title + end + + it 'escapes the title attribute' do + allow_any_instance_of(Commit).to receive(:title).and_return(%{">whatever#{exp}@#{commit.short_id}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Committed #{invalidate_reference(reference)}" + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit]).not_to be_empty + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/emoji_filter_spec.rb b/spec/lib/gitlab/markdown/filter/emoji_filter_spec.rb new file mode 100644 index 0000000000..e0c5622e85 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/emoji_filter_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' + +describe Gitlab::Markdown::EmojiFilter do + include FilterSpecHelper + + before do + @original_asset_host = ActionController::Base.asset_host + ActionController::Base.asset_host = 'https://foo.com' + end + + after do + ActionController::Base.asset_host = @original_asset_host + end + + it 'replaces supported emoji' do + doc = filter('

:heart:

') + expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png' + end + + it 'ignores unsupported emoji' do + exp = act = '

:foo:

' + doc = filter(act) + expect(doc.to_html).to match Regexp.escape(exp) + end + + it 'correctly encodes the URL' do + doc = filter('

:+1:

') + expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/1F44D.png' + end + + it 'matches at the start of a string' do + doc = filter(':+1:') + expect(doc.css('img').size).to eq 1 + end + + it 'matches at the end of a string' do + doc = filter('This gets a :-1:') + expect(doc.css('img').size).to eq 1 + end + + it 'matches with adjacent text' do + doc = filter('+1 (:+1:)') + expect(doc.css('img').size).to eq 1 + end + + it 'matches multiple emoji in a row' do + doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:') + expect(doc.css('img').size).to eq 3 + end + + it 'has a title attribute' do + doc = filter(':-1:') + expect(doc.css('img').first.attr('title')).to eq ':-1:' + end + + it 'has an alt attribute' do + doc = filter(':-1:') + expect(doc.css('img').first.attr('alt')).to eq ':-1:' + end + + it 'has an align attribute' do + doc = filter(':8ball:') + expect(doc.css('img').first.attr('align')).to eq 'absmiddle' + end + + it 'has an emoji class' do + doc = filter(':cat:') + expect(doc.css('img').first.attr('class')).to eq 'emoji' + end + + it 'has height and width attributes' do + doc = filter(':dog:') + img = doc.css('img').first + + expect(img.attr('width')).to eq '20' + expect(img.attr('height')).to eq '20' + end + + it 'keeps whitespace intact' do + doc = filter('This deserves a :+1:, big time.') + + expect(doc.to_html).to match(/^This deserves a , big time\.\z/) + end + + it 'uses a custom asset_root context' do + root = Gitlab.config.gitlab.url + 'gitlab/root' + + doc = filter(':smile:', asset_root: root) + expect(doc.css('img').first.attr('src')).to start_with(root) + end + + it 'uses a custom asset_host context' do + ActionController::Base.asset_host = 'https://cdn.example.com' + + doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?') + expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com') + end +end diff --git a/spec/lib/gitlab/markdown/filter/external_issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/external_issue_reference_filter_spec.rb new file mode 100644 index 0000000000..216fcb66ce --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/external_issue_reference_filter_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe Gitlab::Markdown::ExternalIssueReferenceFilter do + include FilterSpecHelper + + def helper + IssuesHelper + end + + let(:project) { create(:jira_project) } + + context 'JIRA issue references' do + let(:issue) { ExternalIssue.new('JIRA-123', project) } + let(:reference) { issue.to_reference } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Issue #{reference}" + expect(filter(act).to_html).to eq exp + end + end + + it 'ignores valid references when using default tracker' do + expect(project).to receive(:default_issues_tracker?).and_return(true) + + exp = act = "Issue #{reference}" + expect(filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('href')) + .to eq helper.url_for_issue(reference, project) + end + + it 'links to the external tracker' do + doc = filter("Issue #{reference}") + link = doc.css('a').first.attr('href') + + expect(link).to eq "http://jira.example/browse/#{reference}" + end + + it 'links with adjacent text' do + doc = filter("Issue (#{reference}.)") + expect(doc.to_html).to match(/\(#{reference}<\/a>\.\)/) + end + + it 'includes a title attribute' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker" + end + + it 'escapes the title attribute' do + allow(project.external_issue_tracker).to receive(:title). + and_return(%{">
whateverIgnore Me) + expect(filter(act).to_html).to eq exp + end + + it 'ignores non-HTTP(S) links' do + exp = act = %q(IRC) + expect(filter(act).to_html).to eq exp + end + + it 'skips internal links' do + internal = Gitlab.config.gitlab.url + exp = act = %Q(Login) + expect(filter(act).to_html).to eq exp + end + + it 'adds rel="nofollow" to external links' do + act = %q(Google) + doc = filter(act) + + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to eq 'nofollow' + end +end diff --git a/spec/lib/gitlab/markdown/filter/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/issue_reference_filter_spec.rb new file mode 100644 index 0000000000..66eac4f2e3 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/issue_reference_filter_spec.rb @@ -0,0 +1,137 @@ +require 'spec_helper' + +describe Gitlab::Markdown::IssueReferenceFilter do + include FilterSpecHelper + + def helper + IssuesHelper + end + + let(:project) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project) } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Issue #{issue.to_reference}" + expect(filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { issue.to_reference } + + it 'ignores valid references when using non-default tracker' do + expect(project).to receive(:get_issue).with(issue.iid).and_return(nil) + + exp = act = "Issue #{reference}" + expect(filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = filter("Fixed #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project) + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid issue IDs' do + invalid = invalidate_reference(reference) + exp = act = "Fixed #{invalid}" + + expect(filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}" + end + + it 'escapes the title attribute' do + issue.update_attribute(:title, %{">whatever#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid issue IDs on the referenced project' do + exp = act = "Fixed #{invalidate_reference(reference)}" + + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/label_reference_filter_spec.rb new file mode 100644 index 0000000000..5403e811aa --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/label_reference_filter_spec.rb @@ -0,0 +1,142 @@ +require 'spec_helper' +require 'html/pipeline' + +describe Gitlab::Markdown::LabelReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:empty_project, :public) } + let(:label) { create(:label, project: project) } + let(:reference) { label.to_reference } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Label #{reference}" + expect(filter(act).to_html).to eq exp + end + end + + it 'includes default classes' do + doc = filter("Label #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label' + end + + it 'includes a data-project attribute' do + doc = filter("Label #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-label attribute' do + doc = filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-label') + expect(link.attr('data-label')).to eq label.id.to_s + end + + it 'supports an :only_path context' do + doc = filter("Label #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Label #{reference}") + expect(result[:references][:label]).to eq [label] + end + + describe 'label span element' do + it 'includes default classes' do + doc = filter("Label #{reference}") + expect(doc.css('a span').first.attr('class')).to eq 'label color-label' + end + + it 'includes a style attribute' do + doc = filter("Label #{reference}") + expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/) + end + end + + context 'Integer-based references' do + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + end + + it 'links with adjacent text' do + doc = filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(#{label.name}\.\))) + end + + it 'ignores invalid label IDs' do + exp = act = "Label #{invalidate_reference(reference)}" + + expect(filter(act).to_html).to eq exp + end + end + + context 'String-based single-word references' do + let(:label) { create(:label, name: 'gfm', project: project) } + let(:reference) { "#{Label.reference_prefix}#{label.name}" } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.text).to eq 'See gfm' + end + + it 'links with adjacent text' do + doc = filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(#{label.name}\.\))) + end + + it 'ignores invalid label names' do + exp = act = "Label #{Label.reference_prefix}#{label.name.reverse}" + + expect(filter(act).to_html).to eq exp + end + end + + context 'String-based multi-word references in quotes' do + let(:label) { create(:label, name: 'gfm references', project: project) } + let(:reference) { label.to_reference(:name) } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.text).to eq 'See gfm references' + end + + it 'links with adjacent text' do + doc = filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(#{label.name}\.\))) + end + + it 'ignores invalid label names' do + exp = act = %(Label #{Label.reference_prefix}"#{label.name.reverse}") + + expect(filter(act).to_html).to eq exp + end + end + + describe 'edge cases' do + it 'gracefully handles non-references matching the pattern' do + exp = act = '(format nil "~0f" 3.0) ; 3.0' + expect(filter(act).to_html).to eq exp + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/merge_request_reference_filter_spec.rb new file mode 100644 index 0000000000..a48b9634cc --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/merge_request_reference_filter_spec.rb @@ -0,0 +1,118 @@ +require 'spec_helper' + +describe Gitlab::Markdown::MergeRequestReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:project, :public) } + let(:merge) { create(:merge_request, source_project: project) } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Merge #{merge.to_reference}" + expect(filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { merge.to_reference } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_merge_request_url(project.namespace, project, merge) + end + + it 'links with adjacent text' do + doc = filter("Merge (#{reference}.)") + expect(doc.to_html).to match(/\(#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid merge IDs' do + exp = act = "Merge #{invalidate_reference(reference)}" + + expect(filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = filter("Merge #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}" + end + + it 'escapes the title attribute' do + merge.update_attribute(:title, %{">whatever#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid merge IDs on the referenced project' do + exp = act = "Merge #{invalidate_reference(reference)}" + + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Merge #{reference}") + expect(result[:references][:merge_request]).to eq [merge] + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/redactor_filter_spec.rb b/spec/lib/gitlab/markdown/filter/redactor_filter_spec.rb new file mode 100644 index 0000000000..c177d6950a --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/redactor_filter_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe Gitlab::Markdown::RedactorFilter do + include ActionView::Helpers::UrlHelper + include FilterSpecHelper + + it 'ignores non-GFM links' do + html = %(See Google) + doc = filter(html, current_user: double) + + expect(doc.css('a').length).to eq 1 + end + + def reference_link(data) + link_to('text', '', class: 'gfm', data: data) + end + + context 'with data-project' do + it 'removes unpermitted Project references' do + user = create(:user) + project = create(:empty_project) + + link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name) + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 0 + end + + it 'allows permitted Project references' do + user = create(:user) + project = create(:empty_project) + project.team << [user, :master] + + link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name) + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 1 + end + + it 'handles invalid Project references' do + link = reference_link(project: 12345, reference_filter: Gitlab::Markdown::ReferenceFilter.name) + + expect { filter(link) }.not_to raise_error + end + end + + context "for user references" do + + context 'with data-group' do + it 'removes unpermitted Group references' do + user = create(:user) + group = create(:group) + + link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 0 + end + + it 'allows permitted Group references' do + user = create(:user) + group = create(:group) + group.add_developer(user) + + link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 1 + end + + it 'handles invalid Group references' do + link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + + expect { filter(link) }.not_to raise_error + end + end + + context 'with data-user' do + it 'allows any User reference' do + user = create(:user) + + link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + doc = filter(link) + + expect(doc.css('a').length).to eq 1 + end + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/reference_gatherer_filter_spec.rb b/spec/lib/gitlab/markdown/filter/reference_gatherer_filter_spec.rb new file mode 100644 index 0000000000..0884f53abe --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/reference_gatherer_filter_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe Gitlab::Markdown::ReferenceGathererFilter do + include ActionView::Helpers::UrlHelper + include FilterSpecHelper + + def reference_link(data) + link_to('text', '', class: 'gfm', data: data) + end + + context "for issue references" do + + context 'with data-project' do + it 'removes unpermitted Project references' do + user = create(:user) + project = create(:empty_project) + issue = create(:issue, project: project) + + link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name) + result = pipeline_result(link, current_user: user) + + expect(result[:references][:issue]).to be_empty + end + + it 'allows permitted Project references' do + user = create(:user) + project = create(:empty_project) + issue = create(:issue, project: project) + project.team << [user, :master] + + link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name) + result = pipeline_result(link, current_user: user) + + expect(result[:references][:issue]).to eq([issue]) + end + + it 'handles invalid Project references' do + link = reference_link(project: 12345, issue: 12345, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name) + + expect { pipeline_result(link) }.not_to raise_error + end + end + end + + context "for user references" do + + context 'with data-group' do + it 'removes unpermitted Group references' do + user = create(:user) + group = create(:group) + + link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + result = pipeline_result(link, current_user: user) + + expect(result[:references][:user]).to be_empty + end + + it 'allows permitted Group references' do + user = create(:user) + group = create(:group) + group.add_developer(user) + + link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + result = pipeline_result(link, current_user: user) + + expect(result[:references][:user]).to eq([user]) + end + + it 'handles invalid Group references' do + link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + + expect { pipeline_result(link) }.not_to raise_error + end + end + + context 'with data-user' do + it 'allows any User reference' do + user = create(:user) + + link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + result = pipeline_result(link) + + expect(result[:references][:user]).to eq([user]) + end + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/relative_link_filter_spec.rb b/spec/lib/gitlab/markdown/filter/relative_link_filter_spec.rb new file mode 100644 index 0000000000..3d97850556 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/relative_link_filter_spec.rb @@ -0,0 +1,147 @@ +# encoding: UTF-8 + +require 'spec_helper' + +describe Gitlab::Markdown::RelativeLinkFilter do + def filter(doc, contexts = {}) + contexts.reverse_merge!({ + commit: project.commit, + project: project, + project_wiki: project_wiki, + ref: ref, + requested_path: requested_path + }) + + described_class.call(doc, contexts) + end + + def image(path) + %() + end + + def link(path) + %(#{path}) + end + + let(:project) { create(:project) } + let(:project_path) { project.path_with_namespace } + let(:ref) { 'markdown' } + let(:project_wiki) { nil } + let(:requested_path) { '/' } + + shared_examples :preserve_unchanged do + it 'does not modify any relative URL in anchor' do + doc = filter(link('README.md')) + expect(doc.at_css('a')['href']).to eq 'README.md' + end + + it 'does not modify any relative URL in image' do + doc = filter(image('files/images/logo-black.png')) + expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png' + end + end + + shared_examples :relative_to_requested do + it 'rebuilds URL relative to the requested path' do + doc = filter(link('users.md')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/users.md" + end + end + + context 'with a project_wiki' do + let(:project_wiki) { double('ProjectWiki') } + include_examples :preserve_unchanged + end + + context 'without a repository' do + let(:project) { create(:empty_project) } + include_examples :preserve_unchanged + end + + context 'with an empty repository' do + let(:project) { create(:project_empty_repo) } + include_examples :preserve_unchanged + end + + it 'does not raise an exception on invalid URIs' do + act = link("://foo") + expect { filter(act) }.not_to raise_error + end + + context 'with a valid repository' do + it 'rebuilds relative URL for a file in the repo' do + doc = filter(link('doc/api/README.md')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + + it 'rebuilds relative URL for a file in the repo up one directory' do + relative_link = link('../api/README.md') + doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md') + + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + + it 'rebuilds relative URL for a file in the repo up multiple directories' do + relative_link = link('../../../api/README.md') + doc = filter(relative_link, requested_path: 'doc/foo/bar/baz/README.md') + + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + + it 'rebuilds relative URL for a file in the repo with an anchor' do + doc = filter(link('README.md#section')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/README.md#section" + end + + it 'rebuilds relative URL for a directory in the repo' do + doc = filter(link('doc/api/')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/tree/#{ref}/doc/api" + end + + it 'rebuilds relative URL for an image in the repo' do + doc = filter(link('files/images/logo-black.png')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png" + end + + it 'does not modify relative URL with an anchor only' do + doc = filter(link('#section-1')) + expect(doc.at_css('a')['href']).to eq '#section-1' + end + + it 'does not modify absolute URL' do + doc = filter(link('http://example.com')) + expect(doc.at_css('a')['href']).to eq 'http://example.com' + end + + it 'supports Unicode filenames' do + path = 'files/images/한글.png' + escaped = Addressable::URI.escape(path) + + # Stub these methods so the file doesn't actually need to be in the repo + allow_any_instance_of(described_class). + to receive(:file_exists?).and_return(true) + allow_any_instance_of(described_class). + to receive(:image?).with(path).and_return(true) + + doc = filter(image(escaped)) + expect(doc.at_css('img')['src']).to match '/raw/' + end + + context 'when requested path is a file in the repo' do + let(:requested_path) { 'doc/api/README.md' } + include_examples :relative_to_requested + end + + context 'when requested path is a directory in the repo' do + let(:requested_path) { 'doc/api' } + include_examples :relative_to_requested + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/sanitization_filter_spec.rb b/spec/lib/gitlab/markdown/filter/sanitization_filter_spec.rb new file mode 100644 index 0000000000..33d850d0dd --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/sanitization_filter_spec.rb @@ -0,0 +1,197 @@ +require 'spec_helper' + +describe Gitlab::Markdown::SanitizationFilter do + include FilterSpecHelper + + describe 'default whitelist' do + it 'sanitizes tags that are not whitelisted' do + act = %q{ and no blinks} + exp = 'no inputs and no blinks' + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes tag attributes' do + act = %q{Text} + exp = %q{Text} + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes javascript in attributes' do + act = %q(Text) + exp = 'Text' + expect(filter(act).to_html).to eq exp + end + + it 'allows whitelisted HTML tags from the user' do + exp = act = "
\n
Term
\n
Definition
\n
" + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes `class` attribute on any element' do + act = %q{Strong} + expect(filter(act).to_html).to eq %q{Strong} + end + + it 'sanitizes `id` attribute on any element' do + act = %q{Emphasis} + expect(filter(act).to_html).to eq %q{Emphasis} + end + end + + describe 'custom whitelist' do + it 'customizes the whitelist only once' do + instance = described_class.new('Foo') + 3.times { instance.whitelist } + + expect(instance.whitelist[:transformers].size).to eq 5 + end + + it 'allows syntax highlighting' do + exp = act = %q{
def
} + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes `class` attribute from non-highlight spans' do + act = %q{def} + expect(filter(act).to_html).to eq %q{def} + end + + it 'allows `style` attribute on table elements' do + html = <<-HTML.strip_heredoc + + + +
Head
Body
+ HTML + + doc = filter(html) + + expect(doc.at_css('th')['style']).to eq 'text-align: center' + expect(doc.at_css('td')['style']).to eq 'text-align: right' + end + + it 'allows `span` elements' do + exp = act = %q{Hello} + expect(filter(act).to_html).to eq exp + end + + it 'removes `rel` attribute from `a` elements' do + act = %q{Link} + exp = %q{Link} + + expect(filter(act).to_html).to eq exp + end + + # Adapted from the Sanitize test suite: http://git.io/vczrM + protocols = { + 'protocol-based JS injection: simple, no spaces' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: simple, spaces before' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: simple, spaces after' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: simple, spaces before and after' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: preceding colon' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: UTF-8 encoding' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: long UTF-8 encoding' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: long UTF-8 encoding without semicolons' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: hex encoding' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: long hex encoding' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: hex encoding without semicolons' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: null char' => { + input: "foo", + output: '' + }, + + 'protocol-based JS injection: spaces and entities' => { + input: 'foo', + output: 'foo' + }, + } + + protocols.each do |name, data| + it "handles #{name}" do + doc = filter(data[:input]) + + expect(doc.to_html).to eq data[:output] + end + end + + it 'allows non-standard anchor schemes' do + exp = %q{IRC} + act = filter(exp) + + expect(act.to_html).to eq exp + end + + it 'allows relative links' do + exp = %q{foo/bar.md} + act = filter(exp) + + expect(act.to_html).to eq exp + end + end + + context 'when inline_sanitization is true' do + it 'uses a stricter whitelist' do + doc = filter('

Description

', inline_sanitization: true) + expect(doc.to_html.strip).to eq 'Description' + end + + %w(pre code img ol ul li).each do |elem| + it "removes '#{elem}' elements" do + act = "<#{elem}>Description" + expect(filter(act, inline_sanitization: true).to_html.strip). + to eq 'Description' + end + end + + %w(b i strong em a ins del sup sub p).each do |elem| + it "still allows '#{elem}' elements" do + exp = act = "<#{elem}>Description" + expect(filter(act, inline_sanitization: true).to_html).to eq exp + end + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/snippet_reference_filter_spec.rb new file mode 100644 index 0000000000..671aac4959 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/snippet_reference_filter_spec.rb @@ -0,0 +1,116 @@ +require 'spec_helper' + +describe Gitlab::Markdown::SnippetReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:empty_project, :public) } + let(:snippet) { create(:project_snippet, project: project) } + let(:reference) { snippet.to_reference } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Snippet #{reference}" + expect(filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_snippet_url(project.namespace, project, snippet) + end + + it 'links with adjacent text' do + doc = filter("Snippet (#{reference}.)") + expect(doc.to_html).to match(/\(#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid snippet IDs' do + exp = act = "Snippet #{invalidate_reference(reference)}" + + expect(filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = filter("Snippet #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}" + end + + it 'escapes the title attribute' do + snippet.update_attribute(:title, %{">whatever#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid snippet IDs on the referenced project' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Snippet #{reference}") + expect(result[:references][:snippet]).to eq [snippet] + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/syntax_highlight_filter_spec.rb b/spec/lib/gitlab/markdown/filter/syntax_highlight_filter_spec.rb new file mode 100644 index 0000000000..2df81282fa --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/syntax_highlight_filter_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::Markdown::SyntaxHighlightFilter do + include FilterSpecHelper + + it 'highlights valid code blocks' do + result = filter('
def fun end')
+    expect(result.to_html).to eq("
def fun end
\n") + end + + it 'passes through invalid code blocks' do + allow_any_instance_of(described_class).to receive(:block_code).and_raise(StandardError) + + result = filter('
This is a test
') + expect(result.to_html).to eq('
This is a test
') + end +end diff --git a/spec/lib/gitlab/markdown/filter/table_of_contents_filter_spec.rb b/spec/lib/gitlab/markdown/filter/table_of_contents_filter_spec.rb new file mode 100644 index 0000000000..3aba29f0f2 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/table_of_contents_filter_spec.rb @@ -0,0 +1,97 @@ +# encoding: UTF-8 + +require 'spec_helper' + +describe Gitlab::Markdown::TableOfContentsFilter do + include FilterSpecHelper + + def header(level, text) + "#{text}\n" + end + + it 'does nothing when :no_header_anchors is truthy' do + exp = act = header(1, 'Header') + expect(filter(act, no_header_anchors: 1).to_html).to eq exp + end + + it 'does nothing with empty headers' do + exp = act = header(1, nil) + expect(filter(act).to_html).to eq exp + end + + 1.upto(6) do |i| + it "processes h#{i} elements" do + html = header(i, "Header #{i}") + doc = filter(html) + + expect(doc.css("h#{i} a").first.attr('id')).to eq "header-#{i}" + end + end + + describe 'anchor tag' do + it 'has an `anchor` class' do + doc = filter(header(1, 'Header')) + expect(doc.css('h1 a').first.attr('class')).to eq 'anchor' + end + + it 'links to the id' do + doc = filter(header(1, 'Header')) + expect(doc.css('h1 a').first.attr('href')).to eq '#header' + end + + describe 'generated IDs' do + it 'translates spaces to dashes' do + doc = filter(header(1, 'This header has spaces in it')) + expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-has-spaces-in-it' + end + + it 'squeezes multiple spaces and dashes' do + doc = filter(header(1, 'This---header is poorly-formatted')) + expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-poorly-formatted' + end + + it 'removes punctuation' do + doc = filter(header(1, "This, header! is, filled. with @ punctuation?")) + expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-filled-with-punctuation' + end + + it 'appends a unique number to duplicates' do + doc = filter(header(1, 'One') + header(2, 'One')) + + expect(doc.css('h1 a').first.attr('id')).to eq 'one' + expect(doc.css('h2 a').first.attr('id')).to eq 'one-1' + end + + it 'supports Unicode' do + doc = filter(header(1, '한글')) + expect(doc.css('h1 a').first.attr('id')).to eq '한글' + expect(doc.css('h1 a').first.attr('href')).to eq '#한글' + end + end + end + + describe 'result' do + def result(html) + HTML::Pipeline.new([described_class]).call(html) + end + + let(:results) { result(header(1, 'Header 1') + header(2, 'Header 2')) } + let(:doc) { Nokogiri::XML::DocumentFragment.parse(results[:toc]) } + + it 'is contained within a `ul` element' do + expect(doc.children.first.name).to eq 'ul' + expect(doc.children.first.attr('class')).to eq 'section-nav' + end + + it 'contains an `li` element for each header' do + expect(doc.css('li').length).to eq 2 + + links = doc.css('li a') + + expect(links.first.attr('href')).to eq '#header-1' + expect(links.first.text).to eq 'Header 1' + expect(links.last.attr('href')).to eq '#header-2' + expect(links.last.text).to eq 'Header 2' + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/task_list_filter_spec.rb b/spec/lib/gitlab/markdown/filter/task_list_filter_spec.rb new file mode 100644 index 0000000000..8b205adbbc --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/task_list_filter_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +describe Gitlab::Markdown::TaskListFilter do + include FilterSpecHelper + + it 'does not apply `task-list` class to non-task lists' do + exp = act = %(
  • Item
) + expect(filter(act).to_html).to eq exp + end +end diff --git a/spec/lib/gitlab/markdown/filter/upload_link_filter_spec.rb b/spec/lib/gitlab/markdown/filter/upload_link_filter_spec.rb new file mode 100644 index 0000000000..aec22ee416 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/upload_link_filter_spec.rb @@ -0,0 +1,73 @@ +# encoding: UTF-8 + +require 'spec_helper' + +describe Gitlab::Markdown::UploadLinkFilter do + def filter(doc, contexts = {}) + contexts.reverse_merge!({ + project: project + }) + + described_class.call(doc, contexts) + end + + def image(path) + %() + end + + def link(path) + %(
#{path}) + end + + let(:project) { create(:project) } + + shared_examples :preserve_unchanged do + it 'does not modify any relative URL in anchor' do + doc = filter(link('README.md')) + expect(doc.at_css('a')['href']).to eq 'README.md' + end + + it 'does not modify any relative URL in image' do + doc = filter(image('files/images/logo-black.png')) + expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png' + end + end + + it 'does not raise an exception on invalid URIs' do + act = link("://foo") + expect { filter(act) }.not_to raise_error + end + + context 'with a valid repository' do + it 'rebuilds relative URL for a link' do + doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) + expect(doc.at_css('a')['href']). + to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + end + + it 'rebuilds relative URL for an image' do + doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) + expect(doc.at_css('a')['href']). + to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + end + + it 'does not modify absolute URL' do + doc = filter(link('http://example.com')) + expect(doc.at_css('a')['href']).to eq 'http://example.com' + end + + it 'supports Unicode filenames' do + path = '/uploads/한글.png' + escaped = Addressable::URI.escape(path) + + # Stub these methods so the file doesn't actually need to be in the repo + allow_any_instance_of(described_class). + to receive(:file_exists?).and_return(true) + allow_any_instance_of(described_class). + to receive(:image?).with(path).and_return(true) + + doc = filter(image(escaped)) + expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/%ED%95%9C%EA%B8%80.png" + end + end +end diff --git a/spec/lib/gitlab/markdown/filter/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/user_reference_filter_spec.rb new file mode 100644 index 0000000000..8ccd7744d6 --- /dev/null +++ b/spec/lib/gitlab/markdown/filter/user_reference_filter_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe Gitlab::Markdown::UserReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + let(:reference) { user.to_reference } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + it 'ignores invalid users' do + exp = act = "Hey #{invalidate_reference(reference)}" + expect(filter(act).to_html).to eq(exp) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Hey #{reference}" + expect(filter(act).to_html).to eq exp + end + end + + context 'mentioning @all' do + let(:reference) { User.reference_prefix + 'all' } + + before do + project.team << [project.creator, :developer] + end + + it 'supports a special @all mention' do + doc = filter("Hey #{reference}") + expect(doc.css('a').length).to eq 1 + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_url(project.namespace, project) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Hey #{reference}") + expect(result[:references][:user]).to eq [project.creator] + end + end + + context 'mentioning a user' do + it 'links to a User' do + doc = filter("Hey #{reference}") + expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) + end + + it 'links to a User with a period' do + user = create(:user, name: 'alphA.Beta') + + doc = filter("Hey #{user.to_reference}") + expect(doc.css('a').length).to eq 1 + end + + it 'links to a User with an underscore' do + user = create(:user, name: 'ping_pong_king') + + doc = filter("Hey #{user.to_reference}") + expect(doc.css('a').length).to eq 1 + end + + it 'includes a data-user attribute' do + doc = filter("Hey #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-user') + expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Hey #{reference}") + expect(result[:references][:user]).to eq [user] + end + end + + context 'mentioning a group' do + let(:group) { create(:group) } + let(:reference) { group.to_reference } + + it 'links to the Group' do + doc = filter("Hey #{reference}") + expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) + end + + it 'includes a data-group attribute' do + doc = filter("Hey #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-group') + expect(link.attr('data-group')).to eq group.id.to_s + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Hey #{reference}") + expect(result[:references][:user]).to eq group.users + end + end + + it 'links with adjacent text' do + doc = filter("Mention me (#{reference}.)") + expect(doc.to_html).to match(/\(#{reference}<\/a>\.\)/) + end + + it 'includes default classes' do + doc = filter("Hey #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' + end + + it 'supports an :only_path context' do + doc = filter("Hey #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.user_path(user) + end +end diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb deleted file mode 100644 index 94c80ae661..0000000000 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ /dev/null @@ -1,139 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe IssueReferenceFilter do - include FilterSpecHelper - - def helper - IssuesHelper - end - - let(:project) { create(:empty_project, :public) } - let(:issue) { create(:issue, project: project) } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Issue #{issue.to_reference}" - expect(filter(act).to_html).to eq exp - end - end - - context 'internal reference' do - let(:reference) { issue.to_reference } - - it 'ignores valid references when using non-default tracker' do - expect(project).to receive(:get_issue).with(issue.iid).and_return(nil) - - exp = act = "Issue #{reference}" - expect(filter(act).to_html).to eq exp - end - - it 'links to a valid reference' do - doc = filter("Fixed #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq helper.url_for_issue(issue.iid, project) - end - - it 'links with adjacent text' do - doc = filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(#{Regexp.escape(reference)}<\/a>\.\)/) - end - - it 'ignores invalid issue IDs' do - invalid = invalidate_reference(reference) - exp = act = "Fixed #{invalid}" - - expect(filter(act).to_html).to eq exp - end - - it 'includes a title attribute' do - doc = filter("Issue #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}" - end - - it 'escapes the title attribute' do - issue.update_attribute(:title, %{">whatever#{Regexp.escape(reference)}<\/a>\.\)/) - end - - it 'ignores invalid issue IDs on the referenced project' do - exp = act = "Fixed #{invalidate_reference(reference)}" - - expect(filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end - end - end -end diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb deleted file mode 100644 index fc21b65a84..0000000000 --- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb +++ /dev/null @@ -1,144 +0,0 @@ -require 'spec_helper' -require 'html/pipeline' - -module Gitlab::Markdown - describe LabelReferenceFilter do - include FilterSpecHelper - - let(:project) { create(:empty_project, :public) } - let(:label) { create(:label, project: project) } - let(:reference) { label.to_reference } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Label #{reference}" - expect(filter(act).to_html).to eq exp - end - end - - it 'includes default classes' do - doc = filter("Label #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label' - end - - it 'includes a data-project attribute' do - doc = filter("Label #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-project') - expect(link.attr('data-project')).to eq project.id.to_s - end - - it 'includes a data-label attribute' do - doc = filter("See #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-label') - expect(link.attr('data-label')).to eq label.id.to_s - end - - it 'supports an :only_path context' do - doc = filter("Label #{reference}", only_path: true) - link = doc.css('a').first.attr('href') - - expect(link).not_to match %r(https?://) - expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Label #{reference}") - expect(result[:references][:label]).to eq [label] - end - - describe 'label span element' do - it 'includes default classes' do - doc = filter("Label #{reference}") - expect(doc.css('a span').first.attr('class')).to eq 'label color-label' - end - - it 'includes a style attribute' do - doc = filter("Label #{reference}") - expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/) - end - end - - context 'Integer-based references' do - it 'links to a valid reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) - end - - it 'links with adjacent text' do - doc = filter("Label (#{reference}.)") - expect(doc.to_html).to match(%r(\(#{label.name}\.\))) - end - - it 'ignores invalid label IDs' do - exp = act = "Label #{invalidate_reference(reference)}" - - expect(filter(act).to_html).to eq exp - end - end - - context 'String-based single-word references' do - let(:label) { create(:label, name: 'gfm', project: project) } - let(:reference) { "#{Label.reference_prefix}#{label.name}" } - - it 'links to a valid reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) - expect(doc.text).to eq 'See gfm' - end - - it 'links with adjacent text' do - doc = filter("Label (#{reference}.)") - expect(doc.to_html).to match(%r(\(#{label.name}\.\))) - end - - it 'ignores invalid label names' do - exp = act = "Label #{Label.reference_prefix}#{label.name.reverse}" - - expect(filter(act).to_html).to eq exp - end - end - - context 'String-based multi-word references in quotes' do - let(:label) { create(:label, name: 'gfm references', project: project) } - let(:reference) { label.to_reference(:name) } - - it 'links to a valid reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) - expect(doc.text).to eq 'See gfm references' - end - - it 'links with adjacent text' do - doc = filter("Label (#{reference}.)") - expect(doc.to_html).to match(%r(\(#{label.name}\.\))) - end - - it 'ignores invalid label names' do - exp = act = %(Label #{Label.reference_prefix}"#{label.name.reverse}") - - expect(filter(act).to_html).to eq exp - end - end - - describe 'edge cases' do - it 'gracefully handles non-references matching the pattern' do - exp = act = '(format nil "~0f" 3.0) ; 3.0' - expect(filter(act).to_html).to eq exp - end - end - end -end diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb deleted file mode 100644 index 3ef6cdfff3..0000000000 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ /dev/null @@ -1,120 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe MergeRequestReferenceFilter do - include FilterSpecHelper - - let(:project) { create(:project, :public) } - let(:merge) { create(:merge_request, source_project: project) } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Merge #{merge.to_reference}" - expect(filter(act).to_html).to eq exp - end - end - - context 'internal reference' do - let(:reference) { merge.to_reference } - - it 'links to a valid reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_merge_request_url(project.namespace, project, merge) - end - - it 'links with adjacent text' do - doc = filter("Merge (#{reference}.)") - expect(doc.to_html).to match(/\(#{Regexp.escape(reference)}<\/a>\.\)/) - end - - it 'ignores invalid merge IDs' do - exp = act = "Merge #{invalidate_reference(reference)}" - - expect(filter(act).to_html).to eq exp - end - - it 'includes a title attribute' do - doc = filter("Merge #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}" - end - - it 'escapes the title attribute' do - merge.update_attribute(:title, %{">whatever#{Regexp.escape(reference)}<\/a>\.\)/) - end - - it 'ignores invalid merge IDs on the referenced project' do - exp = act = "Merge #{invalidate_reference(reference)}" - - expect(filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Merge #{reference}") - expect(result[:references][:merge_request]).to eq [merge] - end - end - end -end diff --git a/spec/lib/gitlab/markdown/redactor_filter_spec.rb b/spec/lib/gitlab/markdown/redactor_filter_spec.rb deleted file mode 100644 index eea3f1cf37..0000000000 --- a/spec/lib/gitlab/markdown/redactor_filter_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe RedactorFilter do - include ActionView::Helpers::UrlHelper - include FilterSpecHelper - - it 'ignores non-GFM links' do - html = %(See Google) - doc = filter(html, current_user: double) - - expect(doc.css('a').length).to eq 1 - end - - def reference_link(data) - link_to('text', '', class: 'gfm', data: data) - end - - context 'with data-project' do - it 'removes unpermitted Project references' do - user = create(:user) - project = create(:empty_project) - - link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name) - doc = filter(link, current_user: user) - - expect(doc.css('a').length).to eq 0 - end - - it 'allows permitted Project references' do - user = create(:user) - project = create(:empty_project) - project.team << [user, :master] - - link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name) - doc = filter(link, current_user: user) - - expect(doc.css('a').length).to eq 1 - end - - it 'handles invalid Project references' do - link = reference_link(project: 12345, reference_filter: Gitlab::Markdown::ReferenceFilter.name) - - expect { filter(link) }.not_to raise_error - end - end - - context "for user references" do - - context 'with data-group' do - it 'removes unpermitted Group references' do - user = create(:user) - group = create(:group) - - link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - doc = filter(link, current_user: user) - - expect(doc.css('a').length).to eq 0 - end - - it 'allows permitted Group references' do - user = create(:user) - group = create(:group) - group.add_developer(user) - - link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - doc = filter(link, current_user: user) - - expect(doc.css('a').length).to eq 1 - end - - it 'handles invalid Group references' do - link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - - expect { filter(link) }.not_to raise_error - end - end - - context 'with data-user' do - it 'allows any User reference' do - user = create(:user) - - link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - doc = filter(link) - - expect(doc.css('a').length).to eq 1 - end - end - end - end -end diff --git a/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb b/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb deleted file mode 100644 index 4fa473ad19..0000000000 --- a/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe ReferenceGathererFilter do - include ActionView::Helpers::UrlHelper - include FilterSpecHelper - - def reference_link(data) - link_to('text', '', class: 'gfm', data: data) - end - - context "for issue references" do - - context 'with data-project' do - it 'removes unpermitted Project references' do - user = create(:user) - project = create(:empty_project) - issue = create(:issue, project: project) - - link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name) - result = pipeline_result(link, current_user: user) - - expect(result[:references][:issue]).to be_empty - end - - it 'allows permitted Project references' do - user = create(:user) - project = create(:empty_project) - issue = create(:issue, project: project) - project.team << [user, :master] - - link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name) - result = pipeline_result(link, current_user: user) - - expect(result[:references][:issue]).to eq([issue]) - end - - it 'handles invalid Project references' do - link = reference_link(project: 12345, issue: 12345, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name) - - expect { pipeline_result(link) }.not_to raise_error - end - end - end - - context "for user references" do - - context 'with data-group' do - it 'removes unpermitted Group references' do - user = create(:user) - group = create(:group) - - link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - result = pipeline_result(link, current_user: user) - - expect(result[:references][:user]).to be_empty - end - - it 'allows permitted Group references' do - user = create(:user) - group = create(:group) - group.add_developer(user) - - link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - result = pipeline_result(link, current_user: user) - - expect(result[:references][:user]).to eq([user]) - end - - it 'handles invalid Group references' do - link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - - expect { pipeline_result(link) }.not_to raise_error - end - end - - context 'with data-user' do - it 'allows any User reference' do - user = create(:user) - - link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) - result = pipeline_result(link) - - expect(result[:references][:user]).to eq([user]) - end - end - end - end -end diff --git a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb deleted file mode 100644 index 027336ceb7..0000000000 --- a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb +++ /dev/null @@ -1,149 +0,0 @@ -# encoding: UTF-8 - -require 'spec_helper' - -module Gitlab::Markdown - describe RelativeLinkFilter do - def filter(doc, contexts = {}) - contexts.reverse_merge!({ - commit: project.commit, - project: project, - project_wiki: project_wiki, - ref: ref, - requested_path: requested_path - }) - - described_class.call(doc, contexts) - end - - def image(path) - %() - end - - def link(path) - %(#{path}) - end - - let(:project) { create(:project) } - let(:project_path) { project.path_with_namespace } - let(:ref) { 'markdown' } - let(:project_wiki) { nil } - let(:requested_path) { '/' } - - shared_examples :preserve_unchanged do - it 'does not modify any relative URL in anchor' do - doc = filter(link('README.md')) - expect(doc.at_css('a')['href']).to eq 'README.md' - end - - it 'does not modify any relative URL in image' do - doc = filter(image('files/images/logo-black.png')) - expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png' - end - end - - shared_examples :relative_to_requested do - it 'rebuilds URL relative to the requested path' do - doc = filter(link('users.md')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/doc/api/users.md" - end - end - - context 'with a project_wiki' do - let(:project_wiki) { double('ProjectWiki') } - include_examples :preserve_unchanged - end - - context 'without a repository' do - let(:project) { create(:empty_project) } - include_examples :preserve_unchanged - end - - context 'with an empty repository' do - let(:project) { create(:project_empty_repo) } - include_examples :preserve_unchanged - end - - it 'does not raise an exception on invalid URIs' do - act = link("://foo") - expect { filter(act) }.not_to raise_error - end - - context 'with a valid repository' do - it 'rebuilds relative URL for a file in the repo' do - doc = filter(link('doc/api/README.md')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" - end - - it 'rebuilds relative URL for a file in the repo up one directory' do - relative_link = link('../api/README.md') - doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md') - - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" - end - - it 'rebuilds relative URL for a file in the repo up multiple directories' do - relative_link = link('../../../api/README.md') - doc = filter(relative_link, requested_path: 'doc/foo/bar/baz/README.md') - - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" - end - - it 'rebuilds relative URL for a file in the repo with an anchor' do - doc = filter(link('README.md#section')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/README.md#section" - end - - it 'rebuilds relative URL for a directory in the repo' do - doc = filter(link('doc/api/')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/tree/#{ref}/doc/api" - end - - it 'rebuilds relative URL for an image in the repo' do - doc = filter(link('files/images/logo-black.png')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png" - end - - it 'does not modify relative URL with an anchor only' do - doc = filter(link('#section-1')) - expect(doc.at_css('a')['href']).to eq '#section-1' - end - - it 'does not modify absolute URL' do - doc = filter(link('http://example.com')) - expect(doc.at_css('a')['href']).to eq 'http://example.com' - end - - it 'supports Unicode filenames' do - path = 'files/images/한글.png' - escaped = Addressable::URI.escape(path) - - # Stub these methods so the file doesn't actually need to be in the repo - allow_any_instance_of(described_class). - to receive(:file_exists?).and_return(true) - allow_any_instance_of(described_class). - to receive(:image?).with(path).and_return(true) - - doc = filter(image(escaped)) - expect(doc.at_css('img')['src']).to match '/raw/' - end - - context 'when requested path is a file in the repo' do - let(:requested_path) { 'doc/api/README.md' } - include_examples :relative_to_requested - end - - context 'when requested path is a directory in the repo' do - let(:requested_path) { 'doc/api' } - include_examples :relative_to_requested - end - end - end -end diff --git a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb deleted file mode 100644 index b089729172..0000000000 --- a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb +++ /dev/null @@ -1,199 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe SanitizationFilter do - include FilterSpecHelper - - describe 'default whitelist' do - it 'sanitizes tags that are not whitelisted' do - act = %q{ and no blinks} - exp = 'no inputs and no blinks' - expect(filter(act).to_html).to eq exp - end - - it 'sanitizes tag attributes' do - act = %q{Text} - exp = %q{Text} - expect(filter(act).to_html).to eq exp - end - - it 'sanitizes javascript in attributes' do - act = %q(Text) - exp = 'Text' - expect(filter(act).to_html).to eq exp - end - - it 'allows whitelisted HTML tags from the user' do - exp = act = "
\n
Term
\n
Definition
\n
" - expect(filter(act).to_html).to eq exp - end - - it 'sanitizes `class` attribute on any element' do - act = %q{Strong} - expect(filter(act).to_html).to eq %q{Strong} - end - - it 'sanitizes `id` attribute on any element' do - act = %q{Emphasis} - expect(filter(act).to_html).to eq %q{Emphasis} - end - end - - describe 'custom whitelist' do - it 'customizes the whitelist only once' do - instance = described_class.new('Foo') - 3.times { instance.whitelist } - - expect(instance.whitelist[:transformers].size).to eq 5 - end - - it 'allows syntax highlighting' do - exp = act = %q{
def
} - expect(filter(act).to_html).to eq exp - end - - it 'sanitizes `class` attribute from non-highlight spans' do - act = %q{def} - expect(filter(act).to_html).to eq %q{def} - end - - it 'allows `style` attribute on table elements' do - html = <<-HTML.strip_heredoc - - - -
Head
Body
- HTML - - doc = filter(html) - - expect(doc.at_css('th')['style']).to eq 'text-align: center' - expect(doc.at_css('td')['style']).to eq 'text-align: right' - end - - it 'allows `span` elements' do - exp = act = %q{Hello} - expect(filter(act).to_html).to eq exp - end - - it 'removes `rel` attribute from `a` elements' do - act = %q{Link} - exp = %q{Link} - - expect(filter(act).to_html).to eq exp - end - - # Adapted from the Sanitize test suite: http://git.io/vczrM - protocols = { - 'protocol-based JS injection: simple, no spaces' => { - input: 'foo', - output: 'foo' - }, - - 'protocol-based JS injection: simple, spaces before' => { - input: 'foo', - output: 'foo' - }, - - 'protocol-based JS injection: simple, spaces after' => { - input: 'foo', - output: 'foo' - }, - - 'protocol-based JS injection: simple, spaces before and after' => { - input: 'foo', - output: 'foo' - }, - - 'protocol-based JS injection: preceding colon' => { - input: 'foo', - output: 'foo' - }, - - 'protocol-based JS injection: UTF-8 encoding' => { - input: 'foo', - output: 'foo' - }, - - 'protocol-based JS injection: long UTF-8 encoding' => { - input: 'foo', - output: 'foo' - }, - - 'protocol-based JS injection: long UTF-8 encoding without semicolons' => { - input: 'foo', - output: 'foo' - }, - - 'protocol-based JS injection: hex encoding' => { - input: 'foo', - output: 'foo' - }, - - 'protocol-based JS injection: long hex encoding' => { - input: 'foo', - output: 'foo' - }, - - 'protocol-based JS injection: hex encoding without semicolons' => { - input: 'foo', - output: 'foo' - }, - - 'protocol-based JS injection: null char' => { - input: "foo", - output: '' - }, - - 'protocol-based JS injection: spaces and entities' => { - input: 'foo', - output: 'foo' - }, - } - - protocols.each do |name, data| - it "handles #{name}" do - doc = filter(data[:input]) - - expect(doc.to_html).to eq data[:output] - end - end - - it 'allows non-standard anchor schemes' do - exp = %q{IRC} - act = filter(exp) - - expect(act.to_html).to eq exp - end - - it 'allows relative links' do - exp = %q{foo/bar.md} - act = filter(exp) - - expect(act.to_html).to eq exp - end - end - - context 'when inline_sanitization is true' do - it 'uses a stricter whitelist' do - doc = filter('

Description

', inline_sanitization: true) - expect(doc.to_html.strip).to eq 'Description' - end - - %w(pre code img ol ul li).each do |elem| - it "removes '#{elem}' elements" do - act = "<#{elem}>Description" - expect(filter(act, inline_sanitization: true).to_html.strip). - to eq 'Description' - end - end - - %w(b i strong em a ins del sup sub p).each do |elem| - it "still allows '#{elem}' elements" do - exp = act = "<#{elem}>Description" - expect(filter(act, inline_sanitization: true).to_html).to eq exp - end - end - end - end -end diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb deleted file mode 100644 index 9d9652dba4..0000000000 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ /dev/null @@ -1,118 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe SnippetReferenceFilter do - include FilterSpecHelper - - let(:project) { create(:empty_project, :public) } - let(:snippet) { create(:project_snippet, project: project) } - let(:reference) { snippet.to_reference } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Snippet #{reference}" - expect(filter(act).to_html).to eq exp - end - end - - context 'internal reference' do - it 'links to a valid reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_snippet_url(project.namespace, project, snippet) - end - - it 'links with adjacent text' do - doc = filter("Snippet (#{reference}.)") - expect(doc.to_html).to match(/\(#{Regexp.escape(reference)}<\/a>\.\)/) - end - - it 'ignores invalid snippet IDs' do - exp = act = "Snippet #{invalidate_reference(reference)}" - - expect(filter(act).to_html).to eq exp - end - - it 'includes a title attribute' do - doc = filter("Snippet #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}" - end - - it 'escapes the title attribute' do - snippet.update_attribute(:title, %{">whatever#{Regexp.escape(reference)}<\/a>\.\)/) - end - - it 'ignores invalid snippet IDs on the referenced project' do - exp = act = "See #{invalidate_reference(reference)}" - - expect(filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Snippet #{reference}") - expect(result[:references][:snippet]).to eq [snippet] - end - end - end -end diff --git a/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb b/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb deleted file mode 100644 index 6a49067372..0000000000 --- a/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe SyntaxHighlightFilter do - include FilterSpecHelper - - it 'highlights valid code blocks' do - result = filter('
def fun end')
-      expect(result.to_html).to eq("
def fun end
\n") - end - - it 'passes through invalid code blocks' do - allow_any_instance_of(SyntaxHighlightFilter).to receive(:block_code).and_raise(StandardError) - - result = filter('
This is a test
') - expect(result.to_html).to eq('
This is a test
') - end - end -end diff --git a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb b/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb deleted file mode 100644 index ddf583a72c..0000000000 --- a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb +++ /dev/null @@ -1,99 +0,0 @@ -# encoding: UTF-8 - -require 'spec_helper' - -module Gitlab::Markdown - describe TableOfContentsFilter do - include FilterSpecHelper - - def header(level, text) - "#{text}\n" - end - - it 'does nothing when :no_header_anchors is truthy' do - exp = act = header(1, 'Header') - expect(filter(act, no_header_anchors: 1).to_html).to eq exp - end - - it 'does nothing with empty headers' do - exp = act = header(1, nil) - expect(filter(act).to_html).to eq exp - end - - 1.upto(6) do |i| - it "processes h#{i} elements" do - html = header(i, "Header #{i}") - doc = filter(html) - - expect(doc.css("h#{i} a").first.attr('id')).to eq "header-#{i}" - end - end - - describe 'anchor tag' do - it 'has an `anchor` class' do - doc = filter(header(1, 'Header')) - expect(doc.css('h1 a').first.attr('class')).to eq 'anchor' - end - - it 'links to the id' do - doc = filter(header(1, 'Header')) - expect(doc.css('h1 a').first.attr('href')).to eq '#header' - end - - describe 'generated IDs' do - it 'translates spaces to dashes' do - doc = filter(header(1, 'This header has spaces in it')) - expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-has-spaces-in-it' - end - - it 'squeezes multiple spaces and dashes' do - doc = filter(header(1, 'This---header is poorly-formatted')) - expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-poorly-formatted' - end - - it 'removes punctuation' do - doc = filter(header(1, "This, header! is, filled. with @ punctuation?")) - expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-filled-with-punctuation' - end - - it 'appends a unique number to duplicates' do - doc = filter(header(1, 'One') + header(2, 'One')) - - expect(doc.css('h1 a').first.attr('id')).to eq 'one' - expect(doc.css('h2 a').first.attr('id')).to eq 'one-1' - end - - it 'supports Unicode' do - doc = filter(header(1, '한글')) - expect(doc.css('h1 a').first.attr('id')).to eq '한글' - expect(doc.css('h1 a').first.attr('href')).to eq '#한글' - end - end - end - - describe 'result' do - def result(html) - HTML::Pipeline.new([described_class]).call(html) - end - - let(:results) { result(header(1, 'Header 1') + header(2, 'Header 2')) } - let(:doc) { Nokogiri::XML::DocumentFragment.parse(results[:toc]) } - - it 'is contained within a `ul` element' do - expect(doc.children.first.name).to eq 'ul' - expect(doc.children.first.attr('class')).to eq 'section-nav' - end - - it 'contains an `li` element for each header' do - expect(doc.css('li').length).to eq 2 - - links = doc.css('li a') - - expect(links.first.attr('href')).to eq '#header-1' - expect(links.first.text).to eq 'Header 1' - expect(links.last.attr('href')).to eq '#header-2' - expect(links.last.text).to eq 'Header 2' - end - end - end -end diff --git a/spec/lib/gitlab/markdown/task_list_filter_spec.rb b/spec/lib/gitlab/markdown/task_list_filter_spec.rb deleted file mode 100644 index 94f39cc966..0000000000 --- a/spec/lib/gitlab/markdown/task_list_filter_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe TaskListFilter do - include FilterSpecHelper - - it 'does not apply `task-list` class to non-task lists' do - exp = act = %(
  • Item
) - expect(filter(act).to_html).to eq exp - end - end -end diff --git a/spec/lib/gitlab/markdown/upload_link_filter_spec.rb b/spec/lib/gitlab/markdown/upload_link_filter_spec.rb deleted file mode 100644 index 9ae45a6f55..0000000000 --- a/spec/lib/gitlab/markdown/upload_link_filter_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -# encoding: UTF-8 - -require 'spec_helper' - -module Gitlab::Markdown - describe UploadLinkFilter do - def filter(doc, contexts = {}) - contexts.reverse_merge!({ - project: project - }) - - described_class.call(doc, contexts) - end - - def image(path) - %() - end - - def link(path) - %(
#{path}) - end - - let(:project) { create(:project) } - - shared_examples :preserve_unchanged do - it 'does not modify any relative URL in anchor' do - doc = filter(link('README.md')) - expect(doc.at_css('a')['href']).to eq 'README.md' - end - - it 'does not modify any relative URL in image' do - doc = filter(image('files/images/logo-black.png')) - expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png' - end - end - - it 'does not raise an exception on invalid URIs' do - act = link("://foo") - expect { filter(act) }.not_to raise_error - end - - context 'with a valid repository' do - it 'rebuilds relative URL for a link' do - doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.at_css('a')['href']). - to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" - end - - it 'rebuilds relative URL for an image' do - doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.at_css('a')['href']). - to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" - end - - it 'does not modify absolute URL' do - doc = filter(link('http://example.com')) - expect(doc.at_css('a')['href']).to eq 'http://example.com' - end - - it 'supports Unicode filenames' do - path = '/uploads/한글.png' - escaped = Addressable::URI.escape(path) - - # Stub these methods so the file doesn't actually need to be in the repo - allow_any_instance_of(described_class). - to receive(:file_exists?).and_return(true) - allow_any_instance_of(described_class). - to receive(:image?).with(path).and_return(true) - - doc = filter(image(escaped)) - expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/%ED%95%9C%EA%B8%80.png" - end - end - end -end diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb deleted file mode 100644 index d9e0d7c42d..0000000000 --- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb +++ /dev/null @@ -1,122 +0,0 @@ -require 'spec_helper' - -module Gitlab::Markdown - describe UserReferenceFilter do - include FilterSpecHelper - - let(:project) { create(:empty_project, :public) } - let(:user) { create(:user) } - let(:reference) { user.to_reference } - - it 'requires project context' do - expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) - end - - it 'ignores invalid users' do - exp = act = "Hey #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq(exp) - end - - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Hey #{reference}" - expect(filter(act).to_html).to eq exp - end - end - - context 'mentioning @all' do - let(:reference) { User.reference_prefix + 'all' } - - before do - project.team << [project.creator, :developer] - end - - it 'supports a special @all mention' do - doc = filter("Hey #{reference}") - expect(doc.css('a').length).to eq 1 - expect(doc.css('a').first.attr('href')) - .to eq urls.namespace_project_url(project.namespace, project) - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}") - expect(result[:references][:user]).to eq [project.creator] - end - end - - context 'mentioning a user' do - it 'links to a User' do - doc = filter("Hey #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) - end - - it 'links to a User with a period' do - user = create(:user, name: 'alphA.Beta') - - doc = filter("Hey #{user.to_reference}") - expect(doc.css('a').length).to eq 1 - end - - it 'links to a User with an underscore' do - user = create(:user, name: 'ping_pong_king') - - doc = filter("Hey #{user.to_reference}") - expect(doc.css('a').length).to eq 1 - end - - it 'includes a data-user attribute' do - doc = filter("Hey #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-user') - expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}") - expect(result[:references][:user]).to eq [user] - end - end - - context 'mentioning a group' do - let(:group) { create(:group) } - let(:reference) { group.to_reference } - - it 'links to the Group' do - doc = filter("Hey #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) - end - - it 'includes a data-group attribute' do - doc = filter("Hey #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-group') - expect(link.attr('data-group')).to eq group.id.to_s - end - - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}") - expect(result[:references][:user]).to eq group.users - end - end - - it 'links with adjacent text' do - doc = filter("Mention me (#{reference}.)") - expect(doc.to_html).to match(/\(#{reference}<\/a>\.\)/) - end - - it 'includes default classes' do - doc = filter("Hey #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' - end - - it 'supports an :only_path context' do - doc = filter("Hey #{reference}", only_path: true) - link = doc.css('a').first.attr('href') - - expect(link).not_to match %r(https?://) - expect(link).to eq urls.user_path(user) - end - end -end From 52bcfd037fdc17bd73a674c248045dc750cc55c6 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 19 Nov 2015 17:18:07 +0200 Subject: [PATCH 051/908] Update public access documentation [ci skip] --- doc/public_access/public_access.md | 51 +++++++++++++++++++----------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md index bd439f7c6f..6e22ea7b72 100644 --- a/doc/public_access/public_access.md +++ b/doc/public_access/public_access.md @@ -1,44 +1,59 @@ # Public access -GitLab allows you to open selected projects to be accessed **publicly** or **internally**. +GitLab allows you to change your projects' visibility in order be accessed +**publicly** or **internally**. -Projects with either of these visibility levels will be listed in the [public access directory](/public). +Projects with either of these visibility levels will be listed in the +public access directory (`/public` under your GitLab instance). +Here is the [GitLab.com example](https://gitlab.com/public). Internal projects will only be available to authenticated users. -## Public projects +## Visibility of projects + +### Public projects Public projects can be cloned **without any** authentication. -It will also be listed on the [public access directory](/public). +They will also be listed on the public access directory (`/public`). -**Any logged in user** will have [Guest](../permissions/permissions) permissions on the repository. +**Any logged in user** will have [Guest](../permissions/permissions) +permissions on the repository. -## Internal projects +### Internal projects Internal projects can be cloned by any logged in user. -It will also be listed on the [public access directory](/public) for logged in users. +They will also be listed on the public access directory (`/public`) for logged +in users. -Any logged in user will have [Guest](../permissions/permissions) permissions on the repository. +Any logged in user will have [Guest](../permissions/permissions) permissions on +the repository. -## How to change project visibility +### How to change project visibility -1. Go to your project dashboard -1. Click on the "Edit" tab -1. Change "Visibility Level" +1. Go to your project's **Settings** +1. Change "Visibility Level" to either Public, Internal or Private ## Visibility of users -The public page of users, located at `/u/username` is visible if either: +The public page of a user, located at `/u/username`, is always visible whether +you are logged in or not. -- You are logged in. -- You are logged out, and the target user is authorized to (is Guest, Reporter, etc.) at least one public project. +When visiting the public page of a user, you can only see the projects which +you are privileged to. -Otherwise, you will be redirected to the sign in page. +## Visibility of groups -When visiting the public page of an user, you will only see listed projects which you can view yourself. +The public page of a group, located at `/groups/groupname`, is always visible +to everyone. + +Logged out users will be able to see the description and the avatar of the +group as well as all public projects belonging to that group. ## Restricting the use of public or internal projects -In the Admin area under Settings you can disable public projects or public and internal projects for the entire GitLab installation to prevent people making code public by accident. The restricted visibility settings do not apply to admin users. +In the Admin area under **Settings** (`/admin/application_settings`), you can +restrict the use of visibility levels for users when they create a project or a +snippet. This is useful to prevent people exposing their repositories to public +by accident. The restricted visibility settings do not apply to admin users. From 945e4293cb8f745b2e1a1aebdcf4df3eeba338cc Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Thu, 19 Nov 2015 23:40:29 +0100 Subject: [PATCH 052/908] Prevent unnecessary forks iteration at parent update --- app/services/projects/update_service.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 67e3429c0b..cd7a8b1821 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -13,7 +13,7 @@ module Projects end return false unless visibility_level_allowed?(new_visibility) - update_forks_visibility_level(new_visibility) + update_forks_visibility_level(new_visibility.to_i) end new_branch = params[:default_branch] @@ -44,11 +44,13 @@ module Projects end def update_forks_visibility_level(new_level) + return unless new_level < project.visibility_level + project.forks.each do |forked_project| fork_level = forked_project.visibility_level - if fork_level > new_level.to_i - forked_project.visibility_level = new_level.to_i + if fork_level > new_level + forked_project.visibility_level = new_level forked_project.save! end end From 1144b70ab624ee1c1e7f2de0c92de021a7b5ea8e Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Fri, 20 Nov 2015 00:13:56 +0100 Subject: [PATCH 053/908] Change update_forks_visibility_level into after_update hook in Project model --- app/models/project.rb | 13 +++++++++++++ app/services/projects/update_service.rb | 14 -------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 2562c441ab..b8495c6cd4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -61,6 +61,19 @@ class Project < ActiveRecord::Base update_column(:last_activity_at, self.created_at) end + # update visibility_levet of forks + after_update :update_forks_visibility_level + def update_forks_visibility_level + return unless visibility_level < visibility_level_was + + forks.each do |forked_project| + if forked_project.visibility_level > visibility_level + forked_project.visibility_level = visibility_level + forked_project.save! + end + end + end + ActsAsTaggableOn.strict_case_match = true acts_as_taggable_on :tags diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index cd7a8b1821..895e089bea 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -13,7 +13,6 @@ module Projects end return false unless visibility_level_allowed?(new_visibility) - update_forks_visibility_level(new_visibility.to_i) end new_branch = params[:default_branch] @@ -42,18 +41,5 @@ module Projects false end - - def update_forks_visibility_level(new_level) - return unless new_level < project.visibility_level - - project.forks.each do |forked_project| - fork_level = forked_project.visibility_level - - if fork_level > new_level - forked_project.visibility_level = new_level - forked_project.save! - end - end - end end end From 075e3661c534a06753065e9e3323168b786cdbe5 Mon Sep 17 00:00:00 2001 From: Felipe Orlando Date: Sun, 22 Nov 2015 06:04:20 -0200 Subject: [PATCH 054/908] Update autocomplete_controller to be more readable --- app/controllers/autocomplete_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index aa0268b8d6..77c8dafc01 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -9,7 +9,7 @@ class AutocompleteController < ApplicationController @users = @users.reorder(:name) @users = @users.page(params[:page]).per(PER_PAGE) - unless params[:search].present? + if params[:search].blank? # Include current user if available to filter by "Me" if params[:current_user] && current_user @users = [*@users, current_user].uniq From ab238a767713aeb2ae019e22831fc0049873870c Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 22 Nov 2015 07:23:56 -0800 Subject: [PATCH 055/908] Fix CSS styling for clipboard icon in new MR page Closes #3602 --- app/assets/stylesheets/framework/tw_bootstrap.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 50c0cf61f4..94f0ed761d 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -190,6 +190,10 @@ .btn { min-width: 124px; } + + .btn-clipboard { + min-width: 0px; + } } &.panel-small { From 8608c6325e19f529f7b43ff881c562d3a0114e1c Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 23 Nov 2015 09:42:20 +0100 Subject: [PATCH 056/908] Refactor MergeWhenBuildSucceedsService and incorporate feedback --- CHANGELOG | 5 +-- .../projects/merge_requests_controller.rb | 11 ++---- app/models/merge_request.rb | 11 ++++-- .../merge_when_build_succeeds_service.rb | 17 ++++++++- app/services/system_note_service.rb | 4 +-- .../widget/open/_accept.html.haml | 2 +- .../open/_merge_when_build_succeeds.html.haml | 9 ++--- config/routes.rb | 1 + db/schema.rb | 2 -- doc/api/merge_requests.md | 8 ++--- lib/api/merge_requests.rb | 36 +++++++------------ 11 files changed, 52 insertions(+), 54 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 08188324d7..18f5f270ec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) +- Merge when build succeeds (Zeger-Jan van de Weg) v 8.2.0 - Fix grouping of contributors by email in graph. @@ -28,10 +29,6 @@ v 8.2.0 - Allow to define cache in `.gitlab-ci.yml` - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) - Remove deprecated CI events from project settings page - - Use issue editor as cross reference comment author when issue is edited with a new mention. - - Merge when build succeeds (Zeger-Jan van de Weg) - -v 8.1.1 - [API] Add ability to fetch the commit ID of the last commit that actually touched a file - Fix omniauth documentation setting for omnibus configuration (Jon Cairns) - Add "New file" link to dropdown on project page diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 9db6ed5022..f2e9a34dd2 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -151,14 +151,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def cancel_merge_when_build_succeeds - unless @merge_request.can_be_merged_by?(current_user) || @merge_request.author == current_user - return access_denied! - end + return access_denied! unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user) - if @merge_request.merge_when_build_succeeds? - @merge_request.reset_merge_when_build_succeeds - SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user) - end + MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user).cancel(@merge_request) end def merge @@ -171,7 +166,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.update(merge_error: nil) - if params[:merge_when_build_succeeds] && @merge_request.ci_commit.active? + if params[:merge_when_build_succeeds] && @merge_request.ci_commit && @merge_request.ci_commit.active? MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) .execute(@merge_request) @status = :merge_when_build_succeeds diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 05c3bc074b..89f9e8fa6a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -254,8 +254,15 @@ class MergeRequest < ActiveRecord::Base end end - def mergeable_by_or_author(user) - self.can_be_merged_by?(user) || self.author == user + def can_cancel_merge_when_build_succeeds?(user) + can_be_merged_by?(user) || self.author == user + end + + def can_remove_source_branch? + for_fork? && + !project.protected_branch(source_branch) && + !project.repository.root_ref(source_branch) && + can?(current_user, :push_code, project) end def mr_and_commit_notes diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb index d5cae2f98f..15dcace5df 100644 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -1,5 +1,7 @@ module MergeRequests class MergeWhenBuildSucceedsService < MergeRequests::BaseService + # Marks the passed `merge_request` to be marked when the build succeeds or + # updates the params for the automatic merge def execute(merge_request) merge_request.merge_params.merge!(params) @@ -14,10 +16,11 @@ module MergeRequests merge_request.save unless already_approved - SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user) + SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.ci_commit) end end + # Triggers the automatic merge of merge_request once the build succeeds def trigger(build) merge_requests = merge_request_from(build) @@ -31,6 +34,18 @@ module MergeRequests end end + # Cancels the automatic merge + def cancel(merge_request) + if merge_request.merge_when_build_succeeds? && merge_request.open? && !merge_request.merged? + merge_request.reset_merge_when_build_succeeds + SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user) + + success + else + error("Can't cancel the automatic merge", 406) + end + end + private def merge_request_from(build) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 5e8281a3fd..7de4221c4c 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -131,8 +131,8 @@ class SystemNoteService end # Called when 'merge when build succeeds' is executed - def self.merge_when_build_succeeds(noteable, project, author) - body = "This merge request will be automatically merged when the build for #{noteable.ci_commit.short_sha} succeeds" + def self.merge_when_build_succeeds(noteable, project, author, ci_commit) + body = "Approved an automatic merge when the build for #{ci_commit.sha} succeeds" create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index d2189787d4..5ec623b472 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -13,7 +13,7 @@ - else = f.button class: "btn btn-create btn-grouped accept_merge_request #{status_class}" do Accept Merge Request - - if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && !@merge_request.for_fork? + - if @merge_request.can_remove_source_branch? .accept-control.checkbox = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do = check_box_tag :should_remove_source_branch diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index ddd1a7bd63..51e18f8442 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -8,20 +8,17 @@ The changes will be merged into %span.label-branch= @merge_request.target_branch The source branch will be removed. - - elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) - - remove_source_branch_button = true %p = succeed '.' do The changes will be merged into %span.label-branch= @merge_request.target_branch - The source branch won't be removed. + The source branch will not be removed. -- if remove_source_branch_button || @merge_request.can_be_merged_by?(current_user) .clearfix.prepend-top-10 - - if remove_source_branch_button + - if @merge_request.can_remove_source_branch? && !@merge_request.merge_params["should_remove_source_branch"].present? = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do = icon('times') Remove Source Branch When Merged - - if @merge_request.can_be_merged_by?(current_user) || @merge_request.author == current_user + - if @merge_request.merge_when_build_succeeds = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-warning btn-sm" do Cancel Automatic Merge diff --git a/config/routes.rb b/config/routes.rb index 4954378668..81f568cfa7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -570,6 +570,7 @@ Gitlab::Application.routes.draw do member do get :diffs get :commits + get :merge_check post :merge post :cancel_merge_when_build_succeeds get :ci_status diff --git a/db/schema.rb b/db/schema.rb index fa32617cb9..7d5ed55975 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -421,7 +421,6 @@ ActiveRecord::Schema.define(version: 20151116144118) do end add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree - add_index "labels", ["title"], name: "index_labels_on_title", using: :btree create_table "lfs_objects", force: true do |t| t.string "oid", null: false @@ -525,7 +524,6 @@ ActiveRecord::Schema.define(version: 20151116144118) do add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree - add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree create_table "namespaces", force: true do |t| t.string "name", null: false diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 1b2ad1caf4..19e6fbb788 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -296,7 +296,7 @@ Parameters: - `merge_request_id` (required) - ID of MR - `merge_commit_message` (optional) - Custom merge commit message - `should_remove_source_branch` (optional) - if `true` removes the source branch -- `merge_when_build_succeeds` (optional) - if `true` the MR is merge when the build succeeds +- `merged_when_build_succeeds` (optional) - if `true` the MR is merge when the build succeeds ```json { @@ -329,15 +329,13 @@ Parameters: ## Cancel Merge When Build Succeeds -Cancels the merge when build succeeds and reset the merge parameters - -If successfull you'll get `200 OK`. +If successful you'll get `200 OK`. If you don't have permissions to accept this merge request - you'll get a 401 If the merge request is already merged or closed - you get 405 and error message 'Method Not Allowed' -In case the merge request is not set to be merged when the build succeeds, you'll also get a 405 with the error message 'Method Not Allowed' +In case the merge request is not set to be merged when the build succeeds, you'll also get a 406 error. ``` PUT /projects/:id/merge_request/:merge_request_id/cancel_merge_when_build_succeeds ``` diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index f981432db3..b72f816932 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -182,39 +182,35 @@ module API # id (required) - The ID of a project # merge_request_id (required) - ID of MR # merge_commit_message (optional) - Custom merge commit message - # merge_when_build_succeeds (optional) - truethy when this MR should be merged when the build is succesfull + # should_remove_source_branch - When true, the source branch will be deleted if possible + # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds # Example: # PUT /projects/:id/merge_request/:merge_request_id/merge # put ":id/merge_request/:merge_request_id/merge" do merge_request = user_project.merge_requests.find(params[:merge_request_id]) - allowed = ::Gitlab::GitAccess.new(current_user, user_project). - can_push_to_branch?(merge_request.target_branch) - # Merge request can not be merged # because user dont have permissions to push into target branch - unauthorized! unless allowed + unauthorized! unless merge_request.can_be_merged_by?(current_user) - not_allowed! unless merge_request.open? + not_allowed! unless merge_request.open? && !merge_request.work_in_progress? merge_request.check_if_can_be_merged if merge_request.unchecked? + render_api_error!('Branch cannot be merged', 406) if merge_request.can_be_merged? + merge_params = { commit_message: params[:merge_commit_message], should_remove_source_branch: params[:should_remove_source_branch] } - if !merge_request.work_in_progress? - if parse_boolean(params[:merge_when_build_succeeds]) - ::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params). - execute(merge_request) - else - ::MergeRequests::MergeService.new(merge_request.target_project, current_user, merge_params). - execute(merge_request, merge_params) - end + if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.ci_commit && merge_request.ci_commit.active? + ::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params). + execute(merge_request) else - render_api_error!('Branch cannot be merged', 405) + ::MergeRequests::MergeService.new(merge_request.target_project, current_user, merge_params). + execute(merge_request) end present merge_request, with: Entities::MergeRequest @@ -233,15 +229,9 @@ module API # Merge request can not be merged # because user dont have permissions to push into target branch - unauthorized! unless allowed + unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) - if merge_request.merged? || !merge_request.open? || !merge_request.merge_when_build_succeeds? - not_allowed! - end - - merge_request.reset_merge_when_build_succeeds - - present merge_request, with: Entities::MergeRequest + ::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user).cancel(merge_request) end # Get a merge request's comments From 11728b50f96a296c760d9e69273d304f92e51f8f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 22 Nov 2015 14:27:30 +0100 Subject: [PATCH 057/908] Expose artifacts path --- CHANGELOG | 1 + app/models/application_setting.rb | 2 +- app/uploaders/artifact_uploader.rb | 6 +++--- config/gitlab.yml.example | 6 ++++++ config/initializers/1_settings.rb | 9 ++++++++- lib/ci/api/builds.rb | 2 ++ lib/gitlab/current_settings.rb | 2 +- 7 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 75e5b7585f..4b7218b594 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ v 8.3.0 (unreleased) v 8.2.0 - Improved performance of finding projects and groups in various places - Improved performance of rendering user profile pages and Atom feeds + - Expose build artifacts path as config option - Fix grouping of contributors by email in graph. - Improved performance of finding issues with/without labels - Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 9e70247ef5..b2d5fe1558 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -99,7 +99,7 @@ class ApplicationSetting < ActiveRecord::Base restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], - max_artifacts_size: Settings.gitlab_ci['max_artifacts_size'], + max_artifacts_size: Settings.artifacts['max_size'], ) end diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb index b4e0fc5772..1dccc39e7e 100644 --- a/app/uploaders/artifact_uploader.rb +++ b/app/uploaders/artifact_uploader.rb @@ -5,15 +5,15 @@ class ArtifactUploader < CarrierWave::Uploader::Base attr_accessor :build, :field def self.artifacts_path - File.expand_path('shared/artifacts/', Rails.root) + Gitlab.config.artifacts.path end def self.artifacts_upload_path - File.expand_path('shared/artifacts/tmp/uploads/', Rails.root) + File.join(self.artifacts_path, 'tmp/uploads/') end def self.artifacts_cache_path - File.expand_path('shared/artifacts/tmp/cache/', Rails.root) + File.join(self.artifacts_path, 'tmp/cache/') end def initialize(build, field) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 1788d4c2c7..1da42ab38f 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -124,6 +124,12 @@ production: &base # The mailbox where incoming mail will end up. Usually "inbox". mailbox: "inbox" + ## Build Artifacts + artifacts: + enabled: true + # The location where build artifacts are stored (default: shared/artifacts). + # path: shared/artifacts + ## Git LFS lfs: enabled: true diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index b498fb1e1d..b162b8a83f 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -187,7 +187,6 @@ Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_br Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil? Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url) Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root) -Settings.gitlab_ci['max_artifacts_size'] ||= 100 # in megabytes # # Reply by email @@ -199,6 +198,14 @@ Settings.incoming_email['ssl'] = false if Settings.incoming_email['ssl']. Settings.incoming_email['start_tls'] = false if Settings.incoming_email['start_tls'].nil? Settings.incoming_email['mailbox'] = "inbox" if Settings.incoming_email['mailbox'].nil? +# +# Build Artifacts +# +Settings['artifacts'] ||= Settingslogic.new({}) +Settings.artifacts['enabled'] = true if Settings.artifacts['enabled'].nil? +Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"), Rails.root) +Settings.artifacts['max_size'] ||= 100 # in megabytes + # # Git LFS # diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 0a58667280..15faa6edd8 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -58,6 +58,7 @@ module Ci # POST /builds/:id/artifacts/authorize post ":id/artifacts/authorize" do require_gitlab_workhorse! + not_allowed! unless Gitlab.config.artifacts.enabled build = Ci::Build.find_by_id(params[:id]) not_found! unless build authenticate_build_token!(build) @@ -91,6 +92,7 @@ module Ci # POST /builds/:id/artifacts post ":id/artifacts" do require_gitlab_workhorse! + not_allowed! unless Gitlab.config.artifacts.enabled build = Ci::Build.find_by_id(params[:id]) not_found! unless build authenticate_build_token!(build) diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 2d3e32d953..46a4ef0e31 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -25,7 +25,7 @@ module Gitlab session_expire_delay: Settings.gitlab['session_expire_delay'], import_sources: Settings.gitlab['import_sources'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], - max_artifacts_size: Ci::Settings.gitlab_ci['max_artifacts_size'], + max_artifacts_size: Settings.artifacts['max_size'], ) end From 57e974c03b693c37b6ca7e57ce86477e32b770f1 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 23 Nov 2015 13:49:40 +0100 Subject: [PATCH 058/908] Fix 500 when using CI - Fix for Ci::Build state machine, allowing to process builds without the project - Forcefully update builds that didn't want to update with state machine - Fix saving GitLabCiService as Admin Template --- CHANGELOG | 2 ++ app/models/ci/build.rb | 2 ++ app/models/project_services/gitlab_ci_service.rb | 1 + app/workers/stuck_ci_builds_worker.rb | 3 +++ 4 files changed, 8 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index fc7b6e75b1..1e39a9da59 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,8 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) + - Forcefully update builds that didn't want to update with state machine + - Fix: saving GitLabCiService as Admin Template v 8.2.0 - Improved performance of finding projects and groups in various places diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index e78b154084..52ce1b920f 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -97,6 +97,8 @@ module Ci state_machine :status, initial: :pending do after_transition any => [:success, :failed, :canceled] do |build, transition| + return unless build.gl_project + project = build.project if project.web_hooks? diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 095d04e0df..c5657b5070 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -30,6 +30,7 @@ class GitlabCiService < CiService end def ensure_gitlab_ci_project + return unless project project.ensure_gitlab_ci_project end diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb index ad02a3b16d..4e5eddbaba 100644 --- a/app/workers/stuck_ci_builds_worker.rb +++ b/app/workers/stuck_ci_builds_worker.rb @@ -14,5 +14,8 @@ class StuckCiBuildsWorker Rails.logger.debug "Dropping stuck #{build.status} build #{build.id} for runner #{build.runner_id}" build.drop end + + # Update builds that failed to drop + builds.update_all(status: 'failed') end end From e4f3d05c74292ae2dbc3563db38325d982cd6f99 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Mon, 23 Nov 2015 18:35:16 +0100 Subject: [PATCH 059/908] Add tests for (Create|Update)ReleaseService --- spec/services/create_release_service_spec.rb | 34 ++++++++++++++++++++ spec/services/update_release_service_spec.rb | 34 ++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 spec/services/create_release_service_spec.rb create mode 100644 spec/services/update_release_service_spec.rb diff --git a/spec/services/create_release_service_spec.rb b/spec/services/create_release_service_spec.rb new file mode 100644 index 0000000000..26d7f365bb --- /dev/null +++ b/spec/services/create_release_service_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe CreateReleaseService do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:tag_name) { project.repository.tag_names.first } + let(:description) { 'Awesome release!' } + let(:service) { CreateReleaseService.new(project, user) } + + it 'creates a new release' do + result = service.execute(tag_name, description) + expect(result[:status]).to eq(:success) + release = project.releases.find_by(tag: tag_name) + expect(release).not_to be_nil + expect(release.description).to eq(description) + end + + it 'raises an error if the tag does not exist' do + result = service.execute("foobar", description) + expect(result[:status]).to eq(:error) + end + + context 'there already exists a release on a tag' do + before do + service.execute(tag_name, description) + end + + it 'raises an error and does not update the release' do + result = service.execute(tag_name, 'The best release!') + expect(result[:status]).to eq(:error) + expect(project.releases.find_by(tag: tag_name).description).to eq(description) + end + end +end diff --git a/spec/services/update_release_service_spec.rb b/spec/services/update_release_service_spec.rb new file mode 100644 index 0000000000..93368c45b8 --- /dev/null +++ b/spec/services/update_release_service_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe UpdateReleaseService do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:tag_name) { project.repository.tag_names.first } + let(:description) { 'Awesome release!' } + let(:new_description) { 'The best release!' } + let(:service) { UpdateReleaseService.new(project, user) } + + context 'with an existing release' do + let(:create_service) { CreateReleaseService.new(project, user) } + + before do + create_service.execute(tag_name, description) + end + + it 'successfully updates an existing release' do + result = service.execute(tag_name, new_description) + expect(result[:status]).to eq(:success) + expect(project.releases.find_by(tag: tag_name).description).to eq(new_description) + end + end + + it 'raises an error if the tag does not exist' do + result = service.execute("foobar", description) + expect(result[:status]).to eq(:error) + end + + it 'raises an error if the release does not exist' do + result = service.execute(tag_name, description) + expect(result[:status]).to eq(:error) + end +end From 62303bfad1268dbb466f0bfa68fef40958c6d287 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 23 Nov 2015 16:51:16 -0500 Subject: [PATCH 060/908] Update CHANGELOG [ci skip] --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 1e39a9da59..eb5badf6a9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,8 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) + +v 8.2.1 - Forcefully update builds that didn't want to update with state machine - Fix: saving GitLabCiService as Admin Template From d6e0a1883b9b0eb0faabc77f6c439d981e64a84a Mon Sep 17 00:00:00 2001 From: Anton Davydov Date: Tue, 24 Nov 2015 02:19:17 +0300 Subject: [PATCH 061/908] Fix typos in all docs [skip ci] --- doc/api/settings.md | 2 +- doc/development/db_dump.md | 2 +- doc/raketasks/backup_restore.md | 4 ++-- doc/release/monthly.md | 2 +- doc/web_hooks/web_hooks.md | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/api/settings.md b/doc/api/settings.md index d1b93a09c0..96867c6791 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -57,7 +57,7 @@ Parameters: - `default_project_visibility` - what visibility level new project receives - `default_snippet_visibility` - what visibility level new snippet receives - `restricted_signup_domains` - force people to use only corporate emails for signup -- `user_oauth_applications` - allow users to create oauth applicaitons +- `user_oauth_applications` - allow users to create oauth applications - `after_sign_out_path` - where redirect user after logout All parameters are optional. You can send only one that you want to change. diff --git a/doc/development/db_dump.md b/doc/development/db_dump.md index 21f1b3edec..e4ff72aa34 100644 --- a/doc/development/db_dump.md +++ b/doc/development/db_dump.md @@ -1,4 +1,4 @@ -# Importing a database dump into a staging enviroment +# Importing a database dump into a staging environment Sometimes it is useful to import the database from a production environment into a staging environment for testing. The procedure below assumes you have diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 4e645b21a8..014f6a12df 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -360,8 +360,8 @@ If you are using backup restore procedures you might encounter the following war ``` psql:/var/opt/gitlab/backups/db/database.sql:22: ERROR: must be owner of extension plpgsql -psql:/var/opt/gitlab/backups/db/database.sql:2931: WARNING: no privileges could be revoked for "public" (two occurences) -psql:/var/opt/gitlab/backups/db/database.sql:2933: WARNING: no privileges were granted for "public" (two occurences) +psql:/var/opt/gitlab/backups/db/database.sql:2931: WARNING: no privileges could be revoked for "public" (two occurrences) +psql:/var/opt/gitlab/backups/db/database.sql:2933: WARNING: no privileges were granted for "public" (two occurrences) ``` diff --git a/doc/release/monthly.md b/doc/release/monthly.md index aff3f066b2..907c19e65a 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -176,7 +176,7 @@ Tweet about the RC release: 1. Also check the CI changelog 1. Add a proposed tweet text to the blog post WIP MR description. 1. Create a WIP MR for the blog post -1. Make sure merge request title starts with `WIP` so it can not be accidently merged until ready. +1. Make sure merge request title starts with `WIP` so it can not be accidentally merged until ready. 1. Ask Dmitriy (or a team member with OS X) to add screenshots to the WIP MR. 1. Decide with core team who will be the MVP user. 1. Create WIP MR for adding MVP to MVP page on website diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 7d838187a2..d24aa0293c 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -184,7 +184,7 @@ X-Gitlab-Event: Note Hook { "object_kind": "note", "user": { - "name": "Adminstrator", + "name": "Administrator", "username": "root", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" }, @@ -337,7 +337,7 @@ X-Gitlab-Event: Note Hook { "object_kind": "note", "user": { - "name": "Adminstrator", + "name": "Administrator", "username": "root", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" }, From 97f8c6279fc39c4bad87bb880eba04802f6d351d Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 19 Nov 2015 12:33:58 +0100 Subject: [PATCH 062/908] Added total query time to Sherlock This makes it easier to see if a problem is caused by slow queries or slow Ruby code (unrelated to any SQL queries that might be used). --- app/views/sherlock/transactions/_general.html.haml | 6 ++++++ config/locales/sherlock.en.yml | 1 + lib/gitlab/sherlock/transaction.rb | 5 +++++ spec/lib/gitlab/sherlock/transaction_spec.rb | 13 +++++++++++++ 4 files changed, 25 insertions(+) diff --git a/app/views/sherlock/transactions/_general.html.haml b/app/views/sherlock/transactions/_general.html.haml index 4287a0c320..8533b130da 100644 --- a/app/views/sherlock/transactions/_general.html.haml +++ b/app/views/sherlock/transactions/_general.html.haml @@ -25,6 +25,12 @@ %strong = @transaction.duration.round(2) = t('sherlock.seconds') + %li + %span.light + #{t('sherlock.query_time')} + %strong + = @transaction.query_duration.round(2) + = t('sherlock.seconds') %li %span.light #{t('sherlock.finished_at')}: diff --git a/config/locales/sherlock.en.yml b/config/locales/sherlock.en.yml index 683b09dc32..f24b825f58 100644 --- a/config/locales/sherlock.en.yml +++ b/config/locales/sherlock.en.yml @@ -35,3 +35,4 @@ en: events: Events percent: '%' count: Count + query_time: Query Time diff --git a/lib/gitlab/sherlock/transaction.rb b/lib/gitlab/sherlock/transaction.rb index d87a4c9bb4..3489fb251b 100644 --- a/lib/gitlab/sherlock/transaction.rb +++ b/lib/gitlab/sherlock/transaction.rb @@ -36,6 +36,11 @@ module Gitlab @duration ||= started_at && finished_at ? finished_at - started_at : 0 end + # Returns the total query duration in seconds. + def query_duration + @query_duration ||= @queries.map { |q| q.duration }.inject(:+) / 1000.0 + end + def to_param @id end diff --git a/spec/lib/gitlab/sherlock/transaction_spec.rb b/spec/lib/gitlab/sherlock/transaction_spec.rb index bb49fb65cf..fb80c62c79 100644 --- a/spec/lib/gitlab/sherlock/transaction_spec.rb +++ b/spec/lib/gitlab/sherlock/transaction_spec.rb @@ -84,6 +84,19 @@ describe Gitlab::Sherlock::Transaction do end end + describe '#query_duration' do + it 'returns the total query duration in seconds' do + time = Time.now + query1 = Gitlab::Sherlock::Query.new('SELECT 1', time, time + 5) + query2 = Gitlab::Sherlock::Query.new('SELECT 2', time, time + 2) + + transaction.queries << query1 + transaction.queries << query2 + + expect(transaction.query_duration).to be_within(0.1).of(7.0) + end + end + describe '#to_param' do it 'returns the transaction ID' do expect(transaction.to_param).to eq(transaction.id) From d1a73563985a2fdaf7a99fc469c38e957d238fde Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Nov 2015 11:36:53 +0100 Subject: [PATCH 063/908] Fix ultra-light color for small text Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/typography.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index ba0312ba0d..2c4a58c8db 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -256,3 +256,9 @@ textarea.js-gfm-input { .strikethrough { text-decoration: line-through; } + +h1, h2, h3, h4 { + small { + color: $gl-gray; + } +} From 9595ec036a0f6f0d2e18e47094431b5a1b6d427f Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 24 Nov 2015 12:11:31 +0100 Subject: [PATCH 064/908] Maybe rescue session_expire_delay by setting a default value. --- config/initializers/session_store.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index d7c5432da7..eb534625e3 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -7,6 +7,7 @@ include Gitlab::CurrentSettings begin Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay rescue + Settings.gitlab['session_expire_delay'] ||= 10080 end unless Rails.env.test? From 01bd7baf4fbf23507f6adcdaebf3946b892eb001 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 24 Nov 2015 12:21:11 +0100 Subject: [PATCH 065/908] Spell out commands for omnibus-gitlab packages. --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 007e410b67..446eb1189a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,10 +47,10 @@ Please send a merge request with a tested solution or a merge request with a fai 1. **Observed behavior** 1. **Relevant logs and/or screenshots:** Please use code blocks (\`\`\`) to format console output, logs, and code as it's very hard to read otherwise. 1. **Output of checks** - * Results of GitLab [Application Check](doc/install/installation.md#check-application-status) (`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`); we will only investigate if the tests are passing + * Results of GitLab [Application Check](doc/install/installation.md#check-application-status) (For installations with omnibus-gitlab package: `sudo gitlab-rake gitlab:check SANITIZE=true`); For installations from source: `sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`); we will only investigate if the tests are passing * Version of GitLab you are running; we will only investigate issues in the latest stable and development releases as per the [maintenance policy](MAINTENANCE.md) * Add the last commit SHA-1 of the GitLab version you used to replicate the issue (obtainable from the help page) - * Describe your setup (use relevant parts from `sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`) + * Describe your setup (use relevant parts from the env info: For installations with omnibus-gitlab package: `sudo gitlab-rake gitlab:env:info`; For installations from source: `sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`) 1. **Possible fixes**: If you can, link to the line of code that might be responsible for the problem ## Merge requests From 0985032132a2dfa02f2968a3e959df8629b77b12 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 24 Nov 2015 15:57:28 +0100 Subject: [PATCH 066/908] Also fallback to a default value if none is set. --- config/initializers/session_store.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index eb534625e3..f30178ff71 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -3,9 +3,9 @@ require 'gitlab/current_settings' include Gitlab::CurrentSettings -# allow it to fail: it may to do so when create_from_defaults is executed before migrations are actually done +# allow it to fail: it may do so when create_from_defaults is executed before migrations are actually done begin - Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay + Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay || 10080 rescue Settings.gitlab['session_expire_delay'] ||= 10080 end From 4caef63d82143518e6f23ec6183021657d7ed1ae Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Nov 2015 16:13:21 +0100 Subject: [PATCH 067/908] Fix 500 error when update group member permission Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + .../group_members/_group_member.html.haml | 2 +- app/views/groups/group_members/update.js.haml | 2 +- features/groups.feature | 7 +++ features/steps/groups.rb | 51 +++++++++++++++---- 5 files changed, 51 insertions(+), 12 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 90ade156ca..8916b9f040 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.3.0 (unreleased) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) + - Fix 500 error when update group member permission v 8.2.1 - Forcefully update builds that didn't want to update with state machine diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index be94b1abc1..bae67552a1 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -30,7 +30,7 @@ - if should_user_see_group_roles?(current_user, @group) %span.pull-right - %strong= member.human_access + %strong.member-access-level= member.human_access - if show_controls - if can?(current_user, :update_group_member, member) = button_tag class: "btn-xs btn js-toggle-button", diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml index 5bad48abaf..df726e2b2b 100644 --- a/app/views/groups/group_members/update.js.haml +++ b/app/views/groups/group_members/update.js.haml @@ -1,2 +1,2 @@ :plain - $("##{dom_id(@member)}").replaceWith('#{escape_javascript(render(@member, member: @member, show_controls: true))}'); + $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member, show_controls: true))}'); diff --git a/features/groups.feature b/features/groups.feature index abf3769a84..938e658f2a 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -74,6 +74,13 @@ Feature: Groups When I select "sjobs@apple.com" as "Reporter" Then I should see "sjobs@apple.com" in team list as invited "Reporter" + @javascript + Scenario: Edit group member permissions + Given "Mary Jane" is guest of group "Owned" + And I visit group "Owned" members page + When I change the "Mary Jane" role to "Developer" + Then I should see "Mary Jane" as "Developer" + # Leave @javascript diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 9c0313537b..1d530a2528 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -26,7 +26,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'Group "Owned" has a public project "Public-project"' do - group = Group.find_by(name: "Owned") + group = owned_group @project = create :empty_project, :public, group: group, @@ -91,7 +91,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see group "Owned" projects list' do - Group.find_by(name: "Owned").projects.each do |project| + owned_group.projects.each do |project| expect(page).to have_link project.name end end @@ -172,12 +172,12 @@ class Spinach::Features::Groups < Spinach::FeatureSteps step 'I change group "Owned" avatar' do attach_file(:group_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')) click_button "Save group" - Group.find_by(name: "Owned").reload + owned_group.reload end step 'I should see new group "Owned" avatar' do - expect(Group.find_by(name: "Owned").avatar).to be_instance_of AvatarUploader - expect(Group.find_by(name: "Owned").avatar.url).to eq "/uploads/group/avatar/#{ Group.find_by(name:"Owned").id }/banana_sample.gif" + expect(owned_group.avatar).to be_instance_of AvatarUploader + expect(owned_group.avatar.url).to eq "/uploads/group/avatar/#{ Group.find_by(name:"Owned").id }/banana_sample.gif" end step 'I should see the "Remove avatar" button' do @@ -187,16 +187,16 @@ class Spinach::Features::Groups < Spinach::FeatureSteps step 'I have group "Owned" avatar' do attach_file(:group_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')) click_button "Save group" - Group.find_by(name: "Owned").reload + owned_group.reload end step 'I remove group "Owned" avatar' do click_link "Remove avatar" - Group.find_by(name: "Owned").reload + owned_group.reload end step 'I should not see group "Owned" avatar' do - expect(Group.find_by(name: "Owned").avatar?).to eq false + expect(owned_group.avatar?).to eq false end step 'I should not see the "Remove avatar" button' do @@ -295,18 +295,49 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end end + step 'I change the "Mary Jane" role to "Developer"' do + member = mary_jane_member + + page.within "#group_member_#{member.id}" do + find(".js-toggle-button").click + page.within "#edit_group_member_#{member.id}" do + select 'Developer', from: 'group_member_access_level' + click_on 'Save' + end + end + end + + step 'I should see "Mary Jane" as "Developer"' do + member = mary_jane_member + + page.within "#group_member_#{member.id}" do + page.within '.member-access-level' do + expect(page).to have_content "Developer" + end + end + end + protected + def owned_group + @owned_group ||= Group.find_by(name: "Owned") + end + + def mary_jane_member + user = User.find_by(name: "Mary Jane") + member = owned_group.members.where(user_id: user.id).first + end + def assigned_to_me(key) project.send(key).where(assignee_id: current_user.id) end def project - Group.find_by(name: "Owned").projects.first + owned_group.projects.first end def group_milestone - group = Group.find_by(name: "Owned") + group = owned_group @project1 = create :project, group: group From cb9f573818098b2fdc556db867bd9a32f469d471 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Nov 2015 16:17:11 +0100 Subject: [PATCH 068/908] Fix rubocop complain Signed-off-by: Dmitriy Zaporozhets --- features/steps/groups.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 1d530a2528..5086a9e0b8 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -325,7 +325,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps def mary_jane_member user = User.find_by(name: "Mary Jane") - member = owned_group.members.where(user_id: user.id).first + owned_group.members.where(user_id: user.id).first end def assigned_to_me(key) From cbaa33db8a77efaf6446ed294db2e334ff7de8e7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Nov 2015 17:40:41 +0100 Subject: [PATCH 069/908] Small code improvement Signed-off-by: Dmitriy Zaporozhets --- features/steps/groups.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 5086a9e0b8..7c991af4c2 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -325,7 +325,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps def mary_jane_member user = User.find_by(name: "Mary Jane") - owned_group.members.where(user_id: user.id).first + owned_group.members.find_by(user_id: user.id) end def assigned_to_me(key) From 776027e4c7a4e7ff5df18e92cd79461a7d5de73e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 24 Nov 2015 15:36:36 -0500 Subject: [PATCH 070/908] Bump gon to ~> 6.0.1 Closes #2856 --- Gemfile | 2 +- Gemfile.lock | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index bd8e9bd8fa..3bb9448e40 100644 --- a/Gemfile +++ b/Gemfile @@ -193,7 +193,7 @@ gem 'addressable', '~> 2.3.8' gem 'bootstrap-sass', '~> 3.0' gem 'font-awesome-rails', '~> 4.2' gem 'gitlab_emoji', '~> 0.1' -gem 'gon', '~> 5.0.0' +gem 'gon', '~> 6.0.1' gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-rails', '~> 3.1.3' gem 'jquery-scrollto-rails', '~> 1.4.3' diff --git a/Gemfile.lock b/Gemfile.lock index 448941cfc8..70c532f05d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -312,9 +312,11 @@ GEM rouge (~> 1.10.1) sanitize (~> 2.1.0) stringex (~> 2.5.1) - gon (5.0.4) - actionpack (>= 2.3.0) + gon (6.0.1) + actionpack (>= 3.0) json + multi_json + request_store (>= 1.0) grape (0.13.0) activesupport builder @@ -846,7 +848,7 @@ DEPENDENCIES gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.0.2) - gon (~> 5.0.0) + gon (~> 6.0.1) grape (~> 0.13.0) grape-entity (~> 0.4.2) haml-rails (~> 0.9.0) From 26f9dbf8a95d79eccf6a3dd3b779b7a7bc51c03b Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 24 Nov 2015 15:42:42 -0500 Subject: [PATCH 071/908] Bump creole to ~> 0.5.0 Closes #2815 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index bd8e9bd8fa..bcbdabb5d9 100644 --- a/Gemfile +++ b/Gemfile @@ -95,7 +95,7 @@ gem 'redcarpet', '~> 3.3.3' gem 'RedCloth', '~> 4.2.9' gem 'rdoc', '~>3.6' gem 'org-ruby', '~> 0.9.12' -gem 'creole', '~>0.3.6' +gem 'creole', '~> 0.5.0' gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 1.5.2' diff --git a/Gemfile.lock b/Gemfile.lock index 448941cfc8..4d60d8fadc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -134,7 +134,7 @@ GEM thor (~> 0.19.1) crack (0.4.2) safe_yaml (~> 1.0.0) - creole (0.3.8) + creole (0.5.0) d3_rails (3.5.6) railties (>= 3.1.0) daemons (1.2.3) @@ -816,7 +816,7 @@ DEPENDENCIES colored (~> 1.2) colorize (~> 0.5.8) coveralls (~> 0.8.2) - creole (~> 0.3.6) + creole (~> 0.5.0) d3_rails (~> 3.5.5) database_cleaner (~> 1.4.0) default_value_for (~> 3.0.0) From 446a95d3079e8927dd40c8d41a58d66192721cfc Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 24 Nov 2015 15:48:49 -0500 Subject: [PATCH 072/908] Bump rack-oauth2 to ~> 1.2.1 --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index bd8e9bd8fa..0436e93065 100644 --- a/Gemfile +++ b/Gemfile @@ -28,7 +28,7 @@ gem 'omniauth-saml', '~> 1.4.0' gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd' -gem 'rack-oauth2', '~> 1.0.5' +gem 'rack-oauth2', '~> 1.2.1' # Two-factor authentication gem 'devise-two-factor', '~> 2.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index 448941cfc8..ecd83b6979 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -357,7 +357,7 @@ GEM httparty (0.13.5) json (~> 1.8) multi_xml (>= 0.5.2) - httpclient (2.6.0.1) + httpclient (2.7.0.1) i18n (0.7.0) ice_cube (0.11.1) ice_nine (0.11.1) @@ -502,7 +502,7 @@ GEM rack-cors (0.4.0) rack-mount (0.8.3) rack (>= 1.0.0) - rack-oauth2 (1.0.10) + rack-oauth2 (1.2.1) activesupport (>= 2.3) attr_required (>= 0.0.5) httpclient (>= 2.4) @@ -889,7 +889,7 @@ DEPENDENCIES quiet_assets (~> 1.0.2) rack-attack (~> 4.3.0) rack-cors (~> 0.4.0) - rack-oauth2 (~> 1.0.5) + rack-oauth2 (~> 1.2.1) rails (= 4.1.14) raphael-rails (~> 2.1.2) rblineprof From b9828278127724ff5eac60eb71e36f675b9985cd Mon Sep 17 00:00:00 2001 From: Eirik Lygre Date: Tue, 24 Nov 2015 20:55:18 +0000 Subject: [PATCH 073/908] Add reference to Microsoft's Git Credential Manager for Windows. --- doc/workflow/lfs/manage_large_binaries_with_git_lfs.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 210a8f71c3..b59e92cb31 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -121,6 +121,6 @@ git config --global credential.helper 'cache --timeout=3600' This will remember the credentials for an hour after which Git operations will require re-authentication. -If you are using OS X you can use `osxkeychain` to store and encrypt your credentials. For Windows, `wincred` is available. +If you are using OS X you can use `osxkeychain` to store and encrypt your credentials. For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases). -More details about various methods of storing the user credentials can be found on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). +More details about various methods of storing the user credentials can be found on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). \ No newline at end of file From a2915aeb662e30537f3f226e3db6d85fd252905f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 24 Nov 2015 16:12:25 -0500 Subject: [PATCH 074/908] Rescue StandardError, not Exception --- app/services/merge_requests/merge_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 7963af127e..d619b72e3c 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -38,7 +38,7 @@ module MergeRequests } repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options) - rescue Exception => e + rescue StandardError => e merge_request.update(merge_error: "Something went wrong during merge") Rails.logger.error(e.message) return false From 28de6770fae44f65aa81538ff660fc7050072070 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 24 Nov 2015 16:29:21 -0500 Subject: [PATCH 075/908] Bump colorize to ~> 0.7.0 Also removes `colored` which came in during the CI merge and is redundant. Closes #2822 --- Gemfile | 3 +-- Gemfile.lock | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index bd8e9bd8fa..b7ec16198c 100644 --- a/Gemfile +++ b/Gemfile @@ -125,8 +125,7 @@ gem 'sidetiq', '~> 0.6.3' gem "httparty", '~> 0.13.3' # Colored output to console -gem "colored", '~> 1.2' -gem "colorize", '~> 0.5.8' +gem "colorize", '~> 0.7.0' # GitLab settings gem 'settingslogic', '~> 2.0.9' diff --git a/Gemfile.lock b/Gemfile.lock index 448941cfc8..20fc885481 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -123,8 +123,7 @@ GEM coffee-script-source execjs coffee-script-source (1.9.1.1) - colored (1.2) - colorize (0.5.8) + colorize (0.7.7) connection_pool (2.2.0) coveralls (0.8.2) json (~> 1.8) @@ -813,8 +812,7 @@ DEPENDENCIES carrierwave (~> 0.9.0) charlock_holmes (~> 0.7.3) coffee-rails (~> 4.1.0) - colored (~> 1.2) - colorize (~> 0.5.8) + colorize (~> 0.7.0) coveralls (~> 0.8.2) creole (~> 0.3.6) d3_rails (~> 3.5.5) From af65046c5fd9f214124d39a5582b06ee2fe39933 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 22 Nov 2015 23:52:02 -0500 Subject: [PATCH 076/908] Move HTTP/SSH clone button logic to helpers --- app/helpers/button_helper.rb | 35 +++++++++++++++++++++++++ app/views/shared/_clone_panel.html.haml | 18 ++----------- 2 files changed, 37 insertions(+), 16 deletions(-) create mode 100644 app/helpers/button_helper.rb diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb new file mode 100644 index 0000000000..8963e96ea1 --- /dev/null +++ b/app/helpers/button_helper.rb @@ -0,0 +1,35 @@ +module ButtonHelper + def http_clone_button(project) + klass = 'btn' + klass << ' active' if default_clone_protocol == 'http' + klass << ' has_tooltip' if current_user.try(:require_password?) + + protocol = gitlab_config.protocol.upcase + + content_tag :button, protocol, + class: klass, + data: { + clone: project.http_url_to_repo, + container: 'body', + html: 'true', + title: "Set a password on your account
to pull or push via #{protocol}" + }, + type: :button + end + + def ssh_clone_button(project) + klass = 'btn' + klass << ' active' if default_clone_protocol == 'ssh' + klass << ' has_tooltip' if current_user.try(:require_ssh_key?) + + content_tag :button, 'SSH', + class: klass, + data: { + clone: project.ssh_url_to_repo, + container: 'body', + html: 'true', + title: 'Add an SSH key to your profile
to pull or push via SSH.' + }, + type: :button + end +end diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 8bcb24ae9d..a82971157d 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -2,23 +2,9 @@ .git-clone-holder.input-group .input-group-addon.git-protocols .input-group-btn - %button{ | - type: 'button', | - class: "btn #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", | - :"data-clone" => project.ssh_url_to_repo, | - :"data-title" => "Add an SSH key to your profile
to pull or push via SSH.", - :"data-html" => "true", - :"data-container" => "body"} - SSH + = ssh_clone_button(project) .input-group-btn - %button{ | - type: 'button', | - class: "btn #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", | - :"data-clone" => project.http_url_to_repo, | - :"data-title" => "Set a password on your account
to pull or push via #{gitlab_config.protocol.upcase}.", - :"data-html" => "true", - :"data-container" => "body"} - = gitlab_config.protocol.upcase + = http_clone_button(project) = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true - if project.kind_of?(Project) .input-group-addon.has_tooltip{title: "#{visibility_level_label(project.visibility_level)} project", data: { container: "body" } } From acc0f162c864d2a061461467473fca8761b6611f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 22 Nov 2015 23:53:55 -0500 Subject: [PATCH 077/908] Move clipboard_button helper to ButtonHelper module --- app/helpers/button_helper.rb | 7 +++++++ app/helpers/clipboard_helper.rb | 8 -------- 2 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 app/helpers/clipboard_helper.rb diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 8963e96ea1..bbb35afca8 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -1,4 +1,11 @@ module ButtonHelper + def clipboard_button + content_tag :button, + icon('clipboard'), + class: 'btn btn-xs btn-clipboard js-clipboard-trigger', + type: :button + end + def http_clone_button(project) klass = 'btn' klass << ' active' if default_clone_protocol == 'http' diff --git a/app/helpers/clipboard_helper.rb b/app/helpers/clipboard_helper.rb deleted file mode 100644 index 3c1d7569fa..0000000000 --- a/app/helpers/clipboard_helper.rb +++ /dev/null @@ -1,8 +0,0 @@ -module ClipboardHelper - def clipboard_button - content_tag :button, - icon('clipboard'), - class: 'btn btn-xs btn-clipboard js-clipboard-trigger', - type: :button - end -end From 7dab8ed739359bc579d8bc4d3de61816993ca57d Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 24 Nov 2015 18:35:24 -0500 Subject: [PATCH 078/908] Rework the copy_to_clipboard logic It needed to be more flexible in how we set the target text or element. --- .../javascripts/copy_to_clipboard.js.coffee | 63 ++++++++++--------- app/helpers/button_helper.rb | 20 +++++- app/views/projects/commits/_commit.html.haml | 4 +- .../projects/issues/_discussion.html.haml | 4 +- .../merge_requests/_discussion.html.haml | 4 +- 5 files changed, 58 insertions(+), 37 deletions(-) diff --git a/app/assets/javascripts/copy_to_clipboard.js.coffee b/app/assets/javascripts/copy_to_clipboard.js.coffee index 9c68c5cc1b..24301e01b1 100644 --- a/app/assets/javascripts/copy_to_clipboard.js.coffee +++ b/app/assets/javascripts/copy_to_clipboard.js.coffee @@ -1,32 +1,37 @@ #= require clipboard +genericSuccess = (e) -> + showTooltip(e.trigger, 'Copied!') + + # Clear the selection and blur the trigger so it loses its border + e.clearSelection() + $(e.trigger).blur() + +# Safari doesn't support `execCommand`, so instead we inform the user to +# copy manually. +# +# See http://clipboardjs.com/#browser-support +genericError = (e) -> + if /Mac/i.test(navigator.userAgent) + key = '⌘' # Command + else + key = 'Ctrl' + + showTooltip(e.trigger, "Press #{key}-C to copy") + +showTooltip = (target, title) -> + $(target). + tooltip( + container: 'body' + html: 'true' + placement: 'auto bottom' + title: title + trigger: 'manual' + ). + tooltip('show'). + one('mouseleave', -> $(this).tooltip('hide')) + $ -> - clipboard = new Clipboard '.js-clipboard-trigger', - text: (trigger) -> - $target = $(trigger.nextElementSibling || trigger.previousElementSibling) - $target.data('clipboard-text') || $target.text().trim() - - clipboard.on 'success', (e) -> - $(e.trigger). - tooltip(trigger: 'manual', placement: 'auto bottom', title: 'Copied!'). - tooltip('show'). - one('mouseleave', -> $(this).tooltip('hide')) - - # Clear the selection and blur the trigger so it loses its border - e.clearSelection() - $(e.trigger).blur() - - # Safari doesn't support `execCommand`, so instead we inform the user to - # copy manually. - # - # See http://clipboardjs.com/#browser-support - clipboard.on 'error', (e) -> - if /Mac/i.test(navigator.userAgent) - title = "Press ⌘-C to copy" - else - title = "Press Ctrl-C to copy" - - $(e.trigger). - tooltip(trigger: 'manual', placement: 'auto bottom', html: true, title: title). - tooltip('show'). - one('mouseleave', -> $(this).tooltip('hide')) + clipboard = new Clipboard '[data-clipboard-target], [data-clipboard-text]' + clipboard.on 'success', genericSuccess + clipboard.on 'error', genericError diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index bbb35afca8..b9bb1ac8d8 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -1,8 +1,24 @@ module ButtonHelper - def clipboard_button + # Output a "Copy to Clipboard" button + # + # data - Data attributes passed to `content_tag` + # + # Examples: + # + # # Define the clipboard's text + # clipboard_button(clipboard_text: "Foo") + # # => "" + # + # # Define the target element + # clipboard_button(clipboard_target: "#foo") + # # => "" + # + # See http://clipboardjs.com/#usage + def clipboard_button(data = {}) content_tag :button, icon('clipboard'), - class: 'btn btn-xs btn-clipboard js-clipboard-trigger', + class: 'btn btn-xs btn-clipboard', + data: data, type: :button end diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 805be332e6..2e489a0a4d 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -20,8 +20,8 @@ - if ci_commit = render_ci_status(ci_commit)   - = clipboard_button - = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id", data: {clipboard_text: commit.id} + = clipboard_button(clipboard_text: commit.id) + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" .notes_count - if note_count > 0 diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 020952dd00..8f0a1ed9be 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -18,9 +18,9 @@ = link_to_member(@project, participant, name: false, size: 24) .col-md-3 .input-group.cross-project-reference - %span.slead.has_tooltip{title: 'Cross-project reference'} + %span#cross-project-reference.slead.has_tooltip{title: 'Cross-project reference'} = cross_project_reference(@project, @issue) - = clipboard_button + = clipboard_button(clipboard_target: '#cross-project-reference') .row %section.col-md-9 diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index cb75bd8c5b..2b3c3eff5e 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -15,9 +15,9 @@ = render "projects/merge_requests/show/participants" .col-md-3 .input-group.cross-project-reference - %span.slead.has_tooltip{title: 'Cross-project reference'} + %span#cross-project-reference.slead.has_tooltip{title: 'Cross-project reference'} = cross_project_reference(@project, @merge_request) - = clipboard_button + = clipboard_button(clipboard_target: '#cross-project-reference') .row %section.col-md-9 From ec002e802677a691d758ebc6f94f23020b2b063a Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 24 Nov 2015 19:20:23 -0500 Subject: [PATCH 079/908] Remove usage of Colored --- lib/tasks/gitlab/task_helpers.rake | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index c95b6540eb..efb863a876 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -2,16 +2,6 @@ module Gitlab class TaskAbortedByUserError < StandardError; end end -unless STDOUT.isatty - module Colored - extend self - - def colorize(string, options={}) - string - end - end -end - namespace :gitlab do # Ask if the user wants to continue @@ -103,7 +93,7 @@ namespace :gitlab do gitlab_user = Gitlab.config.gitlab.user current_user = run(%W(whoami)).chomp unless current_user == gitlab_user - puts "#{Colored.color(:black)+Colored.color(:on_yellow)} Warning #{Colored.extra(:clear)}" + puts " Warning ".colorize(:black).on_yellow puts " You are running as user #{current_user.magenta}, we hope you know what you are doing." puts " Things may work\/fail for the wrong reasons." puts " For correct results you should run this as user #{gitlab_user.magenta}." From 1d80cd315e403e02f85a99c35eb9e83f4d829f8d Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 24 Nov 2015 19:32:06 -0500 Subject: [PATCH 080/908] Add clipboard button to project clone panel Closes #3585 --- app/assets/javascripts/project.js.coffee | 10 ++++------ app/assets/stylesheets/pages/projects.scss | 7 ++++++- app/helpers/button_helper.rb | 4 ++-- app/helpers/projects_helper.rb | 3 +-- app/views/shared/_clone_panel.html.haml | 2 ++ 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index 0ea8fffce0..d37a00bded 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -3,11 +3,9 @@ class @Project # Git clone panel switcher cloneHolder = $('.git-clone-holder') if cloneHolder.length - $('a, button', cloneHolder).click -> - $('a, button', cloneHolder).removeClass 'active' - $(@).addClass 'active' - $('#project_clone', cloneHolder).val $(@).data 'clone' - $(".clone").text("").append $(@).data 'clone' + $('.js-protocol-switch', cloneHolder).click -> + $('.js-protocol-switch', cloneHolder).toggleClass('active') + $('#project_clone').val($(@).data('clone')) # Ref switcher $('.project-refs-select').on 'change', -> @@ -39,4 +37,4 @@ class @Project when 4 then label = ' On Mention ' $('#notifications-button').empty().append("" + label + "") $(@).parents('ul').find('li.active').removeClass 'active' - $(@).parent().addClass 'active' \ No newline at end of file + $(@).parent().addClass 'active' diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index d3b1004002..ad607ead2b 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -178,6 +178,11 @@ &:active { outline: none; } + + &.btn-clipboard { + padding-left: 15px; + padding-right: 15px; + } } .active { @@ -552,4 +557,4 @@ pre.light-well { z-index: 100; position: relative; } -} \ No newline at end of file +} diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index b9bb1ac8d8..313b6dde91 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -23,7 +23,7 @@ module ButtonHelper end def http_clone_button(project) - klass = 'btn' + klass = 'btn js-protocol-switch' klass << ' active' if default_clone_protocol == 'http' klass << ' has_tooltip' if current_user.try(:require_password?) @@ -41,7 +41,7 @@ module ButtonHelper end def ssh_clone_button(project) - klass = 'btn' + klass = 'btn js-protocol-switch' klass << ' active' if default_clone_protocol == 'ssh' klass << ' has_tooltip' if current_user.try(:require_ssh_key?) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c9cd4a0d54..c0c51aae03 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -173,8 +173,7 @@ module ProjectsHelper 'unknown' end - def default_url_to_repo(project = nil) - project = project || @project + def default_url_to_repo(project = @project) current_user ? project.url_to_repo : project.http_url_to_repo end diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index a82971157d..408a46aaaf 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -6,6 +6,8 @@ .input-group-btn = http_clone_button(project) = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true + .input-group-btn + = clipboard_button(clipboard_target: '#project_clone') - if project.kind_of?(Project) .input-group-addon.has_tooltip{title: "#{visibility_level_label(project.visibility_level)} project", data: { container: "body" } } .visibility-level-label From 637e64c24472a06b32091cb45f1b81260c5e6ec0 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 24 Nov 2015 20:38:36 -0500 Subject: [PATCH 081/908] Clean up the Git protocol switcher JS Also re-adds the `.clone` updating that was removed accidentally. Thanks, tests! --- app/assets/javascripts/project.js.coffee | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index d37a00bded..ec919f0cd6 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -1,11 +1,19 @@ class @Project constructor: -> - # Git clone panel switcher - cloneHolder = $('.git-clone-holder') - if cloneHolder.length - $('.js-protocol-switch', cloneHolder).click -> - $('.js-protocol-switch', cloneHolder).toggleClass('active') - $('#project_clone').val($(@).data('clone')) + # Git protocol switcher + $('.js-protocol-switch').click -> + return if $(@).hasClass('active') + + # Toggle 'active' for both buttons + $('.js-protocol-switch').toggleClass('active') + + url = $(@).data('clone') + + # Update the input field + $('#project_clone').val(url) + + # Update the command line instructions + $('.clone').text(url) # Ref switcher $('.project-refs-select').on 'change', -> From 767bd6f8825661c2cd170172f2b0d5ce34c67a93 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 24 Nov 2015 16:18:14 -0500 Subject: [PATCH 082/908] Enable the Lint/RescueException cop --- .rubocop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 11e4502849..d59edbc8b1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -888,7 +888,7 @@ Lint/RequireParentheses: Lint/RescueException: Description: 'Avoid rescuing the Exception class.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-blind-rescues' - Enabled: false + Enabled: true Lint/ShadowingOuterLocalVariable: Description: >- From d9749c8d36750136cbd8989918b1fec8ff2c4b49 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Nov 2015 16:51:01 +0100 Subject: [PATCH 083/908] Improve UI for group members page Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/lists.scss | 5 ++ .../stylesheets/framework/pagination.scss | 4 ++ app/assets/stylesheets/pages/groups.scss | 4 +- .../group_members/_group_member.html.haml | 4 +- .../groups/group_members/index.html.haml | 58 ++++++++++--------- 5 files changed, 43 insertions(+), 32 deletions(-) diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 45f3b5849b..a798ae812e 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -127,3 +127,8 @@ ul.content-list { } } +.panel > .content-list { + li { + margin: 0; + } +} diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss index 6677f94daf..2cd30491bf 100644 --- a/app/assets/stylesheets/framework/pagination.scss +++ b/app/assets/stylesheets/framework/pagination.scss @@ -32,3 +32,7 @@ } } } + +.panel > .gl-pagination { + margin: 0; +} diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 07a38a19fa..bcb73a9acd 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -1,5 +1,5 @@ .new-group-member-holder { - margin-top: 50px; + margin-top: 10px; padding-top: 20px; } @@ -15,4 +15,4 @@ .form-control { height: 42px; } -} \ No newline at end of file +} diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index bae67552a1..a79a0fcdc8 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -4,7 +4,7 @@ %li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)} %span{class: ("list-item-name" if show_controls)} - if member.user - = image_tag avatar_icon(user, 16), class: "avatar s16", alt: '' + = image_tag avatar_icon(user, 24), class: "avatar s24", alt: '' %strong = link_to user.name, user_path(user) %span.cgray= user.username @@ -14,7 +14,7 @@ %label.label.label-danger %strong Blocked - else - = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: '' + = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: '' %strong = member.invite_email %span.cgray diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index d4ad33a8bf..b3a811b266 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,38 +1,40 @@ +- @blank_container = true - page_title "Members" - header_title group_title(@group, "Members", group_group_members_path(@group)) -- if should_user_see_group_roles?(current_user, @group) - %p.light - Members of group have access to all group projects. - Read more about permissions - %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" -.clearfix.js-toggle-container - = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do - .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input', spellcheck: false } - = button_tag 'Search', class: 'btn' + +.group-members-page - if current_user && current_user.can?(:admin_group_member, @group) - .pull-right - = button_tag class: 'btn btn-new js-toggle-button', type: 'button' do - Add members - %i.fa.fa-chevron-down + .panel.panel-default + .panel-heading + Add new user to group + .panel-body + - if should_user_see_group_roles?(current_user, @group) + %p.light + Members of group have access to all group projects. + Read more about permissions + %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" + .new-group-member-holder + = render "new_group_member" - .js-toggle-content.hide.new-group-member-holder - = render "new_group_member" - -.panel.panel-default.prepend-top-20 - .panel-heading - %strong #{@group.name} - group members - %small - (#{@members.total_count}) - %ul.well-list - - @members.each do |member| - = render 'groups/group_members/group_member', member: member, show_controls: true - -= paginate @members, theme: 'gitlab' + .panel.panel-default + .panel-heading + %strong #{@group.name} + group members + %small + (#{@members.total_count}) + .pull-right + = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false } + = button_tag class: 'btn' do + = icon("search") + %ul.content-list + - @members.each do |member| + = render 'groups/group_members/group_member', member: member, show_controls: true + = paginate @members, theme: 'gitlab' :javascript $('form.member-search-form').on('submit', function(event) { From 34cc8f4a60f25bfa2503f0ad006b047fd2c2f81c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Nov 2015 17:11:45 +0100 Subject: [PATCH 084/908] Improve project members page UI Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/groups.scss | 5 --- .../groups/group_members/index.html.haml | 7 +-- .../project_members/_group_members.html.haml | 8 ++-- .../project_members/_project_member.html.haml | 4 +- .../projects/project_members/_team.html.haml | 16 ++++++- .../projects/project_members/index.html.haml | 45 +++++++------------ 6 files changed, 36 insertions(+), 49 deletions(-) diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index bcb73a9acd..263993f59a 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -1,8 +1,3 @@ -.new-group-member-holder { - margin-top: 10px; - padding-top: 20px; -} - .member-search-form { float: left; } diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index b3a811b266..c83766e729 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,9 +1,6 @@ -- @blank_container = true - page_title "Members" - header_title group_title(@group, "Members", group_group_members_path(@group)) - - - +- @blank_container = true .group-members-page - if current_user && current_user.can?(:admin_group_member, @group) @@ -14,8 +11,6 @@ - if should_user_see_group_roles?(current_user, @group) %p.light Members of group have access to all group projects. - Read more about permissions - %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" .new-group-member-holder = render "new_group_member" diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index 43e92437cf..0c73d7e34a 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -4,11 +4,11 @@ group members %small (#{members.count}) - .panel-head-actions - = link_to group_group_members_path(@group), class: 'btn btn-sm' do - %i.fa.fa-pencil-square-o + .pull-right + = link_to group_group_members_path(@group), class: 'btn' do + = icon('pencil-square-o') Edit group members - %ul.well-list + %ul.content-list - members.each do |member| = render 'groups/group_members/group_member', member: member, show_controls: false - if members.count > 20 diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml index f07cd97e63..05bf3a7ef6 100644 --- a/app/views/projects/project_members/_project_member.html.haml +++ b/app/views/projects/project_members/_project_member.html.haml @@ -4,7 +4,7 @@ %li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)} %span.list-item-name - if member.user - = image_tag avatar_icon(user, 16), class: "avatar s16", alt: '' + = image_tag avatar_icon(user, 24), class: "avatar s24", alt: '' %strong = link_to user.name, user_path(user) %span.cgray= user.username @@ -14,7 +14,7 @@ %label.label.label-danger %strong Blocked - else - = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: '' + = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: '' %strong = member.invite_email %span.cgray diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index b807fb2cc9..8f4c613426 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -1,9 +1,21 @@ -.panel.panel-default.prepend-top-20 +.panel.panel-default .panel-heading %strong #{@project.name} project members %small (#{members.count}) - %ul.well-list + .pull-right + = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false } + = button_tag class: 'btn' do + = icon("search") + %ul.content-list - members.each do |project_member| = render 'project_member', member: project_member + +:javascript + $('form.member-search-form').on('submit', function (event) { + event.preventDefault(); + Turbolinks.visit(this.action + '?' + $(this).serialize()); + }); diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 9fc4be583c..29225a3636 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,36 +1,21 @@ - page_title "Members" = render "header_title" +- @blank_container = true -.gray-content-block.top-block - .clearfix.js-toggle-container - = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do - .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input', spellcheck: false } - = button_tag 'Search', class: 'btn' - - - if can?(current_user, :admin_project_member, @project) - %span.pull-right - = button_tag class: 'btn btn-new btn-grouped js-toggle-button', type: 'button' do - Add members - %i.fa.fa-chevron-down - = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do - Import members - - .js-toggle-content.hide.new-group-member-holder +.project-members-page + - if can?(current_user, :admin_project_member, @project) + .panel.panel-default + .panel-heading + Add new user to project + .pull-right + = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do + Import members + .panel-body + %p.light + Users with access to this project are listed below. = render "new_project_member" -%p.prepend-top-default.light - Users with access to this project are listed below. - Read more about project permissions - %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" + = render "team", members: @project_members -= render "team", members: @project_members - -- if @group - = render "group_members", members: @group_members - -:javascript - $('form.member-search-form').on('submit', function (event) { - event.preventDefault(); - Turbolinks.visit(this.action + '?' + $(this).serialize()); - }); + - if @group + = render "group_members", members: @group_members From ea7467d2be0e367ed1ec6df656cab059a9db6da0 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Nov 2015 22:27:01 +0100 Subject: [PATCH 085/908] Refactor group members tests a bit Signed-off-by: Dmitriy Zaporozhets --- features/groups.feature | 3 --- features/project/team_management.feature | 6 ++--- features/steps/groups.rb | 29 ++++++++++------------- features/steps/project/team_management.rb | 4 ---- 4 files changed, 15 insertions(+), 27 deletions(-) diff --git a/features/groups.feature b/features/groups.feature index 938e658f2a..65ced5c529 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -55,7 +55,6 @@ Feature: Groups Scenario: Add user to group Given gitlab user "Mike" When I visit group "Owned" members page - And I click link "Add members" When I select "Mike" as "Reporter" Then I should see "Mike" in team list as "Reporter" @@ -63,14 +62,12 @@ Feature: Groups Scenario: Ignore add user to group when is already Owner Given gitlab user "Mike" When I visit group "Owned" members page - And I click link "Add members" When I select "Mike" as "Reporter" Then I should see "Mike" in team list as "Owner" @javascript Scenario: Invite user to group When I visit group "Owned" members page - And I click link "Add members" When I select "sjobs@apple.com" as "Reporter" Then I should see "sjobs@apple.com" in team list as invited "Reporter" diff --git a/features/project/team_management.feature b/features/project/team_management.feature index 09a7df59df..06fb45c8bd 100644 --- a/features/project/team_management.feature +++ b/features/project/team_management.feature @@ -13,14 +13,12 @@ Feature: Project Team Management @javascript Scenario: Add user to project - Given I click link "Add members" - And I select "Mike" as "Reporter" + When I select "Mike" as "Reporter" Then I should see "Mike" in team list as "Reporter" @javascript Scenario: Invite user to project - Given I click link "Add members" - And I select "sjobs@apple.com" as "Reporter" + When I select "sjobs@apple.com" as "Reporter" Then I should see "sjobs@apple.com" in team list as invited "Reporter" @javascript diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 7c991af4c2..5de54d9b1e 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -13,10 +13,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps create(:user, name: "Mike") end - step 'I click link "Add members"' do - find(:css, 'button.btn-new').click - end - step 'I should see group "Owned"' do expect(page).to have_content '@owned' end @@ -60,14 +56,14 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see "Mike" in team list as "Reporter"' do - page.within '.well-list' do + page.within '.content-list' do expect(page).to have_content('Mike') expect(page).to have_content('Reporter') end end step 'I should see "Mike" in team list as "Owner"' do - page.within '.well-list' do + page.within '.content-list' do expect(page).to have_content('Mike') expect(page).to have_content('Owner') end @@ -83,7 +79,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do - page.within '.well-list' do + page.within '.content-list' do expect(page).to have_content('sjobs@apple.com') expect(page).to have_content('invited') expect(page).to have_content('Reporter') @@ -114,32 +110,29 @@ class Spinach::Features::Groups < Spinach::FeatureSteps step 'I select user "Mary Jane" from list with role "Reporter"' do user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane") - click_button 'Add members' + page.within ".users-group-form" do select2(user.id, from: "#user_ids", multiple: true) select "Reporter", from: "access_level" end + click_button "Add users to group" end step 'I should see user "John Doe" in team list' do - projects_with_access = find(".panel .well-list") - expect(projects_with_access).to have_content("John Doe") + expect(group_members_list).to have_content("John Doe") end step 'I should not see user "John Doe" in team list' do - projects_with_access = find(".panel .well-list") - expect(projects_with_access).not_to have_content("John Doe") + expect(group_members_list).not_to have_content("John Doe") end step 'I should see user "Mary Jane" in team list' do - projects_with_access = find(".panel .well-list") - expect(projects_with_access).to have_content("Mary Jane") + expect(group_members_list).to have_content("Mary Jane") end step 'I should not see user "Mary Jane" in team list' do - projects_with_access = find(".panel .well-list") - expect(projects_with_access).not_to have_content("Mary Jane") + expect(group_members_list).not_to have_content("Mary Jane") end step 'project from group "Owned" has issues assigned to me' do @@ -401,4 +394,8 @@ class Spinach::Features::Groups < Spinach::FeatureSteps author: current_user, milestone: milestone2_project3 end + + def group_members_list + find(".panel .content-list") + end end diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index 97d6301645..caad52def7 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -15,10 +15,6 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps expect(page).to have_content(user.username) end - step 'I click link "Add members"' do - find(:css, 'button.btn-new').click - end - step 'I select "Mike" as "Reporter"' do user = User.find_by(name: "Mike") From 561634c78f3ab7967709196e8cca39a256d27196 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Nov 2015 10:24:08 +0100 Subject: [PATCH 086/908] Refactor group steps Signed-off-by: Dmitriy Zaporozhets --- features/steps/groups.rb | 77 +++++++++------------------------------- 1 file changed, 16 insertions(+), 61 deletions(-) diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 5de54d9b1e..d99417d178 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -332,67 +332,22 @@ class Spinach::Features::Groups < Spinach::FeatureSteps def group_milestone group = owned_group - @project1 = create :project, - group: group - project2 = create :project, - path: 'gitlab-ci', - group: group - @project3 = create :project, - path: 'cookbook-gitlab', - group: group - milestone1_project1 = create :milestone, - title: "Version 7.2", - project: @project1 - milestone1_project2 = create :milestone, - title: "Version 7.2", - project: project2 - create :milestone, - title: "Version 7.2", - project: @project3 - milestone2_project1 = create :milestone, - title: "GL-113", - project: @project1 - milestone2_project2 = create :milestone, - title: "GL-113", - project: project2 - milestone2_project3 = create :milestone, - title: "GL-113", - project: @project3, - due_date: '2114-08-20', - description: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry' - @issue1 = create :issue, - project: @project1, - assignee: current_user, - author: current_user, - milestone: milestone2_project1 - create :issue, - project: project2, - assignee: current_user, - author: current_user, - milestone: milestone1_project2 - create :issue, - project: @project3, - assignee: current_user, - author: current_user, - milestone: milestone1_project1 - create :merge_request, - source_project: @project1, - target_project: @project1, - assignee: current_user, - author: current_user, - milestone: milestone2_project1 - create :merge_request, - source_project: project2, - target_project: project2, - assignee: current_user, - author: current_user, - milestone: milestone2_project2 - @mr3 = create :merge_request, - source_project: @project3, - target_project: @project3, - assignee: current_user, - author: current_user, - milestone: milestone2_project3 + %w(gitlabhq gitlab-ci cookbook-gitlab).each do |path| + project = create :project, path: path, group: group + milestone = create :milestone, title: "Version 7.2", project: project + create :issue, + project: project, + assignee: current_user, + author: current_user, + milestone: milestone + + milestone = create :milestone, title: "GL-113", project: project + create :issue, + project: project, + assignee: current_user, + author: current_user, + milestone: milestone + end end def group_members_list From d262daa4f0f425ddda189ae9e2eb21bbf287f21a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Nov 2015 11:44:39 +0100 Subject: [PATCH 087/908] Split group feature tests Signed-off-by: Dmitriy Zaporozhets --- .../groups/group_members/index.html.haml | 2 +- .../projects/project_members/_team.html.haml | 2 +- features/group/members.feature | 105 ++++++++ features/group/milestones.feature | 30 +++ features/groups.feature | 129 ---------- features/steps/group/members.rb | 147 ++++++++++++ features/steps/group/milestones.rb | 87 +++++++ features/steps/groups.rb | 226 +----------------- features/steps/shared/group.rb | 4 + features/steps/shared/user.rb | 4 + 10 files changed, 380 insertions(+), 356 deletions(-) create mode 100644 features/group/members.feature create mode 100644 features/group/milestones.feature create mode 100644 features/steps/group/members.rb create mode 100644 features/steps/group/milestones.rb diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index c83766e729..335bf03607 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -24,7 +24,7 @@ = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false } - = button_tag class: 'btn' do + = button_tag class: 'btn', title: 'Search' do = icon("search") %ul.content-list - @members.each do |member| diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 8f4c613426..ccddab13aa 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -8,7 +8,7 @@ = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false } - = button_tag class: 'btn' do + = button_tag class: 'btn', title: 'Search' do = icon("search") %ul.content-list - members.each do |project_member| diff --git a/features/group/members.feature b/features/group/members.feature new file mode 100644 index 0000000000..1f9514bac3 --- /dev/null +++ b/features/group/members.feature @@ -0,0 +1,105 @@ +Feature: Group Members + Background: + Given I sign in as "John Doe" + And "John Doe" is owner of group "Owned" + And "John Doe" is guest of group "Guest" + + @javascript + Scenario: I should add user to group "Owned" + Given User "Mary Jane" exists + When I visit group "Owned" members page + And I select user "Mary Jane" from list with role "Reporter" + Then I should see user "Mary Jane" in team list + + @javascript + Scenario: Add user to group + Given gitlab user "Mike" + When I visit group "Owned" members page + When I select "Mike" as "Reporter" + Then I should see "Mike" in team list as "Reporter" + + @javascript + Scenario: Ignore add user to group when is already Owner + Given gitlab user "Mike" + When I visit group "Owned" members page + When I select "Mike" as "Reporter" + Then I should see "Mike" in team list as "Owner" + + @javascript + Scenario: Invite user to group + When I visit group "Owned" members page + When I select "sjobs@apple.com" as "Reporter" + Then I should see "sjobs@apple.com" in team list as invited "Reporter" + + @javascript + Scenario: Edit group member permissions + Given "Mary Jane" is guest of group "Owned" + And I visit group "Owned" members page + When I change the "Mary Jane" role to "Developer" + Then I should see "Mary Jane" as "Developer" + + # Leave + + @javascript + Scenario: Owner should be able to remove himself from group if he is not the last owner + Given "Mary Jane" is owner of group "Owned" + When I visit group "Owned" members page + Then I should see user "John Doe" in team list + Then I should see user "Mary Jane" in team list + When I click on the "Remove User From Group" button for "John Doe" + And I visit group "Owned" members page + Then I should not see user "John Doe" in team list + Then I should see user "Mary Jane" in team list + + @javascript + Scenario: Owner should not be able to remove himself from group if he is the last owner + Given "Mary Jane" is guest of group "Owned" + When I visit group "Owned" members page + Then I should see user "John Doe" in team list + Then I should see user "Mary Jane" in team list + Then I should not see the "Remove User From Group" button for "John Doe" + + @javascript + Scenario: Guest should be able to remove himself from group + Given "Mary Jane" is guest of group "Guest" + When I visit group "Guest" members page + Then I should see user "John Doe" in team list + Then I should see user "Mary Jane" in team list + When I click on the "Remove User From Group" button for "John Doe" + When I visit group "Guest" members page + Then I should not see user "John Doe" in team list + Then I should see user "Mary Jane" in team list + + @javascript + Scenario: Guest should be able to remove himself from group even if he is the only user in the group + When I visit group "Guest" members page + Then I should see user "John Doe" in team list + When I click on the "Remove User From Group" button for "John Doe" + When I visit group "Guest" members page + Then I should not see user "John Doe" in team list + + # Remove others + + Scenario: Owner should be able to remove other users from group + Given "Mary Jane" is owner of group "Owned" + When I visit group "Owned" members page + Then I should see user "John Doe" in team list + Then I should see user "Mary Jane" in team list + When I click on the "Remove User From Group" button for "Mary Jane" + When I visit group "Owned" members page + Then I should see user "John Doe" in team list + Then I should not see user "Mary Jane" in team list + + Scenario: Guest should not be able to remove other users from group + Given "Mary Jane" is guest of group "Guest" + When I visit group "Guest" members page + Then I should see user "John Doe" in team list + Then I should see user "Mary Jane" in team list + Then I should not see the "Remove User From Group" button for "Mary Jane" + + Scenario: Search member by name + Given "Mary Jane" is guest of group "Guest" + And I visit group "Guest" members page + When I search for 'Mary' member + Then I should see user "Mary Jane" in team list + Then I should not see user "John Doe" in team list diff --git a/features/group/milestones.feature b/features/group/milestones.feature new file mode 100644 index 0000000000..62ea66a783 --- /dev/null +++ b/features/group/milestones.feature @@ -0,0 +1,30 @@ +Feature: Group Milestones + Background: + Given I sign in as "John Doe" + And "John Doe" is owner of group "Owned" + + Scenario: I should see group "Owned" milestone index page with no milestones + When I visit group "Owned" page + And I click on group milestones + Then I should see group milestones index page has no milestones + + Scenario: I should see group "Owned" milestone index page with milestones + Given Group has projects with milestones + When I visit group "Owned" page + And I click on group milestones + Then I should see group milestones index page with milestones + + Scenario: I should see group "Owned" milestone show page + Given Group has projects with milestones + When I visit group "Owned" page + And I click on group milestones + And I click on one group milestone + Then I should see group milestone with descriptions and expiry date + And I should see group milestone with all issues and MRs assigned to that milestone + + Scenario: Create multiple milestones with one form + Given I visit group "Owned" milestones page + And I click new milestone button + And I fill milestone name + When I press create mileston button + Then milestone in each project should be created diff --git a/features/groups.feature b/features/groups.feature index 65ced5c529..c803e95298 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -2,7 +2,6 @@ Feature: Groups Background: Given I sign in as "John Doe" And "John Doe" is owner of group "Owned" - And "John Doe" is guest of group "Guest" Scenario: I should have back to group button When I visit group "Owned" page @@ -24,13 +23,6 @@ Feature: Groups When I visit group "Owned" merge requests page Then I should see merge requests from group "Owned" assigned to me - @javascript - Scenario: I should add user to projects in group "Owned" - Given User "Mary Jane" exists - When I visit group "Owned" members page - And I select user "Mary Jane" from list with role "Reporter" - Then I should see user "Mary Jane" in team list - Scenario: I should see edit group "Owned" page When I visit group "Owned" settings page And I change group "Owned" name to "new-name" @@ -51,127 +43,6 @@ Feature: Groups Then I should not see group "Owned" avatar And I should not see the "Remove avatar" button - @javascript - Scenario: Add user to group - Given gitlab user "Mike" - When I visit group "Owned" members page - When I select "Mike" as "Reporter" - Then I should see "Mike" in team list as "Reporter" - - @javascript - Scenario: Ignore add user to group when is already Owner - Given gitlab user "Mike" - When I visit group "Owned" members page - When I select "Mike" as "Reporter" - Then I should see "Mike" in team list as "Owner" - - @javascript - Scenario: Invite user to group - When I visit group "Owned" members page - When I select "sjobs@apple.com" as "Reporter" - Then I should see "sjobs@apple.com" in team list as invited "Reporter" - - @javascript - Scenario: Edit group member permissions - Given "Mary Jane" is guest of group "Owned" - And I visit group "Owned" members page - When I change the "Mary Jane" role to "Developer" - Then I should see "Mary Jane" as "Developer" - - # Leave - - @javascript - Scenario: Owner should be able to remove himself from group if he is not the last owner - Given "Mary Jane" is owner of group "Owned" - When I visit group "Owned" members page - Then I should see user "John Doe" in team list - Then I should see user "Mary Jane" in team list - When I click on the "Remove User From Group" button for "John Doe" - And I visit group "Owned" members page - Then I should not see user "John Doe" in team list - Then I should see user "Mary Jane" in team list - - @javascript - Scenario: Owner should not be able to remove himself from group if he is the last owner - Given "Mary Jane" is guest of group "Owned" - When I visit group "Owned" members page - Then I should see user "John Doe" in team list - Then I should see user "Mary Jane" in team list - Then I should not see the "Remove User From Group" button for "John Doe" - - @javascript - Scenario: Guest should be able to remove himself from group - Given "Mary Jane" is guest of group "Guest" - When I visit group "Guest" members page - Then I should see user "John Doe" in team list - Then I should see user "Mary Jane" in team list - When I click on the "Remove User From Group" button for "John Doe" - When I visit group "Guest" members page - Then I should not see user "John Doe" in team list - Then I should see user "Mary Jane" in team list - - @javascript - Scenario: Guest should be able to remove himself from group even if he is the only user in the group - When I visit group "Guest" members page - Then I should see user "John Doe" in team list - When I click on the "Remove User From Group" button for "John Doe" - When I visit group "Guest" members page - Then I should not see user "John Doe" in team list - - # Remove others - - Scenario: Owner should be able to remove other users from group - Given "Mary Jane" is owner of group "Owned" - When I visit group "Owned" members page - Then I should see user "John Doe" in team list - Then I should see user "Mary Jane" in team list - When I click on the "Remove User From Group" button for "Mary Jane" - When I visit group "Owned" members page - Then I should see user "John Doe" in team list - Then I should not see user "Mary Jane" in team list - - Scenario: Guest should not be able to remove other users from group - Given "Mary Jane" is guest of group "Guest" - When I visit group "Guest" members page - Then I should see user "John Doe" in team list - Then I should see user "Mary Jane" in team list - Then I should not see the "Remove User From Group" button for "Mary Jane" - - Scenario: Search member by name - Given "Mary Jane" is guest of group "Guest" - And I visit group "Guest" members page - When I search for 'Mary' member - Then I should see user "Mary Jane" in team list - Then I should not see user "John Doe" in team list - - # Group milestones - - Scenario: I should see group "Owned" milestone index page with no milestones - When I visit group "Owned" page - And I click on group milestones - Then I should see group milestones index page has no milestones - - Scenario: I should see group "Owned" milestone index page with milestones - Given Group has projects with milestones - When I visit group "Owned" page - And I click on group milestones - Then I should see group milestones index page with milestones - - Scenario: I should see group "Owned" milestone show page - Given Group has projects with milestones - When I visit group "Owned" page - And I click on group milestones - And I click on one group milestone - Then I should see group milestone with descriptions and expiry date - And I should see group milestone with all issues and MRs assigned to that milestone - - Scenario: Create multiple milestones with one form - Given I visit group "Owned" milestones page - And I click new milestone button - And I fill milestone name - When I press create mileston button - Then milestone in each project should be created - # Group projects in settings Scenario: I should see all projects in the project list in settings Given Group "Owned" has archived project diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb new file mode 100644 index 0000000000..0706df3aec --- /dev/null +++ b/features/steps/group/members.rb @@ -0,0 +1,147 @@ +class Spinach::Features::GroupMembers < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedGroup + include SharedUser + include Select2Helper + + step 'I select "Mike" as "Reporter"' do + user = User.find_by(name: "Mike") + + page.within ".users-group-form" do + select2(user.id, from: "#user_ids", multiple: true) + select "Reporter", from: "access_level" + end + + click_button "Add users to group" + end + + step 'I select "Mike" as "Master"' do + user = User.find_by(name: "Mike") + + page.within ".users-group-form" do + select2(user.id, from: "#user_ids", multiple: true) + select "Master", from: "access_level" + end + + click_button "Add users to group" + end + + step 'I should see "Mike" in team list as "Reporter"' do + page.within '.content-list' do + expect(page).to have_content('Mike') + expect(page).to have_content('Reporter') + end + end + + step 'I should see "Mike" in team list as "Owner"' do + page.within '.content-list' do + expect(page).to have_content('Mike') + expect(page).to have_content('Owner') + end + end + + step 'I select "sjobs@apple.com" as "Reporter"' do + page.within ".users-group-form" do + select2("sjobs@apple.com", from: "#user_ids", multiple: true) + select "Reporter", from: "access_level" + end + + click_button "Add users to group" + end + + step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do + page.within '.content-list' do + expect(page).to have_content('sjobs@apple.com') + expect(page).to have_content('invited') + expect(page).to have_content('Reporter') + end + end + + step 'I select user "Mary Jane" from list with role "Reporter"' do + user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane") + + page.within ".users-group-form" do + select2(user.id, from: "#user_ids", multiple: true) + select "Reporter", from: "access_level" + end + + click_button "Add users to group" + end + + step 'I should see user "John Doe" in team list' do + expect(group_members_list).to have_content("John Doe") + end + + step 'I should not see user "John Doe" in team list' do + expect(group_members_list).not_to have_content("John Doe") + end + + step 'I should see user "Mary Jane" in team list' do + expect(group_members_list).to have_content("Mary Jane") + end + + step 'I should not see user "Mary Jane" in team list' do + expect(group_members_list).not_to have_content("Mary Jane") + end + + step 'I click on the "Remove User From Group" button for "John Doe"' do + find(:css, 'li', text: "John Doe").find(:css, 'a.btn-remove').click + # poltergeist always confirms popups. + end + + step 'I click on the "Remove User From Group" button for "Mary Jane"' do + find(:css, 'li', text: "Mary Jane").find(:css, 'a.btn-remove').click + # poltergeist always confirms popups. + end + + step 'I should not see the "Remove User From Group" button for "John Doe"' do + expect(find(:css, 'li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove') + # poltergeist always confirms popups. + end + + step 'I should not see the "Remove User From Group" button for "Mary Jane"' do + expect(find(:css, 'li', text: "Mary Jane")).not_to have_selector(:css, 'a.btn-remove') + # poltergeist always confirms popups. + end + + step 'I search for \'Mary\' member' do + page.within '.member-search-form' do + fill_in 'search', with: 'Mary' + click_button 'Search' + end + end + + step 'I change the "Mary Jane" role to "Developer"' do + member = mary_jane_member + + page.within "#group_member_#{member.id}" do + find(".js-toggle-button").click + page.within "#edit_group_member_#{member.id}" do + select 'Developer', from: 'group_member_access_level' + click_on 'Save' + end + end + end + + step 'I should see "Mary Jane" as "Developer"' do + member = mary_jane_member + + page.within "#group_member_#{member.id}" do + page.within '.member-access-level' do + expect(page).to have_content "Developer" + end + end + end + + private + + def mary_jane_member + user = User.find_by(name: "Mary Jane") + owned_group.members.find_by(user_id: user.id) + end + + def group_members_list + find(".panel .content-list") + end +end diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb new file mode 100644 index 0000000000..4c4038d44c --- /dev/null +++ b/features/steps/group/milestones.rb @@ -0,0 +1,87 @@ +class Spinach::Features::GroupMilestones < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedGroup + include SharedUser + + step 'I click on group milestones' do + click_link 'Milestones' + end + + step 'I should see group milestones index page has no milestones' do + expect(page).to have_content('No milestones to show') + end + + step 'Group has projects with milestones' do + group_milestone + end + + step 'I should see group milestones index page with milestones' do + expect(page).to have_content('Version 7.2') + expect(page).to have_content('GL-113') + expect(page).to have_link('3 Issues', href: issues_group_path("owned", milestone_title: "Version 7.2")) + expect(page).to have_link('0 Merge Requests', href: merge_requests_group_path("owned", milestone_title: "GL-113")) + end + + step 'I click on one group milestone' do + click_link 'GL-113' + end + + step 'I should see group milestone with descriptions and expiry date' do + expect(page).to have_content('expires at Aug 20, 2114') + end + + step 'I should see group milestone with all issues and MRs assigned to that milestone' do + expect(page).to have_content('Milestone GL-113') + expect(page).to have_content('Progress: 0 closed – 3 open') + issue = Milestone.find_by(name: 'GL-113').issues.first + expect(page).to have_link(issue.title, href: namespace_project_issue_path(issue.project.namespace, issue.project, issue)) + end + + step 'I fill milestone name' do + fill_in 'milestone_title', with: 'v2.9.0' + end + + step 'I click new milestone button' do + click_link "New Milestone" + end + + step 'I press create mileston button' do + click_button "Create Milestone" + end + + step 'milestone in each project should be created' do + group = Group.find_by(name: 'Owned') + expect(page).to have_content "Milestone v2.9.0" + expect(group.projects).to be_present + + group.projects.each do |project| + expect(page).to have_content project.name + end + end + + private + + def group_milestone + group = owned_group + + %w(gitlabhq gitlab-ci cookbook-gitlab).each do |path| + project = create :project, path: path, group: group + milestone = create :milestone, title: "Version 7.2", project: project + create :issue, + project: project, + assignee: current_user, + author: current_user, + milestone: milestone + + milestone = create :milestone, title: "GL-113", project: project, + due_date: '2114-08-20', description: 'Lorem Ipsum is simply dummy text' + + create :issue, + project: project, + assignee: current_user, + author: current_user, + milestone: milestone + end + end +end diff --git a/features/steps/groups.rb b/features/steps/groups.rb index d99417d178..f5e3fee61c 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -3,16 +3,11 @@ class Spinach::Features::Groups < Spinach::FeatureSteps include SharedPaths include SharedGroup include SharedUser - include Select2Helper step 'I should see back to dashboard button' do expect(page).to have_content 'Go to dashboard' end - step 'gitlab user "Mike"' do - create(:user, name: "Mike") - end - step 'I should see group "Owned"' do expect(page).to have_content '@owned' end @@ -33,59 +28,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps expect(page).to have_content 'Public-project' end - step 'I select "Mike" as "Reporter"' do - user = User.find_by(name: "Mike") - - page.within ".users-group-form" do - select2(user.id, from: "#user_ids", multiple: true) - select "Reporter", from: "access_level" - end - - click_button "Add users to group" - end - - step 'I select "Mike" as "Master"' do - user = User.find_by(name: "Mike") - - page.within ".users-group-form" do - select2(user.id, from: "#user_ids", multiple: true) - select "Master", from: "access_level" - end - - click_button "Add users to group" - end - - step 'I should see "Mike" in team list as "Reporter"' do - page.within '.content-list' do - expect(page).to have_content('Mike') - expect(page).to have_content('Reporter') - end - end - - step 'I should see "Mike" in team list as "Owner"' do - page.within '.content-list' do - expect(page).to have_content('Mike') - expect(page).to have_content('Owner') - end - end - - step 'I select "sjobs@apple.com" as "Reporter"' do - page.within ".users-group-form" do - select2("sjobs@apple.com", from: "#user_ids", multiple: true) - select "Reporter", from: "access_level" - end - - click_button "Add users to group" - end - - step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do - page.within '.content-list' do - expect(page).to have_content('sjobs@apple.com') - expect(page).to have_content('invited') - expect(page).to have_content('Reporter') - end - end - step 'I should see group "Owned" projects list' do owned_group.projects.each do |project| expect(page).to have_link project.name @@ -108,33 +50,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end end - step 'I select user "Mary Jane" from list with role "Reporter"' do - user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane") - - page.within ".users-group-form" do - select2(user.id, from: "#user_ids", multiple: true) - select "Reporter", from: "access_level" - end - - click_button "Add users to group" - end - - step 'I should see user "John Doe" in team list' do - expect(group_members_list).to have_content("John Doe") - end - - step 'I should not see user "John Doe" in team list' do - expect(group_members_list).not_to have_content("John Doe") - end - - step 'I should see user "Mary Jane" in team list' do - expect(group_members_list).to have_content("Mary Jane") - end - - step 'I should not see user "Mary Jane" in team list' do - expect(group_members_list).not_to have_content("Mary Jane") - end - step 'project from group "Owned" has issues assigned to me' do create :issue, project: project, @@ -196,67 +111,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps expect(page).not_to have_link("Remove avatar") end - step 'I click on the "Remove User From Group" button for "John Doe"' do - find(:css, 'li', text: "John Doe").find(:css, 'a.btn-remove').click - # poltergeist always confirms popups. - end - - step 'I click on the "Remove User From Group" button for "Mary Jane"' do - find(:css, 'li', text: "Mary Jane").find(:css, 'a.btn-remove').click - # poltergeist always confirms popups. - end - - step 'I should not see the "Remove User From Group" button for "John Doe"' do - expect(find(:css, 'li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove') - # poltergeist always confirms popups. - end - - step 'I should not see the "Remove User From Group" button for "Mary Jane"' do - expect(find(:css, 'li', text: "Mary Jane")).not_to have_selector(:css, 'a.btn-remove') - # poltergeist always confirms popups. - end - - step 'I search for \'Mary\' member' do - page.within '.member-search-form' do - fill_in 'search', with: 'Mary' - click_button 'Search' - end - end - - step 'I click on group milestones' do - click_link 'Milestones' - end - - step 'I should see group milestones index page has no milestones' do - expect(page).to have_content('No milestones to show') - end - - step 'Group has projects with milestones' do - group_milestone - end - - step 'I should see group milestones index page with milestones' do - expect(page).to have_content('Version 7.2') - expect(page).to have_content('GL-113') - expect(page).to have_link('2 Issues', href: issues_group_path("owned", milestone_title: "Version 7.2")) - expect(page).to have_link('3 Merge Requests', href: merge_requests_group_path("owned", milestone_title: "GL-113")) - end - - step 'I click on one group milestone' do - click_link 'GL-113' - end - - step 'I should see group milestone with descriptions and expiry date' do - expect(page).to have_content('expires at Aug 20, 2114') - end - - step 'I should see group milestone with all issues and MRs assigned to that milestone' do - expect(page).to have_content('Milestone GL-113') - expect(page).to have_content('Progress: 0 closed – 4 open') - expect(page).to have_link(@issue1.title, href: namespace_project_issue_path(@project1.namespace, @project1, @issue1)) - expect(page).to have_link(@mr3.title, href: namespace_project_merge_request_path(@project3.namespace, @project3, @mr3)) - end - step 'Group "Owned" has archived project' do group = Group.find_by(name: 'Owned') create(:project, namespace: group, archived: true, path: "archived-project") @@ -266,60 +120,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived') end - step 'I fill milestone name' do - fill_in 'milestone_title', with: 'v2.9.0' - end - - step 'I click new milestone button' do - click_link "New Milestone" - end - - step 'I press create mileston button' do - click_button "Create Milestone" - end - - step 'milestone in each project should be created' do - group = Group.find_by(name: 'Owned') - expect(page).to have_content "Milestone v2.9.0" - expect(group.projects).to be_present - - group.projects.each do |project| - expect(page).to have_content project.name - end - end - - step 'I change the "Mary Jane" role to "Developer"' do - member = mary_jane_member - - page.within "#group_member_#{member.id}" do - find(".js-toggle-button").click - page.within "#edit_group_member_#{member.id}" do - select 'Developer', from: 'group_member_access_level' - click_on 'Save' - end - end - end - - step 'I should see "Mary Jane" as "Developer"' do - member = mary_jane_member - - page.within "#group_member_#{member.id}" do - page.within '.member-access-level' do - expect(page).to have_content "Developer" - end - end - end - - protected - - def owned_group - @owned_group ||= Group.find_by(name: "Owned") - end - - def mary_jane_member - user = User.find_by(name: "Mary Jane") - owned_group.members.find_by(user_id: user.id) - end + private def assigned_to_me(key) project.send(key).where(assignee_id: current_user.id) @@ -328,29 +129,4 @@ class Spinach::Features::Groups < Spinach::FeatureSteps def project owned_group.projects.first end - - def group_milestone - group = owned_group - - %w(gitlabhq gitlab-ci cookbook-gitlab).each do |path| - project = create :project, path: path, group: group - milestone = create :milestone, title: "Version 7.2", project: project - create :issue, - project: project, - assignee: current_user, - author: current_user, - milestone: milestone - - milestone = create :milestone, title: "GL-113", project: project - create :issue, - project: project, - assignee: current_user, - author: current_user, - milestone: milestone - end - end - - def group_members_list - find(".panel .content-list") - end end diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb index 83a0457697..58581653f2 100644 --- a/features/steps/shared/group.rb +++ b/features/steps/shared/group.rb @@ -41,4 +41,8 @@ module SharedGroup project.team << [user, :master] @project_count += 1 end + + def owned_group + @owned_group ||= Group.find_by(name: "Owned") + end end diff --git a/features/steps/shared/user.rb b/features/steps/shared/user.rb index fc1e8d6e88..250cc5b94f 100644 --- a/features/steps/shared/user.rb +++ b/features/steps/shared/user.rb @@ -9,6 +9,10 @@ module SharedUser user_exists("Mary Jane", { username: "mary_jane" }) end + step 'gitlab user "Mike"' do + create(:user, name: "Mike") + end + protected def user_exists(name, options = {}) From acad9d67c7af881c1e4cce1e20ffd9c8f65f3029 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Nov 2015 12:27:34 +0100 Subject: [PATCH 088/908] Fix rubocop complain Signed-off-by: Dmitriy Zaporozhets --- features/steps/group/milestones.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index 4c4038d44c..6e57b16ccb 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -74,8 +74,11 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps author: current_user, milestone: milestone - milestone = create :milestone, title: "GL-113", project: project, - due_date: '2114-08-20', description: 'Lorem Ipsum is simply dummy text' + milestone = create :milestone, + title: "GL-113", + project: project, + due_date: '2114-08-20', + description: 'Lorem Ipsum is simply dummy text' create :issue, project: project, From 18a5f91ed63322af7f2fed29a6cc9a4c1e96978d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Nov 2015 13:08:47 +0100 Subject: [PATCH 089/908] Bump gitlab-shell to 2.6.8 Signed-off-by: Dmitriy Zaporozhets --- GITLAB_SHELL_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index e261122d5c..743af5e125 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.6.7 +2.6.8 From 82ff9e65939d2da279b684cf378ca33e60eed66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurens=20St=C3=B6tzel?= Date: Wed, 25 Nov 2015 12:27:07 +0000 Subject: [PATCH 090/908] Correction of markdown in SSH docs --- doc/ssh/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 9753504ac8..fe5b45dd43 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -15,7 +15,8 @@ Note: It is a best practice to use a password for an SSH key, but it is not required and you can skip creating a password by pressing enter. Note that the password you choose here can't be altered or retrieved. -To generate a new SSH key, use the following commandGitLab```bash +To generate a new SSH key, use the following command: +```bash ssh-keygen -t rsa -C "$your_email" ``` This command will prompt you for a location and filename to store the key @@ -108,4 +109,4 @@ Note in the gitlab.com example above a username was specified to override the de Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of these topics is beyond the scope of this document. Public SSH keys need to be unique, as they will bind to your account. Your SSH key is the only identifier you'll -have when pushing code via SSH. That's why it needs to uniquely map to a single user. +have when pushing code via SSH. That's why it needs to uniquely map to a single user. \ No newline at end of file From 7da750cc30308173e6f21f570ecd5902dfd3d89d Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 25 Nov 2015 13:27:31 +0100 Subject: [PATCH 091/908] Specs for links in email notifications for Gmail Actions. --- spec/mailers/notify_spec.rb | 94 ++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 47863d5457..52bad36ae1 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -77,6 +77,14 @@ describe Notify do end end + shared_examples 'it should have Gmail Actions links' do + it { is_expected.to have_body_text /ViewAction/ } + end + + shared_examples 'it should not have Gmail Actions links' do + it { is_expected.to_not have_body_text /ViewAction/ } + end + describe 'for new users, the email' do let(:example_site_path) { root_path } let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) } @@ -87,6 +95,7 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it_behaves_like 'a new user email', new_user_address + it_behaves_like 'it should not have Gmail Actions links' it 'contains the password text' do is_expected.to have_body_text /Click here to set your password/ @@ -115,6 +124,7 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it_behaves_like 'a new user email', new_user_address + it_behaves_like 'it should not have Gmail Actions links' it 'should not contain the new user\'s password' do is_expected.not_to have_body_text /password/ @@ -127,6 +137,7 @@ describe Notify do subject { Notify.new_ssh_key_email(key.id) } it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' it 'is sent to the new user' do is_expected.to deliver_to key.user.email @@ -150,6 +161,8 @@ describe Notify do subject { Notify.new_email_email(email.id) } + it_behaves_like 'it should not have Gmail Actions links' + it 'is sent to the new user' do is_expected.to deliver_to email.user.email end @@ -194,6 +207,7 @@ describe Notify do it_behaves_like 'an assignee email' it_behaves_like 'an email starting a new thread', 'issue' + it_behaves_like 'it should have Gmail Actions links' it 'has the correct subject' do is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/ @@ -202,6 +216,10 @@ describe Notify do it 'contains a link to the new issue' do is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end + + it 'Gmail Actions contain correct action name' do + is_expected.to have_body_text /View Issue/ + end end describe 'that are new with a description' do @@ -217,6 +235,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'it should have Gmail Actions links' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -239,6 +258,10 @@ describe Notify do it 'contains a link to the issue' do is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end + + it 'Gmail Actions contain correct action name' do + is_expected.to have_body_text /View Issue/ + end end describe 'status changed' do @@ -246,6 +269,7 @@ describe Notify do subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user) } it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'it should have Gmail Actions links' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -268,8 +292,11 @@ describe Notify do it 'contains a link to the issue' do is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end - end + it 'Gmail Actions contain correct action name' do + is_expected.to have_body_text /View Issue/ + end + end end context 'for merge requests' do @@ -282,6 +309,7 @@ describe Notify do it_behaves_like 'an assignee email' it_behaves_like 'an email starting a new thread', 'merge_request' + it_behaves_like 'it should have Gmail Actions links' it 'has the correct subject' do is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ @@ -302,14 +330,24 @@ describe Notify do it 'has the correct message-id set' do is_expected.to have_header 'Message-ID', "" end + + it 'Gmail Actions contain correct action name' do + is_expected.to have_body_text /View Merge request/ + end end describe 'that are new with a description' do subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) } + it_behaves_like 'it should have Gmail Actions links' + it 'contains the description' do is_expected.to have_body_text /#{merge_request_with_description.description}/ end + + it 'Gmail Actions contain correct action name' do + is_expected.to have_body_text /View Merge request/ + end end describe 'that are reassigned' do @@ -317,6 +355,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'it should have Gmail Actions links' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -339,6 +378,10 @@ describe Notify do it 'contains a link to the merge request' do is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ end + + it 'Gmail Actions contain correct action name' do + is_expected.to have_body_text /View Merge request/ + end end describe 'status changed' do @@ -346,6 +389,8 @@ describe Notify do subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user) } it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'it should have Gmail Actions links' + it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -368,6 +413,10 @@ describe Notify do it 'contains a link to the merge request' do is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ end + + it 'Gmail Actions contain correct action name' do + is_expected.to have_body_text /View Merge request/ + end end describe 'that are merged' do @@ -375,6 +424,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'it should have Gmail Actions links' it 'is sent as the merge author' do sender = subject.header[:from].addrs[0] @@ -393,6 +443,10 @@ describe Notify do it 'contains a link to the merge request' do is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ end + + it 'Gmail Actions contain correct action name' do + is_expected.to have_body_text /View Merge request/ + end end end end @@ -403,6 +457,7 @@ describe Notify do subject { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") } it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' it 'has the correct subject' do is_expected.to have_subject /Project was moved/ @@ -424,13 +479,16 @@ describe Notify do subject { Notify.project_access_granted_email(project_member.id) } it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' it 'has the correct subject' do is_expected.to have_subject /Access to project was granted/ end + it 'contains name of project' do is_expected.to have_body_text /#{project.name}/ end + it 'contains new user role' do is_expected.to have_body_text /#{project_member.human_access}/ end @@ -445,6 +503,8 @@ describe Notify do end shared_examples 'a note email' do + it_behaves_like 'it should have Gmail Actions links' + it 'is sent as the author' do sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq(note_author.name) @@ -469,6 +529,7 @@ describe Notify do it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread', 'commit' + it_behaves_like 'it should have Gmail Actions links' it 'has the correct subject' do is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/ @@ -477,6 +538,10 @@ describe Notify do it 'contains a link to the commit' do is_expected.to have_body_text commit.short_id end + + it 'Gmail Actions contain correct action name' do + is_expected.to have_body_text /View Commit/ + end end describe 'on a merge request' do @@ -488,6 +553,7 @@ describe Notify do it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'it should have Gmail Actions links' it 'has the correct subject' do is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ @@ -496,6 +562,10 @@ describe Notify do it 'contains a link to the merge request note' do is_expected.to have_body_text /#{note_on_merge_request_path}/ end + + it 'Gmail Actions contain correct action name' do + is_expected.to have_body_text /View Merge request/ + end end describe 'on an issue' do @@ -507,6 +577,7 @@ describe Notify do it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'it should have Gmail Actions links' it 'has the correct subject' do is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ @@ -515,6 +586,10 @@ describe Notify do it 'contains a link to the issue note' do is_expected.to have_body_text /#{note_on_issue_path}/ end + + it 'Gmail Actions contain correct action name' do + is_expected.to have_body_text /View Issue/ + end end end end @@ -527,6 +602,7 @@ describe Notify do subject { Notify.group_access_granted_email(membership.id) } it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' it 'has the correct subject' do is_expected.to have_subject /Access to group was granted/ @@ -574,6 +650,8 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) } + it_behaves_like 'it should not have Gmail Actions links' + it 'is sent as the author' do sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq(user.name) @@ -600,6 +678,8 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) } + it_behaves_like 'it should not have Gmail Actions links' + it 'is sent as the author' do sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq(user.name) @@ -625,6 +705,8 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) } + it_behaves_like 'it should not have Gmail Actions links' + it 'is sent as the author' do sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq(user.name) @@ -646,6 +728,8 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) } + it_behaves_like 'it should not have Gmail Actions links' + it 'is sent as the author' do sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq(user.name) @@ -671,6 +755,8 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) } + it_behaves_like 'it should not have Gmail Actions links' + it 'is sent as the author' do sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq(user.name) @@ -774,6 +860,8 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) } + it_behaves_like 'it should have Gmail Actions links' + it 'is sent as the author' do sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq(user.name) @@ -799,5 +887,9 @@ describe Notify do it 'contains a link to the diff' do is_expected.to have_body_text /#{diff_path}/ end + + it 'Gmail Actions contain correct action name' do + is_expected.to have_body_text /View Commit/ + end end end From 56135927273cfd722872e893abde3728f3b21d38 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 25 Nov 2015 13:53:02 +0100 Subject: [PATCH 092/908] If action link is not found, return nil. --- app/helpers/emails_helper.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 45788ba95a..41b5bd7be9 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -28,6 +28,8 @@ module EmailsHelper return "View #{action.humanize.singularize}" end end + + nil end def color_email_diff(diffcontent) From 5c0be319cc6d568cefd339be8bc5f80e157836b4 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 25 Nov 2015 13:59:03 +0100 Subject: [PATCH 093/908] Remove some repetition in notify spec. --- spec/mailers/notify_spec.rb | 93 +++++++++++++------------------------ 1 file changed, 32 insertions(+), 61 deletions(-) diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 52bad36ae1..d6796b07a5 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -85,6 +85,24 @@ describe Notify do it { is_expected.to_not have_body_text /ViewAction/ } end + shared_examples 'it should show Gmail Actions View Issue link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text /View Issue/ } + end + + shared_examples 'it should show Gmail Actions View Merge request link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text /View Merge request/ } + end + + shared_examples 'it should show Gmail Actions View Commit link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text /View Commit/ } + end + describe 'for new users, the email' do let(:example_site_path) { root_path } let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) } @@ -207,7 +225,7 @@ describe Notify do it_behaves_like 'an assignee email' it_behaves_like 'an email starting a new thread', 'issue' - it_behaves_like 'it should have Gmail Actions links' + it_behaves_like 'it should show Gmail Actions View Issue link' it 'has the correct subject' do is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/ @@ -216,15 +234,13 @@ describe Notify do it 'contains a link to the new issue' do is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end - - it 'Gmail Actions contain correct action name' do - is_expected.to have_body_text /View Issue/ - end end describe 'that are new with a description' do subject { Notify.new_issue_email(issue_with_description.assignee_id, issue_with_description.id) } + it_behaves_like 'it should show Gmail Actions View Issue link' + it 'contains the description' do is_expected.to have_body_text /#{issue_with_description.description}/ end @@ -235,7 +251,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'issue' - it_behaves_like 'it should have Gmail Actions links' + it_behaves_like 'it should show Gmail Actions View Issue link' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -258,10 +274,6 @@ describe Notify do it 'contains a link to the issue' do is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end - - it 'Gmail Actions contain correct action name' do - is_expected.to have_body_text /View Issue/ - end end describe 'status changed' do @@ -269,7 +281,7 @@ describe Notify do subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user) } it_behaves_like 'an answer to an existing thread', 'issue' - it_behaves_like 'it should have Gmail Actions links' + it_behaves_like 'it should show Gmail Actions View Issue link' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -292,10 +304,6 @@ describe Notify do it 'contains a link to the issue' do is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end - - it 'Gmail Actions contain correct action name' do - is_expected.to have_body_text /View Issue/ - end end end @@ -309,7 +317,7 @@ describe Notify do it_behaves_like 'an assignee email' it_behaves_like 'an email starting a new thread', 'merge_request' - it_behaves_like 'it should have Gmail Actions links' + it_behaves_like 'it should show Gmail Actions View Merge request link' it 'has the correct subject' do is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ @@ -330,24 +338,16 @@ describe Notify do it 'has the correct message-id set' do is_expected.to have_header 'Message-ID', "" end - - it 'Gmail Actions contain correct action name' do - is_expected.to have_body_text /View Merge request/ - end end describe 'that are new with a description' do subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) } - it_behaves_like 'it should have Gmail Actions links' + it_behaves_like 'it should show Gmail Actions View Merge request link' it 'contains the description' do is_expected.to have_body_text /#{merge_request_with_description.description}/ end - - it 'Gmail Actions contain correct action name' do - is_expected.to have_body_text /View Merge request/ - end end describe 'that are reassigned' do @@ -355,7 +355,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'merge_request' - it_behaves_like 'it should have Gmail Actions links' + it_behaves_like 'it should show Gmail Actions View Merge request link' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -378,10 +378,6 @@ describe Notify do it 'contains a link to the merge request' do is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ end - - it 'Gmail Actions contain correct action name' do - is_expected.to have_body_text /View Merge request/ - end end describe 'status changed' do @@ -389,8 +385,7 @@ describe Notify do subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user) } it_behaves_like 'an answer to an existing thread', 'merge_request' - it_behaves_like 'it should have Gmail Actions links' - + it_behaves_like 'it should show Gmail Actions View Merge request link' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -413,10 +408,6 @@ describe Notify do it 'contains a link to the merge request' do is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ end - - it 'Gmail Actions contain correct action name' do - is_expected.to have_body_text /View Merge request/ - end end describe 'that are merged' do @@ -424,7 +415,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'merge_request' - it_behaves_like 'it should have Gmail Actions links' + it_behaves_like 'it should show Gmail Actions View Merge request link' it 'is sent as the merge author' do sender = subject.header[:from].addrs[0] @@ -443,10 +434,6 @@ describe Notify do it 'contains a link to the merge request' do is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ end - - it 'Gmail Actions contain correct action name' do - is_expected.to have_body_text /View Merge request/ - end end end end @@ -529,7 +516,7 @@ describe Notify do it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread', 'commit' - it_behaves_like 'it should have Gmail Actions links' + it_behaves_like 'it should show Gmail Actions View Commit link' it 'has the correct subject' do is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/ @@ -538,10 +525,6 @@ describe Notify do it 'contains a link to the commit' do is_expected.to have_body_text commit.short_id end - - it 'Gmail Actions contain correct action name' do - is_expected.to have_body_text /View Commit/ - end end describe 'on a merge request' do @@ -553,7 +536,7 @@ describe Notify do it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread', 'merge_request' - it_behaves_like 'it should have Gmail Actions links' + it_behaves_like 'it should show Gmail Actions View Merge request link' it 'has the correct subject' do is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ @@ -562,10 +545,6 @@ describe Notify do it 'contains a link to the merge request note' do is_expected.to have_body_text /#{note_on_merge_request_path}/ end - - it 'Gmail Actions contain correct action name' do - is_expected.to have_body_text /View Merge request/ - end end describe 'on an issue' do @@ -577,7 +556,7 @@ describe Notify do it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread', 'issue' - it_behaves_like 'it should have Gmail Actions links' + it_behaves_like 'it should show Gmail Actions View Issue link' it 'has the correct subject' do is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ @@ -586,10 +565,6 @@ describe Notify do it 'contains a link to the issue note' do is_expected.to have_body_text /#{note_on_issue_path}/ end - - it 'Gmail Actions contain correct action name' do - is_expected.to have_body_text /View Issue/ - end end end end @@ -860,7 +835,7 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) } - it_behaves_like 'it should have Gmail Actions links' + it_behaves_like 'it should show Gmail Actions View Commit link' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -887,9 +862,5 @@ describe Notify do it 'contains a link to the diff' do is_expected.to have_body_text /#{diff_path}/ end - - it 'Gmail Actions contain correct action name' do - is_expected.to have_body_text /View Commit/ - end end end From 40ff1318d29884e4d17e7e450d8a7633e5ac36a9 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 25 Nov 2015 18:18:44 +0200 Subject: [PATCH 094/908] Rails update to 4.2.4 --- Gemfile | 7 +- Gemfile.lock | 246 ++++++++++++-------- app/models/sent_notification.rb | 5 +- app/workers/emails_on_push_worker.rb | 2 +- bin/ci/upgrade.rb | 0 bin/rails | 6 +- bin/rake | 9 +- bin/setup | 29 +++ bin/upgrade.rb | 0 config/environment.rb | 2 +- config/environments/development.rb | 2 +- config/environments/production.rb | 2 +- config/environments/test.rb | 2 +- config/initializers/cookies_serializer.rb | 2 +- config/initializers/default_url_options.rb | 2 +- config/initializers/rack_attack.rb.example | 14 +- config/initializers/rack_lineprof.rb | 2 +- config/initializers/secret_token.rb | 8 +- config/initializers/session_store.rb | 4 +- config/initializers/sherlock.rb | 2 +- config/initializers/smtp_settings.rb.sample | 2 +- config/initializers/static_files.rb | 2 +- config/routes.rb | 2 +- db/schema.rb | 106 ++++----- 24 files changed, 264 insertions(+), 194 deletions(-) mode change 100644 => 100755 bin/ci/upgrade.rb create mode 100755 bin/setup mode change 100644 => 100755 bin/upgrade.rb diff --git a/Gemfile b/Gemfile index 4762e2e223..4a9f70a129 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,9 @@ source "https://rubygems.org" -gem 'rails', '4.1.14' +gem 'rails', '4.2.4' + +# Responders respond_to and respond_with +gem 'responders', '~> 2.0' # Specify a sprockets version due to security issue # See https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY @@ -98,6 +101,7 @@ gem 'org-ruby', '~> 0.9.12' gem 'creole', '~> 0.5.0' gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 1.5.2' +gem 'net-ssh', '~> 3.0.1' # Diffs gem 'diffy', '~> 3.0.3' @@ -213,6 +217,7 @@ group :development do gem 'rerun', '~> 0.10.0' gem 'bullet', require: false gem 'rblineprof', platform: :mri, require: false + gem 'web-console', '~> 2.0' # Better errors handler gem 'better_errors', '~> 1.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index 46247dd88e..e946051505 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,31 +1,40 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (2.3.1) + CFPropertyList (2.3.2) RedCloth (4.2.9) ace-rails-ap (2.0.1) - actionmailer (4.1.14) - actionpack (= 4.1.14) - actionview (= 4.1.14) + actionmailer (4.2.4) + actionpack (= 4.2.4) + actionview (= 4.2.4) + activejob (= 4.2.4) mail (~> 2.5, >= 2.5.4) - actionpack (4.1.14) - actionview (= 4.1.14) - activesupport (= 4.1.14) - rack (~> 1.5.2) + rails-dom-testing (~> 1.0, >= 1.0.5) + actionpack (4.2.4) + actionview (= 4.2.4) + activesupport (= 4.2.4) + rack (~> 1.6) rack-test (~> 0.6.2) - actionview (4.1.14) - activesupport (= 4.1.14) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (4.2.4) + activesupport (= 4.2.4) builder (~> 3.1) erubis (~> 2.7.0) - activemodel (4.1.14) - activesupport (= 4.1.14) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + activejob (4.2.4) + activesupport (= 4.2.4) + globalid (>= 0.3.0) + activemodel (4.2.4) + activesupport (= 4.2.4) builder (~> 3.1) - activerecord (4.1.14) - activemodel (= 4.1.14) - activesupport (= 4.1.14) - arel (~> 5.0.0) + activerecord (4.2.4) + activemodel (= 4.2.4) + activesupport (= 4.2.4) + arel (~> 6.0) activerecord-deprecated_finders (1.0.4) - activerecord-session_store (0.1.1) + activerecord-session_store (0.1.2) actionpack (>= 4.0.0, < 5) activerecord (>= 4.0.0, < 5) railties (>= 4.0.0, < 5) @@ -33,31 +42,31 @@ GEM activemodel (~> 4.0) activesupport (~> 4.0) rails-observers (~> 0.1.1) - activesupport (4.1.14) - i18n (~> 0.6, >= 0.6.9) + activesupport (4.2.4) + i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) - thread_safe (~> 0.1) + thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) acts-as-taggable-on (3.5.0) activerecord (>= 3.2, < 5) addressable (2.3.8) - after_commit_queue (1.1.0) - rails (>= 3.0) + after_commit_queue (1.3.0) + activerecord (>= 3.0) annotate (2.6.10) activerecord (>= 3.2, <= 4.3) rake (~> 10.4) - arel (5.0.1.20140414130214) + arel (6.0.3) asana (0.0.6) activeresource (>= 3.2.3) - asciidoctor (1.5.2) + asciidoctor (1.5.3) ast (2.1.0) astrolabe (1.3.1) parser (~> 2.2) attr_encrypted (1.3.4) encryptor (>= 1.3.0) attr_required (1.0.0) - autoprefixer-rails (5.2.1.2) + autoprefixer-rails (6.1.1) execjs json awesome_print (1.2.0) @@ -85,15 +94,15 @@ GEM ruby_parser (~> 3.5.0) sass (~> 3.0) terminal-table (~> 1.4) - browser (1.0.0) + browser (1.0.1) builder (3.2.2) - bullet (4.14.9) + bullet (4.14.10) activesupport (>= 3.0.0) uniform_notifier (~> 1.9.0) bundler-audit (0.4.0) bundler (~> 1.2) thor (~> 0.18) - byebug (6.0.2) + byebug (8.2.0) cal-heatmap-rails (0.0.1) capybara (2.4.4) mime-types (>= 1.16) @@ -108,10 +117,25 @@ GEM activemodel (>= 3.2.0) activesupport (>= 3.2.0) json (>= 1.7) - celluloid (0.16.0) - timers (~> 4.0.0) + celluloid (0.17.2) + celluloid-essentials + celluloid-extras + celluloid-fsm + celluloid-pool + celluloid-supervision + timers (>= 4.1.1) + celluloid-essentials (0.20.5) + timers (>= 4.1.1) + celluloid-extras (0.20.5) + timers (>= 4.1.1) + celluloid-fsm (0.20.5) + timers (>= 4.1.1) + celluloid-pool (0.20.5) + timers (>= 4.1.1) + celluloid-supervision (0.20.5) + timers (>= 4.1.1) charlock_holmes (0.7.3) - chunky_png (1.3.4) + chunky_png (1.3.5) cliver (0.3.2) coderay (1.1.0) coercible (1.0.0) @@ -122,15 +146,16 @@ GEM coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.9.1.1) + coffee-script-source (1.10.0) colorize (0.7.7) connection_pool (2.2.0) - coveralls (0.8.2) + coveralls (0.8.9) json (~> 1.8) rest-client (>= 1.6.8, < 2) simplecov (~> 0.10.0) term-ansicolor (~> 1.3) thor (~> 0.19.1) + tins (~> 1.6.0) crack (0.4.2) safe_yaml (~> 1.0.0) creole (0.5.0) @@ -153,7 +178,7 @@ GEM warden (~> 1.2.3) devise-async (0.9.0) devise (~> 3.2) - devise-two-factor (2.0.0) + devise-two-factor (2.0.1) activesupport attr_encrypted (~> 1.3.2) devise (~> 3.5.0) @@ -162,11 +187,11 @@ GEM diff-lcs (1.2.5) diffy (3.0.7) docile (1.1.5) - domain_name (0.5.24) + domain_name (0.5.25) unf (>= 0.0.5, < 1.0.0) doorkeeper (2.1.4) railties (>= 3.2) - dropzonejs-rails (0.7.1) + dropzonejs-rails (0.7.2) rails (> 3.1) email_reply_parser (0.5.8) email_spec (1.6.0) @@ -202,7 +227,7 @@ GEM flog (4.3.2) ruby_parser (~> 3.1, > 3.1.0) sexp_processor (~> 4.4) - flowdock (0.7.0) + flowdock (0.7.1) httparty (~> 0.7) multi_json fog (1.25.0) @@ -224,13 +249,10 @@ GEM fog-core (~> 1.22) fog-json inflecto (~> 0.0.2) - fog-core (1.32.1) + fog-core (1.35.0) builder excon (~> 0.45) formatador (~> 0.2) - mime-types - net-scp (~> 1.1) - net-ssh (>= 2.1.3) fog-json (1.0.2) fog-core (~> 1.0) multi_json (~> 1.10) @@ -242,10 +264,10 @@ GEM fog-core (>= 1.21.0) fog-json fog-xml (>= 0.0.1) - fog-sakuracloud (1.0.1) + fog-sakuracloud (1.4.0) fog-core fog-json - fog-softlayer (0.4.7) + fog-softlayer (1.0.2) fog-core fog-json fog-terremark (0.1.0) @@ -260,7 +282,7 @@ GEM fog-xml (0.1.2) fog-core nokogiri (~> 1.5, >= 1.5.11) - font-awesome-rails (4.4.0.0) + font-awesome-rails (4.5.0.0) railties (>= 3.2, < 5.0) foreman (0.78.0) thor (~> 0.19.1) @@ -270,11 +292,11 @@ GEM ruby-progressbar (~> 1.4) gemnasium-gitlab-service (0.2.6) rugged (~> 0.21) - gemojione (2.0.1) + gemojione (2.1.0) json get_process_mem (0.2.0) gherkin-ruby (0.3.2) - github-linguist (4.7.0) + github-linguist (4.7.2) charlock_holmes (~> 0.7.3) escape_utils (~> 1.1.0) mime-types (>= 1.19) @@ -289,8 +311,8 @@ GEM diff-lcs (~> 1.1) mime-types (~> 1.15) posix-spawn (~> 0.3) - gitlab_emoji (0.1.1) - gemojione (~> 2.0) + gitlab_emoji (0.2.0) + gemojione (~> 2.1) gitlab_git (7.2.20) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) @@ -302,13 +324,15 @@ GEM omniauth (~> 1.0) pyu-ruby-sasl (~> 0.0.3.1) rubyntlm (~> 0.3) + globalid (0.3.6) + activesupport (>= 4.1.0) gollum-grit_adapter (1.0.0) gitlab-grit (~> 2.7, >= 2.7.1) gollum-lib (4.0.3) github-markup (~> 1.3.3) gollum-grit_adapter (~> 1.0) nokogiri (~> 1.6.4) - rouge (~> 1.10.1) + rouge (~> 1.7.4) sanitize (~> 2.1.0) stringex (~> 2.5.1) gon (6.0.1) @@ -337,7 +361,7 @@ GEM haml (>= 4.0.6, < 5.0) html2haml (>= 1.0.1) railties (>= 4.0.1) - hashie (3.4.2) + hashie (3.4.3) highline (1.6.21) hike (1.2.3) hipchat (1.5.2) @@ -355,7 +379,7 @@ GEM http-cookie (1.0.2) domain_name (~> 0.5) http_parser.rb (0.5.3) - httparty (0.13.5) + httparty (0.13.7) json (~> 1.8) multi_xml (>= 0.5.2) httpclient (2.7.0.1) @@ -365,7 +389,7 @@ GEM inflecto (0.0.2) ipaddress (0.8.0) jquery-atwho-rails (1.3.2) - jquery-rails (3.1.3) + jquery-rails (3.1.4) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) jquery-scrollto-rails (1.4.3) @@ -376,19 +400,21 @@ GEM jquery-ui-rails (4.2.1) railties (>= 3.2.16) json (1.8.3) - jwt (1.5.1) + jwt (1.5.2) kaminari (0.16.3) actionpack (>= 3.0.0) activesupport (>= 3.0.0) - kgio (2.9.3) + kgio (2.10.0) launchy (2.4.3) addressable (~> 2.3) letter_opener (1.1.2) launchy (~> 2.2) - listen (2.10.1) - celluloid (~> 0.16.0) + listen (2.9.0) + celluloid (>= 0.15.2) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) + loofah (2.0.3) + nokogiri (>= 1.5.9) macaddr (1.7.1) systemu (~> 2.6.2) mail (2.6.3) @@ -405,16 +431,14 @@ GEM multipart-post (2.0.0) mysql2 (0.3.20) nested_form (0.3.2) - net-ldap (0.11) - net-scp (1.2.1) - net-ssh (>= 2.6.5) - net-ssh (2.9.2) - netrc (0.10.3) + net-ldap (0.12.1) + net-ssh (3.0.1) + netrc (0.11.0) newrelic-grape (2.0.0) grape newrelic_rpm newrelic_rpm (3.9.4.245) - nokogiri (1.6.6.2) + nokogiri (1.6.6.4) mini_portile (~> 0.6.0) nprogress-rails (0.1.6.7) oauth (0.4.7) @@ -438,12 +462,15 @@ GEM omniauth-github (1.1.2) omniauth (~> 1.0) omniauth-oauth2 (~> 1.1) - omniauth-gitlab (1.0.0) + omniauth-gitlab (1.0.1) omniauth (~> 1.0) omniauth-oauth2 (~> 1.0) - omniauth-google-oauth2 (0.2.6) - omniauth (> 1.0) - omniauth-oauth2 (~> 1.1) + omniauth-google-oauth2 (0.2.10) + addressable (~> 2.3) + jwt (~> 1.0) + multi_json (~> 1.3) + omniauth (>= 1.1.1) + omniauth-oauth2 (~> 1.3.1) omniauth-kerberos (0.3.0) omniauth-multipassword timfel-krb5-auth (~> 0.8) @@ -467,18 +494,18 @@ GEM activesupport nokogiri (>= 1.4.4) omniauth (~> 1.0) - opennebula (4.12.1) + opennebula (4.14.2) json nokogiri rbvmomi org-ruby (0.9.12) rubypants (~> 0.2) orm_adapter (0.5.0) - paranoia (2.1.3) + paranoia (2.1.4) activerecord (~> 4.0) - parser (2.2.2.6) + parser (2.2.3.0) ast (>= 1.1, < 3.0) - pg (0.18.2) + pg (0.18.4) poltergeist (1.6.0) capybara (~> 2.1) cliver (~> 0.3.1) @@ -486,7 +513,7 @@ GEM websocket-driver (>= 0.2.0) posix-spawn (0.3.11) powerpack (0.0.9) - pry (0.10.1) + pry (0.10.3) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) @@ -495,7 +522,7 @@ GEM pyu-ruby-sasl (0.0.3.3) quiet_assets (1.0.3) railties (>= 3.1, < 5.0) - rack (1.5.5) + rack (1.6.4) rack-accept (0.4.5) rack (>= 0.4) rack-attack (4.3.0) @@ -513,28 +540,37 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.1.14) - actionmailer (= 4.1.14) - actionpack (= 4.1.14) - actionview (= 4.1.14) - activemodel (= 4.1.14) - activerecord (= 4.1.14) - activesupport (= 4.1.14) + rails (4.2.4) + actionmailer (= 4.2.4) + actionpack (= 4.2.4) + actionview (= 4.2.4) + activejob (= 4.2.4) + activemodel (= 4.2.4) + activerecord (= 4.2.4) + activesupport (= 4.2.4) bundler (>= 1.3.0, < 2.0) - railties (= 4.1.14) - sprockets-rails (~> 2.0) + railties (= 4.2.4) + sprockets-rails + rails-deprecated_sanitizer (1.0.3) + activesupport (>= 4.2.0.alpha) + rails-dom-testing (1.0.7) + activesupport (>= 4.2.0.beta, < 5.0) + nokogiri (~> 1.6.0) + rails-deprecated_sanitizer (>= 1.0.1) + rails-html-sanitizer (1.0.2) + loofah (~> 2.0) rails-observers (0.1.2) activemodel (~> 4.0) - railties (4.1.14) - actionpack (= 4.1.14) - activesupport (= 4.1.14) + railties (4.2.4) + actionpack (= 4.2.4) + activesupport (= 4.2.4) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.0.0) raindrops (0.15.0) rake (10.4.2) raphael-rails (2.1.2) - rb-fsevent (0.9.5) + rb-fsevent (0.9.6) rb-inotify (0.9.5) ffi (>= 0.5.0) rblineprof (0.3.6) @@ -546,13 +582,13 @@ GEM rdoc (3.12.2) json (~> 1.4) redcarpet (3.3.3) - redis (3.2.1) - redis-actionpack (4.0.0) + redis (3.2.2) + redis-actionpack (4.0.1) actionpack (~> 4) redis-rack (~> 1.5.0) redis-store (~> 1.1.0) - redis-activesupport (4.1.1) - activesupport (~> 4) + redis-activesupport (4.1.5) + activesupport (>= 3, < 5) redis-store (~> 1.1.0) redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) @@ -563,20 +599,20 @@ GEM redis-actionpack (~> 4) redis-activesupport (~> 4) redis-store (~> 1.1.0) - redis-store (1.1.6) + redis-store (1.1.7) redis (>= 2.2) - request_store (1.2.0) + request_store (1.2.1) rerun (0.10.0) listen (~> 2.7, >= 2.7.3) - responders (1.1.2) - railties (>= 3.2, < 4.2) + responders (2.1.0) + railties (>= 4.2.0, < 5) rest-client (1.8.0) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 3.0) netrc (~> 0.7) rinku (1.7.3) rotp (2.1.1) - rouge (1.10.1) + rouge (1.7.7) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -716,14 +752,14 @@ GEM terminal-table (1.5.2) test_after_commit (0.2.7) activerecord (>= 3.2) - thin (1.6.3) + thin (1.6.4) daemons (~> 1.0, >= 1.0.9) - eventmachine (~> 1.0) + eventmachine (~> 1.0, >= 1.0.4) rack (~> 1.0) thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) - timers (4.0.4) + timers (4.1.1) hitimes timfel-krb5-auth (0.8.3) tinder (1.10.1) @@ -756,9 +792,9 @@ GEM kgio (~> 2.6) rack raindrops (~> 0.7) - unicorn-worker-killer (0.4.3) + unicorn-worker-killer (0.4.4) get_process_mem (~> 0) - unicorn (~> 4) + unicorn (>= 4, < 6) uniform_notifier (1.9.0) uuid (2.3.8) macaddr (~> 1.0) @@ -770,10 +806,15 @@ GEM equalizer (~> 0.0, >= 0.0.9) warden (1.2.3) rack (>= 1.0) + web-console (2.2.1) + activemodel (>= 4.0) + binding_of_caller (>= 0.7.2) + railties (>= 4.0) + sprockets-rails (>= 2.0, < 4.0) webmock (1.21.0) addressable (>= 2.3.6) crack (>= 0.3.2) - websocket-driver (0.6.2) + websocket-driver (0.6.3) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) wikicloth (0.8.1) @@ -865,6 +906,7 @@ DEPENDENCIES mousetrap-rails (~> 1.4.6) mysql2 (~> 0.3.16) nested_form (~> 0.3.2) + net-ssh (~> 3.0.1) newrelic-grape newrelic_rpm (~> 3.9.4.245) nprogress-rails (~> 0.1.6.7) @@ -890,7 +932,7 @@ DEPENDENCIES rack-attack (~> 4.3.0) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) - rails (= 4.1.14) + rails (= 4.2.4) raphael-rails (~> 2.1.2) rblineprof rdoc (~> 3.6) @@ -898,6 +940,7 @@ DEPENDENCIES redis-rails (~> 4.0.0) request_store (~> 1.2.0) rerun (~> 0.10.0) + responders (~> 2.0) rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.3.0) rubocop (~> 0.28.0) @@ -938,6 +981,7 @@ DEPENDENCIES unicorn-worker-killer (~> 0.4.2) version_sorter (~> 2.0.0) virtus (~> 1.0.1) + web-console (~> 2.0) webmock (~> 1.21.0) wikicloth (= 0.8.1) diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index 3eed5c16e4..d8fe65b06f 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -17,9 +17,8 @@ class SentNotification < ActiveRecord::Base belongs_to :noteable, polymorphic: true belongs_to :recipient, class_name: "User" - validate :project, :recipient, :reply_key, presence: true - validate :reply_key, uniqueness: true - + validates :project, :recipient, :reply_key, presence: true + validates :reply_key, uniqueness: true validates :noteable_id, presence: true, unless: :for_commit? validates :commit_id, presence: true, if: :for_commit? validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 916a99bb27..c4d8595d45 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -53,7 +53,7 @@ class EmailsOnPushWorker reverse_compare: reverse_compare, send_from_committer_email: send_from_committer_email, disable_diffs: disable_diffs - ).deliver + ).deliver_now # These are input errors and won't be corrected even if Sidekiq retries rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e logger.info("Failed to send e-mail for project '#{project.name_with_namespace}' to #{recipient}: #{e}") diff --git a/bin/ci/upgrade.rb b/bin/ci/upgrade.rb old mode 100644 new mode 100755 diff --git a/bin/rails b/bin/rails index 7feb6a30e6..5191e6927a 100755 --- a/bin/rails +++ b/bin/rails @@ -1,8 +1,4 @@ #!/usr/bin/env ruby -begin - load File.expand_path("../spring", __FILE__) -rescue LoadError -end -APP_PATH = File.expand_path('../../config/application', __FILE__) +APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands' diff --git a/bin/rake b/bin/rake index 0fb4e07e13..17240489f6 100755 --- a/bin/rake +++ b/bin/rake @@ -1,7 +1,4 @@ #!/usr/bin/env ruby -begin - load File.expand_path("../spring", __FILE__) -rescue LoadError -end -require 'bundler/setup' -load Gem.bin_path('rake', 'rake') +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000000..acdb2c1389 --- /dev/null +++ b/bin/setup @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +require 'pathname' + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +Dir.chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file: + + puts "== Installing dependencies ==" + system "gem install bundler --conservative" + system "bundle check || bundle install" + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # system "cp config/database.yml.sample config/database.yml" + # end + + puts "\n== Preparing database ==" + system "bin/rake db:setup" + + puts "\n== Removing old logs and tempfiles ==" + system "rm -f log/*" + system "rm -rf tmp/cache" + + puts "\n== Restarting application server ==" + system "touch tmp/restart.txt" +end diff --git a/bin/upgrade.rb b/bin/upgrade.rb old mode 100644 new mode 100755 diff --git a/config/environment.rb b/config/environment.rb index 3b186a9d57..df3006d349 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -2,4 +2,4 @@ require File.expand_path('../application', __FILE__) # Initialize the rails application -Gitlab::Application.initialize! +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb index 827a110c24..c22722c606 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,4 +1,4 @@ -Gitlab::Application.configure do +Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb # In the development environment your application's code is reloaded on diff --git a/config/environments/production.rb b/config/environments/production.rb index 3316ece387..e8250d6645 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,4 +1,4 @@ -Gitlab::Application.configure do +Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb # Code is not reloaded between requests diff --git a/config/environments/test.rb b/config/environments/test.rb index 955540837d..46982be286 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,4 +1,4 @@ -Gitlab::Application.configure do +Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb # The test environment is used exclusively to run your application's diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb index 43adac8b2c..54516e3f23 100644 --- a/config/initializers/cookies_serializer.rb +++ b/config/initializers/cookies_serializer.rb @@ -1,3 +1,3 @@ # Be sure to restart your server when you modify this file. -Gitlab::Application.config.action_dispatch.cookies_serializer = :hybrid +Rails.application.config.action_dispatch.cookies_serializer = :hybrid diff --git a/config/initializers/default_url_options.rb b/config/initializers/default_url_options.rb index f9f88f95db..8fd27b1d88 100644 --- a/config/initializers/default_url_options.rb +++ b/config/initializers/default_url_options.rb @@ -8,4 +8,4 @@ unless Gitlab.config.gitlab_on_standard_port? default_url_options[:port] = Gitlab.config.gitlab.port end -Gitlab::Application.routes.default_url_options = default_url_options +Rails.application.routes.default_url_options = default_url_options diff --git a/config/initializers/rack_attack.rb.example b/config/initializers/rack_attack.rb.example index 2155ea1456..b1bbcca1d6 100644 --- a/config/initializers/rack_attack.rb.example +++ b/config/initializers/rack_attack.rb.example @@ -4,13 +4,13 @@ # If you change this file in a Merge Request, please also create a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests paths_to_be_protected = [ - "#{Gitlab::Application.config.relative_url_root}/users/password", - "#{Gitlab::Application.config.relative_url_root}/users/sign_in", - "#{Gitlab::Application.config.relative_url_root}/api/#{API::API.version}/session.json", - "#{Gitlab::Application.config.relative_url_root}/api/#{API::API.version}/session", - "#{Gitlab::Application.config.relative_url_root}/users", - "#{Gitlab::Application.config.relative_url_root}/users/confirmation", - "#{Gitlab::Application.config.relative_url_root}/unsubscribes/" + "#{Rails.application.config.relative_url_root}/users/password", + "#{Rails.application.config.relative_url_root}/users/sign_in", + "#{Rails.application.config.relative_url_root}/api/#{API::API.version}/session.json", + "#{Rails.application.config.relative_url_root}/api/#{API::API.version}/session", + "#{Rails.application.config.relative_url_root}/users", + "#{Rails.application.config.relative_url_root}/users/confirmation", + "#{Rails.application.config.relative_url_root}/unsubscribes/" ] diff --git a/config/initializers/rack_lineprof.rb b/config/initializers/rack_lineprof.rb index f0c006d811..22e77a32c6 100644 --- a/config/initializers/rack_lineprof.rb +++ b/config/initializers/rack_lineprof.rb @@ -2,7 +2,7 @@ # with darker backgrounds. This patch tweaks the colors a bit so the output is # actually readable. if Rails.env.development? and RUBY_ENGINE == 'ruby' and ENV['ENABLE_LINEPROF'] - Gitlab::Application.config.middleware.use(Rack::Lineprof) + Rails.application.config.middleware.use(Rack::Lineprof) module Rack class Lineprof diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 1b518c3bec..dae3a4a9a9 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -22,15 +22,15 @@ def find_secure_token end end -Gitlab::Application.config.secret_token = find_secure_token -Gitlab::Application.config.secret_key_base = find_secure_token +Rails.application.config.secret_token = find_secure_token +Rails.application.config.secret_key_base = find_secure_token # CI def generate_new_secure_token SecureRandom.hex(64) end -if Gitlab::Application.secrets.db_key_base.blank? +if Rails.application.secrets.db_key_base.blank? warn "Missing `db_key_base` for '#{Rails.env}' environment. The secrets will be generated and stored in `config/secrets.yml`" all_secrets = YAML.load_file('config/secrets.yml') if File.exist?('config/secrets.yml') @@ -46,5 +46,5 @@ if Gitlab::Application.secrets.db_key_base.blank? file.write(YAML.dump(all_secrets)) end - Gitlab::Application.secrets.db_key_base = env_secrets['db_key_base'] + Rails.application.secrets.db_key_base = env_secrets['db_key_base'] end diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index f30178ff71..d5208b8c93 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -13,11 +13,11 @@ end unless Rails.env.test? Gitlab::Application.config.session_store( :redis_store, # Using the cookie_store would enable session replay attacks. - servers: Gitlab::Application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store + servers: Rails.application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store key: '_gitlab_session', secure: Gitlab.config.gitlab.https, httponly: true, expire_after: Settings.gitlab['session_expire_delay'] * 60, - path: (Gitlab::Application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root + path: (Rails.application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root ) end diff --git a/config/initializers/sherlock.rb b/config/initializers/sherlock.rb index 42b0d78c85..8f2ababb71 100644 --- a/config/initializers/sherlock.rb +++ b/config/initializers/sherlock.rb @@ -1,5 +1,5 @@ if Gitlab::Sherlock.enabled? - Gitlab::Application.configure do |config| + Rails.application.configure do |config| config.middleware.use(Gitlab::Sherlock::Middleware) end end diff --git a/config/initializers/smtp_settings.rb.sample b/config/initializers/smtp_settings.rb.sample index 25ec247a09..ec182502d4 100644 --- a/config/initializers/smtp_settings.rb.sample +++ b/config/initializers/smtp_settings.rb.sample @@ -8,7 +8,7 @@ # If you change this file in a Merge Request, please also create a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests if Rails.env.production? - Gitlab::Application.config.action_mailer.delivery_method = :smtp + Rails.application.config.action_mailer.delivery_method = :smtp ActionMailer::Base.smtp_settings = { address: "email.server.com", diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index e6d5600edb..d9042c652b 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -1,4 +1,4 @@ -app = Gitlab::Application +app = Rails.application if app.config.serve_static_assets # The `ActionDispatch::Static` middleware intercepts requests for static files diff --git a/config/routes.rb b/config/routes.rb index ac81a2aac7..5c114452a3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,7 @@ require 'sidekiq/web' require 'api/api' -Gitlab::Application.routes.draw do +Rails.application.routes.draw do if Gitlab::Sherlock.enabled? namespace :sherlock do resources :transactions, only: [:index, :show] do diff --git a/db/schema.rb b/db/schema.rb index 5bbe0c908e..fbcb711e56 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -16,7 +16,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" - create_table "abuse_reports", force: true do |t| + create_table "abuse_reports", force: :cascade do |t| t.integer "reporter_id" t.integer "user_id" t.text "message" @@ -24,7 +24,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do t.datetime "updated_at" end - create_table "application_settings", force: true do |t| + create_table "application_settings", force: :cascade do |t| t.integer "default_projects_limit" t.boolean "signup_enabled" t.boolean "signin_enabled" @@ -51,7 +51,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do t.integer "max_artifacts_size", default: 100, null: false end - create_table "audit_events", force: true do |t| + create_table "audit_events", force: :cascade do |t| t.integer "author_id", null: false t.string "type", null: false t.integer "entity_id", null: false @@ -65,7 +65,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree - create_table "broadcast_messages", force: true do |t| + create_table "broadcast_messages", force: :cascade do |t| t.text "message", null: false t.datetime "starts_at" t.datetime "ends_at" @@ -76,14 +76,14 @@ ActiveRecord::Schema.define(version: 20151118162244) do t.string "font" end - create_table "ci_application_settings", force: true do |t| + create_table "ci_application_settings", force: :cascade do |t| t.boolean "all_broken_builds" t.boolean "add_pusher" t.datetime "created_at" t.datetime "updated_at" end - create_table "ci_builds", force: true do |t| + create_table "ci_builds", force: :cascade do |t| t.integer "project_id" t.string "status" t.datetime "finished_at" @@ -123,7 +123,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree add_index "ci_builds", ["type"], name: "index_ci_builds_on_type", using: :btree - create_table "ci_commits", force: true do |t| + create_table "ci_commits", force: :cascade do |t| t.integer "project_id" t.string "ref" t.string "sha" @@ -144,7 +144,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree - create_table "ci_events", force: true do |t| + create_table "ci_events", force: :cascade do |t| t.integer "project_id" t.integer "user_id" t.integer "is_admin" @@ -157,7 +157,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "ci_events", ["is_admin"], name: "index_ci_events_on_is_admin", using: :btree add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree - create_table "ci_jobs", force: true do |t| + create_table "ci_jobs", force: :cascade do |t| t.integer "project_id", null: false t.text "commands" t.boolean "active", default: true, null: false @@ -174,7 +174,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "ci_jobs", ["deleted_at"], name: "index_ci_jobs_on_deleted_at", using: :btree add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree - create_table "ci_projects", force: true do |t| + create_table "ci_projects", force: :cascade do |t| t.string "name" t.integer "timeout", default: 3600, null: false t.datetime "created_at" @@ -200,7 +200,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "ci_projects", ["gitlab_id"], name: "index_ci_projects_on_gitlab_id", using: :btree add_index "ci_projects", ["shared_runners_enabled"], name: "index_ci_projects_on_shared_runners_enabled", using: :btree - create_table "ci_runner_projects", force: true do |t| + create_table "ci_runner_projects", force: :cascade do |t| t.integer "runner_id", null: false t.integer "project_id", null: false t.datetime "created_at" @@ -210,7 +210,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree - create_table "ci_runners", force: true do |t| + create_table "ci_runners", force: :cascade do |t| t.string "token" t.datetime "created_at" t.datetime "updated_at" @@ -225,7 +225,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do t.string "architecture" end - create_table "ci_services", force: true do |t| + create_table "ci_services", force: :cascade do |t| t.string "type" t.string "title" t.integer "project_id", null: false @@ -237,7 +237,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree - create_table "ci_sessions", force: true do |t| + create_table "ci_sessions", force: :cascade do |t| t.string "session_id", null: false t.text "data" t.datetime "created_at" @@ -247,7 +247,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "ci_sessions", ["session_id"], name: "index_ci_sessions_on_session_id", using: :btree add_index "ci_sessions", ["updated_at"], name: "index_ci_sessions_on_updated_at", using: :btree - create_table "ci_taggings", force: true do |t| + create_table "ci_taggings", force: :cascade do |t| t.integer "tag_id" t.integer "taggable_id" t.string "taggable_type" @@ -260,14 +260,14 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "ci_taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "ci_taggings_idx", unique: true, using: :btree add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree - create_table "ci_tags", force: true do |t| + create_table "ci_tags", force: :cascade do |t| t.string "name" t.integer "taggings_count", default: 0 end add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree - create_table "ci_trigger_requests", force: true do |t| + create_table "ci_trigger_requests", force: :cascade do |t| t.integer "trigger_id", null: false t.text "variables" t.datetime "created_at" @@ -275,7 +275,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do t.integer "commit_id" end - create_table "ci_triggers", force: true do |t| + create_table "ci_triggers", force: :cascade do |t| t.string "token" t.integer "project_id", null: false t.datetime "deleted_at" @@ -285,7 +285,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree - create_table "ci_variables", force: true do |t| + create_table "ci_variables", force: :cascade do |t| t.integer "project_id", null: false t.string "key" t.text "value" @@ -296,14 +296,14 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree - create_table "ci_web_hooks", force: true do |t| + create_table "ci_web_hooks", force: :cascade do |t| t.string "url", null: false t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" end - create_table "deploy_keys_projects", force: true do |t| + create_table "deploy_keys_projects", force: :cascade do |t| t.integer "deploy_key_id", null: false t.integer "project_id", null: false t.datetime "created_at" @@ -312,7 +312,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree - create_table "emails", force: true do |t| + create_table "emails", force: :cascade do |t| t.integer "user_id", null: false t.string "email", null: false t.datetime "created_at" @@ -322,7 +322,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "emails", ["email"], name: "index_emails_on_email", unique: true, using: :btree add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree - create_table "events", force: true do |t| + create_table "events", force: :cascade do |t| t.string "target_type" t.integer "target_id" t.string "title" @@ -341,7 +341,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "events", ["target_id"], name: "index_events_on_target_id", using: :btree add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree - create_table "forked_project_links", force: true do |t| + create_table "forked_project_links", force: :cascade do |t| t.integer "forked_to_project_id", null: false t.integer "forked_from_project_id", null: false t.datetime "created_at" @@ -350,7 +350,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree - create_table "identities", force: true do |t| + create_table "identities", force: :cascade do |t| t.string "extern_uid" t.string "provider" t.integer "user_id" @@ -361,7 +361,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "identities", ["created_at", "id"], name: "index_identities_on_created_at_and_id", using: :btree add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree - create_table "issues", force: true do |t| + create_table "issues", force: :cascade do |t| t.string "title" t.integer "assignee_id" t.integer "author_id" @@ -387,7 +387,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "issues", ["state"], name: "index_issues_on_state", using: :btree add_index "issues", ["title"], name: "index_issues_on_title", using: :btree - create_table "keys", force: true do |t| + create_table "keys", force: :cascade do |t| t.integer "user_id" t.datetime "created_at" t.datetime "updated_at" @@ -401,7 +401,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree - create_table "label_links", force: true do |t| + create_table "label_links", force: :cascade do |t| t.integer "label_id" t.integer "target_id" t.string "target_type" @@ -412,7 +412,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "label_links", ["label_id"], name: "index_label_links_on_label_id", using: :btree add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree - create_table "labels", force: true do |t| + create_table "labels", force: :cascade do |t| t.string "title" t.string "color" t.integer "project_id" @@ -423,7 +423,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree - create_table "lfs_objects", force: true do |t| + create_table "lfs_objects", force: :cascade do |t| t.string "oid", null: false t.integer "size", null: false t.datetime "created_at" @@ -433,7 +433,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree - create_table "lfs_objects_projects", force: true do |t| + create_table "lfs_objects_projects", force: :cascade do |t| t.integer "lfs_object_id", null: false t.integer "project_id", null: false t.datetime "created_at" @@ -442,7 +442,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree - create_table "members", force: true do |t| + create_table "members", force: :cascade do |t| t.integer "access_level", null: false t.integer "source_id", null: false t.string "source_type", null: false @@ -464,7 +464,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "members", ["type"], name: "index_members_on_type", using: :btree add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree - create_table "merge_request_diffs", force: true do |t| + create_table "merge_request_diffs", force: :cascade do |t| t.string "state" t.text "st_commits" t.text "st_diffs" @@ -475,7 +475,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree - create_table "merge_requests", force: true do |t| + create_table "merge_requests", force: :cascade do |t| t.string "target_branch", null: false t.string "source_branch", null: false t.integer "source_project_id", null: false @@ -507,7 +507,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree - create_table "milestones", force: true do |t| + create_table "milestones", force: :cascade do |t| t.string "title", null: false t.integer "project_id", null: false t.text "description" @@ -523,7 +523,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree - create_table "namespaces", force: true do |t| + create_table "namespaces", force: :cascade do |t| t.string "name", null: false t.string "path", null: false t.integer "owner_id" @@ -542,7 +542,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "namespaces", ["public"], name: "index_namespaces_on_public", using: :btree add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree - create_table "notes", force: true do |t| + create_table "notes", force: :cascade do |t| t.text "note" t.string "noteable_type" t.integer "author_id" @@ -571,7 +571,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree - create_table "oauth_access_grants", force: true do |t| + create_table "oauth_access_grants", force: :cascade do |t| t.integer "resource_owner_id", null: false t.integer "application_id", null: false t.string "token", null: false @@ -584,7 +584,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree - create_table "oauth_access_tokens", force: true do |t| + create_table "oauth_access_tokens", force: :cascade do |t| t.integer "resource_owner_id" t.integer "application_id" t.string "token", null: false @@ -599,7 +599,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "oauth_access_tokens", ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id", using: :btree add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree - create_table "oauth_applications", force: true do |t| + create_table "oauth_applications", force: :cascade do |t| t.string "name", null: false t.string "uid", null: false t.string "secret", null: false @@ -614,12 +614,12 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree - create_table "project_import_data", force: true do |t| + create_table "project_import_data", force: :cascade do |t| t.integer "project_id" t.text "data" end - create_table "projects", force: true do |t| + create_table "projects", force: :cascade do |t| t.string "name" t.string "path" t.text "description" @@ -656,7 +656,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree - create_table "protected_branches", force: true do |t| + create_table "protected_branches", force: :cascade do |t| t.integer "project_id", null: false t.string "name", null: false t.datetime "created_at" @@ -666,7 +666,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree - create_table "releases", force: true do |t| + create_table "releases", force: :cascade do |t| t.string "tag" t.text "description" t.integer "project_id" @@ -677,7 +677,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree add_index "releases", ["project_id"], name: "index_releases_on_project_id", using: :btree - create_table "sent_notifications", force: true do |t| + create_table "sent_notifications", force: :cascade do |t| t.integer "project_id" t.integer "noteable_id" t.string "noteable_type" @@ -689,7 +689,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree - create_table "services", force: true do |t| + create_table "services", force: :cascade do |t| t.string "type" t.string "title" t.integer "project_id" @@ -709,7 +709,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree add_index "services", ["template"], name: "index_services_on_template", using: :btree - create_table "snippets", force: true do |t| + create_table "snippets", force: :cascade do |t| t.string "title" t.text "content" t.integer "author_id", null: false @@ -729,7 +729,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree - create_table "subscriptions", force: true do |t| + create_table "subscriptions", force: :cascade do |t| t.integer "user_id" t.integer "subscribable_id" t.string "subscribable_type" @@ -740,7 +740,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id"], name: "subscriptions_user_id_and_ref_fields", unique: true, using: :btree - create_table "taggings", force: true do |t| + create_table "taggings", force: :cascade do |t| t.integer "tag_id" t.integer "taggable_id" t.string "taggable_type" @@ -753,14 +753,14 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true, using: :btree add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree - create_table "tags", force: true do |t| + create_table "tags", force: :cascade do |t| t.string "name" t.integer "taggings_count", default: 0 end add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree - create_table "users", force: true do |t| + create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false t.string "reset_password_token" @@ -826,7 +826,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree add_index "users", ["username"], name: "index_users_on_username", using: :btree - create_table "users_star_projects", force: true do |t| + create_table "users_star_projects", force: :cascade do |t| t.integer "project_id", null: false t.integer "user_id", null: false t.datetime "created_at" @@ -837,7 +837,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do add_index "users_star_projects", ["user_id", "project_id"], name: "index_users_star_projects_on_user_id_and_project_id", unique: true, using: :btree add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree - create_table "web_hooks", force: true do |t| + create_table "web_hooks", force: :cascade do |t| t.string "url" t.integer "project_id" t.datetime "created_at" From e55473ad6880a68a86f355b7825dbdaf67e1f375 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 25 Nov 2015 11:30:33 -0800 Subject: [PATCH 095/908] Expire application settings from cache at startup If a DB migration occurs, there's a chance that the application settings are loaded from the cache and provide stale values, causing Error 500s. This ensures that at startup the settings are always refreshed. Closes #3643 --- CHANGELOG | 1 + app/models/application_setting.rb | 12 ++++++++++-- app/models/ci/application_setting.rb | 12 ++++++++++-- config/initializers/1_settings.rb | 4 ++++ 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8916b9f040..a6bef82693 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) + - Ensure cached application settings are refreshed at startup (Stan Hu) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - Fix 500 error when update group member permission diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index b2d5fe1558..3df8135acf 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -73,15 +73,23 @@ class ApplicationSetting < ActiveRecord::Base end after_commit do - Rails.cache.write('application_setting.last', self) + Rails.cache.write(cache_key, self) end def self.current - Rails.cache.fetch('application_setting.last') do + Rails.cache.fetch(cache_key) do ApplicationSetting.last end end + def self.expire + Rails.cache.delete(cache_key) + end + + def self.cache_key + 'application_setting.last' + end + def self.create_from_defaults create( default_projects_limit: Settings.gitlab['default_projects_limit'], diff --git a/app/models/ci/application_setting.rb b/app/models/ci/application_setting.rb index 1307fa0b47..4e512d290e 100644 --- a/app/models/ci/application_setting.rb +++ b/app/models/ci/application_setting.rb @@ -14,11 +14,15 @@ module Ci extend Ci::Model after_commit do - Rails.cache.write('ci_application_setting.last', self) + Rails.cache.write(cache_key, self) + end + + def self.expire + Rails.cache.delete(cache_key) end def self.current - Rails.cache.fetch('ci_application_setting.last') do + Rails.cache.fetch(cache_key) do Ci::ApplicationSetting.last end end @@ -29,5 +33,9 @@ module Ci add_pusher: Settings.gitlab_ci['add_pusher'], ) end + + def self.cache_key + 'ci_application_setting.last' + end end end diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index b162b8a83f..80b480eac3 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -293,3 +293,7 @@ if Rails.env.test? Settings.gitlab['default_can_create_group'] = true Settings.gitlab['default_can_create_team'] = false end + +# Force a refresh of application settings at startup +ApplicationSetting.expire +Ci::ApplicationSetting.expire From 8dcef120cd94717b4f82db864191698826ca02a5 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 25 Nov 2015 17:24:07 -0200 Subject: [PATCH 096/908] Fix raw private snippets access workflow --- CHANGELOG | 1 + app/controllers/snippets_controller.rb | 2 +- spec/controllers/snippets_controller_spec.rb | 115 +++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 8916b9f040..d61d85e405 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - Fix 500 error when update group member permission + - Fix: Raw private snippets access workflow v 8.2.1 - Forcefully update builds that didn't want to update with state machine diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 08f2483af3..c72df73af4 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -2,7 +2,7 @@ class SnippetsController < ApplicationController before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow read snippet - before_action :authorize_read_snippet!, only: [:show] + before_action :authorize_read_snippet!, only: [:show, :raw] # Allow modify snippet before_action :authorize_update_snippet!, only: [:edit, :update] diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index e9b823c523..b3dcb52c50 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -115,4 +115,119 @@ describe SnippetsController do end end end + + describe 'GET #raw' do + let(:user) { create(:user) } + + context 'when the personal snippet is private' do + let(:personal_snippet) { create(:personal_snippet, :private, author: user) } + + context 'when signed in' do + before do + sign_in(user) + end + + context 'when signed in user is not the author' do + let(:other_author) { create(:author) } + let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) } + + it 'responds with status 404' do + get :raw, id: other_personal_snippet.to_param + + expect(response.status).to eq(404) + end + end + + context 'when signed in user is the author' do + it 'renders the raw snippet' do + get :raw, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response.status).to eq(200) + end + end + end + + context 'when not signed in' do + it 'redirects to the sign in page' do + get :raw, id: personal_snippet.to_param + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context 'when the personal snippet is internal' do + let(:personal_snippet) { create(:personal_snippet, :internal, author: user) } + + context 'when signed in' do + before do + sign_in(user) + end + + it 'renders the raw snippet' do + get :raw, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response.status).to eq(200) + end + end + + context 'when not signed in' do + it 'redirects to the sign in page' do + get :raw, id: personal_snippet.to_param + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context 'when the personal snippet is public' do + let(:personal_snippet) { create(:personal_snippet, :public, author: user) } + + context 'when signed in' do + before do + sign_in(user) + end + + it 'renders the raw snippet' do + get :raw, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response.status).to eq(200) + end + end + + context 'when not signed in' do + it 'renders the raw snippet' do + get :raw, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response.status).to eq(200) + end + end + end + + context 'when the personal snippet does not exist' do + context 'when signed in' do + before do + sign_in(user) + end + + it 'responds with status 404' do + get :raw, id: 'doesntexist' + + expect(response.status).to eq(404) + end + end + + context 'when not signed in' do + it 'responds with status 404' do + get :raw, id: 'doesntexist' + + expect(response.status).to eq(404) + end + end + end + end end From 73eca2a95ef401a8e3061a14949ee1dbf34f99b9 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 25 Nov 2015 16:26:15 -0500 Subject: [PATCH 097/908] Decouple Markdown previews from DropzoneInput --- .../javascripts/dropzone_input.js.coffee | 80 +---------------- .../javascripts/markdown_preview.js.coffee | 87 +++++++++++++++++++ app/views/projects/_md_preview.html.haml | 2 +- 3 files changed, 92 insertions(+), 77 deletions(-) create mode 100644 app/assets/javascripts/markdown_preview.js.coffee diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index 6f789e668a..30a35a0433 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -1,3 +1,5 @@ +#= require markdown_preview + class @DropzoneInput constructor: (form) -> Dropzone.autoDiscover = false @@ -11,17 +13,14 @@ class @DropzoneInput uploadProgress = $("
") btnAlert = "" project_uploads_path = window.project_uploads_path or null - markdown_preview_path = window.markdown_preview_path or null max_file_size = gon.max_file_size or 10 form_textarea = $(form).find("textarea.markdown-area") form_textarea.wrap "
" form_textarea.on 'paste', (event) => handlePaste(event) - form_textarea.on "input", -> - hideReferencedUsers() - form_textarea.on "blur", -> - renderMarkdown() + + $(form).setupMarkdownPreview() form_dropzone = $(form).find('.div-dropzone') form_dropzone.parent().addClass "div-dropzone-wrapper" @@ -34,42 +33,6 @@ class @DropzoneInput "opacity": 0 "display": "none" - # Preview button - $(document).off "click", ".js-md-preview-button" - $(document).on "click", ".js-md-preview-button", (e) -> - ### - Shows the Markdown preview. - - Lets the server render GFM into Html and displays it. - ### - e.preventDefault() - form = $(this).closest("form") - # toggle tabs - form.find(".js-md-write-button").parent().removeClass "active" - form.find(".js-md-preview-button").parent().addClass "active" - - # toggle content - form.find(".md-write-holder").hide() - form.find(".md-preview-holder").show() - - renderMarkdown() - - # Write button - $(document).off "click", ".js-md-write-button" - $(document).on "click", ".js-md-write-button", (e) -> - ### - Shows the Markdown textarea. - ### - e.preventDefault() - form = $(this).closest("form") - # toggle tabs - form.find(".js-md-write-button").parent().addClass "active" - form.find(".js-md-preview-button").parent().removeClass "active" - - # toggle content - form.find(".md-write-holder").show() - form.find(".md-preview-holder").hide() - dropzone = form_dropzone.dropzone( url: project_uploads_path dictDefaultMessage: "" @@ -136,41 +99,6 @@ class @DropzoneInput child = $(dropzone[0]).children("textarea") - hideReferencedUsers = -> - referencedUsers = form.find(".referenced-users") - referencedUsers.hide() - - renderReferencedUsers = (users) -> - referencedUsers = form.find(".referenced-users") - - if referencedUsers.length - if users.length >= 10 - referencedUsers.show() - referencedUsers.find(".js-referenced-users-count").text users.length - else - referencedUsers.hide() - - renderMarkdown = -> - preview = form.find(".js-md-preview") - mdText = form.find(".markdown-area").val() - if mdText.trim().length is 0 - preview.text "Nothing to preview." - hideReferencedUsers() - else - preview.text "Loading..." - $.ajax( - type: "POST", - url: markdown_preview_path, - data: { - text: mdText - }, - dataType: "json" - ).success (data) -> - preview.html data.body - preview.syntaxHighlight() - - renderReferencedUsers data.references.users - formatLink = (link) -> text = "[#{link.alt}](#{link.url})" text = "!#{text}" if link.is_image diff --git a/app/assets/javascripts/markdown_preview.js.coffee b/app/assets/javascripts/markdown_preview.js.coffee new file mode 100644 index 0000000000..98fc8f1734 --- /dev/null +++ b/app/assets/javascripts/markdown_preview.js.coffee @@ -0,0 +1,87 @@ +# MarkdownPreview +# +# Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, +# and showing a warning when more than `x` users are referenced. +# +class @MarkdownPreview + # Minimum number of users referenced before triggering a warning + referenceThreshold: 10 + + showPreview: (form) -> + preview = form.find('.js-md-preview') + mdText = form.find('textarea.markdown-area').val() + + if mdText.trim().length == 0 + preview.text('Nothing to preview.') + @hideReferencedUsers(form) + else + preview.text('Loading...') + @renderMarkdown mdText, (response) => + preview.html(response.body) + preview.syntaxHighlight() + @renderReferencedUsers(response.references.users, form) + + renderMarkdown: (text, success) -> + return unless window.markdown_preview_path + + $.ajax + type: 'POST' + url: window.markdown_preview_path + data: { text: text } + dataType: 'json' + success: success + + hideReferencedUsers: (form) -> + referencedUsers = form.find('.referenced-users') + referencedUsers.hide() + + renderReferencedUsers: (users, form) -> + referencedUsers = form.find('.referenced-users') + + if referencedUsers.length + if users.length >= @referenceThreshold + referencedUsers.show() + referencedUsers.find('.js-referenced-users-count').text(users.length) + else + referencedUsers.hide() + +markdownPreview = new MarkdownPreview() + +previewButtonSelector = '.js-md-preview-button' +writeButtonSelector = '.js-md-write-button' + +$.fn.setupMarkdownPreview = -> + $form = $(this) + + form_textarea = $form.find('textarea.markdown-area') + + form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form) + form_textarea.on 'blur', -> markdownPreview.showPreview($form) + +$(document).on 'click', previewButtonSelector, (e) -> + e.preventDefault() + + $form = $(this).closest('form') + + # toggle tabs + $form.find(writeButtonSelector).parent().removeClass('active') + $form.find(previewButtonSelector).parent().addClass('active') + + # toggle content + $form.find('.md-write-holder').hide() + $form.find('.md-preview-holder').show() + + markdownPreview.showPreview($form) + +$(document).on 'click', writeButtonSelector, (e) -> + e.preventDefault() + + $form = $(this).closest('form') + + # toggle tabs + $form.find(writeButtonSelector).parent().addClass('active') + $form.find(previewButtonSelector).parent().removeClass('active') + + # toggle content + $form.find('.md-write-holder').show() + $form.find('.md-preview-holder').hide() diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 7b21095ea3..8218cf1120 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -5,7 +5,7 @@ %a.js-md-write-button(href="#md-write-holder" tabindex="-1") Write %li - %a.js-md-preview-button(href="md-preview-holder" tabindex="-1") + %a.js-md-preview-button(href="#md-preview-holder" tabindex="-1") Preview - if defined?(referenced_users) && referenced_users From 36bde0fcb197aa7ca93bb615cda070f55f5e268f Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 25 Nov 2015 14:00:35 -0800 Subject: [PATCH 098/908] Fix Error 500 when viewing user's personal projects from admin page This is a regression introduced in 4d7f00f. Closes #3680 --- CHANGELOG | 1 + app/views/admin/users/_projects.html.haml | 13 +++++++++++++ app/views/admin/users/projects.html.haml | 2 +- 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 app/views/admin/users/_projects.html.haml diff --git a/CHANGELOG b/CHANGELOG index 8916b9f040..6c78cc70b7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) + - Fix Error 500 when viewing user's personal projects from admin page (Stan Hu) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - Fix 500 error when update group member permission diff --git a/app/views/admin/users/_projects.html.haml b/app/views/admin/users/_projects.html.haml new file mode 100644 index 0000000000..a126a858ea --- /dev/null +++ b/app/views/admin/users/_projects.html.haml @@ -0,0 +1,13 @@ +- if local_assigns.has_key?(:contributed_projects) && contributed_projects.present? + .panel.panel-default.contributed-projects + .panel-heading Projects contributed to + = render 'shared/projects/list', + projects: contributed_projects.sort_by(&:star_count).reverse, + projects_limit: 5, stars: true, avatar: false + +- if local_assigns.has_key?(:projects) && projects.present? + .panel.panel-default + .panel-heading Personal projects + = render 'shared/projects/list', + projects: projects.sort_by(&:star_count).reverse, + projects_limit: 10, stars: true, avatar: false diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml index 0d7a1a25a8..b655b2a15f 100644 --- a/app/views/admin/users/projects.html.haml +++ b/app/views/admin/users/projects.html.haml @@ -14,7 +14,7 @@ .row .col-md-6 - if @personal_projects.present? - = render 'users/projects', projects: @personal_projects + = render 'admin/users/projects', projects: @personal_projects - else .nothing-here-block This user has no personal projects. From 59cc61e246dd059d041793cf4fd29ee540c7d7d2 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 25 Nov 2015 17:01:02 -0500 Subject: [PATCH 099/908] Bump doorkeeper to ~> 2.2.0 Closes #2746 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 4762e2e223..491a6acd43 100644 --- a/Gemfile +++ b/Gemfile @@ -16,7 +16,7 @@ gem "pg", '~> 0.18.2', group: :postgres # Authentication libraries gem 'devise', '~> 3.5.2' gem 'devise-async', '~> 0.9.0' -gem 'doorkeeper', '~> 2.1.3' +gem 'doorkeeper', '~> 2.2.0' gem 'omniauth', '~> 1.2.2' gem 'omniauth-bitbucket', '~> 0.0.2' gem 'omniauth-facebook', '~> 3.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index 46247dd88e..3cb986b39d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -164,7 +164,7 @@ GEM docile (1.1.5) domain_name (0.5.24) unf (>= 0.0.5, < 1.0.0) - doorkeeper (2.1.4) + doorkeeper (2.2.2) railties (>= 3.2) dropzonejs-rails (0.7.1) rails (> 3.1) @@ -824,7 +824,7 @@ DEPENDENCIES devise-async (~> 0.9.0) devise-two-factor (~> 2.0.0) diffy (~> 3.0.3) - doorkeeper (~> 2.1.3) + doorkeeper (~> 2.2.0) dropzonejs-rails (~> 0.7.1) email_reply_parser (~> 0.5.8) email_spec (~> 1.6.0) From 91c0aa5e982ee5e3299e6b8c899bfc396fe51279 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 25 Nov 2015 17:03:30 -0500 Subject: [PATCH 100/908] Bump asana to ~> 0.4.0 Closes #2830 --- Gemfile | 2 +- Gemfile.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index 4762e2e223..8325c4d8e6 100644 --- a/Gemfile +++ b/Gemfile @@ -153,7 +153,7 @@ gem "gemnasium-gitlab-service", "~> 0.2" gem "slack-notifier", "~> 1.2.0" # Asana integration -gem 'asana', '~> 0.0.6' +gem 'asana', '~> 0.4.0' # FogBugz integration gem 'ruby-fogbugz', '~> 0.2.1' diff --git a/Gemfile.lock b/Gemfile.lock index 46247dd88e..24aadef5c7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -29,10 +29,6 @@ GEM actionpack (>= 4.0.0, < 5) activerecord (>= 4.0.0, < 5) railties (>= 4.0.0, < 5) - activeresource (4.0.0) - activemodel (~> 4.0) - activesupport (~> 4.0) - rails-observers (~> 0.1.1) activesupport (4.1.14) i18n (~> 0.6, >= 0.6.9) json (~> 1.7, >= 1.7.7) @@ -48,8 +44,11 @@ GEM activerecord (>= 3.2, <= 4.3) rake (~> 10.4) arel (5.0.1.20140414130214) - asana (0.0.6) - activeresource (>= 3.2.3) + asana (0.4.0) + faraday (~> 0.9) + faraday_middleware (~> 0.9) + faraday_middleware-multi_json (~> 0.0) + oauth2 (~> 1.0) asciidoctor (1.5.2) ast (2.1.0) astrolabe (1.3.1) @@ -191,6 +190,9 @@ GEM multipart-post (>= 1.2, < 3) faraday_middleware (0.10.0) faraday (>= 0.7.4, < 0.10) + faraday_middleware-multi_json (0.0.6) + faraday_middleware + multi_json fastercsv (1.5.5) ffaker (2.0.0) ffi (1.9.10) @@ -523,8 +525,6 @@ GEM bundler (>= 1.3.0, < 2.0) railties (= 4.1.14) sprockets-rails (~> 2.0) - rails-observers (0.1.2) - activemodel (~> 4.0) railties (4.1.14) actionpack (= 4.1.14) activesupport (= 4.1.14) @@ -795,7 +795,7 @@ DEPENDENCIES addressable (~> 2.3.8) after_commit_queue annotate (~> 2.6.0) - asana (~> 0.0.6) + asana (~> 0.4.0) asciidoctor (~> 1.5.2) attr_encrypted (~> 1.3.4) awesome_print (~> 1.2.0) From 5f0d1f30b0f0e31d31a2eb0db9f92776bf551f26 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 25 Nov 2015 17:06:04 -0500 Subject: [PATCH 101/908] Remove enumerize gem --- Gemfile | 3 --- Gemfile.lock | 3 --- app/models/project.rb | 3 +-- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 4762e2e223..1beeabd44d 100644 --- a/Gemfile +++ b/Gemfile @@ -62,9 +62,6 @@ gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' # based on human-friendly examples gem "stamp", '~> 0.6.0' -# Enumeration fields -gem 'enumerize', '~> 0.7.0' - # Pagination gem "kaminari", "~> 0.16.3" diff --git a/Gemfile.lock b/Gemfile.lock index 46247dd88e..ef083754f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -173,8 +173,6 @@ GEM launchy (~> 2.1) mail (~> 2.2) encryptor (1.3.0) - enumerize (0.7.0) - activesupport (>= 3.2) equalizer (0.0.11) erubis (2.7.0) escape_utils (1.1.0) @@ -828,7 +826,6 @@ DEPENDENCIES dropzonejs-rails (~> 0.7.1) email_reply_parser (~> 0.5.8) email_spec (~> 1.6.0) - enumerize (~> 0.7.0) factory_girl_rails (~> 4.3.0) ffaker (~> 2.0.0) flay diff --git a/app/models/project.rb b/app/models/project.rb index f0a4b6aae7..6010770a5f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -42,9 +42,8 @@ class Project < ActiveRecord::Base include Sortable include AfterCommitQueue include CaseSensitivity - + extend Gitlab::ConfigHelper - extend Enumerize UNKNOWN_IMPORT_URL = 'http://unknown.git' From 7adfb30c349396f19b71a703419a475bd1d4cd5b Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 25 Nov 2015 17:06:36 -0800 Subject: [PATCH 102/908] Remove duplicate entry shipped in 8.1.4 --- CHANGELOG | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 8916b9f040..1838198417 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,7 +14,6 @@ v 8.2.0 - Expose build artifacts path as config option - Fix grouping of contributors by email in graph. - Improved performance of finding issues with/without labels - - Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu) - Fix Drone CI service template not saving properly (Stan Hu) - Fix avatars not showing in Atom feeds and project issues when Gravatar disabled (Stan Hu) - Added a GitLab specific profiling tool called "Sherlock" (see GitLab CE merge request #1749) From 2497d3d550dc0d4e095c8c3fe75d4452fb163252 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 25 Nov 2015 23:11:35 -0800 Subject: [PATCH 103/908] Fix 404 in redirection after removing a project Closes https://github.com/gitlabhq/gitlabhq/issues/9844 Closes #3559 --- CHANGELOG | 1 + app/controllers/projects_controller.rb | 2 +- spec/controllers/projects_controller_spec.rb | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 8916b9f040..3dd96e34dc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) + - Fix 404 in redirection after removing a project (Stan Hu) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - Fix 500 error when update group member permission diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 23453195e8..10c75370d7 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -123,7 +123,7 @@ class ProjectsController < ApplicationController ::Projects::DestroyService.new(@project, current_user, {}).execute flash[:alert] = "Project '#{@project.name}' was deleted." - redirect_back_or_default(default: dashboard_projects_path, options: {}) + redirect_to dashboard_projects_path rescue Projects::DestroyService::DestroyError => ex redirect_to edit_project_path(@project), alert: ex.message end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 4bb47c6b02..665526fde9 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -88,6 +88,22 @@ describe ProjectsController do end end + describe "#destroy" do + let(:admin) { create(:admin) } + + it "redirects to the dashboard" do + controller.instance_variable_set(:@project, project) + sign_in(admin) + + orig_id = project.id + delete :destroy, namespace_id: project.namespace.path, id: project.path + + expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound) + expect(response.status).to eq(302) + expect(response).to redirect_to(dashboard_projects_path) + end + end + describe "POST #toggle_star" do it "toggles star if user is signed in" do sign_in(user) From 38489d8d7d0a352789c23d3bf9cadb169f765d5b Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 26 Nov 2015 11:57:04 +0200 Subject: [PATCH 104/908] update test_after_commit gem to 0.4.2 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 4a9f70a129..dc801d60b2 100644 --- a/Gemfile +++ b/Gemfile @@ -274,7 +274,7 @@ group :test do gem 'shoulda-matchers', '~> 2.8.0', require: false gem 'email_spec', '~> 1.6.0' gem 'webmock', '~> 1.21.0' - gem 'test_after_commit', '~> 0.2.2' + gem 'test_after_commit', '~> 0.4.2' gem 'sham_rack' end diff --git a/Gemfile.lock b/Gemfile.lock index e946051505..bb4f94aeef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -750,7 +750,7 @@ GEM term-ansicolor (1.3.2) tins (~> 1.0) terminal-table (1.5.2) - test_after_commit (0.2.7) + test_after_commit (0.4.2) activerecord (>= 3.2) thin (1.6.4) daemons (~> 1.0, >= 1.0.9) @@ -970,7 +970,7 @@ DEPENDENCIES task_list (~> 1.0.2) teaspoon (~> 1.0.0) teaspoon-jasmine (~> 2.2.0) - test_after_commit (~> 0.2.2) + test_after_commit (~> 0.4.2) thin (~> 1.6.1) tinder (~> 1.10.0) turbolinks (~> 2.5.0) From 7f214cee74796ceaf7b01bd6e133d4d54c5123db Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 26 Nov 2015 15:48:01 +0200 Subject: [PATCH 105/908] Migrate mailers to ActiveJob --- Gemfile | 1 + Gemfile.lock | 1 + Procfile | 2 +- app/controllers/abuse_reports_controller.rb | 2 +- app/mailers/base_mailer.rb | 4 - .../project_services/ci/mail_service.rb | 2 +- app/services/notification_service.rb | 74 +++++++++++++------ app/workers/email_receiver_worker.rb | 2 +- config/application.rb | 4 + config/environments/production.rb | 2 +- config/environments/test.rb | 2 +- config/initializers/static_files.rb | 2 +- lib/gitlab/markdown/label_reference_filter.rb | 3 +- lib/gitlab/seeder.rb | 2 +- 14 files changed, 67 insertions(+), 36 deletions(-) diff --git a/Gemfile b/Gemfile index 808c5df7ca..91c909a2e7 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,7 @@ source "https://rubygems.org" gem 'rails', '4.2.4' +gem 'rails-deprecated_sanitizer', '~> 1.0.3' # Responders respond_to and respond_with gem 'responders', '~> 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 1671edbc6f..0800137991 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -930,6 +930,7 @@ DEPENDENCIES rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) rails (= 4.2.4) + rails-deprecated_sanitizer (~> 1.0.3) raphael-rails (~> 2.1.2) rblineprof rdoc (~> 3.6) diff --git a/Procfile b/Procfile index 08880b9c42..fd5f7ecb94 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"} -worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default +worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q mailers -q default # mail_room: bundle exec mail_room -q -c config/mail_room.yml diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb index 2f4054eaa1..d8e9059433 100644 --- a/app/controllers/abuse_reports_controller.rb +++ b/app/controllers/abuse_reports_controller.rb @@ -10,7 +10,7 @@ class AbuseReportsController < ApplicationController if @abuse_report.save if current_application_settings.admin_notification_email.present? - AbuseReportMailer.delay.notify(@abuse_report.id) + AbuseReportMailer.deliver_later.notify(@abuse_report.id) end message = "Thank you for your report. A GitLab administrator will look into it shortly." diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index aedb088918..8b83bbd93b 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -8,10 +8,6 @@ class BaseMailer < ActionMailer::Base default from: Proc.new { default_sender_address.format } default reply_to: Proc.new { default_reply_to_address.format } - def self.delay - delay_for(2.seconds) - end - def can? Ability.abilities.allowed?(current_user, action, subject) end diff --git a/app/models/project_services/ci/mail_service.rb b/app/models/project_services/ci/mail_service.rb index d31dd6899c..bdc85667e9 100644 --- a/app/models/project_services/ci/mail_service.rb +++ b/app/models/project_services/ci/mail_service.rb @@ -78,7 +78,7 @@ module Ci end def mailer - Ci::Notify.delay + Ci::Notify.deliver_later end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index d6550fbb55..03fe8c8fe1 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -13,14 +13,14 @@ class NotificationService # even if user disabled notifications def new_key(key) if key.user - mailer.new_ssh_key_email(key.id) + mailer.new_ssh_key_email(key.id).deliver_later end end # Always notify user about email added to profile def new_email(email) if email.user - mailer.new_email_email(email.id) + mailer.new_email_email(email.id).deliver_later end end @@ -79,17 +79,27 @@ class NotificationService end def merge_mr(merge_request, current_user) - close_resource_email(merge_request, merge_request.target_project, current_user, 'merged_merge_request_email') + close_resource_email( + merge_request, + merge_request.target_project, + current_user, + 'merged_merge_request_email' + ) end def reopen_mr(merge_request, current_user) - reopen_resource_email(merge_request, merge_request.target_project, current_user, 'merge_request_status_email', 'reopened') + reopen_resource_email( + merge_request, + merge_request.target_project, + current_user, 'merge_request_status_email', + 'reopened' + ) end # Notify new user with email after creation def new_user(user, token = nil) # Don't email omniauth created users - mailer.new_user_email(user.id, token) unless user.identities.any? + mailer.new_user_email(user.id, token).deliver_later unless user.identities.any? end # Notify users on new note in system @@ -140,48 +150,58 @@ class NotificationService notify_method = "note_#{note.noteable_type.underscore}_email".to_sym recipients.each do |recipient| - mailer.send(notify_method, recipient.id, note.id) + mailer.send(notify_method, recipient.id, note.id).deliver_later end end def invite_project_member(project_member, token) - mailer.project_member_invited_email(project_member.id, token) + mailer.project_member_invited_email(project_member.id, token).deliver_later end def accept_project_invite(project_member) - mailer.project_invite_accepted_email(project_member.id) + mailer.project_invite_accepted_email(project_member.id).deliver_later end def decline_project_invite(project_member) - mailer.project_invite_declined_email(project_member.project.id, project_member.invite_email, project_member.access_level, project_member.created_by_id) + mailer.project_invite_declined_email( + project_member.project.id, + project_member.invite_email, + project_member.access_level, + project_member.created_by_id + ).deliver_later end def new_project_member(project_member) - mailer.project_access_granted_email(project_member.id) + mailer.project_access_granted_email(project_member.id).deliver_later end def update_project_member(project_member) - mailer.project_access_granted_email(project_member.id) + mailer.project_access_granted_email(project_member.id).deliver_later end def invite_group_member(group_member, token) - mailer.group_member_invited_email(group_member.id, token) + mailer.group_member_invited_email(group_member.id, token).deliver_later end def accept_group_invite(group_member) - mailer.group_invite_accepted_email(group_member.id) + mailer.group_invite_accepted_email(group_member.id).deliver_later end def decline_group_invite(group_member) - mailer.group_invite_declined_email(group_member.group.id, group_member.invite_email, group_member.access_level, group_member.created_by_id) + mailer.group_invite_declined_email( + group_member.group.id, + group_member.invite_email, + group_member.access_level, + group_member.created_by_id + ).deliver_later end def new_group_member(group_member) - mailer.group_access_granted_email(group_member.id) + mailer.group_access_granted_email(group_member.id).deliver_later end def update_group_member(group_member) - mailer.group_access_granted_email(group_member.id) + mailer.group_access_granted_email(group_member.id).deliver_later end def project_was_moved(project, old_path_with_namespace) @@ -189,7 +209,11 @@ class NotificationService recipients = reject_muted_users(recipients, project) recipients.each do |recipient| - mailer.project_was_moved_email(project.id, recipient.id, old_path_with_namespace) + mailer.project_was_moved_email( + project.id, + recipient.id, + old_path_with_namespace + ).deliver_later end end @@ -339,7 +363,7 @@ class NotificationService recipients = build_recipients(target, project, target.author) recipients.each do |recipient| - mailer.send(method, recipient.id, target.id) + mailer.send(method, recipient.id, target.id).deliver_later end end @@ -347,7 +371,7 @@ class NotificationService recipients = build_recipients(target, project, current_user) recipients.each do |recipient| - mailer.send(method, recipient.id, target.id, current_user.id) + mailer.send(method, recipient.id, target.id, current_user.id).deliver end end @@ -358,7 +382,13 @@ class NotificationService recipients = build_recipients(target, project, current_user, [previous_assignee]) recipients.each do |recipient| - mailer.send(method, recipient.id, target.id, previous_assignee_id, current_user.id) + mailer.send( + method, + recipient.id, + target.id, + previous_assignee_id, + current_user.id + ).deliver_later end end @@ -366,7 +396,7 @@ class NotificationService recipients = build_recipients(target, project, current_user) recipients.each do |recipient| - mailer.send(method, recipient.id, target.id, status, current_user.id) + mailer.send(method, recipient.id, target.id, status, current_user.id).deliver end end @@ -388,7 +418,7 @@ class NotificationService end def mailer - Notify.delay + Notify end def previous_record(object, attribute) diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb index 5a921a73fe..1df8de1db7 100644 --- a/app/workers/email_receiver_worker.rb +++ b/app/workers/email_receiver_worker.rb @@ -46,6 +46,6 @@ class EmailReceiverWorker return end - EmailRejectionMailer.delay.rejection(reason, raw, can_retry) + EmailRejectionMailer.deliver_later.rejection(reason, raw, can_retry) end end diff --git a/config/application.rb b/config/application.rb index bfa2a809dd..d255ff0719 100644 --- a/config/application.rb +++ b/config/application.rb @@ -99,6 +99,10 @@ module Gitlab redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever config.cache_store = :redis_store, redis_config_hash + config.active_record.raise_in_transactional_callbacks = true + + config.active_job.queue_adapter = :sidekiq + # This is needed for gitlab-shell ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH'] end diff --git a/config/environments/production.rb b/config/environments/production.rb index e8250d6645..317b113e10 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -9,7 +9,7 @@ Rails.application.configure do config.action_controller.perform_caching = true # Disable Rails's static asset server (Apache or nginx will already do this) - config.serve_static_assets = false + config.serve_static_files = false # Compress JavaScripts and CSS. config.assets.js_compressor = :uglifier diff --git a/config/environments/test.rb b/config/environments/test.rb index 46982be286..2eddf0605d 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -8,7 +8,7 @@ Rails.application.configure do config.cache_classes = false # Configure static asset server for tests with Cache-Control for performance - config.serve_static_assets = true + config.serve_static_files = true config.static_cache_control = "public, max-age=3600" # Show full error reports and disable caching diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index d9042c652b..d6dbf8b9fb 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -1,6 +1,6 @@ app = Rails.application -if app.config.serve_static_assets +if app.config.serve_static_files # The `ActionDispatch::Static` middleware intercepts requests for static files # by checking if they exist in the `/public` directory. # We're replacing it with our `Gitlab::Middleware::Static` that does the same, diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb index 618acb7a57..13581b8fb1 100644 --- a/lib/gitlab/markdown/label_reference_filter.rb +++ b/lib/gitlab/markdown/label_reference_filter.rb @@ -60,8 +60,7 @@ module Gitlab def url_for_label(project, label) h = Gitlab::Application.routes.url_helpers h.namespace_project_issues_path(project.namespace, project, - label_name: label.name, - only_path: context[:only_path]) + label_name: label.name) end def render_colored_label(label) diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb index 31aa3528c4..2ef0e98225 100644 --- a/lib/gitlab/seeder.rb +++ b/lib/gitlab/seeder.rb @@ -14,7 +14,7 @@ module Gitlab def self.mute_mailer code = <<-eos -def Notify.delay +def Notify.deliver_later self end eos From b9df1a63550c78396d43b661bd24d2745604f6fc Mon Sep 17 00:00:00 2001 From: Jose Corcuera Date: Thu, 26 Nov 2015 10:16:50 -0500 Subject: [PATCH 106/908] Strip attributes for Milestone and Issuable. #3428 --- CHANGELOG | 1 + app/controllers/projects/issues_controller.rb | 4 +-- .../projects/merge_requests_controller.rb | 4 +-- app/models/concerns/issuable.rb | 2 ++ app/models/concerns/strip_attribute.rb | 34 +++++++++++++++++++ app/models/milestone.rb | 3 ++ spec/models/concerns/strip_attribute_spec.rb | 20 +++++++++++ 7 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 app/models/concerns/strip_attribute.rb create mode 100644 spec/models/concerns/strip_attribute_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 0e1e1a3671..39a57231d9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ v 8.3.0 (unreleased) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - Fix 500 error when update group member permission - Fix: Raw private snippets access workflow + - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) v 8.2.1 - Forcefully update builds that didn't want to update with state machine diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 5250a0f5e6..ae474cf8d6 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -158,12 +158,10 @@ class Projects::IssuesController < Projects::ApplicationController end def issue_params - permitted = params.require(:issue).permit( + 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 6378a1f56b..3f47f2ddb2 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -276,13 +276,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def merge_request_params - permitted = params.require(:merge_request).permit( + 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/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 68138688aa..badeadfa41 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -8,6 +8,7 @@ module Issuable extend ActiveSupport::Concern include Participable include Mentionable + include StripAttribute included do belongs_to :author, class_name: "User" @@ -51,6 +52,7 @@ module Issuable attr_mentionable :title, :description participant :author, :assignee, :notes_with_associations + strip_attributes :title end module ClassMethods diff --git a/app/models/concerns/strip_attribute.rb b/app/models/concerns/strip_attribute.rb new file mode 100644 index 0000000000..8806ebe897 --- /dev/null +++ b/app/models/concerns/strip_attribute.rb @@ -0,0 +1,34 @@ +# == Strip Attribute module +# +# Contains functionality to clean attributes before validation +# +# Usage: +# +# class Milestone < ActiveRecord::Base +# strip_attributes :title +# end +# +# +module StripAttribute + extend ActiveSupport::Concern + + module ClassMethods + def strip_attributes(*attrs) + strip_attrs.concat(attrs) + end + + def strip_attrs + @strip_attrs ||= [] + end + end + + included do + before_validation :strip_attributes + end + + def strip_attributes + self.class.strip_attrs.each do |attr| + self[attr].strip! if self[attr] && self[attr].respond_to?(:strip!) + end + end +end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 2ff16e2825..c2642b75b8 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -22,6 +22,7 @@ class Milestone < ActiveRecord::Base include InternalId include Sortable + include StripAttribute belongs_to :project has_many :issues @@ -35,6 +36,8 @@ class Milestone < ActiveRecord::Base validates :title, presence: true validates :project, presence: true + strip_attributes :title + state_machine :state, initial: :active do event :close do transition active: :closed diff --git a/spec/models/concerns/strip_attribute_spec.rb b/spec/models/concerns/strip_attribute_spec.rb new file mode 100644 index 0000000000..6445e29c3e --- /dev/null +++ b/spec/models/concerns/strip_attribute_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Milestone, "StripAttribute" do + let(:milestone) { create(:milestone) } + + describe ".strip_attributes" do + it { expect(Milestone).to respond_to(:strip_attributes) } + it { expect(Milestone.strip_attrs).to include(:title) } + end + + describe "#strip_attributes" do + before do + milestone.title = ' 8.3 ' + milestone.valid? + end + + it { expect(milestone.title).to eq('8.3') } + end + +end From 78c1ab40e20f2c412719a2140d9de61ada26d1b8 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 26 Nov 2015 07:55:21 -0800 Subject: [PATCH 107/908] Gracefully handle when Redis is not available --- config/initializers/1_settings.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 80b480eac3..444e91109d 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -295,5 +295,10 @@ if Rails.env.test? end # Force a refresh of application settings at startup -ApplicationSetting.expire -Ci::ApplicationSetting.expire +begin + ApplicationSetting.expire + Ci::ApplicationSetting.expire +rescue + # Gracefully handle when Redis is not available. For example, + # omnibus may fail here during assets:precompile. +end From 295d378e9ab1a8e052e35c6e6fedf8d6890b74d0 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 27 Nov 2015 13:56:26 +0100 Subject: [PATCH 108/908] Repeat "client_max_body_size 0" everywhere It turns out that if we do not the declaration from "location /" wins. --- lib/support/nginx/gitlab | 6 ++++++ lib/support/nginx/gitlab-ssl | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index 0cf5292b29..1dfcac91cd 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -114,24 +114,28 @@ server { } location ~ ^/[\w\.-]+/[\w\.-]+/gitlab-lfs/objects { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; } location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; } location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; } location ~ ^/api/v3/projects/.*/repository/archive { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; @@ -139,6 +143,7 @@ server { # Build artifacts should be submitted to this location location ~ ^/[\w\.-]+/[\w\.-]+/builds/download { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; @@ -146,6 +151,7 @@ server { # Build artifacts should be submitted to this location location ~ /ci/api/v1/builds/[0-9]+/artifacts { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 31a651c87f..7cd32de073 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -161,24 +161,28 @@ server { } location ~ ^/[\w\.-]+/[\w\.-]+/gitlab-lfs/objects { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; } location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; } location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; } location ~ ^/api/v3/projects/.*/repository/archive { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; @@ -186,6 +190,7 @@ server { # Build artifacts should be submitted to this location location ~ ^/[\w\.-]+/[\w\.-]+/builds/download { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; @@ -193,6 +198,7 @@ server { # Build artifacts should be submitted to this location location ~ /ci/api/v1/builds/[0-9]+/artifacts { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; From 04049b6b17c9354555115b5d2f049daffa311c16 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 27 Nov 2015 13:57:53 +0100 Subject: [PATCH 109/908] Fix indentation in NGINX config --- lib/support/nginx/gitlab-ssl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 7cd32de073..016f7a536f 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -191,17 +191,17 @@ server { # Build artifacts should be submitted to this location location ~ ^/[\w\.-]+/[\w\.-]+/builds/download { client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; + return 418; } # Build artifacts should be submitted to this location location ~ /ci/api/v1/builds/[0-9]+/artifacts { client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; + return 418; } location @gitlab-workhorse { From f1710073b46c940245d38b46e5436c0745223aee Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 27 Nov 2015 14:39:55 -0500 Subject: [PATCH 110/908] Fix alignment [ci skip] --- lib/support/nginx/gitlab | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index 1dfcac91cd..2a79fbdcf9 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -144,17 +144,17 @@ server { # Build artifacts should be submitted to this location location ~ ^/[\w\.-]+/[\w\.-]+/builds/download { client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; + return 418; } # Build artifacts should be submitted to this location location ~ /ci/api/v1/builds/[0-9]+/artifacts { client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; + return 418; } location @gitlab-workhorse { From 3723182a8831595502570c90211610abb4770781 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 27 Nov 2015 16:15:06 -0500 Subject: [PATCH 111/908] Move project visibility level off of the clone panel --- app/assets/stylesheets/pages/projects.scss | 5 +++++ app/views/projects/_home_panel.html.haml | 10 ++++++---- app/views/shared/_clone_panel.html.haml | 4 ---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index ad607ead2b..4a0fe54684 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -90,7 +90,12 @@ } .visibility-level-label { + @extend .btn; + @extend .btn-gray; + color: $gray; + cursor: auto; + i { color: inherit; } diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 88d54bf6f2..b30036966a 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,5 +1,5 @@ - empty_repo = @project.empty_repo? -.project-home-panel.clearfix{:class => ("empty-project" if empty_repo)} +.project-home-panel.cover-block.clearfix{:class => ("empty-project" if empty_repo)} .project-identicon-holder = project_icon(@project, alt: '', class: 'project-avatar avatar s90') .project-home-desc @@ -12,8 +12,10 @@ Forked from = link_to project_path(forked_from_project) do = forked_from_project.namespace.try(:name) - - + .cover-controls + .visibility-level-label + = visibility_level_icon(@project.visibility_level) + = visibility_level_label(@project.visibility_level) .project-repo-buttons .split-one @@ -21,7 +23,7 @@ = render 'projects/buttons/fork' = render "shared/clone_panel" - + .split-repo-buttons = render "projects/buttons/download" = render 'projects/buttons/dropdown' diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 408a46aaaf..edb5778f42 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -8,7 +8,3 @@ = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true .input-group-btn = clipboard_button(clipboard_target: '#project_clone') - - if project.kind_of?(Project) - .input-group-addon.has_tooltip{title: "#{visibility_level_label(project.visibility_level)} project", data: { container: "body" } } - .visibility-level-label - = visibility_level_icon(project.visibility_level) From 344cb89718d9424e239a24dbc888d7b7a98cb474 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 27 Nov 2015 16:34:59 -0500 Subject: [PATCH 112/908] Bump jquery-turbolinks to ~> 2.1.0 See #2857 --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 685b126a79..51ce6c9ed7 100644 --- a/Gemfile +++ b/Gemfile @@ -183,7 +183,7 @@ gem "sass-rails", '~> 4.0.5' gem "coffee-rails", '~> 4.1.0' gem "uglifier", '~> 2.7.2' gem 'turbolinks', '~> 2.5.0' -gem 'jquery-turbolinks', '~> 2.0.1' +gem 'jquery-turbolinks', '~> 2.1.0' gem 'addressable', '~> 2.3.8' gem 'bootstrap-sass', '~> 3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 8fb2ef778e..54a63ea22d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -121,7 +121,7 @@ GEM coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.9.1.1) + coffee-script-source (1.10.0) colorize (0.7.7) connection_pool (2.2.0) coveralls (0.8.2) @@ -370,7 +370,7 @@ GEM thor (>= 0.14, < 2.0) jquery-scrollto-rails (1.4.3) railties (> 3.1, < 5.0) - jquery-turbolinks (2.0.2) + jquery-turbolinks (2.1.0) railties (>= 3.1.0) turbolinks jquery-ui-rails (4.2.1) @@ -853,7 +853,7 @@ DEPENDENCIES jquery-atwho-rails (~> 1.3.2) jquery-rails (~> 3.1.3) jquery-scrollto-rails (~> 1.4.3) - jquery-turbolinks (~> 2.0.1) + jquery-turbolinks (~> 2.1.0) jquery-ui-rails (~> 4.2.1) kaminari (~> 0.16.3) letter_opener (~> 1.1.2) From 85ad2140fa4ef2209b9d217403e183e33ce4dd37 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Sat, 28 Nov 2015 20:53:17 +0100 Subject: [PATCH 113/908] Remove reverence to satellites [ci skip] --- doc/raketasks/backup_restore.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 4e645b21a8..b4d2786bd7 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -274,9 +274,6 @@ sudo gitlab-rake gitlab:backup:restore BACKUP=1393513186 # Start GitLab sudo gitlab-ctl start -# Create satellites -sudo gitlab-rake gitlab:satellites:create - # Check GitLab sudo gitlab-rake gitlab:check SANITIZE=true ``` From 7d6323a9d4ada6be6c00f7d160b366219952f77f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 29 Nov 2015 11:51:02 +0200 Subject: [PATCH 114/908] Update phantomjs to 2.0.0 * Version 1.9 was removed from upstream Debian repos --- scripts/prepare_build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index dca5e1c5db..5ff4d7fa76 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -1,7 +1,7 @@ #!/bin/bash if [ -f /.dockerinit ]; then - wget -q http://ftp.de.debian.org/debian/pool/main/p/phantomjs/phantomjs_1.9.0-1+b1_amd64.deb - dpkg -i phantomjs_1.9.0-1+b1_amd64.deb + wget -q http://ftp.de.debian.org/debian/pool/main/p/phantomjs/phantomjs_2.0.0+dfsg-1_amd64.deb + dpkg -i phantomjs_2.0.0+dfsg-1_amd64.deb apt-get update -qq apt-get install -y -qq libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client From bb6ce2bb10734cfc45e488378acaa17000fdacc2 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 29 Nov 2015 13:44:19 +0200 Subject: [PATCH 115/908] Test using a 1.9.8 phantomjs version built with fpm https://gitlab.com/axil/phantomjs-debian --- scripts/prepare_build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 5ff4d7fa76..119cc90fc1 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -1,7 +1,7 @@ #!/bin/bash if [ -f /.dockerinit ]; then - wget -q http://ftp.de.debian.org/debian/pool/main/p/phantomjs/phantomjs_2.0.0+dfsg-1_amd64.deb - dpkg -i phantomjs_2.0.0+dfsg-1_amd64.deb + wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb + dpkg -i phantomjs_1.9.8-0jessie_amd64.deb apt-get update -qq apt-get install -y -qq libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client From e1522ec85582962c12f875a46a5853d6ae570d0b Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 29 Nov 2015 17:52:40 -0500 Subject: [PATCH 116/908] Simplify `build_gitlab_shell_ssh_path_prefix` --- config/initializers/1_settings.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index b162b8a83f..abe08d5718 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -33,13 +33,15 @@ class Settings < Settingslogic end def build_gitlab_shell_ssh_path_prefix + user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}" + if gitlab_shell.ssh_port != 22 - "ssh://#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}:#{gitlab_shell.ssh_port}/" + "ssh://#{user_host}:#{gitlab_shell.ssh_port}/" else if gitlab_shell.ssh_host.include? ':' - "[#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}]:" + "[#{user_host}]:" else - "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}:" + "#{user_host}:" end end end From 461731f0769a826d00c4d5846ff6d2f55fd4b829 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 30 Nov 2015 11:21:10 +0200 Subject: [PATCH 117/908] fix notification_service specs --- app/services/notification_service.rb | 5 +- config/environments/test.rb | 2 + spec/services/notification_service_spec.rb | 345 +++++++++------------ spec/spec_helper.rb | 1 + 4 files changed, 144 insertions(+), 209 deletions(-) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 03fe8c8fe1..388a4defb2 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -148,7 +148,6 @@ class NotificationService # build notify method like 'note_commit_email' notify_method = "note_#{note.noteable_type.underscore}_email".to_sym - recipients.each do |recipient| mailer.send(notify_method, recipient.id, note.id).deliver_later end @@ -371,7 +370,7 @@ class NotificationService recipients = build_recipients(target, project, current_user) recipients.each do |recipient| - mailer.send(method, recipient.id, target.id, current_user.id).deliver + mailer.send(method, recipient.id, target.id, current_user.id).deliver_later end end @@ -396,7 +395,7 @@ class NotificationService recipients = build_recipients(target, project, current_user) recipients.each do |recipient| - mailer.send(method, recipient.id, target.id, status, current_user.id).deliver + mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later end end diff --git a/config/environments/test.rb b/config/environments/test.rb index 2eddf0605d..f96ac6f975 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -32,4 +32,6 @@ Rails.application.configure do config.eager_load = false config.cache_store = :null_store + + config.active_job.queue_adapter = :test end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 520140917a..a4e2b2953c 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -3,6 +3,12 @@ require 'spec_helper' describe NotificationService do let(:notification) { NotificationService.new } + around(:each) do |example| + perform_enqueued_jobs do + example.run + end + end + describe 'Keys' do describe :new_key do let!(:key) { create(:personal_key) } @@ -10,8 +16,7 @@ describe NotificationService do it { expect(notification.new_key(key)).to be_truthy } it 'should sent email to key owner' do - expect(Notify).to receive(:new_ssh_key_email).with(key.id) - notification.new_key(key) + expect{ notification.new_key(key) }.to change{ ActionMailer::Base.deliveries.size }.by(1) end end end @@ -23,8 +28,7 @@ describe NotificationService do it { expect(notification.new_email(email)).to be_truthy } it 'should send email to email owner' do - expect(Notify).to receive(:new_email_email).with(email.id) - notification.new_email(email) + expect{ notification.new_email(email) }.to change{ ActionMailer::Base.deliveries.size }.by(1) end end end @@ -47,18 +51,20 @@ describe NotificationService do it do add_users_with_subscription(note.project, issue) - should_email(@u_watcher.id) - should_email(note.noteable.author_id) - should_email(note.noteable.assignee_id) - should_email(@u_mentioned.id) - should_email(@subscriber.id) - should_not_email(note.author_id) - should_not_email(@u_participating.id) - should_not_email(@u_disabled.id) - should_not_email(@unsubscriber.id) - should_not_email(@u_outsider_mentioned) + ActionMailer::Base.deliveries.clear notification.new_note(note) + + should_email(@u_watcher) + should_email(note.noteable.author) + should_email(note.noteable.assignee) + should_email(@u_mentioned) + should_email(@subscriber) + should_not_email(note.author) + should_not_email(@u_participating) + should_not_email(@u_disabled) + should_not_email(@unsubscriber) + should_not_email(@u_outsider_mentioned) end it 'filters out "mentioned in" notes' do @@ -82,27 +88,21 @@ describe NotificationService do group_member = note.project.group.group_members.find_by_user_id(@u_watcher.id) group_member.notification_level = Notification::N_GLOBAL group_member.save + ActionMailer::Base.deliveries.clear end it do - should_email(note.noteable.author_id) - should_email(note.noteable.assignee_id) - should_email(@u_mentioned.id) - should_not_email(@u_watcher.id) - should_not_email(note.author_id) - should_not_email(@u_participating.id) - should_not_email(@u_disabled.id) notification.new_note(note) + + should_email(note.noteable.author) + should_email(note.noteable.assignee) + should_email(@u_mentioned) + should_not_email(@u_watcher) + should_not_email(note.author) + should_not_email(@u_participating) + should_not_email(@u_disabled) end end - - def should_email(user_id) - expect(Notify).to receive(:note_issue_email).with(user_id, note.id) - end - - def should_not_email(user_id) - expect(Notify).not_to receive(:note_issue_email).with(user_id, note.id) - end end context 'issue note mention' do @@ -113,24 +113,26 @@ describe NotificationService do before do build_team(note.project) + ActionMailer::Base.deliveries.clear end describe :new_note do it do + notification.new_note(note) + # Notify all team members note.project.team.members.each do |member| # User with disabled notification should not be notified next if member.id == @u_disabled.id - should_email(member.id) + should_email(member) end - should_email(note.noteable.author_id) - should_email(note.noteable.assignee_id) - should_not_email(note.author_id) - should_not_email(@u_mentioned.id) - should_not_email(@u_disabled.id) - should_not_email(@u_not_mentioned.id) - notification.new_note(note) + should_email(note.noteable.author) + should_email(note.noteable.assignee) + should_not_email(note.author) + should_email(@u_mentioned) + should_not_email(@u_disabled) + should_email(@u_not_mentioned) end it 'filters out "mentioned in" notes' do @@ -140,14 +142,6 @@ describe NotificationService do notification.new_note(mentioned_note) end end - - def should_email(user_id) - expect(Notify).to receive(:note_issue_email).with(user_id, note.id) - end - - def should_not_email(user_id) - expect(Notify).not_to receive(:note_issue_email).with(user_id, note.id) - end end context 'commit note' do @@ -156,43 +150,38 @@ describe NotificationService do before do build_team(note.project) + ActionMailer::Base.deliveries.clear allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer) end - describe :new_note do + describe :new_note, :perform_enqueued_jobs do it do - should_email(@u_committer.id, note) - should_email(@u_watcher.id, note) - should_not_email(@u_mentioned.id, note) - should_not_email(note.author_id, note) - should_not_email(@u_participating.id, note) - should_not_email(@u_disabled.id, note) notification.new_note(note) + + should_email(@u_committer) + should_email(@u_watcher) + should_not_email(@u_mentioned) + should_not_email(note.author) + should_not_email(@u_participating) + should_not_email(@u_disabled) end it do note.update_attribute(:note, '@mention referenced') - should_email(@u_committer.id, note) - should_email(@u_watcher.id, note) - should_email(@u_mentioned.id, note) - should_not_email(note.author_id, note) - should_not_email(@u_participating.id, note) - should_not_email(@u_disabled.id, note) notification.new_note(note) + + should_email(@u_committer) + should_email(@u_watcher) + should_email(@u_mentioned) + should_not_email(note.author) + should_not_email(@u_participating) + should_not_email(@u_disabled) end it do @u_committer.update_attributes(notification_level: Notification::N_MENTION) - should_not_email(@u_committer.id, note) notification.new_note(note) - end - - def should_email(user_id, n) - expect(Notify).to receive(:note_commit_email).with(user_id, n.id) - end - - def should_not_email(user_id, n) - expect(Notify).not_to receive(:note_commit_email).with(user_id, n.id) + should_not_email(@u_committer) end end end @@ -205,99 +194,69 @@ describe NotificationService do before do build_team(issue.project) add_users_with_subscription(issue.project, issue) + ActionMailer::Base.deliveries.clear end describe :new_issue do it do - should_email(issue.assignee_id) - should_email(@u_watcher.id) - should_email(@u_participant_mentioned.id) - should_not_email(@u_mentioned.id) - should_not_email(@u_participating.id) - should_not_email(@u_disabled.id) notification.new_issue(issue, @u_disabled) + + should_email(issue.assignee) + should_email(@u_watcher) + should_email(@u_participant_mentioned) + should_not_email(@u_mentioned) + should_not_email(@u_participating) + should_not_email(@u_disabled) end it do issue.assignee.update_attributes(notification_level: Notification::N_MENTION) - should_not_email(issue.assignee_id) notification.new_issue(issue, @u_disabled) - end - def should_email(user_id) - expect(Notify).to receive(:new_issue_email).with(user_id, issue.id) - end - - def should_not_email(user_id) - expect(Notify).not_to receive(:new_issue_email).with(user_id, issue.id) + should_not_email(issue.assignee) end end describe :reassigned_issue do it 'should email new assignee' do - should_email(issue.assignee_id) - should_email(@u_watcher.id) - should_email(@u_participant_mentioned.id) - should_email(@subscriber.id) - should_not_email(@unsubscriber.id) - should_not_email(@u_participating.id) - should_not_email(@u_disabled.id) - notification.reassigned_issue(issue, @u_disabled) - end - def should_email(user_id) - expect(Notify).to receive(:reassigned_issue_email).with(user_id, issue.id, nil, @u_disabled.id) - end - - def should_not_email(user_id) - expect(Notify).not_to receive(:reassigned_issue_email).with(user_id, issue.id, issue.assignee_id, @u_disabled.id) + should_email(issue.assignee) + should_email(@u_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_disabled) end end describe :close_issue do it 'should sent email to issue assignee and issue author' do - should_email(issue.assignee_id) - should_email(issue.author_id) - should_email(@u_watcher.id) - should_email(@u_participant_mentioned.id) - should_email(@subscriber.id) - should_not_email(@unsubscriber.id) - should_not_email(@u_participating.id) - should_not_email(@u_disabled.id) - notification.close_issue(issue, @u_disabled) - end - def should_email(user_id) - expect(Notify).to receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id) - end - - def should_not_email(user_id) - expect(Notify).not_to receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id) + should_email(issue.assignee) + should_email(issue.author) + should_email(@u_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_disabled) end end describe :reopen_issue do it 'should send email to issue assignee and issue author' do - should_email(issue.assignee_id) - should_email(issue.author_id) - should_email(@u_watcher.id) - should_email(@u_participant_mentioned.id) - should_email(@subscriber.id) - should_not_email(@unsubscriber.id) - should_not_email(@u_participating.id) - should_not_email(@u_disabled.id) - notification.reopen_issue(issue, @u_disabled) - end - def should_email(user_id) - expect(Notify).to receive(:issue_status_changed_email).with(user_id, issue.id, 'reopened', @u_disabled.id) - end - - def should_not_email(user_id) - expect(Notify).not_to receive(:issue_status_changed_email).with(user_id, issue.id, 'reopened', @u_disabled.id) + should_email(issue.assignee) + should_email(issue.author) + should_email(@u_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_not_email(@unsubscriber) + should_not_email(@u_participating) end end end @@ -309,108 +268,74 @@ describe NotificationService do before do build_team(merge_request.target_project) add_users_with_subscription(merge_request.target_project, merge_request) + ActionMailer::Base.deliveries.clear end describe :new_merge_request do it do - should_email(merge_request.assignee_id) - should_email(@u_watcher.id) - should_email(@u_participant_mentioned.id) - should_not_email(@u_participating.id) - should_not_email(@u_disabled.id) notification.new_merge_request(merge_request, @u_disabled) - end - def should_email(user_id) - expect(Notify).to receive(:new_merge_request_email).with(user_id, merge_request.id) - end - - def should_not_email(user_id) - expect(Notify).not_to receive(:new_merge_request_email).with(user_id, merge_request.id) + should_email(merge_request.assignee) + should_email(@u_watcher) + should_email(@u_participant_mentioned) + should_not_email(@u_participating) + should_not_email(@u_disabled) end end describe :reassigned_merge_request do it do - should_email(merge_request.assignee_id) - should_email(@u_watcher.id) - should_email(@u_participant_mentioned.id) - should_email(@subscriber.id) - should_not_email(@unsubscriber.id) - should_not_email(@u_participating.id) - should_not_email(@u_disabled.id) notification.reassigned_merge_request(merge_request, merge_request.author) - end - def should_email(user_id) - expect(Notify).to receive(:reassigned_merge_request_email).with(user_id, merge_request.id, nil, merge_request.author_id) - end - - def should_not_email(user_id) - expect(Notify).not_to receive(:reassigned_merge_request_email).with(user_id, merge_request.id, merge_request.assignee_id, merge_request.author_id) + should_email(merge_request.assignee) + should_email(@u_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_disabled) end end describe :closed_merge_request do it do - should_email(merge_request.assignee_id) - should_email(@u_watcher.id) - should_email(@u_participant_mentioned.id) - should_email(@subscriber.id) - should_not_email(@unsubscriber.id) - should_not_email(@u_participating.id) - should_not_email(@u_disabled.id) notification.close_mr(merge_request, @u_disabled) - end - def should_email(user_id) - expect(Notify).to receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) - end - - def should_not_email(user_id) - expect(Notify).not_to receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) + should_email(merge_request.assignee) + should_email(@u_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_disabled) end end describe :merged_merge_request do it do - should_email(merge_request.assignee_id) - should_email(@u_watcher.id) - should_email(@u_participant_mentioned.id) - should_email(@subscriber.id) - should_not_email(@unsubscriber.id) - should_not_email(@u_participating.id) - should_not_email(@u_disabled.id) notification.merge_mr(merge_request, @u_disabled) - end - def should_email(user_id) - expect(Notify).to receive(:merged_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) - end - - def should_not_email(user_id) - expect(Notify).not_to receive(:merged_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) + should_email(merge_request.assignee) + should_email(@u_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_disabled) end end describe :reopen_merge_request do it do - should_email(merge_request.assignee_id) - should_email(@u_watcher.id) - should_email(@u_participant_mentioned.id) - should_email(@subscriber.id) - should_not_email(@unsubscriber.id) - should_not_email(@u_participating.id) - should_not_email(@u_disabled.id) notification.reopen_mr(merge_request, @u_disabled) - end - def should_email(user_id) - expect(Notify).to receive(:merge_request_status_email).with(user_id, merge_request.id, 'reopened', @u_disabled.id) - end - - def should_not_email(user_id) - expect(Notify).not_to receive(:merge_request_status_email).with(user_id, merge_request.id, 'reopened', @u_disabled.id) + should_email(merge_request.assignee) + should_email(@u_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_disabled) end end end @@ -420,22 +345,16 @@ describe NotificationService do before do build_team(project) + ActionMailer::Base.deliveries.clear end describe :project_was_moved do it do - should_email(@u_watcher.id) - should_email(@u_participating.id) - should_not_email(@u_disabled.id) notification.project_was_moved(project, "gitlab/gitlab") - end - def should_email(user_id) - expect(Notify).to receive(:project_was_moved_email).with(project.id, user_id, "gitlab/gitlab") - end - - def should_not_email(user_id) - expect(Notify).not_to receive(:project_was_moved_email).with(project.id, user_id, "gitlab/gitlab") + should_email(@u_watcher) + should_email(@u_participating) + should_not_email(@u_disabled) end end end @@ -469,4 +388,18 @@ describe NotificationService do issuable.subscriptions.create(user: @subscriber, subscribed: true) issuable.subscriptions.create(user: @unsubscriber, subscribed: false) end + + def sent_to_user?(user) + ActionMailer::Base.deliveries.any? do |message| + message.to.include?(user.email) + end + end + + def should_email(user) + expect(sent_to_user?(user)).to be_truthy + end + + def should_not_email(user) + expect(sent_to_user?(user)).to be_falsey + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2be13bb3e6..0225a0ee53 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -31,6 +31,7 @@ RSpec.configure do |config| config.include StubConfiguration config.include RelativeUrl, type: feature config.include TestEnv + config.include ActiveJob::TestHelper config.include StubGitlabCalls config.include StubGitlabData config.include BenchmarkMatchers, benchmark: true From 113325a9e02b06d9cef82b913e2ab6f33331371b Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 30 Nov 2015 13:43:09 +0100 Subject: [PATCH 118/908] Retarget release --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index fb15225eed..53380d3f6e 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.3.0 (unreleased) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) + - Add rake tasks for git repository maintainance (Zeger-Jan van de Weg) v 8.2.1 - Forcefully update builds that didn't want to update with state machine @@ -56,7 +57,6 @@ v 8.2.0 - Show specific runners from projects where user is master or owner - MR target branch is now visible on a list view when it is different from project's default one - Improve Continuous Integration graphs page - - Add rake tasks for git repository maintainance (Zeger-Jan van de Weg) - 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) From e92ceb7b57139e985674a44cfe75534c52ed4acd Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 30 Nov 2015 16:12:31 +0200 Subject: [PATCH 119/908] fix specs --- Gemfile.lock | 4 +- app/controllers/abuse_reports_controller.rb | 2 +- .../project_services/ci/mail_service.rb | 6 +- app/views/ci/admin/runners/show.html.haml | 2 +- .../ci/notify/build_success_email.html.haml | 2 +- app/views/projects/edit.html.haml | 2 +- app/views/projects/runners/edit.html.haml | 2 +- app/workers/email_receiver_worker.rb | 2 +- lib/gitlab/github_import/client.rb | 2 +- lib/gitlab/gitlab_import/client.rb | 2 +- .../abuse_reports_controller_spec.rb | 36 ++++++----- spec/features/admin/admin_users_spec.rb | 2 +- spec/helpers/application_helper_spec.rb | 2 +- .../markdown/label_reference_filter_spec.rb | 6 +- .../ci/project_services/mail_service_spec.rb | 62 +++++++------------ .../project_services/jira_service_spec.rb | 6 +- spec/workers/email_receiver_worker_spec.rb | 12 ++-- 17 files changed, 72 insertions(+), 80 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0800137991..f06c4a4165 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -332,7 +332,7 @@ GEM github-markup (~> 1.3.3) gollum-grit_adapter (~> 1.0) nokogiri (~> 1.6.4) - rouge (~> 1.7.4) + rouge (~> 1.10.1) sanitize (~> 2.1.0) stringex (~> 2.5.1) gon (6.0.1) @@ -610,7 +610,7 @@ GEM netrc (~> 0.7) rinku (1.7.3) rotp (2.1.1) - rouge (1.7.7) + rouge (1.10.1) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb index d8e9059433..20bc5173f1 100644 --- a/app/controllers/abuse_reports_controller.rb +++ b/app/controllers/abuse_reports_controller.rb @@ -10,7 +10,7 @@ class AbuseReportsController < ApplicationController if @abuse_report.save if current_application_settings.admin_notification_email.present? - AbuseReportMailer.deliver_later.notify(@abuse_report.id) + AbuseReportMailer.notify(@abuse_report.id).deliver_later end message = "Thank you for your report. A GitLab administrator will look into it shortly." diff --git a/app/models/project_services/ci/mail_service.rb b/app/models/project_services/ci/mail_service.rb index bdc85667e9..bb961d0697 100644 --- a/app/models/project_services/ci/mail_service.rb +++ b/app/models/project_services/ci/mail_service.rb @@ -64,9 +64,9 @@ module Ci build.project_recipients.each do |recipient| case build.status.to_sym when :success - mailer.build_success_email(build.id, recipient) + mailer.build_success_email(build.id, recipient).deliver_later when :failed - mailer.build_fail_email(build.id, recipient) + mailer.build_fail_email(build.id, recipient).deliver_later end end end @@ -78,7 +78,7 @@ module Ci end def mailer - Ci::Notify.deliver_later + Ci::Notify end end end diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/ci/admin/runners/show.html.haml index 1498db46a8..fd3d33d657 100644 --- a/app/views/ci/admin/runners/show.html.haml +++ b/app/views/ci/admin/runners/show.html.haml @@ -37,7 +37,7 @@ = label_tag :tag_list, class: 'control-label' do Tags .col-sm-10 - = f.text_field :tag_list, class: 'form-control' + = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control' .help-block You can setup builds to only use runners with specific tags .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/ci/notify/build_success_email.html.haml b/app/views/ci/notify/build_success_email.html.haml index 24c439e50e..6ef1fd67d8 100644 --- a/app/views/ci/notify/build_success_email.html.haml +++ b/app/views/ci/notify/build_success_email.html.haml @@ -8,7 +8,7 @@ = @project.name %p - Commit: #{link_to @build.short_sha, namespace_project_commit_path(@build.gl_project.namespace, @build.gl_project, @build.sha)} + Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.gl_project.namespace, @build.gl_project, @build.sha)} %p Author: #{@build.commit.git_author_name} %p diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 3ebc175648..0c10de1604 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -35,7 +35,7 @@ .form-group = f.label :tag_list, "Tags", class: 'control-label' .col-sm-10 - = f.text_field :tag_list, maxlength: 2000, class: "form-control" + = f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control" %p.help-block Separate tags with commas. %fieldset.features diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml index dde9e448cb..a032470169 100644 --- a/app/views/projects/runners/edit.html.haml +++ b/app/views/projects/runners/edit.html.haml @@ -23,7 +23,7 @@ = label_tag :tag_list, class: 'control-label' do Tags .col-sm-10 - = f.text_field :tag_list, class: 'form-control' + = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control' .help-block You can setup jobs to only use runners with specific tags .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb index 1df8de1db7..f2649e38eb 100644 --- a/app/workers/email_receiver_worker.rb +++ b/app/workers/email_receiver_worker.rb @@ -46,6 +46,6 @@ class EmailReceiverWorker return end - EmailRejectionMailer.deliver_later.rejection(reason, raw, can_retry) + EmailRejectionMailer.rejection(reason, raw, can_retry).deliver_later end end diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 270cbcd9cc..74d1529e1f 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -46,7 +46,7 @@ module Gitlab end def github_options - OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys + OmniAuth::Strategies::GitHub.default_options[:client_options].to_h.symbolize_keys end end end diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb index 9c00896c91..86fb6c5176 100644 --- a/lib/gitlab/gitlab_import/client.rb +++ b/lib/gitlab/gitlab_import/client.rb @@ -75,7 +75,7 @@ module Gitlab end def gitlab_options - OmniAuth::Strategies::GitLab.default_options[:client_options].symbolize_keys + OmniAuth::Strategies::GitLab.default_options[:client_options].to_h.symbolize_keys end end end diff --git a/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb index 0faab8d7ff..15824a1c67 100644 --- a/spec/controllers/abuse_reports_controller_spec.rb +++ b/spec/controllers/abuse_reports_controller_spec.rb @@ -18,27 +18,31 @@ describe AbuseReportsController do end it "sends a notification email" do - post :create, - abuse_report: { - user_id: user.id, - message: message - } - - email = ActionMailer::Base.deliveries.last - - expect(email.to).to eq([admin_email]) - expect(email.subject).to include(user.username) - expect(email.text_part.body).to include(message) - end - - it "saves the abuse report" do - expect do + perform_enqueued_jobs do post :create, abuse_report: { user_id: user.id, message: message } - end.to change { AbuseReport.count }.by(1) + + email = ActionMailer::Base.deliveries.last + + expect(email.to).to eq([admin_email]) + expect(email.subject).to include(user.username) + expect(email.text_part.body).to include(message) + end + end + + it "saves the abuse report" do + perform_enqueued_jobs do + expect do + post :create, + abuse_report: { + user_id: user.id, + message: message + } + end.to change { AbuseReport.count }.by(1) + end end end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 4c756a8e73..c6b6ac58ac 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -87,7 +87,7 @@ describe "Admin::Users", feature: true do end it "should call send mail" do - expect(Notify).to receive(:new_user_email) + expect_any_instance_of(NotificationService).to receive(:new_user) click_button "Create user" end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 1dfae0fbd3..4b8000ecc4 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -59,7 +59,7 @@ describe ApplicationHelper do avatar_url = "http://localhost/uploads/project/avatar/#{project.id}/banana_sample.gif" expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s). - to eq "\"Banana" + to eq "\"Banana" end it 'should give uploaded icon when present' do diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb index fc21b65a84..ae286c8be2 100644 --- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb @@ -71,7 +71,7 @@ module Gitlab::Markdown doc = filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) + namespace_project_issues_path(project.namespace, project, label_name: label.name) end it 'links with adjacent text' do @@ -94,7 +94,7 @@ module Gitlab::Markdown doc = filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) + namespace_project_issues_path(project.namespace, project, label_name: label.name) expect(doc.text).to eq 'See gfm' end @@ -118,7 +118,7 @@ module Gitlab::Markdown doc = filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) + namespace_project_issues_path(project.namespace, project, label_name: label.name) expect(doc.text).to eq 'See gfm references' end diff --git a/spec/models/ci/project_services/mail_service_spec.rb b/spec/models/ci/project_services/mail_service_spec.rb index d9b3d34ff1..c03be3ef75 100644 --- a/spec/models/ci/project_services/mail_service_spec.rb +++ b/spec/models/ci/project_services/mail_service_spec.rb @@ -44,13 +44,10 @@ describe Ci::MailService do end it do - should_email("git@example.com") - mail.execute(build) - end - - def should_email(email) - expect(Ci::Notify).to receive(:build_fail_email).with(build.id, email) - expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email) + perform_enqueued_jobs do + expect{ mail.execute(build) }.to change{ ActionMailer::Base.deliveries.size }.by(1) + expect(ActionMailer::Base.deliveries.last.to).to eq(["git@example.com"]) + end end end @@ -67,13 +64,10 @@ describe Ci::MailService do end it do - should_email("git@example.com") - mail.execute(build) - end - - def should_email(email) - expect(Ci::Notify).to receive(:build_success_email).with(build.id, email) - expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email) + perform_enqueued_jobs do + expect{ mail.execute(build) }.to change{ ActionMailer::Base.deliveries.size }.by(1) + expect(ActionMailer::Base.deliveries.last.to).to eq(["git@example.com"]) + end end end @@ -95,14 +89,12 @@ describe Ci::MailService do end it do - should_email("git@example.com") - should_email("jeroen@example.com") - mail.execute(build) - end - - def should_email(email) - expect(Ci::Notify).to receive(:build_success_email).with(build.id, email) - expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email) + perform_enqueued_jobs do + expect{ mail.execute(build) }.to change{ ActionMailer::Base.deliveries.size }.by(2) + expect( + ActionMailer::Base.deliveries.map(&:to).flatten + ).to include("git@example.com", "jeroen@example.com") + end end end @@ -124,14 +116,11 @@ describe Ci::MailService do end it do - should_email(commit.git_author_email) - should_email("jeroen@example.com") - mail.execute(build) if mail.can_execute?(build) - end - - def should_email(email) - expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email) - expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email) + perform_enqueued_jobs do + expect do + mail.execute(build) if mail.can_execute?(build) + end.to_not change{ ActionMailer::Base.deliveries.size } + end end end @@ -177,14 +166,11 @@ describe Ci::MailService do it do Ci::Build.retry(build) - should_email(commit.git_author_email) - should_email("jeroen@example.com") - mail.execute(build) if mail.can_execute?(build) - end - - def should_email(email) - expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email) - expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email) + perform_enqueued_jobs do + expect do + mail.execute(build) if mail.can_execute?(build) + end.to_not change{ ActionMailer::Base.deliveries.size } + end end end end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index ddd2cce212..576f5fc79e 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -94,9 +94,9 @@ describe JiraService do end it 'should be prepopulated with the settings' do - expect(@service.properties[:project_url]).to eq('http://jira.sample/projects/project_a') - expect(@service.properties[:issues_url]).to eq("http://jira.sample/issues/:id") - expect(@service.properties[:new_issue_url]).to eq("http://jira.sample/projects/project_a/issues/new") + expect(@service.properties["project_url"]).to eq('http://jira.sample/projects/project_a') + expect(@service.properties["issues_url"]).to eq("http://jira.sample/issues/:id") + expect(@service.properties["new_issue_url"]).to eq("http://jira.sample/projects/project_a/issues/new") end end end diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb index 65a8d7d919..de40a6f78a 100644 --- a/spec/workers/email_receiver_worker_spec.rb +++ b/spec/workers/email_receiver_worker_spec.rb @@ -21,12 +21,14 @@ describe EmailReceiverWorker do end it "sends out a rejection email" do - described_class.new.perform(raw_message) + perform_enqueued_jobs do + described_class.new.perform(raw_message) - email = ActionMailer::Base.deliveries.last - expect(email).not_to be_nil - expect(email.to).to eq(["jake@adventuretime.ooo"]) - expect(email.subject).to include("Rejected") + email = ActionMailer::Base.deliveries.last + expect(email).not_to be_nil + expect(email.to).to eq(["jake@adventuretime.ooo"]) + expect(email.subject).to include("Rejected") + end end end end From f1504e1ad52df7aec241b93d0d015da13ddce5a9 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 30 Nov 2015 18:03:07 +0200 Subject: [PATCH 120/908] test fix --- app/models/project_services/gitlab_ci_service.rb | 2 +- spec/features/admin/admin_users_spec.rb | 5 ++++- spec/services/issues/close_service_spec.rb | 4 +++- spec/services/issues/update_service_spec.rb | 5 ++++- spec/services/merge_requests/close_service_spec.rb | 4 +++- spec/services/merge_requests/merge_service_spec.rb | 5 +++-- spec/services/merge_requests/reopen_service_spec.rb | 4 +++- spec/services/merge_requests/update_service_spec.rb | 6 ++++-- 8 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index c5657b5070..234e8e8b58 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -55,7 +55,7 @@ class GitlabCiService < CiService end def get_ci_commit(sha, ref) - Ci::Project.find(project.gitlab_ci_project).commits.find_by_sha!(sha) + Ci::Project.find(project.gitlab_ci_project.id).commits.find_by_sha!(sha) end def commit_status(sha, ref) diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index c6b6ac58ac..86f01faffb 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -93,7 +93,10 @@ describe "Admin::Users", feature: true do end it "should send valid email to user with email & password" do - click_button "Create user" + perform_enqueued_jobs do + click_button "Create user" + end + user = User.find_by(username: 'bang') email = ActionMailer::Base.deliveries.last expect(email.subject).to have_content('Account was created') diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index db547ce0d5..d711e3da10 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -14,7 +14,9 @@ describe Issues::CloseService do describe :execute do context "valid params" do before do - @issue = Issues::CloseService.new(project, user, {}).execute(issue) + perform_enqueued_jobs do + @issue = Issues::CloseService.new(project, user, {}).execute(issue) + end end it { expect(@issue).to be_valid } diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index cc3e648326..73d0b7f7ab 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -36,7 +36,10 @@ describe Issues::UpdateService do label_ids: [label.id] } - @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + perform_enqueued_jobs do + @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + end + @issue.reload end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index b3cbfd4b5b..272f389793 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -18,7 +18,9 @@ describe MergeRequests::CloseService do before do allow(service).to receive(:execute_hooks) - @merge_request = service.execute(merge_request) + perform_enqueued_jobs do + @merge_request = service.execute(merge_request) + end end it { expect(@merge_request).to be_valid } diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 7483f51de0..c0961ceb11 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -17,8 +17,9 @@ describe MergeRequests::MergeService do before do allow(service).to receive(:execute_hooks) - - service.execute(merge_request, 'Awesome message') + perform_enqueued_jobs do + service.execute(merge_request, 'Awesome message') + end end it { expect(merge_request).to be_valid } diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index 9401bc3b55..05146bf43f 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -19,7 +19,9 @@ describe MergeRequests::ReopenService do allow(service).to receive(:execute_hooks) merge_request.state = :closed - service.execute(merge_request) + perform_enqueued_jobs do + service.execute(merge_request) + end end it { expect(merge_request).to be_valid } diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 97f5c009ae..d899b1f01d 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -42,8 +42,10 @@ describe MergeRequests::UpdateService do before do allow(service).to receive(:execute_hooks) - @merge_request = service.execute(merge_request) - @merge_request.reload + perform_enqueued_jobs do + @merge_request = service.execute(merge_request) + @merge_request.reload + end end it { expect(@merge_request).to be_valid } From ae18ba16327abd79cf6207b83209d4ef96d3e158 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 24 Nov 2015 13:35:07 +0200 Subject: [PATCH 121/908] Fire update hook from GitLab --- CHANGELOG | 1 + app/models/repository.rb | 10 +++++++--- lib/gitlab/git/hook.rb | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 090b54f41a..4a18ac821f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ v 8.3.0 (unreleased) - Fix 500 error when update group member permission - Fix: Raw private snippets access workflow - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) + - Fire update hook from GitLab v 8.2.1 - Forcefully update builds that didn't want to update with state machine diff --git a/app/models/repository.rb b/app/models/repository.rb index c183610346..d247b0f501 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -571,9 +571,13 @@ class Repository # Run GitLab pre-receive hook pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', path_to_repo) - status = pre_receive_hook.trigger(gl_id, oldrev, newrev, ref) + pre_receive_hook_status = pre_receive_hook.trigger(gl_id, oldrev, newrev, ref) - if status + # Run GitLab update hook + update_hook = Gitlab::Git::Hook.new('update', path_to_repo) + update_hook_status = update_hook.trigger(gl_id, oldrev, newrev, ref) + + if pre_receive_hook_status && update_hook_status if was_empty # Create branch rugged.references.create(ref, newrev) @@ -596,7 +600,7 @@ class Repository # Remove tmp ref and return error to user rugged.references.delete(tmp_ref) - raise PreReceiveError.new('Commit was rejected by pre-receive hook') + raise PreReceiveError.new('Commit was rejected by git hook') end end diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb index dd393fe09d..07b856ca64 100644 --- a/lib/gitlab/git/hook.rb +++ b/lib/gitlab/git/hook.rb @@ -16,6 +16,17 @@ module Gitlab def trigger(gl_id, oldrev, newrev, ref) return true unless exists? + case name + when "pre-receive", "post-receive" + call_receive_hook(gl_id, oldrev, newrev, ref) + when "update" + call_update_hook(gl_id, oldrev, newrev, ref) + end + end + + private + + def call_receive_hook(gl_id, oldrev, newrev, ref) changes = [oldrev, newrev, ref].join(" ") # function will return true if succesful @@ -54,6 +65,12 @@ module Gitlab exit_status end + + def call_update_hook(gl_id, oldrev, newrev, ref) + Dir.chdir(repo_path) do + system({ 'GL_ID' => gl_id }, path, ref, oldrev, newrev) + end + end end end end From ceeb93fa77783a3fa9a60529195cd187af191bba Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 30 Nov 2015 12:02:44 -0500 Subject: [PATCH 122/908] Update CHANGELOG [ci skip] --- CHANGELOG | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 259a81f42d..1e8fb67032 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,14 +1,17 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) - - Fix Error 500 when viewing user's personal projects from admin page (Stan Hu) - - Ensure cached application settings are refreshed at startup (Stan Hu) - - Fix 404 in redirection after removing a project (Stan Hu) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - Fix 500 error when update group member permission - - Fix: Raw private snippets access workflow - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) +v 8.2.2 + - Fix 404 in redirection after removing a project (Stan Hu) + - Ensure cached application settings are refreshed at startup (Stan Hu) + - Fix Error 500 when viewing user's personal projects from admin page (Stan Hu) + - Fix: Raw private snippets access workflow + - Prevent "413 Request entity too large" errors when pushing large files with LFS + v 8.2.1 - Forcefully update builds that didn't want to update with state machine - Fix: saving GitLabCiService as Admin Template From 7ba00b50827d8d30ea4c54acb2623d6fb387dcf1 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 30 Nov 2015 19:04:44 +0100 Subject: [PATCH 123/908] Use gitlab-shell 2.6.8 in update guide --- doc/update/8.1-to-8.2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md index b57e999cfb..7b228d6a22 100644 --- a/doc/update/8.1-to-8.2.md +++ b/doc/update/8.1-to-8.2.md @@ -68,7 +68,7 @@ sudo -u git -H git checkout 8-2-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.6.7 +sudo -u git -H git checkout v2.6.8 ``` ### 5. Replace gitlab-git-http-server with gitlab-workhorse From a9642ba046db9aa4490d3361b32cdba48b3fbb9c Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 30 Nov 2015 19:05:28 +0100 Subject: [PATCH 124/908] Install gitlab-shell 2.6.8 --- doc/install/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 3f5c03a890..6164778060 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -312,7 +312,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da GitLab Shell is an SSH access and repository management software developed specially for GitLab. # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.7] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production + sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.8] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production # By default, the gitlab-shell config is generated from your main GitLab config. # You can review (and modify) the gitlab-shell config as follows: From 8f43298d967d715628b9b17d357cb9169a3606bf Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 30 Nov 2015 21:09:36 +0100 Subject: [PATCH 125/908] Show local issues and MRs in "This X closes... / closed by..." sentence --- app/helpers/issues_helper.rb | 6 +++++- app/helpers/merge_requests_helper.rb | 6 +++++- app/models/merge_request.rb | 2 +- app/views/projects/issues/_closed_by_box.html.haml | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 493f370d9a..b45d4e1ea8 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -84,7 +84,11 @@ module IssuesHelper end def merge_requests_sentence(merge_requests) - merge_requests.map(&:to_reference).to_sentence(last_word_connector: ', or ') + # Sorting based on the `!123` or `group/project!123` reference will sort + # local merge requests first. + merge_requests.map do |merge_request| + merge_request.to_reference(@project) + end.sort.to_sentence(last_word_connector: ', or ') end def url_to_emoji(name) diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index b804d4f4e3..a6010721df 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -39,7 +39,11 @@ module MergeRequestsHelper end def issues_sentence(issues) - issues.map(&:to_reference).to_sentence + # Sorting based on the `#123` or `group/project#123` reference will sort + # local issues first. + issues.map do |issue| + issue.to_reference(@project) + end.sort.to_sentence end def mr_change_branches_path(merge_request) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 1b3d6079d2..939df8cd8d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -316,7 +316,7 @@ class MergeRequest < ActiveRecord::Base issues = commits.flat_map { |c| c.closes_issues(current_user) } issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user). closed_by_message(description)) - issues.uniq.sort_by(&:id) + issues.uniq else [] end diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml index aef352029d..917d518168 100644 --- a/app/views/projects/issues/_closed_by_box.html.haml +++ b/app/views/projects/issues/_closed_by_box.html.haml @@ -1,3 +1,3 @@ .issue-closed-by-widget = icon('check') - This issue will be closed automatically when merge request #{gfm(merge_requests_sentence(@closed_by_merge_requests.sort))} is accepted. + This issue will be closed automatically when merge request #{gfm(merge_requests_sentence(@closed_by_merge_requests))} is accepted. From a7be01cd07430a4302668224947b2ed135c2d7bb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 30 Nov 2015 21:10:52 +0100 Subject: [PATCH 126/908] Render commit range reference with short shas, link to full shas. --- app/models/commit_range.rb | 70 +++++++---- .../commit_range_reference_filter_spec.rb | 7 +- spec/models/commit_range_spec.rb | 119 ++++++++---------- 3 files changed, 98 insertions(+), 98 deletions(-) diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 86fc9eb01a..fd23e24aff 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -2,12 +2,12 @@ # # Examples: # -# range = CommitRange.new('f3f85602...e86e1013') +# range = CommitRange.new('f3f85602...e86e1013', project) # range.exclude_start? # => false # range.reference_title # => "Commits f3f85602 through e86e1013" # range.to_s # => "f3f85602...e86e1013" # -# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae') +# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project) # range.exclude_start? # => true # range.reference_title # => "Commits f3f85602^ through e86e1013" # range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"} @@ -21,7 +21,7 @@ class CommitRange include ActiveModel::Conversion include Referable - attr_reader :sha_from, :notation, :sha_to + attr_reader :commit_from, :notation, :commit_to # Optional Project model attr_accessor :project @@ -53,17 +53,22 @@ class CommitRange # project - An optional Project model. # # Raises ArgumentError if `range_string` does not match `PATTERN`. - def initialize(range_string, project = nil) + def initialize(range_string, project) + @project = project + range_string.strip! unless range_string.match(/\A#{PATTERN}\z/) raise ArgumentError, "invalid CommitRange string format: #{range_string}" end - @exclude_start = !range_string.include?('...') - @sha_from, @notation, @sha_to = range_string.split(/(\.{2,3})/, 2) + ref_from, @notation, ref_to = range_string.split(/(\.{2,3})/, 2) - @project = project + @exclude_start = @notation == '..' + if project.valid_repo? + @commit_from = project.commit(ref_from) + @commit_to = project.commit(ref_to) + end end def inspect @@ -71,15 +76,16 @@ class CommitRange end def to_s - "#{sha_from[0..7]}#{notation}#{sha_to[0..7]}" + sha_from + notation + sha_to end + alias_method :id, :to_s + def to_reference(from_project = nil) - # Not using to_s because we want the full SHAs - reference = sha_from + notation + sha_to + reference = Commit.truncate_sha(sha_from) + notation + Commit.truncate_sha(sha_to) if cross_project_reference?(from_project) - reference = project.to_reference + '@' + reference + reference = project.to_reference + self.class.reference_prefix + reference end reference @@ -87,14 +93,14 @@ class CommitRange # Returns a String for use in a link's title attribute def reference_title - "Commits #{suffixed_sha_from} through #{sha_to}" + "Commits #{sha_start} through #{sha_to}" end # Return a Hash of parameters for passing to a URL helper # # See `namespace_project_compare_url` def to_param - { from: suffixed_sha_from, to: sha_to } + { from: sha_start, to: sha_to } end def exclude_start? @@ -105,28 +111,42 @@ class CommitRange # repository # # project - An optional Project to check (default: `project`) - def valid_commits?(project = project) - return nil unless project.present? - return false unless project.valid_repo? - - commit_from.present? && commit_to.present? + def valid_commits? + commit_start.present? && commit_end.present? end def persisted? true end - def commit_from - @commit_from ||= project.repository.commit(suffixed_sha_from) + def sha_from + return nil unless @commit_from + + @commit_from.id end - def commit_to - @commit_to ||= project.repository.commit(sha_to) + def sha_to + return nil unless @commit_to + + @commit_to.id end - private + def sha_start + return nil unless sha_from - def suffixed_sha_from - sha_from + (exclude_start? ? '^' : '') + exclude_start? ? sha_from + '^' : sha_from end + + def commit_start + return nil unless sha_start + + if exclude_start? + @commit_start ||= project.commit(sha_start) + else + commit_from + end + end + + alias_method :sha_end, :sha_to + alias_method :commit_end, :commit_to end diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb index e5b8d723fe..e078ff2681 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -8,8 +8,8 @@ module Gitlab::Markdown let(:commit1) { project.commit } let(:commit2) { project.commit("HEAD~2") } - let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}") } - let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}") } + let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}", project) } + let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}", project) } it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) @@ -53,7 +53,7 @@ module Gitlab::Markdown it 'links with adjacent text' do doc = filter("See (#{reference}.)") - exp = Regexp.escape(range.to_s) + exp = Regexp.escape(range.to_reference) expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) end @@ -62,6 +62,7 @@ module Gitlab::Markdown expect(project).to receive(:valid_repo?).and_return(true) expect(project.repository).to receive(:commit).with(commit1.id.reverse) + expect(project.repository).to receive(:commit).with(commit2.id) expect(filter(act).to_html).to eq exp end diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index 1031af097b..58283f0697 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -7,50 +7,56 @@ describe CommitRange do it { is_expected.to include_module(Referable) } end - let(:sha_from) { 'f3f85602' } - let(:sha_to) { 'e86e1013' } + let!(:project) { create(:project, :public) } + let!(:commit1) { project.commit("HEAD~2") } + let!(:commit2) { project.commit } - let(:range) { described_class.new("#{sha_from}...#{sha_to}") } - let(:range2) { described_class.new("#{sha_from}..#{sha_to}") } + let(:sha_from) { commit1.short_id } + let(:sha_to) { commit2.short_id } + + let(:full_sha_from) { commit1.id } + let(:full_sha_to) { commit2.id } + + let(:range) { described_class.new("#{sha_from}...#{sha_to}", project) } + let(:range2) { described_class.new("#{sha_from}..#{sha_to}", project) } it 'raises ArgumentError when given an invalid range string' do - expect { described_class.new("Foo") }.to raise_error(ArgumentError) + expect { described_class.new("Foo", project) }.to raise_error(ArgumentError) end describe '#to_s' do it 'is correct for three-dot syntax' do - expect(range.to_s).to eq "#{sha_from[0..7]}...#{sha_to[0..7]}" + expect(range.to_s).to eq "#{full_sha_from}...#{full_sha_to}" end it 'is correct for two-dot syntax' do - expect(range2.to_s).to eq "#{sha_from[0..7]}..#{sha_to[0..7]}" + expect(range2.to_s).to eq "#{full_sha_from}..#{full_sha_to}" end end describe '#to_reference' do - let(:project) { double('project', to_reference: 'namespace1/project') } + let(:cross) { create(:project) } - before do - range.project = project + it 'returns a String reference to the object' do + expect(range.to_reference).to eq "#{sha_from}...#{sha_to}" end it 'returns a String reference to the object' do - expect(range.to_reference).to eq range.to_s + expect(range2.to_reference).to eq "#{sha_from}..#{sha_to}" end it 'supports a cross-project reference' do - cross = double('project') - expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{range.to_s}" + expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{sha_from}...#{sha_to}" end end describe '#reference_title' do it 'returns the correct String for three-dot ranges' do - expect(range.reference_title).to eq "Commits #{sha_from} through #{sha_to}" + expect(range.reference_title).to eq "Commits #{full_sha_from} through #{full_sha_to}" end it 'returns the correct String for two-dot ranges' do - expect(range2.reference_title).to eq "Commits #{sha_from}^ through #{sha_to}" + expect(range2.reference_title).to eq "Commits #{full_sha_from}^ through #{full_sha_to}" end end @@ -60,11 +66,11 @@ describe CommitRange do end it 'includes the correct values for a three-dot range' do - expect(range.to_param).to eq({ from: sha_from, to: sha_to }) + expect(range.to_param).to eq({ from: full_sha_from, to: full_sha_to }) end it 'includes the correct values for a two-dot range' do - expect(range2.to_param).to eq({ from: sha_from + '^', to: sha_to }) + expect(range2.to_param).to eq({ from: full_sha_from + '^', to: full_sha_to }) end end @@ -79,64 +85,37 @@ describe CommitRange do end describe '#valid_commits?' do - context 'without a project' do - it 'returns nil' do - expect(range.valid_commits?).to be_nil + context 'with a valid repo' do + before do + expect(project).to receive(:valid_repo?).and_return(true) + end + + it 'is false when `sha_from` is invalid' do + expect(project).to receive(:commit).with(sha_from).and_return(nil) + expect(project).to receive(:commit).with(sha_to).and_call_original + + expect(range).not_to be_valid_commits + end + + it 'is false when `sha_to` is invalid' do + expect(project).to receive(:commit).with(sha_from).and_call_original + expect(project).to receive(:commit).with(sha_to).and_return(nil) + + expect(range).not_to be_valid_commits + end + + it 'is true when both `sha_from` and `sha_to` are valid' do + expect(range).to be_valid_commits end end - it 'accepts an optional project argument' do - project1 = double('project1').as_null_object - project2 = double('project2').as_null_object - - # project1 gets assigned through the accessor, but ignored when not given - # as an argument to `valid_commits?` - expect(project1).not_to receive(:present?) - range.project = project1 - - # project2 gets passed to `valid_commits?` - expect(project2).to receive(:present?).and_return(false) - - range.valid_commits?(project2) - end - - context 'with a project' do - let(:project) { double('project', repository: double('repository')) } - - context 'with a valid repo' do - before do - expect(project).to receive(:valid_repo?).and_return(true) - range.project = project - end - - it 'is false when `sha_from` is invalid' do - expect(project.repository).to receive(:commit).with(sha_from).and_return(false) - expect(project.repository).not_to receive(:commit).with(sha_to) - expect(range).not_to be_valid_commits - end - - it 'is false when `sha_to` is invalid' do - expect(project.repository).to receive(:commit).with(sha_from).and_return(true) - expect(project.repository).to receive(:commit).with(sha_to).and_return(false) - expect(range).not_to be_valid_commits - end - - it 'is true when both `sha_from` and `sha_to` are valid' do - expect(project.repository).to receive(:commit).with(sha_from).and_return(true) - expect(project.repository).to receive(:commit).with(sha_to).and_return(true) - expect(range).to be_valid_commits - end + context 'without a valid repo' do + before do + expect(project).to receive(:valid_repo?).and_return(false) end - context 'without a valid repo' do - before do - expect(project).to receive(:valid_repo?).and_return(false) - range.project = project - end - - it 'returns false' do - expect(range).not_to be_valid_commits - end + it 'returns false' do + expect(range).not_to be_valid_commits end end end From d6a5b45c8ea5ec7a68e213636fde405c52bb90e4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 30 Nov 2015 21:14:46 +0100 Subject: [PATCH 127/908] Recognize issue/MR/snippet/commit links as references. --- app/models/commit.rb | 15 ++-- app/models/commit_range.rb | 11 ++- app/models/concerns/referable.rb | 19 +++++ app/models/issue.rb | 11 ++- app/models/merge_request.rb | 11 ++- app/models/snippet.rb | 11 ++- lib/gitlab/markdown.rb | 5 +- .../markdown/abstract_reference_filter.rb | 31 +++++--- .../markdown/commit_range_reference_filter.rb | 70 +++++-------------- .../markdown/commit_reference_filter.rb | 68 +++++------------- lib/gitlab/markdown/external_link_filter.rb | 3 + .../merge_request_reference_filter.rb | 10 +++ lib/gitlab/markdown/redactor_filter.rb | 5 +- .../commit_range_reference_filter_spec.rb | 39 ++++++++++- .../markdown/commit_reference_filter_spec.rb | 31 ++++++++ .../markdown/issue_reference_filter_spec.rb | 32 +++++++++ .../merge_request_reference_filter_spec.rb | 29 +++++++- .../markdown/snippet_reference_filter_spec.rb | 30 ++++++++ 18 files changed, 301 insertions(+), 130 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index 492f6be1ce..fc03d2580d 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -73,16 +73,23 @@ class Commit # This pattern supports cross-project references. def self.reference_pattern %r{ - (?:#{Project.reference_pattern}#{reference_prefix})? - (?\h{6,40}) + #{link_reference_pattern} | + (?: + (?:#{Project.reference_pattern}#{reference_prefix})? + (?\h{6,40}) + ) }x end + def self.link_reference_pattern + super("commit", /(?\h{6,40})/) + end + def to_reference(from_project = nil) if cross_project_reference?(from_project) - "#{project.to_reference}@#{id}" + project.to_reference + self.class.reference_prefix + self.short_id else - id + self.short_id end end diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index fd23e24aff..98067771b7 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -42,11 +42,18 @@ class CommitRange # This pattern supports cross-project references. def self.reference_pattern %r{ - (?:#{Project.reference_pattern}#{reference_prefix})? - (?#{PATTERN}) + #{link_reference_pattern} | + (?: + (?:#{Project.reference_pattern}#{reference_prefix})? + (?#{PATTERN}) + ) }x end + def self.link_reference_pattern + super("compare", /(?#{PATTERN})/) + end + # Initialize a CommitRange # # range_string - The String commit range. diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index cced66cc1e..16e4d05486 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -44,6 +44,25 @@ module Referable def reference_pattern raise NotImplementedError, "#{self} does not implement #{__method__}" end + + def link_reference_pattern(route, pattern) + %r{ + (? + #{Regexp.escape(Gitlab.config.gitlab.url)} + \/#{Project.reference_pattern} + \/#{Regexp.escape(route)} + \/#{pattern} + (? + (\/[a-z0-9_=-]+)* + )? + (? + \?[a-z0-9_=-]+ + (&[a-z0-9_=-]+)* + )? + (?\#[a-z0-9_-]+)? + ) + }x + end end private diff --git a/app/models/issue.rb b/app/models/issue.rb index 7218310803..e62acfdfd9 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -64,11 +64,18 @@ class Issue < ActiveRecord::Base # This pattern supports cross-project references. def self.reference_pattern %r{ - (#{Project.reference_pattern})? - #{Regexp.escape(reference_prefix)}(?\d+) + #{link_reference_pattern} | + (?: + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?\d+) + ) }x end + def self.link_reference_pattern + super("issues", /(?\d+)/) + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 939df8cd8d..c1d3874ade 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -146,11 +146,18 @@ class MergeRequest < ActiveRecord::Base # This pattern supports cross-project references. def self.reference_pattern %r{ - (#{Project.reference_pattern})? - #{Regexp.escape(reference_prefix)}(?\d+) + #{link_reference_pattern} | + (?: + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?\d+) + ) }x end + def self.link_reference_pattern + super("merge_requests", /(?\d+)/) + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" diff --git a/app/models/snippet.rb b/app/models/snippet.rb index b0831982aa..8ec12ddf6e 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -60,11 +60,18 @@ class Snippet < ActiveRecord::Base # This pattern supports cross-project references. def self.reference_pattern %r{ - (#{Project.reference_pattern})? - #{Regexp.escape(reference_prefix)}(?\d+) + #{link_reference_pattern} | + (?: + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?\d+) + ) }x end + def self.link_reference_pattern + super("snippets", /(?\d+)/) + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{id}" diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index b082bfc434..ee458eda02 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -181,8 +181,6 @@ module Gitlab Gitlab::Markdown::RelativeLinkFilter, Gitlab::Markdown::EmojiFilter, Gitlab::Markdown::TableOfContentsFilter, - Gitlab::Markdown::AutolinkFilter, - Gitlab::Markdown::ExternalLinkFilter, Gitlab::Markdown::UserReferenceFilter, Gitlab::Markdown::IssueReferenceFilter, @@ -193,6 +191,9 @@ module Gitlab Gitlab::Markdown::CommitReferenceFilter, Gitlab::Markdown::LabelReferenceFilter, + Gitlab::Markdown::AutolinkFilter, + Gitlab::Markdown::ExternalLinkFilter, + Gitlab::Markdown::TaskListFilter ] end diff --git a/lib/gitlab/markdown/abstract_reference_filter.rb b/lib/gitlab/markdown/abstract_reference_filter.rb index fd5b7eb933..4adc44361b 100644 --- a/lib/gitlab/markdown/abstract_reference_filter.rb +++ b/lib/gitlab/markdown/abstract_reference_filter.rb @@ -40,7 +40,7 @@ module Gitlab # Returns a String replaced with the return of the block. def self.references_in(text) text.gsub(object_class.reference_pattern) do |match| - yield match, $~[object_sym].to_i, $~[:project] + yield match, $~[object_sym].to_i, $~[:project], $~ end end @@ -74,26 +74,41 @@ module Gitlab # Returns a String with references replaced with links. All links # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. def object_link_filter(text) - references_in(text) do |match, id, project_ref| + references_in(text) do |match, id, project_ref, matches| project = project_from_ref(project_ref) if project && object = find_object(project, id) - title = escape_once("#{object_title}: #{object.title}") + title = escape_once(object_link_title(object)) klass = reference_class(object_sym) - data = data_attribute(project: project.id, object_sym => object.id) - url = url_for_object(object, project) + data = data_attribute(project: project.id, object_sym => object.id, original: match) + url = matches[:url] || url_for_object(object, project) + + text = object.to_reference(context[:project]) + + extras = object_link_text_extras(object, matches) + text += " (#{extras.join(", ")})" if extras.any? %(#{match}) + class="#{klass}">#{text}) else match end end end - def object_title - object_class.name.titleize + def object_link_text_extras(object, matches) + extras = [] + + if matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/ + extras << "comment #{$1}" + end + + extras + end + + def object_link_title(object) + "#{object_class.name.titleize}: #{object.title}" end end end diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb index e070edae0a..f24bed7619 100644 --- a/lib/gitlab/markdown/commit_range_reference_filter.rb +++ b/lib/gitlab/markdown/commit_range_reference_filter.rb @@ -5,24 +5,14 @@ module Gitlab # HTML filter that replaces commit range references with links. # # This filter supports cross-project references. - class CommitRangeReferenceFilter < ReferenceFilter - include CrossProjectReference + class CommitRangeReferenceFilter < AbstractReferenceFilter + def self.object_class + CommitRange + end - # Public: Find commit range references in text - # - # CommitRangeReferenceFilter.references_in(text) do |match, commit_range, project_ref| - # "#{commit_range}" - # end - # - # text - String text to search. - # - # Yields the String match, the String commit range, and an optional String - # of the external project reference. - # - # Returns a String replaced with the return of the block. def self.references_in(text) text.gsub(CommitRange.reference_pattern) do |match| - yield match, $~[:commit_range], $~[:project] + yield match, $~[:commit_range], $~[:project], $~ end end @@ -31,9 +21,9 @@ module Gitlab return unless project id = node.attr("data-commit-range") - range = CommitRange.new(id, project) + range = find_object(project, id) - return unless range.valid_commits? + return unless range { commit_range: range } end @@ -44,49 +34,25 @@ module Gitlab @commit_map = {} end - def call - replace_text_nodes_matching(CommitRange.reference_pattern) do |content| - commit_range_link_filter(content) - end + def self.find_object(project, id) + range = CommitRange.new(id, project) + + range.valid_commits? ? range : nil end - # Replace commit range references in text with links to compare the commit - # ranges. - # - # text - String text to replace references in. - # - # Returns a String with commit range references replaced with links. All - # links have `gfm` and `gfm-commit_range` class names attached for - # styling. - def commit_range_link_filter(text) - self.class.references_in(text) do |match, id, project_ref| - project = self.project_from_ref(project_ref) - - range = CommitRange.new(id, project) - - if range.valid_commits? - url = url_for_commit_range(project, range) - - title = range.reference_title - klass = reference_class(:commit_range) - data = data_attribute(project: project.id, commit_range: id) - - project_ref += '@' if project_ref - - %(#{project_ref}#{range}) - else - match - end - end + def find_object(*args) + self.class.find_object(*args) end - def url_for_commit_range(project, range) + def url_for_object(range, project) h = Gitlab::Application.routes.url_helpers h.namespace_project_compare_url(project.namespace, project, range.to_param.merge(only_path: context[:only_path])) end + + def object_link_title(range) + range.reference_title + end end end end diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb index 8cdbeb1f9c..cc7abc08c8 100644 --- a/lib/gitlab/markdown/commit_reference_filter.rb +++ b/lib/gitlab/markdown/commit_reference_filter.rb @@ -5,24 +5,14 @@ module Gitlab # HTML filter that replaces commit references with links. # # This filter supports cross-project references. - class CommitReferenceFilter < ReferenceFilter - include CrossProjectReference + class CommitReferenceFilter < AbstractReferenceFilter + def self.object_class + Commit + end - # Public: Find commit references in text - # - # CommitReferenceFilter.references_in(text) do |match, commit, project_ref| - # "#{commit}" - # end - # - # text - String text to search. - # - # Yields the String match, the String commit identifier, and an optional - # String of the external project reference. - # - # Returns a String replaced with the return of the block. def self.references_in(text) text.gsub(Commit.reference_pattern) do |match| - yield match, $~[:commit], $~[:project] + yield match, $~[:commit], $~[:project], $~ end end @@ -31,58 +21,32 @@ module Gitlab return unless project id = node.attr("data-commit") - commit = commit_from_ref(project, id) + commit = find_object(project, id) return unless commit { commit: commit } end - def call - replace_text_nodes_matching(Commit.reference_pattern) do |content| - commit_link_filter(content) - end - end - - # Replace commit references in text with links to the commit specified. - # - # text - String text to replace references in. - # - # Returns a String with commit references replaced with links. All links - # have `gfm` and `gfm-commit` class names attached for styling. - def commit_link_filter(text) - self.class.references_in(text) do |match, id, project_ref| - project = self.project_from_ref(project_ref) - - if commit = self.class.commit_from_ref(project, id) - url = url_for_commit(project, commit) - - title = escape_once(commit.link_title) - klass = reference_class(:commit) - data = data_attribute(project: project.id, commit: id) - - project_ref += '@' if project_ref - - %(#{project_ref}#{commit.short_id}) - else - match - end - end - end - - def self.commit_from_ref(project, id) + def self.find_object(project, id) if project && project.valid_repo? project.commit(id) end end - def url_for_commit(project, commit) + def find_object(*args) + self.class.find_object(*args) + end + + def url_for_object(commit, project) h = Gitlab::Application.routes.url_helpers h.namespace_project_commit_url(project.namespace, project, commit, only_path: context[:only_path]) end + + def object_link_title(commit) + commit.link_title + end end end end diff --git a/lib/gitlab/markdown/external_link_filter.rb b/lib/gitlab/markdown/external_link_filter.rb index 29e51b6ade..b679293201 100644 --- a/lib/gitlab/markdown/external_link_filter.rb +++ b/lib/gitlab/markdown/external_link_filter.rb @@ -10,6 +10,9 @@ module Gitlab doc.search('a').each do |node| next unless node.has_attribute?('href') + klass = node.attribute('class') + next if klass && klass.include?('gfm') + link = node.attribute('href').value # Skip non-HTTP(S) links diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb index 1f47f03c94..3780a14a13 100644 --- a/lib/gitlab/markdown/merge_request_reference_filter.rb +++ b/lib/gitlab/markdown/merge_request_reference_filter.rb @@ -20,6 +20,16 @@ module Gitlab h.namespace_project_merge_request_url(project.namespace, project, mr, only_path: context[:only_path]) end + + def object_link_text_extras(object, matches) + extras = super + + if matches[:path] && matches[:path] == '/diffs' + extras.unshift "diffs" + end + + extras + end end end end diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/gitlab/markdown/redactor_filter.rb index a1f3a8a8eb..2a58c798f9 100644 --- a/lib/gitlab/markdown/redactor_filter.rb +++ b/lib/gitlab/markdown/redactor_filter.rb @@ -12,7 +12,10 @@ module Gitlab def call doc.css('a.gfm').each do |node| unless user_can_reference?(node) - node.replace(node.text) + # The reference should be replaced by the original text, + # which is not always the same as the rendered text. + text = node.attribute('data-original') || node.text + node.replace(text) end end diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb index e078ff2681..753140d60e 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -125,7 +125,44 @@ module Gitlab::Markdown it 'links with adjacent text' do doc = filter("Fixed (#{reference}.)") - exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}") + exp = Regexp.escape("#{project2.to_reference}@#{range.to_reference}") + expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" + expect(filter(act).to_html).to eq exp + + exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit_range]).not_to be_empty + end + end + + context 'URL cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) } + + before do + range.project = project2 + end + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + + exp = Regexp.escape(range.to_reference(project)) expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) end diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index d080efbf3d..c1bcf29b1b 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -131,5 +131,36 @@ module Gitlab::Markdown expect(result[:references][:commit]).not_to be_empty end end + + context 'URL cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:commit) { project2.commit } + let(:reference) { urls.namespace_project_commit_url(project2.namespace, project2, commit.id) } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + + exp = Regexp.escape(project2.to_reference) + expect(doc.to_html).to match(/\(#{commit.to_reference(project)}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Committed #{invalidate_reference(reference)}" + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit]).not_to be_empty + end + end end end diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb index 94c80ae661..296e8868f4 100644 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -135,5 +135,37 @@ module Gitlab::Markdown expect(result[:references][:issue]).to eq [issue] end end + + context 'URL cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { helper.url_for_issue(issue.iid, project2) + "#note_123" } + + it 'ignores valid references when cross-reference project uses external tracker' do + expect_any_instance_of(Project).to receive(:get_issue). + with(issue.iid).and_return(nil) + + exp = act = "Issue #{reference}" + expect(filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq reference + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end + end end end diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb index 3ef6cdfff3..49d8a6e44d 100644 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -89,7 +89,7 @@ module Gitlab::Markdown context 'cross-project reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:project, :public, namespace: namespace) } - let(:merge) { create(:merge_request, source_project: project2) } + let(:merge) { create(:merge_request, source_project: project2, target_project: project2) } let(:reference) { merge.to_reference(project) } it 'links to a valid reference' do @@ -97,7 +97,7 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_merge_request_url(project2.namespace, - project, merge) + project2, merge) end it 'links with adjacent text' do @@ -116,5 +116,30 @@ module Gitlab::Markdown expect(result[:references][:merge_request]).to eq [merge] end end + + context 'URL cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:merge) { create(:merge_request, source_project: project2, target_project: project2) } + let(:reference) { urls.namespace_project_merge_request_url(project2.namespace, + project2, merge) + '/diffs#note_123' } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq reference + end + + it 'links with adjacent text' do + doc = filter("Merge (#{reference}.)") + expect(doc.to_html).to match(/\(#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Merge #{reference}") + expect(result[:references][:merge_request]).to eq [merge] + end + end end end diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb index 9d9652dba4..3080a8a360 100644 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -114,5 +114,35 @@ module Gitlab::Markdown expect(result[:references][:snippet]).to eq [snippet] end end + + context 'URL cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { urls.namespace_project_snippet_url(project2.namespace, project2, snippet) } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + end + + it 'links with adjacent text' do + doc = filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(#{Regexp.escape(snippet.to_reference(project))}<\/a>\.\)/) + end + + it 'ignores invalid snippet IDs on the referenced project' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Snippet #{reference}") + expect(result[:references][:snippet]).to eq [snippet] + end + end end end From f9017a2b0384eef3f99ec2f3f1f2ef156afff2b8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 30 Nov 2015 21:15:19 +0100 Subject: [PATCH 128/908] Have ClosingIssueExtractor recognize all referenced issues --- config/initializers/1_settings.rb | 2 +- lib/gitlab/closing_issue_extractor.rb | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 6261924100..63d8ae1743 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -164,7 +164,7 @@ Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled']. Settings.gitlab['twitter_sharing_enabled'] ||= true if Settings.gitlab['twitter_sharing_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? -Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' if Settings.gitlab['issue_closing_pattern'].nil? +Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)' if Settings.gitlab['issue_closing_pattern'].nil? Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10 diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb index aeec595782..70b9943d7e 100644 --- a/lib/gitlab/closing_issue_extractor.rb +++ b/lib/gitlab/closing_issue_extractor.rb @@ -1,6 +1,10 @@ module Gitlab class ClosingIssueExtractor - ISSUE_CLOSING_REGEX = Regexp.new(Gitlab.config.gitlab.issue_closing_pattern) + ISSUE_CLOSING_REGEX = begin + pattern = Gitlab.config.gitlab.issue_closing_pattern + pattern = pattern.sub('%{issue_ref}', "(?:#{Issue.reference_pattern})") + Regexp.new(pattern).freeze + end def initialize(project, current_user = nil) @extractor = Gitlab::ReferenceExtractor.new(project, current_user) @@ -9,10 +13,12 @@ module Gitlab def closed_by_message(message) return [] if message.nil? - closing_statements = message.scan(ISSUE_CLOSING_REGEX). - map { |ref| ref[0] }.join(" ") + closing_statements = [] + message.scan(ISSUE_CLOSING_REGEX) do + closing_statements << Regexp.last_match[0] + end - @extractor.analyze(closing_statements) + @extractor.analyze(closing_statements.join(" ")) @extractor.issues end From 4a292aa6042f33d0b63bf95d223ae87bddcea2ce Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 30 Nov 2015 21:15:34 +0100 Subject: [PATCH 129/908] Proper cross-project references when MR is closed by issue --- app/services/system_note_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 7e2bc83417..09c159510c 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -125,7 +125,7 @@ class SystemNoteService # Returns the created Note object def self.change_status(noteable, project, author, status, source) body = "Status changed to #{status}" - body += " by #{source.gfm_reference}" if source + body += " by #{source.gfm_reference(project)}" if source create_note(noteable: noteable, project: project, author: author, note: body) end From bf5e7252ee5ffb5198b7916d1ad8f3436723721a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 30 Nov 2015 21:36:13 +0100 Subject: [PATCH 130/908] Recognize commit range with named refs in compare URLs. --- app/models/commit_range.rb | 31 ++++++++++++------- .../commit_range_reference_filter_spec.rb | 11 ++++--- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 98067771b7..7b1164b024 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -22,16 +22,19 @@ class CommitRange include Referable attr_reader :commit_from, :notation, :commit_to + attr_reader :ref_from, :ref_to # Optional Project model attr_accessor :project - # See `exclude_start?` - attr_reader :exclude_start - - # The beginning and ending SHAs can be between 6 and 40 hex characters, and + # The beginning and ending refs can be named or SHAs, and # the range notation can be double- or triple-dot. - PATTERN = /\h{6,40}\.{2,3}\h{6,40}/ + REF_PATTERN = /[0-9a-zA-Z][0-9a-zA-Z_.-]*[0-9a-zA-Z\^]/ + PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/ + + # In text references, the beginning and ending refs can only be SHAs + # between 6 and 40 hex characters. + STRICT_PATTERN = /\h{6,40}\.{2,3}\h{6,40}/ def self.reference_prefix '@' @@ -45,7 +48,7 @@ class CommitRange #{link_reference_pattern} | (?: (?:#{Project.reference_pattern}#{reference_prefix})? - (?#{PATTERN}) + (?#{STRICT_PATTERN}) ) }x end @@ -69,12 +72,16 @@ class CommitRange raise ArgumentError, "invalid CommitRange string format: #{range_string}" end - ref_from, @notation, ref_to = range_string.split(/(\.{2,3})/, 2) + @ref_from, @notation, @ref_to = range_string.split(/(\.{2,3})/, 2) - @exclude_start = @notation == '..' if project.valid_repo? - @commit_from = project.commit(ref_from) - @commit_to = project.commit(ref_to) + @commit_from = project.commit(@ref_from) + @commit_to = project.commit(@ref_to) + end + + if valid_commits? + @ref_from = Commit.truncate_sha(sha_from) if sha_from.start_with?(@ref_from) + @ref_to = Commit.truncate_sha(sha_to) if sha_to.start_with?(@ref_to) end end @@ -89,7 +96,7 @@ class CommitRange alias_method :id, :to_s def to_reference(from_project = nil) - reference = Commit.truncate_sha(sha_from) + notation + Commit.truncate_sha(sha_to) + reference = ref_from + notation + ref_to if cross_project_reference?(from_project) reference = project.to_reference + self.class.reference_prefix + reference @@ -111,7 +118,7 @@ class CommitRange end def exclude_start? - exclude_start + @notation == '..' end # Check if both the starting and ending commit IDs exist in a project's diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb index 753140d60e..4b4c769a11 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -5,8 +5,8 @@ module Gitlab::Markdown include FilterSpecHelper let(:project) { create(:project, :public) } - let(:commit1) { project.commit } - let(:commit2) { project.commit("HEAD~2") } + let(:commit1) { project.commit("HEAD~2") } + let(:commit2) { project.commit } let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}", project) } let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}", project) } @@ -89,7 +89,7 @@ module Gitlab::Markdown link = doc.css('a').first expect(link).to have_attribute('data-commit-range') - expect(link.attr('data-commit-range')).to eq range.to_reference + expect(link.attr('data-commit-range')).to eq range.to_s end it 'supports an :only_path option' do @@ -146,7 +146,8 @@ module Gitlab::Markdown context 'URL cross-project reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:project, :public, namespace: namespace) } - let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) } + let(:range) { CommitRange.new("#{commit1.id}...master", project) } + let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: 'master') } before do range.project = project2 @@ -156,7 +157,7 @@ module Gitlab::Markdown doc = filter("See #{reference}") expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) + to eq reference end it 'links with adjacent text' do From 4a0ebccf6b96184888f2d28b6e97592c6cb239c7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 30 Nov 2015 22:43:54 +0100 Subject: [PATCH 131/908] Fix specs --- app/models/concerns/mentionable.rb | 11 ++++++++--- .../markdown/commit_reference_filter_spec.rb | 1 - .../merge_request_reference_filter_spec.rb | 3 +-- spec/models/commit_spec.rb | 16 ++++++---------- spec/support/mentionable_shared_examples.rb | 17 +++++++---------- 5 files changed, 22 insertions(+), 26 deletions(-) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 193c91f174..0d6cd86aad 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -62,13 +62,18 @@ module Mentionable return [] if text.blank? refs = all_references(current_user, text, load_lazy_references: load_lazy_references) - (refs.issues + refs.merge_requests + refs.commits) - [local_reference] + refs = (refs.issues + refs.merge_requests + refs.commits) + + # We're using this method instead of Array diffing because that requires + # both of the object's `hash` values to be the same, which may not be the + # case for otherwise identical Commit objects. + refs.reject! { |ref| ref == local_reference } end # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. def create_cross_references!(author = self.author, without = [], text = self.mentionable_text) refs = referenced_mentionables(author, text) - + # We're using this method instead of Array diffing because that requires # both of the object's `hash` values to be the same, which may not be the # case for otherwise identical Commit objects. @@ -111,7 +116,7 @@ module Mentionable # Only include changed fields that are mentionable source.select { |key, val| mentionable.include?(key) } end - + # Determine whether or not a cross-reference Note has already been created between this Mentionable and # the specified target. def cross_reference_exists?(target) diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index c1bcf29b1b..a8c9c7efd5 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -148,7 +148,6 @@ module Gitlab::Markdown it 'links with adjacent text' do doc = filter("Fixed (#{reference}.)") - exp = Regexp.escape(project2.to_reference) expect(doc.to_html).to match(/\(#{commit.to_reference(project)}<\/a>\.\)/) end diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb index 49d8a6e44d..ca3e7151e0 100644 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -121,8 +121,7 @@ module Gitlab::Markdown let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:project, :public, namespace: namespace) } let(:merge) { create(:merge_request, source_project: project2, target_project: project2) } - let(:reference) { urls.namespace_project_merge_request_url(project2.namespace, - project2, merge) + '/diffs#note_123' } + let(:reference) { urls.namespace_project_merge_request_url(project2.namespace, project2, merge) + '/diffs#note_123' } it 'links to a valid reference' do doc = filter("See #{reference}") diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 90be932495..1aaa927c21 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -15,12 +15,12 @@ describe Commit do describe '#to_reference' do it 'returns a String reference to the object' do - expect(commit.to_reference).to eq commit.id + expect(commit.to_reference).to eq commit.short_id end it 'supports a cross-project reference' do cross = double('project') - expect(commit.to_reference(cross)).to eq "#{project.to_reference}@#{commit.id}" + expect(commit.to_reference(cross)).to eq "#{project.to_reference}@#{commit.short_id}" end end @@ -77,14 +77,10 @@ eos let(:other_issue) { create :issue, project: other_project } it 'detects issues that this commit is marked as closing' do - allow(commit).to receive(:safe_message).and_return("Fixes ##{issue.iid}") - expect(commit.closes_issues).to eq([issue]) - end - - it 'does not detect issues from other projects' do ext_ref = "#{other_project.path_with_namespace}##{other_issue.iid}" - allow(commit).to receive(:safe_message).and_return("Fixes #{ext_ref}") - expect(commit.closes_issues).to be_empty + allow(commit).to receive(:safe_message).and_return("Fixes ##{issue.iid} and #{ext_ref}") + expect(commit.closes_issues).to include(issue) + expect(commit.closes_issues).to include(other_issue) end end @@ -92,7 +88,7 @@ eos subject { create(:project).commit } let(:author) { create(:user, email: subject.author_email) } - let(:backref_text) { "commit #{subject.id}" } + let(:backref_text) { "commit #{subject.short_id}" } let(:set_mentionable_text) do ->(txt) { allow(subject).to receive(:safe_message).and_return(txt) } end diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index 3bb568f4d4..33d2b14583 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -10,12 +10,12 @@ def common_mentionable_setup let(:mentioned_issue) { create(:issue, project: project) } let!(:mentioned_mr) { create(:merge_request, :simple, source_project: project) } - let(:mentioned_commit) { project.commit } + let(:mentioned_commit) { project.commit("HEAD~1") } let(:ext_proj) { create(:project, :public) } let(:ext_issue) { create(:issue, project: ext_proj) } let(:ext_mr) { create(:merge_request, :simple, source_project: ext_proj) } - let(:ext_commit) { ext_proj.commit } + let(:ext_commit) { ext_proj.commit("HEAD~2") } # Override to add known commits to the repository stub. let(:extra_commits) { [] } @@ -45,14 +45,11 @@ def common_mentionable_setup before do # Wire the project's repository to return the mentioned commit, and +nil+ # for any unrecognized commits. - commitmap = { - mentioned_commit.id => mentioned_commit - } - extra_commits.each { |c| commitmap[c.short_id] = c } - - allow(Project).to receive(:find).and_call_original - allow(Project).to receive(:find).with(project.id.to_s).and_return(project) - allow(project.repository).to receive(:commit) { |sha| commitmap[sha] } + allow_any_instance_of(::Repository).to receive(:commit).and_call_original + allow_any_instance_of(::Repository).to receive(:commit).with(mentioned_commit.short_id).and_return(mentioned_commit) + extra_commits.each do |commit| + allow_any_instance_of(::Repository).to receive(:commit).with(commit.short_id).and_return(commit) + end set_mentionable_text.call(ref_string) end From 8c4a3c77d87e89bf3fd237fef49fc87fb6170d86 Mon Sep 17 00:00:00 2001 From: Minsik Yoon Date: Mon, 30 Nov 2015 18:30:44 +0900 Subject: [PATCH 132/908] Add ignore whitespace change option to commit view --- CHANGELOG | 1 + app/controllers/projects/commit_controller.rb | 7 ++++++- spec/controllers/commit_controller_spec.rb | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 090b54f41a..35bb9951d8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ v 8.3.0 (unreleased) - Fix 500 error when update group member permission - Fix: Raw private snippets access workflow - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) + - Add ignore whitespace change option to commit view v 8.2.1 - Forcefully update builds that didn't want to update with state machine diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index deefdd7666..3f137440e2 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -67,7 +67,12 @@ class Projects::CommitController < Projects::ApplicationController end def define_show_vars - @diffs = commit.diffs + if params[:w].to_i == 1 + @diffs = commit.diffs({ ignore_whitespace_change: true }) + else + @diffs = commit.diffs + end + @notes_count = commit.notes.count @builds = ci_commit.builds if ci_commit diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb index bb3d87f384..5337a69e84 100644 --- a/spec/controllers/commit_controller_spec.rb +++ b/spec/controllers/commit_controller_spec.rb @@ -69,6 +69,21 @@ describe Projects::CommitController do expect(response.body).to start_with("diff --git") end + + it "should really only be a git diff without whitespace changes" do + get(:show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: '66eceea0db202bb39c4e445e8ca28689645366c5', + # id: commit.id, + format: format, + w: 1) + + expect(response.body).to start_with("diff --git") + # without whitespace option, there are more than 2 diff_splits + diff_splits = assigns(:diffs)[0].diff.split("\n") + expect(diff_splits.length).to be <= 2 + end end describe "as patch" do From 5f6117c0aa391a6e9c96493ca01a8a23eb40f0cd Mon Sep 17 00:00:00 2001 From: Job van der Voort Date: Thu, 5 Nov 2015 16:53:05 +0100 Subject: [PATCH 133/908] update ci docs with yml reason and better quickstart --- doc/ci/quick_start/README.md | 202 ++++++++++++++++++++++++----------- doc/ci/yaml/README.md | 30 +++--- 2 files changed, 156 insertions(+), 76 deletions(-) diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index d69064a91f..74626c8f8a 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -1,44 +1,62 @@ # Quick Start -To start building projects with GitLab CI a few steps needs to be done. +GitLab Continuous Integration (CI) is fully integrated into GitLab itself. You +only need to enable it in the Services settings of your project. -## 1. Install GitLab and CI +This guide assumes that you: -First you need to have a working GitLab and GitLab CI instance. +- have a working GitLab instance of version 8.0 or higher or are using + [GitLab.com](https://gitlab.com/users/sign_in) +- have a project in GitLab that you would like to use CI for -You can omit this step if you use [GitLab.com](https://GitLab.com/). +In brief, the steps needed to have a working CI can be summed up to: -## 2. Create repository on GitLab +1. Create a new project +1. Add `.gitlab-ci.yml` to the git repository and push to GitLab +1. Configure a Runner -Once you login on your GitLab add a new repository where you will store your source code. -Push your application to that repository. +From there on, on every push to your git repository the build will be +automagically started by the Runner and will appear under the project's +`/builds` page. -## 3. Add project to CI +Now, let's break it down to pieces and work on solving the GitLab CI puzzle. -The next part is to login to GitLab CI. -Point your browser to the URL you have set GitLab or use [gitlab.com/ci](https://gitlab.com/ci/). +## 1. Creating a `.gitlab-ci.yml` file -On the first screen you will see a list of GitLab's projects that you have access to: + **GitLab CI** service is enabled automatically on the first push of a + `.gitlab-ci.yml` file in your repository and this is the recommended way. -![Projects](projects.png) +For other methods read [how to enable the GitLab CI service](../enable_ci.md). -Click **Add Project to CI**. -This will create project in CI and authorize GitLab CI to fetch sources from GitLab. +### What is `.gitlab-ci.yml` -> GitLab CI creates unique token that is used to configure GitLab CI service in GitLab. -> This token allows to access GitLab's repository and configures GitLab to trigger GitLab CI webhook on **Push events** and **Tag push events**. -> You can see that token by going to Project's Settings > Services > GitLab CI. -> You will see there token, the same token is assigned in GitLab CI settings of project. +The `.gitlab-ci.yml` file is where you configure what CI does with your project. +It lives in the root of your repository. -## 4. Create project's configuration - .gitlab-ci.yml +On any push to your repository, GitLab will look for the `.gitlab-ci.yml` +file and start builds on _Runners_ according to the contents of the file, +for that commit. -The next: You have to define how your project will be built. -GitLab CI uses [YAML](https://en.wikipedia.org/wiki/YAML) file to store build configuration. -You need to create `.gitlab-ci.yml` in root directory of your repository: +Because `.gitlab-ci.yml` is in the repository, it is version controlled, +old versions still build succesfully, forks can easily make use of CI, +branches can have separate builds and you have a single source of truth for CI. +You can read more about the reasons why we are using `.gitlab-ci.yml` +[in our blog about it][blog-ci]. + +`.gitlab-ci.yml` is a [YAML](https://en.wikipedia.org/wiki/YAML) file. + +### Creating a simple `.gitlab-ci.yml` file + +You need to create a file named `.gitlab-ci.yml` in the root directory of your +repository. Below is an example for a Ruby on Rails project. ```yaml before_script: - - bundle install + - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs + - ruby -v + - which ruby + - gem install bundler --no-ri --no-rdoc + - bundle install --jobs $(nproc) "${FLAGS[@]}" rspec: script: @@ -49,71 +67,129 @@ rubocop: - bundle exec rubocop ``` -This is the simplest possible build configuration that will work for most Ruby applications: -1. Define two jobs `rspec` and `rubocop` with two different commands to be executed. -1. Before every job execute commands defined by `before_script`. +This is the simplest possible build configuration that will work for most Ruby +applications: -The `.gitlab-ci.yml` defines set of jobs with constrains how and when they should be run. -The jobs are defined as top-level elements with name and always have to contain the `script`. -Jobs are used to create builds, which are then picked by [runners](../runners/README.md) and executed within environment of the runner. -What is important that each job is run independently from each other. +1. Define two jobs `rspec` and `rubocop` with different commands to be executed. +1. Before every job, the commands defined by `before_script` are executed. -For more information and complete `.gitlab-ci.yml` syntax, please check the [Configuring project (.gitlab-ci.yml)](../yaml/README.md). +The `.gitlab-ci.yml` file defines sets of jobs with constraints of how and when +they should be run. The jobs are defined as top-level elements with a name and +always have to contain the `script` name. Jobs are used to create builds, +which are then picked by [Runners](../runners/README.md) and executed within +the environment of the Runner. -## 5. Add file and push .gitlab-ci.yml to repository +What is important is that each job is run independently from each other. -Once you created `.gitlab-ci.yml` you should add it to git repository and push it to GitLab. +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. + +For more information and a complete `.gitlab-ci.yml` syntax, please check +[the documentation on .gitlab-ci.yml](../yaml/README.md). + +### Push `.gitlab-ci.yml` to GitLab + +Once you've created `.gitlab-ci.yml`, you should add it to your git repository +and push it to GitLab. ```bash git add .gitlab-ci.yml -git commit +git commit -m "Add .gitlab-ci.yml" git push origin master ``` -If you refresh the project's page on GitLab CI you will notice a one new commit: +Now if you go to the **Builds** page you will see that the builds are pending. -![](new_commit.png) +You can also go to the **Commits** page and notice the little clock icon next +to the commit SHA. -However the commit has status **pending** which means that commit was not yet picked by runner. +![New commit pending](img/new_commit.png) -## 6. Configure runner +Clicking on the clock icon you will be directed to the builds page for that +specific commit. -In GitLab CI, Runners run your builds. -A runner is a machine (can be virtual, bare-metal or VPS) that picks up builds through the coordinator API of GitLab CI. +![Single commit builds page](img/single_commit_status_pending.png) -A runner can be specific to a certain project or serve any project in GitLab CI. -A runner that serves all projects is called a shared runner. -More information about different runner types can be found in [Configuring runner](../runners/README.md). +Notice that there are two jobs pending which are named after what we wrote in +`.gitlab-ci.yml`. The red triangle indicates that there is no Runner configured +yet for these builds. -To check if you have runners assigned to your project go to **Runners**. You will find there information how to setup project specific runner: +The next step is to configure a Runner so that it picks the pending jobs. -1. Install GitLab Runner software. Checkout the [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner) section to install it. -1. Specify following URL during runner setup: https://gitlab.com/ci/ -1. Use the following registration token during setup: TOKEN +## 2. Configuring a Runner -If you do it correctly your runner should be shown under **Runners activated for this project**: +In GitLab, Runners run the builds that you define in `.gitlab-ci.yml`. +A Runner can be a virtual machine, a VPS, a bare-metal machine, a docker +container or even a cluster of containers. GitLab and the Runners communicate +through an API, so the only needed requirement is that the machine on which the +Runner is configured to have Internet access. -![](runners_activated.png) +A Runner can be specific to a certain project or serve multiple projects in +GitLab. If it serves all projects it's called a _Shared Runner_. -### Shared runners +Find more information about different Runners in the +[Runners](../runners/README.md) documentation. -If you use [gitlab.com/ci](https://gitlab.com/ci/) you can use **Shared runners** provided by GitLab Inc. -These are special virtual machines that are run on GitLab's infrastructure that can build any project. -To enable **Shared runners** you have to go to **Runners** and click **Enable shared runners** for this project. +You can find whether any Runners are assigned to your project by going to +**Settings** -> **Runners**. -## 7. Check status of commit +Setting up a Runner is easy and straightforward. The official Runner supported +by GitLab is written in Go and can be found at +. -If everything went OK and you go to commit, the status of the commit should change from **pending** to either **running**, **success** or **failed**. +In order to have a functional Runner you need to: -![](commit_status.png) +1. [Install it][runner-install] +2. [Configure it](../runners/README.md#registering-a-specific-runner) -You can click **Build ID** to view build log for specific job. +For other types of unofficial Runners written in other languages, see the +[instructions for the various GitLab Runners](https://about.gitlab.com/gitlab-ci/#gitlab-runner). -## 8. Congratulations! +Once the Runner has been set up, you should see it on the Runners page of your +project, following **Settings** -> **Runners**. -You managed to build your first project using GitLab CI. -You may need to tune your `.gitlab-ci.yml` file to implement build plan for your project. -A few examples how it can be done you can find on [Examples](../examples/README.md) page. +![Activated runners](img/runners_activated.png) -GitLab CI also offers **the Lint** tool to verify validity of your `.gitlab-ci.yml` which can be useful to troubleshoot potential problems. -The Lint is available from project's settings or by adding `/lint` to GitLab CI url. +### Shared Runners + +If you use [GitLab.com](https://gitlab.com/) you can use **Shared Runners** +provided by GitLab Inc. + +These are special virtual machines that are run on GitLab's infrastructure that +can build any project. + +To enable **Shared Runners** you have to go to your project's +**Settings** -> **Runners** and click **Enable shared runners**. + +[Read more on Shared Runners](../runners/README.md). + +## 3. Seeing the status of your build + +After configuring the Runner succesfully, you should see the status of your +last commit change from _pending_ to either _running_, _success_ or _failed_. + +You can view all builds, by going to the **Builds** page in your project. + +![Commit status](img/builds_status.png) + +By clicking on a Build ID, you will be able to see the log of that build. +This is important to diagnose why a build failed or acted differently than +you expected. + +![Build log](img/build_log.png) + +You are also able to view the status of any commit in the various pages in +GitLab, such as **Commits** and **Merge Requests**. + +## Next steps + +Awesome! You started using CI in GitLab! + +Next you can look into doing more with the CI. Many people are using GitLab +to package, containerize, test and deploy software. + +We have a number of [examples](../examples/README.md) available. + +[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#installation +[blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/ diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 3dbf1afc7a..9ee26c41e6 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -20,6 +20,22 @@ Of course a command can execute code directly (`./configure;make;make install`) Jobs are used to create builds, which are then picked up by [runners](../runners/README.md) and executed within the environment of the runner. What is important, is that each job is run independently from each other. +## Why `.gitlab-ci.yml` + +By placing a single configuration file in the root of your repository, +it is version controlled and you get all the advantages of git. + +In addition, builds for older versions of the repository will work just fine, +as GitLab look at the `.gitlab-ci.yml` of the pushed commit. +This means that forks also build without any problem. + +You can even set up different builds for different branches. This allows you +to only deploy the `production` branch, for instance. + +By having a single source of truth, everyone can view and contribute to the +stability of your CI builds, eventually improving the quality of your development +cycle. + ## .gitlab-ci.yml The YAML syntax allows for using more complex job specifications than in the above example: @@ -185,7 +201,7 @@ This are two parameters that allow for setting a refs policy to limit when jobs There are a few rules that apply to usage of refs policy: -1. `only` and `except` are inclusive. If both `only` and `except` are defined in job specification the ref is filtered by `only` and `except`. +1. `only` and `except` are exclusive. If both `only` and `except` are defined in job specification only `only` is taken into account. 1. `only` and `except` allow for using the regexp expressions. 1. `only` and `except` allow for using special keywords: `branches` and `tags`. These names can be used for example to exclude all tags and all branches. @@ -198,18 +214,6 @@ job: - branches # use special keyword ``` -1. `only` and `except` allow for specify repository path to filter jobs for forks. -The repository path can be used to have jobs executed only for parent repository. - -```yaml -job: - only: - - branches@gitlab-org/gitlab-ce - except: - - master@gitlab-org/gitlab-ce -``` -The above will run `job` for all branches on `gitlab-org/gitlab-ce`, except master . - ### tags `tags` is used to select specific runners from the list of all runners that are allowed to run this project. From 2c30d11e80a4bb4112d01be7e1fbe51a321b3beb Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 5 Nov 2015 22:49:57 +0200 Subject: [PATCH 134/908] Clean up intro --- doc/ci/quick_start/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 74626c8f8a..92dad2be3e 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -16,8 +16,8 @@ In brief, the steps needed to have a working CI can be summed up to: 1. Configure a Runner From there on, on every push to your git repository the build will be -automagically started by the Runner and will appear under the project's -`/builds` page. +automagically started by the runner and will appear under the project's `/builds` +page. Now, let's break it down to pieces and work on solving the GitLab CI puzzle. From 1058652a920d1106d29452aadfef953937a188e5 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 6 Nov 2015 10:43:28 +0200 Subject: [PATCH 135/908] Add section of enabling GitLab CI --- doc/ci/quick_start/README.md | 7 +++++ doc/ci/quick_start/builds_tab.png | Bin 0 -> 3845 bytes doc/ci/quick_start/ci_service_disabled.png | Bin 0 -> 3598 bytes doc/ci/quick_start/ci_service_enabled.png | Bin 0 -> 3545 bytes doc/ci/quick_start/ci_service_mark_active.png | Bin 0 -> 17193 bytes doc/ci/quick_start/enable_ci.md | 24 ++++++++++++++++++ 6 files changed, 31 insertions(+) create mode 100644 doc/ci/quick_start/builds_tab.png create mode 100644 doc/ci/quick_start/ci_service_disabled.png create mode 100644 doc/ci/quick_start/ci_service_enabled.png create mode 100644 doc/ci/quick_start/ci_service_mark_active.png create mode 100644 doc/ci/quick_start/enable_ci.md diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 92dad2be3e..d45ad3e5e2 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -21,6 +21,13 @@ page. Now, let's break it down to pieces and work on solving the GitLab CI puzzle. +## 1. Enable GitLab CI + +After creating a new project, the first thing to do is enable the **GitLab CI** +service in your project's settings if it isn't already enabled. + +Read [how to enable the GitLab CI service](enable_ci.md). + ## 1. Creating a `.gitlab-ci.yml` file **GitLab CI** service is enabled automatically on the first push of a diff --git a/doc/ci/quick_start/builds_tab.png b/doc/ci/quick_start/builds_tab.png new file mode 100644 index 0000000000000000000000000000000000000000..d088b8b329dc5dca75513e4fbe27d2323203d770 GIT binary patch literal 3845 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 literal 0 HcmV?d00001 diff --git a/doc/ci/quick_start/ci_service_enabled.png b/doc/ci/quick_start/ci_service_enabled.png new file mode 100644 index 0000000000000000000000000000000000000000..dfe7488c1ba50d0bbf43523a20ab64f6dceb0eff GIT binary patch literal 3545 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 Services** and search for **GitLab CI**. Its state +should be disabled. + +![CI service disabled](ci_service_disabled.png) + +Click on **GitLab CI** to enter its settings, mark it as active and hit +**Save**. + +![Mark CI service as active](ci_service_mark_active.png) + +Do you see that green dot? Then good, the service is now enabled! You can also +check its status under **Services**. + +![CI service enabled](ci_service_enabled.png) From 3b4806bad95e55a40d3efe02e329d271995d8a70 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sat, 7 Nov 2015 09:11:37 +0200 Subject: [PATCH 136/908] Add option to enable GitLab CI via .gitlab-ci.yml --- doc/ci/quick_start/enable_ci.md | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/doc/ci/quick_start/enable_ci.md b/doc/ci/quick_start/enable_ci.md index 6f539c752d..42635e35d5 100644 --- a/doc/ci/quick_start/enable_ci.md +++ b/doc/ci/quick_start/enable_ci.md @@ -1,15 +1,30 @@ # Enable GitLab CI -GitLab Continuous Integration (CI) is fully integrated into GitLab itself. You -only need to enable it in the **Services** settings of your project. +GitLab Continuous Integration (CI) is fully integrated into GitLab itself as +of [version 8.0](https://about.gitlab.com/2015/09/22/gitlab-8-0-released/). -First, head over your project's page that you would like to enable CI for. -If you can see the **Builds** tab in the sidebar, then CI is enabled. +First, head over your project's page that you would like to enable GitLab CI +for. If you can see the **Builds** tab in the sidebar, then GitLab CI is +already enabled and you can skip reading the rest of this guide. ![Builds tab](builds_tab.png) -If not, go to **Settings > Services** and search for **GitLab CI**. Its state -should be disabled. +If not, there are two ways to enable it in your project. + +## Use .gitlab-ci.yml to enable GitLab CI + +GitLab CI will be automatically enabled if you just push a +[`.gitlab-ci.yml`](../yaml/README.md) in your git repository. GitLab will +pick up the change immediately and GitLab CI will be enabled for this project. +This is the recommended way. + +## Manually enable GitLab CI + +The second way is to manually enable it in the project's **Services** settings +and this is also the way to disable it if needed. + +Go to **Settings > Services** and search for **GitLab CI**. Its state should +be disabled. ![CI service disabled](ci_service_disabled.png) From 32a3c3e068e957716eac56a755f44268c07c5957 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 10 Nov 2015 03:26:51 +0200 Subject: [PATCH 137/908] Place images in their own dir --- doc/ci/{quick_start => }/enable_ci.md | 10 +++++----- doc/ci/{quick_start => img}/builds_tab.png | Bin .../ci_service_disabled.png | Bin .../{quick_start => img}/ci_service_enabled.png | Bin .../ci_service_mark_active.png | Bin doc/ci/quick_start/README.md | 16 +++++++--------- doc/ci/quick_start/{ => img}/build_status.png | Bin doc/ci/quick_start/{ => img}/commit_status.png | Bin doc/ci/quick_start/{ => img}/new_commit.png | Bin doc/ci/quick_start/{ => img}/projects.png | Bin doc/ci/quick_start/{ => img}/runners.png | Bin .../quick_start/{ => img}/runners_activated.png | Bin 12 files changed, 12 insertions(+), 14 deletions(-) rename doc/ci/{quick_start => }/enable_ci.md (80%) rename doc/ci/{quick_start => img}/builds_tab.png (100%) rename doc/ci/{quick_start => img}/ci_service_disabled.png (100%) rename doc/ci/{quick_start => img}/ci_service_enabled.png (100%) rename doc/ci/{quick_start => img}/ci_service_mark_active.png (100%) rename doc/ci/quick_start/{ => img}/build_status.png (100%) rename doc/ci/quick_start/{ => img}/commit_status.png (100%) rename doc/ci/quick_start/{ => img}/new_commit.png (100%) rename doc/ci/quick_start/{ => img}/projects.png (100%) rename doc/ci/quick_start/{ => img}/runners.png (100%) rename doc/ci/quick_start/{ => img}/runners_activated.png (100%) diff --git a/doc/ci/quick_start/enable_ci.md b/doc/ci/enable_ci.md similarity index 80% rename from doc/ci/quick_start/enable_ci.md rename to doc/ci/enable_ci.md index 42635e35d5..01c684dabe 100644 --- a/doc/ci/quick_start/enable_ci.md +++ b/doc/ci/enable_ci.md @@ -7,14 +7,14 @@ First, head over your project's page that you would like to enable GitLab CI for. If you can see the **Builds** tab in the sidebar, then GitLab CI is already enabled and you can skip reading the rest of this guide. -![Builds tab](builds_tab.png) +![Builds tab](img/builds_tab.png) If not, there are two ways to enable it in your project. ## Use .gitlab-ci.yml to enable GitLab CI GitLab CI will be automatically enabled if you just push a -[`.gitlab-ci.yml`](../yaml/README.md) in your git repository. GitLab will +[`.gitlab-ci.yml`](yaml/README.md) in your git repository. GitLab will pick up the change immediately and GitLab CI will be enabled for this project. This is the recommended way. @@ -26,14 +26,14 @@ and this is also the way to disable it if needed. Go to **Settings > Services** and search for **GitLab CI**. Its state should be disabled. -![CI service disabled](ci_service_disabled.png) +![CI service disabled](img/ci_service_disabled.png) Click on **GitLab CI** to enter its settings, mark it as active and hit **Save**. -![Mark CI service as active](ci_service_mark_active.png) +![Mark CI service as active](img/ci_service_mark_active.png) Do you see that green dot? Then good, the service is now enabled! You can also check its status under **Services**. -![CI service enabled](ci_service_enabled.png) +![CI service enabled](img/ci_service_enabled.png) diff --git a/doc/ci/quick_start/builds_tab.png b/doc/ci/img/builds_tab.png similarity index 100% rename from doc/ci/quick_start/builds_tab.png rename to doc/ci/img/builds_tab.png diff --git a/doc/ci/quick_start/ci_service_disabled.png b/doc/ci/img/ci_service_disabled.png similarity index 100% rename from doc/ci/quick_start/ci_service_disabled.png rename to doc/ci/img/ci_service_disabled.png diff --git a/doc/ci/quick_start/ci_service_enabled.png b/doc/ci/img/ci_service_enabled.png similarity index 100% rename from doc/ci/quick_start/ci_service_enabled.png rename to doc/ci/img/ci_service_enabled.png diff --git a/doc/ci/quick_start/ci_service_mark_active.png b/doc/ci/img/ci_service_mark_active.png similarity index 100% rename from doc/ci/quick_start/ci_service_mark_active.png rename to doc/ci/img/ci_service_mark_active.png diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index d45ad3e5e2..ce05a6f417 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -16,23 +16,21 @@ In brief, the steps needed to have a working CI can be summed up to: 1. Configure a Runner From there on, on every push to your git repository the build will be -automagically started by the runner and will appear under the project's `/builds` -page. +automagically started by the runner and will appear under the project's +`/builds` page. Now, let's break it down to pieces and work on solving the GitLab CI puzzle. -## 1. Enable GitLab CI - -After creating a new project, the first thing to do is enable the **GitLab CI** -service in your project's settings if it isn't already enabled. - -Read [how to enable the GitLab CI service](enable_ci.md). - ## 1. Creating a `.gitlab-ci.yml` file **GitLab CI** service is enabled automatically on the first push of a `.gitlab-ci.yml` file in your repository and this is the recommended way. +For other methods read [how to enable the GitLab CI service](../enable_ci.md). + + **GitLab CI** service is enabled automatically on the first push of a + `.gitlab-ci.yml` file in your repository and this is the recommended way. + For other methods read [how to enable the GitLab CI service](../enable_ci.md). ### What is `.gitlab-ci.yml` diff --git a/doc/ci/quick_start/build_status.png b/doc/ci/quick_start/img/build_status.png similarity index 100% rename from doc/ci/quick_start/build_status.png rename to doc/ci/quick_start/img/build_status.png diff --git a/doc/ci/quick_start/commit_status.png b/doc/ci/quick_start/img/commit_status.png similarity index 100% rename from doc/ci/quick_start/commit_status.png rename to doc/ci/quick_start/img/commit_status.png diff --git a/doc/ci/quick_start/new_commit.png b/doc/ci/quick_start/img/new_commit.png similarity index 100% rename from doc/ci/quick_start/new_commit.png rename to doc/ci/quick_start/img/new_commit.png diff --git a/doc/ci/quick_start/projects.png b/doc/ci/quick_start/img/projects.png similarity index 100% rename from doc/ci/quick_start/projects.png rename to doc/ci/quick_start/img/projects.png diff --git a/doc/ci/quick_start/runners.png b/doc/ci/quick_start/img/runners.png similarity index 100% rename from doc/ci/quick_start/runners.png rename to doc/ci/quick_start/img/runners.png diff --git a/doc/ci/quick_start/runners_activated.png b/doc/ci/quick_start/img/runners_activated.png similarity index 100% rename from doc/ci/quick_start/runners_activated.png rename to doc/ci/quick_start/img/runners_activated.png From e70fb9cf75d08e8439d94637726dbee7e2324ab6 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 10 Nov 2015 03:49:32 +0200 Subject: [PATCH 138/908] New images for the commit build status --- doc/ci/quick_start/README.md | 2 -- doc/ci/quick_start/img/new_commit.png | Bin 47527 -> 9033 bytes .../img/single_commit_status_pending.png | Bin 0 -> 36431 bytes doc/ci/quick_start/img/status_pending.png | Bin 0 -> 19782 bytes 4 files changed, 2 deletions(-) create mode 100644 doc/ci/quick_start/img/single_commit_status_pending.png create mode 100644 doc/ci/quick_start/img/status_pending.png diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index ce05a6f417..073146b9d1 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -120,8 +120,6 @@ Notice that there are two jobs pending which are named after what we wrote in `.gitlab-ci.yml`. The red triangle indicates that there is no Runner configured yet for these builds. -The next step is to configure a Runner so that it picks the pending jobs. - ## 2. Configuring a Runner In GitLab, Runners run the builds that you define in `.gitlab-ci.yml`. diff --git a/doc/ci/quick_start/img/new_commit.png b/doc/ci/quick_start/img/new_commit.png index 3839e893c177ee81865b6791d2b81b664a4dcfc0..3d3c9d5c0bd078333a7bc1e670b9a7dc736a2fcc 100644 GIT binary patch literal 9033 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 A-T(jq literal 47527 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 Ot1Ig$RVrA#`hNgeb)0Ab literal 0 HcmV?d00001 diff --git a/doc/ci/quick_start/img/status_pending.png b/doc/ci/quick_start/img/status_pending.png new file mode 100644 index 0000000000000000000000000000000000000000..a049ec2a5ba560c222078da1c45b3e358ce15dd3 GIT binary patch literal 19782 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 n<4{~;*@nc!x6vFp0QR3*pmN!P_v Date: Tue, 10 Nov 2015 21:16:12 +0200 Subject: [PATCH 139/908] Final touches for the quick start quide --- doc/ci/quick_start/README.md | 6 ++++-- doc/ci/quick_start/img/build_log.png | Bin 0 -> 63272 bytes doc/ci/quick_start/img/builds_status.png | Bin 0 -> 49121 bytes doc/ci/quick_start/img/runners_activated.png | Bin 60769 -> 27597 bytes 4 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 doc/ci/quick_start/img/build_log.png create mode 100644 doc/ci/quick_start/img/builds_status.png diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 073146b9d1..7771d78d91 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -16,7 +16,7 @@ In brief, the steps needed to have a working CI can be summed up to: 1. Configure a Runner From there on, on every push to your git repository the build will be -automagically started by the runner and will appear under the project's +automagically started by the Runner and will appear under the project's `/builds` page. Now, let's break it down to pieces and work on solving the GitLab CI puzzle. @@ -120,6 +120,8 @@ Notice that there are two jobs pending which are named after what we wrote in `.gitlab-ci.yml`. The red triangle indicates that there is no Runner configured yet for these builds. +The next step is to configure a Runner so that it picks the pending jobs. + ## 2. Configuring a Runner In GitLab, Runners run the builds that you define in `.gitlab-ci.yml`. @@ -136,8 +138,8 @@ Find more information about different Runners in the You can find whether any Runners are assigned to your project by going to **Settings** -> **Runners**. - Setting up a Runner is easy and straightforward. The official Runner supported + by GitLab is written in Go and can be found at . diff --git a/doc/ci/quick_start/img/build_log.png b/doc/ci/quick_start/img/build_log.png new file mode 100644 index 0000000000000000000000000000000000000000..89e6cd40cb61d890b7ee1ce0137c738a18526e74 GIT binary patch literal 63272 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 o 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 E1r+N0pa1{> literal 0 HcmV?d00001 diff --git a/doc/ci/quick_start/img/runners_activated.png b/doc/ci/quick_start/img/runners_activated.png index c934bd12f41dc17d10eb131b233f630b18fc1761..eafcfd6ecd5daa3ebdea96496b2126f756ace99e 100644 GIT binary patch literal 27597 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 DV4kVw From aea5b72faafa6911377cb0c9fc0801269ac59e1e Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 11 Nov 2015 09:57:21 +0200 Subject: [PATCH 140/908] Remove old images --- doc/ci/quick_start/img/build_status.png | Bin 62140 -> 0 bytes doc/ci/quick_start/img/commit_status.png | Bin 33492 -> 0 bytes doc/ci/quick_start/img/projects.png | Bin 37014 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 doc/ci/quick_start/img/build_status.png delete mode 100644 doc/ci/quick_start/img/commit_status.png delete mode 100644 doc/ci/quick_start/img/projects.png diff --git a/doc/ci/quick_start/img/build_status.png b/doc/ci/quick_start/img/build_status.png deleted file mode 100644 index 333259e6acd301b579ebb7b7682b1df1e635a61a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62140 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�n!5^ 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 diff --git a/doc/ci/quick_start/img/commit_status.png b/doc/ci/quick_start/img/commit_status.png deleted file mode 100644 index 725b79e6f91d1de8e8240c28f911b2753d92222a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33492 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 diff --git a/doc/ci/quick_start/img/projects.png b/doc/ci/quick_start/img/projects.png deleted file mode 100644 index 0b3430a69dbfafbe71d780c13025226499e82407..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37014 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 Date: Tue, 1 Dec 2015 10:05:24 +0200 Subject: [PATCH 141/908] Clean up quick start quide [ci skip] * Remove references to enabling CI since it it will be deprecated * Revert changes in yaml/README.md, refine it in a separate MR --- doc/ci/enable_ci.md | 39 ---------------- doc/ci/img/ci_service_disabled.png | Bin 3598 -> 0 bytes doc/ci/img/ci_service_enabled.png | Bin 3545 -> 0 bytes doc/ci/img/ci_service_mark_active.png | Bin 17193 -> 0 bytes doc/ci/quick_start/README.md | 61 ++++++++++++-------------- doc/ci/quick_start/img/runners.png | Bin 123048 -> 0 bytes doc/ci/yaml/README.md | 30 ++++++------- 7 files changed, 42 insertions(+), 88 deletions(-) delete mode 100644 doc/ci/enable_ci.md delete mode 100644 doc/ci/img/ci_service_disabled.png delete mode 100644 doc/ci/img/ci_service_enabled.png delete mode 100644 doc/ci/img/ci_service_mark_active.png delete mode 100644 doc/ci/quick_start/img/runners.png diff --git a/doc/ci/enable_ci.md b/doc/ci/enable_ci.md deleted file mode 100644 index 01c684dabe..0000000000 --- a/doc/ci/enable_ci.md +++ /dev/null @@ -1,39 +0,0 @@ -# Enable GitLab CI - -GitLab Continuous Integration (CI) is fully integrated into GitLab itself as -of [version 8.0](https://about.gitlab.com/2015/09/22/gitlab-8-0-released/). - -First, head over your project's page that you would like to enable GitLab CI -for. If you can see the **Builds** tab in the sidebar, then GitLab CI is -already enabled and you can skip reading the rest of this guide. - -![Builds tab](img/builds_tab.png) - -If not, there are two ways to enable it in your project. - -## Use .gitlab-ci.yml to enable GitLab CI - -GitLab CI will be automatically enabled if you just push a -[`.gitlab-ci.yml`](yaml/README.md) in your git repository. GitLab will -pick up the change immediately and GitLab CI will be enabled for this project. -This is the recommended way. - -## Manually enable GitLab CI - -The second way is to manually enable it in the project's **Services** settings -and this is also the way to disable it if needed. - -Go to **Settings > Services** and search for **GitLab CI**. Its state should -be disabled. - -![CI service disabled](img/ci_service_disabled.png) - -Click on **GitLab CI** to enter its settings, mark it as active and hit -**Save**. - -![Mark CI service as active](img/ci_service_mark_active.png) - -Do you see that green dot? Then good, the service is now enabled! You can also -check its status under **Services**. - -![CI service enabled](img/ci_service_enabled.png) diff --git a/doc/ci/img/ci_service_disabled.png b/doc/ci/img/ci_service_disabled.png deleted file mode 100644 index 351de4267a4a32318a065cf3797e7b2ec223135b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3598 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 diff --git a/doc/ci/img/ci_service_enabled.png b/doc/ci/img/ci_service_enabled.png deleted file mode 100644 index dfe7488c1ba50d0bbf43523a20ab64f6dceb0eff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3545 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 **CI settings** in your project. +the link under **Settings > CI settings** in your project. For more information and a complete `.gitlab-ci.yml` syntax, please check [the documentation on .gitlab-ci.yml](../yaml/README.md). @@ -122,13 +118,13 @@ yet for these builds. The next step is to configure a Runner so that it picks the pending jobs. -## 2. Configuring a Runner +## Configuring a Runner In GitLab, Runners run the builds that you define in `.gitlab-ci.yml`. A Runner can be a virtual machine, a VPS, a bare-metal machine, a docker container or even a cluster of containers. GitLab and the Runners communicate through an API, so the only needed requirement is that the machine on which the -Runner is configured to have Internet access. +Runner is configured to has Internet access. A Runner can be specific to a certain project or serve multiple projects in GitLab. If it serves all projects it's called a _Shared Runner_. @@ -137,22 +133,23 @@ Find more information about different Runners in the [Runners](../runners/README.md) documentation. You can find whether any Runners are assigned to your project by going to -**Settings** -> **Runners**. -Setting up a Runner is easy and straightforward. The official Runner supported - -by GitLab is written in Go and can be found at +**Settings > Runners**. Setting up a Runner is easy and straightforward. The +official Runner supported by GitLab is written in Go and can be found at . -In order to have a functional Runner you need to: +In order to have a functional Runner you need to follow two steps: 1. [Install it][runner-install] 2. [Configure it](../runners/README.md#registering-a-specific-runner) +Follow the links above to set up your own Runner or use a Shared Runner as +described in the next section. + For other types of unofficial Runners written in other languages, see the [instructions for the various GitLab Runners](https://about.gitlab.com/gitlab-ci/#gitlab-runner). Once the Runner has been set up, you should see it on the Runners page of your -project, following **Settings** -> **Runners**. +project, following **Settings > Runners**. ![Activated runners](img/runners_activated.png) @@ -161,15 +158,15 @@ project, following **Settings** -> **Runners**. If you use [GitLab.com](https://gitlab.com/) you can use **Shared Runners** provided by GitLab Inc. -These are special virtual machines that are run on GitLab's infrastructure that -can build any project. +These are special virtual machines that run on GitLab's infrastructure and can +build any project. To enable **Shared Runners** you have to go to your project's -**Settings** -> **Runners** and click **Enable shared runners**. +**Settings > Runners** and click **Enable shared runners**. [Read more on Shared Runners](../runners/README.md). -## 3. Seeing the status of your build +## Seeing the status of your build After configuring the Runner succesfully, you should see the status of your last commit change from _pending_ to either _running_, _success_ or _failed_. @@ -194,7 +191,7 @@ Awesome! You started using CI in GitLab! Next you can look into doing more with the CI. Many people are using GitLab to package, containerize, test and deploy software. -We have a number of [examples](../examples/README.md) available. +Visit our various languages examples at . [runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#installation [blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/ diff --git a/doc/ci/quick_start/img/runners.png b/doc/ci/quick_start/img/runners.png deleted file mode 100644 index 25b4046bc00a8843d2dc68125ce8a9864e541df3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123048 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

+ "
#{user.name}
-
#{user.username}
+
#{user.username || ""}
" formatSelection: (user) -> diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 78fff58d23..5781983c48 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -123,10 +123,16 @@ } .user-result { + min-height: 24px; + .user-image { float: left; } - .user-name { + + &.no-username { + .user-name { + line-height: 24px; + } } } diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 493f370d9a..bfaae223b1 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -44,9 +44,10 @@ module IssuesHelper end def bulk_update_milestone_options - options_for_select([['None (backlog)', -1]]) + - options_from_collection_for_select(project_active_milestones, 'id', - 'title', params[:milestone_id]) + milestones = project_active_milestones.to_a + milestones.unshift(Milestone::None) + + options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id]) end def milestone_options(object) diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 7e54d4d1b5..7e175d0de8 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -15,12 +15,14 @@ module SelectsHelper html = { class: css_class, - 'data-placeholder' => placeholder, - 'data-null-user' => null_user, - 'data-any-user' => any_user, - 'data-email-user' => email_user, - 'data-first-user' => first_user, - 'data-current-user' => current_user + data: { + placeholder: placeholder, + null_user: null_user, + any_user: any_user, + email_user: email_user, + first_user: first_user, + current_user: current_user + } } unless opts[:scope] == :all diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index d1231438ee..c159e85ef2 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -31,11 +31,11 @@ .issues-other-filters .filter-item.inline = users_select_tag(:assignee_id, selected: params[:assignee_id], - placeholder: 'Assignee', class: 'trigger-submit', any_user: true, null_user: true, first_user: true, current_user: true) + placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true) .filter-item.inline = users_select_tag(:author_id, selected: params[:author_id], - placeholder: 'Author', class: 'trigger-submit', any_user: true, first_user: true, current_user: true) + placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true) .filter-item.inline.milestone-filter = select_tag('milestone_title', projects_milestones_options, From 5d0ab2b9e9854733ba174cd3d403600f45c7dbd1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:12:16 +0100 Subject: [PATCH 186/908] Make style of select2 box more consistent with controls --- app/assets/stylesheets/framework/selects.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 5781983c48..5e5ae3d0af 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -15,6 +15,16 @@ border-left: none; padding-top: 5px; } + + .select2-chosen { + color: $gl-text-color; + } + + &.select2-default { + .select2-chosen { + color: #999; + } + } } } @@ -23,6 +33,7 @@ border: 1px solid #e7e9ed; } + .select2-drop { @include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px); @include border-radius (0px); From 68ee01cfa0de7656b09eb743aabccf7ca8746d57 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:12:46 +0100 Subject: [PATCH 187/908] Use placeholders rather than blank options in bulk issue edit selects. --- app/views/shared/issuable/_filter.html.haml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index c159e85ef2..ac6c248ccf 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -53,12 +53,16 @@ - if controller.controller_name == 'issues' .issues_bulk_update.hide = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do - = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status", class: 'form-control') - = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true) - = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") + .filter-item.inline + = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), include_blank: true, data: { placeholder: "Status" }) + .filter-item.inline + = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true) + .filter-item.inline + = select_tag('update[milestone_id]', bulk_update_milestone_options, include_blank: true, data: { placeholder: "Milestone" }) = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] - = button_tag "Update issues", class: "btn update_selected_issues btn-save" + .filter-item.inline + = button_tag "Update issues", class: "btn update_selected_issues btn-save" :javascript new UsersSelect(); From d399ffa244511b0bfe0b4de746b6775a3a456465 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:18:46 +0100 Subject: [PATCH 188/908] Use "Assigned to USER" tooltip in issue list item --- app/helpers/projects_helper.rb | 5 +++-- app/views/projects/issues/_issue.html.haml | 2 +- app/views/projects/merge_requests/_merge_request.html.haml | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c0c51aae03..48729e5260 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -21,7 +21,7 @@ module ProjectsHelper end def link_to_member(project, author, opts = {}) - default_opts = { avatar: true, name: true, size: 16, author_class: 'author' } + default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" } opts = default_opts.merge(opts) return "(deleted)" unless author @@ -39,7 +39,8 @@ module ProjectsHelper if opts[:name] link_to(author_html, user_path(author), class: "author_link").html_safe else - link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => sanitize(author.name) } ).html_safe + title = opts[:title].sub(":name", sanitize(author.name)) + link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => title, container: 'body' } ).html_safe end end diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index d7657ee7e4..1d452dc601 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -14,7 +14,7 @@ %span CLOSED - if issue.assignee - = link_to_member(@project, issue.assignee, name: false) + = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name") - note_count = issue.notes.user.count - if note_count > 0   diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 83e8ad1198..5842d7ff16 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -20,7 +20,7 @@ - note_count = merge_request.mr_and_commit_notes.user.count - if merge_request.assignee   - = link_to_member(merge_request.source_project, merge_request.assignee, name: false) + = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name") - if note_count > 0   %span From dc809983a4d5c9f61af9ca4392010d243271f7d3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:18:59 +0100 Subject: [PATCH 189/908] Link issue/MR list item comments counter to comments --- app/views/projects/issues/_issue.html.haml | 8 ++++---- .../projects/merge_requests/_merge_request.html.haml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 1d452dc601..4f6ff20caf 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -18,13 +18,13 @@ - note_count = issue.notes.user.count - if note_count > 0   - %span - %i.fa.fa-comments + = link_to issue_path(issue) + "#notes" do + = icon('comments') = note_count - else   - %span.issue-no-comments - %i.fa.fa-comments + = link_to issue_path(issue) + "#notes", class: "issue-no-comments" do + = icon('comments') = 0 .issue-info diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 5842d7ff16..2f2ebc0894 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -23,13 +23,13 @@ = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name") - if note_count > 0   - %span - %i.fa.fa-comments + = link_to merge_request_path(merge_request) + "#notes" do + = icon('comments') = note_count - else   - %span.merge-request-no-comments - %i.fa.fa-comments + = link_to merge_request_path(merge_request) + "#notes", class: "merge-request-no-comments" do + = icon('comments') = 0 .merge-request-info From 08ee38a1c5d55d66070aa6430afa991a395a7da0 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:19:25 +0100 Subject: [PATCH 190/908] Link milestone item to issues with that milestone --- app/views/projects/issues/_issue.html.haml | 8 +++++--- .../projects/merge_requests/_merge_request.html.haml | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 4f6ff20caf..2e50b60331 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -28,11 +28,13 @@ = 0 .issue-info - = "#{issue.to_reference} opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe + #{issue.to_reference} · + opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} + by #{link_to_member(@project, issue.author, avatar: false)} - if issue.milestone   - %span - %i.fa.fa-clock-o + = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do + = icon('clock-o') = issue.milestone.title - if issue.tasks? %span.task-status diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 2f2ebc0894..4940bc7843 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -33,11 +33,13 @@ = 0 .merge-request-info - = "##{merge_request.iid} opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)}".html_safe + \##{merge_request.iid} · + opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} + by #{link_to_member(@project, merge_request.author, avatar: false)} - if merge_request.milestone_id?   - %span - %i.fa.fa-clock-o + = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do + = icon('clock-o') = merge_request.milestone.title - if merge_request.target_project.default_branch != merge_request.target_branch   From fbe6432934973021acddcff1d3b0fd3ed5844d2b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:20:03 +0100 Subject: [PATCH 191/908] Move labels to second line of issue/MR list item --- app/views/projects/issues/_issue.html.haml | 8 +++++--- .../projects/merge_requests/_merge_request.html.haml | 12 +++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 2e50b60331..1eb71990e5 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -6,9 +6,6 @@ .issue-title %span.issue-title-text = link_to_gfm issue.title, issue_path(issue), class: "row_title" - .issue-labels - - issue.labels.each do |label| - = link_to_label(label, project: issue.project) .pull-right.light - if issue.closed? %span @@ -36,7 +33,12 @@ = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do = icon('clock-o') = issue.milestone.title + - if issue.labels.any? +   + - issue.labels.each do |label| + = link_to_label(label, project: issue.project) - if issue.tasks? +   %span.task-status = issue.task_status diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 4940bc7843..8246a432c7 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -3,19 +3,16 @@ .merge-request-title %span.merge-request-title-text = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" - .merge-request-labels - - merge_request.labels.each do |label| - = link_to_label(label, project: merge_request.project) .pull-right.light - if ci_commit = render_ci_status(ci_commit) - if merge_request.merged? %span - %i.fa.fa-check + = icon('check') MERGED - elsif merge_request.closed? %span - %i.fa.fa-ban + = icon('ban') CLOSED - note_count = merge_request.mr_and_commit_notes.user.count - if merge_request.assignee @@ -41,12 +38,17 @@ = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do = icon('clock-o') = merge_request.milestone.title + - if merge_request.labels.any? +   + - merge_request.labels.each do |label| + = link_to_label(label, project: merge_request.project) - if merge_request.target_project.default_branch != merge_request.target_branch   %span %i.fa.fa-code-fork = merge_request.target_branch - if merge_request.tasks? +   %span.task-status = merge_request.task_status From ecd9f5ec027a9801bf39a32facf66fe13667b2ca Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:20:43 +0100 Subject: [PATCH 192/908] Add "No Milestone" option to issue milestone select --- app/helpers/issues_helper.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 493f370d9a..efd6418c70 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -50,8 +50,10 @@ module IssuesHelper end def milestone_options(object) - options_from_collection_for_select(object.project.milestones.active, - 'id', 'title', object.milestone_id) + milestones = object.project.milestones.active.to_a + milestones.unshift(Milestone::None) + + options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id) end def issue_box_class(item) From 89488a7cf930c41028a6e2e842cf0b6b8133e78b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:21:03 +0100 Subject: [PATCH 193/908] Move "Please review the contribution guide" next to MR/issue Submit button --- app/views/shared/issuable/_form.html.haml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 0fc74d7d2b..2e1656954e 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -94,15 +94,17 @@ = link_to 'Change branches', mr_change_branches_path(@merge_request) .form-actions - - if !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) && !issuable.persisted? - %p - Please review the - %strong #{link_to 'guidelines for contribution', guide_url} - to this repository. - if issuable.new_record? = f.submit "Submit new #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' - else = f.submit 'Save changes', class: 'btn btn-save' + + - if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) + .inline.prepend-left-10 + Please review the + %strong #{link_to 'contribution guidelines', guide_url} + for this project. + - if issuable.new_record? - cancel_project = issuable.source_project - else From 6aa43974d4bb674f457833ba6f314bbd7c8a2578 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:21:23 +0100 Subject: [PATCH 194/908] Move subscription info below label info in issue/MR sidebar --- app/views/projects/issues/_discussion.html.haml | 7 ------- app/views/projects/merge_requests/_discussion.html.haml | 7 ------- app/views/shared/issuable/_context.html.haml | 8 ++++++++ 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 8f0a1ed9be..b5f522f207 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -29,10 +29,3 @@ .issuable-affix .context = render 'shared/issuable/context', issuable: @issue - - - if @issue.labels.any? - .issuable-context-title - %label Labels - .issue-show-labels - - @issue.labels.each do |label| - = link_to_label(label) diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 2b3c3eff5e..00b72e5a1a 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -26,10 +26,3 @@ .issuable-affix .context = render 'shared/issuable/context', issuable: @merge_request - - - if @merge_request.labels.any? - .issuable-context-title - %label Labels - .merge-request-show-labels - - @merge_request.labels.each do |label| - = link_to_label(label) diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml index be66256c7b..5d00c87108 100644 --- a/app/views/shared/issuable/_context.html.haml +++ b/app/views/shared/issuable/_context.html.haml @@ -29,6 +29,14 @@ = hidden_field_tag :issuable_context = f.submit class: 'btn hide' + - if issuable.labels.any? + %div.prepend-top-default.clearfix + .issuable-context-title + %label Labels + .merge-request-show-labels + - issuable.labels.each do |label| + = link_to_label(label) + - if current_user - subscribed = issuable.subscribed?(current_user) %div.prepend-top-20.clearfix From 3922d6c22c7c148e100eac1635a8d3f0565585db Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:21:39 +0100 Subject: [PATCH 195/908] Move (Un)subscribe button below subscription status --- app/views/shared/issuable/_context.html.haml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml index 5d00c87108..0b87f11c4d 100644 --- a/app/views/shared/issuable/_context.html.haml +++ b/app/views/shared/issuable/_context.html.haml @@ -41,17 +41,16 @@ - subscribed = issuable.subscribed?(current_user) %div.prepend-top-20.clearfix .issuable-context-title - %label - Subscription: - %button.btn.btn-block.subscribe-button{:type => 'button'} - = icon('eye') - %span= subscribed ? 'Unsubscribe' : 'Subscribe' + %label Subscription - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' .subscription-status{data: {status: subscribtion_status}} .description-block.unsubscribed{class: ( 'hidden' if subscribed )} You're not receiving notifications from this thread. .description-block.subscribed{class: ( 'hidden' unless subscribed )} You're receiving notifications because you're subscribed to this thread. + %button.btn.btn-block.subscribe-button{:type => 'button'} + = icon('eye') + %span= subscribed ? 'Unsubscribe' : 'Subscribe' :javascript new Subscription("#{toggle_subscription_path(issuable)}"); From e3233016be0a94b88e94700507760c9f672661f4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:22:11 +0100 Subject: [PATCH 196/908] Use "opened by" instead of "created by" in issue/MR header, like in list --- app/views/projects/issues/show.html.haml | 3 ++- app/views/projects/merge_requests/show/_mr_title.html.haml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index f01bf2505d..e2de17cee6 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -11,7 +11,8 @@ Open %span.issue-id Issue ##{@issue.iid} %span.creator - · created by #{link_to_member(@project, @issue.author, size: 24)} + · + opened by #{link_to_member(@project, @issue.author, size: 24)} · = time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago') - if @issue.updated_at != @issue.created_at diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 2bf9cd597a..4dfe46e2b8 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -4,7 +4,7 @@ %span.issue-id Merge Request ##{@merge_request.iid} %span.creator · - created by #{link_to_member(@project, @merge_request.author, size: 24)} + opened by #{link_to_member(@project, @merge_request.author, size: 24)} · = time_ago_with_tooltip(@merge_request.created_at) - if @merge_request.updated_at != @merge_request.created_at From 7a65cb3abf63bcd87856c6220e87c7fccfdb8a1f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:22:33 +0100 Subject: [PATCH 197/908] Don't reset target branch when choosing to "Change branches" in MR --- app/helpers/merge_requests_helper.rb | 5 +++-- app/views/projects/merge_requests/new.html.haml | 2 +- features/steps/project/forked_merge_requests.rb | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index b804d4f4e3..cc4243e155 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -49,8 +49,9 @@ module MergeRequestsHelper source_project_id: @merge_request.source_project_id, target_project_id: @merge_request.target_project_id, source_branch: @merge_request.source_branch, - target_branch: nil - } + target_branch: @merge_request.target_branch, + }, + change_branches: true ) end diff --git a/app/views/projects/merge_requests/new.html.haml b/app/views/projects/merge_requests/new.html.haml index 9fdde80c6d..d259968030 100644 --- a/app/views/projects/merge_requests/new.html.haml +++ b/app/views/projects/merge_requests/new.html.haml @@ -1,7 +1,7 @@ - page_title "New Merge Request" = render "header_title" -- if @merge_request.can_be_created +- if @merge_request.can_be_created && !params[:change_branches] = render 'new_submit' - else = render 'new_compare' diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 2a333222fb..789befc9df 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -112,7 +112,6 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I fill out an invalid "Merge Request On Forked Project" merge request' do - select "Select branch", from: "merge_request_target_branch" expect(find(:select, "merge_request_source_project_id", {}).value).to eq @forked_project.id.to_s expect(find(:select, "merge_request_target_project_id", {}).value).to eq @project.id.to_s expect(find(:select, "merge_request_source_branch", {}).value).to eq "" From 1df2908b27bbce4a0c67139dc3358799ffa55550 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:23:36 +0100 Subject: [PATCH 198/908] Link to branches from MR "Request to merge X into Y" sentence --- app/views/projects/merge_requests/_show.html.haml | 5 +++-- app/views/projects/merge_requests/widget/_merged.html.haml | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index eeaa72ed21..c52d870f6e 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -26,9 +26,10 @@ %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) .normal %span Request to merge - %span.label-branch #{source_branch_with_namespace(@merge_request)} + = link_to source_branch_with_namespace(@merge_request), namespace_project_commits_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), class: "label-branch" %span into - %span.label-branch #{@merge_request.target_branch} + = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do + = @merge_request.target_branch = render "projects/merge_requests/show/how_to_merge" = render "projects/merge_requests/widget/show.html.haml" diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index a788fcea23..ac08e0b498 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -10,7 +10,8 @@ - if !@merge_request.source_branch_exists? = succeed '.' do The changes were merged into - %span.label-branch= @merge_request.target_branch + = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do + = @merge_request.target_branch The source branch has been removed. - elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) @@ -18,7 +19,8 @@ %p = succeed '.' do The changes were merged into - %span.label-branch= @merge_request.target_branch + = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do + = @merge_request.target_branch You can remove the source branch now. = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do %i.fa.fa-times From 6c54823dce9aecf54f78b41180ec75efbc1391f4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:24:29 +0100 Subject: [PATCH 199/908] Use select2 placeholder instead of blank option --- .../projects/merge_requests/_new_compare.html.haml | 4 ++-- app/views/shared/issuable/_context.html.haml | 2 +- app/views/shared/issuable/_form.html.haml | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index d9eff1f932..f5a512d385 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -10,7 +10,7 @@ .panel-body = f.select(:source_project_id, [[@merge_request.source_project_path,@merge_request.source_project.id]] , {}, { class: 'source_project select2 span3', disabled: @merge_request.persisted?, required: true })   - = f.select(:source_branch, @merge_request.source_branches, { include_blank: "Select branch" }, {class: 'source_branch select2 span2', required: true}) + = f.select(:source_branch, @merge_request.source_branches, { include_blank: true }, { class: 'source_branch select2 span2', required: true, data: { placeholder: "Select source branch" } }) .panel-footer .mr_source_commit @@ -22,7 +22,7 @@ - projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project] = f.select(:target_project_id, options_from_collection_for_select(projects, 'id', 'path_with_namespace', f.object.target_project_id), {}, { class: 'target_project select2 span3', disabled: @merge_request.persisted?, required: true })   - = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, {class: 'target_branch select2 span2', required: true}) + = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', required: true, data: { placeholder: "Select target branch" } }) .panel-footer .mr_target_commit diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml index 0b87f11c4d..e6d753a8b7 100644 --- a/app/views/shared/issuable/_context.html.haml +++ b/app/views/shared/issuable/_context.html.haml @@ -25,7 +25,7 @@ none .issuable-context-selectbox - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - = f.select(:milestone_id, milestone_options(issuable), { include_blank: 'Select milestone' }, {class: 'select2 select2-compact js-select2 js-milestone'}) + = f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }}) = hidden_field_tag :issuable_context = f.submit class: 'btn hide' diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 2e1656954e..942551c268 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -39,9 +39,9 @@ Assign to .col-sm-10 = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", - placeholder: 'Select a user', class: 'custom-form-control', null_user: true, + placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, selected: issuable.assignee_id, project: @target_project || @project, - first_user: true, current_user: true) + first_user: true, current_user: true, include_blank: true)   = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' .form-group @@ -52,7 +52,7 @@ .col-sm-10 - if milestone_options(issuable).present? = f.select(:milestone_id, milestone_options(issuable), - { include_blank: 'Select milestone' }, { class: 'select2' }) + { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) - else .prepend-top-10 %span.light No open milestones available. @@ -66,7 +66,7 @@ .col-sm-10 - if issuable.project.labels.any? = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, - { selected: issuable.label_ids }, multiple: true, class: 'select2' + { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } - else .prepend-top-10 %span.light No labels yet. @@ -88,7 +88,7 @@ %i.fa.fa-code-fork Target Branch .col-sm-10 - = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record? }) + = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} }) - if @merge_request.new_record? %p.help-block = link_to 'Change branches', mr_change_branches_path(@merge_request) From 3b03987a27f3b6b720251b02abfadc4e9ba06fe8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:24:44 +0100 Subject: [PATCH 200/908] Redesign multiple select2 options --- app/assets/stylesheets/framework/selects.scss | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 78fff58d23..f541444401 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -48,17 +48,38 @@ color: #313236; } +.select2-container-multi { + .select2-choices { + @include border-radius(2px); + border-color: $input-border; + background: white; + padding-left: $gl-padding / 2; -.select2-container-multi .select2-choices { - @include border-radius(2px); - border-color: #CCC; -} + .select2-search-field input { + padding: $gl-padding / 2; + font-size: 13px; + height: auto; + font-family: inherit; + font-size: inherit; + } -.select2-container-multi .select2-choices .select2-search-field input { - padding: 8px 14px; - font-size: 13px; - line-height: 18px; - height: auto; + .select2-search-choice { + margin: 8px 0 0 8px; + background: white; + box-shadow: none; + border-color: $input-border; + color: $gl-text-color; + line-height: 15px; + + .select2-search-choice-close { + top: 5px; + } + + &.select2-search-choice-focus { + border-color: $gl-text-color; + } + } + } } .select2-drop-active { From 05c9d882166710a26e27a7b79e7855db47ca43c0 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:25:03 +0100 Subject: [PATCH 201/908] Only show manual merge instructions if current user can merge --- app/views/projects/merge_requests/_show.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index c52d870f6e..09b228b7c0 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -34,8 +34,8 @@ = render "projects/merge_requests/show/how_to_merge" = render "projects/merge_requests/widget/show.html.haml" - - if @merge_request.open? && @merge_request.can_be_merged? - .light.append-bottom-20 + - if @merge_request.open? && @merge_request.source_branch_exists? && @merge_request.can_be_merged? && @merge_request.can_be_merged_by?(current_user) + .light.prepend-top-default You can also accept this merge request manually using the = succeed '.' do = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" From ed74fa73e227b9666f3f38f17b35a5cf8328fa44 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:27:29 +0100 Subject: [PATCH 202/908] Use consistent casing for page titles --- app/views/admin/labels/edit.html.haml | 8 ++------ app/views/admin/labels/new.html.haml | 6 ++---- app/views/admin/users/edit.html.haml | 3 --- app/views/groups/edit.html.haml | 3 +-- app/views/groups/new.html.haml | 7 ++++++- app/views/projects/blob/new.html.haml | 5 ++--- app/views/projects/branches/new.html.haml | 5 +++-- app/views/projects/deploy_keys/new.html.haml | 2 +- app/views/projects/issues/_form.html.haml | 3 --- app/views/projects/issues/edit.html.haml | 6 ++++++ app/views/projects/issues/new.html.haml | 4 ++++ app/views/projects/labels/edit.html.haml | 8 ++------ app/views/projects/labels/new.html.haml | 6 ++---- app/views/projects/merge_requests/_new_compare.html.haml | 3 ++- app/views/projects/merge_requests/_new_submit.html.haml | 2 +- app/views/projects/merge_requests/edit.html.haml | 2 +- app/views/projects/milestones/_form.html.haml | 7 ------- app/views/projects/milestones/edit.html.haml | 6 ++++++ app/views/projects/milestones/new.html.haml | 6 ++++++ app/views/projects/new.html.haml | 7 ++++++- app/views/projects/services/_form.html.haml | 4 ---- app/views/projects/snippets/edit.html.haml | 2 +- app/views/projects/snippets/new.html.haml | 2 +- app/views/projects/tags/new.html.haml | 2 +- app/views/shared/_confirm_modal.html.haml | 3 ++- app/views/snippets/edit.html.haml | 2 +- app/views/snippets/new.html.haml | 2 +- features/steps/project/forked_merge_requests.rb | 2 +- features/steps/project/source/browse_files.rb | 2 +- 29 files changed, 62 insertions(+), 58 deletions(-) diff --git a/app/views/admin/labels/edit.html.haml b/app/views/admin/labels/edit.html.haml index 45c62a7625..309aedcede 100644 --- a/app/views/admin/labels/edit.html.haml +++ b/app/views/admin/labels/edit.html.haml @@ -1,9 +1,5 @@ - page_title "Edit", @label.name, "Labels" -%h3 - Edit label - %span.light #{@label.name} -.back-link - = link_to admin_labels_path do - ← To labels list +%h3.page-title + Edit Label %hr = render 'form' diff --git a/app/views/admin/labels/new.html.haml b/app/views/admin/labels/new.html.haml index 8d298ad20f..0135ad0723 100644 --- a/app/views/admin/labels/new.html.haml +++ b/app/views/admin/labels/new.html.haml @@ -1,7 +1,5 @@ - page_title "New Label" -%h3 New label -.back-link - = link_to admin_labels_path do - ← To labels list +%h3.page-title + New Label %hr = render 'form' diff --git a/app/views/admin/users/edit.html.haml b/app/views/admin/users/edit.html.haml index a8837d74dd..3b6fd71500 100644 --- a/app/views/admin/users/edit.html.haml +++ b/app/views/admin/users/edit.html.haml @@ -1,8 +1,5 @@ - page_title "Edit", @user.name, "Users" %h3.page-title Edit user: #{@user.name} -.back-link - = link_to admin_user_path(@user) do - ← Back to user page %hr = render 'form' diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 57308a661c..b5afb4ae1c 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -3,8 +3,7 @@ .panel.panel-default .panel-heading - %strong= @group.name - group settings: + Group settings .panel-body = form_for @group, html: { multipart: true, class: "form-horizontal" }, authenticity_token: true do |f| - if @group.errors.any? diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 0665cdf387..3e602559ae 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -1,5 +1,10 @@ - page_title 'New Group' -- header_title 'New Group' +- header_title "Groups", dashboard_groups_path + +%h3.page-title + New Group +%hr + = form_for @group, html: { class: 'group-form form-horizontal' } do |f| - if @group.errors.any? .alert.alert-danger diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 1ff6800545..167fa61518 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,9 +1,8 @@ - page_title "New File", @path.presence, @ref = render "header_title" -.gray-content-block.top-block - %h3.page-title - Create New File +%h3.page-title + New File .file-editor = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-requires-input') do diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index f5577042ca..d103a713c5 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -6,8 +6,9 @@ %button{ type: "button", class: "close", "data-dismiss" => "alert"} × = @error %h3.page-title - %i.fa.fa-code-fork - New branch + New Branch +%hr + = form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal js-requires-input" do .form-group = label_tag :branch_name, 'Name for new branch', class: 'control-label' diff --git a/app/views/projects/deploy_keys/new.html.haml b/app/views/projects/deploy_keys/new.html.haml index 01c810aee1..01fab3008a 100644 --- a/app/views/projects/deploy_keys/new.html.haml +++ b/app/views/projects/deploy_keys/new.html.haml @@ -1,5 +1,5 @@ - page_title "New Deploy Key" -%h3.page-title New Deploy key +%h3.page-title New Deploy Key %hr = render 'form' diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index f39bb7d257..e0e26a26da 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -1,7 +1,4 @@ %div.issue-form-holder - %h3.page-title= @issue.new_record? ? "Create Issue" : "Edit Issue ##{@issue.iid}" - %hr - = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form' } do |f| = render 'shared/issuable/form', f: f, issuable: @issue diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml index 53b6f0879c..20216297d2 100644 --- a/app/views/projects/issues/edit.html.haml +++ b/app/views/projects/issues/edit.html.haml @@ -1,2 +1,8 @@ - page_title "Edit", "#{@issue.title} (##{@issue.iid})", "Issues" += render "header_title" + +%h3.page-title + Edit Issue ##{@issue.iid} +%hr + = render "form" diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml index 153447baa1..b317a0c1cf 100644 --- a/app/views/projects/issues/new.html.haml +++ b/app/views/projects/issues/new.html.haml @@ -1,4 +1,8 @@ - page_title "New Issue" = render "header_title" +%h3.page-title + New Issue +%hr + = render "form" diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml index bc4ab0ca27..675a805e12 100644 --- a/app/views/projects/labels/edit.html.haml +++ b/app/views/projects/labels/edit.html.haml @@ -1,11 +1,7 @@ - page_title "Edit", @label.name, "Labels" = render "header_title" -%h3 - Edit label - %span.light #{@label.name} -.back-link - = link_to namespace_project_labels_path(@project.namespace, @project) do - ← To labels list +%h3.page-title + Edit Label %hr = render 'form' diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml index 342ad4f3f9..e20fd7d689 100644 --- a/app/views/projects/labels/new.html.haml +++ b/app/views/projects/labels/new.html.haml @@ -1,9 +1,7 @@ - page_title "New Label" = render "header_title" -%h3 New label -.back-link - = link_to namespace_project_labels_path(@project.namespace, @project) do - ← To labels list +%h3.page-title + New Label %hr = render 'form' diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index d9eff1f932..46e72e9dee 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -1,4 +1,5 @@ -%p.lead Compare branches for new Merge Request +%h3.page-title + New Merge Request = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: new_namespace_project_merge_request_path(@project.namespace, @project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f| .hide.alert.alert-danger.mr-compare-errors diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 6244d3ba0b..9fa9cc5612 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -1,5 +1,5 @@ %h3.page-title - New merge request + New Merge Request %p.slead - source_title, target_title = format_mr_branch_names(@merge_request) From diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml index 303ca0a880..fc62bb5bce 100644 --- a/app/views/projects/merge_requests/edit.html.haml +++ b/app/views/projects/merge_requests/edit.html.haml @@ -2,6 +2,6 @@ = render "header_title" %h3.page-title - = "Edit merge request ##{@merge_request.iid}" + Edit Merge Request ##{@merge_request.iid} %hr = render 'form' diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 24879b19d2..cc29970f07 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -1,10 +1,3 @@ -%h3.page-title= @milestone.new_record? ? "New Milestone" : "Edit Milestone ##{@milestone.iid}" -.back-link - = link_to namespace_project_milestones_path(@project.namespace, @project) do - ← To milestones - -%hr - = form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-requires-input'} do |f| -if @milestone.errors.any? .alert.alert-danger diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index e9dc0b7746..43f8863163 100644 --- a/app/views/projects/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml @@ -1,3 +1,9 @@ - page_title "Edit", @milestone.title, "Milestones" = render "header_title" + +%h3.page-title + Edit Milestone ##{@milestone.iid} + +%hr + = render "form" diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml index 9ba9acb6f7..0d016f7831 100644 --- a/app/views/projects/milestones/new.html.haml +++ b/app/views/projects/milestones/new.html.haml @@ -1,3 +1,9 @@ - page_title "New Milestone" = render "header_title" + +%h3.page-title + New Milestone + +%hr + = render "form" diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index c9d1fc3da2..fa75a62422 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -1,5 +1,10 @@ - page_title 'New Project' -- header_title 'New Project' +- header_title "Projects", root_path + +%h3.page-title + New Project +%hr + .project-edit-container .project-edit-errors = render 'projects/errors' diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index e1823b5119..fecd157c6d 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -4,10 +4,6 @@ %p= @service.description -.back-link - = link_to namespace_project_services_path(@project.namespace, @project) do - ← to services - %hr = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form| diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml index e69f2d9970..dc3ea1fcf1 100644 --- a/app/views/projects/snippets/edit.html.haml +++ b/app/views/projects/snippets/edit.html.haml @@ -2,6 +2,6 @@ = render "header_title" %h3.page-title - Edit snippet + Edit Snippet %hr = render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet), visibility_level: @snippet.visibility_level diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml index 67cd69fd21..e57237991b 100644 --- a/app/views/projects/snippets/new.html.haml +++ b/app/views/projects/snippets/new.html.haml @@ -2,6 +2,6 @@ = render "header_title" %h3.page-title - New snippet + New Snippet %hr = render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet), visibility_level: default_snippet_visibility diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 86aa15dc5b..97abdb239e 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -7,7 +7,7 @@ = @error %h3.page-title - New git tag + New Tag %hr = form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form" do diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml index 2a44817e05..9bc2d33c27 100644 --- a/app/views/shared/_confirm_modal.html.haml +++ b/app/views/shared/_confirm_modal.html.haml @@ -3,7 +3,8 @@ .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h4 Confirmation required + %h3.page-title + Confirmation required .modal-body %p.cred.lead.js-confirm-text diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml index 1a38003566..82f44a9a5c 100644 --- a/app/views/snippets/edit.html.haml +++ b/app/views/snippets/edit.html.haml @@ -1,5 +1,5 @@ - page_title "Edit", @snippet.title, "Snippets" %h3.page-title - Edit snippet + Edit Snippet %hr = render 'shared/snippets/form', url: snippet_path(@snippet), visibility_level: @snippet.visibility_level diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml index a74d5e792a..79e2392490 100644 --- a/app/views/snippets/new.html.haml +++ b/app/views/snippets/new.html.haml @@ -1,5 +1,5 @@ - page_title "New Snippet" %h3.page-title - New snippet + New Snippet %hr = render "shared/snippets/form", url: snippets_path(@snippet), visibility_level: default_snippet_visibility diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 2a333222fb..155a5ff58a 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -41,7 +41,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps click_button "Compare branches" - expect(page).to have_content "New merge request" + expect(page).to have_content "New Merge Request" fill_in "merge_request_title", with: "Merge Request On Forked Project" end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index f40e0f0d52..99e1e9b4af 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -142,7 +142,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I can see new file page' do - expect(page).to have_content "Create New File" + expect(page).to have_content "New File" expect(page).to have_content "Commit message" end From 67119e15c03d4d1e8abd2ce2cfe1b40aba35c709 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:30:12 +0100 Subject: [PATCH 203/908] Use consistent casing for form field labels --- app/views/admin/labels/_form.html.haml | 2 +- app/views/projects/blob/_new_dir.html.haml | 2 +- app/views/projects/branches/new.html.haml | 2 +- app/views/projects/labels/_form.html.haml | 2 +- app/views/projects/tags/new.html.haml | 4 ++-- app/views/shared/_group_form.html.haml | 2 +- app/views/shared/_new_commit_form.html.haml | 3 +-- app/views/shared/issuable/_form.html.haml | 22 ++++++--------------- features/steps/admin/labels.rb | 8 ++++---- spec/features/issues_spec.rb | 2 +- 10 files changed, 19 insertions(+), 30 deletions(-) diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml index a5ace4e7a3..eaa94ed9e3 100644 --- a/app/views/admin/labels/_form.html.haml +++ b/app/views/admin/labels/_form.html.haml @@ -12,7 +12,7 @@ .col-sm-10 = f.text_field :title, class: "form-control", required: true .form-group - = f.label :color, "Background Color", class: 'control-label' + = f.label :color, "Background color", class: 'control-label' .col-sm-10 .input-group .input-group-addon.label-color-preview   diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index 13b5ffd17f..377f0fa012 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -7,7 +7,7 @@ .modal-body = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form' do .form-group - = label_tag :dir_name, 'Directory Name', class: 'control-label' + = label_tag :dir_name, 'Directory name', class: 'control-label' .col-sm-10 = text_field_tag :dir_name, params[:dir_name], placeholder: "Directory name", required: true, class: 'form-control' diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index d103a713c5..efb5298a5e 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -11,7 +11,7 @@ = form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal js-requires-input" do .form-group - = label_tag :branch_name, 'Name for new branch', class: 'control-label' + = label_tag :branch_name, nil, class: 'control-label' .col-sm-10 = text_field_tag :branch_name, params[:branch_name], placeholder: 'enter new branch name', required: true, tabindex: 1, class: 'form-control' .form-group diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index 4cf13492e9..291703bd60 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -12,7 +12,7 @@ .col-sm-10 = f.text_field :title, class: "form-control js-quick-submit", required: true .form-group - = f.label :color, "Background Color", class: 'control-label' + = f.label :color, "Background color", class: 'control-label' .col-sm-10 .input-group .input-group-addon.label-color-preview   diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 97abdb239e..91e0a5493b 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -12,7 +12,7 @@ = form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form" do .form-group - = label_tag :tag_name, 'Name for new tag', class: 'control-label' + = label_tag :tag_name, nil, class: 'control-label' .col-sm-10 = text_field_tag :tag_name, params[:tag_name], placeholder: 'v3.0.1', required: true, tabindex: 1, class: 'form-control' .form-group @@ -21,7 +21,7 @@ = text_field_tag :ref, params[:ref], placeholder: 'master', required: true, tabindex: 2, class: 'form-control' .help-block Branch name or commit SHA .form-group - = label_tag :message, 'Message', class: 'control-label' + = label_tag :message, nil, class: 'control-label' .col-sm-10 = text_field_tag :message, nil, placeholder: 'Enter message.', required: false, tabindex: 3, class: 'form-control' .help-block (Optional) Entering a message will create an annotated tag. diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index c0a9923348..67072b9fc2 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -23,7 +23,7 @@ %li It will change the git path to repositories under this group. .form-group.group-description-holder - = f.label :description, 'Details', class: 'control-label' + = f.label :description, class: 'control-label' .col-sm-10 = f.text_area :description, maxlength: 250, class: 'form-control js-gfm-input', rows: 4 diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 8636341c60..ce52614e86 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -2,8 +2,7 @@ - unless @project.empty_repo? .form-group.branch - = label_tag 'branch', class: 'control-label' do - Branch + = label_tag 'new_branch', 'Target branch', class: 'control-label' .col-sm-10 = text_field_tag 'new_branch', @new_branch || @ref, class: "form-control js-new-branch" diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 0fc74d7d2b..2a5fb534a3 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -30,13 +30,11 @@ = render 'projects/notes/hints' .clearfix .error-alert - %hr - if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) + %hr .form-group .issue-assignee - = f.label :assignee_id, class: 'control-label' do - %i.fa.fa-user - Assign to + = f.label :assignee_id, "Assignee", class: 'control-label' .col-sm-10 = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", placeholder: 'Select a user', class: 'custom-form-control', null_user: true, @@ -46,9 +44,7 @@ = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' .form-group .issue-milestone - = f.label :milestone_id, class: 'control-label' do - %i.fa.fa-clock-o - Milestone + = f.label :milestone_id, "Milestone", class: 'control-label' .col-sm-10 - if milestone_options(issuable).present? = f.select(:milestone_id, milestone_options(issuable), @@ -60,9 +56,7 @@ - if can? current_user, :admin_milestone, issuable.project = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank .form-group - = f.label :label_ids, class: 'control-label' do - %i.fa.fa-tag - Labels + = f.label :label_ids, "Labels", class: 'control-label' .col-sm-10 - if issuable.project.labels.any? = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, @@ -78,15 +72,11 @@ %hr - if @merge_request.new_record? .form-group - = f.label :source_branch, class: 'control-label' do - %i.fa.fa-code-fork - Source Branch + = f.label :source_branch, class: 'control-label' .col-sm-10 = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) .form-group - = f.label :target_branch, class: 'control-label' do - %i.fa.fa-code-fork - Target Branch + = f.label :target_branch, class: 'control-label' .col-sm-10 = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record? }) - if @merge_request.new_record? diff --git a/features/steps/admin/labels.rb b/features/steps/admin/labels.rb index b45d98658b..cb23b86965 100644 --- a/features/steps/admin/labels.rb +++ b/features/steps/admin/labels.rb @@ -45,21 +45,21 @@ class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps step 'I submit new label \'support\'' do visit new_admin_label_path fill_in 'Title', with: 'support' - fill_in 'Background Color', with: '#F95610' + fill_in 'Background color', with: '#F95610' click_button 'Save' end step 'I submit new label \'bug\'' do visit new_admin_label_path fill_in 'Title', with: 'bug' - fill_in 'Background Color', with: '#F95610' + fill_in 'Background color', with: '#F95610' click_button 'Save' end step 'I submit new label with invalid color' do visit new_admin_label_path fill_in 'Title', with: 'support' - fill_in 'Background Color', with: '#12' + fill_in 'Background color', with: '#12' click_button 'Save' end @@ -101,7 +101,7 @@ class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps step 'I change label \'bug\' to \'fix\'' do fill_in 'Title', with: 'fix' - fill_in 'Background Color', with: '#F15610' + fill_in 'Background color', with: '#F15610' click_button 'Save' end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 32fd4065bb..0af5e6fc1a 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -61,7 +61,7 @@ describe 'Issues', feature: true do it 'allows user to select unasigned', js: true do visit edit_namespace_project_issue_path(project.namespace, project, issue) - expect(page).to have_content "Assign to #{@user.name}" + expect(page).to have_content "Assignee #{@user.name}" first('#s2id_issue_assignee_id').click sleep 2 # wait for ajax stuff to complete From ca016903054fe53be08b8afeb53c019f44247cf9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:31:15 +0100 Subject: [PATCH 204/908] Add cancel button to forms that didn't have one already --- app/views/groups/new.html.haml | 1 + app/views/profiles/show.html.haml | 8 +++----- app/views/projects/new.html.haml | 1 + app/views/projects/services/_form.html.haml | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 3e602559ae..4bc31cabea 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -23,3 +23,4 @@ .form-actions = f.submit 'Create group', class: "btn btn-create", tabindex: 3 + = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel' diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index ac7355dde1..faab12ca0b 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -96,8 +96,6 @@ = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" - .row - .col-md-7 - .form-group - .col-sm-offset-2.col-sm-10 - = f.submit 'Save changes', class: "btn btn-success" + .form-actions + = f.submit 'Save changes', class: "btn btn-success" + = link_to "Cancel", user_path(current_user), class: "btn btn-cancel" diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index fa75a62422..2670b9d9cd 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -100,6 +100,7 @@ .form-actions = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 + = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' - if current_user.can_create_group? .pull-right diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index fecd157c6d..ea5a2302a5 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -15,3 +15,4 @@ - if @service.valid? && @service.activated? - disabled = @service.can_test? ? '':'disabled' = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service.to_param), class: "btn #{disabled}" + = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" From ffabf1df50744564ac99f358f13ab4b1c5d54284 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:33:14 +0100 Subject: [PATCH 205/908] Add cancel button to forms that didn't have one already --- app/views/profiles/preferences/show.html.haml | 2 +- .../projects/deploy_keys/_form.html.haml | 2 +- app/views/projects/edit.html.haml | 20 +++++++++++-------- app/views/projects/labels/_form.html.haml | 6 ++++-- .../merge_requests/_new_compare.html.haml | 4 ++-- app/views/projects/runners/edit.html.haml | 2 +- app/views/projects/services/_form.html.haml | 2 +- app/views/shared/issuable/_form.html.haml | 2 +- app/views/shared/snippets/_form.html.haml | 2 +- .../steps/project/forked_merge_requests.rb | 2 +- features/steps/project/issues/issues.rb | 4 ++-- features/steps/project/merge_requests.rb | 2 +- 12 files changed, 28 insertions(+), 22 deletions(-) diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index cc41d7dd81..877589dc39 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -54,4 +54,4 @@ .help-block Choose what content you want to see on a project's home page. .panel-footer - = f.submit 'Save', class: 'btn btn-save' + = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index 91675b3738..085c9149b1 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -18,6 +18,6 @@ = f.text_area :key, class: "form-control thin_area", rows: 5 .form-actions - = f.submit 'Create', class: "btn-create btn" + = f.submit 'Create Deploy Key', class: "btn-create btn" = link_to "Cancel", namespace_project_deploy_keys_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 0c10de1604..5e7c211a42 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -130,9 +130,11 @@ The project can be committed to. %br %strong Once active this project shows up in the search and on the dashboard. - = link_to 'Unarchive', unarchive_namespace_project_path(@project.namespace, @project), - data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, - method: :post, class: "btn btn-success" + + .form-actions + = link_to 'Unarchive project', unarchive_namespace_project_path(@project.namespace, @project), + data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, + method: :post, class: "btn btn-success" - else .panel.panel-warning .panel-heading @@ -144,9 +146,11 @@ It is hidden from the dashboard and doesn't show up in searches. %br %strong Archived projects cannot be committed to! - = link_to 'Archive', archive_namespace_project_path(@project.namespace, @project), - data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, - method: :post, class: "btn btn-warning" + + .form-actions + = link_to 'Archive project', archive_namespace_project_path(@project.namespace, @project), + data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, + method: :post, class: "btn btn-warning" - else .nothing-here-block Only the project owner can archive a project @@ -175,7 +179,7 @@ %li Be careful. Renaming a project's repository can have unintended side effects. %li You will need to update your local repositories to point to the new location. .form-actions - = f.submit 'Rename', class: "btn btn-warning" + = f.submit 'Rename project', class: "btn btn-warning" - if can?(current_user, :change_namespace, @project) .panel.panel-default.panel.panel-danger @@ -194,7 +198,7 @@ %li You can only transfer the project to namespaces you manage. %li You will need to update your local repositories to point to the new location. .form-actions - = f.submit 'Transfer', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } + = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } - else .nothing-here-block Only the project owner can transfer a project diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index 291703bd60..2d4311412f 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -28,6 +28,8 @@   .form-actions - = f.submit 'Save', class: 'btn btn-save js-save-button' + - if @label.persisted? + = f.submit 'Save changes', class: 'btn btn-save js-save-button' + - else + = f.submit 'Create Label', class: 'btn btn-create js-save-button' = link_to "Cancel", namespace_project_labels_path(@project.namespace, @project), class: 'btn btn-cancel' - diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 46e72e9dee..cc34f58c54 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -52,8 +52,8 @@ are the same. - %div - = f.submit 'Compare branches', class: "btn btn-new mr-compare-btn" + .form-actions + = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn" :javascript var source_branch = $("#merge_request_source_branch") diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml index a032470169..eba03028af 100644 --- a/app/views/projects/runners/edit.html.haml +++ b/app/views/projects/runners/edit.html.haml @@ -26,4 +26,4 @@ = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control' .help-block You can setup jobs to only use runners with specific tags .form-actions - = f.submit 'Save', class: 'btn btn-save' + = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index ea5a2302a5..1b70880043 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -10,7 +10,7 @@ = render 'shared/service_settings', form: form .form-actions - = form.submit 'Save', class: 'btn btn-save' + = form.submit 'Save changes', class: 'btn btn-save'   - if @service.valid? && @service.activated? - disabled = @service.can_test? ? '':'disabled' diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 2a5fb534a3..7f32fac6b2 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -90,7 +90,7 @@ %strong #{link_to 'guidelines for contribution', guide_url} to this repository. - if issuable.new_record? - = f.submit "Submit new #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' + = f.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' - else = f.submit 'Save changes', class: 'btn btn-save' - if issuable.new_record? diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 913b674484..b46ed0dc0a 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -27,7 +27,7 @@ - if @snippet.new_record? = f.submit 'Create snippet', class: "btn-create btn" - else - = f.submit 'Save', class: "btn-save btn" + = f.submit 'Save changes', class: "btn-save btn" - if @snippet.project_id = link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 155a5ff58a..024dc5e72d 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -39,7 +39,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps select "fix", from: "merge_request_source_branch" select "master", from: "merge_request_target_branch" - click_button "Compare branches" + click_button "Compare branches and continue" expect(page).to have_content "New Merge Request" fill_in "merge_request_title", with: "Merge Request On Forked Project" diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index af2da41bad..202778b180 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -72,13 +72,13 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps step 'I submit new issue "500 error on profile"' do fill_in "issue_title", with: "500 error on profile" - click_button "Submit new issue" + click_button "Submit issue" end step 'I submit new issue "500 error on profile" with label \'bug\'' do fill_in "issue_title", with: "500 error on profile" select 'bug', from: "Labels" - click_button "Submit new issue" + click_button "Submit issue" end step 'I click link "500 error on profile"' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index d5f2c4209a..822cf0ffe1 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -86,7 +86,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps select "feature", from: "merge_request_target_branch" click_button "Compare branches" fill_in "merge_request_title", with: "Wiki Feature" - click_button "Submit new merge request" + click_button "Submit merge request" end step 'project "Shop" have "Bug NS-04" open merge request' do From b04c5b069517aa511cdeebbad27169af8940b22c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:34:20 +0100 Subject: [PATCH 206/908] Use form-actions where appropriate --- app/views/groups/edit.html.haml | 3 ++- app/views/profiles/accounts/show.html.haml | 15 ++++++++++----- app/views/projects/blob/_new_dir.html.haml | 7 +++---- app/views/projects/blob/_upload.html.haml | 7 +++---- app/views/projects/edit.html.haml | 7 ++++--- app/views/shared/_confirm_modal.html.haml | 2 +- 6 files changed, 23 insertions(+), 18 deletions(-) diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index b5afb4ae1c..8daac58596 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -44,4 +44,5 @@ %br %strong Removed group can not be restored! - = link_to 'Remove Group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove" + .form-actions + = link_to 'Remove Group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove" diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index cd7b1b0fe0..2fd65cc994 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -23,10 +23,13 @@ %p.cgray - if current_user.private_token = text_field_tag "token", current_user.private_token, class: "form-control" - %div - = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default btn-build-token" - else %span You don`t have one yet. Click generate to fix it. + + .form-actions + - if current_user.private_token + = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default btn-build-token" + - else = f.submit 'Generate', class: "btn btn-default btn-build-token" - unless current_user.ldap_user? @@ -54,7 +57,8 @@ %p Each time you log in you’ll be required to provide your username and password as usual, plus a randomly-generated code from your phone. - %div + + .form-actions = link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' - if button_based_providers.any? @@ -89,7 +93,7 @@ Saving new username %p.light = user_url(@user) - %div + .form-actions = f.submit 'Save username', class: "btn btn-warning" - if signup_enabled? @@ -104,7 +108,8 @@ - rp = current_user.personal_projects.count - unless rp.zero? %li #{pluralize rp, 'personal project'} will be removed and cannot be restored - = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" + .form-actions + = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" - else - if @user.solo_owned_groups.present? %p diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index 377f0fa012..40d0b68c6c 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -13,10 +13,9 @@ = render 'shared/new_commit_form', placeholder: "Add new directory" - .form-group - .col-sm-offset-2.col-sm-10 - = submit_tag "Create directory", class: 'btn btn-primary btn-create' - = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + .form-actions + = submit_tag "Create directory", class: 'btn btn-create' + = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" :javascript disableButtonIfAnyEmptyField($(".js-create-dir-form"), ".form-control", ".btn-create"); diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index 3bb61f0c94..ecc90a30e7 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -16,10 +16,9 @@ = render 'shared/new_commit_form', placeholder: placeholder - .form-group - .col-sm-offset-2.col-sm-10 - = button_tag button_title, class: 'btn btn-small btn-primary btn-upload-file', id: 'submit-all' - = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + .form-actions + = button_tag button_title, class: 'btn btn-small btn-create btn-upload-file', id: 'submit-all' + = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" :javascript disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file'); diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 5e7c211a42..7bf89d4e55 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -213,7 +213,8 @@ #{link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)}. %br %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. - = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } + .form-actions + = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } - else .nothing-here-block Only the project owner can remove the fork relationship. @@ -226,8 +227,8 @@ Removing the project will delete its repository and all related resources including issues, merge requests etc. %br %strong Removed projects cannot be restored! - - = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } + .form-actions + = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } - else .nothing-here-block Only the project owner can remove a project. diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml index 9bc2d33c27..34241cd8aa 100644 --- a/app/views/shared/_confirm_modal.html.haml +++ b/app/views/shared/_confirm_modal.html.haml @@ -19,5 +19,5 @@ .form-group = text_field_tag 'confirm_name_input', '', class: 'form-control js-confirm-danger-input' - .form-group + .form-actions = submit_tag 'Confirm', class: "btn btn-danger js-confirm-danger-submit" From da48fdc2a51af0f02ebb22cc13fabf7d1a636690 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:35:19 +0100 Subject: [PATCH 207/908] Don't write "Optional" or "Required" unless non-obvious --- app/views/groups/milestones/new.html.haml | 1 - app/views/projects/milestones/_form.html.haml | 1 - app/views/projects/tags/new.html.haml | 4 ++-- app/views/shared/issuable/_form.html.haml | 3 +-- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index 800bac4ef0..ccba58a1ac 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -15,7 +15,6 @@ = f.label :title, "Title", class: "control-label" .col-sm-10 = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true - %p.hint Required .form-group.milestone-description = f.label :description, "Description", class: "control-label" .col-sm-10 diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index cc29970f07..c8e0baa1d1 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -10,7 +10,6 @@ = f.label :title, "Title", class: "control-label" .col-sm-10 = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true - %p.hint Required .form-group.milestone-description = f.label :description, "Description", class: "control-label" .col-sm-10 diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 91e0a5493b..450d3309e2 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -24,7 +24,7 @@ = label_tag :message, nil, class: 'control-label' .col-sm-10 = text_field_tag :message, nil, placeholder: 'Enter message.', required: false, tabindex: 3, class: 'form-control' - .help-block (Optional) Entering a message will create an annotated tag. + .help-block Optionally, enter a message to create an annotated tag. %hr .form-group = label_tag :release_description, 'Release notes', class: 'control-label' @@ -32,7 +32,7 @@ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', attr: :release_description, classes: 'description js-quick-submit form-control' = render 'projects/notes/hints' - .help-block (Optional) You can add release notes to your tag. It will be stored in the GitLab database and shown on the tags page + .help-block Optionally, you can add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. .form-actions = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 7f32fac6b2..ae9d100d1d 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -6,8 +6,7 @@ %span= msg %br .form-group - = f.label :title, class: 'control-label' do - %strong= 'Title *' + = f.label :title, class: 'control-label' .col-sm-10 = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off', class: 'form-control pad js-gfm-input js-quick-submit', required: true From a70c507882289a42a9d9b359a730e6f166fedd74 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:36:44 +0100 Subject: [PATCH 208/908] Only use input placeholders when they add value --- app/views/projects/blob/_new_dir.html.haml | 2 +- app/views/projects/branches/new.html.haml | 5 +++-- app/views/projects/edit.html.haml | 6 +++--- app/views/projects/new.html.haml | 2 +- app/views/projects/tags/new.html.haml | 6 +++--- app/views/shared/snippets/_form.html.haml | 3 ++- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index 40d0b68c6c..7f95b46efc 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -9,7 +9,7 @@ .form-group = label_tag :dir_name, 'Directory name', class: 'control-label' .col-sm-10 - = text_field_tag :dir_name, params[:dir_name], placeholder: "Directory name", required: true, class: 'form-control' + = text_field_tag :dir_name, params[:dir_name], required: true, class: 'form-control' = render 'shared/new_commit_form', placeholder: "Add new directory" diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index efb5298a5e..11567fc439 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -13,11 +13,12 @@ .form-group = label_tag :branch_name, nil, class: 'control-label' .col-sm-10 - = text_field_tag :branch_name, params[:branch_name], placeholder: 'enter new branch name', required: true, tabindex: 1, class: 'form-control' + = text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control' .form-group = label_tag :ref, 'Create from', class: 'control-label' .col-sm-10 - = text_field_tag :ref, params[:ref], placeholder: 'existing branch name, tag or commit SHA', required: true, tabindex: 2, class: 'form-control' + = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control' + .help-block Existing branch name, tag, or commit SHA .form-actions = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', namespace_project_branches_path(@project.namespace, @project), class: 'btn btn-cancel' diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 7bf89d4e55..35581ee1aa 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -14,7 +14,7 @@ = f.label :name, class: 'control-label' do Project name .col-sm-10 - = f.text_field :name, placeholder: "Example Project", class: "form-control", id: "project_name_edit" + = f.text_field :name, class: "form-control", id: "project_name_edit" .form-group @@ -22,7 +22,7 @@ Project description %span.light (optional) .col-sm-10 - = f.text_area :description, placeholder: "Awesome project", class: "form-control", rows: 3, maxlength: 250 + = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 - if @project.repository.exists? && @project.repository.branch_names.any? .form-group @@ -164,7 +164,7 @@ Project name .col-sm-9 .form-group - = f.text_field :name, placeholder: "Example Project", class: "form-control" + = f.text_field :name, class: "form-control" .form-group = f.label :path, class: 'control-label' do %span Path diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 2670b9d9cd..da0267604e 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -95,7 +95,7 @@ Description %span.light (optional) .col-sm-10 - = f.text_area :description, placeholder: "Awesome project", class: "form-control", rows: 3, maxlength: 250, tabindex: 3 + = f.text_area :description, class: "form-control", rows: 3, maxlength: 250, tabindex: 3 = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project .form-actions diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 450d3309e2..e65ce308d6 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -14,16 +14,16 @@ .form-group = label_tag :tag_name, nil, class: 'control-label' .col-sm-10 - = text_field_tag :tag_name, params[:tag_name], placeholder: 'v3.0.1', required: true, tabindex: 1, class: 'form-control' + = text_field_tag :tag_name, params[:tag_name], required: true, tabindex: 1, autofocus: true, class: 'form-control' .form-group = label_tag :ref, 'Create from', class: 'control-label' .col-sm-10 - = text_field_tag :ref, params[:ref], placeholder: 'master', required: true, tabindex: 2, class: 'form-control' + = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control' .help-block Branch name or commit SHA .form-group = label_tag :message, nil, class: 'control-label' .col-sm-10 - = text_field_tag :message, nil, placeholder: 'Enter message.', required: false, tabindex: 3, class: 'form-control' + = text_field_tag :message, nil, required: false, tabindex: 3, class: 'form-control' .help-block Optionally, enter a message to create an annotated tag. %hr .form-group diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index b46ed0dc0a..99d9365967 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -8,7 +8,8 @@ .form-group = f.label :title, class: 'control-label' - .col-sm-10= f.text_field :title, placeholder: "Example Snippet", class: 'form-control', required: true + .col-sm-10 + = f.text_field :title, class: 'form-control', required: true, autofocus: true = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet From 9bad736fb3ccd0a48cd64fc37538d8b021bce205 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:38:14 +0100 Subject: [PATCH 209/908] Remove unnecesary wrapper elements --- app/assets/stylesheets/pages/merge_requests.scss | 4 ---- app/views/projects/_commit_button.html.haml | 4 +--- app/views/projects/issues/_form.html.haml | 5 ++--- app/views/projects/merge_requests/_form.html.haml | 3 +-- .../projects/merge_requests/_new_submit.html.haml | 11 +++++------ app/views/projects/notes/_edit_form.html.haml | 5 ++--- app/views/projects/notes/_form.html.haml | 9 ++++----- 7 files changed, 15 insertions(+), 26 deletions(-) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 08e4bcdf52..8ff0a0cd54 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -192,10 +192,6 @@ line-height: 1.1; } -.merge-request-form-info { - padding-top: 15px; -} - // hide mr close link for inline diff comment form .diff-file .close-mr-link, .diff-file .reopen-mr-link { diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml index 35f7e7bb34..2fd3d9e1be 100644 --- a/app/views/projects/_commit_button.html.haml +++ b/app/views/projects/_commit_button.html.haml @@ -1,6 +1,4 @@ .form-actions - .commit-button-annotation - = button_tag 'Commit Changes', - class: 'btn commit-btn js-commit-button btn-create' + = button_tag 'Commit Changes', class: 'btn commit-btn js-commit-button btn-create' = link_to 'Cancel', cancel_path, class: 'btn btn-cancel', data: {confirm: leave_edit_message} diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index e0e26a26da..6588d9bdbe 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -1,6 +1,5 @@ -%div.issue-form-holder - = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form' } do |f| - = render 'shared/issuable/form', f: f, issuable: @issue += form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-requires-input' } do |f| + = render 'shared/issuable/form', f: f, issuable: @issue :javascript $('.assign-to-me-link').on('click', function(e){ diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 9cf389dbe3..3e4ab09c6d 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,6 +1,5 @@ = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f| - .merge-request-form-info - = render 'shared/issuable/form', f: f, issuable: @merge_request + = render 'shared/issuable/form', f: f, issuable: @merge_request :javascript $('.assign-to-me-link').on('click', function(e){ diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 9fa9cc5612..8b0579f341 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -11,12 +11,11 @@ = link_to 'Change branches', mr_change_branches_path(@merge_request) %hr = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f| - .merge-request-form-info - = render 'shared/issuable/form', f: f, issuable: @merge_request - = f.hidden_field :source_project_id - = f.hidden_field :source_branch - = f.hidden_field :target_project_id - = f.hidden_field :target_branch + = render 'shared/issuable/form', f: f, issuable: @merge_request + = f.hidden_field :source_project_id + = f.hidden_field :source_branch + = f.hidden_field :target_project_id + = f.hidden_field :target_branch .mr-compare.merge-request %ul.merge-request-tabs diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index a21c019986..3ccda1b381 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -6,6 +6,5 @@ = render 'projects/notes/hints' .note-form-actions - .buttons - = f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button' - = link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel' + = f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button' + = link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel' diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 5dd84317e3..88e711ab53 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -12,8 +12,7 @@ = render 'projects/notes/hints' .error-alert - .note-form-actions - .buttons.clearfix - = f.submit 'Add Comment', class: "btn btn-green comment-btn btn-grouped js-comment-button" - = yield(:note_actions) - %a.btn.grouped.js-close-discussion-note-form Cancel + .note-form-actions.clearfix + = f.submit 'Add Comment', class: "btn btn-create comment-btn btn-grouped js-comment-button" + = yield(:note_actions) + %a.btn.btn-cancel.js-close-discussion-note-form Cancel From 2aeb26bd8823fa681413bd0f64b7b068f41ec4e3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:38:33 +0100 Subject: [PATCH 210/908] Use select2 placeholder instead of blank option --- app/views/projects/protected_branches/index.html.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 52b3a50c1e..2541105b00 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -22,7 +22,7 @@ .form-group = f.label :name, "Branch", class: 'control-label' .col-sm-10 - = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: "Select branch"}, {class: "select2"}) + = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: true}, {class: "select2", data: {placeholder: "Select branch"}}) .form-group .col-sm-offset-2.col-sm-10 .checkbox @@ -33,4 +33,3 @@ .form-actions = f.submit 'Protect', class: "btn-create btn" = render 'branches_list' - From 72ab3b17d338c15f27e3bb1fbd0e7cefaa10b94d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:39:39 +0100 Subject: [PATCH 211/908] Use js-requires-input where appropriate --- app/views/profiles/keys/_form.html.haml | 7 +++---- app/views/projects/blob/_new_dir.html.haml | 3 +-- app/views/projects/deploy_keys/_form.html.haml | 7 +++---- app/views/projects/tags/new.html.haml | 3 +-- app/views/shared/_new_commit_form.html.haml | 2 +- app/views/shared/snippets/_form.html.haml | 2 +- 6 files changed, 10 insertions(+), 14 deletions(-) diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index b76a5b636a..2a8800de60 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -1,5 +1,5 @@ %div - = form_for [:profile, @key], html: { class: 'form-horizontal' } do |f| + = form_for [:profile, @key], html: { class: 'form-horizontal js-requires-input' } do |f| - if @key.errors.any? .alert.alert-danger %ul @@ -9,12 +9,11 @@ .form-group = f.label :key, class: 'control-label' .col-sm-10 - = f.text_area :key, class: "form-control", rows: 8 + = f.text_area :key, class: "form-control", rows: 8, autofocus: true, required: true .form-group = f.label :title, class: 'control-label' - .col-sm-10= f.text_field :title, class: "form-control" + .col-sm-10= f.text_field :title, class: "form-control", required: true .form-actions = f.submit 'Add key', class: "btn btn-create" = link_to "Cancel", profile_keys_path, class: "btn btn-cancel" - diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index 7f95b46efc..fc6c9f5fd0 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -5,7 +5,7 @@ %a.close{href: "#", "data-dismiss" => "modal"} × %h3.page-title Create New Directory .modal-body - = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form' do + = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-requires-input' do .form-group = label_tag :dir_name, 'Directory name', class: 'control-label' .col-sm-10 @@ -18,5 +18,4 @@ = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" :javascript - disableButtonIfAnyEmptyField($(".js-create-dir-form"), ".form-control", ".btn-create"); new NewCommitForm($('.js-create-dir-form')) diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index 085c9149b1..5e182af266 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -1,5 +1,5 @@ %div - = form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal' } do |f| + = form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal js-requires-input' } do |f| -if @key.errors.any? .alert.alert-danger %ul @@ -8,16 +8,15 @@ .form-group = f.label :title, class: "control-label" - .col-sm-10= f.text_field :title, class: 'form-control' + .col-sm-10= f.text_field :title, class: 'form-control', autofocus: true, required: true .form-group = f.label :key, class: "control-label" .col-sm-10 %p.light Paste a machine public key here. Read more about how to generate it = link_to "here", help_page_path("ssh", "README") - = f.text_area :key, class: "form-control thin_area", rows: 5 + = f.text_area :key, class: "form-control thin_area", rows: 5, required: true .form-actions = f.submit 'Create Deploy Key', class: "btn-create btn" = link_to "Cancel", namespace_project_deploy_keys_path(@project.namespace, @project), class: "btn btn-cancel" - diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index e65ce308d6..58d2e5c713 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -10,7 +10,7 @@ New Tag %hr -= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form" do += form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-requires-input" do .form-group = label_tag :tag_name, nil, class: 'control-label' .col-sm-10 @@ -38,7 +38,6 @@ = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' :javascript - disableButtonIfAnyEmptyField($("#new-tag-form"), ".form-control", ".btn-create"); var availableTags = #{@project.repository.ref_names.to_json}; $("#ref").autocomplete({ diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index ce52614e86..31b02ed93d 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -4,7 +4,7 @@ .form-group.branch = label_tag 'new_branch', 'Target branch', class: 'control-label' .col-sm-10 - = text_field_tag 'new_branch', @new_branch || @ref, class: "form-control js-new-branch" + = text_field_tag 'new_branch', @new_branch || @ref, required: true, class: "form-control js-new-branch" .form-group.js-create-merge-request-form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 99d9365967..1041eccd1d 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -1,5 +1,5 @@ .snippet-form-holder - = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form" } do |f| + = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f| - if @snippet.errors.any? .alert.alert-danger %ul From dccc22775472a7053b85f496011808c35fd6d62c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:40:01 +0100 Subject: [PATCH 212/908] Use autofocus where appropriate --- app/views/groups/milestones/new.html.haml | 2 +- app/views/projects/labels/_form.html.haml | 2 +- app/views/projects/milestones/_form.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index ccba58a1ac..3894a0ece7 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -14,7 +14,7 @@ .form-group = f.label :title, "Title", class: "control-label" .col-sm-10 - = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true + = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true .form-group.milestone-description = f.label :description, "Description", class: "control-label" .col-sm-10 diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index 2d4311412f..5ce2a7b985 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -10,7 +10,7 @@ .form-group = f.label :title, class: 'control-label' .col-sm-10 - = f.text_field :title, class: "form-control js-quick-submit", required: true + = f.text_field :title, class: "form-control js-quick-submit", required: true, autofocus: true .form-group = f.label :color, "Background color", class: 'control-label' .col-sm-10 diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index c8e0baa1d1..39aa2437e1 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -9,7 +9,7 @@ .form-group = f.label :title, "Title", class: "control-label" .col-sm-10 - = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true + = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true .form-group.milestone-description = f.label :description, "Description", class: "control-label" .col-sm-10 From cdd4c331b36e75b2b01a1648f1e5563c4afc5a49 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:40:18 +0100 Subject: [PATCH 213/908] Use select2 instead of regular selectbox for profile public email --- app/views/profiles/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index faab12ca0b..9459d8a629 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -43,7 +43,7 @@ .form-group = f.label :public_email, class: "control-label" .col-sm-10 - = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show in profile'}, class: "form-control" + = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show on profile'}, class: "select2" %span.help-block This email will be displayed on your public profile. .form-group = f.label :skype, class: "control-label" From 8d98245783832a4d6617fb7076343469918ed273 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:40:36 +0100 Subject: [PATCH 214/908] Remove `.git` suffix to project path field. --- app/views/projects/edit.html.haml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 35581ee1aa..b28ada909b 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -174,7 +174,6 @@ .input-group-addon #{URI.join(root_url, @project.namespace.path)}/ = f.text_field :path, class: 'form-control' - %span.input-group-addon .git %ul %li Be careful. Renaming a project's repository can have unintended side effects. %li You will need to update your local repositories to point to the new location. From d0166334ba9997b5eed89d6f54c99f7bfbe9c816 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:40:52 +0100 Subject: [PATCH 215/908] Add `http://gitlab.example.com/u/` prefix to "Change username" field. --- app/views/profiles/accounts/show.html.haml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 2fd65cc994..319bdd57c3 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -85,14 +85,15 @@ %p Changing your username will change path to all personal projects! %div - = f.text_field :username, required: true, class: 'form-control' + .input-group + .input-group-addon + = "#{root_url}u/" + = f.text_field :username, required: true, class: 'form-control'   .loading-gif.hide %p = icon('spinner spin') Saving new username - %p.light - = user_url(@user) .form-actions = f.submit 'Save username', class: "btn btn-warning" From c0e614d1c46cd273f83385ef61c76200d492d0a4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:41:02 +0100 Subject: [PATCH 216/908] Rename variable --- app/views/projects/branches/new.html.haml | 4 ++-- app/views/projects/tags/new.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index 11567fc439..31943a2407 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -24,9 +24,9 @@ = link_to 'Cancel', namespace_project_branches_path(@project.namespace, @project), class: 'btn btn-cancel' :javascript - var availableTags = #{@project.repository.ref_names.to_json}; + var availableRefs = #{@project.repository.ref_names.to_json}; $("#ref").autocomplete({ - source: availableTags, + source: availableRefs, minLength: 1 }); diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 58d2e5c713..9c9bfa3f55 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -38,9 +38,9 @@ = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' :javascript - var availableTags = #{@project.repository.ref_names.to_json}; + var availableRefs = #{@project.repository.ref_names.to_json}; $("#ref").autocomplete({ - source: availableTags, + source: availableRefs, minLength: 1 }); From 092e093b2c77c88707060e0cd95eec338641d6b1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 2 Dec 2015 14:42:43 +0100 Subject: [PATCH 217/908] Small css cleanup and ui dev kit improvements Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/files.scss | 1 - app/assets/stylesheets/framework/lists.scss | 7 ----- app/assets/stylesheets/pages/ui_dev_kit.scss | 5 +--- app/views/help/ui.html.haml | 30 ++++++++++---------- 4 files changed, 16 insertions(+), 27 deletions(-) diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 35db00281e..53be2f2ce0 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -22,7 +22,6 @@ position: relative; background: $background-color; border-bottom: 1px solid $border-color; - text-shadow: 0 1px 1px #fff; margin: 0; text-align: left; padding: 10px 15px; diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index a798ae812e..927641216e 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -72,13 +72,6 @@ } } -ol, ul { - &.styled { - li { - padding: 2px; - } - } -} /** light list with border-bottom between li **/ ul.bordered-list { diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss index 277afa1db9..185f3622e6 100644 --- a/app/assets/stylesheets/pages/ui_dev_kit.scss +++ b/app/assets/stylesheets/pages/ui_dev_kit.scss @@ -1,9 +1,6 @@ .gitlab-ui-dev-kit { > h2 { - font-size: 27px; - border-bottom: 1px solid #CCC; - color: #666; - margin: 30px 0; + margin: 35px 0 20px; font-weight: bold; } } diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 8a60d6852b..d9ffda884c 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -31,7 +31,7 @@ %h2#blocks Blocks - %h3 + %h4 %code .gray-content-block .gray-content-block.middle-block @@ -43,9 +43,9 @@ = lorem - %h3 + %h4 %code .cover-block - + %br .cover-block .avatar-holder = image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: '' @@ -64,7 +64,7 @@ %h2#lists Lists - %h3 + %h4 %code .content-list %ul.content-list %li @@ -74,7 +74,7 @@ %li One item - %h3 + %h4 %code .well-list %ul.well-list %li @@ -84,7 +84,7 @@ %li One item - %h3 + %h4 %code .panel .well-list .panel.panel-default @@ -97,7 +97,7 @@ %li One item - %h3 + %h4 %code .bordered-list %ul.bordered-list %li @@ -138,7 +138,7 @@ %h2#navs Navigation - %h3 + %h4 %code .center-top-menu .example %ul.center-top-menu @@ -147,7 +147,7 @@ %li %a Closed - %h3 + %h4 %code .btn-group.btn-group-next .example %div.btn-group.btn-group-next @@ -155,7 +155,7 @@ %a.btn Closed - %h3 + %h4 %code .nav.nav-tabs .example %ul.nav.nav-tabs @@ -221,7 +221,7 @@ %h2#forms Forms - %h3 + %h4 %code form.horizontal-form %form.form-horizontal @@ -243,7 +243,7 @@ .col-sm-offset-2.col-sm-10 %button.btn.btn-default{:type => "submit"} Sign in - %h3 + %h4 %code form %form @@ -260,7 +260,7 @@ %button.btn.btn-default{:type => "submit"} Sign in %h2#file File - %h3 + %h4 %code .file-holder - blob = Snippet.new(content: "Wow\nSuch\nFile") @@ -271,12 +271,12 @@ .file-actions .btn-group %a.btn Edit - %a.btn Remove + %a.btn.btn-danger Remove .file-contenta.code = render 'shared/file_highlight', blob: blob %h2#markdown Markdown - %h3 + %h4 %code .md or .wiki and others Markdown rendering has a bit different css and presented in next UI elements: From 58dad2a9dadea647b5665e1de6a6e997484b96b1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:43:23 +0100 Subject: [PATCH 218/908] Remove bottom margin from page-titles --- app/assets/stylesheets/framework/typography.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 2c4a58c8db..aef338cfa5 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -181,6 +181,10 @@ body { line-height: 1.3; font-size: 1.25em; font-weight: 600; + + &:last-child { + margin-bottom: 0; + } } .page-title-empty { From 3ae7dba931b880b8090edffb247ebfe32d26648e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:46:15 +0100 Subject: [PATCH 219/908] Use gl-padding instead of 15px/20px where appropriate --- app/assets/stylesheets/framework.scss | 3 ++- app/assets/stylesheets/framework/common.scss | 10 +++++++++- app/assets/stylesheets/framework/files.scss | 4 +--- app/assets/stylesheets/framework/panels.scss | 15 +++++++++++++++ app/assets/stylesheets/framework/tables.scss | 2 ++ app/assets/stylesheets/pages/merge_requests.scss | 2 +- app/assets/stylesheets/pages/note_form.scss | 7 +++---- app/assets/stylesheets/pages/projects.scss | 2 +- app/views/profiles/notifications/show.html.haml | 4 +--- app/views/projects/compare/show.html.haml | 4 ++-- .../merge_requests/_new_compare.html.haml | 3 +-- app/views/projects/merge_requests/_show.html.haml | 2 +- app/views/shared/issuable/_context.html.haml | 6 +++--- 13 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 app/assets/stylesheets/framework/panels.scss diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 1ec9d2fd84..48a4971c8f 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -1,9 +1,9 @@ @import "framework/fonts"; @import "framework/variables"; @import "framework/mixins"; -@import "framework/layout"; @import 'framework/tw_bootstrap_variables'; @import 'framework/tw_bootstrap'; +@import "framework/layout"; @import "framework/avatar.scss"; @import "framework/blocks.scss"; @@ -25,6 +25,7 @@ @import "framework/markdown_area.scss"; @import "framework/mobile.scss"; @import "framework/pagination.scss"; +@import "framework/panels.scss"; @import "framework/selects.scss"; @import "framework/sidebar.scss"; @import "framework/tables.scss"; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 61689aff57..61ecd58e6c 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -7,7 +7,7 @@ /** COMMON CLASSES **/ .prepend-top-10 { margin-top:10px } -.prepend-top-default { margin-top: $gl-padding; } +.prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-20 { margin-top:20px } .prepend-left-10 { margin-left:10px } .prepend-left-20 { margin-left:20px } @@ -52,6 +52,10 @@ pre { } } +hr { + margin: $gl-padding 0; +} + .dropdown-menu > li > a { text-shadow: none; } @@ -433,3 +437,7 @@ table { .space-right { margin-right: 10px; } + +.alert, .progress { + margin-bottom: $gl-padding; +} diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 35db00281e..6bf2857e83 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -8,7 +8,6 @@ border: none; border-top: 1px solid #E7E9EE; border-bottom: 1px solid #E7E9EE; - margin-bottom: 1em; &.readme-holder { border-bottom: 0; @@ -25,7 +24,7 @@ text-shadow: 0 1px 1px #fff; margin: 0; text-align: left; - padding: 10px 15px; + padding: 10px $gl-padding; .file-actions { float: right; @@ -171,4 +170,3 @@ } } } - diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss new file mode 100644 index 0000000000..406aff3d72 --- /dev/null +++ b/app/assets/stylesheets/framework/panels.scss @@ -0,0 +1,15 @@ +.panel { + margin-bottom: $gl-padding; + + .panel-heading { + padding: 10px $gl-padding; + } + .panel-body { + padding: $gl-padding; + + .form-actions { + margin: -$gl-padding; + margin-top: $gl-padding; + } + } +} diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 66e16e8df7..793ab3d9bb 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -6,6 +6,8 @@ table { &.table { + margin-bottom: $gl-padding; + .dropdown-menu a { text-decoration: none; } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 08e4bcdf52..177cf6ca45 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -87,7 +87,7 @@ .mr-widget-body, .ci_widget, .mr-widget-footer { - padding: 15px; + padding: $gl-padding; } .normal { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 268fc995aa..02db44c8eb 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -7,6 +7,7 @@ } .reply-btn { @extend .btn-primary; + margin: 10px $gl-padding; } .diff-file .diff-content { tr.line_holder:hover { @@ -38,9 +39,8 @@ } .new_note, .edit_note { - .buttons { - margin-top: 8px; - margin-bottom: 3px; + .note-form-actions { + margin-top: $gl-padding; } .note-preview-holder { @@ -150,7 +150,6 @@ .discussion-reply-holder { background: $background-color; - padding: 10px 15px; border-top: 1px solid $border-color; } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 4a0fe54684..b3054e3624 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -376,7 +376,7 @@ table.table.protected-branches-list tr.no-border { .project-stats { text-align: center; - margin-top: 15px; + margin-top: $gl-padding; margin-bottom: 0; padding-top: 10px; padding-bottom: 4px; diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 8eebd96b67..d4d9f24627 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -53,9 +53,7 @@ .form-actions = f.submit 'Save changes', class: "btn btn-create" -.clearfix - %hr -.row.all-notifications +.row.all-notifications.prepend-top-default .col-md-6 %p You can also specify notification level per group or per project. diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 39755efd2f..51088a7dea 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -7,11 +7,11 @@ = render "form" - if @commits.present? - .prepend-top-20 + .prepend-top-default = render "projects/commits/commit_list" = render "projects/diffs/diffs", diffs: @diffs, project: @project - else - .light-well.prepend-top-20 + .light-well.prepend-top-default .center %h4 There isn't anything to compare. diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index d9eff1f932..6def9d6266 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -37,7 +37,7 @@ %h4 Compare failed %p We can't compare selected branches. It may be because of huge diff. Please try again or select different branches. - else - .light-well.append-bottom-10 + .light-well.append-bottom-default .center %h4 There isn't anything to merge. @@ -86,4 +86,3 @@ return; } }); - diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index eeaa72ed21..94bd154aeb 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -8,7 +8,7 @@ .merge-request-details.issuable-details = render "projects/merge_requests/show/mr_title" = render "projects/merge_requests/show/mr_box" - .append-bottom-20.mr-source-target.prepend-top-default + .append-bottom-default.mr-source-target.prepend-top-default - if @merge_request.open? .pull-right - if @merge_request.source_branch_exists? diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml index be66256c7b..f1646b4ce6 100644 --- a/app/views/shared/issuable/_context.html.haml +++ b/app/views/shared/issuable/_context.html.haml @@ -1,5 +1,5 @@ = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| - %div.prepend-top-20 + %div.prepend-top-default .issuable-context-title %label Assignee: @@ -11,7 +11,7 @@ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true) - %div.prepend-top-20.clearfix + %div.prepend-top-default.clearfix .issuable-context-title %label Milestone: @@ -31,7 +31,7 @@ - if current_user - subscribed = issuable.subscribed?(current_user) - %div.prepend-top-20.clearfix + %div.prepend-top-default.clearfix .issuable-context-title %label Subscription: From c95b64d1cad8aa8f0861feba0c919ae3d1722df0 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:46:55 +0100 Subject: [PATCH 220/908] Fix alignment of buttons in issue header --- app/assets/stylesheets/framework/issue_box.scss | 5 +++-- app/assets/stylesheets/pages/issuable.scss | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 93377e45e7..f12d68b5a1 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -7,8 +7,9 @@ .issue-box { @include border-radius(2px); - display: inline-block; - padding: 10px $gl-padding; + display: block; + float: left; + padding: 0 $gl-padding; font-weight: normal; margin-right: 10px; font-size: $gl-font-size; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 3a08ee70bc..51d8e5b465 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -51,11 +51,12 @@ .issuable-details { .page-title { - margin-top: -15px; - padding: 10px 0; + margin-top: -$gl-padding; + padding: 7px 0; margin-bottom: 0; color: #5c5d5e; font-size: 16px; + line-height: 42px; .author { color: #5c5d5e; From aa01b23937d05d4110cf24683bbe96dcc77c730c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:47:19 +0100 Subject: [PATCH 221/908] Fix padding around editor path, input and select box. --- app/assets/stylesheets/pages/editor.scss | 48 +++++++++-------------- app/views/projects/blob/_editor.html.haml | 22 +++++------ 2 files changed, 30 insertions(+), 40 deletions(-) diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index e2c521af91..39d916cd33 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -19,48 +19,38 @@ color: #B94A48; } } - .commit-button-annotation { - display: inline-block; - margin: 0; - padding: 2px; - - > * { - float: left; - } - - .message { - display: inline-block; - margin: 5px 8px 0 8px; - } - } .file-title { @extend .monospace; + + line-height: 42px; + padding-top: 7px; + padding-bottom: 7px; } .editor-ref { background: $background-color; - padding: 11px 15px; + padding-right: $gl-padding; border-right: 1px solid $border-color; - display: inline-block; - margin: -5px -5px; + display: block; + float: left; margin-right: 10px; } .editor-file-name { - .new-file-name { - display: inline-block; - width: 450px; - } - - .form-control { - margin-top: -3px; - } + @extend .monospace; + + float: left; + margin-right: 10px; } - .form-actions { - margin: -$gl-padding; - margin-top: 0; - padding: $gl-padding + .new-file-name { + display: inline-block; + width: 450px; + float: left; + } + + .select2 { + float: right; } } diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index f1ad0c3c40..333f5d470e 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -1,19 +1,19 @@ -.file-holder.file - .file-title +.file-holder.file.append-bottom-default + .file-title.clearfix .editor-ref - %i.fa.fa-code-fork + = icon('code-fork') = ref %span.editor-file-name - - if @path - %span.monospace - = @path + = @path - - if current_action?(:new) || current_action?(:create) + - if current_action?(:new) || current_action?(:create) + %span.editor-file-name \/ - = text_field_tag 'file_name', params[:file_name], placeholder: "File name", - required: true, class: 'form-control new-file-name js-quick-submit' - .pull-right - = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control' + = text_field_tag 'file_name', params[:file_name], placeholder: "File name", + required: true, class: 'form-control new-file-name js-quick-submit' + + .pull-right + = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' .file-content.code %pre.js-edit-mode-pane#editor From 0159d78a3b7dbc58f74c90c41adf8ef3049ed0aa Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:48:06 +0100 Subject: [PATCH 222/908] Remove padding around form-actions --- app/assets/stylesheets/framework/forms.scss | 7 ++++--- app/views/profiles/notifications/show.html.haml | 2 +- app/views/projects/releases/edit.html.haml | 2 +- app/views/shared/issuable/_form.html.haml | 3 ++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 0edfe24f19..ddb522bb63 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -22,9 +22,10 @@ input[type='text'].danger { } .form-actions { - padding: 17px 20px 18px; - margin-top: 18px; - margin-bottom: 18px; + margin: -$gl-padding; + margin-top: 0; + margin-bottom: -$gl-padding; + padding: $gl-padding; background-color: $background-color; border-top: 1px solid $border-color; } diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index d4d9f24627..0bcadc965f 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -50,7 +50,7 @@ Watch %p You will receive notifications for any activity - .form-actions + .gray-content-block = f.submit 'Save changes', class: "btn btn-create" .row.all-notifications.prepend-top-default diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index f516b65ecd..bc80f2f29a 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -14,6 +14,6 @@ = render 'projects/zen', f: f, attr: :description, classes: 'description js-quick-submit form-control' = render 'projects/notes/hints' .error-alert - .prepend-top-default + .form-actions.prepend-top-default = f.submit 'Save changes', class: 'btn btn-save' = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 0fc74d7d2b..7558b37f83 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -93,7 +93,8 @@ %p.help-block = link_to 'Change branches', mr_change_branches_path(@merge_request) -.form-actions +- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) +.gray-content-block{class: (is_footer ? "footer-block" : "middle-block")} - if !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) && !issuable.persisted? %p Please review the From 3ad1d320ef3f3aa5a0896a9b30a2fba791696f58 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:48:51 +0100 Subject: [PATCH 223/908] Remove duplicated styling for center top menus --- app/assets/stylesheets/pages/commit.scss | 13 ----------- .../stylesheets/pages/merge_requests.scss | 23 ++----------------- app/views/projects/commit/_ci_menu.html.haml | 2 +- app/views/projects/diffs/_diffs.html.haml | 2 +- .../merge_requests/_discussion.html.haml | 2 +- .../merge_requests/_new_submit.html.haml | 5 ++-- .../projects/merge_requests/_show.html.haml | 2 +- .../merge_requests/show/_commits.html.haml | 2 +- 8 files changed, 9 insertions(+), 42 deletions(-) diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index a0e5f7554e..74bbe0880f 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -108,16 +108,3 @@ z-index: 2; } } - -.commit-ci-menu { - padding: 0; - margin: 0; - list-style: none; - margin-top: 5px; - height: 56px; - margin: -16px; - padding: 16px; - text-align: center; - margin-top: 0px; - margin-bottom: 2px; -} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 177cf6ca45..017a86bcd9 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -4,7 +4,6 @@ */ .mr-state-widget { background: #F7F8FA; - margin-bottom: 20px; color: $gl-gray; border: 1px solid #dce0e6; @include border-radius(2px); @@ -116,26 +115,8 @@ } } -.merge-request .merge-request-tabs { - @include nav-menu; - margin: -$gl-padding; - padding: $gl-padding; - text-align: center; - margin-bottom: 1px; -} - -// Mobile -@media (max-width: 480px) { - .merge-request .merge-request-tabs { - margin: 0; - padding: 0; - - li { - a { - padding: 0; - } - } - } +.merge-request-details { + margin-bottom: $gl-padding; } .mr_source_commit, diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index c73ba74f5e..76dc87a882 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -1,4 +1,4 @@ -%ul.center-top-menu.commit-ci-menu +%ul.center-top-menu.no-top.no-bottom.commit-ci-menu = nav_link(path: 'commit#show') do = link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do Changes diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 416fb4da07..f9d661d59d 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -3,7 +3,7 @@ - diff_files = safe_diff_files(diffs) -.gray-content-block.second-block.oneline-block +.gray-content-block.middle-block.oneline-block .inline-parallel-buttons .btn-group = inline_diff_btn diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 2b3c3eff5e..4a192aeb2c 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -7,7 +7,7 @@ = render 'shared/show_aside' -.gray-content-block.second-block.oneline-block +.gray-content-block.middle-block.oneline-block .row .col-md-9 .votes-holder.pull-right diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 6244d3ba0b..72132344c8 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -19,7 +19,7 @@ = f.hidden_field :target_branch .mr-compare.merge-request - %ul.merge-request-tabs + %ul.merge-request-tabs.center-top-menu.no-top.no-bottom %li.commits-tab = link_to url_for(params), data: {target: '#commits', action: 'commits', toggle: 'tab'} do Commits @@ -31,7 +31,7 @@ .tab-content #commits.commits.tab-pane - = render "projects/commits/commits", project: @project + = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane.active - if @diffs.present? = render "projects/diffs/diffs", diffs: @diffs, project: @project @@ -57,4 +57,3 @@ diffs_loaded: true, commits_loaded: true }); - diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 94bd154aeb..e7eb006659 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -40,7 +40,7 @@ = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - if @commits.present? - %ul.merge-request-tabs + %ul.merge-request-tabs.center-top-menu.no-top.no-bottom %li.notes-tab = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#notes', action: 'notes', toggle: 'tab'} do Discussion diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index 478054db51..7f904ec42a 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -1,4 +1,4 @@ -.gray-content-block.second-block.oneline-block +.gray-content-block.middle-block.oneline-block = icon("sort-amount-desc") Most recent commits displayed first From 1c595681e5b60cfdf402abc6fd43c282897b6260 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:49:29 +0100 Subject: [PATCH 224/908] Remove double border between gray blocks --- app/assets/stylesheets/framework/blocks.scss | 4 ++++ app/views/projects/commit/show.html.haml | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 1635df9c97..dec50a0e0f 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -114,3 +114,7 @@ right: 10px; } } + +.block-connector { + margin-top: -1px; +} diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 85e203cbe5..069b8b1f16 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,6 +1,9 @@ - page_title "#{@commit.title} (#{@commit.short_id})", "Commits" = render "projects/commits/header_title" = render "commit_box" -= render "ci_menu" if @ci_commit +- if @ci_commit + = render "ci_menu" +- else + %div.block-connector = render "projects/diffs/diffs", diffs: @diffs, project: @project = render "projects/notes/notes_with_form" From 9ea72430921efaa4a1a79ea21c54763bda8bba78 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:49:46 +0100 Subject: [PATCH 225/908] Fix padding of no readme and empty repo messages --- app/views/projects/_readme.html.haml | 25 +++++++++++++------------ app/views/projects/empty.html.haml | 13 ++++++------- app/views/projects/show.html.haml | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index b5ef0aca54..d1191928d4 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -7,15 +7,16 @@ = cache(readme_cache_key) do = render_readme(readme) - else - %h3.page-title - This project does not have README yet - - if can?(current_user, :push_code, @project) - %p.slead - A - %code README - file contains information about other files in a repository and is commonly - distributed with computer software, forming part of its documentation. - %br - We recommend you to - = link_to "add README", new_readme_path, class: 'underlined-link' - file to the repository and GitLab will render it here instead of this message. + .gray-content-block.second-block.center + %h3.page-title + This project does not have README yet + - if can?(current_user, :push_code, @project) + %p + A + %code README + file contains information about other files in a repository and is commonly + distributed with computer software, forming part of its documentation. + %p + We recommend you to + = link_to "add README", new_readme_path, class: 'underlined-link' + file to the repository and GitLab will render it here instead of this message. diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index c3858e78ca..950ab33825 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -5,17 +5,16 @@ = render "home_panel" -.gray-content-block.center +.gray-content-block.second-block.center %h3.page-title The repository for this project is empty - - if can?(current_user, :download_code, @project) + - if can?(current_user, :push_code, @project) %p If you already have files you can push them using command line instructions below. - %br - - if can?(current_user, :push_code, @project) - Otherwise you can start with - = link_to "adding README", new_readme_path, class: 'underlined-link' - file to this project. + %p + Otherwise you can start with + = link_to "adding README", new_readme_path, class: 'underlined-link' + file to this project. - if can?(current_user, :download_code, @project) .prepend-top-20 diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 585caf674c..9c7a5584da 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -11,7 +11,7 @@ = render "home_panel" -.project-stats.gray-content-block +.project-stats.gray-content-block.second-block %ul.nav.nav-pills %li = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do From ba4bf27fe486af1790844ac40bc188c6462720b8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:49:59 +0100 Subject: [PATCH 226/908] Use table-holder where appropriate --- app/views/profiles/applications.html.haml | 84 ++++++++++---------- app/views/profiles/keys/_key_table.html.haml | 16 ++-- 2 files changed, 51 insertions(+), 49 deletions(-) diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml index 2342936a5d..0436c2213d 100644 --- a/app/views/profiles/applications.html.haml +++ b/app/views/profiles/applications.html.haml @@ -15,24 +15,25 @@ .pull-right = link_to 'New Application', new_oauth_application_path, class: 'btn btn-success' - if @applications.any? - %table.table.table-striped - %thead - %tr - %th Name - %th Callback URL - %th Clients - %th - %th - %tbody - - @applications.each do |application| - %tr{:id => "application_#{application.id}"} - %td= link_to application.name, oauth_application_path(application) - %td - - application.redirect_uri.split.each do |uri| - %div= uri - %td= application.access_tokens.count - %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm' - %td= render 'doorkeeper/applications/delete_form', application: application + .table-holder + %table.table.table-striped + %thead + %tr + %th Name + %th Callback URL + %th Clients + %th + %th + %tbody + - @applications.each do |application| + %tr{:id => "application_#{application.id}"} + %td= link_to application.name, oauth_application_path(application) + %td + - application.redirect_uri.split.each do |uri| + %div= uri + %td= application.access_tokens.count + %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm' + %td= render 'doorkeeper/applications/delete_form', application: application .oauth-authorized-applications.prepend-top-20 - if user_oauth_applications? @@ -40,29 +41,30 @@ Authorized applications - if @authorized_tokens.any? - %table.table.table-striped - %thead - %tr - %th Name - %th Authorized At - %th Scope - %th - %tbody - - @authorized_apps.each do |app| - - token = app.authorized_tokens.order('created_at desc').first - %tr{:id => "application_#{app.id}"} - %td= app.name - %td= token.created_at - %td= token.scopes - %td= render 'doorkeeper/authorized_applications/delete_form', application: app - - @authorized_anonymous_tokens.each do |token| + .table-holder + %table.table.table-striped + %thead %tr - %td - Anonymous - %div.help-block - %em Authorization was granted by entering your username and password in the application. - %td= token.created_at - %td= token.scopes - %td= render 'doorkeeper/authorized_applications/delete_form', token: token + %th Name + %th Authorized At + %th Scope + %th + %tbody + - @authorized_apps.each do |app| + - token = app.authorized_tokens.order('created_at desc').first + %tr{:id => "application_#{app.id}"} + %td= app.name + %td= token.created_at + %td= token.scopes + %td= render 'doorkeeper/authorized_applications/delete_form', application: app + - @authorized_anonymous_tokens.each do |token| + %tr + %td + Anonymous + %div.help-block + %em Authorization was granted by entering your username and password in the application. + %td= token.created_at + %td= token.scopes + %td= render 'doorkeeper/authorized_applications/delete_form', token: token - else %p.light You don't have any authorized applications diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml index ef0075aad3..8c9d546af4 100644 --- a/app/views/profiles/keys/_key_table.html.haml +++ b/app/views/profiles/keys/_key_table.html.haml @@ -1,6 +1,6 @@ - is_admin = defined?(admin) ? true : false -.panel.panel-default - - if @keys.any? +- if @keys.any? + .table-holder %table.table %thead.panel-heading %tr @@ -11,9 +11,9 @@ %tbody - @keys.each do |key| = render 'profiles/keys/key', key: key, is_admin: is_admin - - else - .nothing-here-block - - if is_admin - User has no ssh keys - - else - There are no SSH keys with access to your account. +- else + .nothing-here-block + - if is_admin + User has no ssh keys + - else + There are no SSH keys with access to your account. From d5fd9b26b13678a9e09a56890af10babe46362f0 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:50:12 +0100 Subject: [PATCH 227/908] Remove style duplication between snippet and issue detail pages --- app/assets/stylesheets/pages/snippets.scss | 44 +++------------------ app/views/shared/snippets/_header.html.haml | 7 ++-- 2 files changed, 9 insertions(+), 42 deletions(-) diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index 242783a7b7..bb74e50151 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -27,56 +27,22 @@ } .snippet-holder { - .snippet-details { - .page-title { - margin-top: -15px; - padding: 10px 0; - margin-bottom: 0; - color: #5c5d5e; - font-size: 16px; - - .author { - color: #5c5d5e; - } - - .snippet-id { - color: #5c5d5e; - } - } - - .snippet-title { - margin: 0; - font-size: 23px; - color: #313236; - } - - @media (max-width: $screen-md-max) { - .new-snippet-link { - display: none; - } - } - - @media (max-width: $screen-sm-max) { - .creator, - .page-title .btn-close { - display: none; - } - } - } + margin-bottom: -$gl-padding; .file-holder { border-top: 0; } } - .snippet-box { @include border-radius(2px); - display: inline-block; - padding: 10px $gl-padding; + display: block; + float: left; + padding: 0 $gl-padding; font-weight: normal; margin-right: 10px; font-size: $gl-font-size; border: 1px solid; + line-height: 40px; } diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 0a4a790ec5..3524102928 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -1,9 +1,9 @@ -.snippet-details +.issuable-details .page-title .snippet-box{class: visibility_level_color(@snippet.visibility_level)} = visibility_level_icon(@snippet.visibility_level) = visibility_level_label(@snippet.visibility_level) - %span.snippet-id Snippet ##{@snippet.id} + Snippet ##{@snippet.id} %span.creator · created by #{link_to_member(@project, @snippet.author, size: 24)} · @@ -19,6 +19,7 @@ = render "projects/snippets/actions" - else = render "snippets/actions" + .gray-content-block.middle-block - %h2.snippet-title + %h2.issue-title = gfm escape_once(@snippet.title) From 09396b2eaa529a7c9049e482142c3885c4510b90 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:50:41 +0100 Subject: [PATCH 228/908] Render all form controls at the same height --- app/assets/stylesheets/framework/forms.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index ddb522bb63..80283a56ec 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -74,6 +74,8 @@ label { .form-control { @include box-shadow(none); + height: 42px; + padding: 8px $gl-padding; } .wiki-content { From 8196d269ebcc15925bf532a9edcccf75248bfffb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:50:50 +0100 Subject: [PATCH 229/908] No bottom margin for form help blocks --- app/assets/stylesheets/framework/forms.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 80283a56ec..cc92966c45 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -95,3 +95,7 @@ label { background-color: #f7f8fa; } } + +.help-block { + margin-bottom: 0; +} From ac389c6a3ec7f54174edb243b54b279a4916c30a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:51:10 +0100 Subject: [PATCH 230/908] Consistent styling for dashboard groups gray block --- app/views/dashboard/groups/index.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index f3f3f58111..d5b7e729e7 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -8,8 +8,8 @@ = link_to new_group_path, class: "btn btn-new" do %i.fa.fa-plus New Group - .title Welcome to the groups! - Group members have access to all group projects. + .oneline + Group members have access to all group projects. %ul.content-list - @group_members.each do |group_member| From 6ec9999a8e49e5374fe9e10dc60dc98a7ede1075 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:51:25 +0100 Subject: [PATCH 231/908] Use consistent border color --- app/assets/stylesheets/pages/note_form.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 02db44c8eb..e1a72af001 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -79,8 +79,8 @@ padding: $gl-padding; margin-left: -$gl-padding; margin-right: -$gl-padding; - border-right: 1px solid #ECEEF1; - border-top: 1px solid #ECEEF1; + border-right: 1px solid $border-color; + border-top: 1px solid $border-color; margin-bottom: -$gl-padding; } From aec4b290fd154ac382b49aefed4e34386785630f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:51:37 +0100 Subject: [PATCH 232/908] Remove unused style --- app/assets/stylesheets/pages/projects.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index b3054e3624..ac2d50f9c1 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -18,10 +18,6 @@ } } -.project-edit-content { - padding: 7px; -} - .project-name-holder { .help-inline { vertical-align: top; From 9517b6211782619ba4d4f9f1322230bd833c0a2c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:20:11 +0100 Subject: [PATCH 233/908] Link MR list item branch name to branch --- app/assets/stylesheets/framework/common.scss | 4 ---- app/views/projects/merge_requests/_merge_request.html.haml | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 61689aff57..1c7c31b02a 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -337,10 +337,6 @@ table { text-align: center; } -.task-status { - margin-left: 10px; -} - #nprogress .spinner { top: 15px !important; right: 10px !important; diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 8246a432c7..60dd4f4f87 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -44,8 +44,8 @@ = link_to_label(label, project: merge_request.project) - if merge_request.target_project.default_branch != merge_request.target_branch   - %span - %i.fa.fa-code-fork + = link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do + = icon('code-fork') = merge_request.target_branch - if merge_request.tasks?   From 080a0c026bbd2f651eab68c816991e31e57e11ef Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:52:36 +0100 Subject: [PATCH 234/908] Use same text color for commit box as issue title --- app/assets/stylesheets/pages/commit.scss | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index a0e5f7554e..39cde517c1 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -2,10 +2,6 @@ display: block; } -.commit-title{ - margin-bottom: 10px; -} - .commit-author, .commit-committer{ display: block; color: #999; @@ -41,6 +37,8 @@ .commit-box { .commit-title { margin: 0; + font-size: 23px; + color: #313236; } .commit-description { From 67ff47b39cfa47a6904bd5e4e84499a098a158cf Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:53:11 +0100 Subject: [PATCH 235/908] Capitalize tab titles --- app/helpers/blob_helper.rb | 2 +- app/views/projects/blob/edit.html.haml | 6 +++--- features/steps/project/source/browse_files.rb | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 77d99140c4..df5f5fae23 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -60,7 +60,7 @@ module BlobHelper if Gitlab::MarkupHelper.previewable?(filename) 'Preview' else - 'Preview changes' + 'Preview Changes' end end diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 5674516525..a47fe7ede8 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -5,12 +5,12 @@ %ul.center-top-menu.no-bottom.js-edit-mode %li.active = link_to '#editor' do - %i.fa.fa-edit - Edit file + = icon('edit') + Edit File %li = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do - %i.fa.fa-eye + = icon('eye') = editing_preview_title(@blob.name) = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input js-edit-blob-form') do diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index f40e0f0d52..bee3150677 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -87,7 +87,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I click link "Diff"' do - click_link 'Preview changes' + click_link 'Preview Changes' end step 'I click on "Commit Changes"' do From e9e9f537e84c1b3a1df7b4920d3b696029ba6dcc Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:53:20 +0100 Subject: [PATCH 236/908] Truncate submodule commit SHAs to 7 characters --- app/helpers/diff_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index bfd3622a6a..24134310fc 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -146,9 +146,9 @@ module DiffHelper def submodule_link(blob, ref, repository = @repository) tree, commit = submodule_links(blob, ref, repository) commit_id = if commit.nil? - blob.id[0..10] + Commit.truncate_sha(blob.id) else - link_to "#{blob.id[0..10]}", commit + link_to Commit.truncate_sha(blob.id), commit end [ From b8a926fab0acc85dd22456366390a93f4c4b6ad9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:53:28 +0100 Subject: [PATCH 237/908] Don't use success state for explore projects search button --- app/views/explore/projects/_filter.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 2761272aa8..28b12c8dca 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -3,7 +3,7 @@ .form-group = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "projects_search", spellcheck: false .form-group - = button_tag 'Search', class: "btn btn-success" + = button_tag 'Search', class: "btn" .pull-right.hidden-sm.hidden-xs - if current_user From 12dd4c7c8a393cea2f264832e38df4286591f48c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:53:56 +0100 Subject: [PATCH 238/908] Use "Delete" in milestone and label delete buttons instead of "Remove" --- app/views/projects/labels/_label.html.haml | 2 +- app/views/projects/milestones/_milestone.html.haml | 2 +- features/steps/admin/labels.rb | 2 +- features/steps/project/issues/labels.rb | 2 +- features/steps/project/issues/milestones.rb | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index c6ebfa281a..b70a9fc9fe 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -7,4 +7,4 @@ - if can? current_user, :admin_label, @project = link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm' - = link_to 'Remove', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} + = link_to 'Delete', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index 5e93d55b1f..334172b976 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -31,4 +31,4 @@ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close" = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do %i.fa.fa-trash-o - Remove + Delete diff --git a/features/steps/admin/labels.rb b/features/steps/admin/labels.rb index b45d98658b..2d5db8f739 100644 --- a/features/steps/admin/labels.rb +++ b/features/steps/admin/labels.rb @@ -17,7 +17,7 @@ class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps step 'I remove label \'bug\'' do page.within "#label_#{bug_label.id}" do - click_link 'Remove' + click_link 'Delete' end end diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index d656acf422..047cf701bb 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -9,7 +9,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps step 'I remove label \'bug\'' do page.within "#label_#{bug_label.id}" do - click_link 'Remove' + click_link 'Delete' end end diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb index c8708572ec..e2eda51149 100644 --- a/features/steps/project/issues/milestones.rb +++ b/features/steps/project/issues/milestones.rb @@ -63,7 +63,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps end step 'I click link to remove milestone' do - click_link 'Remove' + click_link 'Delete' end step 'I should see no milestones' do From f8e577591b29b4cde211f275db6f870f98f58eb3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:54:12 +0100 Subject: [PATCH 239/908] Don't use success state for "Download zip" button --- app/views/projects/repositories/_download_archive.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index 07c24950ee..b9486a9b49 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -3,10 +3,10 @@ - split_button = split_button || false - if split_button == true %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn btn-success col-xs-10', rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do %i.fa.fa-download %span Download zip - %a.col-xs-2.btn.btn-success.dropdown-toggle{ 'data-toggle' => 'dropdown' } + %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } %span.caret %span.sr-only Select Archive Format From 40760d3f7bc1b5fbed17bff2f185e92538590484 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:54:24 +0100 Subject: [PATCH 240/908] Add header title to import pages --- app/views/import/bitbucket/status.html.haml | 1 + app/views/import/fogbugz/new.html.haml | 1 + app/views/import/fogbugz/new_user_map.html.haml | 1 + app/views/import/fogbugz/status.html.haml | 1 + app/views/import/github/status.html.haml | 1 + app/views/import/gitlab/status.html.haml | 1 + app/views/import/gitorious/status.html.haml | 1 + app/views/import/google_code/new.html.haml | 3 ++- .../import/google_code/new_user_map.html.haml | 17 +++++++++-------- app/views/import/google_code/status.html.haml | 1 + 10 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 1f09a27e2d..aec2e836c9 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -1,4 +1,5 @@ - page_title "Bitbucket import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-bitbucket Import projects from Bitbucket diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml index e1bb88ca4e..5515fad6f4 100644 --- a/app/views/import/fogbugz/new.html.haml +++ b/app/views/import/fogbugz/new.html.haml @@ -1,4 +1,5 @@ - page_title "FogBugz Import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-bug Import projects from FogBugz diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml index bc3c90294e..07338736ba 100644 --- a/app/views/import/fogbugz/new_user_map.html.haml +++ b/app/views/import/fogbugz/new_user_map.html.haml @@ -1,4 +1,5 @@ - page_title 'User map', 'FogBugz import' +- header_title "Projects", root_path %h3.page-title %i.fa.fa-bug Import projects from FogBugz diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml index b902006597..6ee16c8be4 100644 --- a/app/views/import/fogbugz/status.html.haml +++ b/app/views/import/fogbugz/status.html.haml @@ -1,4 +1,5 @@ - page_title "FogBugz import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-bug Import projects from FogBugz diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 0699321c8c..1416ee5bd5 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -1,4 +1,5 @@ - page_title "GitHub import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-github Import projects from GitHub diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index f4a2b33af2..911a55eb85 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -1,4 +1,5 @@ - page_title "GitLab.com import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-heart Import projects from GitLab.com diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml index 71752d21ef..6b0fa1edf8 100644 --- a/app/views/import/gitorious/status.html.haml +++ b/app/views/import/gitorious/status.html.haml @@ -1,4 +1,5 @@ - page_title "Gitorious import" +- header_title "Projects", root_path %h3.page-title %i.icon-gitorious.icon-gitorious-big Import projects from Gitorious.org diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml index 9c64e0a009..5d2f149cd5 100644 --- a/app/views/import/google_code/new.html.haml +++ b/app/views/import/google_code/new.html.haml @@ -1,4 +1,5 @@ - page_title "Google Code import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-google Import projects from Google Code @@ -6,7 +7,7 @@ = form_tag callback_import_google_code_path, class: 'form-horizontal', multipart: true do %p - Follow the steps below to export your Google Code project data. + Follow the steps below to export your Google Code project data. In the next step, you'll be able to select the projects you want to import. %ol %li diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml index e53ebda7dc..0738b3db1e 100644 --- a/app/views/import/google_code/new_user_map.html.haml +++ b/app/views/import/google_code/new_user_map.html.haml @@ -1,4 +1,5 @@ - page_title "User map", "Google Code import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-google Import projects from Google Code @@ -8,31 +9,31 @@ %p Customize how Google Code email addresses and usernames are imported into GitLab. In the next step, you'll be able to select the projects you want to import. - %p + %p The user map is a JSON document mapping the Google Code users that participated on your projects to the way their email addresses and usernames will be imported into GitLab. You can change this by changing the value on the right hand side of :. Be sure to preserve the surrounding double quotes, other punctuation and the email address or username on the left hand side. %ul %li %strong Default: Directly import the Google Code email address or username %p - "johnsmith@example.com": "johnsm...@example.com" - will add "By johnsm...@example.com" to all issues and comments originally created by johnsmith@example.com. + "johnsmith@example.com": "johnsm...@example.com" + will add "By johnsm...@example.com" to all issues and comments originally created by johnsmith@example.com. The email address or username is masked to ensure the user's privacy. %li %strong Map a Google Code user to a GitLab user %p - "johnsmith@example.com": "@johnsmith" - will add "By @johnsmith" to all issues and comments originally created by johnsmith@example.com, + "johnsmith@example.com": "@johnsmith" + will add "By @johnsmith" to all issues and comments originally created by johnsmith@example.com, and will set @johnsmith as the assignee on all issues originally assigned to johnsmith@example.com. %li %strong Map a Google Code user to a full name %p - "johnsmith@example.com": "John Smith" + "johnsmith@example.com": "John Smith" will add "By John Smith" to all issues and comments originally created by johnsmith@example.com. %li %strong Map a Google Code user to a full email address %p - "johnsmith@example.com": "johnsmith@example.com" - will add "By johnsmith@example.com" to all issues and comments originally created by johnsmith@example.com. + "johnsmith@example.com": "johnsmith@example.com" + will add "By johnsmith@example.com" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address. .form-group diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index 8c64fd27e6..175ef6921c 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -1,4 +1,5 @@ - page_title "Google Code import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-google Import projects from Google Code From 0ec0c93824096fc6b26de369e170ffc729967bd3 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 2 Dec 2015 15:54:44 +0200 Subject: [PATCH 241/908] Also update gitlab-workhorse in patch updates [ci skip] --- doc/update/patch_versions.md | 45 +++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index 593722eb01..957354decb 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -6,7 +6,8 @@ For example from 7.14.0 to 7.14.3, also see the [semantic versioning specificati ### 0. Backup It's useful to make a backup just in case things go south: -(With MySQL, this may require granting "LOCK TABLES" privileges to the GitLab user on the database version) +(With MySQL, this may require granting "LOCK TABLES" privileges to the GitLab +user on the database version) ```bash cd /home/git/gitlab @@ -15,19 +16,23 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ### 1. Stop server - sudo service gitlab stop +```bash +sudo service gitlab stop +``` ### 2. Get latest code for the stable branch +In the commands below, replace `LATEST_TAG` with the latest GitLab tag you want +to update to, for example `v8.0.3`. Use `git tag -l 'v*.[0-9]' --sort='v:refname'` +to see a list of all tags. Make sure to update patch versions only (check your +current version with `cat VERSION`). + ```bash cd /home/git/gitlab sudo -u git -H git fetch --all sudo -u git -H git checkout -- Gemfile.lock db/schema.rb sudo -u git -H git checkout LATEST_TAG -b LATEST_TAG ``` -Replace `LATEST_TAG` with the latest GitLab tag you want to update to, for example `v8.0.3`. -Use `git tag -l 'v*.[0-9]' --sort='v:refname'` to see a list of all tags. -Make sure to update patch versions only (check your current version with `cat VERSION`) ### 3. Update gitlab-shell to the corresponding version @@ -37,12 +42,20 @@ sudo -u git -H git fetch sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` ``` -### 4. Install libs, migrations, etc. +### 4. Update gitlab-workhorse to the corresponding version + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch +sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` -b v`cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` +``` + +### 5. Install libs, migrations, etc. ```bash cd /home/git/gitlab -#PostgreSQL +# PostgreSQL sudo -u git -H bundle install --without development test mysql --deployment # MySQL @@ -52,19 +65,25 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production ``` -### 5. Start application +### 6. Start application - sudo service gitlab start - sudo service nginx restart +```bash +sudo service gitlab start +sudo service nginx restart +``` -### 6. Check application status +### 7. Check application status Check if GitLab and its environment are configured correctly: - sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +```bash +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` To make sure you didn't miss anything run a more thorough check with: - sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +```bash +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` If all items are green, then congratulations upgrade complete! From 90df3701e753257a7b71d97c8eec590cc148f409 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:54:50 +0100 Subject: [PATCH 242/908] Add plus icon to all "Add X" buttons --- app/views/profiles/keys/index.html.haml | 4 +++- app/views/projects/branches/index.html.haml | 2 +- app/views/projects/labels/index.html.haml | 1 + app/views/projects/tags/index.html.haml | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index 14adba1c79..17a4195030 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -3,7 +3,9 @@ .gray-content-block.top-block .pull-right - = link_to "Add SSH Key", new_profile_key_path, class: "btn btn-new" + = link_to new_profile_key_path, class: "btn btn-new" do + = icon('plus') + Add SSH Key .oneline Before you can add an SSH key you need to = link_to "generate it.", help_page_path("ssh", "README") diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 03ade02a0c..204def6079 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -5,7 +5,7 @@ .pull-right - if can? current_user, :push_code, @project = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do - %i.fa.fa-add-sign + = icon('plus') New branch   .dropdown.inline diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index fb784ee5f4..9081bcfe9b 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -4,6 +4,7 @@ .gray-content-block.top-block - if can? current_user, :admin_label, @project = link_to new_namespace_project_label_path(@project.namespace, @project), class: "pull-right btn btn-new" do + = icon('plus') New label .oneline Labels can be applied to issues and merge requests. diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 85d76eae3b..760347de0a 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -6,7 +6,7 @@ - if can? current_user, :push_code, @project .pull-right = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do - %i.fa.fa-add-sign + = icon('plus') New tag .oneline Tags give the ability to mark specific points in history as being important From a3da3d8e3b60f8eeca716ce231ab5670a637e3eb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:54:58 +0100 Subject: [PATCH 243/908] Use monospace font for commit SHA in branch list --- app/views/projects/branches/_commit.html.haml | 2 +- app/views/projects/tree/_tree_content.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml index 22d77dda93..9fe65cbb10 100644 --- a/app/views/projects/branches/_commit.html.haml +++ b/app/views/projects/branches/_commit.html.haml @@ -1,5 +1,5 @@ .branch-commit - = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-id" + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-id monospace" · %span.str-truncated = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index c64e684df2..1bc90edd8f 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -12,7 +12,7 @@ %i.fa.fa-angle-right   %small.light - = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit) + = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" – = truncate(@commit.title, length: 50) = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right' From 59c3ec4e67f598e1a67df233f2d12fbec3d033dc Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:55:36 +0100 Subject: [PATCH 244/908] Use file-type specific file icon in blame view and diff item --- app/views/projects/blame/show.html.haml | 3 +-- app/views/projects/diffs/_file.html.haml | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 6518c4173e..8d9ec068a4 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -6,7 +6,7 @@ #tree-holder.tree-holder .file-holder .file-title - %i.fa.fa-file + = blob_icon @blob.mode, @blob.name %strong = @path %small= number_to_human_size @blob.size @@ -43,4 +43,3 @@ - blame_group[:lines].each do |line| :erb <%= highlight(@blob.name, line, nowrap: true, continue: true).html_safe %> - diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index c745b4e69b..b3392d00e0 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -2,19 +2,27 @@ .diff-header{id: "file-path-#{hexdigest(diff_file.file_path)}"} - if diff_file.diff.submodule? %span + = icon('archive fw') - submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path) - = submodule_link(submodule_item, @commit.id, project.repository) + %strong + = submodule_link(submodule_item, @commit.id, project.repository) - else %span + = blob_icon blob.mode, blob.name + = link_to "#diff-#{i}" do + %strong + = diff_file.new_path + - if diff_file.deleted_file - = "#{diff_file.old_path} deleted" + deleted - elsif diff_file.renamed_file - = "#{diff_file.old_path} renamed to #{diff_file.new_path}" - - else - = diff_file.new_path + renamed from + %strong + = diff_file.old_path - if diff_file.mode_changed? - %span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" + %small + = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" .diff-controls - if blob.text? From 854deae4129c002faf97f615c17e742e8eff9166 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:56:17 +0100 Subject: [PATCH 245/908] Brefer "Directory" over "Dir", "Files" over "Code --- app/helpers/commits_helper.rb | 4 ++-- app/views/projects/commit/_commit_box.html.haml | 5 +++-- features/steps/project/source/browse_files.rb | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 9df20c9fce..590d20ac7b 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -109,7 +109,7 @@ module CommitsHelper ) elsif @path.present? return link_to( - "Browse Dir »", + "Browse Directory »", namespace_project_tree_path(project.namespace, project, tree_join(commit.id, @path)), class: "pull-right" @@ -117,7 +117,7 @@ module CommitsHelper end end link_to( - "Browse Code »", + "Browse Files »", namespace_project_tree_path(project.namespace, project, commit), class: "pull-right" ) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 776768537d..d8bfe6a07a 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -13,8 +13,9 @@ - unless @commit.parents.length > 1 %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) %li= link_to "Plain Diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff) - = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-primary btn-grouped" do - %span Browse Code » + = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-grouped" do + = icon('files-o') + Browse Files %div %p diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index bee3150677..ceab23b9a4 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -192,7 +192,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I see Browse dir link' do - expect(page).to have_link 'Browse Dir »' + expect(page).to have_link 'Browse Directory »' expect(page).not_to have_link 'Browse Code »' end @@ -204,13 +204,13 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps step 'I see Browse file link' do expect(page).to have_link 'Browse File »' - expect(page).not_to have_link 'Browse Code »' + expect(page).not_to have_link 'Browse Files »' end step 'I see Browse code link' do - expect(page).to have_link 'Browse Code »' + expect(page).to have_link 'Browse Files »' expect(page).not_to have_link 'Browse File »' - expect(page).not_to have_link 'Browse Dir »' + expect(page).not_to have_link 'Browse Directory »' end step 'I click on Permalink' do From 03277af65d111be7832fab060b3de8c42f885847 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:57:11 +0100 Subject: [PATCH 246/908] Use standard style for tabs in profile view --- app/views/users/show.html.haml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index d5a92cb816..e35e69bda8 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -73,7 +73,7 @@ .user-calendar-activities -%ul.center-middle-menu +%ul.center-top-menu.no-top.no-bottom %li.active = link_to "#activity", 'data-toggle' => 'tab' do Activity @@ -92,17 +92,26 @@ .tab-content .tab-pane.active#activity + .gray-content-block.middle-block + Public activity by #{@user.name} + .content_list = spinner - if @groups.any? .tab-pane#groups + .gray-content-block.middle-block + Groups #{@user.name} is a member of + %ul.content-list - @groups.each do |group| = render 'shared/groups/group', group: group - if @contributed_projects.present? .tab-pane#contributed + .gray-content-block.middle-block + Projects #{@user.name} has contributed to + .contributed-projects = render 'shared/projects/list', projects: @contributed_projects.sort_by(&:star_count).reverse, @@ -110,6 +119,9 @@ - if @projects.present? .tab-pane#personal + .gray-content-block.middle-block + Projects owned by #{@user.name} + .personal-projects = render 'shared/projects/list', projects: @projects.sort_by(&:star_count).reverse, From c12e4de50cab653a61e15ef4eec915ae093c32d2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:57:38 +0100 Subject: [PATCH 247/908] Move "Start new merge request" checkbox closer to New branch field --- app/assets/javascripts/new_commit_form.js.coffee | 6 +++--- app/views/shared/_new_commit_form.html.haml | 13 ++++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/new_commit_form.js.coffee b/app/assets/javascripts/new_commit_form.js.coffee index 2e561dea3e..3c7b776155 100644 --- a/app/assets/javascripts/new_commit_form.js.coffee +++ b/app/assets/javascripts/new_commit_form.js.coffee @@ -3,7 +3,7 @@ class @NewCommitForm @newBranch = form.find('.js-new-branch') @originalBranch = form.find('.js-original-branch') @createMergeRequest = form.find('.js-create-merge-request') - @createMergeRequestFormGroup = form.find('.js-create-merge-request-form-group') + @createMergeRequestContainer = form.find('.js-create-merge-request-container') @renderDestination() @newBranch.keyup @renderDestination @@ -12,10 +12,10 @@ class @NewCommitForm different = @newBranch.val() != @originalBranch.val() if different - @createMergeRequestFormGroup.show() + @createMergeRequestContainer.show() @createMergeRequest.prop('checked', true) unless @wasDifferent else - @createMergeRequestFormGroup.hide() + @createMergeRequestContainer.hide() @createMergeRequest.prop('checked', false) @wasDifferent = different diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 8636341c60..2931fb0c2c 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -7,12 +7,11 @@ .col-sm-10 = text_field_tag 'new_branch', @new_branch || @ref, class: "form-control js-new-branch" - .form-group.js-create-merge-request-form-group - .col-sm-offset-2.col-sm-10 - .checkbox - - nonce = SecureRandom.hex - = label_tag "create_merge_request-#{nonce}" do - = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" - Start a new merge request with this commit + .js-create-merge-request-container + .checkbox + - nonce = SecureRandom.hex + = label_tag "create_merge_request-#{nonce}" do + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" + Start a new merge request with these changes = hidden_field_tag 'original_branch', @ref, class: 'js-original-branch' From 69709bbdd094dc5e5157fffd8710791ae6efb826 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:57:51 +0100 Subject: [PATCH 248/908] Separate page title segments by middot rather than pipe --- app/helpers/page_layout_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 775cf5a3dd..9bf750124b 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -4,7 +4,8 @@ module PageLayoutHelper @page_title.push(*titles.compact) if titles.any? - @page_title.join(" | ") + # Segments are seperated by middot + @page_title.join(" \u00b7 ") end def header_title(title = nil, title_url = nil) From d2b30dd2319023bc5d553e0e7eeb1ed6e1d745cd Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:58:04 +0100 Subject: [PATCH 249/908] Fix incorrect active state for "Your Projects" tab --- app/views/dashboard/_projects_head.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index ed480b8caf..efcd7b50b4 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -1,5 +1,5 @@ %ul.center-top-menu - = nav_link(path: ['projects#index', 'root#index']) do + = nav_link(page: [dashboard_projects_path, root_path]) do = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do Your Projects = nav_link(page: starred_dashboard_projects_path) do From 7128756d9f68146c51225fec79a7a9173a805911 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:58:12 +0100 Subject: [PATCH 250/908] Extract title from copy/pasted SSH key when it ends in a newline --- app/views/profiles/keys/new.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/profiles/keys/new.html.haml b/app/views/profiles/keys/new.html.haml index 2bf207a322..11166dc6d9 100644 --- a/app/views/profiles/keys/new.html.haml +++ b/app/views/profiles/keys/new.html.haml @@ -9,7 +9,7 @@ $('#key_key').on('focusout', function(){ var title = $('#key_title'), val = $('#key_key').val(), - comment = val.match(/^\S+ \S+ (.+)$/); + comment = val.match(/^\S+ \S+ (.+)\n?$/); if( comment && comment.length > 1 && title.val() == '' ){ $('#key_title').val( comment[1] ); From 0827455710d9bd836e2bd771249bfdbd0bf1bdf4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:58:27 +0100 Subject: [PATCH 251/908] Move branch list info tags out of main link --- app/views/projects/branches/_branch.html.haml | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 3f95e2a1bf..ba6995aa67 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -3,17 +3,17 @@ %div = link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do %strong.str-truncated= branch.name -   - - if branch.name == @repository.root_ref - %span.label.label-primary default - - elsif @repository.merged_to_root_ref? branch.name - %span.label.label-info.has_tooltip(title="Merged into #{@repository.root_ref}") - merged +   + - if branch.name == @repository.root_ref + %span.label.label-primary default + - elsif @repository.merged_to_root_ref? branch.name + %span.label.label-info.has_tooltip(title="Merged into #{@repository.root_ref}") + merged - - if @project.protected_branch? branch.name - %span.label.label-success - %i.fa.fa-lock - protected + - if @project.protected_branch? branch.name + %span.label.label-success + %i.fa.fa-lock + protected .controls.hidden-xs - if create_mr_button?(@repository.root_ref, branch.name) = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-grouped btn-xs' do From 7a6986039dd2bd349bbc3dd7d97ab9bbec14e86c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:58:40 +0100 Subject: [PATCH 252/908] Only show "Manage group members" in project member list when user has access --- .../projects/project_members/_group_members.html.haml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index 0c73d7e34a..d2810f9707 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -4,10 +4,11 @@ group members %small (#{members.count}) - .pull-right - = link_to group_group_members_path(@group), class: 'btn' do - = icon('pencil-square-o') - Edit group members + - if can?(current_user, :admin_group_member, @group) + .pull-right + = link_to group_group_members_path(@group), class: 'btn' do + = icon('pencil-square-o') + Manage group members %ul.content-list - members.each do |member| = render 'groups/group_members/group_member', member: member, show_controls: false From 6789bd25ef8686e47ea2fd047e737aefd5033526 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:59:04 +0100 Subject: [PATCH 253/908] Make tree view "Add file" button smaller --- app/views/projects/tree/_tree_header.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 1115ca6b4c..12356dbcb6 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -14,7 +14,7 @@ - if allowed_tree_edit? %li %span.dropdown - %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"} + %a.dropdown-toggle.btn.btn-sm.add-to-tree{href: '#', "data-toggle" => "dropdown"} = icon('plus') %ul.dropdown-menu %li From e718bab9e16be3b1bbdcf8dada5825b7e595682d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:59:25 +0100 Subject: [PATCH 254/908] "CI build passed", not "CI build success" --- app/assets/stylesheets/pages/profile.scss | 6 ------ app/helpers/ci_status_helper.rb | 16 ++++++++++++++-- app/views/projects/_last_commit.html.haml | 2 +- app/views/projects/builds/show.html.haml | 2 +- app/views/projects/commit/_commit_box.html.haml | 2 +- .../merge_requests/widget/_heading.html.haml | 5 ++--- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 1d6ca0dfc1..95fc26a608 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -5,12 +5,6 @@ } } -.btn-build-token { - float: left; - padding: 6px 20px; - margin-right: 12px; -} - .profile-avatar-form-option { hr { margin: 10px 0; diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 0ecf77bb45..70f8c9ae22 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -8,6 +8,10 @@ module CiStatusHelper ci_icon_for_status(ci_commit.status) end + def ci_status_label(ci_commit) + ci_label_for_status(ci_commit.status) + end + def ci_status_color(ci_commit) case ci_commit.status when 'success' @@ -23,7 +27,15 @@ module CiStatusHelper def ci_status_with_icon(status) content_tag :span, class: "ci-status ci-#{status}" do - ci_icon_for_status(status) + ' '.html_safe + status + ci_icon_for_status(status) + ' '.html_safe + ci_label_for_status(status) + end + end + + def ci_label_for_status(status) + if status == 'success' + 'passed' + else + status end end @@ -46,7 +58,7 @@ module CiStatusHelper def render_ci_status(ci_commit) link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}", - title: "Build status: #{ci_commit.status}", + title: "Build status: #{ci_status_label(ci_commit)}", data: { toggle: 'tooltip', placement: 'left' } do ci_status_icon(ci_commit) end diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml index 7e1ee2b7fc..386d72e778 100644 --- a/app/views/projects/_last_commit.html.haml +++ b/app/views/projects/_last_commit.html.haml @@ -3,7 +3,7 @@ - if ci_commit = link_to ci_status_path(ci_commit), class: "ci-status ci-#{ci_commit.status}" do = ci_status_icon(ci_commit) - = ci_commit.status + = ci_status_label(ci_commit) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message" diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 907e1ce10b..7890232540 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@build.name} (#{@build.id})", "Builds" +- page_title "#{@build.name} (##{@build.id})", "Builds" = render "header_title" .build-page diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 776768537d..593dc0f7f3 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -43,7 +43,7 @@ = link_to ci_status_path(@ci_commit), class: "ci-status ci-#{@ci_commit.status}" do = ci_status_icon(@ci_commit) build: - = @ci_commit.status + = ci_status_label(@ci_commit) .commit-info-row.branches %i.fa.fa-spinner.fa-spin diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index ba5ad22bca..0f2996efe2 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,10 +1,9 @@ - ci_commit = @merge_request.ci_commit - if ci_commit - - status = ci_commit.status .mr-widget-heading - .ci_widget{class: "ci-#{status}"} + .ci_widget{class: "ci-#{ci_commit.status}"} = ci_status_icon(ci_commit) - %span CI build #{status} + %span CI build #{ci_status_label(ci_commit)} for #{@merge_request.last_commit_short_sha}. %span.ci-coverage = link_to "View build details", ci_status_path(ci_commit) From 8b7dd1c1ce9822e0c1d1d126e1c618d1094d0b22 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:59:33 +0100 Subject: [PATCH 255/908] Add clipboard button to commit box SHA --- app/views/projects/commit/_commit_box.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 593dc0f7f3..c1e854cf86 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -19,7 +19,8 @@ %p %span.light Commit - = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" + = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace", data: { clipboard_text: @commit.id } + = clipboard_button .commit-info-row %span.light Authored by %strong From 67c74071a8b63cf905e6c65e9818317a99db9105 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 15:00:16 +0100 Subject: [PATCH 256/908] Add permission check for tag list release notes button --- app/views/projects/tags/_tag.html.haml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index e2c5178185..00c0b0b934 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -11,11 +11,12 @@ = strip_gpg_signature(tag.message) .controls - = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn' do - = icon("pencil") - - if can? current_user, :download_code, @project + - if can?(current_user, :download_code, @project) = render 'projects/tags/download', ref: tag.name, project: @project + - if can?(current_user, :push_code, @project) + = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn has_tooltip', title: "Edit release notes" do + = icon("pencil") - if commit = render 'projects/branches/commit', commit: commit, project: @project - else From ebc5a62bc082df08e5b5508a3dd9322b2e8f2bcf Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 15:00:22 +0100 Subject: [PATCH 257/908] Add tag delete button --- app/views/projects/tags/_tag.html.haml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 00c0b0b934..28b706c5c7 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -17,6 +17,11 @@ - if can?(current_user, :push_code, @project) = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn has_tooltip', title: "Edit release notes" do = icon("pencil") + + - if can?(current_user, :admin_project, @project) + = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has_tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do + = icon("trash-o") + - if commit = render 'projects/branches/commit', commit: commit, project: @project - else From d8a0e4538d0189b524f6745c32d5b98cd3ed8dc8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 15:00:28 +0100 Subject: [PATCH 258/908] Add tooltip to branch delete button --- app/views/projects/branches/_branch.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index ba6995aa67..5081bae680 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -26,7 +26,7 @@ Compare - if can_remove_branch?(@project, branch.name) - = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row', method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?" }, remote: true do + = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has_tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do = icon("trash-o") - if commit From de33866dce1c544b51b9b42ce382bcebb6822fcf Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 15:02:02 +0100 Subject: [PATCH 259/908] Add tooltips to tag buttons --- app/views/projects/tags/show.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 879c6c7d31..b594d4f1f2 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -5,17 +5,17 @@ .gray-content-block .pull-right - if can?(current_user, :push_code, @project) - = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn', title: 'Edit release notes' do + = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn has_tooltip', title: 'Edit release notes' do = icon("pencil") - = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped', title: 'Browse source code' do + = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has_tooltip', title: 'Browse files' do = icon('files-o') - = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped', title: 'Browse commits' do + = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has_tooltip', title: 'Browse commits' do = icon('history') - if can? current_user, :download_code, @project = render 'projects/tags/download', ref: @tag.name, project: @project - if can?(current_user, :admin_project, @project) .pull-right - = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped', method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do + = 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 From e3fe255c057e86d841a68f9a96b1a62b07036781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Wed, 2 Dec 2015 15:21:02 +0100 Subject: [PATCH 260/908] fixed the documentation of the Guest role in permission.md --- doc/permissions/permissions.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 8d4c2ceab7..bcd00cfc6b 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -6,6 +6,9 @@ If a user is both in a project group and in the project itself, the highest perm If a user is a GitLab administrator they receive all permissions. +On public projects the Guest role is not enforced. +All users will be able to create issues, leave comments, and pull or download the project code. + To add or import a user, you can follow the [project users and members documentation](doc/workflow/add-user/add-user.md). @@ -15,8 +18,8 @@ documentation](doc/workflow/add-user/add-user.md). |---------------------------------------|---------|------------|-------------|----------|--------| | Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ | | Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ | -| Pull project code | ✓ | ✓ | ✓ | ✓ | ✓ | -| Download project | ✓ | ✓ | ✓ | ✓ | ✓ | +| Pull project code | | ✓ | ✓ | ✓ | ✓ | +| Download project | | ✓ | ✓ | ✓ | ✓ | | Create code snippets | | ✓ | ✓ | ✓ | ✓ | | Manage issue tracker | | ✓ | ✓ | ✓ | ✓ | | Manage labels | | ✓ | ✓ | ✓ | ✓ | From 1fb3c0996acbe9b8e44376e72e24c867a588f4f0 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 15:32:45 +0100 Subject: [PATCH 261/908] Fix styling of empty repo message --- app/assets/stylesheets/framework/blocks.scss | 4 ++++ app/assets/stylesheets/pages/projects.scss | 7 ------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index dec50a0e0f..8836c8b666 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -68,6 +68,10 @@ .oneline { line-height: 42px; } + + > p:last-child { + margin-bottom: 0; + } } .cover-block { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index ac2d50f9c1..9d5b62c75d 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -26,12 +26,6 @@ } .project-home-panel { - text-align: center; - background: #f7f8fa; - margin: -$gl-padding; - padding: $gl-padding; - padding: 44px 0 17px 0; - .project-identicon-holder { margin-bottom: 16px; @@ -101,7 +95,6 @@ display: inline-table; position: relative; top: 17px; - margin-bottom: 44px; } .project-repo-buttons { From 845f26d07b97a281057a09facae28b2bbdb0168b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 15:04:29 +0100 Subject: [PATCH 262/908] Remove minimum containe height --- app/assets/stylesheets/framework/layout.scss | 7 ++++--- app/assets/stylesheets/framework/sidebar.scss | 5 ++--- app/assets/stylesheets/pages/login.scss | 2 ++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index b91c15d891..a60940a8be 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -2,10 +2,10 @@ html { overflow-y: scroll; &.touch .tooltip { display: none !important; } +} - body { - padding-top: $header-height; - } +body { + background-color: #EAEBEC !important; } .container { @@ -18,6 +18,7 @@ html { } .navless-container { + padding-top: $header-height; margin-top: 30px; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index c1b0129c86..81cda68b94 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -1,4 +1,6 @@ .page-with-sidebar { + padding-top: $header-height; + .sidebar-wrapper { position: fixed; top: 0; @@ -18,15 +20,12 @@ } .content-wrapper { - min-height: 100vh; width: 100%; padding: 20px; - background: #EAEBEC; .container-fluid { background: #FFF; padding: $gl-padding; - min-height: 90vh; &.container-blank { background: none; diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 83b866c3a6..edd5170513 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -1,5 +1,7 @@ /* Login Page */ .login-page { + background-color: white; + .container { max-width: 960px; } From acc35a58a7455a9107d6ebf2035f88350b1fb1ce Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 15:52:33 +0100 Subject: [PATCH 263/908] Revert unrelated changes --- app/assets/stylesheets/framework/sidebar.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 458af76cb7..fa46397580 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -20,12 +20,15 @@ } .content-wrapper { + min-height: 100vh; width: 100%; padding: 20px; + background: #EAEBEC; .container-fluid { background: #FFF; padding: $gl-padding; + min-height: 90vh; &.container-blank { background: none; From 74a81cbfe3e7b6fb6c3b7a620b07905f19971a86 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 17:30:50 +0100 Subject: [PATCH 264/908] Fix branch rendering --- app/helpers/merge_requests_helper.rb | 6 ++++-- app/views/projects/merge_requests/_show.html.haml | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index cc4243e155..7f3a61a5e3 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -56,12 +56,14 @@ module MergeRequestsHelper end def source_branch_with_namespace(merge_request) + branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch)) + if merge_request.for_fork? namespace = link_to(merge_request.source_project_namespace, project_path(merge_request.source_project)) - namespace + ":#{merge_request.source_branch}" + namespace + ":" + branch else - merge_request.source_branch + branch end end diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 09b228b7c0..e199250423 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -26,7 +26,8 @@ %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) .normal %span Request to merge - = link_to source_branch_with_namespace(@merge_request), namespace_project_commits_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), class: "label-branch" + %span.label-branch + = source_branch_with_namespace(@merge_request) %span into = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do = @merge_request.target_branch From f10bd7df90cd509848dfcc9a574d7e4569a0428e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 17:37:06 +0100 Subject: [PATCH 265/908] Tweak merge request list item --- .../projects/merge_requests/_merge_request.html.haml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 60dd4f4f87..1d4c9b66c4 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -33,7 +33,12 @@ \##{merge_request.iid} · opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)} - - if merge_request.milestone_id? + - if merge_request.target_project.default_branch != merge_request.target_branch +   + = link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do + = icon('code-fork') + = merge_request.target_branch + - if merge_request.milestone   = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do = icon('clock-o') @@ -42,11 +47,6 @@   - merge_request.labels.each do |label| = link_to_label(label, project: merge_request.project) - - if merge_request.target_project.default_branch != merge_request.target_branch -   - = link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do - = icon('code-fork') - = merge_request.target_branch - if merge_request.tasks?   %span.task-status From 044e0e33dce9371430a3c91e63f9f687b536d35b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 18:48:39 +0100 Subject: [PATCH 266/908] Allow invalid URLs in closing pattern --- lib/gitlab/closing_issue_extractor.rb | 4 +- .../gitlab/closing_issue_extractor_spec.rb | 49 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb index 0cf4c91873..9bef9037ad 100644 --- a/lib/gitlab/closing_issue_extractor.rb +++ b/lib/gitlab/closing_issue_extractor.rb @@ -1,8 +1,10 @@ module Gitlab class ClosingIssueExtractor ISSUE_CLOSING_REGEX = begin + link_pattern = URI.regexp(%w(http https)) + pattern = Gitlab.config.gitlab.issue_closing_pattern - pattern = pattern.sub('%{issue_ref}', "(?:(?:#{Issue.link_reference_pattern})|(?:#{Issue.reference_pattern}))") + pattern = pattern.sub('%{issue_ref}', "(?:(?:#{link_pattern})|(?:#{Issue.reference_pattern}))") Regexp.new(pattern).freeze end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 21254f778d..e9d7e8c111 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -2,11 +2,18 @@ require 'spec_helper' describe Gitlab::ClosingIssueExtractor do let(:project) { create(:project) } + let(:project2) { create(:project) } let(:issue) { create(:issue, project: project) } + let(:issue2) { create(:issue, project: project2) } let(:reference) { issue.to_reference } + let(:cross_reference) { issue2.to_reference(project) } subject { described_class.new(project, project.creator) } + before do + project2.team << [project.creator, :master] + end + describe "#closed_by_message" do context 'with a single reference' do it do @@ -130,6 +137,27 @@ describe Gitlab::ClosingIssueExtractor do end end + context "with a cross-project reference" do + it do + message = "Closes #{cross_reference}" + expect(subject.closed_by_message(message)).to eq([issue2]) + end + end + + context "with a cross-project URL" do + it do + message = "Closes #{Gitlab.config.gitlab.url}/#{project2.to_reference}/issues/#{issue2.iid}" + expect(subject.closed_by_message(message)).to eq([issue2]) + end + end + + context "with an invalid URL" do + it do + message = "Closes https://google.com/#{project2.to_reference}/issues/#{issue2.iid}" + expect(subject.closed_by_message(message)).to eq([]) + end + end + context 'with multiple references' do let(:other_issue) { create(:issue, project: project) } let(:third_issue) { create(:issue, project: project) } @@ -171,6 +199,27 @@ describe Gitlab::ClosingIssueExtractor do expect(subject.closed_by_message(message)). to match_array([issue, other_issue, third_issue]) end + + it "fetches cross-project references" do + message = "Closes #{reference} and #{cross_reference}" + + expect(subject.closed_by_message(message)). + to match_array([issue, issue2]) + end + + it "fetches cross-project URL references" do + message = "Closes #{Gitlab.config.gitlab.url}/#{project2.to_reference}/issues/#{issue2.iid} and #{reference}" + + expect(subject.closed_by_message(message)). + to match_array([issue, issue2]) + end + + it "ignores invalid cross-project URL references" do + message = "Closes https://google.com/#{project2.to_reference}/issues/#{issue2.iid} and #{reference}" + + expect(subject.closed_by_message(message)). + to match_array([issue]) + end end end end From ec754d221368fad2b765fa60c665a461b2b29c78 Mon Sep 17 00:00:00 2001 From: Andrew Tomaka Date: Wed, 2 Dec 2015 13:21:07 -0500 Subject: [PATCH 267/908] Be more explicit with the impersonate return URL --- app/controllers/admin/impersonation_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/impersonation_controller.rb b/app/controllers/admin/impersonation_controller.rb index 102dd43740..bf98af7861 100644 --- a/app/controllers/admin/impersonation_controller.rb +++ b/app/controllers/admin/impersonation_controller.rb @@ -11,7 +11,7 @@ class Admin::ImpersonationController < Admin::ApplicationController redirect_to admin_user_path(@user) else session[:impersonator_id] = current_user.username - session[:impersonator_return_to] = request.env['HTTP_REFERER'] + session[:impersonator_return_to] = admin_user_path(@user) warden.set_user(user, scope: 'user') From f1bbd8119db3e494e662cd219a9882e5e17bf4bc Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 2 Dec 2015 22:03:43 -0200 Subject: [PATCH 268/908] Style warning about mentioning many people in a comment --- CHANGELOG | 1 + app/views/projects/_md_preview.html.haml | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index db812796b6..89d3f854ae 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ v 8.3.0 (unreleased) - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) - Add ignore whitespace change option to commit view - Fire update hook from GitLab + - Style warning about mentioning many people in a comment v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 8218cf1120..c4d3c9cc30 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -8,17 +8,17 @@ %a.js-md-preview-button(href="#md-preview-holder" tabindex="-1") Preview - - if defined?(referenced_users) && referenced_users - %span.referenced-users.pull-left.hide - = icon('exclamation-triangle') - You are about to add - %strong - %span.js-referenced-users-count 0 - people - to the discussion. Proceed with caution. - %div .md-write-holder = yield .md.md-preview-holder.hide .js-md-preview{class: (preview_class if defined?(preview_class))} + + - if defined?(referenced_users) && referenced_users + %span.referenced-users.pull-left.hide + = icon('exclamation-triangle') + You are about to add + %strong + %span.js-referenced-users-count 0 + people + to the discussion. Proceed with caution. From f94afdbdb232418da08658516bc469cf8429dadc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 2 Dec 2015 14:40:43 -0500 Subject: [PATCH 269/908] Fix broken spec related to MySQL and fractional seconds support. --- spec/models/project_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f80fada45e..06a02c13bf 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -444,7 +444,9 @@ describe Project do before do 2.times do - create(:note_on_commit, project: project2, created_at: date) + # Little fix for special issue related to Fractional Seconds support for MySQL. + # See: https://github.com/rails/rails/pull/14359/files + create(:note_on_commit, project: project2, created_at: date + 1) end end From 162cd0099c6c1f4ee0051e35562636468e88ee0c Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 3 Dec 2015 10:33:38 +0200 Subject: [PATCH 270/908] fix deprecation messages in tests --- app/models/project_services/buildkite_service.rb | 2 +- app/models/project_services/drone_ci_service.rb | 2 +- app/models/user.rb | 2 +- app/views/ci/notify/build_fail_email.html.haml | 2 +- lib/gitlab/lfs/response.rb | 2 +- spec/mailers/notify_spec.rb | 12 +++++++----- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 40058b53df..199ee3a9d0 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -37,7 +37,7 @@ class BuildkiteService < CiService def compose_service_hook hook = service_hook || build_service_hook hook.url = webhook_url - hook.enable_ssl_verification = enable_ssl_verification + hook.enable_ssl_verification = !!enable_ssl_verification hook.save end diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index 127684bd27..06c3922593 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -34,7 +34,7 @@ class DroneCiService < CiService hook = service_hook || build_service_hook # If using a service template, project may not be available hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.path}", "&name=#{project.path}", "&access_token=#{token}"].join if project - hook.enable_ssl_verification = enable_ssl_verification + hook.enable_ssl_verification = !!enable_ssl_verification hook.save end diff --git a/app/models/user.rb b/app/models/user.rb index 15aab31564..719b49b16f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -690,7 +690,7 @@ class User < ActiveRecord::Base end def starred?(project) - starred_projects.exists?(project) + starred_projects.exists?(project.id) end def toggle_star(project) diff --git a/app/views/ci/notify/build_fail_email.html.haml b/app/views/ci/notify/build_fail_email.html.haml index b0aaea8907..de6291aa91 100644 --- a/app/views/ci/notify/build_fail_email.html.haml +++ b/app/views/ci/notify/build_fail_email.html.haml @@ -7,7 +7,7 @@ = @project.name %p - Commit: #{link_to @build.short_sha, namespace_project_commit_path(@build.gl_project.namespace, @build.gl_project, @build.sha)} + Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.gl_project.namespace, @build.gl_project, @build.sha)} %p Author: #{@build.commit.git_author_name} %p diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb index c18dfbd485..9be9a65671 100644 --- a/lib/gitlab/lfs/response.rb +++ b/lib/gitlab/lfs/response.rb @@ -260,7 +260,7 @@ module Gitlab end def link_to_project(object) - if object && !object.projects.exists?(@project) + if object && !object.projects.exists?(@project.id) object.projects << @project object.save end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index d6796b07a5..27e509933b 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -247,7 +247,7 @@ describe Notify do end describe 'that have been reassigned' do - subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user) } + subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user.id) } it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'issue' @@ -278,7 +278,7 @@ describe Notify do describe 'status changed' do let(:status) { 'closed' } - subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user) } + subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) } it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'it should show Gmail Actions View Issue link' @@ -382,7 +382,7 @@ describe Notify do describe 'status changed' do let(:status) { 'reopened' } - subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user) } + subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) } it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'it should show Gmail Actions View Merge request link' @@ -597,8 +597,10 @@ describe Notify do let(:user) { create(:user, email: 'old-email@mail.com') } before do - user.email = "new-email@mail.com" - user.save + perform_enqueued_jobs do + user.email = "new-email@mail.com" + user.save + end end subject { ActionMailer::Base.deliveries.last } From 54b74aa99885b4f35f580bbb2cd923a381562c39 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 3 Dec 2015 10:49:02 +0200 Subject: [PATCH 271/908] fix Celluloid warnings --- Gemfile.lock | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index dcb4a74e23..cd1855758b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -116,23 +116,8 @@ GEM activemodel (>= 3.2.0) activesupport (>= 3.2.0) json (>= 1.7) - celluloid (0.17.2) - celluloid-essentials - celluloid-extras - celluloid-fsm - celluloid-pool - celluloid-supervision - timers (>= 4.1.1) - celluloid-essentials (0.20.5) - timers (>= 4.1.1) - celluloid-extras (0.20.5) - timers (>= 4.1.1) - celluloid-fsm (0.20.5) - timers (>= 4.1.1) - celluloid-pool (0.20.5) - timers (>= 4.1.1) - celluloid-supervision (0.20.5) - timers (>= 4.1.1) + celluloid (0.16.0) + timers (~> 4.0.0) charlock_holmes (0.7.3) chunky_png (1.3.5) cliver (0.3.2) @@ -757,7 +742,7 @@ GEM thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) - timers (4.1.1) + timers (4.0.4) hitimes timfel-krb5-auth (0.8.3) tinder (1.10.1) From f21bbd4444f9466d0dc622b0de286deace642598 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 3 Dec 2015 10:53:58 +0200 Subject: [PATCH 272/908] Rails deprecation warning about log_level --- config/environments/production.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 317b113e10..909526605a 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -32,7 +32,7 @@ Rails.application.configure do # config.force_ssl = true # See everything in the log (default is :info) - # config.log_level = :debug + config.log_level = :info # Suppress 'Rendered template ...' messages in the log # source: http://stackoverflow.com/a/16369363 From b871c38bf2ca23c915fd5ce55ecfb2245b53bbe3 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 3 Dec 2015 11:49:11 +0200 Subject: [PATCH 273/908] Fix failures in master --- app/views/admin/labels/_label.html.haml | 2 +- app/views/projects/milestones/show.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml index 596e06243d..e3ccbf6c3a 100644 --- a/app/views/admin/labels/_label.html.haml +++ b/app/views/admin/labels/_label.html.haml @@ -2,4 +2,4 @@ = render_colored_label(label) .pull-right = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm' - = link_to 'Remove', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} + = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"} diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 3a898dfbcf..eab5333ca2 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -23,7 +23,7 @@ = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped" = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-remove" do %i.fa.fa-trash-o - Remove + Delete %hr - if @milestone.issues.any? && @milestone.can_be_closed? From 129ad8425e8fcbb1d7025e247d29772831d76b06 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 3 Dec 2015 13:17:04 +0100 Subject: [PATCH 274/908] Tweak tag form wording --- app/views/projects/tags/new.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 9c9bfa3f55..3a2f75feca 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -32,7 +32,7 @@ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', attr: :release_description, classes: 'description js-quick-submit form-control' = render 'projects/notes/hints' - .help-block Optionally, you can add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. + .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. .form-actions = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' From 9cf67f72a581d4648b2b02b53403dd4318e4f1e9 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 1 Dec 2015 12:00:11 +0100 Subject: [PATCH 275/908] Add validator for award-emoji note --- app/models/note.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/note.rb b/app/models/note.rb index 1c6345e735..40b46b85da 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -41,6 +41,7 @@ class Note < ActiveRecord::Base validates :note, :project, presence: true validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } + validates :note, format: { with: /\A[-_+[:alnum:]]*\z/ }, if: -> (n){ n.is_award } validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true # Attachments are deprecated and are handled by Markdown uploader validates :attachment, file_size: { maximum: :max_attachment_size } From 22d87b74a9de5d603f101699d7a3665db9627037 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 1 Dec 2015 14:45:24 +0100 Subject: [PATCH 276/908] Support award-emoji notes only when it a comment for an issue --- app/services/notes/create_service.rb | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index dbff58dfb9..20a3ba3075 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -5,9 +5,9 @@ module Notes note.author = current_user note.system = false - if contains_emoji_only?(params[:note]) + if award_emoji_note? note.is_award = true - note.note = emoji_name(params[:note]) + note.note = emoji_name end if note.save @@ -34,12 +34,23 @@ module Notes note.project.execute_services(note_data, :note_hooks) end - def contains_emoji_only?(note) - note =~ /\A:[-_+[:alnum:]]*:\s?\z/ + private + + def award_emoji_note? + # We support award-emojis only in issue discussion + issue_comment? && contains_emoji_only? end - def emoji_name(note) - note.match(/\A:([-_+[:alnum:]]*):\s?/)[1] + def contains_emoji_only? + params[:note] =~ /\A:[-_+[:alnum:]]*:\s?\z/ + end + + def issue_comment? + params[:noteable_type] == 'Issue' + end + + def emoji_name + params[:note].match(/\A:([-_+[:alnum:]]*):\s?/)[1] end end end From ba08811d07cd1804b027b1a37a5278723cdbeb2c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 2 Dec 2015 08:48:21 +0100 Subject: [PATCH 277/908] Move note emoji-award implementation to note model (feature envy) --- app/models/note.rb | 33 ++++++++++++++++++++++++++++ app/services/notes/create_service.rb | 24 -------------------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/app/models/note.rb b/app/models/note.rb index 40b46b85da..8acb2cf758 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -72,6 +72,7 @@ class Note < ActiveRecord::Base serialize :st_diff before_create :set_diff, if: ->(n) { n.line_code.present? } + before_validation :set_award! class << self def discussions_from_notes(notes) @@ -349,4 +350,36 @@ class Note < ActiveRecord::Base def editable? !system? end + + # Checks if note is an award added from an issue comment. + # + # If note is an award, this method sets is_award to true, + # and changes note content to award-emoji name. + # + # Awards are only supported for issue comments. + # + # Method is executed as a before_validation callback. + # + def set_award! + return unless issue_comment? && contains_emoji_only? + + self.is_award = true + self.note = award_emoji_name + end + + private + + def issue_comment? + noteable.kind_of?(Issue) + end + + def contains_emoji_only? + (note =~ /\A:[-_+[:alnum:]]*:\s?\z/) ? true : false + end + + def award_emoji_name + return nil unless contains_emoji_only? + + note.match(/\A:([-_+[:alnum:]]*):\s?/)[1] + end end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 20a3ba3075..a8486e6a5a 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -5,11 +5,6 @@ module Notes note.author = current_user note.system = false - if award_emoji_note? - note.is_award = true - note.note = emoji_name - end - if note.save notification_service.new_note(note) @@ -33,24 +28,5 @@ module Notes note.project.execute_hooks(note_data, :note_hooks) note.project.execute_services(note_data, :note_hooks) end - - private - - def award_emoji_note? - # We support award-emojis only in issue discussion - issue_comment? && contains_emoji_only? - end - - def contains_emoji_only? - params[:note] =~ /\A:[-_+[:alnum:]]*:\s?\z/ - end - - def issue_comment? - params[:noteable_type] == 'Issue' - end - - def emoji_name - params[:note].match(/\A:([-_+[:alnum:]]*):\s?/)[1] - end end end From c92b6e66dc3a5bb1b07d1561fa09e8fd051c1956 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 2 Dec 2015 09:36:11 +0100 Subject: [PATCH 278/908] Scroll to awards after adding emoji-award comment This makes it more intuitive, as user can see that something actually happened after adding emoji-only comment in long discussions. --- app/assets/javascripts/awards_handler.coffee | 7 ++++++- app/assets/javascripts/notes.js.coffee | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 09b48fe557..96fd8f8773 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -88,4 +88,9 @@ class @AwardsHandler callback.call() findEmojiIcon: (emoji) -> - $(".icon[data-emoji='" + emoji + "']") \ No newline at end of file + $(".icon[data-emoji='" + emoji + "']") + + scrollToAwards: -> + $('body, html').animate({ + scrollTop: $('.awards').offset().top - 80 + }, 200) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 7de7632201..797234e6d9 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -122,6 +122,7 @@ class @Notes if note.award awards_handler.addAwardToEmojiBar(note.note, note.emoji_path) + awards_handler.scrollToAwards() ### Check if note does not exists on page From bfce5d716835f07b98b6d26ccc121d3ac8322aa9 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 2 Dec 2015 10:13:29 +0100 Subject: [PATCH 279/908] Render json message with errors if note didn't pass validation --- app/controllers/projects/notes_controller.rb | 24 ++++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 5ac18446aa..a7ff5fcd09 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -131,16 +131,20 @@ class Projects::NotesController < Projects::ApplicationController end def render_note_json(note) - render json: { - id: note.id, - discussion_id: note.discussion_id, - html: note_to_html(note), - award: note.is_award, - emoji_path: note.is_award ? view_context.image_url(::AwardEmoji.path_to_emoji_image(note.note)) : "", - note: note.note, - discussion_html: note_to_discussion_html(note), - discussion_with_diff_html: note_to_discussion_with_diff_html(note) - } + if note.valid? + render json: { + id: note.id, + discussion_id: note.discussion_id, + html: note_to_html(note), + award: note.is_award, + emoji_path: note.is_award ? view_context.image_url(::AwardEmoji.path_to_emoji_image(note.note)) : "", + note: note.note, + discussion_html: note_to_discussion_html(note), + discussion_with_diff_html: note_to_discussion_with_diff_html(note) + } + else + render json: { invalid: true, errors: note.errors } + end end def authorize_admin_note! From a527f5c27ff92d2ee7e2d5e78dc20b6d1d982aa0 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 2 Dec 2015 10:51:46 +0100 Subject: [PATCH 280/908] Notify user when award-emoji comment is invalid --- app/assets/javascripts/notes.js.coffee | 4 ++++ app/controllers/projects/notes_controller.rb | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 797234e6d9..af0d62c849 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -111,6 +111,10 @@ class @Notes Note: for rendering inline notes use renderDiscussionNote ### renderNote: (note) -> + unless note.valid + alert('You have already used this award emoji !') if note.award + return + # render note if it not present in loaded list # or skip if rendered if @isNewNote(note) && !note.award diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index a7ff5fcd09..88b949a27a 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -133,6 +133,7 @@ class Projects::NotesController < Projects::ApplicationController def render_note_json(note) if note.valid? render json: { + valid: true, id: note.id, discussion_id: note.discussion_id, html: note_to_html(note), @@ -143,7 +144,11 @@ class Projects::NotesController < Projects::ApplicationController discussion_with_diff_html: note_to_discussion_with_diff_html(note) } else - render json: { invalid: true, errors: note.errors } + render json: { + valid: false, + award: note.is_award, + errors: note.errors + } end end From 4676ba1f7c7b37498d26815c1fbe0e02c2ffaeb8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 2 Dec 2015 14:11:32 +0000 Subject: [PATCH 281/908] Add test for award-emoji being added as regular comment --- features/project/issues/award_emoji.feature | 6 +++- features/steps/project/issues/award_emoji.rb | 31 ++++++++++++-------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature index a9bc8ffb9b..2609f129d0 100644 --- a/features/project/issues/award_emoji.feature +++ b/features/project/issues/award_emoji.feature @@ -11,4 +11,8 @@ Feature: Award Emoji And I click to emoji in the picker Then I have award added And I can remove it by clicking to icon - \ No newline at end of file + + @javascript + Scenario: I add award emoji using regular comment + Given I leave comment with a single emoji + Then I have award added diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb index 8f7a45dec0..325eaf2ea6 100644 --- a/features/steps/project/issues/award_emoji.rb +++ b/features/steps/project/issues/award_emoji.rb @@ -9,33 +9,40 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps end step 'I click to emoji-picker' do - page.within ".awards-controls" do - page.find(".add-award").click + page.within '.awards-controls' do + page.find('.add-award').click end end step 'I click to emoji in the picker' do - page.within ".awards-menu" do - page.first("img").click + page.within '.awards-menu' do + page.first('img').click end end step 'I can remove it by clicking to icon' do - page.within ".awards" do - page.first(".award").click - expect(page).to_not have_selector ".award" + page.within '.awards' do + page.first('.award').click + expect(page).to_not have_selector '.award' end end step 'I have award added' do - page.within ".awards" do - expect(page).to have_selector ".award" - expect(page.find(".award .counter")).to have_content "1" + page.within '.awards' do + expect(page).to have_selector '.award' + expect(page.find('.award .counter')).to have_content '1' end end step 'project "Shop" has issue "Bugfix"' do - @project = Project.find_by(name: "Shop") - @issue = create(:issue, title: "Bugfix", project: project) + @project = Project.find_by(name: 'Shop') + @issue = create(:issue, title: 'Bugfix', project: project) + end + + step 'I leave comment with a single emoji' do + page.within('.js-main-target-form') do + fill_in 'note[note]', with: ':smile:' + click_button 'Add Comment' + end end end From 6fc1b948560b9e19ac5c1412dd2deb98987aefe8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 2 Dec 2015 18:39:12 +0100 Subject: [PATCH 282/908] Show flash message instead of alert when note is invalid --- app/assets/javascripts/notes.js.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index af0d62c849..ea190fa8b7 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -112,7 +112,8 @@ class @Notes ### renderNote: (note) -> unless note.valid - alert('You have already used this award emoji !') if note.award + if note.award + new Flash('You have already used this award emoji !', 'alert') return # render note if it not present in loaded list From 70a076c059482e5c26cb723b722bc865142460ea Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 3 Dec 2015 08:51:14 +0100 Subject: [PATCH 283/908] Add new features to javascript flash message --- app/assets/javascripts/flash.js.coffee | 19 +++++++++++++------ app/assets/stylesheets/framework/flash.scss | 10 ++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee index b39ab0c447..5e34b49c20 100644 --- a/app/assets/javascripts/flash.js.coffee +++ b/app/assets/javascripts/flash.js.coffee @@ -1,12 +1,19 @@ class @Flash constructor: (message, type)-> - flash = $(".flash-container") - flash.html("") + @flash = $(".flash-container") + @flash.html("") - $('
', + innerDiv = $('
', class: "flash-#{type}", text: message - ).appendTo(".flash-container") + ) + innerDiv.appendTo(".flash-container") - flash.click -> $(@).fadeOut() - flash.show() + @flash.click -> $(@).fadeOut() + @flash.show() + + pinToTop: -> + @flash.addClass('flash-pinned') + + raise: -> + @flash.addClass('flash-raised') diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index 82eb50ad4b..1b723021d7 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -15,3 +15,13 @@ @extend .alert-danger; } } + +.flash-pinned { + position: fixed; + top: 80px; + width: 80%; +} + +.flash-raised { + z-index: 1000; +} From 554f4684622564fe496acb25cacb2daed3b9f3ac Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 3 Dec 2015 08:51:37 +0100 Subject: [PATCH 284/908] Pin flash message to top if award note is invalid --- app/assets/javascripts/notes.js.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index ea190fa8b7..8eacd05d05 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -113,7 +113,9 @@ class @Notes renderNote: (note) -> unless note.valid if note.award - new Flash('You have already used this award emoji !', 'alert') + flash = new Flash('You have already used this award emoji !', 'alert') + flash.pinToTop() + flash.raise() return # render note if it not present in loaded list From 2b577551177c631d229eca6844520e29478085f8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 3 Dec 2015 09:30:30 +0100 Subject: [PATCH 285/908] Add feature test for emoji-only diff notes This specs is related to bug described in #3734 (award-emojis). --- features/project/commits/diff_comments.feature | 6 ++++++ features/steps/shared/diff_note.rb | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/features/project/commits/diff_comments.feature b/features/project/commits/diff_comments.feature index 4a2b870e08..d6e0c84537 100644 --- a/features/project/commits/diff_comments.feature +++ b/features/project/commits/diff_comments.feature @@ -13,6 +13,12 @@ Feature: Project Commits Diff Comments Given I leave a diff comment like "Typo, please fix" Then I should see a diff comment saying "Typo, please fix" + @javascript + Scenario: I can add a diff comment with a single emoji + Given I open a diff comment form + And I write a diff comment like ":smile:" + Then I should see a diff comment with an emoji image + @javascript Scenario: I get a temporary form for the first comment on a diff line Given I open a diff comment form diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 72621911a3..dd466cde28 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -87,6 +87,17 @@ module SharedDiffNote end end + step 'I write a diff comment like ":smile:"' do + page.within(diff_file_selector) do + click_diff_line(sample_commit.line_code) + + page.within("form[rel$='#{sample_commit.line_code}']") do + fill_in 'note[note]', with: ':smile:' + click_button('Add Comment') + end + end + end + step 'I submit the diff comment' do page.within(diff_file_selector) do click_button("Add Comment") @@ -197,6 +208,12 @@ module SharedDiffNote end end + step 'I should see a diff comment with an emoji image' do + page.within("#{diff_file_selector} .note") do + expect(page).to have_xpath("//img[@alt=':smile:']") + end + end + step 'I click side-by-side diff button' do find('#parallel-diff-btn').trigger('click') end From f08f921d537c68ec0bbfb8a1ae7e905cded1bbad Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 3 Dec 2015 12:46:39 +0100 Subject: [PATCH 286/908] Support emoji awards also in merge requests --- app/models/note.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models/note.rb b/app/models/note.rb index 8acb2cf758..30d15d93fe 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -361,7 +361,7 @@ class Note < ActiveRecord::Base # Method is executed as a before_validation callback. # def set_award! - return unless issue_comment? && contains_emoji_only? + return unless supports_awards? && contains_emoji_only? self.is_award = true self.note = award_emoji_name @@ -369,8 +369,9 @@ class Note < ActiveRecord::Base private - def issue_comment? - noteable.kind_of?(Issue) + def supports_awards? + noteable.kind_of?(Issue) || + noteable.is_a?(MergeRequest) end def contains_emoji_only? From 7af67f619a9ce3dab0a66560bc57ccf606077779 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 3 Dec 2015 12:59:44 +0100 Subject: [PATCH 287/908] Combine new javascript Flash methods into one --- app/assets/javascripts/flash.js.coffee | 7 ++----- app/assets/javascripts/notes.js.coffee | 3 +-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee index 5e34b49c20..9b59d4e57f 100644 --- a/app/assets/javascripts/flash.js.coffee +++ b/app/assets/javascripts/flash.js.coffee @@ -12,8 +12,5 @@ class @Flash @flash.click -> $(@).fadeOut() @flash.show() - pinToTop: -> - @flash.addClass('flash-pinned') - - raise: -> - @flash.addClass('flash-raised') + pin: -> + @flash.addClass('flash-pinned flash-raised') diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 8eacd05d05..4f559e8637 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -114,8 +114,7 @@ class @Notes unless note.valid if note.award flash = new Flash('You have already used this award emoji !', 'alert') - flash.pinToTop() - flash.raise() + flash.pin() return # render note if it not present in loaded list From 83d8185f5afee7553bf5756ce61b6b855d48c1e5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 3 Dec 2015 13:03:50 +0100 Subject: [PATCH 288/908] Make method `supports_award?` public in `Note` --- app/models/note.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/note.rb b/app/models/note.rb index 30d15d93fe..03640be7c9 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -367,13 +367,13 @@ class Note < ActiveRecord::Base self.note = award_emoji_name end - private - def supports_awards? noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest) end + private + def contains_emoji_only? (note =~ /\A:[-_+[:alnum:]]*:\s?\z/) ? true : false end From f905fd239c79f391ce5a7cc2c5c6648b3d6380e4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 3 Dec 2015 14:00:00 +0100 Subject: [PATCH 289/908] Use URL helpers in specs --- lib/gitlab/markdown/label_reference_filter.rb | 4 ++-- spec/fixtures/markdown.md.erb | 20 +++++++++---------- .../gitlab/closing_issue_extractor_spec.rb | 12 +++++++---- .../markdown/label_reference_filter_spec.rb | 6 +++--- spec/support/markdown_feature.rb | 4 ++++ 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb index a23aa0adee..ec2b179b7d 100644 --- a/lib/gitlab/markdown/label_reference_filter.rb +++ b/lib/gitlab/markdown/label_reference_filter.rb @@ -65,8 +65,8 @@ module Gitlab def url_for_label(project, label) h = Gitlab::Application.routes.url_helpers - h.namespace_project_issues_path(project.namespace, project, - label_name: label.name) + h.namespace_project_issues_url( project.namespace, project, label_name: label.name, + only_path: context[:only_path]) end def render_colored_label(label) diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 76e733165c..e8dfc5c0eb 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -161,9 +161,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Issue in another project: <%= xissue.to_reference(project) %> - Ignored in code: `<%= issue.to_reference %>` - Ignored in links: [Link to <%= issue.to_reference %>](#issue-link) -- Issue by URL: <%= Gitlab.config.gitlab.url %>/<%= issue.project.to_reference %>/issues/<%= issue.iid %> +- Issue by URL: <%= urls.namespace_project_issue_url(issue.project.namespace, issue.project, issue) %> - Link to issue by reference: [Issue](<%= issue.to_reference %>) -- Link to issue by URL: [Issue](<%= Gitlab.config.gitlab.url %>/<%= issue.project.to_reference %>/issues/<%= issue.iid %>) +- Link to issue by URL: [Issue](<%= urls.namespace_project_issue_url(issue.project.namespace, issue.project, issue) %>) #### MergeRequestReferenceFilter @@ -171,9 +171,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Merge request in another project: <%= xmerge_request.to_reference(project) %> - Ignored in code: `<%= merge_request.to_reference %>` - Ignored in links: [Link to <%= merge_request.to_reference %>](#merge-request-link) -- Merge request by URL: <%= Gitlab.config.gitlab.url %>/<%= merge_request.project.to_reference %>/merge_requests/<%= merge_request.iid %> +- Merge request by URL: <%= urls.namespace_project_merge_request_url(merge_request.project.namespace, merge_request.project, merge_request) %> - Link to merge request by reference: [Merge request](<%= merge_request.to_reference %>) -- Link to merge request by URL: [Merge request](<%= Gitlab.config.gitlab.url %>/<%= merge_request.project.to_reference %>/merge_requests/<%= merge_request.iid %>) +- Link to merge request by URL: [Merge request](<%= urls.namespace_project_merge_request_url(merge_request.project.namespace, merge_request.project, merge_request) %>) #### SnippetReferenceFilter @@ -181,9 +181,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Snippet in another project: <%= xsnippet.to_reference(project) %> - Ignored in code: `<%= snippet.to_reference %>` - Ignored in links: [Link to <%= snippet.to_reference %>](#snippet-link) -- Snippet by URL: <%= Gitlab.config.gitlab.url %>/<%= snippet.project.to_reference %>/snippets/<%= snippet.id %> +- Snippet by URL: <%= urls.namespace_project_snippet_url(snippet.project.namespace, snippet.project, snippet) %> - Link to snippet by reference: [Snippet](<%= snippet.to_reference %>) -- Link to snippet by URL: [Snippet](<%= Gitlab.config.gitlab.url %>/<%= snippet.project.to_reference %>/snippets/<%= snippet.id %>) +- Link to snippet by URL: [Snippet](<%= urls.namespace_project_snippet_url(snippet.project.namespace, snippet.project, snippet) %>) #### CommitRangeReferenceFilter @@ -191,9 +191,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Range in another project: <%= xcommit_range.to_reference(project) %> - Ignored in code: `<%= commit_range.to_reference %>` - Ignored in links: [Link to <%= commit_range.to_reference %>](#commit-range-link) -- Range by URL: <%= Gitlab.config.gitlab.url %>/<%= commit_range.project.to_reference %>/compare/<%= commit_range.id %> +- Range by URL: <%= urls.namespace_project_compare_url(commit_range.project.namespace, commit_range.project, commit_range.to_param) %> - Link to range by reference: [Range](<%= commit_range.to_reference %>) -- Link to range by URL: [Range](<%= Gitlab.config.gitlab.url %>/<%= commit_range.project.to_reference %>/compare/<%= commit_range.id %>) +- Link to range by URL: [Range](<%= urls.namespace_project_compare_url(commit_range.project.namespace, commit_range.project, commit_range.to_param) %>) #### CommitReferenceFilter @@ -201,9 +201,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Commit in another project: <%= xcommit.to_reference(project) %> - Ignored in code: `<%= commit.to_reference %>` - Ignored in links: [Link to <%= commit.to_reference %>](#commit-link) -- Commit by URL: <%= Gitlab.config.gitlab.url %>/<%= commit.project.to_reference %>/commit/<%= commit.id %> +- Commit by URL: <%= urls.namespace_project_commit_url(commit.project.namespace, commit.project, commit) %> - Link to commit by reference: [Commit](<%= commit.to_reference %>) -- Link to commit by URL: [Commit](<%= Gitlab.config.gitlab.url %>/<%= commit.project.to_reference %>/commit/<%= commit.id %>) +- Link to commit by URL: [Commit](<%= urls.namespace_project_commit_url(commit.project.namespace, commit.project, commit) %>) #### LabelReferenceFilter diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index e9d7e8c111..fe1b94a484 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -146,14 +146,14 @@ describe Gitlab::ClosingIssueExtractor do context "with a cross-project URL" do it do - message = "Closes #{Gitlab.config.gitlab.url}/#{project2.to_reference}/issues/#{issue2.iid}" + message = "Closes #{urls.namespace_project_issue_url(issue2.project.namespace, issue2.project, issue2)}" expect(subject.closed_by_message(message)).to eq([issue2]) end end context "with an invalid URL" do it do - message = "Closes https://google.com/#{project2.to_reference}/issues/#{issue2.iid}" + message = "Closes https://google.com#{urls.namespace_project_issue_path(issue2.project.namespace, issue2.project, issue2)}" expect(subject.closed_by_message(message)).to eq([]) end end @@ -208,18 +208,22 @@ describe Gitlab::ClosingIssueExtractor do end it "fetches cross-project URL references" do - message = "Closes #{Gitlab.config.gitlab.url}/#{project2.to_reference}/issues/#{issue2.iid} and #{reference}" + message = "Closes #{urls.namespace_project_issue_url(issue2.project.namespace, issue2.project, issue2)} and #{reference}" expect(subject.closed_by_message(message)). to match_array([issue, issue2]) end it "ignores invalid cross-project URL references" do - message = "Closes https://google.com/#{project2.to_reference}/issues/#{issue2.iid} and #{reference}" + message = "Closes https://google.com#{urls.namespace_project_issue_path(issue2.project.namespace, issue2.project, issue2)} and #{reference}" expect(subject.closed_by_message(message)). to match_array([issue]) end end end + + def urls + Gitlab::Application.routes.url_helpers + end end diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb index f5a6d67f77..ef6dd524ab 100644 --- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb @@ -71,7 +71,7 @@ module Gitlab::Markdown doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_path(project.namespace, project, label_name: label.name) + namespace_project_issues_url(project.namespace, project, label_name: label.name) end it 'links with adjacent text' do @@ -94,7 +94,7 @@ module Gitlab::Markdown doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_path(project.namespace, project, label_name: label.name) + namespace_project_issues_url(project.namespace, project, label_name: label.name) expect(doc.text).to eq 'See gfm' end @@ -118,7 +118,7 @@ module Gitlab::Markdown doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_path(project.namespace, project, label_name: label.name) + namespace_project_issues_url(project.namespace, project, label_name: label.name) expect(doc.text).to eq 'See gfm references' end diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index bedc1a7f1d..d6d3062a19 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -93,6 +93,10 @@ class MarkdownFeature end end + def urls + Gitlab::Application.routes.url_helpers + end + def raw_markdown markdown = File.read(Rails.root.join('spec/fixtures/markdown.md.erb')) ERB.new(markdown).result(binding) From 0de8e260662a62603dbee5efb133cd4f1e0f0ed7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 3 Dec 2015 14:00:41 +0100 Subject: [PATCH 290/908] Pass original text along with label reference filter. --- lib/gitlab/markdown/label_reference_filter.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb index ec2b179b7d..36cf7a8f4c 100644 --- a/lib/gitlab/markdown/label_reference_filter.rb +++ b/lib/gitlab/markdown/label_reference_filter.rb @@ -51,7 +51,11 @@ module Gitlab if label = project.labels.find_by(params) url = url_for_label(project, label) klass = reference_class(:label) - data = data_attribute(project: project.id, label: label.id) + data = data_attribute( + original: link_text || match, + project: project.id, + label: label.id + ) text = link_text || render_colored_label(label) From 9ce8c867eeb5da53eb76bc01ef7eaef5c798243c Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 3 Dec 2015 15:05:43 +0200 Subject: [PATCH 291/908] Fix mailer queue --- Procfile | 2 +- bin/background_jobs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Procfile b/Procfile index fd5f7ecb94..eaf79ccc2e 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"} -worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q mailers -q default +worker: bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default # mail_room: bundle exec mail_room -q -c config/mail_room.yml diff --git a/bin/background_jobs b/bin/background_jobs index d4578f6a22..5c85fb339e 100755 --- a/bin/background_jobs +++ b/bin/background_jobs @@ -37,7 +37,7 @@ start_no_deamonize() start_sidekiq() { - bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 + bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 } load_ok() From 90899b3208035f5c74feb439da5fbe689dd1d2d7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 3 Dec 2015 14:54:14 +0100 Subject: [PATCH 292/908] Fix specs --- features/steps/project/forked_merge_requests.rb | 2 +- features/steps/project/issues/labels.rb | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 024dc5e72d..de1dbfdfa9 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -46,7 +46,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I submit the merge request' do - click_button "Submit new merge request" + click_button "Submit merge request" end step 'I follow the target commit link' do diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index 047cf701bb..f749ea2eca 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -31,19 +31,19 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps step 'I submit new label \'support\'' do fill_in 'Title', with: 'support' - fill_in 'Background Color', with: '#F95610' + fill_in 'Background color', with: '#F95610' click_button 'Save' end step 'I submit new label \'bug\'' do fill_in 'Title', with: 'bug' - fill_in 'Background Color', with: '#F95610' + fill_in 'Background color', with: '#F95610' click_button 'Save' end step 'I submit new label with invalid color' do fill_in 'Title', with: 'support' - fill_in 'Background Color', with: '#12' + fill_in 'Background color', with: '#12' click_button 'Save' end @@ -85,7 +85,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps step 'I change label \'bug\' to \'fix\'' do fill_in 'Title', with: 'fix' - fill_in 'Background Color', with: '#F15610' + fill_in 'Background color', with: '#F15610' click_button 'Save' end From 0a081e7eff9730beebd4bea1eb40873d907b6293 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 3 Dec 2015 14:59:10 +0100 Subject: [PATCH 293/908] If a user clicks on the LFS object, it should be served if the user has access to the object. --- app/controllers/projects/blob_controller.rb | 17 +++++++++++++++++ app/models/lfs_object.rb | 10 ++++++++++ 2 files changed, 27 insertions(+) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 31a33bfd23..d0108c823a 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -17,6 +17,7 @@ class Projects::BlobController < Projects::ApplicationController before_action :require_branch_head, only: [:edit, :update] before_action :editor_variables, except: [:show, :preview, :diff] before_action :after_edit_path, only: [:edit, :update] + before_action :show_lfs_object, only: :show def new commit unless @repository.empty? @@ -193,4 +194,20 @@ class Projects::BlobController < Projects::ApplicationController file_content_encoding: params[:encoding] } end + + def show_lfs_object + return unless @blob && @blob.text? && @blob.data.present? + + if @blob.data.starts_with?("version https://git-lfs.github.com/spec") + oid = @blob.data.match(/#{LfsObject::MATCH_FROM_POINTER_REGEX}/) + if oid && oid[1] + lfs_object = LfsObject.find_by_oid(oid[1]) + return nil unless lfs_object && lfs_object.file.exists? + + if lfs_object.projects.exists?(lfs_object.storage_project(@project).id) + send_file lfs_object.file.path, filename: @blob.name, disposition: 'attachment' + end + end + end + end end diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 3c1426f59d..f087d07b5b 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -5,4 +5,14 @@ class LfsObject < ActiveRecord::Base validates :oid, presence: true, uniqueness: true mount_uploader :file, LfsObjectUploader + + MATCH_FROM_POINTER_REGEX = "(?<=sha256:)([0-9a-f]{64})" + + def storage_project(project) + if project && project.forked? + project.forked_from_project + else + project + end + end end From 5145706c82613d64462fe736850d09799224cd77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 25 Nov 2015 19:20:40 -0500 Subject: [PATCH 294/908] Run custom Git hooks when creating or deleting branches through the UI. #1156 --- CHANGELOG | 1 + app/models/repository.rb | 54 +++++++------ app/services/create_branch_service.rb | 5 +- app/services/delete_branch_service.rb | 4 +- app/services/files/base_service.rb | 2 +- app/services/git_hooks_service.rb | 32 ++++++++ spec/models/repository_spec.rb | 100 ++++++++++++++++++++++++ spec/services/git_hooks_service_spec.rb | 38 +++++++++ 8 files changed, 207 insertions(+), 29 deletions(-) create mode 100644 app/services/git_hooks_service.rb create mode 100644 spec/services/git_hooks_service_spec.rb diff --git a/CHANGELOG b/CHANGELOG index db812796b6..882648b36a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ v 8.3.0 (unreleased) - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) - Add ignore whitespace change option to commit view - Fire update hook from GitLab + - Run custom Git hooks when branch is created or deleted. #1156 v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) diff --git a/app/models/repository.rb b/app/models/repository.rb index d247b0f501..c304955b0b 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1,7 +1,6 @@ require 'securerandom' class Repository - class PreReceiveError < StandardError; end class CommitError < StandardError; end include Gitlab::ShellAdapter @@ -108,10 +107,19 @@ class Repository tags.find { |tag| tag.name == name } end - def add_branch(branch_name, ref) - expire_branches_cache + def add_branch(user, branch_name, target) + oldrev = Gitlab::Git::BLANK_SHA + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + target = commit(target).try(:id) - gitlab_shell.add_branch(path_with_namespace, branch_name, ref) + return false unless target + + GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do + rugged.branches.create(branch_name, target) + end + + expire_branches_cache + find_branch(branch_name) end def add_tag(tag_name, ref, message = nil) @@ -120,10 +128,20 @@ class Repository gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message) end - def rm_branch(branch_name) + def rm_branch(user, branch_name) expire_branches_cache - gitlab_shell.rm_branch(path_with_namespace, branch_name) + branch = find_branch(branch_name) + oldrev = branch.try(:target) + newrev = Gitlab::Git::BLANK_SHA + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + + GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do + rugged.branches.delete(branch_name) + end + + expire_branches_cache + true end def rm_tag(tag_name) @@ -550,7 +568,6 @@ class Repository def commit_with_hooks(current_user, branch) oldrev = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + branch - gl_id = Gitlab::ShellEnv.gl_id(current_user) was_empty = empty? # Create temporary ref @@ -569,15 +586,7 @@ class Repository raise CommitError.new('Failed to create commit') end - # Run GitLab pre-receive hook - pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', path_to_repo) - pre_receive_hook_status = pre_receive_hook.trigger(gl_id, oldrev, newrev, ref) - - # Run GitLab update hook - update_hook = Gitlab::Git::Hook.new('update', path_to_repo) - update_hook_status = update_hook.trigger(gl_id, oldrev, newrev, ref) - - if pre_receive_hook_status && update_hook_status + GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do if was_empty # Create branch rugged.references.create(ref, newrev) @@ -592,16 +601,11 @@ class Repository raise CommitError.new('Commit was rejected because branch received new push') end end - - # Run GitLab post receive hook - post_receive_hook = Gitlab::Git::Hook.new('post-receive', path_to_repo) - post_receive_hook.trigger(gl_id, oldrev, newrev, ref) - else - # Remove tmp ref and return error to user - rugged.references.delete(tmp_ref) - - raise PreReceiveError.new('Commit was rejected by git hook') end + rescue GitHooksService::PreReceiveError + # Remove tmp ref and return error to user + rugged.references.delete(tmp_ref) + raise end private diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index cf7ae4345f..de18f3bc55 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -13,8 +13,7 @@ class CreateBranchService < BaseService return error('Branch already exists') end - repository.add_branch(branch_name, ref) - new_branch = repository.find_branch(branch_name) + new_branch = repository.add_branch(current_user, branch_name, ref) if new_branch push_data = build_push_data(project, current_user, new_branch) @@ -27,6 +26,8 @@ class CreateBranchService < BaseService else error('Invalid reference name') end + rescue GitHooksService::PreReceiveError + error('Branch creation was rejected by Git hook') end def success(branch) diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index b19b112a0c..22bf9dd935 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -24,7 +24,7 @@ class DeleteBranchService < BaseService return error('You dont have push access to repo', 405) end - if repository.rm_branch(branch_name) + if repository.rm_branch(current_user, branch_name) push_data = build_push_data(branch) EventCreateService.new.push(project, current_user, push_data) @@ -35,6 +35,8 @@ class DeleteBranchService < BaseService else error('Failed to remove branch') end + rescue GitHooksService::PreReceiveError + error('Branch deletion was rejected by Git hook') end def error(message, return_code = 400) diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 008833eed8..f50aaf2eb5 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -26,7 +26,7 @@ module Files else error("Something went wrong. Your changes were not committed") end - rescue Repository::CommitError, Repository::PreReceiveError, ValidationError => ex + rescue Repository::CommitError, GitHooksService::PreReceiveError, ValidationError => ex error(ex.message) end diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb new file mode 100644 index 0000000000..53f1fdef79 --- /dev/null +++ b/app/services/git_hooks_service.rb @@ -0,0 +1,32 @@ +class GitHooksService + PreReceiveError = Class.new(StandardError) + + def execute(user, repo_path, oldrev, newrev, ref) + @repo_path = repo_path + @user = Gitlab::ShellEnv.gl_id(user) + @oldrev = oldrev + @newrev = newrev + @ref = ref + + pre_status = run_hook('pre-receive') + + if pre_status + yield + + run_hook('post-receive') + end + end + + private + + def run_hook(name) + hook = Gitlab::Git::Hook.new(name, @repo_path) + status = hook.trigger(@user, @oldrev, @newrev, @ref) + + if !status && (name != 'post-receive') + raise PreReceiveError.new("Git operation was rejected by #{name} hook") + end + + status + end +end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 319fa0a7c8..c746b8db62 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -4,6 +4,7 @@ describe Repository do include RepoHelpers let(:repository) { create(:project).repository } + let(:user) { create(:user) } describe :branch_names_contains do subject { repository.branch_names_contains(sample_commit.id) } @@ -99,5 +100,104 @@ describe Repository do it { expect(subject.startline).to eq(186) } it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") } end + end + + describe :add_branch do + context 'when pre hooks were successful' do + it 'should run without errors' do + hook = double(trigger: true) + expect(Gitlab::Git::Hook).to receive(:new).twice.and_return(hook) + + expect { repository.add_branch(user, 'new_feature', 'master') }.not_to raise_error + end + + it 'should create the branch' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true) + + branch = repository.add_branch(user, 'new_feature', 'master') + + expect(branch.name).to eq('new_feature') + end + end + + context 'when pre hooks failed' do + it 'should get an error' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false) + + expect do + repository.add_branch(user, 'new_feature', 'master') + end.to raise_error(GitHooksService::PreReceiveError) + end + + it 'should not create the branch' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false) + + expect do + repository.add_branch(user, 'new_feature', 'master') + end.to raise_error(GitHooksService::PreReceiveError) + expect(repository.find_branch('new_feature')).to be_nil + end + end + end + + describe :rm_branch do + context 'when pre hooks were successful' do + it 'should run without errors' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true) + + expect { repository.rm_branch(user, 'feature') }.not_to raise_error + end + + it 'should delete the branch' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true) + + expect { repository.rm_branch(user, 'feature') }.not_to raise_error + + expect(repository.find_branch('feature')).to be_nil + end + end + + context 'when pre hooks failed' do + it 'should get an error' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false) + + expect do + repository.rm_branch(user, 'new_feature') + end.to raise_error(GitHooksService::PreReceiveError) + end + + it 'should not delete the branch' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false) + + expect do + repository.rm_branch(user, 'feature') + end.to raise_error(GitHooksService::PreReceiveError) + expect(repository.find_branch('feature')).not_to be_nil + end + end + end + + describe :commit_with_hooks do + context 'when pre hooks were successful' do + it 'should run without errors' do + expect_any_instance_of(GitHooksService).to receive(:execute).and_return(true) + + expect do + repository.commit_with_hooks(user, 'feature') { sample_commit.id } + end.not_to raise_error + end + end + + context 'when pre hooks failed' do + it 'should get an error' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false) + + expect do + repository.commit_with_hooks(user, 'feature') { sample_commit.id } + end.to raise_error(GitHooksService::PreReceiveError) + end + end + end + end diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb new file mode 100644 index 0000000000..21585cc462 --- /dev/null +++ b/spec/services/git_hooks_service_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe GitHooksService do + include RepoHelpers + + let(:user) { create :user } + let(:project) { create :project } + let(:service) { GitHooksService.new } + + before do + @blankrev = Gitlab::Git::BLANK_SHA + @oldrev = sample_commit.parent_id + @newrev = sample_commit.id + @ref = 'refs/heads/feature' + @repo_path = project.repository.path_to_repo + end + + describe '#execute' do + + context 'when pre hooks were successful' do + it 'should call post hooks' do + expect(service).to receive(:run_hook).with('pre-receive').and_return(true) + expect(service).to receive(:run_hook).with('post-receive').and_return(true) + expect(service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }).to eq(true) + end + end + + context 'when pre hooks failed' do + it 'should not call post hooks' do + expect(service).to receive(:run_hook).with('pre-receive').and_return(false) + expect(service).not_to receive(:run_hook).with('post-receive') + + service.execute(user, @repo_path, @blankrev, @newrev, @ref) + end + end + + end +end From 338eb2c41ea766779d6bb7798079a1dd3a50e11d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 1 Dec 2015 00:22:45 -0500 Subject: [PATCH 295/908] Call update hook from new GitHooksService class. #3069 --- app/services/git_hooks_service.rb | 4 +--- spec/models/repository_spec.rb | 2 +- spec/services/git_hooks_service_spec.rb | 23 +++++++++++++++++------ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb index 53f1fdef79..b7804ed472 100644 --- a/app/services/git_hooks_service.rb +++ b/app/services/git_hooks_service.rb @@ -8,9 +8,7 @@ class GitHooksService @newrev = newrev @ref = ref - pre_status = run_hook('pre-receive') - - if pre_status + if run_hook('pre-receive') && run_hook('update') yield run_hook('post-receive') diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index c746b8db62..fa261e64c3 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -107,7 +107,7 @@ describe Repository do context 'when pre hooks were successful' do it 'should run without errors' do hook = double(trigger: true) - expect(Gitlab::Git::Hook).to receive(:new).twice.and_return(hook) + expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) expect { repository.add_branch(user, 'new_feature', 'master') }.not_to raise_error end diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb index 21585cc462..bb639a5ae2 100644 --- a/spec/services/git_hooks_service_spec.rb +++ b/spec/services/git_hooks_service_spec.rb @@ -17,16 +17,17 @@ describe GitHooksService do describe '#execute' do - context 'when pre hooks were successful' do - it 'should call post hooks' do - expect(service).to receive(:run_hook).with('pre-receive').and_return(true) - expect(service).to receive(:run_hook).with('post-receive').and_return(true) + context 'when receive hooks were successful' do + it 'should call post-receive hook' do + hook = double(trigger: true) + expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) + expect(service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }).to eq(true) end end - context 'when pre hooks failed' do - it 'should not call post hooks' do + context 'when pre-receive hook failed' do + it 'should not call post-receive hook' do expect(service).to receive(:run_hook).with('pre-receive').and_return(false) expect(service).not_to receive(:run_hook).with('post-receive') @@ -34,5 +35,15 @@ describe GitHooksService do end end + context 'when update hook failed' do + it 'should not call post-receive hook' do + expect(service).to receive(:run_hook).with('pre-receive').and_return(true) + expect(service).to receive(:run_hook).with('update').and_return(false) + expect(service).not_to receive(:run_hook).with('post-receive') + + service.execute(user, @repo_path, @blankrev, @newrev, @ref) + end + end + end end From 5e6a5270d52ea2f13ca9967913012691e257439b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 1 Dec 2015 12:31:44 -0500 Subject: [PATCH 296/908] Raise the exception from #execute instead of #run_hook. #1156 #3069 --- app/services/git_hooks_service.rb | 20 +++++++++----------- spec/services/git_hooks_service_spec.rb | 8 ++++++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb index b7804ed472..8f5c3393df 100644 --- a/app/services/git_hooks_service.rb +++ b/app/services/git_hooks_service.rb @@ -8,23 +8,21 @@ class GitHooksService @newrev = newrev @ref = ref - if run_hook('pre-receive') && run_hook('update') - yield - - run_hook('post-receive') + %w(pre-receive update).each do |hook_name| + unless run_hook(hook_name) + raise PreReceiveError.new("Git operation was rejected by #{hook_name} hook") + end end + + yield + + run_hook('post-receive') end private def run_hook(name) hook = Gitlab::Git::Hook.new(name, @repo_path) - status = hook.trigger(@user, @oldrev, @newrev, @ref) - - if !status && (name != 'post-receive') - raise PreReceiveError.new("Git operation was rejected by #{name} hook") - end - - status + hook.trigger(@user, @oldrev, @newrev, @ref) end end diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb index bb639a5ae2..7e018d3c9f 100644 --- a/spec/services/git_hooks_service_spec.rb +++ b/spec/services/git_hooks_service_spec.rb @@ -31,7 +31,9 @@ describe GitHooksService do expect(service).to receive(:run_hook).with('pre-receive').and_return(false) expect(service).not_to receive(:run_hook).with('post-receive') - service.execute(user, @repo_path, @blankrev, @newrev, @ref) + expect do + service.execute(user, @repo_path, @blankrev, @newrev, @ref) + end.to raise_error(GitHooksService::PreReceiveError) end end @@ -41,7 +43,9 @@ describe GitHooksService do expect(service).to receive(:run_hook).with('update').and_return(false) expect(service).not_to receive(:run_hook).with('post-receive') - service.execute(user, @repo_path, @blankrev, @newrev, @ref) + expect do + service.execute(user, @repo_path, @blankrev, @newrev, @ref) + end.to raise_error(GitHooksService::PreReceiveError) end end From dbbd2b863b402e460ac1dc90f852fcae617a2351 Mon Sep 17 00:00:00 2001 From: Greg Smethells Date: Mon, 30 Nov 2015 14:47:44 -0600 Subject: [PATCH 297/908] sort milestones by due_date --- CHANGELOG | 1 + app/controllers/concerns/global_milestones.rb | 2 ++ app/finders/milestones_finder.rb | 2 +- app/helpers/milestones_helper.rb | 2 ++ app/models/global_milestone.rb | 31 ++++++++++++++++++- .../dashboard/milestones/_milestone.html.haml | 11 ++++--- .../projects/milestones/_milestone.html.haml | 6 +--- app/views/shared/_milestone_expired.html.haml | 5 +++ 8 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 app/views/shared/_milestone_expired.html.haml diff --git a/CHANGELOG b/CHANGELOG index db812796b6..aad58af667 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ v 8.3.0 (unreleased) - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) - Add ignore whitespace change option to commit view - Fire update hook from GitLab + - Fix: sort milestones by due date once again (Greg Smethells) v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) diff --git a/app/controllers/concerns/global_milestones.rb b/app/controllers/concerns/global_milestones.rb index b428249acd..3e4c0e6360 100644 --- a/app/controllers/concerns/global_milestones.rb +++ b/app/controllers/concerns/global_milestones.rb @@ -2,8 +2,10 @@ module GlobalMilestones extend ActiveSupport::Concern def milestones + epoch = DateTime.parse('1970-01-01') @milestones = MilestonesFinder.new.execute(@projects, params) @milestones = GlobalMilestone.build_collection(@milestones) + @milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } @milestones = Kaminari.paginate_array(@milestones).page(params[:page]).per(ApplicationController::PER_PAGE) end diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index b704e87890..630c73c2a9 100644 --- a/app/finders/milestones_finder.rb +++ b/app/finders/milestones_finder.rb @@ -1,7 +1,7 @@ class MilestonesFinder def execute(projects, params) milestones = Milestone.of_projects(projects) - milestones = milestones.order("due_date ASC") + milestones = milestones.reorder("due_date ASC") case params[:state] when 'closed' then milestones.closed diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index ad43892b63..a42cbcff18 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -28,7 +28,9 @@ module MilestonesHelper Milestone.where(project_id: @projects) end.active + epoch = DateTime.parse('1970-01-01') grouped_milestones = GlobalMilestone.build_collection(milestones) + grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } grouped_milestones.unshift(Milestone::None) grouped_milestones.unshift(Milestone::Any) diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 1321ccd963..33ddb265fb 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -19,6 +19,14 @@ class GlobalMilestone @title.parameterize end + def expired? + if due_date + due_date.past? + else + false + end + end + def projects milestones.map { |milestone| milestone.project } end @@ -98,4 +106,25 @@ class GlobalMilestone def complete? total_items_count == closed_items_count end -end + + def due_date + return @due_date if defined?(@due_date) + + @due_date = + if @milestones.all? { |x| x.due_date == @milestones.first.due_date } + @milestones.first.due_date + else + nil + end + end + + def expires_at + if due_date + if due_date.past? + "expired at #{due_date.stamp("Aug 21, 2011")}" + else + "expires at #{due_date.stamp("Aug 21, 2011")}" + end + end + end +end \ No newline at end of file diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml index 55080d6b3f..7c882a3270 100644 --- a/app/views/dashboard/milestones/_milestone.html.haml +++ b/app/views/dashboard/milestones/_milestone.html.haml @@ -16,7 +16,10 @@ = milestone_progress_bar(milestone) .row .col-sm-6 - - milestone.milestones.each do |milestone| - = link_to milestone_path(milestone) do - %span.label.label-gray - = milestone.project.name_with_namespace + .expiration + = render 'shared/milestone_expired', milestone: milestone + .projects + - milestone.milestones.each do |milestone| + = link_to milestone_path(milestone) do + %span.label.label-gray + = milestone.project.name_with_namespace diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index 334172b976..d6a44c9f0a 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -18,11 +18,7 @@ .row .col-sm-6 - - if milestone.expired? and not milestone.closed? - %span.cred (Expired) - - if milestone.expires_at - %span - = milestone.expires_at + = render 'shared/milestone_expired', milestone: milestone .col-sm-6 - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs edit-milestone-link btn-grouped" do diff --git a/app/views/shared/_milestone_expired.html.haml b/app/views/shared/_milestone_expired.html.haml new file mode 100644 index 0000000000..b8eef15fbe --- /dev/null +++ b/app/views/shared/_milestone_expired.html.haml @@ -0,0 +1,5 @@ +- if milestone.expired? and not milestone.closed? + %span.cred (Expired) +- if milestone.expires_at + %span + = milestone.expires_at From 86ed2e43d58ef074ae3b1d80e0b18fe338ca9afd Mon Sep 17 00:00:00 2001 From: Phillip Berndt Date: Thu, 3 Dec 2015 16:04:39 +0100 Subject: [PATCH 298/908] time_ago_with_tooltip javascript snippet: Don't reinitialize older elements see bug #3758 --- app/helpers/application_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3230ff1b00..21f962df20 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -209,7 +209,7 @@ module ApplicationHelper title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'), data: { toggle: 'tooltip', placement: placement, container: 'body' } - element += javascript_tag "$('.js-timeago').timeago()" unless skip_js + element += javascript_tag "$('.js-timeago').last().timeago()" unless skip_js element end From 11b54edf36c8d44f02004de166cf8b809df721ad Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Thu, 3 Dec 2015 09:36:45 -0600 Subject: [PATCH 299/908] Filter current user to the top of issue assignee list --- app/views/shared/issuable/_context.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml index f1646b4ce6..ced16f820c 100644 --- a/app/views/shared/issuable/_context.html.haml +++ b/app/views/shared/issuable/_context.html.haml @@ -9,7 +9,7 @@ none .issuable-context-selectbox - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true) + = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true) %div.prepend-top-default.clearfix .issuable-context-title From ea52a81da4888af232e9868d722cc91d5e442723 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 3 Dec 2015 17:08:09 +0100 Subject: [PATCH 300/908] Move the file serving to Raw controller, add a few ifs to view. --- app/controllers/projects/blob_controller.rb | 17 ----------- app/controllers/projects/raw_controller.rb | 31 ++++++++++++++++----- app/views/projects/blob/_actions.html.haml | 6 ++-- app/views/projects/blob/_blob.html.haml | 6 ++-- app/views/projects/blob/_download.html.haml | 6 +++- 5 files changed, 36 insertions(+), 30 deletions(-) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index d0108c823a..31a33bfd23 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -17,7 +17,6 @@ class Projects::BlobController < Projects::ApplicationController before_action :require_branch_head, only: [:edit, :update] before_action :editor_variables, except: [:show, :preview, :diff] before_action :after_edit_path, only: [:edit, :update] - before_action :show_lfs_object, only: :show def new commit unless @repository.empty? @@ -194,20 +193,4 @@ class Projects::BlobController < Projects::ApplicationController file_content_encoding: params[:encoding] } end - - def show_lfs_object - return unless @blob && @blob.text? && @blob.data.present? - - if @blob.data.starts_with?("version https://git-lfs.github.com/spec") - oid = @blob.data.match(/#{LfsObject::MATCH_FROM_POINTER_REGEX}/) - if oid && oid[1] - lfs_object = LfsObject.find_by_oid(oid[1]) - return nil unless lfs_object && lfs_object.file.exists? - - if lfs_object.projects.exists?(lfs_object.storage_project(@project).id) - send_file lfs_object.file.path, filename: @blob.name, disposition: 'attachment' - end - end - end - end end diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index d5ee6ac866..c56f432a1f 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -10,15 +10,13 @@ class Projects::RawController < Projects::ApplicationController @blob = @repository.blob_at(@commit.id, @path) if @blob - type = get_blob_type - headers['X-Content-Type-Options'] = 'nosniff' - send_data( - @blob.data, - type: type, - disposition: 'inline' - ) + if @blob.lfs_pointer? + send_lfs_object + else + stream_data + end else render_404 end @@ -35,4 +33,23 @@ class Projects::RawController < Projects::ApplicationController 'application/octet-stream' end end + + def stream_data + type = get_blob_type + + send_data( + @blob.data, + type: type, + disposition: 'inline' + ) + end + + def send_lfs_object + lfs_object = LfsObject.find_by_oid(@blob.lfs_oid) + return nil unless lfs_object && lfs_object.file.exists? + + if lfs_object.projects.exists?(lfs_object.storage_project(@project).id) + send_file lfs_object.file.path, filename: @blob.name, disposition: 'attachment' + end + end end diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index ba3e0c3c59..15cd8f056f 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -1,9 +1,9 @@ .btn-group.tree-btn-group - = edit_blob_link(@project, @ref, @path) + = edit_blob_link(@project, @ref, @path) unless @blob.lfs_pointer? = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), class: 'btn btn-sm', target: '_blank' -# only show normal/blame view links for text files - - if @blob.text? + - if @blob.text? && !@blob.lfs_pointer? - if current_page? namespace_project_blame_path(@project.namespace, @project, @id) = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id), class: 'btn btn-sm' @@ -16,7 +16,7 @@ = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.sha, @path)), class: 'btn btn-sm' -- if allowed_tree_edit? +- if allowed_tree_edit? && !@blob.lfs_pointer? .btn-group{ role: "group" } %button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Delete diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 42f632b38e..bb9e1c6341 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -29,10 +29,12 @@ %strong = blob.name %small - = number_to_human_size(blob.size) + = number_to_human_size(blob.size) unless blob.lfs_pointer? .file-actions.hidden-xs = render "actions" - - if blob.text? + - if blob.lfs_pointer? + = render "download", blob: blob + - elsif blob.text? = render "text", blob: blob - elsif blob.image? = render "image", blob: blob diff --git a/app/views/projects/blob/_download.html.haml b/app/views/projects/blob/_download.html.haml index f2c5e95ecf..39ec6f693e 100644 --- a/app/views/projects/blob/_download.html.haml +++ b/app/views/projects/blob/_download.html.haml @@ -4,4 +4,8 @@ %h1.light %i.fa.fa-download %h4 - Download (#{number_to_human_size blob.size}) + - if blob.lfs_pointer? + - size = blob.lfs_size + - else + - size = blob.size + Download (#{number_to_human_size size}) From c204aca83b0c8308080a9f53b692f509a80ddffc Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 3 Dec 2015 15:30:10 -0200 Subject: [PATCH 301/908] Improve style of the warning when mentioning many people in a comment --- .../stylesheets/framework/markdown_area.scss | 8 +++----- app/views/projects/_md_preview.html.haml | 15 ++++++++------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index cc660529cb..11b609f3b7 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -73,11 +73,9 @@ } .referenced-users { - padding: 10px 0; - color: #999; - margin-left: 10px; - margin-top: 1px; - margin-right: 130px; + color: #4c4e54; + display: inline-block; + padding-top: 10px; } .md-preview-holder { diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index c4d3c9cc30..5bdceb9523 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -15,10 +15,11 @@ .js-md-preview{class: (preview_class if defined?(preview_class))} - if defined?(referenced_users) && referenced_users - %span.referenced-users.pull-left.hide - = icon('exclamation-triangle') - You are about to add - %strong - %span.js-referenced-users-count 0 - people - to the discussion. Proceed with caution. + %div.clearfix + %span.referenced-users.hide + = icon('exclamation-triangle') + You are about to add + %strong + %span.js-referenced-users-count 0 + people + to the discussion. Proceed with caution. From 0cbb7717df8d243d7812041099d99f731de7f82b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 3 Dec 2015 18:32:29 +0100 Subject: [PATCH 302/908] Fix spec --- features/steps/project/issues/labels.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index f749ea2eca..e273bb391b 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -32,19 +32,19 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps step 'I submit new label \'support\'' do fill_in 'Title', with: 'support' fill_in 'Background color', with: '#F95610' - click_button 'Save' + click_button 'Create Label' end step 'I submit new label \'bug\'' do fill_in 'Title', with: 'bug' fill_in 'Background color', with: '#F95610' - click_button 'Save' + click_button 'Create Label' end step 'I submit new label with invalid color' do fill_in 'Title', with: 'support' fill_in 'Background color', with: '#12' - click_button 'Save' + click_button 'Create Label' end step 'I should see label label exist error message' do @@ -86,7 +86,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps step 'I change label \'bug\' to \'fix\'' do fill_in 'Title', with: 'fix' fill_in 'Background color', with: '#F15610' - click_button 'Save' + click_button 'Save changes' end step 'I should see label \'fix\'' do From 5ab1b11e901712ab462b10f030971610ba140257 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 3 Dec 2015 18:42:37 +0100 Subject: [PATCH 303/908] Fix specs --- features/steps/project/source/markdown_render.rb | 4 ++-- features/steps/project/wiki.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb index ec88c8c20c..3a4f7a6e01 100644 --- a/features/steps/project/source/markdown_render.rb +++ b/features/steps/project/source/markdown_render.rb @@ -252,7 +252,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps step 'I see Gitlab API document' do expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "api") - expect(page).to have_content "Editing" + expect(page).to have_content "Edit Page api" end step 'I click on Rake tasks link' do @@ -261,7 +261,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps step 'I see Rake tasks directory' do expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "raketasks") - expect(page).to have_content "Editing" + expect(page).to have_content "Edit Page raketasks" end step 'I go directory which contains README file' do diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index 935c1ca8a2..91d227fadb 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -156,7 +156,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I should see the Editing page' do - expect(page).to have_content('Editing') + expect(page).to have_content('Edit Page') end step 'I view the page history of a Wiki page that has a path' do From 1dd7c978862b900b92bde355f99ce5e869a98328 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 3 Dec 2015 18:51:44 +0100 Subject: [PATCH 304/908] Fix background and padding of login and error pages --- app/assets/stylesheets/framework/layout.scss | 8 ++++++-- app/assets/stylesheets/pages/login.scss | 3 +-- app/views/layouts/devise.html.haml | 4 ++-- app/views/layouts/errors.html.haml | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index a60940a8be..aa5acb93cc 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -6,6 +6,10 @@ html { body { background-color: #EAEBEC !important; + + &.navless { + background-color: white !important; + } } .container { @@ -18,8 +22,8 @@ body { } .navless-container { - padding-top: $header-height; - margin-top: 30px; + margin-top: $header-height; + padding-top: $gl-padding * 2; } .container-limited { diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index edd5170513..f9c6f1b39f 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -1,7 +1,5 @@ /* Login Page */ .login-page { - background-color: white; - .container { max-width: 960px; } @@ -21,6 +19,7 @@ h1:first-child { font-weight: normal; margin-bottom: 30px; + margin-top: 0; } img { diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 95e077c339..f08cb0a542 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -1,13 +1,13 @@ !!! 5 %html{ lang: "en"} = render "layouts/head" - %body.ui_charcoal.login-page.application + %body.ui_charcoal.login-page.application.navless = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container .content = render "layouts/flash" - .row.prepend-top-20 + .row .col-sm-5.pull-right = yield .col-sm-7.brand-holder.pull-left diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 2af265a229..915acc4612 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head" - %body{class: "#{user_application_theme} application"} + %body{class: "#{user_application_theme} application navless"} = render "layouts/header/empty" .container.navless-container = render "layouts/flash" From a89d6d1428d61bd2ae6f530acfc5a34d5a9c46e8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 3 Dec 2015 18:53:17 +0100 Subject: [PATCH 305/908] Add authorization to new branch/tag pages. --- app/controllers/projects/branches_controller.rb | 2 +- app/controllers/projects/tags_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 3ac0a75fa7..3c2849a760 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -3,7 +3,7 @@ class Projects::BranchesController < Projects::ApplicationController # Authorize before_action :require_non_empty_project before_action :authorize_download_code! - before_action :authorize_push_code!, only: [:create, :destroy] + before_action :authorize_push_code!, only: [:new, :create, :destroy] def index @sort = params[:sort] || 'name' diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index cb39c2b878..280fe12cc7 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -2,7 +2,7 @@ class Projects::TagsController < Projects::ApplicationController # Authorize before_action :require_non_empty_project before_action :authorize_download_code! - before_action :authorize_push_code!, only: [:create] + before_action :authorize_push_code!, only: [:new, :create] before_action :authorize_admin_project!, only: [:destroy] def index From 494ebad39b465ca6a70888ca376db0dd962a618f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 3 Dec 2015 18:56:43 +0100 Subject: [PATCH 306/908] Fix spec --- features/steps/project/issues/issues.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index af2da41bad..9683bebd56 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -65,7 +65,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps step 'I see current user as the first user' do expect(page).to have_selector('.user-result', visible: true, count: 4) users = page.all('.user-name') - expect(users[0].text).to eq 'Any' + expect(users[0].text).to eq 'Any Assignee' expect(users[1].text).to eq 'Unassigned' expect(users[2].text).to eq current_user.name end From e41a0c60f448057f69c2952f15d88f9d8191c2fe Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 3 Dec 2015 18:59:03 +0100 Subject: [PATCH 307/908] Add missing milestone ID --- app/models/milestone.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index c2642b75b8..58acc5d6cc 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -16,9 +16,9 @@ class Milestone < ActiveRecord::Base # Represents a "No Milestone" state used for filtering Issues and Merge # Requests that have no milestone assigned. - MilestoneStruct = Struct.new(:title, :name) - None = MilestoneStruct.new('No Milestone', 'No Milestone') - Any = MilestoneStruct.new('Any', '') + MilestoneStruct = Struct.new(:title, :name, :id) + None = MilestoneStruct.new('No Milestone', 'No Milestone', 0) + Any = MilestoneStruct.new('Any', '', -1) include InternalId include Sortable From f9d954fae71d40a314a5812c1a7eec5a601d1575 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 3 Dec 2015 19:02:56 +0100 Subject: [PATCH 308/908] Satisfy rubocop --- lib/gitlab/markdown/label_reference_filter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb index 36cf7a8f4c..a2026eecae 100644 --- a/lib/gitlab/markdown/label_reference_filter.rb +++ b/lib/gitlab/markdown/label_reference_filter.rb @@ -70,7 +70,7 @@ module Gitlab def url_for_label(project, label) h = Gitlab::Application.routes.url_helpers h.namespace_project_issues_url( project.namespace, project, label_name: label.name, - only_path: context[:only_path]) + only_path: context[:only_path]) end def render_colored_label(label) From 82e916b0f2cbd500d14f545095ac82464c0a3889 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Tue, 1 Dec 2015 18:11:12 -0200 Subject: [PATCH 309/908] Touch project when toggling stars to update cache --- app/models/users_star_project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb index 3d49cb0594..413f3f485a 100644 --- a/app/models/users_star_project.rb +++ b/app/models/users_star_project.rb @@ -10,7 +10,7 @@ # class UsersStarProject < ActiveRecord::Base - belongs_to :project, counter_cache: :star_count + belongs_to :project, counter_cache: :star_count, touch: true belongs_to :user validates :user, presence: true From e5c865eebf95d58ed33ef3d86f7582aa2db9bb09 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 3 Dec 2015 20:01:35 +0100 Subject: [PATCH 310/908] Fix specs --- app/assets/stylesheets/pages/issuable.scss | 11 +++++++++++ app/assets/stylesheets/pages/issues.scss | 11 ----------- app/assets/stylesheets/pages/merge_requests.scss | 11 ----------- app/views/shared/issuable/_context.html.haml | 2 +- features/steps/project/forked_merge_requests.rb | 2 +- features/steps/project/issues/issues.rb | 2 +- 6 files changed, 14 insertions(+), 25 deletions(-) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 51d8e5b465..b5f449ff69 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -90,6 +90,17 @@ } } +.issuable-show-labels { + a { + margin-right: 5px; + margin-bottom: 5px; + display: inline-block; + .color-label { + padding: 6px 10px; + } + } +} + .cross-project-reference { text-align: center; width: 100%; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 41c069f0ad..f5548c5b88 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -56,17 +56,6 @@ } } -.issue-show-labels { - a { - margin-right: 5px; - margin-bottom: 5px; - display: inline-block; - .color-label { - padding: 6px 10px; - } - } -} - form.edit-issue { margin: 0; } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 017a86bcd9..af6c6830fa 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -183,17 +183,6 @@ display: none; } -.merge-request-show-labels { - a { - margin-right: 5px; - margin-bottom: 5px; - display: inline-block; - .color-label { - padding: 6px 10px; - } - } -} - .merge-request-form .select2-container { width: 250px !important; } diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml index 521364ac85..f44b439a84 100644 --- a/app/views/shared/issuable/_context.html.haml +++ b/app/views/shared/issuable/_context.html.haml @@ -33,7 +33,7 @@ %div.prepend-top-default.clearfix .issuable-context-title %label Labels - .merge-request-show-labels + .issuable-show-labels - issuable.labels.each do |label| = link_to_label(label) diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 789befc9df..34b435919a 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -115,7 +115,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps expect(find(:select, "merge_request_source_project_id", {}).value).to eq @forked_project.id.to_s expect(find(:select, "merge_request_target_project_id", {}).value).to eq @project.id.to_s expect(find(:select, "merge_request_source_branch", {}).value).to eq "" - expect(find(:select, "merge_request_target_branch", {}).value).to eq "" + expect(find(:select, "merge_request_target_branch", {}).value).to eq "master" click_button "Compare branches" end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index af2da41bad..264408e85e 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -86,7 +86,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I should see label \'bug\' with issue' do - page.within '.issue-show-labels' do + page.within '.issuable-show-labels' do expect(page).to have_content 'bug' end end From c5b6b31c847d5d2f467453d8292e13ca735ee630 Mon Sep 17 00:00:00 2001 From: Anton Baklanov Date: Wed, 18 Nov 2015 20:08:07 +0200 Subject: [PATCH 311/908] Fixed invalid link on starred projects dashboard. Fixes #3468 --- CHANGELOG | 1 + app/views/dashboard/projects/index.html.haml | 2 +- app/views/dashboard/projects/starred.html.haml | 2 +- app/views/explore/projects/index.html.haml | 2 +- app/views/explore/projects/starred.html.haml | 2 +- app/views/explore/projects/trending.html.haml | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index db812796b6..438cae617a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ v 8.2.2 - Fix Error 500 when viewing user's personal projects from admin page (Stan Hu) - Fix: Raw private snippets access workflow - Prevent "413 Request entity too large" errors when pushing large files with LFS + - Fix invalid links within projects dashboard header v 8.2.1 - Forcefully update builds that didn't want to update with state machine diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 7a16b811f6..53abf274bd 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -3,7 +3,7 @@ = auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity") - page_title "Projects" -- header_title "Projects", root_path +- header_title "Projects", dashboard_projects_path = render 'dashboard/projects_head' diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index f75f2e0a32..70705923d4 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -1,5 +1,5 @@ - page_title "Starred Projects" -- header_title "Projects", projects_path +- header_title "Projects", dashboard_projects_path = render 'dashboard/projects_head' diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index 67e38ca312..76bdd68fd7 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -1,5 +1,5 @@ - page_title "Projects" -- header_title "Projects", root_path +- header_title "Projects", dashboard_projects_path - if current_user = render 'dashboard/projects_head' diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml index 596cb0a96c..e30c363322 100644 --- a/app/views/explore/projects/starred.html.haml +++ b/app/views/explore/projects/starred.html.haml @@ -1,5 +1,5 @@ - page_title "Projects" -- header_title "Projects", root_path +- header_title "Projects", dashboard_projects_path - if current_user = render 'dashboard/projects_head' diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml index 5ea6d81c5b..1412b19acd 100644 --- a/app/views/explore/projects/trending.html.haml +++ b/app/views/explore/projects/trending.html.haml @@ -1,5 +1,5 @@ - page_title "Projects" -- header_title "Projects", root_path +- header_title "Projects", dashboard_projects_path - if current_user = render 'dashboard/projects_head' From 0decc7f941b96bf53e172de0340847d3b7d714b7 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 3 Dec 2015 19:40:31 -0200 Subject: [PATCH 312/908] Fix specs --- app/assets/stylesheets/framework/markdown_area.scss | 1 - app/views/projects/_md_preview.html.haml | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 11b609f3b7..2b04478673 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -74,7 +74,6 @@ .referenced-users { color: #4c4e54; - display: inline-block; padding-top: 10px; } diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 5bdceb9523..54c818baaf 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -15,8 +15,8 @@ .js-md-preview{class: (preview_class if defined?(preview_class))} - if defined?(referenced_users) && referenced_users - %div.clearfix - %span.referenced-users.hide + %div.referenced-users.hide + %span = icon('exclamation-triangle') You are about to add %strong From 83e6d8294f9e1c4ee199a3577e5fc810df33def6 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Thu, 3 Dec 2015 23:14:19 +0100 Subject: [PATCH 313/908] Update .ruby-version to 2.1.7 --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ruby-version b/.ruby-version index 399088bf46..04b10b4f15 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.1.6 +2.1.7 From 387e5656a1e158eaa1c010f22d332d758b336179 Mon Sep 17 00:00:00 2001 From: Kevin Pankonen Date: Thu, 3 Dec 2015 15:40:08 -0700 Subject: [PATCH 314/908] fixes #3263 slashes are replaced with two underscores --- doc/ci/docker/using_docker_images.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index ef8a7ec1e8..64e52eba3a 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -60,11 +60,11 @@ This is image that have fully preconfigured `wordpress` and have `MySQL` server ``` Next time when you run your application the `tutum/wordpress` will be started -and you will have access to it from your build container under hostname: `tutum_wordpress`. +and you will have access to it from your build container under hostname: `tutum__wordpress`. Alias hostname for the service is made from the image name: 1. Everything after `:` is stripped, -2. '/' is replaced to `_`. +2. '/' is replaced with `__`. ### Configuring services Many services accept environment variables, which allow you to easily change database names or set account names depending on the environment. From 0ccd7de7f369d98615833867abe84142bcc25193 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 4 Dec 2015 10:55:36 +0100 Subject: [PATCH 315/908] Fix wrong doc in merge request API Signed-off-by: Dmitriy Zaporozhets --- doc/api/merge_requests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 0cef09d5b2..3a1fc406fd 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -159,7 +159,7 @@ Parameters: "updated_at": "2015-02-02T19:49:26.013Z", "due_date": null }, - "files": [ + "changes": [ { "old_path": "VERSION", "new_path": "VERSION", From 0b68a0e79e1cbe12c44beb6c23e79d48b71f219e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 4 Dec 2015 11:08:10 +0100 Subject: [PATCH 316/908] Add API endpoint to fetch merge request commits list Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + doc/api/merge_requests.md | 39 ++++++++++++++++++++++++ lib/api/merge_requests.rb | 16 ++++++++++ spec/requests/api/merge_requests_spec.rb | 17 +++++++++++ 4 files changed, 73 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 2f310a4b02..2876e36f9e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ v 8.3.0 (unreleased) - Add ignore whitespace change option to commit view - Fire update hook from GitLab - Don't show project fork event as "imported" + - Add API endpoint to fetch merge request commits list v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 3a1fc406fd..2b1498c85a 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -101,6 +101,45 @@ Parameters: } ``` +## Get single MR commits + +Get a list of repository commits in a merge request. + +``` +GET /projects/:id/merge_request/:merge_request_id/commits +``` + +Parameters: + +- `id` (required) - The ID of a project +- `merge_request_id` (required) - The ID of MR + + +```json +[ + { + "id": "ed899a2f4b50b4370feeea94676502b42383c746", + "short_id": "ed899a2f4b5", + "title": "Replace sanitize with escape once", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dzaporozhets@sphereconsultinginc.com", + "created_at": "2012-09-20T11:50:22+03:00", + "message": "Replace sanitize with escape once", + "allow_failure": false + }, + { + "id": "6104942438c14ec7bd21c6cd5bd995272b3faff6", + "short_id": "6104942438c", + "title": "Sanitize for network graph", + "author_name": "randx", + "author_email": "dmitriy.zaporozhets@gmail.com", + "created_at": "2012-09-20T09:06:12+03:00", + "message": "Sanitize for network graph", + "allow_failure": false + } +] +``` + ## Get single MR changes Shows information about the merge request including its files and changes. diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 6eb84baf9c..e7c5f808ae 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -76,6 +76,22 @@ module API present merge_request, with: Entities::MergeRequest end + # Show MR commits + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - The ID of MR + # + # Example: + # GET /projects/:id/merge_request/:merge_request_id/commits + # + get ':id/merge_request/:merge_request_id/commits' do + merge_request = user_project.merge_requests. + find(params[:merge_request_id]) + authorize! :read_merge_request, merge_request + present merge_request.commits, with: Entities::RepoCommit + end + # Show MR changes # # Parameters: diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index a68c7b1e46..c6d3aef0af 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -131,6 +131,23 @@ describe API::API, api: true do end end + describe 'GET /projects/:id/merge_request/:merge_request_id/commits' do + context 'valid merge request' do + before { get api("/projects/#{project.id}/merge_request/#{merge_request.id}/commits", user) } + let(:commit) { merge_request.commits.first } + + it { expect(response.status).to eq 200 } + it { expect(json_response.size).to eq(merge_request.commits.size) } + it { expect(json_response.first['id']).to eq(commit.id) } + it { expect(json_response.first['title']).to eq(commit.title) } + end + + it 'returns a 404 when merge_request_id not found' do + get api("/projects/#{project.id}/merge_request/999/commits", user) + expect(response.status).to eq(404) + end + end + describe 'GET /projects/:id/merge_request/:merge_request_id/changes' do it 'should return the change information of the merge_request' do get api("/projects/#{project.id}/merge_request/#{merge_request.id}/changes", user) From c366e81da7fb6c9ef921cbd57e5ac662999f0bc0 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 4 Dec 2015 11:26:35 +0100 Subject: [PATCH 317/908] Improve docs Signed-off-by: Dmitriy Zaporozhets --- doc/api/merge_requests.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 2b1498c85a..82f2cef969 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -103,7 +103,7 @@ Parameters: ## Get single MR commits -Get a list of repository commits in a merge request. +Get a list of merge request commits. ``` GET /projects/:id/merge_request/:merge_request_id/commits @@ -124,8 +124,7 @@ Parameters: "author_name": "Dmitriy Zaporozhets", "author_email": "dzaporozhets@sphereconsultinginc.com", "created_at": "2012-09-20T11:50:22+03:00", - "message": "Replace sanitize with escape once", - "allow_failure": false + "message": "Replace sanitize with escape once" }, { "id": "6104942438c14ec7bd21c6cd5bd995272b3faff6", @@ -134,8 +133,7 @@ Parameters: "author_name": "randx", "author_email": "dmitriy.zaporozhets@gmail.com", "created_at": "2012-09-20T09:06:12+03:00", - "message": "Sanitize for network graph", - "allow_failure": false + "message": "Sanitize for network graph" } ] ``` From b9a0f96d42f52dfdad57ac8a2a5056a9262e6a88 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 4 Dec 2015 12:01:08 +0100 Subject: [PATCH 318/908] Don't show diffs for lfs pointers. --- app/models/lfs_object.rb | 2 -- app/views/projects/diffs/_file.html.haml | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index f087d07b5b..43b845b29c 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -6,8 +6,6 @@ class LfsObject < ActiveRecord::Base mount_uploader :file, LfsObjectUploader - MATCH_FROM_POINTER_REGEX = "(?<=sha256:)([0-9a-f]{64})" - def storage_project(project) if project && project.forked? project.forked_from_project diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index b3392d00e0..ec1c665716 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -25,7 +25,7 @@ = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" .diff-controls - - if blob.text? + - if blob.text? && !blob.lfs_pointer? = link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do %i.fa.fa-comments   @@ -40,7 +40,7 @@ .diff-content.diff-wrap-lines -# Skipp all non non-supported blobs - return unless blob.respond_to?('text?') - - if blob.text? + - if blob.text? && !blob.lfs_pointer? - if diff_view == 'parallel' = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i - else From 3227a5ead22c90873794b0c0e4c788436283fb3e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 4 Dec 2015 12:21:06 +0100 Subject: [PATCH 319/908] Extent Event and Note API * add note to Events API * add author section to Events API * add noteable_id and noteable_type to Notes API Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + doc/api/notes.md | 15 +++++-- doc/api/projects.md | 71 ++++++++++++++++++++++++++++-- lib/api/entities.rb | 3 ++ spec/requests/api/projects_spec.rb | 30 ++++++++++--- 5 files changed, 106 insertions(+), 14 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2f310a4b02..002a132b9c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ v 8.3.0 (unreleased) - Add ignore whitespace change option to commit view - Fire update hook from GitLab - Don't show project fork event as "imported" + - Expose events API with comment information and author info v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) diff --git a/doc/api/notes.md b/doc/api/notes.md index e7f299c099..4d7ef288df 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -35,7 +35,9 @@ Parameters: "created_at": "2013-10-02T09:22:45Z", "system": true, "upvote": false, - "downvote": false + "downvote": false, + "noteable_id": 377, + "noteable_type": "Issue" }, { "id": 305, @@ -52,7 +54,9 @@ Parameters: "created_at": "2013-10-02T09:56:03Z", "system": true, "upvote": false, - "downvote": false + "downvote": false, + "noteable_id": 121, + "noteable_type": "Issue" } ] ``` @@ -219,7 +223,12 @@ Parameters: "state": "active", "created_at": "2013-09-30T13:46:01Z" }, - "created_at": "2013-10-02T08:57:14Z" + "created_at": "2013-10-02T08:57:14Z", + "system": false, + "upvote": false, + "downvote": false, + "noteable_id": 2, + "noteable_type": "MergeRequest" } ``` diff --git a/doc/api/projects.md b/doc/api/projects.md index 755cc6525c..42919a312a 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -245,9 +245,17 @@ Parameters: "target_id": 830, "target_type": "Issue", "author_id": 1, - "author_username": "john", "data": null, - "target_title": "Public project search field" + "target_title": "Public project search field", + "author": { + "name": "Dmitriy Zaporozhets", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", + "web_url": "http://localhost:3000/u/root" + }, + "author_username": "root" }, { "title": null, @@ -256,6 +264,14 @@ Parameters: "target_id": null, "target_type": null, "author_id": 1, + "author": { + "name": "Dmitriy Zaporozhets", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", + "web_url": "http://localhost:3000/u/root" + }, "author_username": "john", "data": { "before": "50d4420237a9de7be1304607147aec22e4a14af7", @@ -292,9 +308,56 @@ Parameters: "target_id": 840, "target_type": "Issue", "author_id": 1, - "author_username": "john", "data": null, - "target_title": "Finish & merge Code search PR" + "target_title": "Finish & merge Code search PR", + "author": { + "name": "Dmitriy Zaporozhets", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", + "web_url": "http://localhost:3000/u/root" + }, + "author_username": "root" + }, + { + "title": null, + "project_id": 15, + "action_name": "commented on", + "target_id": 1312, + "target_type": "Note", + "author_id": 1, + "data": null, + "target_title": null, + "created_at": "2015-12-04T10:33:58.089Z", + "note": { + "id": 1312, + "body": "What an awesome day!", + "attachment": null, + "author": { + "name": "Dmitriy Zaporozhets", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", + "web_url": "http://localhost:3000/u/root" + }, + "created_at": "2015-12-04T10:33:56.698Z", + "system": false, + "upvote": false, + "downvote": false, + "noteable_id": 377, + "noteable_type": "Issue" + }, + "author": { + "name": "Dmitriy Zaporozhets", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", + "web_url": "http://localhost:3000/u/root" + }, + "author_username": "root" } ] ``` diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 9f337bc3cc..96b73df6af 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -194,6 +194,7 @@ module API expose :author, using: Entities::UserBasic expose :created_at expose :system?, as: :system + expose :noteable_id, :noteable_type # upvote? and downvote? are deprecated, always return false expose :upvote?, as: :upvote expose :downvote?, as: :downvote @@ -224,6 +225,8 @@ module API expose :target_id, :target_type, :author_id expose :data, :target_title expose :created_at + expose :note, using: Entities::Note, if: ->(event, options) { event.note? } + expose :author, using: Entities::UserBasic, if: ->(event, options) { event.author } expose :author_username do |event, options| if event.author diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 9fc294118a..c59ee7af8a 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -389,14 +389,30 @@ describe API::API, api: true do describe 'GET /projects/:id/events' do before { project_member2 } - it 'should return a project events' do - get api("/projects/#{project.id}/events", user) - expect(response.status).to eq(200) - json_event = json_response.first + context 'valid request' do + before do + note = create(:note_on_issue, note: 'What an awesome day!', project: project) + EventCreateService.new.leave_note(note, note.author) + get api("/projects/#{project.id}/events", user) + end - expect(json_event['action_name']).to eq('joined') - expect(json_event['project_id'].to_i).to eq(project.id) - expect(json_event['author_username']).to eq(user3.username) + it { expect(response.status).to eq(200) } + + context 'joined event' do + let(:json_event) { json_response[1] } + + it { expect(json_event['action_name']).to eq('joined') } + it { expect(json_event['project_id'].to_i).to eq(project.id) } + it { expect(json_event['author_username']).to eq(user3.username) } + it { expect(json_event['author']['name']).to eq(user3.name) } + end + + context 'comment event' do + let(:json_event) { json_response.first } + + it { expect(json_event['action_name']).to eq('commented on') } + it { expect(json_event['note']['body']).to eq('What an awesome day!') } + end end it 'should return a 404 error if not found' do From b2c4675cb0e681027334e5bd046451d3116924c8 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 4 Dec 2015 12:32:13 +0100 Subject: [PATCH 320/908] Recursivity needed if a fork is a fork of a fork. --- app/models/lfs_object.rb | 2 +- lib/gitlab/lfs/response.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 43b845b29c..a243c7b77c 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -8,7 +8,7 @@ class LfsObject < ActiveRecord::Base def storage_project(project) if project && project.forked? - project.forked_from_project + storage_project(project.forked_from_project) else project end diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb index 9be9a65671..9d9617761b 100644 --- a/lib/gitlab/lfs/response.rb +++ b/lib/gitlab/lfs/response.rb @@ -220,7 +220,7 @@ module Gitlab def storage_project(project) if project.forked? - project.forked_from_project + storage_project(project.forked_from_project) else project end From 496870ddec632ae13cff8b5e8f6ca0bff0fa3ec7 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Thu, 3 Dec 2015 21:24:39 -0200 Subject: [PATCH 321/908] Migrate from Sidetiq to Sidekiq-cron Updated Sidekiq to 3.5.x --- Gemfile | 4 +-- Gemfile.lock | 46 +++++++++++++++++---------- app/workers/stuck_ci_builds_worker.rb | 3 -- config/initializers/sidekiq.rb | 6 ++++ config/routes.rb | 1 + config/schedule.yml | 10 ++++++ 6 files changed, 49 insertions(+), 21 deletions(-) create mode 100644 config/schedule.yml diff --git a/Gemfile b/Gemfile index 67640bb9ae..954c5e1350 100644 --- a/Gemfile +++ b/Gemfile @@ -120,8 +120,8 @@ gem 'acts-as-taggable-on', '~> 3.4' # Background jobs gem 'sinatra', '~> 1.4.4', require: nil -gem 'sidekiq', '3.3.0' -gem 'sidetiq', '~> 0.6.3' +gem 'sidekiq', '~> 3.5.0' +gem 'sidekiq-cron', '~> 0.3.0' # HTTP requests gem "httparty", '~> 0.13.3' diff --git a/Gemfile.lock b/Gemfile.lock index cd1855758b..67807bed4c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -116,8 +116,23 @@ GEM activemodel (>= 3.2.0) activesupport (>= 3.2.0) json (>= 1.7) - celluloid (0.16.0) - timers (~> 4.0.0) + celluloid (0.17.2) + celluloid-essentials + celluloid-extras + celluloid-fsm + celluloid-pool + celluloid-supervision + timers (>= 4.1.1) + celluloid-essentials (0.20.5) + timers (>= 4.1.1) + celluloid-extras (0.20.5) + timers (>= 4.1.1) + celluloid-fsm (0.20.5) + timers (>= 4.1.1) + celluloid-pool (0.20.5) + timers (>= 4.1.1) + celluloid-supervision (0.20.5) + timers (>= 4.1.1) charlock_holmes (0.7.3) chunky_png (1.3.5) cliver (0.3.2) @@ -369,7 +384,6 @@ GEM multi_xml (>= 0.5.2) httpclient (2.7.0.1) i18n (0.7.0) - ice_cube (0.11.1) ice_nine (0.11.1) inflecto (0.0.2) ipaddress (0.8.0) @@ -640,6 +654,7 @@ GEM sexp_processor (~> 4.1) rubyntlm (0.5.2) rubypants (0.2.0) + rufus-scheduler (3.1.10) rugged (0.23.3) safe_yaml (1.0.4) sanitize (2.1.0) @@ -667,16 +682,15 @@ GEM rack shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - sidekiq (3.3.0) - celluloid (>= 0.16.0) - connection_pool (>= 2.0.0) - json - redis (>= 3.0.6) - redis-namespace (>= 1.3.1) - sidetiq (0.6.3) - celluloid (>= 0.14.1) - ice_cube (= 0.11.1) - sidekiq (>= 3.0.0) + sidekiq (3.5.3) + celluloid (~> 0.17.2) + connection_pool (~> 2.2, >= 2.2.0) + json (~> 1.0) + redis (~> 3.2, >= 3.2.1) + redis-namespace (~> 1.5, >= 1.5.2) + sidekiq-cron (0.3.1) + rufus-scheduler (>= 2.0.24) + sidekiq (>= 2.17.3) simple_oauth (0.1.9) simplecov (0.10.0) docile (~> 1.1.0) @@ -742,7 +756,7 @@ GEM thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) - timers (4.0.4) + timers (4.1.1) hitimes timfel-krb5-auth (0.8.3) tinder (1.10.1) @@ -936,8 +950,8 @@ DEPENDENCIES settingslogic (~> 2.0.9) sham_rack shoulda-matchers (~> 2.8.0) - sidekiq (= 3.3.0) - sidetiq (~> 0.6.3) + sidekiq (~> 3.5.0) + sidekiq-cron (~> 0.3.0) simplecov (~> 0.10.0) sinatra (~> 1.4.4) six (~> 0.2.0) diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb index 4e5eddbaba..ca594e77e7 100644 --- a/app/workers/stuck_ci_builds_worker.rb +++ b/app/workers/stuck_ci_builds_worker.rb @@ -1,11 +1,8 @@ class StuckCiBuildsWorker include Sidekiq::Worker - include Sidetiq::Schedulable BUILD_STUCK_TIMEOUT = 1.day - recurrence { daily } - def perform Rails.logger.info 'Cleaning stuck builds' diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index e856499732..6e5701e33d 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -17,6 +17,12 @@ Sidekiq.configure_server do |config| chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS'] chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] end + + # Sidekiq-cron: load recurring jobs from schedule.yml + schedule_file = 'config/schedule.yml' + if File.exists?(schedule_file) + Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file) + end end Sidekiq.configure_client do |config| diff --git a/config/routes.rb b/config/routes.rb index 5c114452a3..6aa822bf85 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ require 'sidekiq/web' +require 'sidekiq/cron/web' require 'api/api' Rails.application.routes.draw do diff --git a/config/schedule.yml b/config/schedule.yml new file mode 100644 index 0000000000..993a95fef5 --- /dev/null +++ b/config/schedule.yml @@ -0,0 +1,10 @@ +# Here is a list of jobs that are scheduled to run periodically. +# We use a UNIX cron notation to specify execution schedule. +# +# Please read here for more information: +# https://github.com/ondrejbartas/sidekiq-cron#adding-cron-job + +stuck_ci_builds_worker: + cron: "0 0 * * *" + class: "StuckCiBuildsWorker" + queue: "default" From 5c1b49f494f07bf37ba3c60f3b9f70d1842d8b60 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 4 Dec 2015 16:23:21 +0200 Subject: [PATCH 322/908] Add added, modified and removed properties to commit object in webhook --- CHANGELOG | 3 +++ app/models/commit.rb | 24 ++++++++++++++++- doc/web_hooks/web_hooks.md | 14 ++++++---- lib/gitlab/push_data_builder.rb | 32 ++--------------------- spec/lib/gitlab/push_data_builder_spec.rb | 9 +++---- spec/models/commit_spec.rb | 11 ++++++++ 6 files changed, 51 insertions(+), 42 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 58a0a3c894..d8d878c45c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,9 @@ v 8.3.0 (unreleased) - Add API endpoint to fetch merge request commits list - Expose events API with comment information and author info +v 8.2.3 + - Webhook payload has an added, modified and removed properties for each commit + v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) - Ensure cached application settings are refreshed at startup (Stan Hu) diff --git a/app/models/commit.rb b/app/models/commit.rb index 492f6be1ce..912b4dedf5 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -146,7 +146,10 @@ class Commit author: { name: author_name, email: author_email - } + }, + added: repo_changes[:added], + modified: repo_changes[:modified], + removed: repo_changes[:removed] } end @@ -196,4 +199,23 @@ class Commit def status ci_commit.try(:status) || :not_found end + + private + + def repo_changes + changes = { added: [], modified: [], removed: [] } + + diffs.each do |diff| + case true + when diff.deleted_file + changes[:removed] << diff.old_path + when diff.renamed_file, diff.new_file + changes[:added] << diff.new_path + else + changes[:modified] << diff.new_path + end + end + + changes + end end diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 7d838187a2..03746dd9df 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -57,6 +57,9 @@ X-Gitlab-Event: Push Hook "name": "Jordi Mallach", "email": "jordi@softcatala.org" } + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] }, { "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", @@ -66,13 +69,14 @@ X-Gitlab-Event: Push Hook "author": { "name": "GitLab dev user", "email": "gitlabdev@dv6700.(none)" - } + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] } ], - "total_commits_count": 4, - "added": ["CHANGELOG"], - "modified": ["app/controller/application.rb"], - "removed": [] + "total_commits_count": 4 + } ``` diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index fa068d5076..cdcdb02a05 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -18,10 +18,7 @@ module Gitlab # homepage: String, # }, # commits: Array, - # total_commits_count: Fixnum, - # added: ["CHANGELOG"], - # modified: [], - # removed: ["tmp/file.txt"] + # total_commits_count: Fixnum # } # def build(project, user, oldrev, newrev, ref, commits = [], message = nil) @@ -37,7 +34,6 @@ module Gitlab type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push" - repo_changes = repo_changes(project, newrev, oldrev) # Hash to be passed as post_receive_data data = { object_kind: type, @@ -60,10 +56,7 @@ module Gitlab visibility_level: project.visibility_level }, commits: commit_attrs, - total_commits_count: commits_count, - added: repo_changes[:added], - modified: repo_changes[:modified], - removed: repo_changes[:removed] + total_commits_count: commits_count } data @@ -94,27 +87,6 @@ module Gitlab newrev end end - - def repo_changes(project, newrev, oldrev) - changes = { added: [], modified: [], removed: [] } - compare_result = CompareService.new. - execute(project, newrev, project, oldrev) - - if compare_result - compare_result.diffs.each do |diff| - case true - when diff.deleted_file - changes[:removed] << diff.old_path - when diff.renamed_file, diff.new_file - changes[:added] << diff.new_path - else - changes[:modified] << diff.new_path - end - end - end - - changes - end end end end diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb index 0271074262..2170399ab5 100644 --- a/spec/lib/gitlab/push_data_builder_spec.rb +++ b/spec/lib/gitlab/push_data_builder_spec.rb @@ -17,9 +17,9 @@ describe 'Gitlab::PushDataBuilder' do it { expect(data[:repository][:git_ssh_url]).to eq(project.ssh_url_to_repo) } it { expect(data[:repository][:visibility_level]).to eq(project.visibility_level) } it { expect(data[:total_commits_count]).to eq(3) } - it { expect(data[:added]).to eq(["gitlab-grack"]) } - it { expect(data[:modified]).to eq([".gitmodules", "files/ruby/popen.rb", "files/ruby/regex.rb"]) } - it { expect(data[:removed]).to eq([]) } + it { expect(data[:commits].first[:added]).to eq(["gitlab-grack"]) } + it { expect(data[:commits].first[:modified]).to eq([".gitmodules"]) } + it { expect(data[:commits].first[:removed]).to eq([]) } end describe :build do @@ -38,8 +38,5 @@ describe 'Gitlab::PushDataBuilder' do it { expect(data[:ref]).to eq('refs/tags/v1.1.0') } it { expect(data[:commits]).to be_empty } it { expect(data[:total_commits_count]).to be_zero } - it { expect(data[:added]).to eq([]) } - it { expect(data[:modified]).to eq([]) } - it { expect(data[:removed]).to eq([]) } end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 90be932495..b417bc98fa 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -100,4 +100,15 @@ eos # Include the subject in the repository stub. let(:extra_commits) { [subject] } end + + describe '#hook_attrs' do + let(:data) { commit.hook_attrs } + + it { expect(data).to be_a(Hash) } + it { expect(data[:message]).to include('Add submodule from gitlab.com') } + it { expect(data[:timestamp]).to eq('2014-02-27T11:01:38+02:00') } + it { expect(data[:added]).to eq(["gitlab-grack"]) } + it { expect(data[:modified]).to eq([".gitmodules"]) } + it { expect(data[:removed]).to eq([]) } + end end From 32b45493b89b35b7b7d3f086e8996d1ed7b8d0f3 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 3 Dec 2015 00:09:10 -0800 Subject: [PATCH 323/908] Fix application settings cache not expiring after changes cache_key is an instance method that relies on updated_at. When changes were made, the time-dependent key was being used instead of X.application_setting.last. Closes #3609 --- CHANGELOG | 1 + app/models/application_setting.rb | 12 +++++------- app/models/ci/application_setting.rb | 11 ++++------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 58a0a3c894..9ea091c3ce 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) + - Fix application settings cache not expiring after changes (Stan Hu) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - Fix 500 error when update group member permission - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 3df8135acf..5ddcf3d9a0 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -30,6 +30,8 @@ # class ApplicationSetting < ActiveRecord::Base + CACHE_KEY = 'application_setting.last' + serialize :restricted_visibility_levels serialize :import_sources serialize :restricted_signup_domains, Array @@ -73,21 +75,17 @@ class ApplicationSetting < ActiveRecord::Base end after_commit do - Rails.cache.write(cache_key, self) + Rails.cache.write(CACHE_KEY, self) end def self.current - Rails.cache.fetch(cache_key) do + Rails.cache.fetch(CACHE_KEY) do ApplicationSetting.last end end def self.expire - Rails.cache.delete(cache_key) - end - - def self.cache_key - 'application_setting.last' + Rails.cache.delete(CACHE_KEY) end def self.create_from_defaults diff --git a/app/models/ci/application_setting.rb b/app/models/ci/application_setting.rb index 4e512d290e..7f5df8ce6c 100644 --- a/app/models/ci/application_setting.rb +++ b/app/models/ci/application_setting.rb @@ -12,17 +12,18 @@ module Ci class ApplicationSetting < ActiveRecord::Base extend Ci::Model + CACHE_KEY = 'ci_application_setting.last' after_commit do - Rails.cache.write(cache_key, self) + Rails.cache.write(CACHE_KEY, self) end def self.expire - Rails.cache.delete(cache_key) + Rails.cache.delete(CACHE_KEY) end def self.current - Rails.cache.fetch(cache_key) do + Rails.cache.fetch(CACHE_KEY) do Ci::ApplicationSetting.last end end @@ -33,9 +34,5 @@ module Ci add_pusher: Settings.gitlab_ci['add_pusher'], ) end - - def self.cache_key - 'ci_application_setting.last' - end end end From f1fd4880d9bbb7c34e910b357bc52874d2e6188e Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 2 Dec 2015 20:11:58 +0000 Subject: [PATCH 324/908] Check GitLab Workhorse status in init.d script when reporting all components are up and running Closes https://github.com/gitlabhq/gitlabhq/issues/9869 --- lib/support/init.d/gitlab | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index f0a6c2b30e..43fda6fa92 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -327,7 +327,7 @@ print_status() { printf "The GitLab MailRoom email processor is \033[31mnot running\033[0m.\n" fi fi - if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; }; then + if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; }; then printf "GitLab and all its components are \033[32mup and running\033[0m.\n" fi } From a120b78940b6c7150f405091d620b34c0fccbd28 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 1 Dec 2015 16:15:01 -0800 Subject: [PATCH 325/908] Handle and report SSL errors in Web hook test. Check for status 200 for success. If a Web hook test fails due to an SSL error or some other error, report the result back to the user instead of an Error 500. Closes #3656 Handle response --- CHANGELOG | 1 + app/controllers/projects/hooks_controller.rb | 5 ++- app/models/hooks/web_hook.rb | 36 +++++++++++--------- spec/models/hooks/web_hook_spec.rb | 6 ++++ 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 58a0a3c894..faee22405d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) + - Handle and report SSL errors in Web hook test (Stan Hu) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - Fix 500 error when update group member permission - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index c756954189..6a62880cb7 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -25,13 +25,12 @@ class Projects::HooksController < Projects::ApplicationController def test if !@project.empty_repo? - status = TestHookService.new.execute(hook, current_user) + status, message = TestHookService.new.execute(hook, current_user) if status flash[:notice] = 'Hook successfully executed.' else - flash[:alert] = 'Hook execution failed. '\ - 'Ensure hook URL is correct and service is up.' + flash[:alert] = "Hook execution failed: #{message}" end else flash[:alert] = 'Hook execution failed. Ensure the project has commits.' diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index d6c6f415c4..2caf26cc8c 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -37,31 +37,33 @@ class WebHook < ActiveRecord::Base def execute(data, hook_name) parsed_url = URI.parse(url) if parsed_url.userinfo.blank? - WebHook.post(url, - body: data.to_json, - headers: { - "Content-Type" => "application/json", - "X-Gitlab-Event" => hook_name.singularize.titleize - }, - verify: enable_ssl_verification) + response = WebHook.post(url, + body: data.to_json, + headers: { + "Content-Type" => "application/json", + "X-Gitlab-Event" => hook_name.singularize.titleize + }, + verify: enable_ssl_verification) else post_url = url.gsub("#{parsed_url.userinfo}@", "") auth = { username: URI.decode(parsed_url.user), password: URI.decode(parsed_url.password), } - WebHook.post(post_url, - body: data.to_json, - headers: { - "Content-Type" => "application/json", - "X-Gitlab-Event" => hook_name.singularize.titleize - }, - verify: enable_ssl_verification, - basic_auth: auth) + response = WebHook.post(post_url, + body: data.to_json, + headers: { + "Content-Type" => "application/json", + "X-Gitlab-Event" => hook_name.singularize.titleize + }, + verify: enable_ssl_verification, + basic_auth: auth) end - rescue SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e + + [response.code == 200, ActionView::Base.full_sanitizer.sanitize(response.to_s)] + rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e logger.error("WebHook Error => #{e}") - false + [false, e.to_s] end def async_execute(data, hook_name) diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 2fdc49f02e..35042788c6 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -71,5 +71,11 @@ describe ProjectHook do expect { @project_hook.execute(@data, 'push_hooks') }.to raise_error(RuntimeError) end + + it "handles SSL exceptions" do + expect(WebHook).to receive(:post).and_raise(OpenSSL::SSL::SSLError.new('SSL error')) + + expect(@project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error']) + end end end From ba3c702073f2f57eebbeabb2926fbd367aad87ea Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 1 Dec 2015 16:45:40 -0800 Subject: [PATCH 326/908] Fix spec --- features/steps/project/hooks.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb index df4a23a371..be4db77094 100644 --- a/features/steps/project/hooks.rb +++ b/features/steps/project/hooks.rb @@ -70,8 +70,6 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps step 'I should see hook service down error message' do expect(page).to have_selector '.flash-alert', - text: 'Hook execution failed. '\ - 'Ensure hook URL is correct and '\ - 'service is up.' + text: 'Hook execution failed: Exception from' end end From 253301bb47d14494ea45230aeb682a9b7dffd5bb Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 25 Nov 2015 16:38:23 -0800 Subject: [PATCH 327/908] Make current user the first user in assignee dropdown in issues detail page Closes #3679 --- CHANGELOG | 1 + app/views/shared/issuable/_context.html.haml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 58a0a3c894..93d5996687 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ v 8.2.2 - Fix: Raw private snippets access workflow - Prevent "413 Request entity too large" errors when pushing large files with LFS - Fix invalid links within projects dashboard header + - Make current user the first user in assignee dropdown in issues detail page (Stan Hu) v 8.2.1 - Forcefully update builds that didn't want to update with state machine diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml index f44b439a84..2aa4666261 100644 --- a/app/views/shared/issuable/_context.html.haml +++ b/app/views/shared/issuable/_context.html.haml @@ -9,7 +9,7 @@ none .issuable-context-selectbox - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true) + = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true) %div.prepend-top-default.clearfix .issuable-context-title From aa1ba0093632a66c9c9c0eac710d63d7513ad358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 24 Nov 2015 22:41:36 -0500 Subject: [PATCH 328/908] Ensure "Remove Source Branch" button is not shown when branch is being deleted. #3583 --- CHANGELOG | 1 + .../merge_request_widget.js.coffee | 9 +++-- app/helpers/gitlab_routing_helper.rb | 2 +- .../projects/merge_requests/merge.js.haml | 2 +- .../merge_requests/widget/_merged.html.haml | 2 +- .../project/merge_requests/accept.feature | 17 +++++++++ .../project/merge_requests/acceptance.rb | 35 +++++++++++++++++++ 7 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 features/project/merge_requests/accept.feature create mode 100644 features/steps/project/merge_requests/acceptance.rb diff --git a/CHANGELOG b/CHANGELOG index 58a0a3c894..f21129c6d6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ v 8.3.0 (unreleased) - Don't show project fork event as "imported" - Add API endpoint to fetch merge request commits list - Expose events API with comment information and author info + - Fix: Ensure "Remove Source Branch" button is not shown when branch is being deleted. #3583 v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 3176e5a896..c4b63966fe 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -10,17 +10,20 @@ class @MergeRequestWidget constructor: (@opts) -> modal = $('#modal_merge_info').modal(show: false) - mergeInProgress: -> + mergeInProgress: (deleteSourceBranch = false)-> $.ajax type: 'GET' url: $('.merge-request').data('url') success: (data) => if data.state == "merged" - location.reload() + urlSuffix = if deleteSourceBranch then '?delete_source=true' else '' + + window.location.href = window.location.href + urlSuffix else if data.merge_error $('.mr-widget-body').html("

" + data.merge_error + "

") else - setTimeout(merge_request_widget.mergeInProgress, 2000) + callback = -> merge_request_widget.mergeInProgress(deleteSourceBranch) + setTimeout(callback, 2000) dataType: 'json' getMergeStatus: -> diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index b0b536d464..f3fddef01c 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -6,7 +6,7 @@ # # For example instead of this: # -# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.projects, merge_request) +# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) # # We can simply use shortcut: # diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml index 33321651e3..518ecb9f00 100644 --- a/app/views/projects/merge_requests/merge.js.haml +++ b/app/views/projects/merge_requests/merge.js.haml @@ -1,6 +1,6 @@ - if @status :plain - merge_request_widget.mergeInProgress(); + merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'}); - else :plain $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}"); diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index ac08e0b498..5c6fece8c5 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -7,7 +7,7 @@ by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)} #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} %div - - if !@merge_request.source_branch_exists? + - if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true') = succeed '.' do The changes were merged into = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do diff --git a/features/project/merge_requests/accept.feature b/features/project/merge_requests/accept.feature new file mode 100644 index 0000000000..3e6e59a380 --- /dev/null +++ b/features/project/merge_requests/accept.feature @@ -0,0 +1,17 @@ +Feature: Project Merge Requests Acceptance + Background: + Given There is an open Merge Request + And I am signed in as a developer of the project + + @javascript + Scenario: Accepting the Merge Request and removing the source branch + Given I am on the Merge Request detail page + When I click on "Remove source branch" option + And I click on Accept Merge Request + Then I should not see the Remove Source Branch button + + @javascript + Scenario: Accepting the Merge Request without removing the source branch + Given I am on the Merge Request detail page + When I click on Accept Merge Request + Then I should see the Remove Source Branch button diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb new file mode 100644 index 0000000000..6adecaa838 --- /dev/null +++ b/features/steps/project/merge_requests/acceptance.rb @@ -0,0 +1,35 @@ +class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps + include LoginHelpers + include GitlabRoutingHelper + + step 'I am on the Merge Request detail page' do + visit merge_request_path(@merge_request) + end + + step 'I click on "Remove source branch" option' do + check('Remove source branch') + end + + step 'I click on Accept Merge Request' do + click_button('Accept Merge Request') + end + + step 'I should see the Remove Source Branch button' do + expect(page).to have_link('Remove Source Branch') + end + + step 'I should not see the Remove Source Branch button' do + expect(page).not_to have_link('Remove Source Branch') + end + + step 'There is an open Merge Request' do + @user = create(:user) + @project = create(:project, :public) + @project_member = create(:project_member, user: @user, project: @project, access_level: ProjectMember::DEVELOPER) + @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project) + end + + step 'I am signed in as a developer of the project' do + login_as(@user) + end +end From 12fdc13ad386299b04431c43c7d4ab307698b1a8 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 4 Dec 2015 17:31:01 -0200 Subject: [PATCH 329/908] Fix 500 error when creating a merge request that removes a submodule --- CHANGELOG | 1 + app/views/projects/diffs/_file.html.haml | 3 +-- spec/controllers/commit_controller_spec.rb | 20 ++++++++++++++++ .../merge_requests_controller_spec.rb | 24 +++++++++++++++++++ spec/support/test_env.rb | 3 ++- 5 files changed, 48 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6b61cbc11e..9b26f526fd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ v 8.3.0 (unreleased) - Add API endpoint to fetch merge request commits list - Expose events API with comment information and author info - Fix: Ensure "Remove Source Branch" button is not shown when branch is being deleted. #3583 + - Fix 500 error when creating a merge request that removes a submodule v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index b3392d00e0..b77e9f9f40 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -3,9 +3,8 @@ - if diff_file.diff.submodule? %span = icon('archive fw') - - submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path) %strong - = submodule_link(submodule_item, @commit.id, project.repository) + = submodule_link(blob, @commit.id, project.repository) - else %span = blob_icon blob.mode, blob.name diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb index 5337a69e84..7793bf1e42 100644 --- a/spec/controllers/commit_controller_spec.rb +++ b/spec/controllers/commit_controller_spec.rb @@ -110,6 +110,26 @@ describe Projects::CommitController do expect(response.body).to match(/^diff --git/) end end + + context 'commit that removes a submodule' do + render_views + + let(:fork_project) { create(:forked_project_with_submodules) } + let(:commit) { fork_project.commit('remove-submodule') } + + before do + fork_project.team << [user, :master] + end + + it 'renders it' do + get(:show, + namespace_id: fork_project.namespace.to_param, + project_id: fork_project.to_param, + id: commit.id) + + expect(response).to be_success + end + end end describe "#branches" do diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 3e5e1fa87a..6aaec224f6 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -10,6 +10,30 @@ describe Projects::MergeRequestsController do project.team << [user, :master] end + describe '#new' do + context 'merge request that removes a submodule' do + render_views + + let(:fork_project) { create(:forked_project_with_submodules) } + + before do + fork_project.team << [user, :master] + end + + it 'renders it' do + get :new, + namespace_id: fork_project.namespace.to_param, + project_id: fork_project.to_param, + merge_request: { + source_branch: 'remove-submodule', + target_branch: 'master' + } + + expect(response).to be_success + end + end + end + describe "#show" do shared_examples "export merge as" do |format| it "should generally work" do diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 787670e929..78b9a0f42f 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -21,7 +21,8 @@ module TestEnv # We currently only need a subset of the branches FORKED_BRANCH_SHA = { 'add-submodule-version-bump' => '3f547c08', - 'master' => '5937ac0' + 'master' => '5937ac0', + 'remove-submodule' => '2a33e0c0' } # Test environment From 3c8051776b25add2e2845344a328328db33d1671 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 4 Dec 2015 14:35:29 -0500 Subject: [PATCH 330/908] Update CHANGELOG [ci skip] --- CHANGELOG | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6b61cbc11e..ad15ed43b7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,6 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) - - Fix application settings cache not expiring after changes (Stan Hu) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - Fix 500 error when update group member permission - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) @@ -12,6 +11,9 @@ v 8.3.0 (unreleased) - Expose events API with comment information and author info - Fix: Ensure "Remove Source Branch" button is not shown when branch is being deleted. #3583 +v 8.2.3 + - Fix application settings cache not expiring after changes (Stan Hu) + v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) - Ensure cached application settings are refreshed at startup (Stan Hu) From d800a949d2d5497e8aff3ae28ec8520e5b99cdb8 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 3 Dec 2015 23:33:52 -0800 Subject: [PATCH 331/908] Fix Error 500 when creating global milestones with Unicode characters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues: 1. The constraints in the resources were incorrect. Here's what it was before: ``` group_milestone GET /groups/:group_id/milestones/:id(.:format) groups/milestones#show {:id=>/[a-zA-Z.0-9_\-]+(?/[a-zA-Z.0-9_\-]+(?/[^\/]+/, :group_id=>/[a-zA-Z.0-9_\-]+(?"show", :controller=>"groups/milestones", :group_id=>#, :id=>"", :title=>"肯定不是中文的问题"} missing required keys: [:id]): This change uses the babosa library to create a better slug, which surprisingly isn't actually used by the global milestone controllers. Instead, they use the title passed as a query string for some reason. Closes https://github.com/gitlabhq/gitlabhq/issues/9881 Fix constraints --- CHANGELOG | 1 + Gemfile | 1 + Gemfile.lock | 2 ++ .../groups/milestones_controller.rb | 2 +- app/models/global_milestone.rb | 2 +- config/routes.rb | 2 +- .../groups/milestones_controller_spec.rb | 27 +++++++++++++++++++ 7 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 spec/controllers/groups/milestones_controller_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 7b2f152865..228848c13f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ v 8.3.0 (unreleased) v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) + - Fix Error 500s when creating global milestones with Unicode characters (Stan Hu) v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) diff --git a/Gemfile b/Gemfile index 67640bb9ae..860e6bee47 100644 --- a/Gemfile +++ b/Gemfile @@ -171,6 +171,7 @@ gem "underscore-rails", "~> 1.4.4" # Sanitize user input gem "sanitize", '~> 2.0' +gem 'babosa', '~> 1.0.2' # Protect against bruteforcing gem "rack-attack", '~> 4.3.0' diff --git a/Gemfile.lock b/Gemfile.lock index cd1855758b..be3e39d8e8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,6 +73,7 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) + babosa (1.0.2) bcrypt (3.1.10) benchmark-ips (2.3.0) better_errors (1.0.1) @@ -823,6 +824,7 @@ DEPENDENCIES asciidoctor (~> 1.5.2) attr_encrypted (~> 1.3.4) awesome_print (~> 1.2.0) + babosa (~> 1.0.2) benchmark-ips better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 10233222ee..0c2a350bc3 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -46,7 +46,7 @@ class Groups::MilestonesController < Groups::ApplicationController end def milestone_path(title) - group_milestone_path(@group, title.parameterize, title: title) + group_milestone_path(@group, title.to_slug.to_s, title: title) end def projects diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 1321ccd963..85aa71662f 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -16,7 +16,7 @@ class GlobalMilestone end def safe_title - @title.parameterize + @title.to_slug.to_s end def projects diff --git a/config/routes.rb b/config/routes.rb index 5c114452a3..fdd387fd18 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -368,7 +368,7 @@ Rails.application.routes.draw do end resource :avatar, only: [:destroy] - resources :milestones, only: [:index, :show, :update, :new, :create] + resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] end end diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb new file mode 100644 index 0000000000..eb0c6ac6d8 --- /dev/null +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Groups::MilestonesController do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + let(:project2) { create(:empty_project, group: group) } + let(:user) { create(:user) } + let(:title) { '肯定不是中文的问题' } + + before do + sign_in(user) + group.add_owner(user) + project.team << [user, :master] + controller.instance_variable_set(:@group, group) + end + + describe "#create" do + it "should create group milestone with Chinese title" do + post :create, + group_id: group.id, + milestone: { project_ids: [project.id, project2.id], title: title } + + expect(response).to redirect_to(group_milestone_path(group, title.to_slug.to_s, title: title)) + expect(Milestone.where(title: title).count).to eq(2) + end + end +end From 4fab178850f512ad15715b6f723ace6fce7882fc Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 4 Dec 2015 22:56:10 -0800 Subject: [PATCH 332/908] Fix spec that broke due to fact that iid is needed, not id, for MilestonesController --- spec/controllers/projects/milestones_controller_spec.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index 8127efabe6..d173bb350f 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -5,7 +5,7 @@ describe Projects::MilestonesController do let(:user) { create(:user) } let(:milestone) { create(:milestone, project: project) } let(:issue) { create(:issue, project: project, milestone: milestone) } - let(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) } before do sign_in(user) @@ -15,10 +15,9 @@ describe Projects::MilestonesController do describe "#destroy" do it "should remove milestone" do - merge_request.reload expect(issue.milestone_id).to eq(milestone.id) - delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.id, format: :js + delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid, format: :js expect(response).to be_success expect(Event.first.action).to eq(Event::DESTROYED) From 2462a96e459c95f987f39e3c380de7c7cc350cfd Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Thu, 3 Dec 2015 10:27:34 +0100 Subject: [PATCH 333/908] Incorporate feedback --- app/models/merge_request.rb | 46 ++++++++++--------- .../merge_when_build_succeeds_service.rb | 9 ++-- app/services/system_note_service.rb | 6 +-- .../merge_requests/_merge_request.html.haml | 5 +- .../merge_requests/widget/_heading.html.haml | 9 ++-- .../widget/open/_accept.html.haml | 3 +- .../open/_merge_when_build_succeeds.html.haml | 24 +++++----- spec/factories/merge_requests.rb | 5 ++ .../merge_when_build_succeeds_spec.rb | 21 +++++++-- spec/models/merge_request_spec.rb | 44 ++++++++++++++---- spec/requests/api/merge_requests_spec.rb | 2 +- .../merge_when_build_succeeds_service_spec.rb | 30 +++++++----- .../merge_requests/refresh_service_spec.rb | 3 +- spec/services/system_note_service_spec.rb | 10 ++-- 14 files changed, 134 insertions(+), 83 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 131dfda6b5..1f81e23c7a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -2,25 +2,28 @@ # # Table name: merge_requests # -# id :integer not null, primary key -# target_branch :string(255) not null -# source_branch :string(255) not null -# source_project_id :integer not null -# author_id :integer -# assignee_id :integer -# title :string(255) -# created_at :datetime -# updated_at :datetime -# milestone_id :integer -# state :string(255) -# merge_status :string(255) -# target_project_id :integer not null -# iid :integer -# description :text -# position :integer default(0) -# locked_at :datetime -# updated_by_id :integer -# merge_error :string(255) +# id :integer not null, primary key +# target_branch :string(255) not null +# source_branch :string(255) not null +# source_project_id :integer not null +# author_id :integer +# assignee_id :integer +# title :string(255) +# created_at :datetime +# updated_at :datetime +# milestone_id :integer +# state :string(255) +# merge_status :string(255) +# target_project_id :integer not null +# iid :integer +# description :text +# position :integer default(0) +# locked_at :datetime +# updated_by_id :integer +# merge_error :string(255) +# merge_params :text (serialized to hash) +# merge_when_build_succeeds :boolean default(false), not null +# merge_user_id :integer # require Rails.root.join("app/models/commit") @@ -124,6 +127,7 @@ class MergeRequest < ActiveRecord::Base validates :source_branch, presence: true validates :target_project, presence: true validates :target_branch, presence: true + validates :merge_user, presence: true, if: :merge_when_build_succeeds? validate :validate_branches validate :validate_fork @@ -496,8 +500,6 @@ class MergeRequest < ActiveRecord::Base end def ci_commit - if last_commit - source_project.ci_commit(last_commit.id) - end + @ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit end end diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb index 2f101e53a3..5cf7404a49 100644 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -1,6 +1,6 @@ module MergeRequests class MergeWhenBuildSucceedsService < MergeRequests::BaseService - # Marks the passed `merge_request` to be marked when the build succeeds or + # Marks the passed `merge_request` to be merged when the build succeeds or # updates the params for the automatic merge def execute(merge_request) merge_request.merge_params.merge!(params) @@ -12,7 +12,7 @@ module MergeRequests merge_request.merge_when_build_succeeds = true merge_request.merge_user = @current_user - SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.ci_commit) + SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.last_commit) end merge_request.save @@ -25,8 +25,7 @@ module MergeRequests merge_requests.each do |merge_request| next unless merge_request.merge_when_build_succeeds? - ci_commit = merge_request.ci_commit - if ci_commit && ci_commit.success? && merge_request.mergeable? + if merge_request.ci_commit && merge_request.ci_commit.success? && merge_request.mergeable? MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) end end @@ -34,7 +33,7 @@ module MergeRequests # Cancels the automatic merge def cancel(merge_request) - if merge_request.merge_when_build_succeeds? && merge_request.open? && !merge_request.merged? + if merge_request.merge_when_build_succeeds? && merge_request.open? merge_request.reset_merge_when_build_succeeds SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index ed557fef81..f84e480ca9 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -131,15 +131,15 @@ class SystemNoteService end # Called when 'merge when build succeeds' is executed - def self.merge_when_build_succeeds(noteable, project, author, ci_commit) - body = "Enabled an automatic merge when the build for #{ci_commit.sha} succeeds" + def self.merge_when_build_succeeds(noteable, project, author, last_commit) + body = "Enabled an automatic merge when the build for #{last_commit.to_reference} succeeds" create_note(noteable: noteable, project: project, author: author, note: body) end # Called when 'merge when build succeeds' is canceled def self.cancel_merge_when_build_succeeds(noteable, project, author) - body = "Canceled the automatic merge" + body = "Cancelled the automatic merge" create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index c5234c0618..d10ccb3057 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,4 +1,3 @@ -- ci_commit = merge_request.ci_commit %li{ class: mr_css_classes(merge_request) } .merge-request-title %span.merge-request-title-text @@ -7,8 +6,8 @@ - merge_request.labels.each do |label| = link_to_label(label, project: merge_request.project) .pull-right.light - - if ci_commit - = render_ci_status(ci_commit) + - if merge_request.ci_commit + = render_ci_status(merge_request.ci_commit) - if merge_request.merged? %span %i.fa.fa-check diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index ba5ad22bca..9513a18f10 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,13 +1,12 @@ -- ci_commit = @merge_request.ci_commit -- if ci_commit - - status = ci_commit.status +- if @merge_request.ci_commit + - status = @merge_request.ci_commit.status .mr-widget-heading .ci_widget{class: "ci-#{status}"} - = ci_status_icon(ci_commit) + = ci_status_icon(@merge_request.ci_commit) %span CI build #{status} for #{@merge_request.last_commit_short_sha}. %span.ci-coverage - = link_to "View build details", ci_status_path(ci_commit) + = link_to "View build details", ci_status_path(@merge_request.ci_commit) - elsif @merge_request.has_ci? - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 279e2ec91f..f7d872aa45 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -4,8 +4,7 @@ = hidden_field_tag :authenticity_token, form_authenticity_token .accept-merge-holder.clearfix.js-toggle-container .accept-action - - ci_commit = @merge_request.ci_commit - - if ci_commit && ci_commit.active? + - if @merge_request.ci_commit && @merge_request.ci_commit.active? = f.button class: "btn btn-create btn-grouped merge_when_build_succeeds", name: "merge_when_build_succeeds" do Merge When Build Succeeds = f.button class: "btn btn-create btn-grouped accept_merge_request #{status_class}" do diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index 43ba49c5a5..9788d68b54 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -1,27 +1,27 @@ %h4 - Approved by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} + Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} to be merged automatically when #{link_to "the build", ci_status_path(@merge_request.ci_commit)} succeeds. %div - - source_branch_removed = @merge_request.merge_params["should_remove_source_branch"].present? - - if source_branch_removed + - should_remove_source_branch = @merge_request.merge_params["should_remove_source_branch"].present? + %p = succeed '.' do The changes will be merged into %span.label-branch= @merge_request.target_branch - The source branch will be removed. - - else - %p - = succeed '.' do - The changes will be merged into - %span.label-branch= @merge_request.target_branch + - if should_remove_source_branch + The source branch will be removed. + - else The source branch will not be removed. - - if (@merge_request.can_remove_source_branch?(current_user) && !source_branch_removed) || @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + - remove_source_branch_button = @merge_request.can_remove_source_branch?(current_user) && !should_remove_source_branch + - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + - if remove_source_branch_button || user_can_cancel_automatic_merge .clearfix.prepend-top-10 - - if @merge_request.can_remove_source_branch?(current_user) && !source_branch_removed + - if remove_source_branch_button = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do = icon('times') Remove Source Branch When Merged - - if @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + + - if user_can_cancel_automatic_merge = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-warning btn-sm" do Cancel Automatic Merge diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 729a49c9f7..5b4d7f41bc 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -65,6 +65,11 @@ FactoryGirl.define do target_branch "master" end + trait :merge_when_build_succeeds do + merge_when_build_succeeds true + merge_user author + end + factory :closed_merge_request, traits: [:closed] factory :reopened_merge_request, traits: [:reopened] factory :merge_request_with_diffs, traits: [:with_diffs] diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index b25a3f05e2..2e64e903d1 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -1,4 +1,6 @@ require 'spec_helper' +# rubocop:disable Lint/UselessAssignment +# As rubocop doesn't see a need for both `ci_commit` and `ci_build` feature 'Merge When Build Succeeds', feature: true, js: true do let(:user) { create(:user) } @@ -32,16 +34,20 @@ feature 'Merge When Build Succeeds', feature: true, js: true do it 'activates Merge When Build Succeeds feature' do expect(page).to have_link "Cancel Automatic Merge" - expect(page).to have_content "Approved by #{user.name} to be merged automatically when the build succeeds." + expect(page).to have_content "Set by #{user.name} to be merged automatically when the build succeeds." expect(page).to have_content "The source branch will not be removed." + + visit_merge_request(merge_request) # Needed to refresh the page + expect(page).to have_content /Enabled an automatic merge when the build for [0-9a-f]{8} succeeds/i end end end context 'When it is enabled' do - # No clue how, but push a new commit to the branch - let(:merge_request) { create(:merge_request_with_diffs, source_project: project, # source_branch: "mepmep", - author: user, title: "Bug NS-04", merge_when_build_succeeds: true) } + let(:merge_request) do + create(:merge_request_with_diffs, source_project: project, author: user, + merge_user: user, title: "MepMep", merge_when_build_succeeds: true) + end before do merge_request.source_project.team << [user, :master] @@ -60,10 +66,16 @@ feature 'Merge When Build Succeeds', feature: true, js: true do click_link "Cancel Automatic Merge" expect(page).to have_button "Merge When Build Succeeds" + + visit_merge_request(merge_request) # Needed to refresh the page + expect(page).to have_content "Cancelled the automatic merge" end it "allows the user to remove the source branch" do expect(page).to have_link "Remove Source Branch When Merged" + + click_link "Remove Source Branch When Merged" + expect(page).to have_content "The source branch will be removed" end end @@ -78,3 +90,4 @@ feature 'Merge When Build Succeeds', feature: true, js: true do visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) end end +# rubocop:enable Lint/UselessAssignment diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index c7a9765825..63e1cd1fb9 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -48,6 +48,24 @@ describe MergeRequest do describe 'validation' do it { is_expected.to validate_presence_of(:target_branch) } it { is_expected.to validate_presence_of(:source_branch) } + + context "Validation of merge user with Merge When Build succeeds" do + it "allows user to be nil when the feature is disabled" do + expect(subject).to be_valid + end + + it "is invalid without merge user" do + subject.merge_when_build_succeeds = true + expect(subject).not_to be_valid + end + + it "is valid with merge user" do + subject.merge_when_build_succeeds = true + subject.merge_user = build(:user) + + expect(subject).to be_valid + end + end end describe 'respond to' do @@ -175,31 +193,41 @@ describe MergeRequest do end describe '#can_remove_source_branch' do - let(:user) { build(:user)} + let(:user) { create(:user) } + let(:user2) { create(:user) } before do subject.source_project.team << [user, :master] + + subject.source_branch = "feature" + subject.target_branch = "master" + subject.save! end - it "cant be merged when its a a protected branch" do - subject.source_project.protected_branches = []; - + it "can't be removed when its a protected branch" do + allow(subject.source_project).to receive(:protected_branch?).and_return(true) expect(subject.can_remove_source_branch?(user)).to be_falsey end it "cant remove a root ref" do - subject.source_branch = "master"; + subject.source_branch = "master" + subject.target_branch = "feature" expect(subject.can_remove_source_branch?(user)).to be_falsey end - it "is truthy in all other cases" do - expect(subject.can_remove_source_branch?(user)) + it "is unable to remove the source branch for a project the user cannot push to" do + expect(subject.can_remove_source_branch?(user2)).to be_falsey + end + + it "is can be removed in all other cases" do + expect(subject.can_remove_source_branch?(user)).to be_truthy end end describe "#reset_merge_when_build_succeeds" do - let(:merge_if_green) { create :merge_request, merge_when_build_succeeds: true } + let(:merge_if_green) { create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user) } + it "sets the item to false" do merge_if_green.reset_merge_when_build_succeeds merge_if_green.reload diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 91ae2059e9..6b7a066ac4 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -303,7 +303,7 @@ describe API::API, api: true do end describe "PUT /projects/:id/merge_request/:merge_request_id/merge" do - let (:ci_commit) { create(:ci_commit_without_jobs) } + let(:ci_commit) { create(:ci_commit_without_jobs) } it "should return merge_request in case of success" do put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index 8638539173..a62d88cde8 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -3,37 +3,38 @@ require 'spec_helper' describe MergeRequests::MergeWhenBuildSucceedsService do let(:user) { create(:user) } let(:merge_request) { create(:merge_request) } - let(:mr_merge_if_green_enabled) { create(:merge_request, merge_when_build_succeeds: true, - source_branch: "source_branch", target_branch: project.default_branch, - source_project: project, target_project: project, state: "opened") } - let(:ci_commit) { create(:ci_commit_with_one_job, ref: mr_merge_if_green_enabled.source_branch) } + + let(:mr_merge_if_green_enabled) do + create(:merge_request, merge_when_build_succeeds: true, merge_user: user, + source_branch: "source_branch", target_branch: project.default_branch, + source_project: project, target_project: project, state: "opened") + end + let(:project) { create(:project) } + let(:ci_commit) { create(:ci_commit_with_one_job, ref: mr_merge_if_green_enabled.source_branch, gl_project: project) } let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, commit_message: 'Awesome message') } - before do - project.ci_commits = [ci_commit] - project.save! - end describe "#execute" do context 'first time enabling' do before do allow(merge_request).to receive(:ci_commit).and_return(ci_commit) + service.execute(merge_request) end it 'sets the params, merge_user, and flag' do - service.execute(merge_request) - expect(merge_request).to be_valid expect(merge_request.merge_when_build_succeeds).to be_truthy expect(merge_request.merge_params).to eq commit_message: 'Awesome message' expect(merge_request.merge_user).to be user + end + it 'creates a system note' do note = merge_request.notes.last - expect(note.note).to include 'Enabled an automatic merge when the build for' + expect(note.note).to match /Enabled an automatic merge when the build for (\w+\/\w+@)?[0-9a-z]{8}/ end end - context 'allready approved' do + context 'already approved' do let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, new_key: true) } let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) } @@ -74,5 +75,10 @@ describe MergeRequests::MergeWhenBuildSucceedsService do expect(mr_merge_if_green_enabled.merge_params).to eq({}) expect(mr_merge_if_green_enabled.merge_user).to be nil end + + it 'Posts a system note' do + note = mr_merge_if_green_enabled.notes.last + expect(note.note).to include 'Cancelled the automatic merge' + end end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 18b2659c1f..9a8174f95f 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -18,7 +18,8 @@ describe MergeRequests::RefreshService do source_branch: 'master', target_branch: 'feature', target_project: @project, - merge_when_build_succeeds: true) + merge_when_build_succeeds: true, + merge_user: @user) @fork_merge_request = create(:merge_request, source_project: @fork_project, diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 5d41a5cdc6..333035f2d2 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -208,20 +208,20 @@ describe SystemNoteService do end describe '.merge_when_build_succeeds' do - let(:ci_commit) { create :ci_commit_without_jobs } + let(:ci_commit) { build :ci_commit_without_jobs } let(:noteable) { create :merge_request } - subject { described_class.merge_when_build_succeeds(noteable, project, author, ci_commit) } + subject { described_class.merge_when_build_succeeds(noteable, project, author, noteable.last_commit) } it_behaves_like 'a system note' it "posts the Merge When Build Succeeds system note" do - expect(subject.note).to eq "Enabled an automatic merge when the build for 97de212e80737a608d939f648d959671fb0a0142 succeeds" + expect(subject.note).to match /Enabled an automatic merge when the build for (\w+\/\w+@)?[0-9a-f]{40} succeeds/ end end describe '.cancel_merge_when_build_succeeds' do - let(:ci_commit) { create :ci_commit_without_jobs } + let(:ci_commit) { build :ci_commit_without_jobs } let(:noteable) { create :merge_request } subject { described_class.cancel_merge_when_build_succeeds(noteable, project, author) } @@ -229,7 +229,7 @@ describe SystemNoteService do it_behaves_like 'a system note' it "posts the Merge When Build Succeeds system note" do - expect(subject.note).to eq "Canceled the automatic merge" + expect(subject.note).to eq "Cancelled the automatic merge" end end From 1c53dc28b505f2853750ed4ea8b954385c5bf598 Mon Sep 17 00:00:00 2001 From: Andrew Tomaka Date: Wed, 2 Dec 2015 19:02:15 -0500 Subject: [PATCH 334/908] Notify user if they cannot create projects --- app/assets/javascripts/user.js.coffee | 6 ++++++ app/assets/stylesheets/pages/projects.scss | 2 +- app/controllers/profiles_controller.rb | 1 + app/views/dashboard/_projects_head.html.haml | 3 +++ app/views/shared/_project_limit.html.haml | 8 ++++++++ .../20151203162133_add_hide_project_limit_to_users.rb | 5 +++++ db/schema.rb | 3 ++- 7 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 app/views/shared/_project_limit.html.haml create mode 100644 db/migrate/20151203162133_add_hide_project_limit_to_users.rb diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee index d0d81f9692..ec4271b092 100644 --- a/app/assets/javascripts/user.js.coffee +++ b/app/assets/javascripts/user.js.coffee @@ -2,3 +2,9 @@ class @User constructor: -> $('.profile-groups-avatars').tooltip("placement": "top") new ProjectsList() + + $('.hide-project-limit-message').on 'click', (e) -> + path = '/' + $.cookie('hide_project_limit_message', 'false', { path: path }) + $(@).parents('.project-limit-message').remove() + e.preventDefault() diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 352f0ba278..2ded32dba1 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -5,7 +5,7 @@ font-weight: normal; } } -.no-ssh-key-message { +.no-ssh-key-message, .project-limit-message { background-color: #f28d35; margin-bottom: 16px; } diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 8da7b4d50e..28803164fc 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -70,6 +70,7 @@ class ProfilesController < Profiles::ApplicationController :email, :hide_no_password, :hide_no_ssh_key, + :hide_project_limit, :linkedin, :location, :name, diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index ed480b8caf..991e67b1cd 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -1,3 +1,6 @@ += content_for :flash_message do + = render 'shared/project_limit' + %ul.center-top-menu = nav_link(path: ['projects#index', 'root#index']) do = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml new file mode 100644 index 0000000000..960ff00b49 --- /dev/null +++ b/app/views/shared/_project_limit.html.haml @@ -0,0 +1,8 @@ +- if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project? + .project-limit-message.alert.alert-warning.hidden-xs + You won't be able to create new projects because you have reached your project limit. + + .pull-right + = link_to "Don't show again", profile_path(user: {hide_project_limit: true}), method: :put, class: 'alert-link' + | + = link_to 'Remind later', '#', class: 'hide-project-limit-message alert-link' diff --git a/db/migrate/20151203162133_add_hide_project_limit_to_users.rb b/db/migrate/20151203162133_add_hide_project_limit_to_users.rb new file mode 100644 index 0000000000..6ffadfa189 --- /dev/null +++ b/db/migrate/20151203162133_add_hide_project_limit_to_users.rb @@ -0,0 +1,5 @@ +class AddHideProjectLimitToUsers < ActiveRecord::Migration + def change + add_column :users, :hide_project_limit, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index fbcb711e56..fb59e18762 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151118162244) do +ActiveRecord::Schema.define(version: 20151203162133) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -814,6 +814,7 @@ ActiveRecord::Schema.define(version: 20151118162244) do t.integer "project_view", default: 0 t.integer "consumed_timestep" t.integer "layout", default: 0 + t.boolean "hide_project_limit", default: false end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree From 176d6e2a8ff97a33d533495aa3a2775dbb87284f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 5 Dec 2015 22:09:52 +0100 Subject: [PATCH 335/908] Refactor note awards to reuse `emoji_pattern` and improve validator --- app/models/note.rb | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/app/models/note.rb b/app/models/note.rb index 03640be7c9..2bee19479c 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -39,9 +39,11 @@ class Note < ActiveRecord::Base delegate :name, to: :project, prefix: true delegate :name, :email, to: :author, prefix: true + before_validation :set_award! + validates :note, :project, presence: true validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } - validates :note, format: { with: /\A[-_+[:alnum:]]*\z/ }, if: -> (n){ n.is_award } + validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award } validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true # Attachments are deprecated and are handled by Markdown uploader validates :attachment, file_size: { maximum: :max_attachment_size } @@ -72,7 +74,6 @@ class Note < ActiveRecord::Base serialize :st_diff before_create :set_diff, if: ->(n) { n.line_code.present? } - before_validation :set_award! class << self def discussions_from_notes(notes) @@ -351,36 +352,31 @@ class Note < ActiveRecord::Base !system? end - # Checks if note is an award added from an issue comment. + # Checks if note is an award added as a comment # - # If note is an award, this method sets is_award to true, - # and changes note content to award-emoji name. - # - # Awards are only supported for issue comments. + # If note is an award, this method sets is_award to true + # and changes content of the note to award name. # # Method is executed as a before_validation callback. # def set_award! - return unless supports_awards? && contains_emoji_only? - + return unless awards_supported? && contains_emoji_only? self.is_award = true self.note = award_emoji_name end - def supports_awards? - noteable.kind_of?(Issue) || - noteable.is_a?(MergeRequest) - end - private + def awards_supported? + noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest) + end + def contains_emoji_only? - (note =~ /\A:[-_+[:alnum:]]*:\s?\z/) ? true : false + emoji_only_pattern = /\A#{Gitlab::Markdown::EmojiFilter.emoji_pattern}\s?\Z/ + (note =~ emoji_only_pattern) ? true : false end def award_emoji_name - return nil unless contains_emoji_only? - - note.match(/\A:([-_+[:alnum:]]*):\s?/)[1] + note.match(Gitlab::Markdown::EmojiFilter.emoji_pattern)[1] end end From bfe91b692a89f7a5ee8a0b044fabf5ec397b2904 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 5 Dec 2015 22:18:13 +0100 Subject: [PATCH 336/908] Remove space before exclamation mark in award alert [ci skip] --- app/assets/javascripts/notes.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 4f559e8637..dd6cbcfc70 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -113,7 +113,7 @@ class @Notes renderNote: (note) -> unless note.valid if note.award - flash = new Flash('You have already used this award emoji !', 'alert') + flash = new Flash('You have already used this award emoji!', 'alert') flash.pin() return From 0ba1efbb05fb3ed2cbbaf646b7fae5b9098c3021 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Sat, 5 Dec 2015 14:24:56 -0800 Subject: [PATCH 337/908] Link to the gitlab-ci.yml file of GitLab itself. --- doc/ci/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/ci/README.md b/doc/ci/README.md index 97325069ce..1746ea6dd4 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -12,6 +12,7 @@ ### Examples ++ [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) + [Test and deploy Ruby applications to Heroku](examples/test-and-deploy-ruby-application-to-heroku.md) + [Test and deploy Python applications to Heroku](examples/test-and-deploy-python-application-to-heroku.md) + [Test Clojure applications](examples/test-clojure-application.md) From 6ee43ada3a68407affd9e73ff4f1d5f9665da2d4 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 5 Dec 2015 17:49:32 -0500 Subject: [PATCH 338/908] Bump gitlab_emoji to ~> 0.2.0 --- Gemfile | 2 +- Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 67640bb9ae..efcfcb10a5 100644 --- a/Gemfile +++ b/Gemfile @@ -193,7 +193,7 @@ gem 'jquery-turbolinks', '~> 2.1.0' gem 'addressable', '~> 2.3.8' gem 'bootstrap-sass', '~> 3.0' gem 'font-awesome-rails', '~> 4.2' -gem 'gitlab_emoji', '~> 0.1' +gem 'gitlab_emoji', '~> 0.2.0' gem 'gon', '~> 6.0.1' gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-rails', '~> 3.1.3' diff --git a/Gemfile.lock b/Gemfile.lock index cd1855758b..7eaa0d2c84 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -864,7 +864,7 @@ DEPENDENCIES github-linguist (~> 4.7.0) github-markup (~> 1.3.1) gitlab-flowdock-git-hook (~> 1.0.1) - gitlab_emoji (~> 0.1) + gitlab_emoji (~> 0.2.0) gitlab_git (~> 7.2.20) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) From ee134d09e7cedb57cd021314d5de459a4adf3d4d Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Sat, 5 Dec 2015 15:06:32 -0800 Subject: [PATCH 339/908] Move release cycle comments to the documentation. --- README.md | 2 +- doc/release/README.md | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 52e2d97762..4cc9b35063 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ There are a lot of [third-party applications integrating with GitLab](https://ab ## GitLab release cycle -Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). For more information about the release process see the [release documentation](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/release). Features that will likely be in the next releases can be found on the [feature request forum](http://feedback.gitlab.com/forums/176466-general) with the status [started](http://feedback.gitlab.com/forums/176466-general/status/796456) and [completed](http://feedback.gitlab.com/forums/176466-general/status/796457). +For more information about the release process see the [release documentation](http://doc.gitlab.com/ce/release/). ## Upgrading diff --git a/doc/release/README.md b/doc/release/README.md index 1342b90f3b..52eca7c02a 100644 --- a/doc/release/README.md +++ b/doc/release/README.md @@ -1,4 +1,8 @@ -GitLab has the following updates: +## Release cycle + +Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). Features that will likely be in the next releases can be found on the [direction page](https://about.gitlab.com/direction/). + +## Release process documentation - [Monthly release](monthly.md), every month on the 22nd. - [Patch release](patch.md), if there are serious regressions. From caa6851bf5a65e454b702104a2895e63e368a21a Mon Sep 17 00:00:00 2001 From: Anton Baklanov Date: Sun, 29 Nov 2015 22:42:54 +0200 Subject: [PATCH 340/908] Fixed duplicated issue note email notifications. Fixes #2560 --- CHANGELOG | 1 + app/services/notification_service.rb | 1 + spec/services/notification_service_spec.rb | 9 ++++++--- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7b2f152865..99c5fdd4d0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -23,6 +23,7 @@ v 8.2.2 - Prevent "413 Request entity too large" errors when pushing large files with LFS - Fix invalid links within projects dashboard header - Make current user the first user in assignee dropdown in issues detail page (Stan Hu) + - Fix: duplicate email notifications on issue comments v 8.2.1 - Forcefully update builds that didn't want to update with state machine diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 388a4defb2..bdf7b3ad2b 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -145,6 +145,7 @@ class NotificationService recipients = reject_unsubscribed_users(recipients, note.noteable) recipients.delete(note.author) + recipients = recipients.uniq # build notify method like 'note_commit_email' notify_method = "note_#{note.noteable_type.underscore}_email".to_sym diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index a4e2b2953c..35fa412ed8 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -45,6 +45,7 @@ describe NotificationService do project.team << [issue.author, :master] project.team << [issue.assignee, :master] project.team << [note.author, :master] + create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@subscribed_participant cc this guy') end describe :new_note do @@ -60,6 +61,7 @@ describe NotificationService do should_email(note.noteable.assignee) should_email(@u_mentioned) should_email(@subscriber) + should_email(@subscribed_participant) should_not_email(note.author) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -381,18 +383,19 @@ describe NotificationService do def add_users_with_subscription(project, issuable) @subscriber = create :user @unsubscriber = create :user + @subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: Notification::N_PARTICIPATING) + project.team << [@subscribed_participant, :master] project.team << [@subscriber, :master] project.team << [@unsubscriber, :master] issuable.subscriptions.create(user: @subscriber, subscribed: true) + issuable.subscriptions.create(user: @subscribed_participant, subscribed: true) issuable.subscriptions.create(user: @unsubscriber, subscribed: false) end def sent_to_user?(user) - ActionMailer::Base.deliveries.any? do |message| - message.to.include?(user.email) - end + ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1 end def should_email(user) From 1c4213acd5dde6ce44a70b79dd766e9e7f8b59b4 Mon Sep 17 00:00:00 2001 From: Vyacheslav Stetskevych Date: Sun, 6 Dec 2015 03:10:29 +0200 Subject: [PATCH 341/908] Fix gitlab-ssl nginx config to work when multiple server_names are served over https --- lib/support/nginx/gitlab-ssl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 016f7a536f..79fe147482 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -56,7 +56,7 @@ server { listen [::]:80 ipv6only=on default_server; server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com server_tokens off; ## Don't show the nginx version number, a security best practice - return 301 https://$server_name$request_uri; + return 301 https://$http_host$request_uri; access_log /var/log/nginx/gitlab_access.log; error_log /var/log/nginx/gitlab_error.log; } From 631a30276e30354cfde6b759527abbb26ff6cf96 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 5 Dec 2015 17:36:19 -0800 Subject: [PATCH 342/908] Fix API setting of 'public' attribute to false will make a project private Closes #3864 --- CHANGELOG | 1 + lib/api/projects.rb | 8 ++++++-- spec/requests/api/projects_spec.rb | 12 ++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7b2f152865..65be6b0547 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) + - Fix API setting of 'public' attribute to false will make a project private (Stan Hu) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - Fix 500 error when update group member permission - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 2b4ada6e2e..6928fe0eb9 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -7,8 +7,12 @@ module API helpers do def map_public_to_visibility_level(attrs) publik = attrs.delete(:public) - publik = parse_boolean(publik) - attrs[:visibility_level] = Gitlab::VisibilityLevel::PUBLIC if !attrs[:visibility_level].present? && publik == true + if publik.present? && !attrs[:visibility_level].present? + publik = parse_boolean(publik) + # Since setting the public attribute to private could mean either + # private or internal, use the more conservative option, private. + attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE + end attrs end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index c59ee7af8a..24b765f497 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -742,6 +742,18 @@ describe API::API, api: true do end end + it 'should update visibility_level from public to private' do + project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC }) + + project_param = { public: false } + put api("/projects/#{project3.id}", user), project_param + expect(response.status).to eq(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + it 'should not update name to existing name' do project_param = { name: project3.name } put api("/projects/#{project.id}", user), project_param From 88217029fc37e972fd84aec7eb9a77247a9532bf Mon Sep 17 00:00:00 2001 From: Eirik Lygre Date: Sun, 6 Dec 2015 20:48:04 +0100 Subject: [PATCH 343/908] When rendering the clone page, check user profile to decide default clone protocol. If the user has uploaded SSH-keys, use SSH; otherwise, use http(s). Close #3504. --- app/helpers/projects_helper.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 48729e5260..22db8d860e 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -175,11 +175,21 @@ module ProjectsHelper end def default_url_to_repo(project = @project) - current_user ? project.url_to_repo : project.http_url_to_repo + if default_clone_protocol == "ssh" + project.ssh_url_to_repo + else + project.http_url_to_repo + end end def default_clone_protocol - current_user ? "ssh" : "http" + if !current_user + "http" + elsif current_user.require_ssh_key? + "http" + else + "ssh" + end end def project_last_activity(project) From 46278ec7ba7c618acaf7381ad466742ce84e33db Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 7 Dec 2015 09:59:04 +0100 Subject: [PATCH 344/908] Button fix --- app/services/system_note_service.rb | 2 +- .../widget/open/_accept.html.haml | 33 ++++++++++++++++--- .../merge_when_build_succeeds_spec.rb | 2 +- spec/models/merge_request_spec.rb | 2 +- .../merge_when_build_succeeds_service_spec.rb | 2 +- spec/services/system_note_service_spec.rb | 2 +- 6 files changed, 34 insertions(+), 9 deletions(-) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index f84e480ca9..6d15a49145 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -139,7 +139,7 @@ class SystemNoteService # Called when 'merge when build succeeds' is canceled def self.cancel_merge_when_build_succeeds(noteable, project, author) - body = "Cancelled the automatic merge" + body = "Canceled the automatic merge" create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index f7d872aa45..c2badf342d 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -5,10 +5,22 @@ .accept-merge-holder.clearfix.js-toggle-container .accept-action - if @merge_request.ci_commit && @merge_request.ci_commit.active? - = f.button class: "btn btn-create btn-grouped merge_when_build_succeeds", name: "merge_when_build_succeeds" do - Merge When Build Succeeds - = f.button class: "btn btn-create btn-grouped accept_merge_request #{status_class}" do - Accept Merge Request Now + %span.btn-group + = link_to "#", class: "btn btn-create merge_when_build_succeeds" do + Merge When Build Succeeds + %a.btn.btn-success.dropdown-toggle{ 'data-toggle' => 'dropdown' } + %span.caret + %span.sr-only + Select Merge Moment + %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' } + %li + = link_to "#", class: "merge_when_build_succeeds" do + = icon('check fw') + Merge When Build Succeeds + %li + = link_to "#", class: "accept_merge_request" do + = icon('warning fw') + Accept Merge Request Now - else = f.button class: "btn btn-create btn-grouped accept_merge_request #{status_class}" do Accept Merge Request @@ -26,6 +38,8 @@ text: @merge_request.merge_commit_message, rows: 14, hint: true + = hidden_field_tag :merge_when_build_succeeds, "" + :javascript $('.accept_merge_request').on('click', function() { $(this).html(" Merge in progress"); @@ -34,3 +48,14 @@ $('.accept-mr-form').on('ajax:send', function() { $(".accept-mr-form :input").disable(); }); + + $('a.accept_merge_request').on('click', function(e) { + e.preventDefault(); + $(this).closest("form").submit(); + }); + + $('a.merge_when_build_succeeds').on('click', function(e) { + e.preventDefault(); + $("#merge_when_build_succeeds").val("1"); + $(this).closest("form").submit(); + }); diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index 2e64e903d1..b5f319f204 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -68,7 +68,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do expect(page).to have_button "Merge When Build Succeeds" visit_merge_request(merge_request) # Needed to refresh the page - expect(page).to have_content "Cancelled the automatic merge" + expect(page).to have_content "Canceled the automatic merge" end it "allows the user to remove the source branch" do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 63e1cd1fb9..33acfa37fe 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -192,7 +192,7 @@ describe MergeRequest do end end - describe '#can_remove_source_branch' do + describe '#can_remove_source_branch?' do let(:user) { create(:user) } let(:user2) { create(:user) } diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index a62d88cde8..188fda6211 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -78,7 +78,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do it 'Posts a system note' do note = mr_merge_if_green_enabled.notes.last - expect(note.note).to include 'Cancelled the automatic merge' + expect(note.note).to include 'Canceled the automatic merge' end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 333035f2d2..15173cee0a 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -229,7 +229,7 @@ describe SystemNoteService do it_behaves_like 'a system note' it "posts the Merge When Build Succeeds system note" do - expect(subject.note).to eq "Cancelled the automatic merge" + expect(subject.note).to eq "Canceled the automatic merge" end end From cfc95b828724b0d7cb07254091e675d6d8093dba Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 7 Dec 2015 10:14:37 +0100 Subject: [PATCH 345/908] Fix schema --- db/schema.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index fb59e18762..94b87040d8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -476,9 +476,9 @@ ActiveRecord::Schema.define(version: 20151203162133) do add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree create_table "merge_requests", force: :cascade do |t| - t.string "target_branch", null: false - t.string "source_branch", null: false - t.integer "source_project_id", null: false + t.string "target_branch", null: false + t.string "source_branch", null: false + t.integer "source_project_id", null: false t.integer "author_id" t.integer "assignee_id" t.string "title" @@ -487,13 +487,16 @@ ActiveRecord::Schema.define(version: 20151203162133) do t.integer "milestone_id" t.string "state" t.string "merge_status" - t.integer "target_project_id", null: false + t.integer "target_project_id", null: false t.integer "iid" t.text "description" - t.integer "position", default: 0 + t.integer "position", default: 0 t.datetime "locked_at" t.integer "updated_by_id" t.string "merge_error" + t.text "merge_params" + t.boolean "merge_when_build_succeeds", default: false, null: false + t.integer "merge_user_id" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree From 900d3a09a73b953e3271a233aa6fc4937ac9d59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20St=C3=B6ckler?= Date: Mon, 7 Dec 2015 10:28:47 +0100 Subject: [PATCH 346/908] Fix typos in integration docs --- doc/integration/bitbucket.md | 2 +- doc/integration/crowd.md | 2 +- doc/integration/github.md | 2 +- doc/integration/gitlab.md | 2 +- doc/integration/google.md | 2 +- doc/integration/saml.md | 2 +- doc/integration/twitter.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 6a0fa4ce01..63432b0443 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -30,7 +30,7 @@ Bitbucket will generate an application ID and secret key for you to use. sudo editor /etc/gitlab/gitlab.rb ``` - For instalations from source: + For installations from source: ```sh cd /home/git/gitlab diff --git a/doc/integration/crowd.md b/doc/integration/crowd.md index 2ecc8795ac..40d93aef2a 100644 --- a/doc/integration/crowd.md +++ b/doc/integration/crowd.md @@ -10,7 +10,7 @@ To enable the Crowd OmniAuth provider you must register your application with Cr sudo editor /etc/gitlab/gitlab.rb ``` - For instalations from source: + For installations from source: ```sh cd /home/git/gitlab diff --git a/doc/integration/github.md b/doc/integration/github.md index b64501c2aa..a789d2c814 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -32,7 +32,7 @@ GitHub will generate an application ID and secret key for you to use. sudo editor /etc/gitlab/gitlab.rb ``` - For instalations from source: + For installations from source: ```sh cd /home/git/gitlab diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md index 216f1f11a9..80e3c0142a 100644 --- a/doc/integration/gitlab.md +++ b/doc/integration/gitlab.md @@ -38,7 +38,7 @@ GitLab.com will generate an application ID and secret key for you to use. sudo editor /etc/gitlab/gitlab.rb ``` - For instalations from source: + For installations from source: ```sh cd /home/git/gitlab diff --git a/doc/integration/google.md b/doc/integration/google.md index e1c14c7c94..91e9b2495c 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -35,7 +35,7 @@ To enable the Google OAuth2 OmniAuth provider you must register your application sudo editor /etc/gitlab/gitlab.rb ``` - For instalations from source: + For installations from source: ```sh cd /home/git/gitlab diff --git a/doc/integration/saml.md b/doc/integration/saml.md index 4aa6dbe758..1b8c28dd0f 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -14,7 +14,7 @@ First configure SAML 2.0 support in GitLab, then register the GitLab application sudo editor /etc/gitlab/gitlab.rb ``` - For instalations from source: + For installations from source: ```sh cd /home/git/gitlab diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md index 1350c8f693..52ed4a2233 100644 --- a/doc/integration/twitter.md +++ b/doc/integration/twitter.md @@ -37,7 +37,7 @@ To enable the Twitter OmniAuth provider you must register your application with sudo editor /etc/gitlab/gitlab.rb ``` - For instalations from source: + For installations from source: ```sh cd /home/git/gitlab From 893d08c0dc6a1eba14db7694636707f30b28a7f4 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 7 Dec 2015 11:00:03 +0100 Subject: [PATCH 347/908] Simplify `contains_emoji_only?` method in `Note` --- app/models/note.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/models/note.rb b/app/models/note.rb index 2bee19479c..239a0f77f8 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -372,8 +372,7 @@ class Note < ActiveRecord::Base end def contains_emoji_only? - emoji_only_pattern = /\A#{Gitlab::Markdown::EmojiFilter.emoji_pattern}\s?\Z/ - (note =~ emoji_only_pattern) ? true : false + note =~ /\A#{Gitlab::Markdown::EmojiFilter.emoji_pattern}\s?\Z/ end def award_emoji_name From 6eb75ed6ad2710126b577f8093e78faff64bae1b Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 7 Dec 2015 11:29:23 +0100 Subject: [PATCH 348/908] Fix specs --- .../merge_requests/merge_when_build_succeeds_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index b5f319f204..3a8f0a344e 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -23,12 +23,12 @@ feature 'Merge When Build Succeeds', feature: true, js: true do end it 'displays the Merge When Build Succeeds button' do - expect(page).to have_button "Merge When Build Succeeds" + expect(page).to have_link "Merge When Build Succeeds" end context "Merge When Build succeeds enabled" do before do - click_button "Merge When Build Succeeds" + click_link "Merge When Build Succeeds" end it 'activates Merge When Build Succeeds feature' do @@ -65,7 +65,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do it 'cancels the automatic merge' do click_link "Cancel Automatic Merge" - expect(page).to have_button "Merge When Build Succeeds" + expect(page).to have_link "Merge When Build Succeeds" visit_merge_request(merge_request) # Needed to refresh the page expect(page).to have_content "Canceled the automatic merge" @@ -82,7 +82,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do context 'Build is not active' do it "should not allow for enabling" do visit_merge_request(merge_request) - expect(page).not_to have_button "Merge When Build Succeeds" + expect(page).not_to have_link "Merge When Build Succeeds" end end From 96520227ae6fc5b02b0725bc515511960bcfefed Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 7 Dec 2015 11:29:48 +0100 Subject: [PATCH 349/908] Prevent Firefox from remembering hidden field value --- app/views/projects/merge_requests/widget/open/_accept.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index c2badf342d..0fe3af97b7 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -38,7 +38,7 @@ text: @merge_request.merge_commit_message, rows: 14, hint: true - = hidden_field_tag :merge_when_build_succeeds, "" + = hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off" :javascript $('.accept_merge_request').on('click', function() { From 4c8666e69fc64200bdd2f5069bbc8c9b22fe12ab Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 7 Dec 2015 11:30:09 +0100 Subject: [PATCH 350/908] Fix commit message textarea position --- .../widget/open/_accept.html.haml | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 0fe3af97b7..6b4395fe4d 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -3,37 +3,38 @@ = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-requires-input' } do |f| = hidden_field_tag :authenticity_token, form_authenticity_token .accept-merge-holder.clearfix.js-toggle-container - .accept-action - - if @merge_request.ci_commit && @merge_request.ci_commit.active? - %span.btn-group - = link_to "#", class: "btn btn-create merge_when_build_succeeds" do - Merge When Build Succeeds - %a.btn.btn-success.dropdown-toggle{ 'data-toggle' => 'dropdown' } - %span.caret - %span.sr-only - Select Merge Moment - %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' } - %li - = link_to "#", class: "merge_when_build_succeeds" do - = icon('check fw') - Merge When Build Succeeds - %li - = link_to "#", class: "accept_merge_request" do - = icon('warning fw') - Accept Merge Request Now - - else - = f.button class: "btn btn-create btn-grouped accept_merge_request #{status_class}" do - Accept Merge Request - - if @merge_request.can_remove_source_branch?(current_user) - .accept-control.checkbox - = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do - = check_box_tag :should_remove_source_branch - Remove source branch - .accept-control.right - = link_to "#", class: "modify-merge-commit-link js-toggle-button" do - = icon('edit') - Modify commit message - .js-toggle-content.hide.prepend-top-20 + .clearfix + .accept-action + - if @merge_request.ci_commit && @merge_request.ci_commit.active? + %span.btn-group + = link_to "#", class: "btn btn-create merge_when_build_succeeds" do + Merge When Build Succeeds + %a.btn.btn-success.dropdown-toggle{ 'data-toggle' => 'dropdown' } + %span.caret + %span.sr-only + Select Merge Moment + %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' } + %li + = link_to "#", class: "merge_when_build_succeeds" do + = icon('check fw') + Merge When Build Succeeds + %li + = link_to "#", class: "accept_merge_request" do + = icon('warning fw') + Merge Immediately + - else + = f.button class: "btn btn-create btn-grouped accept_merge_request #{status_class}" do + Accept Merge Request + - if @merge_request.can_remove_source_branch?(current_user) + .accept-control.checkbox + = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do + = check_box_tag :should_remove_source_branch + Remove source branch + .accept-control.right + = link_to "#", class: "modify-merge-commit-link js-toggle-button" do + = icon('edit') + Modify commit message + .js-toggle-content.hide.prepend-top-default = render 'shared/commit_message_container', params: params, text: @merge_request.merge_commit_message, rows: 14, hint: true From f5ec1ebe2caa0d8b4ccccb671ae6a4cc99cddbe0 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 7 Dec 2015 12:07:13 +0100 Subject: [PATCH 351/908] Remove changelog entry issue number --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 882648b36a..00a24d4317 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,7 +6,7 @@ v 8.3.0 (unreleased) - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) - Add ignore whitespace change option to commit view - Fire update hook from GitLab - - Run custom Git hooks when branch is created or deleted. #1156 + - Run custom Git hooks when branch is created or deleted. v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) From 839a8b924972d87e37085333adf1b74b41bbd26c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 7 Dec 2015 12:07:40 +0100 Subject: [PATCH 352/908] Move changelog item --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 9c00aa7a65..2cb341d882 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,10 +11,10 @@ v 8.3.0 (unreleased) - Add API endpoint to fetch merge request commits list - Expose events API with comment information and author info - Fix: Ensure "Remove Source Branch" button is not shown when branch is being deleted. #3583 + - Run custom Git hooks when branch is created or deleted. v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) - - Run custom Git hooks when branch is created or deleted. v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) From 5df2c4419c5019b5003ddfa6adb59c84c3d9910c Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 7 Dec 2015 14:11:15 +0200 Subject: [PATCH 353/908] fox specs --- app/models/commit.rb | 37 ++++++++++++++++++++------------- app/models/merge_request.rb | 2 +- lib/gitlab/push_data_builder.rb | 4 +++- spec/models/commit_spec.rb | 2 +- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index 912b4dedf5..fecadfeec8 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -135,10 +135,10 @@ class Commit description.present? end - def hook_attrs + def hook_attrs(with_changed_files = false) path_with_namespace = project.path_with_namespace - { + data = { id: id, message: safe_message, timestamp: committed_date.xmlschema, @@ -146,11 +146,18 @@ class Commit author: { name: author_name, email: author_email - }, - added: repo_changes[:added], - modified: repo_changes[:modified], - removed: repo_changes[:removed] + } } + + if with_changed_files + data.merge!({ + added: repo_changes[:added], + modified: repo_changes[:modified], + removed: repo_changes[:removed] + }) + end + + data end # Discover issues should be closed when this commit is pushed to a project's @@ -205,14 +212,16 @@ class Commit def repo_changes changes = { added: [], modified: [], removed: [] } - diffs.each do |diff| - case true - when diff.deleted_file - changes[:removed] << diff.old_path - when diff.renamed_file, diff.new_file - changes[:added] << diff.new_path - else - changes[:modified] << diff.new_path + if diffs.any? + diffs.each do |diff| + case true + when diff.deleted_file + changes[:removed] << diff.old_path + when diff.renamed_file, diff.new_file + changes[:added] << diff.new_path + else + changes[:modified] << diff.new_path + end end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 1b3d6079d2..92a82d44c7 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -291,7 +291,7 @@ class MergeRequest < ActiveRecord::Base work_in_progress: work_in_progress? } - unless last_commit.nil? + if last_commit attrs.merge!(last_commit: last_commit.hook_attrs) end diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index cdcdb02a05..5842b740e8 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -30,7 +30,9 @@ module Gitlab # For performance purposes maximum 20 latest commits # will be passed as post receive hook data. - commit_attrs = commits_limited.map(&:hook_attrs) + commit_attrs = commits_limited.map do |commit| + commit.hook_attrs(true) + end type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push" diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index b417bc98fa..6728722b50 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -102,7 +102,7 @@ eos end describe '#hook_attrs' do - let(:data) { commit.hook_attrs } + let(:data) { commit.hook_attrs(true) } it { expect(data).to be_a(Hash) } it { expect(data[:message]).to include('Add submodule from gitlab.com') } From e7969d6f6c743ead94b3b430b9184ad21f647337 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 7 Dec 2015 11:54:07 +0100 Subject: [PATCH 354/908] Satisfy Douwe Maan --- CHANGELOG | 2 +- app/services/system_note_service.rb | 2 +- .../merge_when_build_succeeds_spec.rb | 14 ++++++-------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6c2fa75899..163a0ea54e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) -- Merge when build succeeds (Zeger-Jan van de Weg) + - Merge when build succeeds (Zeger-Jan van de Weg) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - Fix 500 error when update group member permission - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 7cb9417b13..6975b2ee55 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -132,7 +132,7 @@ class SystemNoteService # Called when 'merge when build succeeds' is executed def self.merge_when_build_succeeds(noteable, project, author, last_commit) - body = "Enabled an automatic merge when the build for #{last_commit.to_reference} succeeds" + body = "Enabled an automatic merge when the build for #{last_commit.to_reference(project)} succeeds" create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index 3a8f0a344e..ee2fb25e9e 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -1,6 +1,4 @@ require 'spec_helper' -# rubocop:disable Lint/UselessAssignment -# As rubocop doesn't see a need for both `ci_commit` and `ci_build` feature 'Merge When Build Succeeds', feature: true, js: true do let(:user) { create(:user) } @@ -14,10 +12,10 @@ feature 'Merge When Build Succeeds', feature: true, js: true do end context "Active build for Merge Request" do - before do - ci_commit = create(:ci_commit, gl_project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) - ci_build = create(:ci_build, commit: ci_commit) + let!(:ci_commit) { create(:ci_commit, gl_project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let!(:ci_build) { create(:ci_build, commit: ci_commit) } + before do login_as user visit_merge_request(merge_request) end @@ -49,14 +47,15 @@ feature 'Merge When Build Succeeds', feature: true, js: true do merge_user: user, title: "MepMep", merge_when_build_succeeds: true) end + let!(:ci_commit) { create(:ci_commit, gl_project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let!(:ci_build) { create(:ci_build, commit: ci_commit) } + before do merge_request.source_project.team << [user, :master] merge_request.source_branch = "feature" merge_request.target_branch = "master" merge_request.save! - ci_commit = create(:ci_commit, gl_project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) - ci_build = create(:ci_build, commit: ci_commit) login_as user visit_merge_request(merge_request) @@ -90,4 +89,3 @@ feature 'Merge When Build Succeeds', feature: true, js: true do visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) end end -# rubocop:enable Lint/UselessAssignment From ff08ce9ca4bef1a4f81f7a4b323614a639efe959 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 7 Dec 2015 13:45:00 +0100 Subject: [PATCH 355/908] Satisfy Rubocop --- app/models/global_milestone.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 33ddb265fb..dd9f88704a 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -127,4 +127,4 @@ class GlobalMilestone end end end -end \ No newline at end of file +end From 1a942111653e9376d6275c5bd31f23cb27f58ab5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 7 Dec 2015 13:55:30 +0100 Subject: [PATCH 356/908] Remove pointless explanation for user tabs --- app/assets/stylesheets/framework/common.scss | 10 +++++++--- app/views/users/show.html.haml | 14 +------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index d2f491daf7..58c750bc37 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -377,10 +377,9 @@ table { .center-top-menu { @include nav-menu; text-align: center; - margin-top: 5px; - margin-bottom: $gl-padding; height: 56px; - margin-top: -$gl-padding; + margin: -$gl-padding; + margin-bottom: $gl-padding; padding-top: $gl-padding; &.no-bottom { @@ -390,6 +389,11 @@ table { &.no-top { margin-top: 0; } + + &.bottom-border { + border-bottom: 1px solid $border-color; + height: 57px; + } } .center-middle-menu { diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index e35e69bda8..a0a6e2d981 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -73,7 +73,7 @@ .user-calendar-activities -%ul.center-top-menu.no-top.no-bottom +%ul.center-top-menu.no-top.no-bottom.bottom-border %li.active = link_to "#activity", 'data-toggle' => 'tab' do Activity @@ -92,26 +92,17 @@ .tab-content .tab-pane.active#activity - .gray-content-block.middle-block - Public activity by #{@user.name} - .content_list = spinner - if @groups.any? .tab-pane#groups - .gray-content-block.middle-block - Groups #{@user.name} is a member of - %ul.content-list - @groups.each do |group| = render 'shared/groups/group', group: group - if @contributed_projects.present? .tab-pane#contributed - .gray-content-block.middle-block - Projects #{@user.name} has contributed to - .contributed-projects = render 'shared/projects/list', projects: @contributed_projects.sort_by(&:star_count).reverse, @@ -119,9 +110,6 @@ - if @projects.present? .tab-pane#personal - .gray-content-block.middle-block - Projects owned by #{@user.name} - .personal-projects = render 'shared/projects/list', projects: @projects.sort_by(&:star_count).reverse, From a426fb1596978221510c8c78a17703658ad7d161 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 7 Dec 2015 14:11:13 +0100 Subject: [PATCH 357/908] Update documentation about automatic issue closing --- config/gitlab.yml.example | 2 +- doc/customization/issue_closing.md | 27 ++++++++++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 1da42ab38f..db378118f8 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -76,7 +76,7 @@ production: &base # This happens when the commit is pushed or merged into the default branch of a project. # When not specified the default issue_closing_pattern as specified below will be used. # Tip: you can test your closing pattern at http://rubular.com. - # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' + # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)' ## Default project features settings default_projects_features: diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md index 64f128f5a6..00edfc97ed 100644 --- a/doc/customization/issue_closing.md +++ b/doc/customization/issue_closing.md @@ -1,38 +1,39 @@ # Issue closing pattern -Here's how to close multiple issues in one commit message: +When a commit or merge request resolves one or more issues, it is possible to automatically have these issues closed when the commit or merge request lands in the project's default branch. -If a commit message matches the regular expression below, all issues referenced from -the matched text will be closed. This happens when the commit is pushed or merged -into the default branch of a project. +If a commit message or merge request description contains a sentence matching the regular expression below, all issues referenced from +the matched text will be closed. This happens when the commit is pushed to a project's default branch, or when a commit or merge request is merged into there. -When not specified, the default issue_closing_pattern as shown below will be used: +When not specified, the default `issue_closing_pattern` as shown below will be used: ```bash -((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+) +((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+) ``` +Here, `%{issue_ref}` is a complex regular expression defined inside GitLab, that matches a reference to a local issue (`#123`), cross-project issue (`group/project#123`) or a link to an issue (`https://gitlab.example.com/group/project/issues/123`). + For example: ``` -git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes #22). This commit is also related to #17 and fixes #18, #19 and #23." +git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes group/otherproject#2). This commit is also related to #17 and fixes #18, #19 and https://gitlab.example.com/group/otherproject/issues/23." ``` -will close `#20`, `#21`, `#22`, `#18`, `#19` and `#23`, but `#17` won't be closed -as it does not match the pattern. It also works with multiline commit messages. +will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as it does not match the pattern. It also works with multiline commit messages. Tip: you can test this closing pattern at [http://rubular.com][1]. Use this site to test your own patterns. +Because Rubular doesn't understand `%{issue_ref}`, you can replace this by `#\d+` in testing, which matches only local issue references like `#123`. ## Change the pattern For Omnibus installs you can change the default pattern in `/etc/gitlab/gitlab.rb`: ``` -issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' +issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)' ``` -For manual installs you can customize the pattern in [gitlab.yml][0]. +For manual installs you can customize the pattern in [gitlab.yml][0] using the `issue_closing_pattern` key. -[0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/40c3675372320febf5264061c9bcd63db2dfd13c/config/gitlab.yml.example#L65 -[1]: http://rubular.com/r/Xmbexed1OJ \ No newline at end of file +[0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example +[1]: http://rubular.com/r/Xmbexed1OJ From 3c97cbc74cf87856ed7b1af197358d4e3adb1240 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 7 Dec 2015 15:13:06 +0200 Subject: [PATCH 358/908] fixes after review --- app/models/commit.rb | 25 +++++++++---------------- lib/gitlab/push_data_builder.rb | 2 +- spec/models/commit_spec.rb | 2 +- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index fecadfeec8..14883c96f5 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -135,7 +135,7 @@ class Commit description.present? end - def hook_attrs(with_changed_files = false) + def hook_attrs(with_changed_files: false) path_with_namespace = project.path_with_namespace data = { @@ -150,11 +150,7 @@ class Commit } if with_changed_files - data.merge!({ - added: repo_changes[:added], - modified: repo_changes[:modified], - removed: repo_changes[:removed] - }) + data.merge!(repo_changes) end data @@ -212,16 +208,13 @@ class Commit def repo_changes changes = { added: [], modified: [], removed: [] } - if diffs.any? - diffs.each do |diff| - case true - when diff.deleted_file - changes[:removed] << diff.old_path - when diff.renamed_file, diff.new_file - changes[:added] << diff.new_path - else - changes[:modified] << diff.new_path - end + diffs.each do |diff| + if diff.deleted_file + changes[:removed] << diff.old_path + elsif diff.renamed_file || diff.new_file + changes[:added] << diff.new_path + else + changes[:modified] << diff.new_path end end diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index 5842b740e8..4f9cdef386 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -31,7 +31,7 @@ module Gitlab # For performance purposes maximum 20 latest commits # will be passed as post receive hook data. commit_attrs = commits_limited.map do |commit| - commit.hook_attrs(true) + commit.hook_attrs(with_changed_files: true) end type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push" diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 6728722b50..0b1e2bf74d 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -102,7 +102,7 @@ eos end describe '#hook_attrs' do - let(:data) { commit.hook_attrs(true) } + let(:data) { commit.hook_attrs(with_changed_files: true) } it { expect(data).to be_a(Hash) } it { expect(data[:message]).to include('Add submodule from gitlab.com') } From e53b350cb6db7438c1a50c500b324fd87afc41c4 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 7 Dec 2015 15:03:50 +0100 Subject: [PATCH 359/908] Add specs for showing lfs object in UI. --- app/controllers/projects/raw_controller.rb | 24 ++++++++++---- app/helpers/blob_helper.rb | 14 +++++++- app/helpers/tree_helper.rb | 4 +++ app/models/lfs_object.rb | 4 +++ app/views/projects/blob/_actions.html.haml | 6 ++-- app/views/projects/blob/_blob.html.haml | 2 +- app/views/projects/blob/_download.html.haml | 6 +--- app/views/projects/blob/show.html.haml | 2 +- app/views/projects/diffs/_file.html.haml | 4 +-- features/project/source/browse_files.feature | 6 ++++ features/steps/project/source/browse_files.rb | 26 +++++++++++++++ .../projects/raw_controller_spec.rb | 32 +++++++++++++++++++ spec/support/test_env.rb | 2 +- 13 files changed, 111 insertions(+), 21 deletions(-) diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index c56f432a1f..be7d5c187f 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -38,18 +38,28 @@ class Projects::RawController < Projects::ApplicationController type = get_blob_type send_data( - @blob.data, - type: type, - disposition: 'inline' - ) + @blob.data, + type: type, + disposition: 'inline' + ) end def send_lfs_object - lfs_object = LfsObject.find_by_oid(@blob.lfs_oid) - return nil unless lfs_object && lfs_object.file.exists? + lfs_object = find_lfs_object - if lfs_object.projects.exists?(lfs_object.storage_project(@project).id) + if lfs_object && lfs_object.project_allowed_access?(@project) send_file lfs_object.file.path, filename: @blob.name, disposition: 'attachment' + else + render_404 + end + end + + def find_lfs_object + lfs_object = LfsObject.find_by_oid(@blob.lfs_oid) + if lfs_object && lfs_object.file.exists? + lfs_object + else + nil end end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index df5f5fae23..fa1b252205 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -30,7 +30,7 @@ module BlobHelper nil end - if blob && blob.text? + if blob && blob.text? && !blob.lfs_pointer? text = 'Edit' after = options[:after] || '' from_mr = options[:from_merge_request_id] @@ -71,4 +71,16 @@ module BlobHelper def blob_icon(mode, name) icon("#{file_type_icon_class('file', mode, name)} fw") end + + def viewable?(blob) + blob.text? && !blob.lfs_pointer? + end + + def blob_size(blob) + if blob.lfs_pointer? + blob.lfs_size + else + blob.size + end + end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 03a49e119b..6afa1aacc5 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -54,6 +54,10 @@ module TreeHelper ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) end + def can_delete_or_replace?(blob) + allowed_tree_edit? && !blob.lfs_pointer? + end + def tree_breadcrumbs(tree, max_links = 2) if @path.present? part_path = "" diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index a243c7b77c..18657c3e1c 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -13,4 +13,8 @@ class LfsObject < ActiveRecord::Base project end end + + def project_allowed_access?(project) + projects.exists?(storage_project(project).id) + end end diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index 15cd8f056f..c31f6442f1 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -1,9 +1,9 @@ .btn-group.tree-btn-group - = edit_blob_link(@project, @ref, @path) unless @blob.lfs_pointer? + = edit_blob_link(@project, @ref, @path) = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), class: 'btn btn-sm', target: '_blank' -# only show normal/blame view links for text files - - if @blob.text? && !@blob.lfs_pointer? + - if viewable?(@blob) - if current_page? namespace_project_blame_path(@project.namespace, @project, @id) = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id), class: 'btn btn-sm' @@ -16,7 +16,7 @@ = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.sha, @path)), class: 'btn btn-sm' -- if allowed_tree_edit? && !@blob.lfs_pointer? +- if can_delete_or_replace?(@blob) .btn-group{ role: "group" } %button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Delete diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index bb9e1c6341..2a3315da3d 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -29,7 +29,7 @@ %strong = blob.name %small - = number_to_human_size(blob.size) unless blob.lfs_pointer? + = number_to_human_size(blob_size(blob)) .file-actions.hidden-xs = render "actions" - if blob.lfs_pointer? diff --git a/app/views/projects/blob/_download.html.haml b/app/views/projects/blob/_download.html.haml index 39ec6f693e..7908fcae3d 100644 --- a/app/views/projects/blob/_download.html.haml +++ b/app/views/projects/blob/_download.html.haml @@ -4,8 +4,4 @@ %h1.light %i.fa.fa-download %h4 - - if blob.lfs_pointer? - - size = blob.lfs_size - - else - - size = blob.size - Download (#{number_to_human_size size}) + Download (#{number_to_human_size blob_size(blob)}) diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index b7276868ce..09d6fc18e3 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -6,7 +6,7 @@ %div#tree-holder.tree-holder = render 'blob', blob: @blob -- if allowed_tree_edit? +- if can_delete_or_replace?(@blob) = render 'projects/blob/remove' - title = "Replace #{@blob.name}" diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index ec1c665716..f6ba64d31b 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -25,7 +25,7 @@ = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" .diff-controls - - if blob.text? && !blob.lfs_pointer? + - if viewable?(blob) = link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do %i.fa.fa-comments   @@ -40,7 +40,7 @@ .diff-content.diff-wrap-lines -# Skipp all non non-supported blobs - return unless blob.respond_to?('text?') - - if blob.text? && !blob.lfs_pointer? + - if viewable?(blob) - if diff_view == 'parallel' = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i - else diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index e545ea63ca..37f99b3761 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -221,3 +221,9 @@ Feature: Project Source Browse Files Given I switch ref to fix And I visit the fix tree Then I see the commit data for a directory with a leading dot + + Scenario: I browse LFS object + Given I click on "files/lfs/lfs_object.iso" file in repo + Then I should see download link and object size + And I should not see lfs pointer details + And I should see buttons for allowed commands diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index ceab23b9a4..2edbca9b7f 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -305,6 +305,32 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(page).not_to have_content('Loading commit data...') end + step 'I click on "files/lfs/lfs_object.iso" file in repo' do + click_link 'files' + click_link "lfs" + click_link "lfs_object.iso" + end + + step 'I should see download link and object size' do + expect(page).to have_content 'Download (1.5 MB)' + end + + step 'I should not see lfs pointer details' do + expect(page).not_to have_content 'version https://git-lfs.github.com/spec/v1' + expect(page).not_to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' + expect(page).not_to have_content 'size 1575078' + end + + step 'I should see buttons for allowed commands' do + expect(page).to have_content 'Raw' + expect(page).to have_content 'History' + expect(page).to have_content 'Permalink' + expect(page).not_to have_content 'Edit' + expect(page).not_to have_content 'Blame' + expect(page).not_to have_content 'Delete' + expect(page).not_to have_content 'Replace' + end + private def set_new_content diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index c114f34202..2329c246da 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -33,5 +33,37 @@ describe Projects::RawController do expect(response.header['Content-Type']).to eq('image/jpeg') end end + + context 'lfs object' do + let(:id) { 'master/files/lfs/lfs_object.iso' } + let!(:lfs_object) { create(:lfs_object, oid: '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', size: '1575078') } + + context 'when project has access' do + before do + public_project.lfs_objects << lfs_object + end + + it 'serves the file' do + get(:show, + namespace_id: public_project.namespace.to_param, + project_id: public_project.to_param, + id: id) + + expect(response.status).to eq(200) + expect(response.header['Content-Type']).to eq('application/octet-stream') + end + end + + context 'when project does not have access' do + it 'does not serve the file' do + get(:show, + namespace_id: public_project.namespace.to_param, + project_id: public_project.to_param, + id: id) + + expect(response.status).to eq(404) + end + end + end end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 787670e929..47c67475cd 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -12,7 +12,7 @@ module TestEnv 'fix' => '48f0be4', 'improve/awesome' => '5937ac0', 'markdown' => '0ed8c6c', - 'master' => '5937ac0', + 'master' => 'be93687', "'test'" => 'e56497b', } From bf17609e2a4cc062a9bccd67e90dd6524f0389b2 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 7 Dec 2015 15:56:38 +0100 Subject: [PATCH 360/908] Rename blob helper, bump version of gitlab_git to 7.2.21. --- Gemfile.lock | 2 +- app/helpers/blob_helper.rb | 6 +++--- app/views/projects/blob/_actions.html.haml | 2 +- app/views/projects/diffs/_file.html.haml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index cd1855758b..e2fc414824 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -298,7 +298,7 @@ GEM posix-spawn (~> 0.3) gitlab_emoji (0.2.0) gemojione (~> 2.1) - gitlab_git (7.2.20) + gitlab_git (7.2.21) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index fa1b252205..4a3d971f7c 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -30,7 +30,7 @@ module BlobHelper nil end - if blob && blob.text? && !blob.lfs_pointer? + if blob_viewable?(blob) text = 'Edit' after = options[:after] || '' from_mr = options[:from_merge_request_id] @@ -72,8 +72,8 @@ module BlobHelper icon("#{file_type_icon_class('file', mode, name)} fw") end - def viewable?(blob) - blob.text? && !blob.lfs_pointer? + def blob_viewable?(blob) + blob && blob.text? && !blob.lfs_pointer? end def blob_size(blob) diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index c31f6442f1..0e54e59e95 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -3,7 +3,7 @@ = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), class: 'btn btn-sm', target: '_blank' -# only show normal/blame view links for text files - - if viewable?(@blob) + - if blob_viewable?(@blob) - if current_page? namespace_project_blame_path(@project.namespace, @project, @id) = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id), class: 'btn btn-sm' diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index f6ba64d31b..a930e9e559 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -25,7 +25,7 @@ = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" .diff-controls - - if viewable?(blob) + - if blob_viewable?(blob) = link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do %i.fa.fa-comments   @@ -40,7 +40,7 @@ .diff-content.diff-wrap-lines -# Skipp all non non-supported blobs - return unless blob.respond_to?('text?') - - if viewable?(blob) + - if blob_viewable?(blob) - if diff_view == 'parallel' = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i - else From 8a0507d523293289ae4b7d61bb3fce8a873e3dcc Mon Sep 17 00:00:00 2001 From: "Michael A. Smith" Date: Mon, 7 Dec 2015 10:29:42 -0500 Subject: [PATCH 361/908] Update Docker Syntax --- doc/ci/docker/using_docker_images.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 64e52eba3a..1feae62b1c 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -190,7 +190,7 @@ This will create two service containers (MySQL and PostgreSQL). 1. Create a build container and execute script in its context: ``` -$ cat build_script | docker run -n build -i -l mysql:service-mysql -l postgres:service-postgres ruby:2.1 /bin/bash +$ docker run --name build -i --link=service-mysql:mysql --link=service-postgres:postgres ruby:2.1 /bin/bash < build_script ``` This will create build container that has two service containers linked. The build_script is piped using STDIN to bash interpreter which executes the build script in container. From 8f817c7b08bcad23e1b047f84cc60d1748104e2a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 7 Dec 2015 17:10:40 +0100 Subject: [PATCH 362/908] Add API group projects endpoint. --- doc/api/groups.md | 70 +++++++++++++++++++++++++++++++++++++++++++++-- lib/api/groups.rb | 12 ++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/doc/api/groups.md b/doc/api/groups.md index 0b9f6406d8..808675d860 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -1,6 +1,6 @@ # Groups -## List project groups +## List groups Get a list of groups. (As user: my groups, as admin: all groups) @@ -21,6 +21,70 @@ GET /groups You can search for groups by name or path, see below. + +## List a group's projects + +Get a list of projects in this group. + +``` +GET /groups/:id/projects +``` + +Parameters: + +- `archived` (optional) - if passed, limit by archived status +- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` +- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` +- `search` (optional) - Return list of authorized projects according to a search criteria +- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first + +```json +[ + { + "id": 4, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 0, + "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git", + "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git", + "web_url": "http://example.com/diaspora/diaspora-client", + "tag_list": [ + "example", + "disapora client" + ], + "owner": { + "id": 3, + "name": "Diaspora", + "created_at": "2013-09-30T13: 46: 02Z" + }, + "name": "Diaspora Client", + "name_with_namespace": "Diaspora / Diaspora Client", + "path": "diaspora-client", + "path_with_namespace": "diaspora/diaspora-client", + "issues_enabled": true, + "merge_requests_enabled": true, + "builds_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-09-30T13: 46: 02Z", + "last_activity_at": "2013-09-30T13: 46: 02Z", + "creator_id": 3, + "namespace": { + "created_at": "2013-09-30T13: 46: 02Z", + "description": "", + "id": 3, + "name": "Diaspora", + "owner_id": 1, + "path": "diaspora", + "updated_at": "2013-09-30T13: 46: 02Z" + }, + "archived": false, + "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png" + } +] +``` + ## Details of a group Get all details of a group. @@ -186,7 +250,7 @@ To get more (up to 100), pass the following as an argument to the API call: /groups?per_page=100 ``` -And to switch pages add: +And to switch pages add: ``` /groups?per_page=100&page=2 -``` \ No newline at end of file +``` diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 024aeec2e1..1a14d870a4 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -65,6 +65,18 @@ module API DestroyGroupService.new(group, current_user).execute end + # Get a list of projects in this group + # + # Example Request: + # GET /groups/:id/projects + get ":id/projects" do + group = find_group(params[:id]) + projects = group.projects + projects = filter_projects(projects) + projects = paginate projects + present projects, with: Entities::Project + end + # Transfer a project to the Group namespace # # Parameters: From cbcb8dbe702c0b1532327aeb2dc53caffb6e8ed9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 7 Dec 2015 17:12:04 +0100 Subject: [PATCH 363/908] Use select2 dropdown for dashboard/group 'New X' buttons --- app/assets/javascripts/api.js.coffee | 31 +++++++++++++++++++ .../javascripts/project_select.js.coffee | 26 ++++++++++++++++ app/assets/stylesheets/framework/common.scss | 13 ++++++++ app/helpers/selects_helper.rb | 13 ++++++++ app/views/dashboard/issues.html.haml | 13 +------- app/views/dashboard/merge_requests.html.haml | 13 +------- .../dashboard/milestones/index.html.haml | 13 +------- app/views/groups/issues.html.haml | 13 +------- app/views/groups/merge_requests.html.haml | 13 +------- .../shared/_new_project_item_select.html.haml | 20 ++++++++++++ 10 files changed, 108 insertions(+), 60 deletions(-) create mode 100644 app/assets/javascripts/project_select.js.coffee create mode 100644 app/views/shared/_new_project_item_select.html.haml diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index 9e5d594c86..746fa3cea8 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -2,6 +2,8 @@ groups_path: "/api/:version/groups.json" group_path: "/api/:version/groups/:id.json" namespaces_path: "/api/:version/namespaces.json" + group_projects_path: "/api/:version/groups/:id/projects.json" + projects_path: "/api/:version/projects.json" group: (group_id, callback) -> url = Api.buildUrl(Api.group_path) @@ -44,6 +46,35 @@ ).done (namespaces) -> callback(namespaces) + # Return projects list. Filtered by query + projects: (query, callback) -> + url = Api.buildUrl(Api.projects_path) + + $.ajax( + url: url + data: + private_token: gon.api_token + search: query + per_page: 20 + dataType: "json" + ).done (projects) -> + callback(projects) + + # Return group projects list. Filtered by query + groupProjects: (group_id, query, callback) -> + url = Api.buildUrl(Api.group_projects_path) + url = url.replace(':id', group_id) + + $.ajax( + url: url + data: + private_token: gon.api_token + search: query + per_page: 20 + dataType: "json" + ).done (projects) -> + callback(projects) + buildUrl: (url) -> url = gon.relative_url_root + url if gon.relative_url_root? return url.replace(':version', gon.api_version) diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee new file mode 100644 index 0000000000..43b18a3da5 --- /dev/null +++ b/app/assets/javascripts/project_select.js.coffee @@ -0,0 +1,26 @@ +class @ProjectSelect + constructor: -> + $('.ajax-project-select').each (i, select) -> + @groupId = $(select).data('group-id') + + $(select).select2 + placeholder: "Search for project" + multiple: $(select).hasClass('multiselect') + minimumInputLength: 0 + query: (query) => + callback = (projects) -> + data = { results: projects } + query.callback(data) + + if @groupId + Api.groupProjects @groupId, query.term, callback + else + Api.projects query.term, callback + + id: (project) -> + project.web_url + + text: (project) -> + project.name_with_namespace + + dropdownCssClass: "ajax-project-dropdown" diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 61ecd58e6c..19b0868dfe 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -441,3 +441,16 @@ table { .alert, .progress { margin-bottom: $gl-padding; } + +.new-project-item-select-holder { + display: inline-block; + position: relative; + + .new-project-item-select { + position: absolute; + top: 0; + right: 0; + width: 250px !important; + visibility: hidden; + } +} diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 7e54d4d1b5..418120a3f7 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -46,6 +46,19 @@ module SelectsHelper select2_tag(id, opts) end + def project_select_tag(id, opts = {}) + opts[:class] ||= '' + opts[:class] << ' ajax-project-select' + + unless opts.delete(:scope) == :all + if @group + opts['data-group-id'] = @group.id + end + end + + hidden_field_tag(id, opts[:selected], opts) + end + def select2_tag(id, opts = {}) css_class = '' css_class << 'multiselect ' if opts[:multiple] diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 829c3c8376..2d3da01178 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -12,18 +12,7 @@ = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do %i.fa.fa-rss - - if @projects.any? { |project| can?(current_user, :create_issue, project) } - .dropdown.inline.prepend-left-10 - %button.dropdown-toggle.btn.btn-new{type: 'button', 'data-toggle' => 'dropdown'} - %i.fa.fa-plus - New Issue - %b.caret - %ul.dropdown-menu.dropdown-menu-align-right - - @projects.each do |project| - - if can?(current_user, :create_issue, project) - %li - = link_to new_namespace_project_issue_path(project.namespace, project) do - = project.name_with_namespace + = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/issuable/filter', type: :issues diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index 2e91c8dec8..c5a5ec21f7 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -3,18 +3,7 @@ .project-issuable-filter .controls - - if @projects.any? { |project| can?(current_user, :create_merge_request, project) } - .dropdown.inline - %button.dropdown-toggle.btn.btn-new{type: 'button', 'data-toggle' => 'dropdown'} - %i.fa.fa-plus - New Merge Request - %b.caret - %ul.dropdown-menu.dropdown-menu-align-right - - @projects.each do |project| - - if can?(current_user, :create_merge_request, project) - %li - = link_to new_namespace_project_merge_request_path(project.namespace, project) do - = project.name_with_namespace + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" = render 'shared/issuable/filter', type: :merge_requests diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 9aea75e50d..94ff259a33 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -3,18 +3,7 @@ .project-issuable-filter .controls - - if @projects.any? { |project| can?(current_user, :admin_milestone, project) } - .dropdown.inline - %button.dropdown-toggle.btn.btn-new{type: 'button', 'data-toggle' => 'dropdown'} - %i.fa.fa-plus - New Milestone - %b.caret - %ul.dropdown-menu.dropdown-menu-align-right - - @projects.each do |project| - - if can?(current_user, :admin_milestone, project) - %li - = link_to new_namespace_project_milestone_path(project.namespace, project) do - = project.name_with_namespace + = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone" = render 'shared/milestones_filter' diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 5a9739a0cd..90ade1e168 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -12,18 +12,7 @@ = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do %i.fa.fa-rss - - if @projects.any? { |project| can?(current_user, :create_issue, project) } - .dropdown.inline.prepend-left-10 - %button.dropdown-toggle.btn.btn-new{type: 'button', 'data-toggle' => 'dropdown'} - %i.fa.fa-plus - New Issue - %b.caret - %ul.dropdown-menu.dropdown-menu-align-right - - @projects.each do |project| - - if can?(current_user, :create_issue, project) - %li - = link_to new_namespace_project_issue_path(project.namespace, project) do - = project.name_with_namespace + = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/issuable/filter', type: :issues diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 95c503a3af..f662f5a8c1 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -3,18 +3,7 @@ .project-issuable-filter .controls - - if @projects.any? { |project| can?(current_user, :create_merge_request, project) } - .dropdown.inline - %button.dropdown-toggle.btn.btn-new{type: 'button', 'data-toggle' => 'dropdown'} - %i.fa.fa-plus - New Merge Request - %b.caret - %ul.dropdown-menu.dropdown-menu-align-right - - @projects.each do |project| - - if can?(current_user, :create_merge_request, project) - %li - = link_to new_namespace_project_merge_request_path(project.namespace, project) do - = project.name_with_namespace + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" = render 'shared/issuable/filter', type: :merge_requests diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml new file mode 100644 index 0000000000..d7243b2d51 --- /dev/null +++ b/app/views/shared/_new_project_item_select.html.haml @@ -0,0 +1,20 @@ +- if @projects.any? + .prepend-left-10.new-project-item-select-holder + = project_select_tag :project_path, class: "new-project-item-select" + %a.btn.btn-new.new-project-item-select-button + = icon('plus') + = local_assigns[:label] + %b.caret + + :javascript + $('.new-project-item-select-button').on('click', function() { + $('.new-project-item-select').select2('open'); + }); + + var relativePath = '#{local_assigns[:path]}'; + + $('.new-project-item-select').on('click', function() { + window.location = $(this).val() + '/' + relativePath; + }); + + new ProjectSelect() From 6fb90a2ca37d2735cda3b904139af1d487bcf125 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 7 Dec 2015 17:24:15 +0100 Subject: [PATCH 364/908] Include groups in dashboard "New Milestone" select. --- .../javascripts/project_select.js.coffee | 27 ++++++++++++++----- .../dashboard/milestones/index.html.haml | 2 +- .../shared/_new_project_item_select.html.haml | 2 +- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee index 43b18a3da5..0ae274f336 100644 --- a/app/assets/javascripts/project_select.js.coffee +++ b/app/assets/javascripts/project_select.js.coffee @@ -2,25 +2,38 @@ class @ProjectSelect constructor: -> $('.ajax-project-select').each (i, select) -> @groupId = $(select).data('group-id') + @includeGroups = $(select).data('include-groups') + + placeholder = "Search for project" + placeholder += " or group" if @includeGroups $(select).select2 - placeholder: "Search for project" - multiple: $(select).hasClass('multiselect') + placeholder: placeholder minimumInputLength: 0 query: (query) => - callback = (projects) -> + finalCallback = (projects) -> data = { results: projects } query.callback(data) - if @groupId - Api.groupProjects @groupId, query.term, callback + if @includeGroups + projectsCallback = (projects) -> + groupsCallback = (groups) -> + data = groups.concat(projects) + finalCallback(data) + + Api.groups query.term, false, groupsCallback else - Api.projects query.term, callback + projectsCallback = finalCallback + + if @groupId + Api.groupProjects @groupId, query.term, projectsCallback + else + Api.projects query.term, projectsCallback id: (project) -> project.web_url text: (project) -> - project.name_with_namespace + project.name_with_namespace || project.name dropdownCssClass: "ajax-project-dropdown" diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 94ff259a33..bec1692a4d 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -3,7 +3,7 @@ .project-issuable-filter .controls - = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone" + = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true = render 'shared/milestones_filter' diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml index d7243b2d51..c4431d6692 100644 --- a/app/views/shared/_new_project_item_select.html.haml +++ b/app/views/shared/_new_project_item_select.html.haml @@ -1,6 +1,6 @@ - if @projects.any? .prepend-left-10.new-project-item-select-holder - = project_select_tag :project_path, class: "new-project-item-select" + = project_select_tag :project_path, class: "new-project-item-select", data: { include_groups: local_assigns[:include_groups] } %a.btn.btn-new.new-project-item-select-button = icon('plus') = local_assigns[:label] From 5cc2315ee8beeed3af251e67108d86ae65d35109 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 7 Dec 2015 19:06:28 +0100 Subject: [PATCH 365/908] Fix migrations for postgres on test environment Signed-off-by: Dmitriy Zaporozhets --- db/migrate/20121220064453_init_schema.rb | 74 +++++++++---------- ...140122112253_create_merge_request_diffs.rb | 9 ++- .../20140903115954_migrate_to_new_shell.rb | 2 + 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/db/migrate/20121220064453_init_schema.rb b/db/migrate/20121220064453_init_schema.rb index 90f5eb08e8..d7644b6847 100644 --- a/db/migrate/20121220064453_init_schema.rb +++ b/db/migrate/20121220064453_init_schema.rb @@ -1,6 +1,6 @@ class InitSchema < ActiveRecord::Migration def up - + create_table "events", force: true do |t| t.string "target_type" t.integer "target_id" @@ -12,14 +12,14 @@ class InitSchema < ActiveRecord::Migration t.integer "action" t.integer "author_id" end - + add_index "events", ["action"], name: "index_events_on_action", using: :btree add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree add_index "events", ["created_at"], name: "index_events_on_created_at", using: :btree add_index "events", ["project_id"], name: "index_events_on_project_id", using: :btree add_index "events", ["target_id"], name: "index_events_on_target_id", using: :btree add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree - + create_table "issues", force: true do |t| t.string "title" t.integer "assignee_id" @@ -33,7 +33,7 @@ class InitSchema < ActiveRecord::Migration t.text "description" t.integer "milestone_id" end - + add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree add_index "issues", ["closed"], name: "index_issues_on_closed", using: :btree @@ -41,7 +41,7 @@ class InitSchema < ActiveRecord::Migration add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree add_index "issues", ["title"], name: "index_issues_on_title", using: :btree - + create_table "keys", force: true do |t| t.integer "user_id" t.datetime "created_at" @@ -51,11 +51,11 @@ class InitSchema < ActiveRecord::Migration t.string "identifier" t.integer "project_id" end - + add_index "keys", ["identifier"], name: "index_keys_on_identifier", using: :btree add_index "keys", ["project_id"], name: "index_keys_on_project_id", using: :btree add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree - + create_table "merge_requests", force: true do |t| t.string "target_branch", null: false t.string "source_branch", null: false @@ -66,13 +66,13 @@ class InitSchema < ActiveRecord::Migration t.boolean "closed", default: false, null: false t.datetime "created_at" t.datetime "updated_at" - t.text "st_commits", limit: 2147483647 - t.text "st_diffs", limit: 2147483647 + t.text "st_commits" + t.text "st_diffs" t.boolean "merged", default: false, null: false t.integer "state", default: 1, null: false t.integer "milestone_id" end - + add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree add_index "merge_requests", ["closed"], name: "index_merge_requests_on_closed", using: :btree @@ -82,7 +82,7 @@ class InitSchema < ActiveRecord::Migration add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree - + create_table "milestones", force: true do |t| t.string "title", null: false t.integer "project_id", null: false @@ -92,10 +92,10 @@ class InitSchema < ActiveRecord::Migration t.datetime "created_at" t.datetime "updated_at" end - + add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree - + create_table "namespaces", force: true do |t| t.string "name", null: false t.string "path", null: false @@ -104,12 +104,12 @@ class InitSchema < ActiveRecord::Migration t.datetime "updated_at" t.string "type" end - + add_index "namespaces", ["name"], name: "index_namespaces_on_name", using: :btree add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree - + create_table "notes", force: true do |t| t.text "note" t.string "noteable_type" @@ -122,13 +122,13 @@ class InitSchema < ActiveRecord::Migration t.string "commit_id" t.integer "noteable_id" end - + add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree - + create_table "projects", force: true do |t| t.string "name" t.string "path" @@ -144,17 +144,17 @@ class InitSchema < ActiveRecord::Migration t.boolean "wiki_enabled", default: true, null: false t.integer "namespace_id" end - + add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree add_index "projects", ["owner_id"], name: "index_projects_on_owner_id", using: :btree - + create_table "protected_branches", force: true do |t| t.integer "project_id", null: false t.string "name", null: false t.datetime "created_at" t.datetime "updated_at" end - + create_table "services", force: true do |t| t.string "type" t.string "title" @@ -165,9 +165,9 @@ class InitSchema < ActiveRecord::Migration t.boolean "active", default: false, null: false t.string "project_url" end - + add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree - + create_table "snippets", force: true do |t| t.string "title" t.text "content" @@ -178,11 +178,11 @@ class InitSchema < ActiveRecord::Migration t.string "file_name" t.datetime "expires_at" end - + add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree - + create_table "taggings", force: true do |t| t.integer "tag_id" t.integer "taggable_id" @@ -192,14 +192,14 @@ class InitSchema < ActiveRecord::Migration t.string "context" t.datetime "created_at" end - + add_index "taggings", ["tag_id"], name: "index_taggings_on_tag_id", using: :btree add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree - + create_table "tags", force: true do |t| t.string "name" end - + create_table "user_team_project_relationships", force: true do |t| t.integer "project_id" t.integer "user_team_id" @@ -207,7 +207,7 @@ class InitSchema < ActiveRecord::Migration t.datetime "created_at" t.datetime "updated_at" end - + create_table "user_team_user_relationships", force: true do |t| t.integer "user_id" t.integer "user_team_id" @@ -216,7 +216,7 @@ class InitSchema < ActiveRecord::Migration t.datetime "created_at" t.datetime "updated_at" end - + create_table "user_teams", force: true do |t| t.string "name" t.string "path" @@ -224,7 +224,7 @@ class InitSchema < ActiveRecord::Migration t.datetime "created_at" t.datetime "updated_at" end - + create_table "users", force: true do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false @@ -255,7 +255,7 @@ class InitSchema < ActiveRecord::Migration t.string "provider" t.string "username" end - + add_index "users", ["admin"], name: "index_users_on_admin", using: :btree add_index "users", ["blocked"], name: "index_users_on_blocked", using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree @@ -263,7 +263,7 @@ class InitSchema < ActiveRecord::Migration add_index "users", ["name"], name: "index_users_on_name", using: :btree add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree add_index "users", ["username"], name: "index_users_on_username", using: :btree - + create_table "users_projects", force: true do |t| t.integer "user_id", null: false t.integer "project_id", null: false @@ -271,11 +271,11 @@ class InitSchema < ActiveRecord::Migration t.datetime "updated_at" t.integer "project_access", default: 0, null: false end - + add_index "users_projects", ["project_access"], name: "index_users_projects_on_project_access", using: :btree add_index "users_projects", ["project_id"], name: "index_users_projects_on_project_id", using: :btree add_index "users_projects", ["user_id"], name: "index_users_projects_on_user_id", using: :btree - + create_table "web_hooks", force: true do |t| t.string "url" t.integer "project_id" @@ -284,7 +284,7 @@ class InitSchema < ActiveRecord::Migration t.string "type", default: "ProjectHook" t.integer "service_id" end - + create_table "wikis", force: true do |t| t.string "title" t.text "content" @@ -294,10 +294,10 @@ class InitSchema < ActiveRecord::Migration t.string "slug" t.integer "user_id" end - + add_index "wikis", ["project_id"], name: "index_wikis_on_project_id", using: :btree add_index "wikis", ["slug"], name: "index_wikis_on_slug", using: :btree - + end def down diff --git a/db/migrate/20140122112253_create_merge_request_diffs.rb b/db/migrate/20140122112253_create_merge_request_diffs.rb index ef592305a2..47bee269f9 100644 --- a/db/migrate/20140122112253_create_merge_request_diffs.rb +++ b/db/migrate/20140122112253_create_merge_request_diffs.rb @@ -2,11 +2,16 @@ class CreateMergeRequestDiffs < ActiveRecord::Migration def change create_table :merge_request_diffs do |t| t.string :state, null: false, default: 'collected' - t.text :st_commits, null: true, limit: 2147483647 - t.text :st_diffs, null: true, limit: 2147483647 + t.text :st_commits, null: true + t.text :st_diffs, null: true t.integer :merge_request_id, null: false t.timestamps end + + if ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/ + change_column :merge_request_diffs, :st_commits, :text, limit: 2147483647 + change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647 + end end end diff --git a/db/migrate/20140903115954_migrate_to_new_shell.rb b/db/migrate/20140903115954_migrate_to_new_shell.rb index 2d83210951..54cbe48960 100644 --- a/db/migrate/20140903115954_migrate_to_new_shell.rb +++ b/db/migrate/20140903115954_migrate_to_new_shell.rb @@ -1,5 +1,7 @@ class MigrateToNewShell < ActiveRecord::Migration def change + return if Rails.env.test? + gitlab_shell_path = Gitlab.config.gitlab_shell.path if system("#{gitlab_shell_path}/bin/create-hooks") puts 'Repositories updated with new hooks' From 419d6fa6b19f2e2ee6429f7db453573155ca2d21 Mon Sep 17 00:00:00 2001 From: Eirik Lygre Date: Mon, 7 Dec 2015 19:24:51 +0100 Subject: [PATCH 366/908] Simplify expression per feedback from @rspeicher --- app/helpers/projects_helper.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 22db8d860e..d061136b7b 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -183,9 +183,7 @@ module ProjectsHelper end def default_clone_protocol - if !current_user - "http" - elsif current_user.require_ssh_key? + if !current_user || current_user.require_ssh_key? "http" else "ssh" From 2cec90254f5753ac1a8b92931613aaa6c9f19cf7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 7 Dec 2015 19:37:05 +0100 Subject: [PATCH 367/908] Dont use cached collection for Repository find_branch and find_tag methods Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/models/repository.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 13207552e8..f1ee21ec36 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ v 8.3.0 (unreleased) - Expose events API with comment information and author info - Fix: Ensure "Remove Source Branch" button is not shown when branch is being deleted. #3583 - Run custom Git hooks when branch is created or deleted. + - Fix bug when simultaneously accepting multiple MRs results in MRs that are of "merged" status, but not merged to the target branch v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) diff --git a/app/models/repository.rb b/app/models/repository.rb index c304955b0b..1d43307e1e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -100,11 +100,11 @@ class Repository end def find_branch(name) - branches.find { |branch| branch.name == name } + raw_repository.branches.find { |branch| branch.name == name } end def find_tag(name) - tags.find { |tag| tag.name == name } + raw_repository.tags.find { |tag| tag.name == name } end def add_branch(user, branch_name, target) From 74d73bd953c0a083cbf8aaf919a7034067542382 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 7 Dec 2015 20:09:30 +0100 Subject: [PATCH 368/908] Fix random failing test Make sure we wait till page reloads after request was merged. Otherwise we get request running which fails next test Signed-off-by: Dmitriy Zaporozhets --- features/project/merge_requests/accept.feature | 6 ++++-- features/steps/project/merge_requests/acceptance.rb | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/features/project/merge_requests/accept.feature b/features/project/merge_requests/accept.feature index 3e6e59a380..9bc2b7c8ec 100644 --- a/features/project/merge_requests/accept.feature +++ b/features/project/merge_requests/accept.feature @@ -8,10 +8,12 @@ Feature: Project Merge Requests Acceptance Given I am on the Merge Request detail page When I click on "Remove source branch" option And I click on Accept Merge Request - Then I should not see the Remove Source Branch button + Then I should see merge request merged + And I should not see the Remove Source Branch button @javascript Scenario: Accepting the Merge Request without removing the source branch Given I am on the Merge Request detail page When I click on Accept Merge Request - Then I should see the Remove Source Branch button + Then I should see merge request merged + And I should see the Remove Source Branch button diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb index 6adecaa838..383c055c4e 100644 --- a/features/steps/project/merge_requests/acceptance.rb +++ b/features/steps/project/merge_requests/acceptance.rb @@ -32,4 +32,8 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps step 'I am signed in as a developer of the project' do login_as(@user) end + + step 'I should see merge request merged' do + expect(page).to have_content('The changes were merged into') + end end From a1d3b8d7fa61f757978e237dd95de8eeb186212c Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 7 Dec 2015 16:50:20 -0500 Subject: [PATCH 369/908] Fix spec failure introduced by 9d03bc6fa31f123e070bab4a58b67dbb008e75e9 --- spec/helpers/application_helper_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 0a64b70d6a..5568f06639 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -278,7 +278,7 @@ describe ApplicationHelper do el = element.next_element expect(el.name).to eq 'script' - expect(el.text).to include "$('.js-timeago').timeago()" + expect(el.text).to include "$('.js-timeago').last().timeago()" end it 'allows the script tag to be excluded' do From d5ea93469b4ec95916361c61876c949f60539211 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 1 Dec 2015 18:45:36 -0500 Subject: [PATCH 370/908] Add custom UrlValidator --- app/models/application_setting.rb | 4 +-- app/models/ci/web_hook.rb | 3 +- app/models/hooks/web_hook.rb | 3 +- app/models/project.rb | 2 +- app/models/project_services/bamboo_service.rb | 7 ++-- .../project_services/drone_ci_service.rb | 29 +++++++-------- .../project_services/external_wiki_service.rb | 6 ++-- .../project_services/teamcity_service.rb | 10 +++--- app/validators/url_validator.rb | 36 +++++++++++++++++++ spec/models/application_setting_spec.rb | 16 +++++++++ 10 files changed, 79 insertions(+), 37 deletions(-) create mode 100644 app/validators/url_validator.rb diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 5ddcf3d9a0..1880ad9f33 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -43,12 +43,12 @@ class ApplicationSetting < ActiveRecord::Base validates :home_page_url, allow_blank: true, - format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, + url: true, if: :home_page_url_column_exist validates :after_sign_out_path, allow_blank: true, - format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" } + url: true validates :admin_notification_email, allow_blank: true, diff --git a/app/models/ci/web_hook.rb b/app/models/ci/web_hook.rb index 7ca16a1bde..0dc15eb668 100644 --- a/app/models/ci/web_hook.rb +++ b/app/models/ci/web_hook.rb @@ -20,8 +20,7 @@ module Ci # HTTParty timeout default_timeout 10 - validates :url, presence: true, - format: { with: URI::regexp(%w(http https)), message: "should be a valid url" } + validates :url, presence: true, url: true def execute(data) parsed_url = URI.parse(url) diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 2caf26cc8c..715ec5908b 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -31,8 +31,7 @@ class WebHook < ActiveRecord::Base # HTTParty timeout default_timeout Gitlab.config.gitlab.webhook_timeout - validates :url, presence: true, - format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" } + validates :url, presence: true, url: true def execute(data, hook_name) parsed_url = URI.parse(url) diff --git a/app/models/project.rb b/app/models/project.rb index 6010770a5f..af034a6692 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -152,7 +152,7 @@ class Project < ActiveRecord::Base validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :path, scope: :namespace_id validates :import_url, - format: { with: /\A#{URI.regexp(%w(ssh git http https))}\z/, message: 'should be a valid url' }, + url: { protocols: %w(ssh git http https) }, if: :external_import? validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index d31b12f539..0a61ad96a0 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -23,10 +23,7 @@ class BambooService < CiService prop_accessor :bamboo_url, :build_key, :username, :password - validates :bamboo_url, - presence: true, - format: { with: /\A#{URI.regexp}\z/ }, - if: :activated? + validates :bamboo_url, presence: true, url: true, if: :activated? validates :build_key, presence: true, if: :activated? validates :username, presence: true, @@ -84,7 +81,7 @@ class BambooService < CiService def supported_events %w(push) end - + def build_info(sha) url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}") diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index 06c3922593..08e5ccb385 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -19,14 +19,11 @@ # class DroneCiService < CiService - + prop_accessor :drone_url, :token, :enable_ssl_verification - validates :drone_url, - presence: true, - format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :activated? - validates :token, - presence: true, - if: :activated? + + validates :drone_url, presence: true, url: true, if: :activated? + validates :token, presence: true, if: :activated? after_save :compose_service_hook, if: :activated? @@ -58,16 +55,16 @@ class DroneCiService < CiService end def merge_request_status_path(iid, sha = nil, ref = nil) - url = [drone_url, - "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}", + url = [drone_url, + "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}", "?access_token=#{token}"] URI.join(*url).to_s end def commit_status_path(sha, ref) - url = [drone_url, - "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}", + url = [drone_url, + "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}", "?branch=#{URI::encode(ref.to_s)}&access_token=#{token}"] URI.join(*url).to_s @@ -114,15 +111,15 @@ class DroneCiService < CiService end def merge_request_page(iid, sha, ref) - url = [drone_url, + url = [drone_url, "gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"] URI.join(*url).to_s end def commit_page(sha, ref) - url = [drone_url, - "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}", + url = [drone_url, + "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}", "?branch=#{URI::encode(ref.to_s)}"] URI.join(*url).to_s @@ -163,10 +160,10 @@ class DroneCiService < CiService end def push_valid?(data) - opened_merge_requests = project.merge_requests.opened.where(source_project_id: project.id, + opened_merge_requests = project.merge_requests.opened.where(source_project_id: project.id, source_branch: Gitlab::Git.ref_name(data[:ref])) - opened_merge_requests.empty? && data[:total_commits_count] > 0 && + opened_merge_requests.empty? && data[:total_commits_count] > 0 && !Gitlab::Git.blank_ref?(data[:after]) end diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index 9c46af7e72..74c57949b4 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -22,10 +22,8 @@ class ExternalWikiService < Service include HTTParty prop_accessor :external_wiki_url - validates :external_wiki_url, - presence: true, - format: { with: /\A#{URI.regexp}\z/ }, - if: :activated? + + validates :external_wiki_url, presence: true, url: true, if: :activated? def title 'External Wiki' diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 0b02246125..29d4236745 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -23,16 +23,16 @@ class TeamcityService < CiService prop_accessor :teamcity_url, :build_type, :username, :password - validates :teamcity_url, - presence: true, - format: { with: /\A#{URI.regexp}\z/ }, if: :activated? + validates :teamcity_url, presence: true, url: true, if: :activated? validates :build_type, presence: true, if: :activated? validates :username, presence: true, - if: ->(service) { service.password? }, if: :activated? + if: ->(service) { service.password? }, + if: :activated? validates :password, presence: true, - if: ->(service) { service.username? }, if: :activated? + if: ->(service) { service.username? }, + if: :activated? attr_accessor :response diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb new file mode 100644 index 0000000000..2848b9cd33 --- /dev/null +++ b/app/validators/url_validator.rb @@ -0,0 +1,36 @@ +# UrlValidator +# +# Custom validator for URLs. +# +# By default, only URLs for the HTTP(S) protocols will be considered valid. +# Provide a `:protocols` option to configure accepted protocols. +# +# Example: +# +# class User < ActiveRecord::Base +# validates :personal_url, url: true +# +# validates :ftp_url, url: { protocols: %w(ftp) } +# +# validates :git_url, url: { protocols: %w(http https ssh git) } +# end +# +class UrlValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless valid_url?(value) + record.errors.add(attribute, "must be a valid URL") + end + end + + private + + def default_options + @default_options ||= { protocols: %w(http https) } + end + + def valid_url?(value) + options = default_options.merge(self.options) + + value =~ /\A#{URI.regexp(options[:protocols])}\z/ + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index dfbac7b400..b67b84959d 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -36,6 +36,22 @@ describe ApplicationSetting, models: true do it { expect(setting).to be_valid } + describe 'validations' do + let(:http) { 'http://example.com' } + let(:https) { 'https://example.com' } + let(:ftp) { 'ftp://example.com' } + + it { is_expected.to allow_value(nil).for(:home_page_url) } + it { is_expected.to allow_value(http).for(:home_page_url) } + it { is_expected.to allow_value(https).for(:home_page_url) } + it { is_expected.not_to allow_value(ftp).for(:home_page_url) } + + it { is_expected.to allow_value(nil).for(:after_sign_out_path) } + it { is_expected.to allow_value(http).for(:after_sign_out_path) } + it { is_expected.to allow_value(https).for(:after_sign_out_path) } + it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) } + end + context 'restricted signup domains' do it 'set single domain' do setting.restricted_signup_domains_raw = 'example.com' From b3200c8c44f2351d88d5d78d7ded3ac06001bd7c Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 1 Dec 2015 18:46:00 -0500 Subject: [PATCH 371/908] Move EmailValidator to app/validators --- {lib => app/validators}/email_validator.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {lib => app/validators}/email_validator.rb (100%) diff --git a/lib/email_validator.rb b/app/validators/email_validator.rb similarity index 100% rename from lib/email_validator.rb rename to app/validators/email_validator.rb From e48391b813d3e5079238aa3f0662e7a46e1b4a54 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 1 Dec 2015 18:53:44 -0500 Subject: [PATCH 372/908] Add custom ColorValidator --- app/models/broadcast_message.rb | 8 ++++---- app/models/label.rb | 4 +--- app/validators/color_validator.rb | 20 ++++++++++++++++++++ features/steps/admin/labels.rb | 2 +- features/steps/project/issues/labels.rb | 2 +- spec/models/broadcast_message_spec.rb | 15 +++++++++++++++ spec/requests/api/labels_spec.rb | 10 +++++----- 7 files changed, 47 insertions(+), 14 deletions(-) create mode 100644 app/validators/color_validator.rb diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index 05f5e97969..ad51470616 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -16,12 +16,12 @@ class BroadcastMessage < ActiveRecord::Base include Sortable - validates :message, presence: true + validates :message, presence: true validates :starts_at, presence: true - validates :ends_at, presence: true + validates :ends_at, presence: true - validates :color, format: { with: /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/ }, allow_blank: true - validates :font, format: { with: /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/ }, allow_blank: true + validates :color, allow_blank: true, color: true + validates :font, allow_blank: true, color: true def self.current where("ends_at > :now AND starts_at < :now", now: Time.zone.now).last diff --git a/app/models/label.rb b/app/models/label.rb index bef6063fe8..220da10a6a 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -27,9 +27,7 @@ class Label < ActiveRecord::Base has_many :label_links, dependent: :destroy has_many :issues, through: :label_links, source: :target, source_type: 'Issue' - validates :color, - format: { with: /\A#[0-9A-Fa-f]{6}\Z/ }, - allow_blank: false + validates :color, color: true, allow_blank: false validates :project, presence: true, unless: Proc.new { |service| service.template? } # Don't allow '?', '&', and ',' for label titles diff --git a/app/validators/color_validator.rb b/app/validators/color_validator.rb new file mode 100644 index 0000000000..571d0007aa --- /dev/null +++ b/app/validators/color_validator.rb @@ -0,0 +1,20 @@ +# ColorValidator +# +# Custom validator for web color codes. It requires the leading hash symbol and +# will accept RGB triplet or hexadecimal formats. +# +# Example: +# +# class User < ActiveRecord::Base +# validates :background_color, allow_blank: true, color: true +# end +# +class ColorValidator < ActiveModel::EachValidator + PATTERN = /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/.freeze + + def validate_each(record, attribute, value) + unless value =~ PATTERN + record.errors.add(attribute, "must be a valid color code") + end + end +end diff --git a/features/steps/admin/labels.rb b/features/steps/admin/labels.rb index 2ea5dffdc6..55ddcc2508 100644 --- a/features/steps/admin/labels.rb +++ b/features/steps/admin/labels.rb @@ -71,7 +71,7 @@ class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps step 'I should see label color error message' do page.within '.label-form' do - expect(page).to have_content 'Color is invalid' + expect(page).to have_content 'Color must be a valid color code' end end diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index e273bb391b..2ab8956867 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -55,7 +55,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps step 'I should see label color error message' do page.within '.label-form' do - expect(page).to have_content 'Color is invalid' + expect(page).to have_content 'Color must be a valid color code' end end diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index d80748f23a..2b325f44f6 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -20,6 +20,21 @@ describe BroadcastMessage do it { is_expected.to be_valid } + describe 'validations' do + let(:triplet) { '#000' } + let(:hex) { '#AABBCC' } + + it { is_expected.to allow_value(nil).for(:color) } + it { is_expected.to allow_value(triplet).for(:color) } + it { is_expected.to allow_value(hex).for(:color) } + it { is_expected.not_to allow_value('000').for(:color) } + + it { is_expected.to allow_value(nil).for(:font) } + it { is_expected.to allow_value(triplet).for(:font) } + it { is_expected.to allow_value(hex).for(:font) } + it { is_expected.not_to allow_value('000').for(:font) } + end + describe :current do it "should return last message if time match" do broadcast_message = create(:broadcast_message, starts_at: Time.now.yesterday, ends_at: Time.now.tomorrow) diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index aff109a942..667f0dbea5 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -47,7 +47,7 @@ describe API::API, api: true do name: 'Foo', color: '#FFAA' expect(response.status).to eq(400) - expect(json_response['message']['color']).to eq(['is invalid']) + expect(json_response['message']['color']).to eq(['must be a valid color code']) end it 'should return 400 for too long color code' do @@ -55,7 +55,7 @@ describe API::API, api: true do name: 'Foo', color: '#FFAAFFFF' expect(response.status).to eq(400) - expect(json_response['message']['color']).to eq(['is invalid']) + expect(json_response['message']['color']).to eq(['must be a valid color code']) end it 'should return 400 for invalid name' do @@ -151,12 +151,12 @@ describe API::API, api: true do expect(json_response['message']['title']).to eq(['is invalid']) end - it 'should return 400 for invalid name' do + it 'should return 400 when color code is too short' do put api("/projects/#{project.id}/labels", user), name: 'label1', color: '#FF' expect(response.status).to eq(400) - expect(json_response['message']['color']).to eq(['is invalid']) + expect(json_response['message']['color']).to eq(['must be a valid color code']) end it 'should return 400 for too long color code' do @@ -164,7 +164,7 @@ describe API::API, api: true do name: 'Foo', color: '#FFAAFFFF' expect(response.status).to eq(400) - expect(json_response['message']['color']).to eq(['is invalid']) + expect(json_response['message']['color']).to eq(['must be a valid color code']) end end end From 96e51a0304022664c06a025f4a54c4a41c25edd2 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 1 Dec 2015 19:21:45 -0500 Subject: [PATCH 373/908] Minor EmailValidator refactor --- app/validators/email_validator.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/validators/email_validator.rb b/app/validators/email_validator.rb index f509f0a584..b35af10080 100644 --- a/app/validators/email_validator.rb +++ b/app/validators/email_validator.rb @@ -1,3 +1,5 @@ +# EmailValidator +# # Based on https://github.com/balexand/email_validator # # Extended to use only strict mode with following allowed characters: @@ -6,15 +8,10 @@ # See http://www.remote.org/jochen/mail/info/chars.html # class EmailValidator < ActiveModel::EachValidator - @@default_options = {} - - def self.default_options - @@default_options - end + PATTERN = /\A\s*([-a-z0-9+._']{1,64})@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*\z/i.freeze def validate_each(record, attribute, value) - options = @@default_options.merge(self.options) - unless value =~ /\A\s*([-a-z0-9+._']{1,64})@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*\z/i + unless value =~ PATTERN record.errors.add(attribute, options[:message] || :invalid) end end From ad6a771dc680b52e4b46c73f20bc39340d08bf32 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 1 Dec 2015 19:30:01 -0500 Subject: [PATCH 374/908] Add custom LineCodeValidator --- app/models/note.rb | 2 +- app/models/sent_notification.rb | 2 +- app/validators/line_code_validator.rb | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 app/validators/line_code_validator.rb diff --git a/app/models/note.rb b/app/models/note.rb index 239a0f77f8..8d433c57ce 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -44,7 +44,7 @@ class Note < ActiveRecord::Base validates :note, :project, presence: true validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award } - validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true + validates :line_code, line_code: true, allow_blank: true # Attachments are deprecated and are handled by Markdown uploader validates :attachment, file_size: { maximum: :max_attachment_size } diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index d8fe65b06f..f36eda1531 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -21,7 +21,7 @@ class SentNotification < ActiveRecord::Base validates :reply_key, uniqueness: true validates :noteable_id, presence: true, unless: :for_commit? validates :commit_id, presence: true, if: :for_commit? - validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true + validates :line_code, line_code: true, allow_blank: true class << self def reply_key diff --git a/app/validators/line_code_validator.rb b/app/validators/line_code_validator.rb new file mode 100644 index 0000000000..ed29e5aeb6 --- /dev/null +++ b/app/validators/line_code_validator.rb @@ -0,0 +1,12 @@ +# LineCodeValidator +# +# Custom validator for GitLab line codes. +class LineCodeValidator < ActiveModel::EachValidator + PATTERN = /\A[a-z0-9]+_\d+_\d+\z/.freeze + + def validate_each(record, attribute, value) + unless value =~ PATTERN + record.errors.add(attribute, "must be a valid line code") + end + end +end From 9321d382bd5a0697e0e15a5065ec274e75541851 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 7 Dec 2015 16:17:12 -0500 Subject: [PATCH 375/908] Add custom NamespaceValidator --- app/models/namespace.rb | 8 +++----- app/models/user.rb | 6 ++---- app/validators/namespace_validator.rb | 22 ++++++++++++++++++++++ spec/models/user_spec.rb | 18 +++++++++++++++++- spec/requests/api/users_spec.rb | 4 ++-- 5 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 app/validators/namespace_validator.rb diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 20b92e68d6..e07c676a9f 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -30,12 +30,10 @@ class Namespace < ActiveRecord::Base validates :description, length: { within: 0..255 } validates :path, - uniqueness: { case_sensitive: false }, - presence: true, length: { within: 1..255 }, - exclusion: { in: Gitlab::Blacklist.path }, - format: { with: Gitlab::Regex.namespace_regex, - message: Gitlab::Regex.namespace_regex_message } + namespace: true, + presence: true, + uniqueness: { case_sensitive: false } delegate :name, to: :owner, allow_nil: true, prefix: true diff --git a/app/models/user.rb b/app/models/user.rb index 719b49b16f..cfed797e72 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -148,11 +148,9 @@ class User < ActiveRecord::Base validates :bio, length: { maximum: 255 }, allow_blank: true validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :username, + namespace: true, presence: true, - uniqueness: { case_sensitive: false }, - exclusion: { in: Gitlab::Blacklist.path }, - format: { with: Gitlab::Regex.namespace_regex, - message: Gitlab::Regex.namespace_regex_message } + uniqueness: { case_sensitive: false } validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true validate :namespace_uniq, if: ->(user) { user.username_changed? } diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb new file mode 100644 index 0000000000..4ab1706abd --- /dev/null +++ b/app/validators/namespace_validator.rb @@ -0,0 +1,22 @@ +# NamespaceValidator +# +# Custom validator for GitLab namespace values. +# +# Values are checked for formatting and exclusion from `Gitlab::Blacklist.path`. +class NamespaceValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless value =~ Gitlab::Regex.namespace_regex + record.errors.add(attribute, Gitlab::Regex.namespace_regex_message) + end + + if blacklisted?(value) + record.errors.add(attribute, "#{value} is a reserved name") + end + end + + private + + def blacklisted?(value) + Gitlab::Blacklist.path.include?(value) + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 4631b12faf..a0f78d3b33 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -91,7 +91,23 @@ describe User do end describe 'validations' do - it { is_expected.to validate_presence_of(:username) } + describe 'username' do + it 'validates presence' do + expect(subject).to validate_presence_of(:username) + end + + it 'rejects blacklisted names' do + user = build(:user, username: 'dashboard') + + expect(user).not_to be_valid + expect(user.errors.values).to eq [['dashboard is a reserved name']] + end + + it 'validates uniqueness' do + expect(subject).to validate_uniqueness_of(:username) + end + end + it { is_expected.to validate_presence_of(:projects_limit) } it { is_expected.to validate_numericality_of(:projects_limit) } it { is_expected.to allow_value(0).for(:projects_limit) } diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index a9ef2fe588..2f609c6333 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -153,7 +153,7 @@ describe API::API, api: true do expect(json_response['message']['projects_limit']). to eq(['must be greater than or equal to 0']) expect(json_response['message']['username']). - to eq([Gitlab::Regex.send(:namespace_regex_message)]) + to eq([Gitlab::Regex.namespace_regex_message]) end it "shouldn't available for non admin users" do @@ -296,7 +296,7 @@ describe API::API, api: true do expect(json_response['message']['projects_limit']). to eq(['must be greater than or equal to 0']) expect(json_response['message']['username']). - to eq([Gitlab::Regex.send(:namespace_regex_message)]) + to eq([Gitlab::Regex.namespace_regex_message]) end context "with existing user" do From 175f482c3cd584ba73c66e65aa180c1107e72913 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 7 Dec 2015 16:17:24 -0500 Subject: [PATCH 376/908] Add custom NamespaceNameValidator --- app/models/namespace.rb | 6 +++--- app/validators/namespace_name_validator.rb | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 app/validators/namespace_name_validator.rb diff --git a/app/models/namespace.rb b/app/models/namespace.rb index e07c676a9f..1c4e101cc1 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -23,10 +23,10 @@ class Namespace < ActiveRecord::Base validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, - presence: true, uniqueness: true, length: { within: 0..255 }, - format: { with: Gitlab::Regex.namespace_name_regex, - message: Gitlab::Regex.namespace_name_regex_message } + namespace_name: true, + presence: true, + uniqueness: true validates :description, length: { within: 0..255 } validates :path, diff --git a/app/validators/namespace_name_validator.rb b/app/validators/namespace_name_validator.rb new file mode 100644 index 0000000000..2e51af2982 --- /dev/null +++ b/app/validators/namespace_name_validator.rb @@ -0,0 +1,10 @@ +# NamespaceNameValidator +# +# Custom validator for GitLab namespace name strings. +class NamespaceNameValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless value =~ Gitlab::Regex.namespace_name_regex + record.errors.add(attribute, Gitlab::Regex.namespace_name_regex_message) + end + end +end From 2379c8beeac600c3352e33fda0c2b4f4f39c8b84 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 7 Dec 2015 16:29:39 -0500 Subject: [PATCH 377/908] Inline Gitlab::Blacklist in NamespaceValidator --- app/validators/namespace_validator.rb | 36 ++++++++++++++++++++++++--- lib/gitlab/blacklist.rb | 34 ------------------------- 2 files changed, 32 insertions(+), 38 deletions(-) delete mode 100644 lib/gitlab/blacklist.rb diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb index 4ab1706abd..10e35ce665 100644 --- a/app/validators/namespace_validator.rb +++ b/app/validators/namespace_validator.rb @@ -2,21 +2,49 @@ # # Custom validator for GitLab namespace values. # -# Values are checked for formatting and exclusion from `Gitlab::Blacklist.path`. +# Values are checked for formatting and exclusion from a list of reserved path +# names. class NamespaceValidator < ActiveModel::EachValidator + RESERVED = %w( + admin + all + assets + ci + dashboard + files + groups + help + hooks + issues + merge_requests + notes + profile + projects + public + repository + s + search + services + snippets + teams + u + unsubscribes + users + ).freeze + def validate_each(record, attribute, value) unless value =~ Gitlab::Regex.namespace_regex record.errors.add(attribute, Gitlab::Regex.namespace_regex_message) end - if blacklisted?(value) + if reserved?(value) record.errors.add(attribute, "#{value} is a reserved name") end end private - def blacklisted?(value) - Gitlab::Blacklist.path.include?(value) + def reserved?(value) + RESERVED.include?(value) end end diff --git a/lib/gitlab/blacklist.rb b/lib/gitlab/blacklist.rb deleted file mode 100644 index 43145e0ee1..0000000000 --- a/lib/gitlab/blacklist.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Gitlab - module Blacklist - extend self - - def path - %w( - admin - dashboard - files - groups - help - profile - projects - search - public - assets - u - s - teams - merge_requests - issues - users - snippets - services - repository - hooks - notes - unsubscribes - all - ci - ) - end - end -end From bdb4945dcf3c899c6a88fd42657a1b3f04baced1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 7 Dec 2015 23:08:22 +0100 Subject: [PATCH 378/908] Fix random failing test - delete attachment Make sure we wait for AJAX request to finish before end test and cleanup database Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/notes.js.coffee | 4 ++-- spec/features/notes_on_merge_requests_spec.rb | 5 +++-- spec/support/wait_for_ajax.rb | 11 +++++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 spec/support/wait_for_ajax.rb diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index dd6cbcfc70..533d00bfb0 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -369,8 +369,8 @@ class @Notes note = $(this).closest(".note") note.find(".note-attachment").remove() note.find(".note-body > .note-text").show() - note.find(".js-note-attachment-delete").hide() - note.find(".note-edit-form").hide() + note.find(".note-header").show() + note.find(".current-note-edit-form").remove() ### Called when clicking on the "reply" button for a diff line. diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index d7cb3b2e86..f0fc6916c4 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe 'Comments', feature: true do include RepoHelpers + include WaitForAjax describe 'On a merge request', js: true, feature: true do let!(:merge_request) { create(:merge_request) } @@ -123,8 +124,8 @@ describe 'Comments', feature: true do it 'removes the attachment div and resets the edit form' do find('.js-note-attachment-delete').click is_expected.not_to have_css('.note-attachment') - expect(find('.current-note-edit-form', visible: false)). - not_to be_visible + is_expected.not_to have_css('.current-note-edit-form') + wait_for_ajax end end end diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb new file mode 100644 index 0000000000..692d219e9f --- /dev/null +++ b/spec/support/wait_for_ajax.rb @@ -0,0 +1,11 @@ +module WaitForAjax + def wait_for_ajax + Timeout.timeout(Capybara.default_wait_time) do + loop until finished_all_ajax_requests? + end + end + + def finished_all_ajax_requests? + page.evaluate_script('jQuery.active').zero? + end +end From 611912fe6870cac43fef9eea389f1229945f186a Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 7 Dec 2015 19:40:46 -0500 Subject: [PATCH 379/908] Store the demodulized reference filter name in data attribute --- .../markdown/filter/reference_gatherer_filter.rb | 2 +- lib/gitlab/markdown/reference_filter.rb | 6 +++--- .../gitlab/markdown/filter/redactor_filter_spec.rb | 14 +++++++------- .../filter/reference_gatherer_filter_spec.rb | 14 +++++++------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/gitlab/markdown/filter/reference_gatherer_filter.rb b/lib/gitlab/markdown/filter/reference_gatherer_filter.rb index 00f983675e..62f241b496 100644 --- a/lib/gitlab/markdown/filter/reference_gatherer_filter.rb +++ b/lib/gitlab/markdown/filter/reference_gatherer_filter.rb @@ -31,7 +31,7 @@ module Gitlab return unless node.has_attribute?('data-reference-filter') reference_type = node.attr('data-reference-filter') - reference_filter = reference_type.constantize + reference_filter = Gitlab::Markdown.const_get(reference_type) return if context[:reference_filter] && reference_filter != context[:reference_filter] diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index 1b741d647d..3b83b8bd8f 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -57,14 +57,14 @@ module Gitlab # Examples: # # data_attribute(project: 1, issue: 2) - # # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\"" + # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\"" # # data_attribute(project: 3, merge_request: 4) - # # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\"" + # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\"" # # Returns a String def data_attribute(attributes = {}) - attributes[:reference_filter] = self.class.name + attributes[:reference_filter] = self.class.name.demodulize attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{value}") }.join(" ") end diff --git a/spec/lib/gitlab/markdown/filter/redactor_filter_spec.rb b/spec/lib/gitlab/markdown/filter/redactor_filter_spec.rb index c177d6950a..7760a1c86f 100644 --- a/spec/lib/gitlab/markdown/filter/redactor_filter_spec.rb +++ b/spec/lib/gitlab/markdown/filter/redactor_filter_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::Markdown::RedactorFilter do user = create(:user) project = create(:empty_project) - link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name) + link = reference_link(project: project.id, reference_filter: 'ReferenceFilter') doc = filter(link, current_user: user) expect(doc.css('a').length).to eq 0 @@ -31,14 +31,14 @@ describe Gitlab::Markdown::RedactorFilter do project = create(:empty_project) project.team << [user, :master] - link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name) + link = reference_link(project: project.id, reference_filter: 'ReferenceFilter') doc = filter(link, current_user: user) expect(doc.css('a').length).to eq 1 end it 'handles invalid Project references' do - link = reference_link(project: 12345, reference_filter: Gitlab::Markdown::ReferenceFilter.name) + link = reference_link(project: 12345, reference_filter: 'ReferenceFilter') expect { filter(link) }.not_to raise_error end @@ -51,7 +51,7 @@ describe Gitlab::Markdown::RedactorFilter do user = create(:user) group = create(:group) - link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter') doc = filter(link, current_user: user) expect(doc.css('a').length).to eq 0 @@ -62,14 +62,14 @@ describe Gitlab::Markdown::RedactorFilter do group = create(:group) group.add_developer(user) - link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter') doc = filter(link, current_user: user) expect(doc.css('a').length).to eq 1 end it 'handles invalid Group references' do - link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter') expect { filter(link) }.not_to raise_error end @@ -79,7 +79,7 @@ describe Gitlab::Markdown::RedactorFilter do it 'allows any User reference' do user = create(:user) - link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter') doc = filter(link) expect(doc.css('a').length).to eq 1 diff --git a/spec/lib/gitlab/markdown/filter/reference_gatherer_filter_spec.rb b/spec/lib/gitlab/markdown/filter/reference_gatherer_filter_spec.rb index 0884f53abe..caa9d5f36c 100644 --- a/spec/lib/gitlab/markdown/filter/reference_gatherer_filter_spec.rb +++ b/spec/lib/gitlab/markdown/filter/reference_gatherer_filter_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::Markdown::ReferenceGathererFilter do project = create(:empty_project) issue = create(:issue, project: project) - link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name) + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') result = pipeline_result(link, current_user: user) expect(result[:references][:issue]).to be_empty @@ -28,14 +28,14 @@ describe Gitlab::Markdown::ReferenceGathererFilter do issue = create(:issue, project: project) project.team << [user, :master] - link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name) + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') result = pipeline_result(link, current_user: user) expect(result[:references][:issue]).to eq([issue]) end it 'handles invalid Project references' do - link = reference_link(project: 12345, issue: 12345, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name) + link = reference_link(project: 12345, issue: 12345, reference_filter: 'IssueReferenceFilter') expect { pipeline_result(link) }.not_to raise_error end @@ -49,7 +49,7 @@ describe Gitlab::Markdown::ReferenceGathererFilter do user = create(:user) group = create(:group) - link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter') result = pipeline_result(link, current_user: user) expect(result[:references][:user]).to be_empty @@ -60,14 +60,14 @@ describe Gitlab::Markdown::ReferenceGathererFilter do group = create(:group) group.add_developer(user) - link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter') result = pipeline_result(link, current_user: user) expect(result[:references][:user]).to eq([user]) end it 'handles invalid Group references' do - link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter') expect { pipeline_result(link) }.not_to raise_error end @@ -77,7 +77,7 @@ describe Gitlab::Markdown::ReferenceGathererFilter do it 'allows any User reference' do user = create(:user) - link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter') result = pipeline_result(link) expect(result[:references][:user]).to eq([user]) From 9b561e7e15723a82e1f0dcf780aeb7fac5ec139b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 8 Dec 2015 02:35:34 +0100 Subject: [PATCH 380/908] Implement languages graph page Signed-off-by: Dmitriy Zaporozhets --- app/controllers/projects/graphs_controller.rb | 20 ++++++++++++ app/views/projects/graphs/_head.html.haml | 2 ++ app/views/projects/graphs/languages.html.haml | 32 +++++++++++++++++++ config/routes.rb | 1 + 4 files changed, 55 insertions(+) create mode 100644 app/views/projects/graphs/languages.html.haml diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 418b92040b..c3942c52c6 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -34,6 +34,26 @@ class Projects::GraphsController < Projects::ApplicationController @charts[:build_times] = Ci::Charts::BuildTime.new(ci_project) end + def languages + @languages = Linguist::Repository.new(@repository.rugged, @repository.rugged.head.target_id).languages + total = @languages.map(&:last).sum + + @languages = @languages.map do |language| + name, share = language + color = Digest::SHA256.hexdigest(name)[0...6] + { + value: (share.to_f * 100 / total).round(2), + label: name, + color: "##{color}", + highlight: "##{color}" + } + end + + @languages.sort! do |x, y| + y[:value] <=> x[:value] + end + end + private def fetch_graph diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index 03d0733f91..a47643bd09 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -3,6 +3,8 @@ = link_to 'Contributors', namespace_project_graph_path = nav_link(action: :commits) do = link_to 'Commits', commits_namespace_project_graph_path + = nav_link(action: :languages) do + = link_to 'Languages', languages_namespace_project_graph_path - if @project.builds_enabled? = nav_link(action: :ci) do = link_to ci_namespace_project_graph_path do diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml new file mode 100644 index 0000000000..a7fab5b6d7 --- /dev/null +++ b/app/views/projects/graphs/languages.html.haml @@ -0,0 +1,32 @@ +- page_title "Languages", "Graphs" += render "header_title" += render 'head' + +.gray-content-block.append-bottom-default + .oneline + Programming languages used in this repository + +.row + .col-md-8 + %canvas#languages-chart{ height: 400 } + .col-md-4 + %ul.bordered-list + - @languages.each do |language| + %li + %span{ style: "color: #{language[:color]}" } + = icon('circle') +   + = language[:label] + .pull-right + = language[:value] + \% + +:javascript + var data = #{@languages.to_json}; + var ctx = $("#languages-chart").get(0).getContext("2d"); + var options = { + scaleOverlay: true, + responsive: true, + maintainAspectRatio: false + } + var myPieChart = new Chart(ctx).Pie(data, options); diff --git a/config/routes.rb b/config/routes.rb index 3b151891a6..59879c401a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -500,6 +500,7 @@ Rails.application.routes.draw do member do get :commits get :ci + get :languages end end From 9b20731d4aa2b6ffce614cd77606809ad0e2a832 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 8 Dec 2015 02:38:59 +0100 Subject: [PATCH 381/908] Add tests and changelog item for language graphs Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + features/project/graph.feature | 5 +++++ features/steps/project/graph.rb | 9 +++++++++ 3 files changed, 15 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 604efe51a3..7253aaef89 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ v 8.3.0 (unreleased) - Fix: Ensure "Remove Source Branch" button is not shown when branch is being deleted. #3583 - Run custom Git hooks when branch is created or deleted. - Fix bug when simultaneously accepting multiple MRs results in MRs that are of "merged" status, but not merged to the target branch + - Add languages page to graphs v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) diff --git a/features/project/graph.feature b/features/project/graph.feature index 2acd65aea5..63793d6f98 100644 --- a/features/project/graph.feature +++ b/features/project/graph.feature @@ -18,3 +18,8 @@ Feature: Project Graph Given project "Shop" has CI enabled When I visit project "Shop" CI graph page Then page should have CI graphs + + @javascript + Scenario: I should see project languages graphs + When I visit project "Shop" languages graph page + Then page should have languages graphs diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb index 98f31f3b76..b09ec86e5d 100644 --- a/features/steps/project/graph.rb +++ b/features/steps/project/graph.rb @@ -14,6 +14,15 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps visit commits_namespace_project_graph_path(project.namespace, project, "master") end + step 'I visit project "Shop" languages graph page' do + visit languages_namespace_project_graph_path(project.namespace, project, "master") + end + + step 'page should have languages graphs' do + expect(page).to have_content "Ruby 66.63 %" + expect(page).to have_content "JavaScript 22.96 %" + end + step 'page should have commits graphs' do expect(page).to have_content "Commit statistics for master" expect(page).to have_content "Commits per day of month" From 2e270c0705895f7535ca3f62abc1c1a9cbff769d Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 1 Dec 2015 16:02:26 +0200 Subject: [PATCH 382/908] add explicit reference to rouge 1.10.1 --- Gemfile | 3 ++- Gemfile.lock | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 25e30e0211..fc4d565fc8 100644 --- a/Gemfile +++ b/Gemfile @@ -99,7 +99,7 @@ gem 'org-ruby', '~> 0.9.12' gem 'creole', '~> 0.5.0' gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 1.5.2' -gem 'net-ssh', '~> 3.0.1' +gem 'rouge', '~> 1.10.1' # Diffs gem 'diffy', '~> 3.0.3' @@ -205,6 +205,7 @@ gem 'raphael-rails', '~> 2.1.2' gem 'request_store', '~> 1.2.0' gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' +gem 'net-ssh', '~> 3.0.1' group :development do gem "foreman" diff --git a/Gemfile.lock b/Gemfile.lock index 9e7b93be3a..5d70788d98 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -940,6 +940,7 @@ DEPENDENCIES request_store (~> 1.2.0) rerun (~> 0.10.0) responders (~> 2.0) + rouge (~> 1.10.1) rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.3.0) rubocop (~> 0.28.0) From 79fb993a6598df4836e5c0ed4e27a72e844429fc Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 14 Nov 2015 20:19:38 +0100 Subject: [PATCH 383/908] Enable rubocop metrics This enables rubocop metrics like CyclomaticComplexity and ABCSize. Initial threshold values are high, should be probably decreased. --- .rubocop.yml | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index d59edbc8b1..fd382182ab 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -735,21 +735,37 @@ Metrics/AbcSize: Description: >- A calculated magnitude based on number of assignments, branches, and conditions. - Enabled: false - -Metrics/BlockNesting: - Description: 'Avoid excessive block nesting' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count' - Enabled: false - -Metrics/ClassLength: - Description: 'Avoid classes longer than 100 lines of code.' - Enabled: false + Enabled: true + Max: 70 Metrics/CyclomaticComplexity: Description: >- A complexity metric that is strongly correlated to the number of test cases needed to validate a method. + Enabled: true + Max: 16 + +Metrics/PerceivedComplexity: + Description: >- + A complexity metric geared towards measuring complexity for a + human reader. + Enabled: true + Max: 16 + +Metrics/ParameterLists: + Description: 'Avoid parameter lists longer than three or four parameters.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params' + Enabled: true + Max: 8 + +Metrics/BlockNesting: + Description: 'Avoid excessive block nesting' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count' + Enabled: true + Max: 4 + +Metrics/ClassLength: + Description: 'Avoid classes longer than 100 lines of code.' Enabled: false Metrics/LineLength: @@ -762,17 +778,6 @@ Metrics/MethodLength: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods' Enabled: false -Metrics/ParameterLists: - Description: 'Avoid parameter lists longer than three or four parameters.' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params' - Enabled: false - -Metrics/PerceivedComplexity: - Description: >- - A complexity metric geared towards measuring complexity for a - human reader. - Enabled: false - #################### Lint ################################ ### Warnings From 8c6db54e1283348f64b46733875db7ffe08993a6 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 17 Nov 2015 13:08:42 +0100 Subject: [PATCH 384/908] Extract repository_push_email to separate class --- app/mailers/emails/projects.rb | 94 +++++----------------- lib/gitlab/email/repository_push.rb | 116 ++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 76 deletions(-) create mode 100644 lib/gitlab/email/repository_push.rb diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index caba63006d..92bca4e618 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -59,85 +59,27 @@ module Emails subject: subject("Project was moved")) end - def repository_push_email(project_id, recipient, author_id: nil, - ref: nil, - action: nil, - compare: nil, - reverse_compare: false, - send_from_committer_email: false, - disable_diffs: false) - unless author_id && ref && action - raise ArgumentError, "missing keywords: author_id, ref, action" - end - - @project = Project.find(project_id) - @current_user = @author = User.find(author_id) - @reverse_compare = reverse_compare - @compare = compare - @ref_name = Gitlab::Git.ref_name(ref) - @ref_type = Gitlab::Git.tag_ref?(ref) ? "tag" : "branch" - @action = action - @disable_diffs = disable_diffs - - if @compare - @commits = Commit.decorate(compare.commits, @project) - @diffs = compare.diffs - end - - @action_name = - case action - when :create - "pushed new" - when :delete - "deleted" - else - "pushed to" - end - - @subject = "[Git]" - @subject << "[#{@project.path_with_namespace}]" - @subject << "[#{@ref_name}]" if action == :push - @subject << " " - - if action == :push - if @commits.length > 1 - @target_url = namespace_project_compare_url(@project.namespace, - @project, - from: Commit.new(@compare.base, @project), - to: Commit.new(@compare.head, @project)) - @subject << "Deleted " if @reverse_compare - @subject << "#{@commits.length} commits: #{@commits.first.title}" - else - @target_url = namespace_project_commit_url(@project.namespace, - @project, @commits.first) - - @subject << "Deleted 1 commit: " if @reverse_compare - @subject << @commits.first.title - end - else - unless action == :delete - @target_url = namespace_project_tree_url(@project.namespace, - @project, @ref_name) - end - - subject_action = @action_name.dup - subject_action[0] = subject_action[0].capitalize - @subject << "#{subject_action} #{@ref_type} #{@ref_name}" - end + def repository_push_email(project_id, recipient, opts = {}) + email = Gitlab::Email::RepositoryPush.new(project_id, recipient, opts) + @project = email.project + @current_user = @author = email.author + @reverse_compare = email.reverse_compare + @compare = email.compare + @ref_name = email.ref_name + @ref_type = email.ref_type + @action = email.action + @disable_diffs = email.disable_diffs + @commits = email.commits + @diffs = email.diffs + @action_name = email.action_name + @target_url = email.target_url @disable_footer = true - reply_to = - if send_from_committer_email && can_send_from_user_email?(@author) - @author.email - else - Gitlab.config.gitlab.email_reply_to - end - - mail(from: sender(author_id, send_from_committer_email), - reply_to: reply_to, - to: recipient, - subject: @subject) + mail(from: sender(email.author_id, email.send_from_committer_email), + reply_to: email.reply_to, + to: email.recipient, + subject: email.subject) end end end diff --git a/lib/gitlab/email/repository_push.rb b/lib/gitlab/email/repository_push.rb new file mode 100644 index 0000000000..f484f3cb76 --- /dev/null +++ b/lib/gitlab/email/repository_push.rb @@ -0,0 +1,116 @@ +module Gitlab + module Email + class RepositoryPush + attr_reader :compare, :reverse_compare, :send_from_cmmitter_email, :disable_diffs, + :action, :ref, :author_id + + def initialize(project_id, recipient, opts = {}) + raise ArgumentError, 'Missing arguments: author_id, ref, action' unless + opts[:author_id] && opts[:ref] && opts[:action] + + @project_id = project_id + @recipient = recipient + + @author_id = opts[:author_id] + @ref = opts[:ref] + @action = opts[:action] + + @compare = opts[:compare] || nil + @reverse_compare = opts[:reverse_compare] || false + @send_from_committer_email = opts[:send_from_committer_email] || false + @disable_diffs = opts[:disable_diffs] || false + + @author = author + @project = project + @commits = commits + @diffs = diffs + @ref_name = ref_name + @ref_type = ref_type + @action_name = action_name + end + + def project + Project.find(@project_id) + end + + def author + User.find(@author_id) + end + + def commits + Commit.decorate(@compare.commits, @project) if @compare + end + + def diffs + @compare.diffs if @compare + end + + def action_name + case @action + when :create + 'pushed new' + when :delete + 'deleted' + else + 'pushed to' + end + end + + def subject + subject_text = '[Git]' + subject_text << "[#{@project.path_with_namespace}]" + subject_text << "[#{@ref_name}]" if @action == :push + subject_text << ' ' + + if @action == :push + if @commits.length > 1 + subject_text << "Deleted " if @reverse_compare + subject_text << "#{@commits.length} commits: #{@commits.first.title}" + else + subject_text << "Deleted 1 commit: " if @reverse_compare + subject_text << @commits.first.title + end + end + + subject_action = @action_name.dup + subject_action[0] = subject_action[0].capitalize + subject_text << "#{subject_action} #{@ref_type} #{@ref_name}" + end + + def ref_name + Gitlab::Git.ref_name(@ref) + end + + def ref_type + Gitlab::Git.tag_ref?(@ref) ? 'tag' : 'branch' + end + + def target_url + if action == :push + if @commits.length > 1 + namespace_project_compare_url(@project.namespace, + @project, + from: Commit.new(@compare.base, @project), + to: Commit.new(@compare.head, @project)) + else + namespace_project_commit_url(@project.namespace, + @project, @commits.first) + end + end + + if action != :delete && action != :push + namespace_project_tree_url(@project.namespace, + @project, @ref_name) + end + end + + def reply_to + if @send_from_committer_email && can_send_from_user_email?(@author) + @author.email + else + Gitlab.config.gitlab.email_reply_to + end + end + end + end +end From 45f7f01f19a6c5f977d71b094cbe5fedb44dc9e2 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 18 Nov 2015 10:12:09 +0100 Subject: [PATCH 385/908] Make `can_send_from_user_email?` public in Notify --- app/mailers/notify.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 50a409c375..0534eb025c 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -33,13 +33,13 @@ class Notify < BaseMailer allowed_domains end - private - def can_send_from_user_email?(sender) sender_domain = sender.email.split("@").last self.class.allowed_email_domains.include?(sender_domain) end + private + # Return an email address that displays the name of the sender. # Only the displayed name changes; the actual email address is always the same. def sender(sender_id, send_from_user_email = false) From e2f937ce22e6b0eb458bbdb3fa93b06d80ecfd21 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 18 Nov 2015 10:13:34 +0100 Subject: [PATCH 386/908] Refactor RepositoryPush, move to Message namespace --- app/mailers/emails/projects.rb | 36 +++--- lib/gitlab/email/message/repository_push.rb | 120 ++++++++++++++++++++ lib/gitlab/email/repository_push.rb | 116 ------------------- 3 files changed, 139 insertions(+), 133 deletions(-) create mode 100644 lib/gitlab/email/message/repository_push.rb delete mode 100644 lib/gitlab/email/repository_push.rb diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 92bca4e618..c303e5a1a9 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -60,26 +60,28 @@ module Emails end def repository_push_email(project_id, recipient, opts = {}) - email = Gitlab::Email::RepositoryPush.new(project_id, recipient, opts) + repository_push = + Gitlab::Email::Message::RepositoryPush.new(self, project_id, recipient, opts) - @project = email.project - @current_user = @author = email.author - @reverse_compare = email.reverse_compare - @compare = email.compare - @ref_name = email.ref_name - @ref_type = email.ref_type - @action = email.action - @disable_diffs = email.disable_diffs - @commits = email.commits - @diffs = email.diffs - @action_name = email.action_name - @target_url = email.target_url + @project = repository_push.project + @current_user = @author = repository_push.author + @reverse_compare = repository_push.reverse_compare + @compare = repository_push.compare + @ref_name = repository_push.ref_name + @ref_type = repository_push.ref_type + @action = repository_push.action + @action_name = repository_push.action_name + @disable_diffs = repository_push.disable_diffs + @commits = repository_push.commits + @diffs = repository_push.diffs + @target_url = repository_push.target_url @disable_footer = true - mail(from: sender(email.author_id, email.send_from_committer_email), - reply_to: email.reply_to, - to: email.recipient, - subject: email.subject) + mail(from: sender(repository_push.author_id, + repository_push.send_from_committer_email), + reply_to: repository_push.reply_to, + to: repository_push.recipient, + subject: repository_push.subject) end end end diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb new file mode 100644 index 0000000000..bb92df9e1b --- /dev/null +++ b/lib/gitlab/email/message/repository_push.rb @@ -0,0 +1,120 @@ +module Gitlab + module Email + module Message + class RepositoryPush + attr_reader :compare, :reverse_compare, :send_from_committer_email, + :disable_diffs, :action, :ref, :author_id + attr_accessor :recipient + + def initialize(notify, project_id, recipient, opts = {}) + raise ArgumentError, 'Missing arguments: author_id, ref, action' unless + opts[:author_id] && opts[:ref] && opts[:action] + + @notify = notify + @project_id = project_id + @recipient = recipient + + @author_id = opts[:author_id] + @ref = opts[:ref] + @action = opts[:action] + + @compare = opts[:compare] || nil + @reverse_compare = opts[:reverse_compare] || false + @send_from_committer_email = opts[:send_from_committer_email] || false + @disable_diffs = opts[:disable_diffs] || false + + @author = author + @project = project + @commits = commits + @diffs = diffs + @ref_name = ref_name + @ref_type = ref_type + @action_name = action_name + end + + def project + Project.find(@project_id) + end + + def author + User.find(@author_id) + end + + def commits + Commit.decorate(@compare.commits, @project) if @compare + end + + def diffs + @compare.diffs if @compare + end + + def action_name + case @action + when :create + 'pushed new' + when :delete + 'deleted' + else + 'pushed to' + end + end + + def subject + subject_text = '[Git]' + subject_text << "[#{@project.path_with_namespace}]" + subject_text << "[#{@ref_name}]" if @action == :push + subject_text << ' ' + + if @action == :push + if @commits.length > 1 + subject_text << "Deleted " if @reverse_compare + subject_text << "#{@commits.length} commits: #{@commits.first.title}" + else + subject_text << "Deleted 1 commit: " if @reverse_compare + subject_text << @commits.first.title + end + else + subject_action = @action_name.dup + subject_action[0] = subject_action[0].capitalize + subject_text << "#{subject_action} #{@ref_type} #{@ref_name}" + end + end + + def ref_name + Gitlab::Git.ref_name(@ref) + end + + def ref_type + Gitlab::Git.tag_ref?(@ref) ? 'tag' : 'branch' + end + + def target_url + if @action == :push + if @commits.length > 1 + @notify.namespace_project_compare_url(@project.namespace, + @project, + from: Commit.new(@compare.base, @project), + to: Commit.new(@compare.head, @project)) + else + @notify.namespace_project_commit_url(@project.namespace, + @project, @commits.first) + end + else + unless @action == :delete + @notify.namespace_project_tree_url(@project.namespace, + @project, @ref_name) + end + end + end + + def reply_to + if @send_from_committer_email && @notify.can_send_from_user_email?(@author) + @author.email + else + Gitlab.config.gitlab.email_reply_to + end + end + end + end + end +end diff --git a/lib/gitlab/email/repository_push.rb b/lib/gitlab/email/repository_push.rb deleted file mode 100644 index f484f3cb76..0000000000 --- a/lib/gitlab/email/repository_push.rb +++ /dev/null @@ -1,116 +0,0 @@ -module Gitlab - module Email - class RepositoryPush - attr_reader :compare, :reverse_compare, :send_from_cmmitter_email, :disable_diffs, - :action, :ref, :author_id - - def initialize(project_id, recipient, opts = {}) - raise ArgumentError, 'Missing arguments: author_id, ref, action' unless - opts[:author_id] && opts[:ref] && opts[:action] - - @project_id = project_id - @recipient = recipient - - @author_id = opts[:author_id] - @ref = opts[:ref] - @action = opts[:action] - - @compare = opts[:compare] || nil - @reverse_compare = opts[:reverse_compare] || false - @send_from_committer_email = opts[:send_from_committer_email] || false - @disable_diffs = opts[:disable_diffs] || false - - @author = author - @project = project - @commits = commits - @diffs = diffs - @ref_name = ref_name - @ref_type = ref_type - @action_name = action_name - end - - def project - Project.find(@project_id) - end - - def author - User.find(@author_id) - end - - def commits - Commit.decorate(@compare.commits, @project) if @compare - end - - def diffs - @compare.diffs if @compare - end - - def action_name - case @action - when :create - 'pushed new' - when :delete - 'deleted' - else - 'pushed to' - end - end - - def subject - subject_text = '[Git]' - subject_text << "[#{@project.path_with_namespace}]" - subject_text << "[#{@ref_name}]" if @action == :push - subject_text << ' ' - - if @action == :push - if @commits.length > 1 - subject_text << "Deleted " if @reverse_compare - subject_text << "#{@commits.length} commits: #{@commits.first.title}" - else - subject_text << "Deleted 1 commit: " if @reverse_compare - subject_text << @commits.first.title - end - end - - subject_action = @action_name.dup - subject_action[0] = subject_action[0].capitalize - subject_text << "#{subject_action} #{@ref_type} #{@ref_name}" - end - - def ref_name - Gitlab::Git.ref_name(@ref) - end - - def ref_type - Gitlab::Git.tag_ref?(@ref) ? 'tag' : 'branch' - end - - def target_url - if action == :push - if @commits.length > 1 - namespace_project_compare_url(@project.namespace, - @project, - from: Commit.new(@compare.base, @project), - to: Commit.new(@compare.head, @project)) - else - namespace_project_commit_url(@project.namespace, - @project, @commits.first) - end - end - - if action != :delete && action != :push - namespace_project_tree_url(@project.namespace, - @project, @ref_name) - end - end - - def reply_to - if @send_from_committer_email && can_send_from_user_email?(@author) - @author.email - else - Gitlab.config.gitlab.email_reply_to - end - end - end - end -end From 4beba7494b096f2540b19017bb7c1c8e91679135 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 20 Nov 2015 15:29:47 +0100 Subject: [PATCH 387/908] Improve Messagee::RepositoryPush --- app/mailers/emails/projects.rb | 6 +- lib/gitlab/email/message/repository_push.rb | 130 ++++++++++---------- 2 files changed, 70 insertions(+), 66 deletions(-) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index c303e5a1a9..bedadb583c 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -65,20 +65,20 @@ module Emails @project = repository_push.project @current_user = @author = repository_push.author - @reverse_compare = repository_push.reverse_compare @compare = repository_push.compare @ref_name = repository_push.ref_name @ref_type = repository_push.ref_type @action = repository_push.action @action_name = repository_push.action_name - @disable_diffs = repository_push.disable_diffs @commits = repository_push.commits @diffs = repository_push.diffs @target_url = repository_push.target_url + @disable_diffs = repository_push.disable_diffs? + @reverse_compare = repository_push.reverse_compare? @disable_footer = true mail(from: sender(repository_push.author_id, - repository_push.send_from_committer_email), + repository_push.send_from_committer_email?), reply_to: repository_push.reply_to, to: repository_push.recipient, subject: repository_push.subject) diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index bb92df9e1b..61962abf82 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -2,118 +2,122 @@ module Gitlab module Email module Message class RepositoryPush - attr_reader :compare, :reverse_compare, :send_from_committer_email, - :disable_diffs, :action, :ref, :author_id attr_accessor :recipient + attr_reader :author_id, :ref, :action def initialize(notify, project_id, recipient, opts = {}) - raise ArgumentError, 'Missing arguments: author_id, ref, action' unless + raise ArgumentError, 'Missing options: author_id, ref, action' unless opts[:author_id] && opts[:ref] && opts[:action] @notify = notify @project_id = project_id @recipient = recipient + @opts = opts - @author_id = opts[:author_id] - @ref = opts[:ref] - @action = opts[:action] - - @compare = opts[:compare] || nil - @reverse_compare = opts[:reverse_compare] || false - @send_from_committer_email = opts[:send_from_committer_email] || false - @disable_diffs = opts[:disable_diffs] || false - - @author = author - @project = project - @commits = commits - @diffs = diffs - @ref_name = ref_name - @ref_type = ref_type - @action_name = action_name + @author_id = opts.delete(:author_id) + @ref = opts.delete(:ref) + @action = opts.delete(:action) end def project - Project.find(@project_id) + @project ||= Project.find(@project_id) end def author - User.find(@author_id) + @author ||= User.find(@author_id) end def commits - Commit.decorate(@compare.commits, @project) if @compare + @commits ||= (Commit.decorate(compare.commits, project) if compare) end def diffs - @compare.diffs if @compare + @diffs ||= (compare.diffs if compare) + end + + def compare + @opts[:compare] + end + + def reverse_compare? + @opts[:reverse_compare] || false + end + + def disable_diffs? + @opts[:disable_diffs] || false + end + + def send_from_committer_email? + @opts[:send_from_committer_email] || false end def action_name - case @action - when :create - 'pushed new' - when :delete - 'deleted' - else - 'pushed to' - end - end - - def subject - subject_text = '[Git]' - subject_text << "[#{@project.path_with_namespace}]" - subject_text << "[#{@ref_name}]" if @action == :push - subject_text << ' ' - - if @action == :push - if @commits.length > 1 - subject_text << "Deleted " if @reverse_compare - subject_text << "#{@commits.length} commits: #{@commits.first.title}" + @action_name ||= + case @action + when :create + 'pushed new' + when :delete + 'deleted' else - subject_text << "Deleted 1 commit: " if @reverse_compare - subject_text << @commits.first.title + 'pushed to' end - else - subject_action = @action_name.dup - subject_action[0] = subject_action[0].capitalize - subject_text << "#{subject_action} #{@ref_type} #{@ref_name}" - end end def ref_name - Gitlab::Git.ref_name(@ref) + @ref_name ||= Gitlab::Git.ref_name(@ref) end def ref_type - Gitlab::Git.tag_ref?(@ref) ? 'tag' : 'branch' + @ref_type ||= Gitlab::Git.tag_ref?(@ref) ? 'tag' : 'branch' end def target_url if @action == :push - if @commits.length > 1 - @notify.namespace_project_compare_url(@project.namespace, - @project, - from: Commit.new(@compare.base, @project), - to: Commit.new(@compare.head, @project)) + if commits.length > 1 && compare + @notify.namespace_project_compare_url(project.namespace, + project, + from: Commit.new(compare.base, project), + to: Commit.new(compare.head, project)) else - @notify.namespace_project_commit_url(@project.namespace, - @project, @commits.first) + @notify.namespace_project_commit_url(project.namespace, + project, commits.first) end else unless @action == :delete - @notify.namespace_project_tree_url(@project.namespace, - @project, @ref_name) + @notify.namespace_project_tree_url(project.namespace, + project, ref_name) end end end def reply_to - if @send_from_committer_email && @notify.can_send_from_user_email?(@author) - @author.email + if send_from_committer_email? && @notify.can_send_from_user_email?(author) + author.email else Gitlab.config.gitlab.email_reply_to end end + + def subject + subject_text = '[Git]' + subject_text << "[#{project.path_with_namespace}]" + subject_text << "[#{ref_name}]" if @action == :push + subject_text << ' ' + + if @action == :push + if commits.length > 1 + subject_text << "Deleted " if reverse_compare? + subject_text << "#{commits.length} commits: #{commits.first.title}" + else + subject_text << "Deleted 1 commit: " if reverse_compare? + subject_text << commits.first.title + end + else + subject_action = action_name.dup + subject_action[0] = subject_action[0].capitalize + subject_text << "#{subject_action} #{ref_type} #{ref_name}" + end + end end end end From 9f2752e5dcc31dc4e9d91ee18caf1d36f1b7684e Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 21 Nov 2015 20:54:19 +0100 Subject: [PATCH 388/908] Remove obsolete variables in `repository_push_email` --- app/mailers/emails/projects.rb | 25 ++++------------- .../notify/repository_push_email.html.haml | 28 ++++++++++--------- .../notify/repository_push_email.text.haml | 24 ++++++++-------- lib/gitlab/email/message/repository_push.rb | 17 +++++++++-- 4 files changed, 46 insertions(+), 48 deletions(-) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index bedadb583c..35015ca34f 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -60,28 +60,13 @@ module Emails end def repository_push_email(project_id, recipient, opts = {}) - repository_push = + @message = Gitlab::Email::Message::RepositoryPush.new(self, project_id, recipient, opts) - @project = repository_push.project - @current_user = @author = repository_push.author - @compare = repository_push.compare - @ref_name = repository_push.ref_name - @ref_type = repository_push.ref_type - @action = repository_push.action - @action_name = repository_push.action_name - @commits = repository_push.commits - @diffs = repository_push.diffs - @target_url = repository_push.target_url - @disable_diffs = repository_push.disable_diffs? - @reverse_compare = repository_push.reverse_compare? - @disable_footer = true - - mail(from: sender(repository_push.author_id, - repository_push.send_from_committer_email?), - reply_to: repository_push.reply_to, - to: repository_push.recipient, - subject: repository_push.subject) + mail(from: sender(@message.author_id, @message.send_from_committer_email?), + reply_to: @message.reply_to, + to: @message.recipient, + subject: @message.subject) end end end diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 12f83aae04..4361f67a74 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -1,30 +1,32 @@ -%h3 #{@author.name} #{@action_name} #{@ref_type} #{@ref_name} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} +%h3 + #{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name} + at #{link_to(@message.project_name_with_namespace, namespace_project_url(@message.project_namespace, @message.project))} -- if @compare - - if @reverse_compare +- if @message.compare + - if @message.reverse_compare? %p %strong WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below. %h4 - = @reverse_compare ? "Deleted commits:" : "Commits:" + = @message.reverse_compare? ? "Deleted commits:" : "Commits:" %ul - - @commits.each do |commit| + - @message.commits.each do |commit| %li - %strong #{link_to commit.short_id, namespace_project_commit_url(@project.namespace, @project, commit)} + %strong #{link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit))} %div %span by #{commit.author_name} %i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} %pre.commit-message = commit.safe_message - %h4 #{pluralize @diffs.count, "changed file"}: + %h4 #{pluralize @message.diffs_count, "changed file"}: %ul - - @diffs.each_with_index do |diff, i| + - @message.diffs.each_with_index do |diff, i| %li.file-stats - %a{href: "#{@target_url if @disable_diffs}#diff-#{i}" } + %a{href: "#{@message.target_url if @message.disable_diffs?}#diff-#{i}" } - if diff.deleted_file %span.deleted-file − @@ -40,11 +42,11 @@ - else = diff.new_path - - unless @disable_diffs + - unless @message.disable_diffs? %h4 Changes: - - @diffs.each_with_index do |diff, i| + - @message.diffs.each_with_index do |diff, i| %li{id: "diff-#{i}"} - %a{href: @target_url + "#diff-#{i}"} + %a{href: @message.target_url + "#diff-#{i}"} - if diff.deleted_file %strong = diff.old_path @@ -62,5 +64,5 @@ = color_email_diff(diff.diff) %br - - if @compare.timeout + - if @message.compare_timeout %h5 Huge diff. To prevent performance issues changes are hidden diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index 97a176ed2a..aa0e263b6d 100644 --- a/app/views/notify/repository_push_email.text.haml +++ b/app/views/notify/repository_push_email.text.haml @@ -1,21 +1,21 @@ -#{@author.name} #{@action_name} #{@ref_type} #{@ref_name} at #{@project.name_with_namespace} -- if @compare +#{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name} at #{@message.project_name_with_namespace} +- if @message.compare \ \ - - if @reverse_compare + - if @message.reverse_compare? WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below. \ \ - = @reverse_compare ? "Deleted commits:" : "Commits:" - - @commits.each do |commit| + = @message.reverse_compare? ? "Deleted commits:" : "Commits:" + - @message.commits.each do |commit| #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} #{commit.safe_message} \- - - - - \ \ - #{pluralize @diffs.count, "changed file"}: + #{pluralize @message.diffs_count, "changed file"}: \ - - @diffs.each do |diff| + - @message.diffs.each do |diff| - if diff.deleted_file \- − #{diff.old_path} - elsif diff.renamed_file @@ -24,11 +24,11 @@ \- + #{diff.new_path} - else \- #{diff.new_path} - - unless @disable_diffs + - unless @message.disable_diffs? \ \ Changes: - - @diffs.each do |diff| + - @message.diffs.each do |diff| \ \===================================== - if diff.deleted_file @@ -39,11 +39,11 @@ = diff.new_path \===================================== != diff.diff - - if @compare.timeout + - if @message.compare_timeout \ \ Huge diff. To prevent performance issues it was hidden - - if @target_url + - if @message.target_url \ \ - View it on GitLab: #{@target_url} + View it on GitLab: #{@message.target_url} diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 61962abf82..b0af3feee9 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -5,6 +5,9 @@ module Gitlab attr_accessor :recipient attr_reader :author_id, :ref, :action + delegate :namespace, :name_with_namespace, to: :project, prefix: :project + delegate :name, to: :author, prefix: :author + def initialize(notify, project_id, recipient, opts = {}) raise ArgumentError, 'Missing options: author_id, ref, action' unless opts[:author_id] && opts[:ref] && opts[:action] @@ -35,10 +38,18 @@ module Gitlab @diffs ||= (compare.diffs if compare) end + def diffs_count + diffs.count if diffs + end + def compare @opts[:compare] end + def compare_timeout + compare.timeout if compare + end + def reverse_compare? @opts[:reverse_compare] || false end @@ -74,17 +85,17 @@ module Gitlab def target_url if @action == :push if commits.length > 1 && compare - @notify.namespace_project_compare_url(project.namespace, + @notify.namespace_project_compare_url(project_namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) else - @notify.namespace_project_commit_url(project.namespace, + @notify.namespace_project_commit_url(project_namespace, project, commits.first) end else unless @action == :delete - @notify.namespace_project_tree_url(project.namespace, + @notify.namespace_project_tree_url(project_namespace, project, ref_name) end end From d835fbc79f6cd6f17fc7472af48074805622a573 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 22 Nov 2015 20:59:31 +0100 Subject: [PATCH 389/908] Fix url helpers in RepositoryPush --- lib/gitlab/email/message/repository_push.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index b0af3feee9..bc8aece733 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -16,6 +16,7 @@ module Gitlab @project_id = project_id @recipient = recipient @opts = opts + @urls = Gitlab::Application.routes.url_helpers @author_id = opts.delete(:author_id) @ref = opts.delete(:ref) @@ -85,17 +86,17 @@ module Gitlab def target_url if @action == :push if commits.length > 1 && compare - @notify.namespace_project_compare_url(project_namespace, + @urls.namespace_project_compare_url(project_namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) else - @notify.namespace_project_commit_url(project_namespace, + @urls.namespace_project_commit_url(project_namespace, project, commits.first) end else unless @action == :delete - @notify.namespace_project_tree_url(project_namespace, + @urls.namespace_project_tree_url(project_namespace, project, ref_name) end end From 360a96a299397cbb5f8b15f916a9efd5962ff6be Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 23 Nov 2015 20:25:49 +0000 Subject: [PATCH 390/908] Fix specs by adding forgotten instance variable --- app/mailers/emails/projects.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 35015ca34f..b96418679b 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -63,6 +63,9 @@ module Emails @message = Gitlab::Email::Message::RepositoryPush.new(self, project_id, recipient, opts) + # used in notify layout + @target_url = @message.target_url + mail(from: sender(@message.author_id, @message.send_from_committer_email?), reply_to: @message.reply_to, to: @message.recipient, From 75c6b29f6b15e164717c27e6c3d7eecb84c923f8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 3 Dec 2015 14:16:16 +0100 Subject: [PATCH 391/908] Add `RepositoryPush` specs --- lib/gitlab/email/message/repository_push.rb | 10 +- .../email/message/repository_push_spec.rb | 122 ++++++++++++++++++ 2 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 spec/lib/gitlab/email/message/repository_push_spec.rb diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index bc8aece733..3526eb2cad 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -87,17 +87,17 @@ module Gitlab if @action == :push if commits.length > 1 && compare @urls.namespace_project_compare_url(project_namespace, - project, - from: Commit.new(compare.base, project), - to: Commit.new(compare.head, project)) + project, + from: Commit.new(compare.base, project), + to: Commit.new(compare.head, project)) else @urls.namespace_project_commit_url(project_namespace, - project, commits.first) + project, commits.first) end else unless @action == :delete @urls.namespace_project_tree_url(project_namespace, - project, ref_name) + project, ref_name) end end end diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb new file mode 100644 index 0000000000..56ae2a8d12 --- /dev/null +++ b/spec/lib/gitlab/email/message/repository_push_spec.rb @@ -0,0 +1,122 @@ +require 'spec_helper' + +describe Gitlab::Email::Message::RepositoryPush do + include RepoHelpers + + let!(:group) { create(:group, name: 'my_group') } + let!(:project) { create(:project, name: 'my_project', namespace: group) } + let!(:author) { create(:author, name: 'Author') } + + let(:message) do + described_class.new(Notify, project.id, 'recipient@example.com', opts) + end + + context 'new commits have been pushed to repository' do + let(:opts) do + { author_id: author.id, ref: 'master', action: :push, compare: compare, + send_from_committer_email: true } + end + let(:compare) do + Gitlab::Git::Compare.new(project.repository.raw_repository, + sample_image_commit.id, sample_commit.id) + end + + describe '#project' do + subject { message.project } + it { is_expected.to eq project } + it { is_expected.to be_an_instance_of Project } + end + + describe '#project_namespace' do + subject { message.project_namespace } + it { is_expected.to eq group } + it { is_expected.to be_kind_of Namespace } + end + + describe '#project_name_with_namespace' do + subject { message.project_name_with_namespace } + it { is_expected.to eq 'my_group / my_project' } + end + + describe '#author' do + subject { message.author } + it { is_expected.to eq author } + it { is_expected.to be_an_instance_of User } + end + + describe '#author_name' do + subject { message.author_name } + it { is_expected.to eq 'Author' } + end + + describe '#commits' do + subject { message.commits } + it { is_expected.to be_kind_of Array } + it { is_expected.to all(be_instance_of Commit) } + end + + describe '#diffs' do + subject { message.diffs } + it { is_expected.to all(be_an_instance_of Gitlab::Git::Diff) } + end + + describe '#diffs_count' do + subject { message.diffs_count } + it { is_expected.to eq compare.diffs.count } + end + + describe '#compare' do + subject { message.compare } + it { is_expected.to be_an_instance_of Gitlab::Git::Compare } + end + + describe '#compare_timeout' do + subject { message.compare_timeout } + it { is_expected.to eq compare.timeout } + end + + describe '#reverse_compare?' do + subject { message.reverse_compare? } + it { is_expected.to eq false } + end + + describe '#disable_diffs?' do + subject { message.disable_diffs? } + it { is_expected.to eq false } + end + + describe '#send_from_committer_email?' do + subject { message.send_from_committer_email? } + it { is_expected.to eq true } + end + + describe '#action_name' do + subject { message.action_name } + it { is_expected.to eq 'pushed to' } + end + + describe '#ref_name' do + subject { message.ref_name } + it { is_expected.to eq 'master' } + end + + describe '#ref_type' do + subject { message.ref_type } + it { is_expected.to eq 'branch' } + end + + describe '#target_url' do + subject { message.target_url } + it { is_expected.to include 'compare' } + it { is_expected.to include compare.commits.first.parents.first.id } + it { is_expected.to include compare.commits.last.id } + end + + describe '#subject' do + subject { message.subject } + it { is_expected.to include "[Git][#{project.path_with_namespace}]" } + it { is_expected.to include "#{compare.commits.length} commits" } + it { is_expected.to include compare.commits.first.message.split("\n").first } + end + end +end From 7e9109c43bdaedb79b06ba8948cd729ac5f68deb Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 4 Dec 2015 13:07:42 +0100 Subject: [PATCH 392/908] Move `common_mentionable_setup` to shared context in specs --- spec/support/mentionable_shared_examples.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index 33d2b14583..fce91015fd 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -4,7 +4,7 @@ # - let(:backref_text) { "the way that +subject+ should refer to itself in backreferences " } # - let(:set_mentionable_text) { lambda { |txt| "block that assigns txt to the subject's mentionable_text" } } -def common_mentionable_setup +shared_context 'mentionable context' do let(:project) { subject.project } let(:author) { subject.author } @@ -56,7 +56,7 @@ def common_mentionable_setup end shared_examples 'a mentionable' do - common_mentionable_setup + include_context 'mentionable context' it 'generates a descriptive back-reference' do expect(subject.gfm_reference).to eq(backref_text) @@ -88,7 +88,7 @@ shared_examples 'a mentionable' do end shared_examples 'an editable mentionable' do - common_mentionable_setup + include_context 'mentionable context' it_behaves_like 'a mentionable' From 591035968dc96acd27155ced4c0ae04649fcd113 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 4 Dec 2015 12:55:19 +0000 Subject: [PATCH 393/908] Duplicate options in `RepositoryPush` --- lib/gitlab/email/message/repository_push.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 3526eb2cad..ca89b67e58 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -15,12 +15,12 @@ module Gitlab @notify = notify @project_id = project_id @recipient = recipient - @opts = opts + @opts = opts.dup @urls = Gitlab::Application.routes.url_helpers - @author_id = opts.delete(:author_id) - @ref = opts.delete(:ref) - @action = opts.delete(:action) + @author_id = @opts.delete(:author_id) + @ref = @opts.delete(:ref) + @action = @opts.delete(:action) end def project From 66f658a9b543b1493f625b2f44f3f845d64b749d Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 4 Dec 2015 14:11:17 +0100 Subject: [PATCH 394/908] Check if commits are available in `RepositoryPush` --- lib/gitlab/email/message/repository_push.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index ca89b67e58..c53148d2ed 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -84,8 +84,8 @@ module Gitlab end def target_url - if @action == :push - if commits.length > 1 && compare + if @action == :push && commits + if commits.length > 1 @urls.namespace_project_compare_url(project_namespace, project, from: Commit.new(compare.base, project), @@ -116,7 +116,7 @@ module Gitlab subject_text << "[#{ref_name}]" if @action == :push subject_text << ' ' - if @action == :push + if @action == :push && commits if commits.length > 1 subject_text << "Deleted " if reverse_compare? subject_text << "#{commits.length} commits: #{commits.first.title}" From 652de0b820587983e0af76186db4570b536d7ce3 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 7 Dec 2015 10:43:07 +0100 Subject: [PATCH 395/908] Refactor CI YAML processor's validators --- lib/ci/gitlab_ci_yaml_processor.rb | 68 ++++++++++++-------- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 6 +- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 3beafcad11..9c11fc3c81 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -132,26 +132,36 @@ module Ci end def validate_job!(name, job) + validate_job_name!(name) + validate_job_keys!(name, job) + validate_job_types!(name, job) + + validate_job_stage!(name, job) if job[:stage] + validate_job_cache!(name, job) if job[:cache] + validate_job_artifacts!(name, job) if job[:artifacts] + end + + private + + def validate_job_name!(name) if name.blank? || !validate_string(name) raise ValidationError, "job name should be non-empty string" end + end + def validate_job_keys!(name, job) job.keys.each do |key| unless ALLOWED_JOB_KEYS.include? key raise ValidationError, "#{name} job: unknown parameter #{key}" end end + end + def validate_job_types!(name, job) if !validate_string(job[:script]) && !validate_array_of_strings(job[:script]) raise ValidationError, "#{name} job: script should be a string or an array of a strings" end - if job[:stage] - unless job[:stage].is_a?(String) && job[:stage].in?(stages) - raise ValidationError, "#{name} job: stage parameter should be #{stages.join(", ")}" - end - end - if job[:image] && !validate_string(job[:image]) raise ValidationError, "#{name} job: image should be a string" end @@ -172,26 +182,6 @@ module Ci raise ValidationError, "#{name} job: except parameter should be an array of strings" end - if job[:cache] - if job[:cache][:untracked] && !validate_boolean(job[:cache][:untracked]) - raise ValidationError, "#{name} job: cache:untracked parameter should be an boolean" - end - - if job[:cache][:paths] && !validate_array_of_strings(job[:cache][:paths]) - raise ValidationError, "#{name} job: cache:paths parameter should be an array of strings" - end - end - - if job[:artifacts] - if job[:artifacts][:untracked] && !validate_boolean(job[:artifacts][:untracked]) - raise ValidationError, "#{name} job: artifacts:untracked parameter should be an boolean" - end - - if job[:artifacts][:paths] && !validate_array_of_strings(job[:artifacts][:paths]) - raise ValidationError, "#{name} job: artifacts:paths parameter should be an array of strings" - end - end - if job[:allow_failure] && !validate_boolean(job[:allow_failure]) raise ValidationError, "#{name} job: allow_failure parameter should be an boolean" end @@ -201,7 +191,31 @@ module Ci end end - private + def validate_job_stage!(name, job) + unless job[:stage].is_a?(String) && job[:stage].in?(stages) + raise ValidationError, "#{name} job: stage parameter should be #{stages.join(", ")}" + end + end + + def validate_job_cache!(name, job) + if job[:cache][:untracked] && !validate_boolean(job[:cache][:untracked]) + raise ValidationError, "#{name} job: cache:untracked parameter should be an boolean" + end + + if job[:cache][:paths] && !validate_array_of_strings(job[:cache][:paths]) + raise ValidationError, "#{name} job: cache:paths parameter should be an array of strings" + end + end + + def validate_job_artifacts!(name, job) + if job[:artifacts][:untracked] && !validate_boolean(job[:artifacts][:untracked]) + raise ValidationError, "#{name} job: artifacts:untracked parameter should be an boolean" + end + + if job[:artifacts][:paths] && !validate_array_of_strings(job[:artifacts][:paths]) + raise ValidationError, "#{name} job: artifacts:paths parameter should be an array of strings" + end + end def validate_array_of_strings(values) values.is_a?(Array) && values.all? { |value| validate_string(value) } diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 6f287719ba..0d50f20ae3 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -532,21 +532,21 @@ module Ci end it "returns errors if job stage is not a string" do - config = YAML.dump({ rspec: { script: "test", type: 1, allow_failure: "string" } }) + config = YAML.dump({ rspec: { script: "test", type: 1 } }) expect do GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") end it "returns errors if job stage is not a pre-defined stage" do - config = YAML.dump({ rspec: { script: "test", type: "acceptance", allow_failure: "string" } }) + config = YAML.dump({ rspec: { script: "test", type: "acceptance" } }) expect do GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") end it "returns errors if job stage is not a defined stage" do - config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", type: "acceptance", allow_failure: "string" } }) + config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", type: "acceptance" } }) expect do GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test") From 6ffe8a06fdf1ffc40e60a487a730b50c4699907d Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 7 Dec 2015 10:52:22 +0100 Subject: [PATCH 396/908] Bump cyclomatic and perceived complexity threshold by one --- .rubocop.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index fd382182ab..b4ca11c834 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -743,14 +743,14 @@ Metrics/CyclomaticComplexity: A complexity metric that is strongly correlated to the number of test cases needed to validate a method. Enabled: true - Max: 16 + Max: 17 Metrics/PerceivedComplexity: Description: >- A complexity metric geared towards measuring complexity for a human reader. Enabled: true - Max: 16 + Max: 17 Metrics/ParameterLists: Description: 'Avoid parameter lists longer than three or four parameters.' From 9bf51ae47d344c5939f275bb5ae429023456d30e Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 8 Dec 2015 10:58:15 +0100 Subject: [PATCH 397/908] Fix specs caused by update of gitlab-test repo. --- features/steps/project/source/browse_files.rb | 1 + spec/controllers/projects/raw_controller_spec.rb | 2 +- spec/support/test_env.rb | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 2edbca9b7f..02d46e2823 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -306,6 +306,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I click on "files/lfs/lfs_object.iso" file in repo' do + visit namespace_project_tree_path(@project.namespace, @project, "lfs") click_link 'files' click_link "lfs" click_link "lfs_object.iso" diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index 2329c246da..cd42a59275 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -35,7 +35,7 @@ describe Projects::RawController do end context 'lfs object' do - let(:id) { 'master/files/lfs/lfs_object.iso' } + let(:id) { 'be93687/files/lfs/lfs_object.iso' } let!(:lfs_object) { create(:lfs_object, oid: '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', size: '1575078') } context 'when project has access' do diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 47c67475cd..985bd141e6 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -12,7 +12,8 @@ module TestEnv 'fix' => '48f0be4', 'improve/awesome' => '5937ac0', 'markdown' => '0ed8c6c', - 'master' => 'be93687', + 'lfs' => 'be93687', + 'master' => '5937ac0', "'test'" => 'e56497b', } From 13f44822d97e643b55047cf619f7bb1f354f165b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 12:29:48 +0100 Subject: [PATCH 398/908] Move CombinedPipeline methods around --- lib/gitlab/markdown/combined_pipeline.rb | 12 +++++++----- lib/gitlab/markdown/pipeline/full_pipeline.rb | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/markdown/combined_pipeline.rb b/lib/gitlab/markdown/combined_pipeline.rb index a3d717550f..6b08a5e9f7 100644 --- a/lib/gitlab/markdown/combined_pipeline.rb +++ b/lib/gitlab/markdown/combined_pipeline.rb @@ -7,16 +7,18 @@ module Gitlab Class.new(Pipeline) do const_set :PIPELINES, pipelines + def self.pipelines + self::PIPELINES + end + def self.filters pipelines.flat_map(&:filters) end def self.transform_context(context) - pipelines.reduce(context) { |context, pipeline| pipeline.transform_context(context) } - end - - def self.pipelines - self::PIPELINES + pipelines.reduce(context) do |context, pipeline| + pipeline.transform_context(context) + end end end end diff --git a/lib/gitlab/markdown/pipeline/full_pipeline.rb b/lib/gitlab/markdown/pipeline/full_pipeline.rb index 553e9367c1..b3b7a3c27c 100644 --- a/lib/gitlab/markdown/pipeline/full_pipeline.rb +++ b/lib/gitlab/markdown/pipeline/full_pipeline.rb @@ -3,7 +3,7 @@ require 'gitlab/markdown' module Gitlab module Markdown class FullPipeline < CombinedPipeline.new(PlainMarkdownPipeline, GfmPipeline) - + end end end From c64a881a40b86d7ed1018daf7d59ffa1bda99ea1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 12:53:02 +0100 Subject: [PATCH 399/908] Allow center top menu to span multiple lines --- app/assets/stylesheets/framework/common.scss | 12 +++++++++--- app/assets/stylesheets/framework/mobile.scss | 3 --- app/assets/stylesheets/pages/builds.scss | 5 ----- app/views/projects/builds/show.html.haml | 2 +- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index d2f491daf7..cafcfc308e 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -333,7 +333,7 @@ table { } .well { - margin-bottom: 0; + margin-bottom: $gl-padding; } .search_box { @@ -379,9 +379,8 @@ table { text-align: center; margin-top: 5px; margin-bottom: $gl-padding; - height: 56px; + height: auto; margin-top: -$gl-padding; - padding-top: $gl-padding; &.no-bottom { margin-bottom: 0; @@ -390,6 +389,13 @@ table { &.no-top { margin-top: 0; } + + li a { + display: inline-block; + padding-top: $gl-padding; + padding-bottom: 11px; + margin-bottom: -1px; + } } .center-middle-menu { diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index cea47fba19..6f44c32373 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -82,9 +82,6 @@ } .center-top-menu { - height: 45px; - margin-bottom: 30px; - li a { font-size: 14px; padding: 19px 10px; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index da9965f007..3c2997c1d5 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -67,9 +67,4 @@ color: #3084bb !important; } } - - .build-top-menu { - margin-top: 0; - margin-bottom: 2px; - } } diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 907e1ce10b..5a55cb8f8d 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -11,7 +11,7 @@ #up-build-trace - if @commit.matrix_for_ref?(@build.ref) - %ul.center-top-menu.build-top-menu + %ul.center-top-menu.no-top.no-bottom - @commit.latest_builds_for_ref(@build.ref).each do |build| %li{class: ('active' if build == @build) } = link_to namespace_project_build_path(@project.namespace, @project, build) do From 507fdb4b2bf67419613abff150de5b4283faaeff Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 12:53:57 +0100 Subject: [PATCH 400/908] Use consistent padding in panel heading --- app/assets/stylesheets/framework/panels.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index 406aff3d72..61053aff91 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -1,9 +1,11 @@ .panel { margin-bottom: $gl-padding; - + .panel-heading { - padding: 10px $gl-padding; + padding: 7px $gl-padding; + line-height: 42px !important; } + .panel-body { padding: $gl-padding; From badeb82f63e416eb7a7c3bf7def7fc3d5ba10222 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 12:54:13 +0100 Subject: [PATCH 401/908] Use consistent padding for .gitlab-ci.yml info callouts --- app/assets/stylesheets/framework/callout.scss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index f3ce4e3c21..20a9bfb981 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -7,8 +7,8 @@ /* Common styles for all types */ .bs-callout { - margin: 20px 0; - padding: 20px; + margin: $gl-padding 0; + padding: $gl-padding; border-left: 3px solid $border-color; color: $text-color; background: $background-color; @@ -42,4 +42,3 @@ border-color: #5cA64d; color: #3c763d; } - From 3eb209123de65fceec260914fc59f772043b174d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 12:54:32 +0100 Subject: [PATCH 402/908] Render monospace text at 90% of the parent font-size --- app/assets/stylesheets/framework/typography.scss | 1 + app/assets/stylesheets/pages/merge_requests.scss | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index aef338cfa5..c3e4ad0ad0 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -220,6 +220,7 @@ pre { .monospace { font-family: $monospace_font; + font-size: 90%; } code { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index f21ad694d0..6a1d3bd19d 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -136,7 +136,7 @@ font-family: $monospace_font; font-weight: bold; overflow: hidden; - font-size: 14px; + font-size: 90%; margin: 0 3px; } From 86a09cfaf1f1b8106f1538e8bf5a1aac5f086554 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 12:55:38 +0100 Subject: [PATCH 403/908] `builds_enabled` rather than `ci_enabled` --- app/controllers/projects/application_controller.rb | 2 +- app/controllers/projects/graphs_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index d3f926b62b..c2aaf094e6 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -28,7 +28,7 @@ class Projects::ApplicationController < ApplicationController private - def ci_enabled + def builds_enabled return render_404 unless @project.builds_enabled? end diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 418b92040b..734697839c 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -5,7 +5,7 @@ class Projects::GraphsController < Projects::ApplicationController before_action :require_non_empty_project before_action :assign_ref_vars before_action :authorize_download_code! - before_action :ci_enabled, only: :ci + before_action :builds_enabled, only: :ci def show respond_to do |format| From 53a6f0b19472e86a8571a1ee35eb3a2b86264a3d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 12:55:53 +0100 Subject: [PATCH 404/908] Fix profile private token form actions --- app/views/profiles/accounts/show.html.haml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 319bdd57c3..17e47c622c 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -26,11 +26,11 @@ - else %span You don`t have one yet. Click generate to fix it. - .form-actions - - if current_user.private_token - = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default btn-build-token" - - else - = f.submit 'Generate', class: "btn btn-default btn-build-token" + .form-actions + - if current_user.private_token + = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default" + - else + = f.submit 'Generate', class: "btn btn-default" - unless current_user.ldap_user? .panel.panel-default From f8fdec687f29b2d3ae6405725696fea65967214e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 12:56:12 +0100 Subject: [PATCH 405/908] Fix padding around protected branches well --- app/views/projects/protected_branches/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 2541105b00..cfd7e1534c 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -3,7 +3,7 @@ %p.light Keep stable branches secure and force developers to use Merge Requests %hr -.well.append-bottom-20 +.well %p Protected branches are designed to %ul %li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"} From f523c3c6856ea3cee883579dfe5a9ad63274fd89 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 12:56:27 +0100 Subject: [PATCH 406/908] Use gray content block for network graph header --- app/views/projects/network/_head.html.haml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/views/projects/network/_head.html.haml b/app/views/projects/network/_head.html.haml index 415c98ec6a..9e0e0dc6bb 100644 --- a/app/views/projects/network/_head.html.haml +++ b/app/views/projects/network/_head.html.haml @@ -1,3 +1,6 @@ -.append-bottom-20 - = render partial: 'shared/ref_switcher', locals: {destination: 'graph'} - .pull-right.visible-lg.light You can move around the graph by using the arrow keys. +.gray-content-block.top-block.append-bottom-default + .tree-ref-holder + = render partial: 'shared/ref_switcher', locals: {destination: 'graph'} + + .oneline + You can move around the graph by using the arrow keys. From 3f1e72a0cc18ac20481224c41cb2ad30dfbe7ab1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 12:57:03 +0100 Subject: [PATCH 407/908] Memoize ci_yaml_file. --- app/models/ci/commit.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 971e899de8..cb90b0de63 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -199,7 +199,7 @@ module Ci end def ci_yaml_file - gl_project.repository.blob_at(sha, '.gitlab-ci.yml').data + @ci_yaml_file ||= gl_project.repository.blob_at(sha, '.gitlab-ci.yml').data rescue nil end From 90a46535935115cc3556d6bc0d282938758cd29a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 12:57:30 +0100 Subject: [PATCH 408/908] Remove top border from build page --- app/views/projects/builds/show.html.haml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 5a55cb8f8d..dc6f9a82b5 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -2,10 +2,9 @@ = render "header_title" .build-page - .gray-content-block + .gray-content-block.top-block Build ##{@build.id} for commit - %strong.monospace - = link_to @build.commit.short_sha, ci_status_path(@build.commit) + %strong.monospace= link_to @build.commit.short_sha, ci_status_path(@build.commit) from = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref) @@ -22,7 +21,6 @@ - else = build.id - - if @build.retried? %li.active %a @@ -31,7 +29,7 @@ %i.fa.fa-warning This build was retried. - .gray-content-block.second-block + .gray-content-block.middle-block .build-head .clearfix = ci_status_with_icon(@build.status) @@ -140,7 +138,7 @@ %h4.title Commit .pull-right - %small + %small = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace" %p %span.attr-name Branch: @@ -162,7 +160,7 @@ - if @builds.present? .build-widget - %h4.title #{pluralize(@builds.count(:id), "other build")} for + %h4.title #{pluralize(@builds.count(:id), "other build")} for = succeed ":" do = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace" %table.table.builds From 1b5302f8e7050c5c4cd4e7d4cc63be3cd3fbbcb5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 12:57:49 +0100 Subject: [PATCH 409/908] Reduce MR/issue duplication --- app/views/projects/issues/_discussion.html.haml | 5 +---- app/views/projects/merge_requests/_discussion.html.haml | 4 ++-- .../show => shared/issuable}/_participants.html.haml | 3 ++- 3 files changed, 5 insertions(+), 7 deletions(-) rename app/views/{projects/merge_requests/show => shared/issuable}/_participants.html.haml (67%) diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index b5f522f207..21eee70b42 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -12,10 +12,7 @@ .col-md-9 .votes-holder.pull-right #votes= render 'votes/votes_block', votable: @issue - .participants - %span= pluralize(@participants.count, 'participant') - - @participants.each do |participant| - = link_to_member(@project, participant, name: false, size: 24) + = render "shared/issuable/participants" .col-md-3 .input-group.cross-project-reference %span#cross-project-reference.slead.has_tooltip{title: 'Cross-project reference'} diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index ea46256166..3d7bd78dce 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -12,7 +12,7 @@ .col-md-9 .votes-holder.pull-right #votes= render 'votes/votes_block', votable: @merge_request - = render "projects/merge_requests/show/participants" + = render "shared/issuable/participants" .col-md-3 .input-group.cross-project-reference %span#cross-project-reference.slead.has_tooltip{title: 'Cross-project reference'} @@ -21,7 +21,7 @@ .row %section.col-md-9 - = render "projects/notes/notes_with_form" + .voting_notes#notes= render "projects/notes/notes_with_form" %aside.col-md-3 .issuable-affix .context diff --git a/app/views/projects/merge_requests/show/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml similarity index 67% rename from app/views/projects/merge_requests/show/_participants.html.haml rename to app/views/shared/issuable/_participants.html.haml index c67afe963e..b4e0def48b 100644 --- a/app/views/projects/merge_requests/show/_participants.html.haml +++ b/app/views/shared/issuable/_participants.html.haml @@ -1,4 +1,5 @@ .participants - %span #{@participants.count} participants + %span + = pluralize @participants.count, "participant" - @participants.each do |participant| = link_to_member(@project, participant, name: false, size: 24) From b53769aedc30d40730ba73928c6dbd61cd358ef0 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 12:58:30 +0100 Subject: [PATCH 410/908] Change "Cancel all" to "Cancel running" --- app/views/projects/builds/index.html.haml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index dab7164153..742676305a 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -3,10 +3,10 @@ .project-issuable-filter .controls - - if @ci_project && current_user && can?(current_user, :manage_builds, @project) + - if @ci_project && can?(current_user, :manage_builds, @project) .pull-left.hidden-xs - if @all_builds.running_or_pending.any? - = link_to 'Cancel all', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post + = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post %ul.center-top-menu %li{class: ('active' if @scope.nil?)} @@ -50,4 +50,3 @@ = render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, allow_retry: true = paginate @builds, theme: 'gitlab' - From 526e64e0c4f43c21cdaa17969be30f2ff0c3793b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 12:58:51 +0100 Subject: [PATCH 411/908] Show "#123" instead of "Builds #123" to make column smaller --- app/views/projects/commit_statuses/_commit_status.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 9a0e7bff3f..615296a575 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -5,9 +5,9 @@ %td.commit_status-link - if commit_status.target_url = link_to commit_status.target_url do - %strong Build ##{commit_status.id} + %strong ##{commit_status.id} - else - %strong Build ##{commit_status.id} + %strong ##{commit_status.id} - if commit_status.show_warning? %i.fa.fa-warning.text-warning From 35fd7112bfc65d5773a64359f108d3c252885479 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 12:59:13 +0100 Subject: [PATCH 412/908] Link builds list status to build details --- .../projects/commit_statuses/_commit_status.html.haml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 615296a575..a527bb2f84 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -1,6 +1,11 @@ %tr.commit_status %td.status - = ci_status_with_icon(commit_status.status) + - if commit_status.target_url + = link_to commit_status.target_url, class: "ci-status ci-#{commit_status.status}" do + = ci_icon_for_status(commit_status.status) + = commit_status.status + - else + = ci_status_with_icon(commit_status.status) %td.commit_status-link - if commit_status.target_url From 9907a7e6ed7dbfac4c927cefd16ac8e4b3c681f6 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 13:02:13 +0100 Subject: [PATCH 413/908] Get ci_commit in MR controller --- app/controllers/projects/merge_requests_controller.rb | 2 ++ .../projects/merge_requests/widget/_heading.html.haml | 9 ++++----- .../merge_requests/widget/open/_accept.html.haml | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 3f47f2ddb2..c5fb49de46 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -264,6 +264,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request_diff = @merge_request.merge_request_diff + @ci_commit = @merge_request.ci_commit + if @merge_request.locked_long_ago? @merge_request.unlock_mr @merge_request.close diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index ba5ad22bca..49aab96171 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,13 +1,12 @@ -- ci_commit = @merge_request.ci_commit -- if ci_commit - - status = ci_commit.status +- if @ci_commit + - status = @ci_commit.status .mr-widget-heading .ci_widget{class: "ci-#{status}"} - = ci_status_icon(ci_commit) + = ci_status_icon(@ci_commit) %span CI build #{status} for #{@merge_request.last_commit_short_sha}. %span.ci-coverage - = link_to "View build details", ci_status_path(ci_commit) + = link_to "View build details", ci_status_path(@ci_commit) - elsif @merge_request.has_ci? - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 9b31014b58..6d12af1614 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -1,4 +1,4 @@ -- status_class = @merge_request.ci_commit ? " ci-#{@merge_request.ci_commit.status}" : nil +- status_class = @ci_commit ? " ci-#{@ci_commit.status}" : nil = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-requires-input' } do |f| = hidden_field_tag :authenticity_token, form_authenticity_token From a17ba43bfd05cd49bab18d6c7f80226004870bc2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 13:03:28 +0100 Subject: [PATCH 414/908] Move commit builds to partial --- app/controllers/projects/commit_controller.rb | 8 +-- app/views/projects/commit/_builds.html.haml | 67 ++++++++++++++++++ app/views/projects/commit/_ci_menu.html.haml | 2 +- app/views/projects/commit/builds.html.haml | 68 +------------------ 4 files changed, 73 insertions(+), 72 deletions(-) create mode 100644 app/views/projects/commit/_builds.html.haml diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 3f137440e2..e8af205b78 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -37,7 +37,7 @@ class Projects::CommitController < Projects::ApplicationController def cancel_builds ci_commit.builds.running_or_pending.each(&:cancel) - redirect_to builds_namespace_project_commit_path(project.namespace, project, commit.sha) + redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha) end def retry_builds @@ -47,7 +47,7 @@ class Projects::CommitController < Projects::ApplicationController end end - redirect_to builds_namespace_project_commit_path(project.namespace, project, commit.sha) + redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha) end def branches @@ -74,8 +74,8 @@ class Projects::CommitController < Projects::ApplicationController end @notes_count = commit.notes.count - - @builds = ci_commit.builds if ci_commit + + @statuses = ci_commit.statuses if ci_commit end def authorize_manage_builds! diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml new file mode 100644 index 0000000000..e4d81182c1 --- /dev/null +++ b/app/views/projects/commit/_builds.html.haml @@ -0,0 +1,67 @@ +.gray-content-block.middle-block + .pull-right + - if @ci_project && can?(current_user, :manage_builds, @ci_commit.gl_project) + - if @ci_commit.builds.latest.failed.any?(&:retryable?) + = link_to "Retry failed", retry_builds_namespace_project_commit_path(@ci_commit.gl_project.namespace, @ci_commit.gl_project, @ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post + + - if @ci_commit.builds.running_or_pending.any? + = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@ci_commit.gl_project.namespace, @ci_commit.gl_project, @ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post + + .oneline + = pluralize @statuses.count(:id), "build" + - if defined?(link_to_commit) && link_to_commit + for commit + = link_to @ci_commit.short_sha, namespace_project_commit_path(@ci_commit.gl_project.namespace, @ci_commit.gl_project, @ci_commit.sha), class: "monospace" + - if @ci_commit.duration > 0 + in + = time_interval_in_words @ci_commit.duration + +- if @ci_commit.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - @ci_commit.yaml_errors.split(",").each do |error| + %li= error + +- if @ci_commit.gl_project.builds_enabled? && !@ci_commit.ci_yaml_file + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit + +.table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Ref + %th Stage + %th Name + %th Duration + %th Finished at + - if @ci_project && @ci_project.coverage_enabled? + %th Coverage + %th + - @ci_commit.refs.each do |ref| + = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered, + locals: { coverage: @ci_project.try(:coverage_enabled?), stage: true, allow_retry: true } + +- if @ci_commit.retried.any? + .gray-content-block.second-block + Retried builds + + .table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Ref + %th Stage + %th Name + %th Duration + %th Finished at + - if @ci_project && @ci_project.coverage_enabled? + %th Coverage + %th + = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, + locals: { coverage: @ci_project.try(:coverage_enabled?), stage: true } diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index 76dc87a882..f74f8b427e 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -6,4 +6,4 @@ = nav_link(path: 'commit#builds') do = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do Builds - %span.badge= @builds.count(:id) + %span.badge= @statuses.count diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml index 00cf9c7610..99d62503a9 100644 --- a/app/views/projects/commit/builds.html.haml +++ b/app/views/projects/commit/builds.html.haml @@ -3,70 +3,4 @@ = render "commit_box" = render "ci_menu" - -- if @ci_commit.yaml_errors.present? - .bs-callout.bs-callout-danger - %h4 Found errors in your .gitlab-ci.yml: - %ul - - @ci_commit.yaml_errors.split(",").each do |error| - %li= error - -- unless @ci_commit.ci_yaml_file - .bs-callout.bs-callout-warning - \.gitlab-ci.yml not found in this commit - -.gray-content-block.second-block - Latest builds - - .pull-right - - if @ci_commit.duration > 0 - %i.fa.fa-time - #{time_interval_in_words @ci_commit.duration} - -   - - - if @ci_project && current_user && can?(current_user, :manage_builds, @project) - - if @ci_commit.builds.latest.failed.any?(&:retryable?) - = link_to "Retry failed", retry_builds_namespace_project_commit_path(@project.namespace, @project, @commit.sha), class: 'btn btn-xs btn-primary', method: :post - - - if @ci_commit.builds.running_or_pending.any? - = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@project.namespace, @project, @commit.sha), class: 'btn btn-xs btn-danger', method: :post - -.table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Ref - %th Stage - %th Name - %th Duration - %th Finished at - - if @ci_project && @ci_project.coverage_enabled? - %th Coverage - %th - - @ci_commit.refs.each do |ref| - = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered, - locals: { coverage: @ci_project.try(:coverage_enabled?), stage: true, allow_retry: true } - -- if @ci_commit.retried.any? - .gray-content-block.second-block - Retried builds - - .table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Ref - %th Stage - %th Name - %th Duration - %th Finished at - - if @ci_project && @ci_project.coverage_enabled? - %th Coverage - %th - = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, - locals: { coverage: @ci_project.try(:coverage_enabled?), stage: true } += render "builds" From 1567572e79bbeace4a68f00c01e64ed0dad9106a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 13:04:24 +0100 Subject: [PATCH 415/908] Add Builds tab to MR detail page --- .../javascripts/merge_request_tabs.js.coffee | 22 +++++++++- .../projects/merge_requests_controller.rb | 41 +++++++++++++------ .../merge_requests/_new_submit.html.haml | 8 ++++ .../projects/merge_requests/_show.html.haml | 10 ++++- .../merge_requests/show/_builds.html.haml | 1 + .../merge_requests/widget/_heading.html.haml | 2 +- config/routes.rb | 1 + 7 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 app/views/projects/merge_requests/show/_builds.html.haml diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 593a8f4213..69a12fdd04 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -43,6 +43,7 @@ # class @MergeRequestTabs diffsLoaded: false + buildsLoaded: false commitsLoaded: false constructor: (@opts = {}) -> @@ -54,6 +55,12 @@ class @MergeRequestTabs bindEvents: -> $(document).on 'shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', @tabShown + $(document).on 'click', '.js-show-tab', @showTab + + showTab: (event) => + event.preventDefault() + + @activateTab $(event.target).data('action') tabShown: (event) => $target = $(event.target) @@ -61,6 +68,8 @@ class @MergeRequestTabs if action == 'commits' @loadCommits($target.attr('href')) + else if action == 'builds' + @loadBuilds($target.attr('href')) else if action == 'diffs' @loadDiff($target.attr('href')) @@ -101,7 +110,7 @@ class @MergeRequestTabs action = 'notes' if action == 'show' # Remove a trailing '/commits' or '/diffs' - new_state = @_location.pathname.replace(/\/(commits|diffs)(\.html)?\/?$/, '') + new_state = @_location.pathname.replace(/\/(commits|builds|diffs)(\.html)?\/?$/, '') # Append the new action if we're on a tab other than 'notes' unless action == 'notes' @@ -129,6 +138,17 @@ class @MergeRequestTabs @commitsLoaded = true @scrollToElement("#commits") + loadBuilds: (source) -> + return if @buildsLoaded + + @_get + url: "#{source}.json" + success: (data) => + document.getElementById('builds').innerHTML = data.html + $('.js-timeago').timeago() + @buildsLoaded = true + @scrollToElement("#builds") + loadDiff: (source) -> return if @diffsLoaded diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 3f47f2ddb2..04642294cd 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -1,13 +1,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ - :edit, :update, :show, :diffs, :commits, :merge, :merge_check, + :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check, :ci_status, :toggle_subscription ] - before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits] - before_action :validates_merge_request, only: [:show, :diffs, :commits] - before_action :define_show_vars, only: [:show, :diffs, :commits] - before_action :ensure_ref_fetched, only: [:show, :commits, :diffs] + before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds] + before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] + before_action :define_show_vars, only: [:show, :diffs, :commits, :builds] + before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds] # Allow read any merge_request before_action :authorize_read_merge_request! @@ -79,6 +79,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def builds + @ci_project = @merge_request.source_project.gitlab_ci_project + + respond_to do |format| + format.html { render 'show' } + format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_builds') } } + end + end + def new params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute @@ -91,20 +100,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController @target_project = merge_request.target_project @source_project = merge_request.source_project - @commits = @merge_request.compare_commits + @commits = @merge_request.compare_commits.reverse @commit = @merge_request.last_commit @first_commit = @merge_request.first_commit @diffs = @merge_request.compare_diffs + + @ci_project = @source_project.gitlab_ci_project + @ci_commit = @merge_request.ci_commit + @statuses = @ci_commit.statuses if @ci_commit + @note_counts = Note.where(commit_id: @commits.map(&:id)). group(:commit_id).count end - def edit - @source_project = @merge_request.source_project - @target_project = @merge_request.target_project - @target_branches = @merge_request.target_project.repository.branch_names - end - def create @target_branches ||= [] @merge_request = MergeRequests::CreateService.new(project, current_user, merge_request_params).execute @@ -118,6 +126,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def edit + @source_project = @merge_request.source_project + @target_project = @merge_request.target_project + @target_branches = @merge_request.target_project.repository.branch_names + end + def update @merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request) @@ -264,6 +278,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request_diff = @merge_request.merge_request_diff + @ci_commit = @merge_request.ci_commit + @statuses = @ci_commit.statuses if @ci_commit + if @merge_request.locked_long_ago? @merge_request.unlock_mr @merge_request.close diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 156922cea4..f0a821d2d9 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -23,6 +23,11 @@ = link_to url_for(params), data: {target: '#commits', action: 'commits', toggle: 'tab'} do Commits %span.badge= @commits.size + - if @ci_commit + %li.builds-tab.active + = link_to url_for(params), data: {target: '#builds', action: 'builds', toggle: 'tab'} do + Builds + %span.badge= @statuses.size %li.diffs-tab.active = link_to url_for(params), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do Changes @@ -31,6 +36,9 @@ .tab-content #commits.commits.tab-pane = render "projects/merge_requests/show/commits" + - if @ci_commit + #builds.builds.tab-pane + = render "projects/merge_requests/show/builds" #diffs.diffs.tab-pane.active - if @diffs.present? = render "projects/diffs/diffs", diffs: @diffs, project: @project diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index f5aff0877e..79d71a784c 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -26,8 +26,7 @@ %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) .normal %span Request to merge - %span.label-branch - = source_branch_with_namespace(@merge_request) + %span.label-branch= source_branch_with_namespace(@merge_request) %span into = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do = @merge_request.target_branch @@ -51,6 +50,11 @@ = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#commits', action: 'commits', toggle: 'tab'} do Commits %span.badge= @commits.size + - if @ci_commit + %li.builds-tab + = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do + Builds + %span.badge= @statuses.size %li.diffs-tab = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do Changes @@ -61,6 +65,8 @@ = render "projects/merge_requests/discussion" #commits.commits.tab-pane - # This tab is always loaded via AJAX + #builds.builds.tab-pane + - # This tab is always loaded via AJAX #diffs.diffs.tab-pane - # This tab is always loaded via AJAX diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml new file mode 100644 index 0000000000..307a75d02c --- /dev/null +++ b/app/views/projects/merge_requests/show/_builds.html.haml @@ -0,0 +1 @@ += render "projects/commit/builds", link_to_commit: true diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index ba5ad22bca..f277014f84 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -7,7 +7,7 @@ %span CI build #{status} for #{@merge_request.last_commit_short_sha}. %span.ci-coverage - = link_to "View build details", ci_status_path(ci_commit) + = link_to "View build details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} - elsif @merge_request.has_ci? - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX diff --git a/config/routes.rb b/config/routes.rb index 3b151891a6..38f0b16a41 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -570,6 +570,7 @@ Rails.application.routes.draw do resources :merge_requests, constraints: { id: /\d+/ }, except: [:destroy] do member do get :diffs + get :builds get :commits post :merge get :merge_check From 14969ac87fb83a647572e32c4a0bf0b5d08a487e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 13:17:05 +0100 Subject: [PATCH 416/908] Add '#' to build page title --- app/views/projects/builds/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index dc6f9a82b5..d5e81f84b5 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@build.name} (#{@build.id})", "Builds" +- page_title "#{@build.name} (##{@build.id})", "Builds" = render "header_title" .build-page From c767d0e6bd95e73217a92040d568b34e14b65949 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 13:04:47 +0100 Subject: [PATCH 417/908] Change text of MR widget heading --- app/assets/stylesheets/pages/merge_requests.scss | 4 ++++ .../merge_requests/widget/_heading.html.haml | 15 ++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index f21ad694d0..ab7df97876 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -81,6 +81,10 @@ &.ci-error { color: $gl-danger; } + + a.monospace { + color: inherit; + } } .mr-widget-body, diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index f277014f84..e94b07eaea 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -4,10 +4,12 @@ .mr-widget-heading .ci_widget{class: "ci-#{status}"} = ci_status_icon(ci_commit) - %span CI build #{status} - for #{@merge_request.last_commit_short_sha}. + %span Build #{status} + for + = succeed "." do + = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace" %span.ci-coverage - = link_to "View build details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} + = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} - elsif @merge_request.has_ci? - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX @@ -21,10 +23,13 @@ - else = icon("circle") %span CI build #{status} - for #{@merge_request.last_commit_short_sha}. + for + - commit = @merge_request.last_commit + = succeed "." do + = link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace" %span.ci-coverage - if ci_build_details_path(@merge_request) - = link_to "View build details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + = link_to "View details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" .ci_widget = icon("spinner spin") From af6b5437421106caf34719e37d359808b88eb45c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 13:04:59 +0100 Subject: [PATCH 418/908] Make commit and MR ref filters aware of /builds path --- lib/gitlab/markdown/commit_reference_filter.rb | 11 +++++++++++ lib/gitlab/markdown/merge_request_reference_filter.rb | 8 +++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb index b4036578e6..e3066a89b0 100644 --- a/lib/gitlab/markdown/commit_reference_filter.rb +++ b/lib/gitlab/markdown/commit_reference_filter.rb @@ -47,6 +47,17 @@ module Gitlab def object_link_title(commit) commit.link_title end + + def object_link_text_extras(object, matches) + extras = super + + path = matches[:path] if matches.names.include?("path") + if path == '/builds' + extras.unshift "builds" + end + + extras + end end end end diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb index de71fc76a9..79d67870b1 100644 --- a/lib/gitlab/markdown/merge_request_reference_filter.rb +++ b/lib/gitlab/markdown/merge_request_reference_filter.rb @@ -24,8 +24,14 @@ module Gitlab def object_link_text_extras(object, matches) extras = super - if matches.names.include?("path") && matches[:path] && matches[:path] == '/diffs' + path = matches[:path] if matches.names.include?("path") + case path + when '/diffs' extras.unshift "diffs" + when '/builds' + extras.unshift "builds" + when '/commits' + extras.unshift "commits" end extras From ea5b462a1408f9fcd77dfd2961049057fc8ccad9 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 8 Dec 2015 13:33:12 +0100 Subject: [PATCH 419/908] Stub the calls to disk and check what send_file returns. --- spec/controllers/projects/raw_controller_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index cd42a59275..42c6d4f3f8 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -41,16 +41,18 @@ describe Projects::RawController do context 'when project has access' do before do public_project.lfs_objects << lfs_object + allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true) + allow(controller).to receive(:send_file) { controller.render :nothing => true } end it 'serves the file' do + expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", :filename => "lfs_object.iso", :disposition => 'attachment') get(:show, namespace_id: public_project.namespace.to_param, project_id: public_project.to_param, id: id) expect(response.status).to eq(200) - expect(response.header['Content-Type']).to eq('application/octet-stream') end end From 0c3f70acf4838194f517c02874b8423303c21b48 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 13:34:09 +0100 Subject: [PATCH 420/908] Add API group projects specs --- spec/requests/api/groups_spec.rb | 59 ++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 13cced8187..4cfa49d156 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -10,6 +10,8 @@ describe API::API, api: true do let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } let!(:group1) { create(:group, avatar: File.open(avatar_file_path)) } let!(:group2) { create(:group) } + let!(:project1) { create(:project, namespace: group1) } + let!(:project2) { create(:project, namespace: group2) } before do group1.add_owner(user1) @@ -67,7 +69,7 @@ describe API::API, api: true do it "should return any existing group" do get api("/groups/#{group2.id}", admin) expect(response.status).to eq(200) - json_response['name'] == group2.name + expect(json_response['name']).to eq(group2.name) end it "should not return a non existing group" do @@ -80,7 +82,7 @@ describe API::API, api: true do it 'should return any existing group' do get api("/groups/#{group1.path}", admin) expect(response.status).to eq(200) - json_response['name'] == group2.name + expect(json_response['name']).to eq(group1.name) end it 'should not return a non existing group' do @@ -95,6 +97,59 @@ describe API::API, api: true do end end + describe "GET /groups/:id/projects" do + context "when authenticated as user" do + it "should return the group's projects" do + get api("/groups/#{group1.id}/projects", user1) + expect(response.status).to eq(200) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(project1.name) + end + + it "should not return a non existing group" do + get api("/groups/1328/projects", user1) + expect(response.status).to eq(404) + end + + it "should not return a group not attached to user1" do + get api("/groups/#{group2.id}/projects", user1) + expect(response.status).to eq(403) + end + end + + context "when authenticated as admin" do + it "should return any existing group" do + get api("/groups/#{group2.id}/projects", admin) + expect(response.status).to eq(200) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(project2.name) + end + + it "should not return a non existing group" do + get api("/groups/1328/projects", admin) + expect(response.status).to eq(404) + end + end + + context 'when using group path in URL' do + it 'should return any existing group' do + get api("/groups/#{group1.path}/projects", admin) + expect(response.status).to eq(200) + expect(json_response.first['name']).to eq(project1.name) + end + + it 'should not return a non existing group' do + get api('/groups/unknown/projects', admin) + expect(response.status).to eq(404) + end + + it 'should not return a group not attached to user1' do + get api("/groups/#{group2.path}/projects", user1) + expect(response.status).to eq(403) + end + end + end + describe "POST /groups" do context "when authenticated as user without group permissions" do it "should not create group" do From 1a10945066d0da1801bb4cf89ce5f54996f1756f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 13:40:23 +0100 Subject: [PATCH 421/908] Fix RedactorFilter --- lib/gitlab/markdown/filter/redactor_filter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/markdown/filter/redactor_filter.rb b/lib/gitlab/markdown/filter/redactor_filter.rb index bea714a01e..33ef7ce18b 100644 --- a/lib/gitlab/markdown/filter/redactor_filter.rb +++ b/lib/gitlab/markdown/filter/redactor_filter.rb @@ -27,7 +27,7 @@ module Gitlab def user_can_reference?(node) if node.has_attribute?('data-reference-filter') reference_type = node.attr('data-reference-filter') - reference_filter = reference_type.constantize + reference_filter = Gitlab::Markdown.const_get(reference_type) reference_filter.user_can_reference?(current_user, node, context) else From f36fe92fcbda02ec130889cc508cab5812fd2b20 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 8 Dec 2015 04:43:17 -0800 Subject: [PATCH 422/908] Bump gollum-lib to 4.1.0 and fix dependency mismatch with rouge Closes #3767 --- CHANGELOG | 1 + Gemfile | 2 +- Gemfile.lock | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 604efe51a3..0eb5e63a1a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) + - Bump gollum-lib to 4.1.0 (Stan Hu) - Fix API setting of 'public' attribute to false will make a project private (Stan Hu) - Handle and report SSL errors in Web hook test (Stan Hu) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) diff --git a/Gemfile b/Gemfile index fc4d565fc8..91ad4b6fb4 100644 --- a/Gemfile +++ b/Gemfile @@ -52,7 +52,7 @@ gem "gitlab_git", '~> 7.2.20' gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap" # Git Wiki -gem 'gollum-lib', '~> 4.0.2' +gem 'gollum-lib', '~> 4.1.0' # Language detection gem "github-linguist", "~> 4.7.0", require: "linguist" diff --git a/Gemfile.lock b/Gemfile.lock index 5d70788d98..d8858bd1f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -329,11 +329,11 @@ GEM activesupport (>= 4.1.0) gollum-grit_adapter (1.0.0) gitlab-grit (~> 2.7, >= 2.7.1) - gollum-lib (4.0.3) + gollum-lib (4.1.0) github-markup (~> 1.3.3) gollum-grit_adapter (~> 1.0) nokogiri (~> 1.6.4) - rouge (~> 1.10.1) + rouge (~> 1.9) sanitize (~> 2.1.0) stringex (~> 2.5.1) gon (6.0.1) @@ -884,7 +884,7 @@ DEPENDENCIES gitlab_git (~> 7.2.20) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) - gollum-lib (~> 4.0.2) + gollum-lib (~> 4.1.0) gon (~> 6.0.1) grape (~> 0.13.0) grape-entity (~> 0.4.2) From 6245be083d985df3dd5daebb78ecf300bacff7b6 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 8 Dec 2015 14:00:15 +0100 Subject: [PATCH 423/908] All for you rubocop. --- spec/controllers/projects/raw_controller_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index 42c6d4f3f8..1caa476d37 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -42,11 +42,11 @@ describe Projects::RawController do before do public_project.lfs_objects << lfs_object allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true) - allow(controller).to receive(:send_file) { controller.render :nothing => true } + allow(controller).to receive(:send_file) { controller.render nothing: true } end it 'serves the file' do - expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", :filename => "lfs_object.iso", :disposition => 'attachment') + expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: "lfs_object.iso", disposition: 'attachment') get(:show, namespace_id: public_project.namespace.to_param, project_id: public_project.to_param, From b2b548de9d74b01816baca822d39f9dd543bbbf7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 14:02:34 +0100 Subject: [PATCH 424/908] Rewrite docs --- doc/workflow/README.md | 1 + .../disable_merge_when_build_succeeds.png | Bin 20551 -> 0 bytes .../enable_merge_when_build_succeeds.png | Bin 13150 -> 0 bytes doc/workflow/merge_when_build_succeeds.md | 19 +++++++----------- .../merge_when_build_succeeds/enable.png | Bin 0 -> 151112 bytes .../merge_when_build_succeeds/status.png | Bin 0 -> 180318 bytes 6 files changed, 8 insertions(+), 12 deletions(-) delete mode 100644 doc/workflow/merge_requests/disable_merge_when_build_succeeds.png delete mode 100644 doc/workflow/merge_requests/enable_merge_when_build_succeeds.png create mode 100644 doc/workflow/merge_when_build_succeeds/enable.png create mode 100644 doc/workflow/merge_when_build_succeeds/status.png diff --git a/doc/workflow/README.md b/doc/workflow/README.md index a6b4d95118..d2642495c9 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -17,4 +17,5 @@ - [Milestones](milestones.md) - [Merge Requests](merge_requests.md) - ["Work In Progress" Merge Requests](wip_merge_requests.md) +- [Merge When Build Succeeds](merge_when_build_succeeds.md) - [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md) diff --git a/doc/workflow/merge_requests/disable_merge_when_build_succeeds.png b/doc/workflow/merge_requests/disable_merge_when_build_succeeds.png deleted file mode 100644 index a45a4890b6249d6b8ad24408dece3f57873a24ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20551 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 diff --git a/doc/workflow/merge_requests/enable_merge_when_build_succeeds.png b/doc/workflow/merge_requests/enable_merge_when_build_succeeds.png deleted file mode 100644 index 62a46c9508b582f82c1e8f3cb5c469f7216f5d01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13150 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 literal 0 HcmV?d00001 diff --git a/doc/workflow/merge_when_build_succeeds/status.png b/doc/workflow/merge_when_build_succeeds/status.png new file mode 100644 index 0000000000000000000000000000000000000000..c856c7d14dc6ba2f0c89f3e6d7c0e3de39e0dcfe GIT binary patch literal 180318 zcmeFZWmH^SwkV8-0KqM|2ZsQ`HMqOG6CgN+ySuvvcXxNU;O<_yJ9(A9-S?a~zP@*y zK7YPHkFgnJ*V=ooHGNH4RUvXRq6ly}aA06y2;yQw3SeNcj$mMrBJbaUMiiZe^1#48 zV44XE%83gK63N+Fo0xq!1_KidNl1cGQyj(|JV-18ivf0FLyvJ^2zgL~5Fl|J_h;6xwIgYI~?m6cpl3!qHYuFqDnT zv@rYE+6^zdP&_L{Tre^SJ0^;t4Cu%AU`?EwL}Sq2KcP=uukD}>JmJ;V-$O(2lZA08 zkKh={B-Z5jQE*P<)*yiyQHPOBLG#O`XQd3v1i0)%Mbkav5fEO<3|XCg!Gy6x+NDxg zw)D|S>b#tlg{=)Ukrq#FCSt#t5rV3eCx?!Lsm%|h5Y z;U6a%8%$uytA7$A?CiZ0iL$M+vD#Jh{1TP6gUd2uw%Qe4onHEoqQ9e+&~pv%|3MYU zm5h&$Q>FxsCv7kc!H*WEnTCRt+Ij4kyl1M@VpEhHI@Np`(xYYQPX+~`U$MzvcoclC zUM(4p60zHEfiR4iVYRcBjQda%E8@bSY6Dq~Yi8_A$S7*q<1;hP!;+YFxq z=j38{dRx)uqD8ZfkN5$&TlxfCXf$we9oCLCCx6)n&mYWs6Ec|}%7Q5wej2>ezRCsJ zGp^wQ9241BsSA4FF!3^&T^ycJCzqf10Ui}=SE|oNT%(x?X}MEMEnn3>n#628PRPi+ zFuK+Tu!`W+Ex0)csbaB;;)D}rI1+U)aYO!u3>oYx6bFm{y@(S{g?RI|@Klp*iz++q zBH1SHr(hGjuRaY{qV$upjg&jrk$6f&HvUi!x3VXXd5R}+%ed*zia zohF)Dr23r?TKLI34K`$%Og9#7y>!eSQKy*!F$v z&Fy6%v5Pfn_~^#>r9v

cD|nYt}>pu)$0Sey{uSf_ofZonvtJ^?pp)>pWI1cVaNO zmkqgh=KS!7;D+p=5!U0Ngdo@BS^PlVg;<3< z>kGMw{Oz6P<`;IP3U7KFwACPsT*5Xm!A<;+Uy=R&e@8XJ0nf>QHd-6x(XUkthizF5#xIFh(9MpBH;4(mwxUG%~b1-f8y5mQMBPL86y z=qwt7NbZ#SH|o@g{oH^aL{xC66drAI)(*{x5pLS9E0BFM^ z?CC`P28G~@-JOCg7EFA_Pd$Mv5BpAqQm&oHq@&&tY3&8)w_m zfb>-4OMy{$zE)zn5Ou%IEDCv*lQk=k$Mg5QtFAQ4B7QC^e)pC z?-lG7%N6rgaJX-{Pk60V(+5!#pE1r99;R|8RpTdP5z0_>(oh=m)L0pH8LGlBauf0d za_d7QBft^PVVR`8Vfr|_BpymP`3BWHQsoeHiQ9sSywDJ%L>!K%zBag7S8Xv4vnQ zZK9y^$9Z;E?mZ1!LPR`d!c2l>JQAf7HBvEU@nT6|$(U-f5=X^!Z`a4baDI_bhWI2PxU;Eq1SLDb0^rQt zL9uFamLg$#8TXPglbhY;u3+4`*$JpG2m!;%qWq0k}1gHivxs%5NE3cCqTp@!oko1-T@at z2$a9UyH&VhJ*q!OII=hn0y0cjE_9j_8E$@yHmU00^zC*NB<*GIO$ZzhoQzP4fQYz? z07ejeoC#lxsEsI%uuevn#*=1}YDjZP$tiFwFe^wXP#u?Jc4N+5a;!b0@~4tfd{8VY zTxBgxbxS=+SxCFr_)IH63rCwurz10$mC6q2orPEI)L2$G(KxGav(7idFjCf=GrwJ4 zo=vsXHuLBWG}$q&oh|6!Pbg2JTWA=$i^N}}pS8-@Ze7E2gK^9D@O$<4>ky3XWer-1 z9En7Uw7_HFL`iQ;PvorU@Nj5wngH`d5II(nUCA9Pn`_Nmm}nkwKJN zPvq{qGqKk1+;p~KwSCuAv8r%Zi@p#)9e+?*S}m2u`?z1Sx59S9Jvl2h z+A3#i0hQb3KrPE!*JEI1fBN<)c_O(ndDcSJg4Wv-@Zc&-bfKW^RLiup!kovJ$LqBm z;SASY5<~J@@|iWz>icrvs#4q0={`pqL&8$x>7u|A$?~)!fx>!?ve)rbc^1tywUvsi zGON|hVuS0-^J)6&LJ4;Xj^np8%B4#)3hRRm?YGv{5`@xGM>MB{Q_mI($H@R)Jg?%F z(vC)>^CTQkX<197(_%GKB zdAip4aJ;jgiPRoQr z<;Y5|uN?Bu1~*1;!&{2JihhNPviJ3qW}RN2-o_$oXC@sp;dz}d+cqAVvA7x1+YVi; zPh+q14U_O0*mSEqr)TvSY;T-eH=-PuomjO2P0Q1JvvbQl<(*nF0Md!0R{%1X{My+pe8NFX=rUpuWw{+U`+39X#;v242;{E6Lf27?4VEN zZ28^Fp3|9!EW*!7Q55WjfgxEcPN_q=cpxm&hiVEka>LSL1f!A~+^^w5QI1Nj8R zD6^g9j>H(I!xZvL^v>H28PHpmaDTj)n9&ukq6vmZj?Qs7Y8N|iheB1@%5v)cZgPJX zJGw>=x7g46 zH;CnjSPBPmYU#wcit|6_)bI85u~z;Dv0$)9%pgt~*TVMz|6@-5UJoQ($iIW{{}b+i z!nOZ@gv;+Nf`ZPV@+DV2ITK3E`#-<?Gf_3q}RgW@ANeTTOD^?fv?x`p<94C$W z8`|xy3q^Ty1&@aIcYyilQGQg>9IIW6{U?(87mb@(;Bc)kJ$*ud2RIr4!h3{so*VV= zc%TPi?;iTj&x|O42iOQ7gm+COOVr|ZrPdUOa|=ER}m%3sla2wb}- z!;A3w%Vh^HpOB}DTF{-hKZSf#A}vZykCcs8WVDTqP9`&*%tR=mHTeY2<}(zmr9ea- zrRS3Ej|YQ&D#+}&bLhvPn^(R>zsRoBHkTxfhW1{4#A5f^ zl&_lS_P)o1Rdv7#@?{&VCDs3@fZp`A7AbMwVOYFk^SEHq-Z?I&vOL`fa z-AQNpMaMbqBiVe_>BeE20{rpmO}3_8Fu}57HCLGQ(NQ@S-c(Gv3=kalf$F|rPh`cU z?hJV}oGJHSiTBllMg=g+Ck z`*aZAKDCB2e+PI7{dWWmr-CW*pE&M+iQ5Magtv|3nE2lTP7weZhgA27fcW2tu83?P zwtP&D|4g$Fd`Q>!4eB6`n>epjfa0ffd>HsV5zm}*P!Tg>ssI-go z4|C@ChT6nInXlcXW-Nt_Tq=BKOLQA3<7JH!{Y#W%ae@3C{T6ENDODpJg$) z*u8f3M0UzXEP{vpYo4F~5R~p?a3<1*{NcH3!ojqN-%$fO-R{bYe(^wTE8Mv(9mOnT zGn(v%(*mH6xH%jD(8>SBhBmiJkv%UnP$z@`n4I1Ug0WCRi^$}GmiSO1rKiVkD5X|h zqAy{Fdi!LFIhHhN`uTu8_by;KWf$E?H4=~Gh1cJ4l>McYf=pK_Z{smJ`j5%}CoF16 zeI6d1)!SVozOOWB$HvX=hGY?|3egv9S*`$D()Lhq;rrSFIY#9GIXv+Ue~gJ zdX55lm1DkjFz2U#>YYtCN^oRkLn$5s0lmxhetqMNMz=TDr%wYizV1U_#DN_=9VqXM znNamY;EA0w-xupVqr#3RP+BCOCG_pAoK|CTf95TF{@M+hFP)}hceN|-a#d_@sVN_@ie9}X)>ONk)lSwT%G)%uDz-1z5ClE z7L&!@_txoh4Q=u$?w6|eQw!*v0%Af>N*dLQnmF$JliKMjeMq-w0reCf03cACz+7JS z&DSN*WNkFhrPMr@lv`b9kW9kI*CqcbVQ#mT(M$4relCBEynn9-F37lB7uRUAf;2YW zVL)J~vJ0M`M1F7{-$IvCOkEZgq~Tgp9vi}LG4AP$6B%54^$QzS_X4vl-w)-6Q<(RT zdEH=C|0jI_jSI+Z9!}+6o!B#sT<#3gjcVd^0iwB^?Vml;$&A1Mkn8qMEP_uN2*ELvFQ?%sM1sEv^B-ksw=2dQ*P2=@6+cg>K9ZjLYnavr8pWK9r}Jr%tS2| zj=5KW$Zf4lo_jy49n(|Y2xX1+J)^jk_sT%+U3=O*WA*uaPs^$*rX!2 z=4d>(rB?_;mM^$42dvA4PprI z)Q87wtczA8)@$30e{a_G)DS=(z)Ma&>hm$4xLV6sq2`7-JbhI8#+u#M+;*WaMfU8n>6vZ(+a?SNyRV{UyZZfbl14MBvG;*0s5VENe|B0Az_TYoo-ZDZr!mNKUpA2+J{_NP%x7Az zG>*bE+(lRGdS+TJ+3%0Vt&Ny}T;+O#3xN$$D7GUvTcxb`prDHA%^OzSbvQAbvNsSf zDJT2%Nut`IKhJ!QdOlrJh-2VY;6vH@)enI29t_Wt|^4Ei*#>lg*Y;m}#vhDY= zOC2{$Q&@QT>&40U8&LFZ*wPyZv3qWz2NM)DRvup^4x}}|L3FFG)Fc?+w^vHW!4gin zswV0dhZ9~*Vlx}{_-El`@1UXXrcPww+nz;Gc}c~9M8%O4hCwHypX3*8Z+JXk&h!)~ zn-OF7J%K@2hoI>fZ?RIb0#0!gjT*%9LS+)*cwQ69g}aK&2^e2dHMPJ!t@3$5DDj37L4U`U5utxM~c<(O98I4 zKx7DnX_B^`&IgYyw#00_(!(nxJZ?(9w~O2|jrQfxo{xuqNb8LqWiw`Nbj0o%Ova%v zUp#$p75b$7l1NTh8sk>1Tct@zNXE3WHDu+y!ao18&;L0{>henAzp8+=JM?+8+uvkF z`cO#R_GFkX^reJeIOske&t<{`y<&YO-Px&(tYXb%Z`jQ~EgZI0=`D_QV02i1!o?6h zvF%q8S;Z1b;oE)bZhL+?y6fxLnDq62YTMexkttKYi=sy50U#af>l>TGX7dU;aEl%h zTy;#Axt(Aomk6-x-dKNa5I8_ePWnPtW!o!4vuD}3zHN#)X@m^mx$I-Ekr{6#;BwS@ zhv2H}dgrcQ*JQCXAZ_<_V>TQ|5oNk>&sbeuooM_^vCzQ1V2X>t<>6~kmBsPA<#gjY zSo_LxjZ$&!bf+$%+0RY5%I9&W3(s1^!Mlbki{&~#rTLpPRe$w+*YJL{PH3vkOp!|`3;mP$U?i?-^Q3)POZ0yD6Wq>02wJ{d zR3<7~+?$?A(kMBsRuPn5xy6+~Ik++(x0Xk>!B9Nk8>klVL?!K00QTiJL~} z0{CdKK_PHo!v-k|)b zK&}it7sa)My`fVMnD6^eWu^C=gU_D(NmbM;uvLhBPYnugRPY8B(M`5Ns`5!RDMCoE z>oxnv1ExhCCL$Fu(nKP#CCEhXaAe=)7pe^h^UP*GF@3v$x!Q@^p|e#jEv={vbw8;N znn4E;qwucIt@liq4q`8~j-@|D^JP?d{Q`GUY7TI}Z~v-Ok9@{tM+xupL0#4xvlYVV zO@0(N&%h_g@yw%oYWitG{^~rW*Ku{wVqbj89eOV-EwdV05910eMpko;Bm7KL@=~L9r4`3? zrV>%*ej`uNWdLF!;rvu@mK}R4r?j)rrW1H6ygVM@8!@D!dB40Hcgw+FdHVFvn3|;_ zu$X-MuUVmu%HeY$g6s6M;eE@6UQlDnWjn-%t&@!dk(-T<3}WMLSji(KlHwf(M#7k; znfi|-EWS&%{g^1{n!jt{BC|CdaHqm$vx=u^0gT$b3Mlye`bLb0_A1KY&dPNv7q0xX z5rL3#x_7*CM`;EdG6J7#g2~Tn|Em{l+_ued5v0&nmd9H&_wyL@N#z%G`hTCtb`e61 z^si>aB+L;je#xg*!f@Va9mO!1o`AG7xLBadVZhMcU86|A89?f^Il|*QtK387@=PRE zK68v9`{lO~QgnaBsQaql>^}=jM9sxw6)MDIQy$r)>SedxZd<%!PFEw0Km6Tx zKeAT-uuTu8c9zO$SWx3R*S$P3#CN}HpEO&fBR#%D#!<5cIa@HE%D%mdd)~ITarLFa zV!>*U&Mf@n9)a`6Be`9DIq;1&mLHv<_l z!#z*i8T@uKS?5rjtpT|DU1;OEV7$^M~N*9c+dC)QuSQH)C@#=QWY z*$1WtTlea{-hK)fHoKSFQkW^dC>$pbwLT4b9<@5O>?F5%0|O+*!W8z(rSmi$YzjB= zHY}&r&9SKHn&X9|;zDe%ZaYVfPYUumSG-JMp=pJv4!;w9X}+G|YRi1yu-|u$Ff|%@ z-<5T~`4#e1N_?bdtmF0B2uP@CeAedT$Xzi=s8~1>C;JmF6Mr6c#~vzE)>sDDU`=>7+KK#aM($bn ze7WWkB|U=zgzY62Nq5-I%`|tp*Z!}xeNSty`j3)fEmeIyh#3NTcEe33b0)_1G@bF4 zY9jkZd_<)SWkq-SCxe86O|ryLexGThy*Xj1PpIXHI^8ecU=ru5K-t++omrgw>v&_0 z*Dp^|WlUPNqBrCshN_pr=w>MPPa|3#v};}vp#4}##AK63S`~v)C#AE?tor`#`7Exq z(#oQJOs(ELq3u^U@^L?OO;7tx$l;-bHdhN`uk^pKM?J`*1wZNsrbo!!uEr{Q{LnJk zcFUo&-BUQPzjiP;Z`KO`t{~Y{s)NbRa4YlD*HwwPhkoc1CA7A%6iIWV!baX#LN};G zpg$<=b(3os;#{xIw#O?N%X;2$FWOLM(HuJ4Qyc86OfCW2AKA&M0e9CzF&B`RK?)!@ z9VUrAmm!CO^Z7dhvAE=h0f4?|7===<-(4wYHO> z+Msk@<^t_oc3jaqfSPoBavQ1*wQpkG@hH++9x)1+94m^jDy zOr!-H464b2>$w%{SWv}Kj7&UUH?Y*aEq--dX@{^~K+;qlc~!>MXblzGPvAvZk&J@w z)LcQ9N3gJMM-LvND~v;he%7tn7|=*H1~lcBaH~E`xEN0vJ&LJAUSqza{JL;WExm3} z!j$pOC97gi>s|Sa!e~sH^?h0Qq#jJ3=?dz5^`Baw*eUnU*Z4?Aut( zFN5{q@bEf1ge~rau?(6c6fLUrM$K#m_}MBO(A@ip({^2AV%7cCYl67okA)dBQcMOAf*_-Z~G%$#G(R(y1YV)x1y!>!j)MRrB zOidyYI1MBVQdu#8&x4G%R_{Nbt958`xV&5r=E-ddTs@!1oousOuN#CQu6`nl0aHQQ ztaN^TLKunFsN0Pkv&^t~eY(y2HkIRu-J#NDzI0^_A^E+jLtb8fo~4_k2YRz&t+kxf zpuGt-x0~7z5gDJ1vY*kn@ikcV5xMN>T4qpu2=m#vh`)Z3`#T=M0`%#QkBY@GnkA1W-ZUhJq5*jj9?;nXizFxalH&U zHo66}sI>Oim#CDd&X%gOjn~{TLYPrBowe}nbTDYQ)G6aFk6-PL8Xr_>tF-!<&9O?b zt2Wyid$4DX@&olT_ah*mk_RGiCC5`)j8_^?O|HBbrWl;LFq3U-1RjWZ){Db_90BEO7Zsf(q2boM`nacXO!W-BE)Qtuwv%8^8_KE z?P>|!vUG7QInSACttD~x)|rmW8sU!DcE7NFs=M&8X8{NzhgzV~=`JUu!19NDQ8am>iuv+>zk=BSge zM78qF{w3VC0K@mx-ln4n?JZI;D}VuqMx~4>zkL;QeVG%8dDtF5?_x{%Zz`EuN>K6j z6^Ais3bNwryVP%h4W+I4_1yI|boV{ac8nV)+gDccbwC$A#nng)qI}YN%)v5dY?W(R zpSy?EoAw-=)d1@7On!dC?sqNU&4c&!h6efpIe9~(Yxv@Amgzj@>w;Sug-jzo(LAtu z6hMJ;LbsC`Z-4ctaJ0GSE`Er0LGq%z;sVVRZzLs{n{%4V&Z(8qUbl-43h6{q0?(f^ zaZ?y-d(&G=QsFF(*^SK~ez!`%t4fWU^ui@&Vu?%l^E4$o$-PFj6OJ|KE2AH#iv%LM z#Z47IW$MjY=qdJc=aMF<`IZxpw~kCbsEzt2Hb2LeTE&Q#u(9Ui9mjC=?$y&Qj|}@e zlV7A-o=dXFeXrb5DXs{j99u6|;nZ<$?!rm#Oza(N!|LVaG*tWG(=_-Y*}rSlU8+HE z8ywshpC|iU!Q8Rf{i)N&a6FaQA!XDaB+@E_YNO?GuxQ8qJH&WU*-7IPFe{{Vf5xCu zarimKERd$6sRz?>Jd00P#Oul|6~J`eqcBpHEr?%$%$Uu7k{eYXg27BTPh8^jW4~Ti zH-_3gmiwnSOkgEgb~x2ZXmIcarw@zuy8Gj<;`zbT-eR>x`m{#ZJi2nqpw`VK-__+{ z?>MCe{sLT15++^hs4QPZ<2~O$%Ze7b3#LOsg81|vW3QYupbB7~?k{lH8O|tuLB4na z39UmY;rXb0X@Sk+9gYC%@)bFPjz1#*umn|9wof zrt_T|iP`&2Kcu0}?sGS-18infjYs|R4=N$<0sJe&@qBO5=gM4Py`#yCUI z_7IyYeis+q&gFF@imtCQA8L0u6;H>X_aYg^C6egqgAfT4dV?aV^nuHSQwR-G{Wk-;<>GLVC z?yDR*n+%24a2tmn?7J7P!Qx(FCGeit?l)EjyRTg$P}vqrH5VdhXt?5kz+>7I-Ppps zp15B2V?iwp;jensH=2?@v^>A6lvgNtJ#c@oKIhZ(UX#s!2_keATb+m2_hD+kv%C07 z?6Nnva6&Ecjm33#h}KKy7|!2Z%AfC(=|sA5M93K5!;Nhf9-pW0s!i{$-An|V>!}-2 z*V9`NA>Msqiq}MXbK?72>#TK!9^@~i2o+fz6(vf=IXI(j)P%*hV14Ch-n`c#R#B4I z`{Qe^OqN{9*Vih=;Wr-ZSA{GEFZ(Oj<<$yEEUbW95m3%P=~xHws7xO8`FS?J?m=6- z%Bd5TPh^KQrv`1jqwaV+S1dqV*A03R=B4a+1-GK6y{w|I$HW_%CR$D-!bOG37Wh-Y z7w@|qCN0(*w{6bN@ya0TFE<*#WZSUkRPVG#+tTN!mS?G#_M7R^%0+{wpxH&3Z8l~^ zz@T%UWg6=|D#W!9B+PIK8U?ouya)Thbye(qfwV>Cs~?vlN2j!99KdbXSg$xgRcicVCyM-paRmo zmcL`?Hw}=xBmx5O9&*1be|EB(LlpS9++azX9~dQf{gFywV5#<3EDT}OB;!4DPj7V* z7_2{Z@;P5dxyF_fJll#aLBd|pecq^Hn#BF@tiQ!PKw-3sRGS7K=_Hvg1T5rXx+j6gaM5R;q@ zfX!IgaAua4#~Q(A-j}R2>q!ukg;85R;@hMf6$pszMMWbq9*J+rAyW9rVP3b9LvQ4p zkFMAR4zEASd*24Cn*fk+kv>VtVbO8UTxM&u(B(IeLVKwRePrQ0weQcHc^+v`a%p!^ zXEqtTpaxDq-=D4=AOnbbwL-|p(L%rKxO{F;U-dS)w0vw#T1mmt&}d{$S+L01ml;zxP1r>b*144%m7&UmGgb+e60ueGau;^&^qvHFepM?2tT)BVmM>$yWvV>g3u z<0$ob*`l($!n^m5u2}b-aUJq3PoZ%+jTD`tr#XSv)ow(*-JaxdHR)81Nx#W(8bc#G zeeCpoP*9=Tsc#c}&SzZ#H7_8l)S5>^RHfJ|=A}xd&S?yu)#OFn?I+-L$mNNzGlnm| zlg)kDnQ5-f`Pn^jivXCL+UA$umfMNvkEhToQDas5J0eJ11E39NoGkHJd`3Xugs+g< zP|BYv%N=Nj&1IE7_{b@vVkwpW5cYD>8+G=r`{Mxyc^{K-5ASChPR=IfmD?dHVwWK_d^0v@;GC}M8t;;Iwl zcOWNYduTn@5aHokw%0*1?!M1?*3J*{DNk7szk{4@OtmDPV>+_s8Qf zM#J6KbA|p6*DN>eP+pk(?y4x>t0%V9XXpKfrFlVHh7@D_f7g+rH-RwKz7}>_&ikk_ zbYefDKk-_858MK3w^Slm@cx*PC(3GnE_?dCgb2i!Sl3)xPE!+CYtup0S}+!T%8m+dbuFVUUtJ6(`i&ldz(#uhHL6mhZUTjG!OM zCF>xN&dvQSYQ}qMh}~|L{XvUSv&4%}ED>+nlA^q*EKpmuqNROJv)OSLz=4iM3h=@s zc};vXJ0j7r6=bBVqHs{@?h~5LTV;0-k}j=5PW8lGItprcr-z{gIaz>vMDhBA$j(OO|J%d z{%-Z&!1eL=bu-f+3hNYejuvv8a0WGM-E@7BQ-)#97Xu%uRPgK=U5aaJFIDp1CFS|% z$$Esm#BG~%(Urdndeq^7=D~nhNjmoU?mbq74SWgYHnEZe%uOrI+-oUh8GOsRdi&&t z@n?Dj+i*~4eN+Ie%z?Z}*X~N41Hhy*qN5>@T319jXRZ>>dWthVe5h8cD=ouIY8pFH zbQh>wqiUPe3E1c$PYCFAnU)MX21p-FL>hsr2==pRuba_Gd=7ct@2*_!vmeLVHZ)W^ zxtTtqbd&{Si*u_Sx0D5Ek0;+F(Qb7~=VAYBK*Y1L)2tPF^|6Mk)mmdXYAA^dc;blg zq=`2qS;ehVJ4Afwozf)aa2Y{I(#OoiJ=-khd*@{BYkFVZ7Gk?Ln9N~Oz(qZN%AduN z&E8o@y)U~_cx{+|`mPF< zD}4FnCe2qD-fv#)L=|Ixp4u;^avD#;&};rl=R5uC_xyNv%zMdV1)Dck5ImQr zr`bwRz;#9=+8N2&wFamykjD37-a4o7daHmH&VodtdL#G(Nz2PyNn6^3QOHNkwJP~G z8)x=!y6Hq*$YQ((?nNO;NeRK!G`Owtm`4b4vc)B57cG{Vo_n5z;o|;tK zRTqdi%2g#ZC&q6I+WotZhYZh$IufHskA|;bQJpg~YxoVk9#<~8csyd#BV_AF_ax>~ zf96yGMv~v{D`vXfK@WeB)P5?H$q1FdUfRLy1Qov~N_F@T#4SSZ{p&3jYT{l>+o+ol za85F`IIN-4O-v>cd}6$2IqZUdb|aXuBJ-;*mI_X!ic?|1?s4$h|1|Fxrs zeoqFf0%F|ee$9zW@-sf|C_yq9PVuS-om$gR3E$y26r@S&1w4du*21_tk)ewxSZoU|3)ry5OfF^yRJL&|Q6-@lk zR9|G&kC`A>$#`xy{UkOM4e$+~vb;Ejtad1*s`?z71~#qWaveApDA`;aSKZ@ST%bxN zbSY`fll@0dyr04qF{Q@5VV^RA%im&_KN-XBls9fPRGz0d+wbjbS|tiV!%IkwQ8AaY z*uKqszp*j|LkC5Dq;g64QDt8!qoScE>aB?{_0ruF!2$lhQ3>QPtERVbQsR;$Av0DE zXRCATtmZ~)qNiJBb&m;rh~5GCgF`};6Ng+iFNG7H``4{I^!o*)gTaOJB@)UkUnfD0 zz64M$iEEi0c)zDcK{~3PJ`q1KeQJevMyv~*o7s;OQF&BU2QZgM6Voj(lOpjM)uRQE z+Dz;V?OAvJvbsg+55J0GtXN91kd=#j`xYGRrP%4+*2DEThvl8hBwG#P8v%Ds{Dxw1d z5pcy^$B3aUe`u`;{0x5H3nKKYi{cY(j<#h(_@hvT@S(jy~cW^7?my)<5xIzV50 z3_K3MV!|Y;Lo7C<1 z?r9Dy`EgoVLBs>pemoGORndzwo7MS2;CK#3I8 z8XyNgkWpSj*FLN5?g@9k_SXd0c@-2c^ql<%aH|nLd42N{t@nx~UD%=QaIHl%2-Q5x{LP@bO~_@<_D=CD6Z` zFEh#JxD+xIdo9yh;b$i35SBr`bo+TX4564RUggY_hC$m#z9d#@<(rNNnD1pdVv{Hl zdbw%S-Ncw}7_U{8WL^IAS*t3rG>Y*jAMjGRZnTGBm9Q6q31S&<05Qw!GGLDvcOXh$9e}_H?FixBlI?2WmiFSvog`(xUa_ zKnxi{y@XVzCYD=7qDQrocU^Wq$uUklK>P^OqP74SUb+}*i{Z=00m?XS8I<{@Z?_W< z`CF*ZD_T@fwn#0S4qc<`xEizdn(YUK9NVPWOh(Dy6?!O{{d-<>)H5q@omqlDej7?+ z$jV^pp2j9Cct%{C=t`g^<`2{UYCs=NQ^zYMpK*c5(LQfJH|1gXd4g)5zlSC?p&m+A zElS=X)2x+E+yle(Ek}tbYOqkI%seS<$S@FXL?c;qHsF;@hCtD-0-Z)eU*hAPE?jbO zzmecDDpd|>k(2YViWeerBt zx1@8sJ<$(qLG~F=!qRnKk1?5(*w|(j6ebsgRF!XecD5meEdh~qNH0Ax2V+huC z>u9r6j+hKzH?sw6ruBkDcGxzY*DwKF$)LtfOzW8h#*01#;Q8)U1+eN16I}Q+65#y< z%%CI_r~zyUGDN4>T5QSWL_9)4`bu=Sm6TZo@1o~}XhtkB6SrQU@J%jBzD!M?t3x2* z3UsDQmatJQriXF>5t*&~6rn=3jY%Kg7GKLxK+h;MDT6oyV>Kw*ofVSS>2#xWBmAl* z@>&1o0XEX3YC5Q)UYn`)g)Cp-X zf)ga;wYQloi?-;oz8LeWhJ2c`LMC!#Ucp=54mq6VyNI^x_C?pPsL&k6q`NWi8eL3& z@V+J?rM3C`Gk~y9yJ{Kw-ky=MFaeBN#nEbm!nSayydhLELKNq=7uCf^=jm81XW0*ItZ!hvHZu8LDe2dxs z0-WC$qe(F5PvN%Jq0|DVP7yJkBdM$X5UUW20+7@n(+8^MCgs9IZkoG7cWD?p$#O*_ z>IMWgK)Q=OZcf}TM6FKJ~tj`wwS=%eO&9s`S;ap^%hvi zg3xi@8zUoFykPH@-|wlBD*?>s%Zd%T1MQ}GH&LIFZuNZ;KVHLJbA#=eW_m7zen6=& z3JL41`M`?t92<{j-DV>rWe3wkr<;DNuq~HYsj0GW9kyj7Wc$p`)(0B z+rJC$aM6OlYmwGFj^HS456n817IO)-6};(0eUEot&BK4*8-tzXaD6T+(GgEm!-0eJ$T`enHL)3Dg1(arm3 z71Yh@JfDxx9IyygFQzT9&reuaN z!uXYxc+J)d>ZKtiR^mpWNj)_i^BsFESLzZVpOF@iR%vVk3>=qT1$r_N(PH;LIJr1K zMumv!g7=^XBdmhsUxNDacvH>SyN=%tAyv{Gx^}A_3H{ic(3mL*^#HK*q^scbd1E1w zhJy(oyq1>nu z@5c3p zEF*bv_L<4996+Ee>6Ky)I@oa*L{!8mJ?+~cx<_*&xY=CgGK%Sj+k>ix1jh=sGlA@Z z>91{Dh*nNWYtv6-p%om|Pp@{}!-H%OtOGEZ9qU}GJVA-e9aZ!FM3!2c;Vf7xY?`rL zf2kyu(42J;DnV!hvIt8oc7paKrp#Zv$JEdWr#jn! z0Pq^W%<+7kG%SfWX36Vy?=f8LfWs4X#1X&8X102fBO7)eMNe5QtXI*X?HBGCvuM&s z;PvRYjUY!LfoXTf4>zfRCmpA?dc^f_3s$PRPi+FtLimvQrT#etd-Rf>S%ac5-B9j& zKa1oAMNtlQB@$?lf>)Xt0a7Ptgwdasb}$FO`YkpcoVD@{;I}lf@AQ&s?RdVzsGXI| zKH5x`U*|V|DEL3@y=PQZ+tN0=MHB=Tl$=2Xl#JwzARs}a#0FYGa?X+k1SE@OkSI;g zIp-iWIfo|Gq$Z~(HQePs`=s}J_StvbKi@aT`>)67?zQHuSyi)U)$`OGb*@Vg^TdSP;Vg@Q% z)c%BLA7{5>4~dbJT;MlI@FEj;ki7!+0x8wF z>KP@g)}u-&QaLl^SWC$=frWY==(Dm4nPk5hHf_6I87UI@j;72;e3yaMFD$Car%`d&ewOO4wuOuqf(NRj&LN@J-l-)A#` zy}HbKN%7-ip24m0pj7O7-J-`%9#XM%M_KWS@_Z|x{iw*p_zB)y@&!B=Gh@!8IRpXjV2&zBf0VS9h{Nt71&ecRJDhr#A|_v9RtRq46gwtGx7N5wI1 z+U{@TaEXoG%9qm6QC?663>LaEocbd|^m-LHx3<=x?BGk;fNxM4O91ZeJBIdi6C&xj2>E1v}1!><7{wD@{}Y&b8pwkm!9u3uCuDJZo?v?GE|wu~E#A z{`)ziXVgGg(Pw|+rc!nk@w8(_h~~v8Dp?^vu61JNOPP{MY#2hh$-`CGBa7v>{=-uW zK}Sc;u_SKyD!P{TCKeEn-~hxZB(~}#OlYSbd!-jzPP@!Kv zxOaQqF{kTQ-L>1j{2<#XKE+z_{Z|z}96igOa?Hr_n>u-2gyC7e{M!`~myc>Pp(lM! zvfR`zFCuo{rSKc;rwcWUU5Ft0u$L`tJkbiITh*$bqmQmg9(k~4Ice@u9Kkg&qgy)3 zzi&`{mx`$BN;>1C`SOuNu$Q~murau7(fp8SPNn`bP$?XQ#*g7bdF&oWnyo#`ja-5K ztUaE;`w?hdVGk7=zDNh}<-LmMj$t2rzx?`JmsFak0wp))$So@}!jIVauL;(v)F z2kly!J}1}OF+Rhc<1v3{6mD|4=}W`ny>T5UM!BG%+wh^CVkWuwf(4&?E+0Zk;CEjX zXwpQ4n#4>#IPrbmnSG--Ld!|}BJ8*MS4QyGIQ$kAgd=4tz7vm9Y@uLz=l;_*t8 zVg*KOc&!UHrGw!LmFf=9Sk)>K3P}^r0A6%^4iN#tq<<1x{0T4Shs98D^HX7>Nbg-c z?XnVVc(4YV%oaqrphE1d)KI$Dj{xsHK6L07o=*IJc}ljxNaZUxWXEm2t9l0ZHwWSQ zpYPikh)>;bzPiXa8b@n;R#QO+L08JMy-WBU=EQYmNTJ?1g1s3NsqSTaS59;DBZ!%^ z;i4D1+&}WkmBrCbl{~PfqGtJnv902#gv&AmUCOj=|Ea2KMV8Tk+ddfQU6XmeFp z`)NJbDE+iOmB<*~_gX6l1NOWfvK(Z{VZZhM+ZNICxbs(g_&3#u`D|g!(RYfYhD&49 zxTR{1Lxl<0h8Yx~J$R+|Qm9t)KJJ%uLJqe`Mz5ack_jlGf218Xvg^FVC$ z4%}EX>;T+q0}9{Y#>j3VU%sJhkP?`)DF!#smo3&B_meU`xPA%V^s1T_94vgl<{=+%EgAe)z$f~a zr?6>Icz|)IY}?e{HrwA{phl#TxP5Eff;@vYRGCA)vz(N0z3KcESFQI=!*Ds64R-D+`YK*kT_~Fuz7fKpb{Wd>^{}2_SiQcN#L zVuc?cwWogF8+b@bQo;sQdwX2s)oMO-H;k5X$Xl$}J-wUC`z@vS_M>`*3tAMITt|uU3k!|>iyP*+wZ;-XOwq_5P`Jz_lXae0>kea>k&2=x=g#+7<6&_n7A8{Z4-Tz^p|S3Er_j zr#|+Jd89+8iT?7;bL6Q-?u`$&w6}N|={Vdw)yrOCJ5C}9!L& zE56mJB|~^y`GlRDswwAuP2vZ6Cc70 z{=&xiV#9!XNWf$N)0U{i>v@kzSz;|EEc}5&bo1;OC6-jWU2&qvg&jKX&J>Z!vk$eO zu#@w}i}HypyC8eMhXMH9Cf^t0<9TYH!KKueN4zcL@7UK7JA5U?TIn9HAo6=mY#1?+ zIsi2^)5D}zV0sU+7_1fYZ1UOg>+XjXG!9#?$v*1d9$>(H zeh!&LCd7Y@IeFtPkpjzQZASGw&}2$1hyom??L6_PGpJ6Yf=ev%mT(F|%o%DJ#_oLM ztO9ZlDf;YaK=_Gj}=&g8FDdN9IeeHg6O@nmGTGw0Ph6;jx`!DL1dACUOxk_P|GxxiP^?$XeU z7(pKa_3$g#Le0;08I%nQ3J-6d)5W+od+o1jWKLxlzkcOs{y~N@2vH4P!nhFZ?Hx@I z`b3~uS#@WLXe)eWr#>$U-|gg9kHZ^>o=1@Rgk0qJ>}(Wmw#a3Fk&FV|njTIaVq+nMW<3oZzW*&y6M`b zcCzBL&~|&`WYUA#psE4?d%evuE%$E`c2E;Sa|O0_dfg_WC$m3xMw`n{4^oF7=V%l+ zWs^}lVakWA^9Wdhr908TfJ%m%y+xm=7hHcpcsTLnK8oLc*em$Kb`5|}^WZ2s<62+2ae49DmlnnU?yf&Rz2Ei+2b(C$7cjJd;H zQ>&BS!789xG<2AWx%uvqGzLX#{Fe>Ymd{5Y$(T78FW#{m-Nn3(dSD8*mJ{BDJ1yYY zuH1$SC@ZH-ynqop$UZ|D_`tetEzIu1XzQ#3@{;4>7-f`}!Z4jefgFT;lel)DQ*u#5 z83c|cn<}W&GG3rOT2=AM5uo_SSkdp=;iS(!rAvmRd$<0%$u9kLgI#JzH{||)Byao|-ro9;Jn=7p;BA}ct~fZuuU;{bZ>?{~q~ZFd z!}b|Ra=*5^D*WuCok2;Tj+n=EsD;pSNvvMq%XQ`M9FBubQ`=Z|)*}LOCba-; z6S86d%0Wf%-X`vsyOaI1HQlmbByZ3U`+ByHj>eJcsIJ;BRa+OWYU?~XCA<|5x(*(R z7)G_eH{AkV?nLHdNiSyAgz2O@h=1Il@`A(T#x-o%rqJ9QKHTf@&DJVvM(keG0Gs&r z1PkpZYyGjx=5S*@-8Wz8#Ddq&9L>ZhO+qRrp@M?i-{GxFJl7(%N8y62ct&PJ_pOzQ zW}I)lP}@!N+5H9XGI4`+zXG_1qCeSEixJY**51WEIe&TIA3UHU`PB7BnZk-C0(mKK zU)?0$7M$tUHga#-m^rpl16IsSA(zRp#9x_uZ*X2+ee8H!72I&kD*pHnHLJ*i1KemuZ9 zNOq|$v}U9z`?i#A{>$fp+`S%8`qhWcm|PY!uYjC}3)J8SfDCNWOo`LrdM~ZByi+h9 z&hTEHwNYiROK(v6l+vhopw$PYioVcL-9HuR_NmCVtFETUNO!ROThY>XWnc?E5ly$^&Z_QIbD-i=lDpyzWb~;5xUk71XEgp2qVh( zw%~h3`Mmo2OE#Nl$hR_1`emZ0nfJkh3yj8x0}p;Sbuv>Dk{q&?>zo-WR+e^kICvY; ze&53w4RqLV8b7UO7G4y>#@{FZl(^|pNXu4GeS(^s|VJ@TDH=Cbt$5r_9=6gId0XE2R zOEpG~DhxGi4oYU#e*-&W|yqPHa(5`ObH~aB_!O`v|hSCI`N&r+s z2nn+yXd|hS*!SfK7Q)V{P)sGdhVMfo^739|KO%25 ztMU~%CbCpk+vvO=2la7Ux~)IG`lCA}W7#BJRZYr9MuIXfVI}qhouw-MTMDNM>-Q9q zG_a3q1t9BMt-5FVTC~z(DqwlmN;z=8$UCumyJ{IhAMJZpgD>FwE!~nUYmu+Zr$!ZL;YeMtEGt=tGTSmt8IA|q5Zo0=dC&=4qBb-$ox;p8qFT~~- zaNPyeJ>Pnd$<9|IoJIRnHoHf;OO)%Pg?b zX&cYR!x|ejJ2{n&#vAgJc2~dWscY=$3cJv!Kx3-09J}?TKp8=}@unC`>Mz$@Rj(K6 z$}2=?I6F^MUhf<=s6Ds!%w(+2KfJ-_*&~e+{t(IdFQ3vZJ6YI%SeQ@e>sg#Pp@i+P zTD{N^`VydL5H<-h^!*o2evh#eZrNQscf|je2hjG+%(tq&xtn~ymyy6^Q4;RODuDFthb7POBi^C}a)Aka|%CeNf4KSU|8O`D2m9Y2P- z>a&D!Y}r2OrgYd2$oSA3ElO@zVrmos;2?T4ystUFZ#&6f)G(yLVpD8WY{45MM4H%~ zg`89Vq)b07?qYiM+1oaX62It>UoO=vRgsh?|8#%BYkFm< z=C1W52&lpw#)}>OAbyX&i+AKDX7{u1EuExUY5-w58xDLWxFeaUVo3#$I!$-C=90G+K0?=C!XRDSFLbM=fpI zeDjS+k9N1{1{*Yv&Bn$M?jf8#EaFRo=Q9@&Mncyh=W6_&qbFGur9@R26t$7FZPDUni z<1mozuEV;3sJeEQW$o3)tPPDYi-#$P{g|mVS?jI+^p-cgbMfhh{uzU0Au-OhAe@|e{B8KLUhAkmo(?PF zWnM8p#k{J$nkwA=p6%RchjvBJ#tphV-v@|%TK(o@Oj12(4q^Tg-5iSWy}eI|Qp%O) z1g@J9%8|?d$?{eF7t8#Z^FMa$_b34nHm8xAXbTF^o|JOhbu#%x zYdII!6DLsmfU?4(zyiY5y1fvEv)CSx*!~fr*KDam(T!nfwJxRIl5F2d!Fzth`usgR z%^Qe<9J}}SjHwnjVlu~%<+ps*ii*Ychn2z6Uje%Aj|)SLLfx6h?}Ev-$-F{sF#NH- zX3{iGNrp^@baq#8T?K;)*kiW_<*ZfmF5SxWc1A*quzSyKN;vAJ(!(l}RN?)o6i1Ai%M32+jw}@VeD2kj^ z2%*MQ(V$Okp&vGeUMBEZ)%7I`gS3p@lo!M4-`+B)!6N7tWR?s~Wq(;zwIC|01Od10m z{5X#$0bm*YC+w4+IQyDoJX=$q3J>pNWwap6C}TUo1vZ+JrqY_UpSw6gYgKbg&wTu8 zQSK&z(_~tPL%&8@H{2@-(meut zWsD1mYT|#ClUn7J@M@2tp7E(?xlh3(ueUX>8f23ZouglF|J>OhKGVzG-WcAa^_d#? z$(Y6i0ct(PvZMpIX1$CQTIujI9*rVxI@`fJ5WZIgKe_P2xdG^=*~x-VBU+qRK`TNT zu&aifmwD_s>t}5tbMRV|)Y6&hJhglymtj#j!<|_{fLLzW9Ueu{D+zQH(leY( zEqTIaxF#?CLX*y7IHQ(=$WpKr8_S+<$}LS-JRUh~(>%*Ptdr`wW)_0cVPCT+LDJ#@ zd5o1FA(pxk0+cCP8Yl^iZu-`J5^~1XG^SA@Bvc!2D}-uj{ac(=7fkYw-Q9G_at=~K zwNKysvf?N5f-}*zU9KXz4&uOLMC`Tj{mAx-a#iH>HJPRBI=JzPbmgNV`Xyh&K{^WR zOz~7Nq^^tBr18jV&mqv>05Lc5_{mam04C&p&(~AVw+tQta4lx>OnKb+Fwg7Mq`O=u z`=IKTU?BXJ5D2!ULbv5RPJ~_|sd>niN-H4TMP|J;a_*sk!AUQEnXg+PyHYFYcFZtQ zemCParhdRf3S&_=uGsVc1gD?a z2dD9xhsc<74z%7@&zU+Wiy*ZLxKu7;ozQo|4a%aPH1X}=N&Ed8h2n;bM7JstX4{b9 z7jK|d#@Y_TZw+IB&N0v#CyO19HuN6I*RWr`0d&C1=IBpBq#fv+E9TLxqz52tqZ;ZPJYJb=YhKyICR%$Qkz07GUnOeg`?RFA?2rhr&x^cF6qD7B z$VD{v`0g>~2{GiR7UP~QI{8wypVNFq3gW=Li%$eaxp5J~s};E^QtVf`iV#@>qv;XJ z>!&9P^xusfH-;2E@#hCjTw#xhZw*#Q_{1Eg_TNcYkEknLJr~fpz<1mU3sc`ETJLQk zC?AERYlMl~fM_-13ZLAe{kBHA5*4)yeIo~%U07+1JsLPnC_5gqCh>xYspN_&7n=c> znLM>&OELqxq?k@VH%gX3VaS2)wutB3=855MuN!p+bLlz(TZdQS8V)mMD=I8x`LfE=OOTC;|YhwWXt6cr*PC*YBQpCH8V=0T_ z+ZV60#2;OQpqACQs_=^f!@$$nPTb_Il(LcEZrv`>+xU1d#>emO=9}(AC|kFz6LD)?3vk%|JR#y}`|3nbT76013+=WW;fjWDxjlwn}8JAGszOjd2 z!|aFmrB$`-x6wpy3-spDPeA{O512~S&sx}MZ=EEFdixlvxPLFRKJ3jec4INVQe1)S z(+T)G`QePeuX%`wi7!xG@Nye93<_fZJOki7C_dR|<=n^9uxXeEF+{p7xXunsZBuk% z>3rS{>NC$nZCs)s)OcYUH$;vncDS$4A-A7>YX@zuGl{WpI+w07S(O~be{opASv;DU z-8*9@eS*s}v!C#K=OajTXJ!yoCr#-()sFTmZKnft-r8{o6rn&N*h3{NlskKW3$4f| zUuLfFzHK4Gx!8houT+}qKtL;m3pe_m?*a>-1juGACtXYa^#mDD@e{eV8?vuuN|-6a z#y?1kV7v7Bk9>lqQ;CCrBl_xdSIA@puWgw$O|Ok;kqh_$h!)je1|- zqSr+HWkX^6Nb2)#@kD?kTjn)C0yecvaP=bDxg26mb=#7Oup?ydO;Cr+cYR=WQ8JX} zG=22l6>LzjWg&02#yu&)t?olM2lhtwGtZQUUk~BqE?w6HXehZU|C4S?&oN~eO$o#_ zf-YZCT<=K8sTG-n!z0cuHDQ!hdB3yf&SbO93jITUqVS*yOfQA=aOy%v^oqUw;^r#< zPa7_!VX4JKGFro9sGMT1)>HmxMBcoJj~3s6wq{S7cx2Bo(>c)1Fw^Ve94UTPaPhfe zyrntP#wO?ICjGTszBKLOJ#4)tHI)WlbT!aKOSqLv2;vWScJqcO$M%+Yd+$214y+-CK zD#r~7S|RCk4|m*Ekli>1b-(92ZWA_=AS%eU{~uO-8c1s-c6 zjUqQK&Oukl1WXUMP60}t+oZR>>)@OVG@ebQ+NN9xCWby2ck9+|d04HA6! zq(qrE+r8k<76SFN1R%qd(}M)!psCr-Ld&_j{z&v#3G2r3W`yrONpL#Qw9ejjJg>ENt#4KYY&88ozH6m=iQB80&#aT)_(~Fvd}l<}Ug>Kw3ncS}KQRG0_yb1D zq3LR?g(3}4*5QXrRG&WUycFtq~g&<9UX?T&wsw`f`y{fYQXYE}IbEA{sOf z3JtB0!Q^slJ`V`|Op07**%T_S9BS7UwV738Lta~cq+{b8J6^uVyR?`*xE7j3si+HQ z_t)L!q&J^$sKsaAxU}Fp;+vnK+43cT-p80cnpn8HXeu4W&HVVso6uv7+cwE_hWM}2XD)7zP>b&-kvn7ao*P2n~n zKZ+vBun+rymziR3s`U1BA#N~`wCwy<((;c?LrbS3bw#-gk!Ug3;C^u<2o9Hwf=|0P z|2R7Gj?u$r=*jRC9PL}NGI>9MFJbPdJ`md-<5bjreJwMuD*^?nq}t_2>{f3b_Q8VG zNgCtiqv(1Qi6k~K7v$ z_s4oh%zNKNJY4$(2Cn$x@Kf+}$DWu<@xJCy-FaZGWJjSN!;pV_H;`A)`wgYr#>--> zJ0~v%5jWnaySL?w@^-1t*cvrPUZdT+C;pKrcSLgY2^!WjwP0$=&bXoe8{1R*ffQR5di@wFf;%;a!;fvbi6E1@iD5-emzKSV%q)~qbDSGk&Iv0ObfLAE z+Grd%mnazSG^xwLI-nQDEvHFPmFkZ3!t+{`(gYhKnSb=s{EHWCnHUhT;v?6}TKVR)=9VX(UKvUH+O;=9J4 z^_y#4uW37o5%Fj|Qzc~mE1D7ph2uLNuW3kR>?X5^ZtNC=aB6-T#1;X{<7TmOGpd4F zE*bK^KiY9tq790INfN zOfN5WMqZ9 z8`;Q)N{;cm9$~hFR2{=kXAn&M$V8Ih*uv_|U2t!|UcIHOfPzmyHXEO4(^pjC+j0m` z;CpBWYds`&Z|%Bpq!Kh~lvIeU+2>YLZ$D*Rb zXp=W{$GqE>lR8nC?!qU!=5kiqzoBHNU+kY$sySHE%{zSEE4vX@8THa#7+r8P%N=R^ z;z8#XyS1gre+vyA6PH@-<8S}+!AdS|ZuPlp9M4+p-G@0>_wmCvMf(}YC#q7C>pEsJ zQba`#YiwRFjQY@}T;DxV@-cc%5x+1!zZV)Ls=hri26MQzK3Wrys@&niAmC&7dp)(q<;@;!6)Fv4z5-QILf+%Tv&R$s-?mbtfC*r?9AVU3y zCdj@DV4QUZQYk=FtXA7hHvPuzi&?T!_FI{g+846=?Ov#hHRBDl0cJM%@pQZZ_ln#g z#AJ}N_Fx<-AUAl}U>Vff2A^w6y~Cht3dt%wUIM#SJzpJ*PtDJ2QXq9`hL2h8@{J&@ zd!uuwnN>Uu>`omfzojMzbj^|<9bcmsx+zjq6Cmlxfq<@&q4Rp{YdM$$RP}+H#lDA| zCuD(FLIXjSA4ZNFLdY6^%kQEC+nbCGZ~mMbdvgr+(!($8Cc8}pydcozyw3wy2k)MY zgo^GNrH=uuw)dVaVJTWlX&lWKM9iAFz&X9&%bPVceA@k@#mBF#_!iU?_GTxI`znRC zq5Amfx)np!!KdojA=+z=>}Vc&aOBE#sk^{Ea^13x^jt9RyQ|JuMWFBP3IjXLUw)NI zjA2CSsvIFLE|N+)n#)J_j=4gVwP#pSED63}o%~x#-R?5AEik%9ZQERIj%IZ(Z+kk& zSe};pOfPhwpxz7(HL^q;aSt}Znn76IU%Z+lC@$6eQiPcsQrcw?` z4XJ22?n_9$qD?p!c>QuhrL}(TntA*fA^0x_0+>urbW6txP9Hb-F=Uo;JkweIiO*V- z7rp!ibvC!hYv`JadWLkB%KTRFp+M4$la)z#37mA-+{gfJmz3Xp%FtC_8?XL|G#a;( zhj8VOaSmQ_d-psPw4_SCgjJodv?cFU&$5EL_M<y zF255fLxHZVn*Fkb-KyUzKesqaww~LCeB5J>yZdS>@FpI|iLRZiefjV{#ctK4$#PJj zUT|)dDqrp<$W3Ikv0#C*(VptdU<$XWasRg@e+?qubBV*Y;rqm6f+Lz5nJk|1*9U!? zotzf2f?fEB6jA-ZjcZp;?7PJ0BA^RM7_)+WlP>^b<=h-*aAjy^-55 zYQFYXTeW+8?qsNl zB&=?!v^F#|!jd+++X(mMuvB)MtoNKx#w|A`lD0=ti4qIg==MB!x}ERgYPzD2SbE}> zA)#EVlf~XgNj1o(%V29Sq$U3({G4+5yGdMdpKpw~?(^?6=@RuDW3`6X%^OmQu3wC| zWDP4H{dBZGRZqUe-n`+&ny@sJ8m_BaPrZ>I9tqi;fuAE^>aHj${F^WSn|BsN5V0Fh zVjbQmV%7RaC4-@#*x;YMb}!RAELG%H&OH~@L8P>SGSA8wk@GYihzH_bbbzn$VQVvc z1z-j;Zdra0;|Ud+{u;*fQygcn7J-;42+G=g73J&Os94r8nHVf*;p!8tBK8$O|pSxfc+|$9wViMT=#YBN^ z=mZnfdE8I6P_>+JAi|TXmRn@pM66GTU3AyuD0O^GThvO2ngzjO9>%>KqXF(;*j$Ma zrQFPKvWiht53-?f2=!Leqx$NAj#&_m#@2jj-*c%T*G%cqH>~EqAawNg_VO%-E}4Vz zARB**J(j8$;v|k?txZ?h%{i#jZl`M20emutPzPBN{507UHM{VC-9i7cr#fMIl!c2Wt9{D}Xi23H4@+%6{HCPA zU80d8G7`$nkn%yipmr_MXyJslew#c_$3vONLN2?kSSx`s3&)nwq&$VdI)red@xGkC z@FhCd9g#$i@B8cx;lX_BKm53&#^u;h;KI@{Qf@}p=IyeL>2i}o2WUPui$grx+vl&x zqgha1ufe0){o&8|20vFXc!+j23)8I)=nk#gYQM>WJ`R23`lN{Fr&1E=rUC8C?IsdT z){I}VniQB#ciB+deIR!@R(^9mr>!8m^*^J8pK~0lx-;`8O zyw>WwV6F~^tDhfDk5sWBtB3J$8u2G}uHHd?@Q<$wqnb?bb)n(Z|2|TlJ0mwZLN^iK z9S(yimaYgYpkJp#|C}k7JPyf=r&8hMkI1Yllr!lPRcoM9|E`(ZNx%F02d*1()sZ1!1JK1K zFUq(TsXgCCwb}BZ^%HE&)oVB;*?MAQ-lr7e8<`9)^Yn^4ageAzn2}DZWHxlzW#}Js z+TakxbmGPsuKa!c=74qKSe6}h)fTIBn%<`S_mJ&(g-aX9I$QrMZSRw} z#tfYzZ%>31S)dvBn?Zkn+Ase6w<+UeV>%PK^F8_F2NeQ7k>4>o$EzvvXr&(H%}l9+ za|g$&!+xpjzmEY>rCw~!;L^h^?Wcb{x5NcaySL`*Q++`xY=0 z6Fc6LX|u&&!g z!v<5t)z|0_TEDSb71dzXYsj3oGW{s_+{ZtJa6Ndo(n4eF7)xtn%U5@{NWk3x6Iad_ zwq;H4(3VTy-_`bizTgint5EhCNDA+no0cY;f6V|t2^?0fu=wT=%7y{O)?~P^%d6%}<|dbqB&4f->g}-q`SO3a_rD2BlLK8KoRlhPVQ(>89hfKw;{ZFvutOM-=C z-y5U*V(gDt$To4@gyj}7sxhTt~men9%=sQ==Iu|dewhE(|;IvjvQd|tR(Qi_;XVlz@ZTOPO|*@ z%Kk&RTaR(K^W>y{nGN~HE45z(qw!yKlKV5?{7 z!*o^~)K>h3uKj8epZ&B3X8M?4efJAhQ4$6eWJ?S2S0`=+j3%Zt^YOpP;?D+_$3Q)$ zCTt=6`%&q?|0j(Z7)@FH+rQ{x#7+R?$ulWr_={^DO$UsIFiQ5%M*J^w%Srk8PUEED zD7`2^m7m-_GO+Y2}r z#t<30zbMMLih!EAO}PElmC^!6^Z!ffF;wqw{Jj^zpGxe14d6d((f=C2e~wxHPd9-7 zRr)_%6X5j))Qg#fEART8AFX!_?u8EGRJs)FaeVl(cvYG_!Nl&gg~0(_CwBF~achi5 zDwLRH2OzP>P5(@0{Wot2#LqxH_Tiyi9BWvq;X-hPNFt@@w|H~h(Tp zJJO}Jf2EUKJ{}$X3@~$ryq)ubUQ9=WqHF@rma{bRz$JC7d;B|&J0p;dsh0?X4H(2> zcoDcYL(_5lzfTLuXmma@9d9iHuFLEOGQ5;>am-=PJ4sdFU;tghpnlMX==btM_TgHR zVTicyX2+M*l))nAe{cI`4Tns8J&`R!@vleT*L0=G<7vDC-J{Dl;3=?FoA3v z+ULaoydnIXc8by6nSq7s#&nn!TJv`SiXHS5+p59gZ< z5{r0L5_6fp+pXydV{=%_l!-F9Iy>O7INjE%&p$>`Nd!I!QB&V+mhV5r0h-B^;LUsT zW_h0JGi%ncUm$mgP#4G$fOO0a7{qnN^ws%MIfvp8QQ*!20TWg~gUay$X#jZwr82!^ zu8_pTQa>4VFf2VJk`wVk)1OwS+}&fa-_M?)7hY-tN0L!$qEfXZwCQ%gZ5bp+K`*A<-Pb z(mJm($cg1*nfv3c_BT&fLOH#NDFntE@4v@lr*Ik@<}h3R=o1br6Sr#}5HSv_Pc%_1QkH-2s42D9|dGQ|l2r zjW7%s5x_={dEWRFv1(WFU7sSGJCj{t1cSmzou%9i6wo!hTgxPw9{(mGdJ6-S@~A+Pj_fh zRLhg%b+P%?Dyt1&ma!5tISzOe(j?0=ncUIZGJN|wcot|m#eJ`^d?3zwmgV7P7mZep zxoNUfqWcr!85Z`2tbY?>{G-tnok6@BeTN|MJ1q3h<^*LZz+_>)m6yRI7tc`)whT ztwmgNBQ8xRJA9}>(Y{I7)ktZ>`W=x}PpddKkn{Rv^SM>S`P!6+9TtnH`Ela;O4fE? z60ayWb21eG{qMfZ=Dn4hP3!5oKD!X?iJYyT$JI73099K@wwW&U?w%~%cs0(Sdb?h- ztpJx>K-*;F#>sZ`?gb^g&%2gVCn*=CvyAhfj8Sy}s2oyha=Qnh%DZk{hv;X_z}XL+rgQB}l3HIBsjSw4<9n_k6r7m0}(0?m`5 zkhlKLH>h&~Ki!C{gUGWb5|h}vf}Cv^93kAze3e_R8C~UmTx8S8CU5hG{RpC1VKv0r z>8y3cv6C}l$E;m8g+(%(+84cMlN48Vq4U)!h{8G)E7hW@2fhdPv&T$)qm$t;b;x zL+2umUq~;h!xwShODbD2t@NTcXI1J=s+%)uedQih-1Q#h3@8YWyvxa7TV7A~76C{m zO^NN=3#y+@7K(SMOj4ZGfBxyNb1&IV_t| z51%oOsZD8NWzIQ{zVVl&L&h;{J?^G-x9m}x1a2#4G|(zD^#AXNNFM^7mQdaP4`RTL zArVmuA{?jF>lWwgBQN}UVINdIkW+iBP;+N5*KP_){Vc%HSe$`dI1@+1uA6`>tN4JX zZ!0L7y~#Qrmci;-YjCJqU{T@;zbN+A6-!aWVV=j0*>r0d&rHsQ6O@p-zF_5{pf#1? zUW3DZG680Kd15Le9b0AjB%Z>JG5~l<({vcaCY=rRZ0;R>32@>J&i1!wjACbJQF7qm z9hWIqb+BipC1GhehuyNf9pH1uwU_tXu-Srv9V_eoY^4AcYk+?pma&XaD6rph+Yq3X zmA<>asyWC?vN$=2AkzEnvf#-}2HldJoFH$wz96$horvc1_j&FCJHpi}gJ_V`1WA;j z0#sh)bi=UrH;{g(h^Jfk^94bm=9rSiM9EWddKlz=F&bqjd0K!~UD2@qJo_6t$g)=Q zcz2v>q+F4vsC@9+?m8jsLiDWlrUL-=5H*$w^v{jhe+NL=ZwuFxj1%ObuGUS{#*xNF za)Vod&IEN+vFV|4krWENH{EW4DmXoaS2v?{GwEJoSF==4Jy8&WXA-}7|GJ3JpU9@) z_*Om8A{i^clhS>gGReBm?3A3SqfHXKoQ~H5M5w#?7k1sD;NN#^iSDiW5rCSp@(C{@ zLXNGL`DSX=QVtArn$&dYA3iUukk`?!E4CG60u>b*c0cr&44Y}bnQRWXn+|>J57HL+ z87r9;ly;H&cWT9(S57%ivUS|iV&6*0P7UWA;A?C(BE}|rt)F61+(=KQBfBmYrcJ9N zwjD~9LJ~pLmw7TQ&1nyP6ApxK`gpY{`~bn-vzvi_v>2>#p~Iw7=3>RE%+a-Pu>UL#NS!ef{pFmdIkn%kQil9A%x1OS)2yph^lB;n zY)b>#GikLo0C=PXo9e7E{5pY4X%oo^N@4S0dnlE4yl@0Qmmq)(JA#`ruvFI`(343g z_E=6^T^w)Pos6f}WFNWjoW$@geOjSyR4}i?R}oy>_-@c)p5GmTZ8_pF!{MItH;BTY z#IRx?#X$IHPlbo2cCD{!Tz&>E2LQT+`z&UQ1-MOZ+T|K9jO_Dw$08G8!>-6&l$lYX zf)4Cg>@{6VWE1w(zrB{-Ai0w z`D(F36^Qcv8KUNv0=;R$(Z-DKi>(+(-OydwP5vcq|Al%vB%uW$ujOSZY4q0~H`*+S z*qx|p5E6C*UAUu?hVyeuA~DPetH`X+o+`z$DkAM${nv^7>#&mMgg~*!U*%XCnS1-` zo%b3cackdR7?}?@X01mTU`YTD*HeE|*P!uaqMt~3zs2uh#^PpxYW6Q2&MN49c!;)T zIWM;B_pYby-N82dh|DO3AgT6e1Y2X%RQjML}DP+!<5it4)+0urMxx-y%AK8kX1NAXF|`tu#K_u)TI~A$ba_OY!n;k=(5~o zFkEo2PSE?C^MEk&o7;|R^3N!DWF(0jgcZ?vxMjf|_~znzs2zuhj@dlP~M z3kmK{8h3XmxYIyGW5Hb-m)V>%Gxg5AM{?`dt-2phRezwU?%lBW^E_*B*Kd$KC6-3&-Y_Cw{NtACAixyK-? z5f=Su9m0%u-1P=Cy)qGWtk1P-0XJFGK`G(}Ii<%Jh()Gso8si~`|Bh{p-O&xd{vJ` zqIrJvu3sJvVan~*!d=ZLj$Ncri#{Q_EO)(i?b3TmSS%pAtE=1r11j4~kJp$u#5PGb z<8$h6l$zt{bQ##v#c=`TwiJ*F zM*oA@UHz<78NGq&25+227nma1&zBTC0e4^5Th1W?^~G5?9(I=jN}sier1Gd(LrOjy z-O-#LukBnk6irEzX!laPrOa}RVgU;$Eo5I_^xi&_5AmyAGQwE)6@oZIB&$L)n`(Rc&E< zu7j!OL7CZsJ}VflOsj$%Qfy8( z++JkcVgzq@JpBU_u1G8W)_GEUDaE>_!=((S(M;gjr32uj- z@3}@4R9s3kq>b!|LO&*GTatAHW4vKAraba2+RFNW6;5apf9mILjvZ7-!mKv?fD?Vm zZNDivC!ZBo?x68om)8t=3SU|F%Toa#e6PO|yDoL>F6NOc(XuqjClvKO%(-K(?`hbt zXSg(xeH#0;m_p3(-Dp)5^dp;~&A{6{7;Ho>)DMiTiZj`jDe3Vsmi<0uRdRDxHCGNb zcd1DP#+1tQG}P@!h@5ItLeBZNuq)(Jb-h5(rNUS&Iv)J%U2tDeq!(dS))~OwqEi!^ zKW>Pf`40KKa?jXJ+xhnX`*pBhxNtO139c6o4R;Sw-3RNcvDvDH_yhX-A+ z-YIWp&^zs5&!2x~pdzF%-@bHby8tLOIPDfcmp90_6<1AZb5r|VA71l3T}aWiT&tAh zoqypYx*J9#CVG1@O!DjoD`xhmiL5rlp}$BH>T$ZG^+yoVi25VZ1W{=C+-RfwvG%om zJuvpx0ZVANQoQ#_h7btS*#v{CIpME`CU079op~GLnIM!f3zgEJWmNjvKg%lqyg!Jea0A(tQHqu4JBjI>kn*4<%bWiF_uMV5V*mbcdzlHzR6xaKA$&m*o)gn^M( z7devaCbGFeT6=51;~EiOR5Qz^oZ@5J@1$?nhI%2$gPrCwYhJYF4C1y~_=A1E3h1U{ zvQ$k{Uh7Qb4hvTd&Eg6l80?g;;FL*Haq~{PFL5!9VaWvOJm~;NG|~fFd_B#^cY7^> z^|I=nUO}!Fs#x$}uc%UH_e_JB~C*cw&C#H#JNJ<|f3b=o;pguosMTQRf57_tsUq+VmxphZaO2xHa#z0 zvLBb@n;_d_%5Qu~)tEZ?OpSCa-`32$yMQLe+a=j<`H;RtZgciE_vEi1&qEU3S3xDO zJ*tZ{(GuN&?aPeX^s@O(f2JsBT^MK;n0bkUhV^S6thD5Vb9;7i$Ev1Veh(!a0_)*; z+I&4lCRO9Zzb`h-m>-(#ImUWGobQgg^dFU(F#=T zMl0EE`LWzy9cZDM(`+$49yndRC~D?U`=_lc2GuumHpd2xe7x79Rx!sHee7oQMk`Ds zuDvS*QwTQ%w>H2>;`0untjmjjEvcSE?6=2V@u{o<)9gAj4g>L=Wk_C~o}Wop1dQJ& zMovL@Rr*RXQZ$|{0Mud|Bch@{+BL|pnd5V(={%_ho~yOLP^WfV^qt<}PRrwtk1dJc z&E5vAOLgs;q)lrZnJbBY03h)aXx?>{*m1swAJn65Swe+tQURBX=b$qgNKByU_t-8b zfkSNInzr-KedOZJZ-xFKo~ttf+^;Ws4PeUWU!OY;(H8vu-46_$x^?A%3`am-1c`oe zY+lz)YEdM%~0N{WZUJ$HmpBN9o?1FHh7LG!LYN--zNvaA&e5S!xK<$SOC za6SY-&Je_A49irpkg%Y+(s${mYj z=Vp5qNQo$S%KO$Z7K}PR+ z8?*+iCu4OAN4SS3!^Z#QjOPLPo5Va}@5__g#dgdPZ2_S4t0-02_tHvyusOj3VDR22 zqhaS=>^3Yc78AD?1x3USkV8yKST=8q{q)k|QV@GK2^j`R^=FD1E9yS+Y$}?gj!BlKgeq{$L(rYA(q8z?=#M{&z-iM z*Kv|mciQdAr=p|wFZXN}P{0!Hdg34Ke90(?wKy3++~(|>RyTKiZG7`sh`ZwRC7*S0J} zO?qW~3zRF&!^x}OQ#w!Ztz2&V#O?#|jZFxZ%e3f}uA@_P;am#gFM-RAtQBAqZ)jr9 z)mYV2L<%10a*W#k_m73JVcWC1joBQ_>^GAICC%h8ATWnfoY`Y+=0Q`3+TZzv02y)d z99xoJ2|%A6-X8^xVw*Q!I`>dpyr8j+*UCYM(c+@RR-OGR{ei)|n!^OjC zKb1zlTx=t~q{nc9pQU=%p??F>q*Jsw4)7ZFThulRv?9)Q+*};7(&`kuzQvWSXb=I4#lzoGKHv6z?mWvz z-&05e^g;z^BMA^D-(=IjkDHel-t;HswVE#Ps#`hT6(U5v8M8dY1f3gY8~l#v1ebg6 zX!dgrI>cS>z-tt22AomHWn6N<|9g@FK#{iO<0~_h%w=YC5-U0d~!)0qM%5MRu}=87GtE)lRrV`{gP{>kx6`LKzUYwK-ZriPuGcnN;;} zBR${drn6aQ(d)&I+QaUokVMbRO#lI(tjS5ADcNgaCwZ+?tq+x_3JFo3@4yv%rc8PW zpg2WH!4076G%;GEAdcwyNale-{oYQ6kLrYap5M*s0{iJ4I6@ifU<|+)!R&kWmVY~2 z1@m@~huKpfu=_Y9q&z@T4=&lOQ1Ds`Z7|7bebArm{3p?H{vi_;PU*SDE>fc89AKL#n)Ep)t*_?%wT2ink|a#tKc{;E zxn(~r;wqIWUC0SP?4cp)fvm%w*^rv!Q}|Mp)D$+AhJ%J#%?c@(Z*@KAky>s${f9dh z!@OJCF@KQWG1tFIdg78}yn%+JE6RPw(%^--2ku@U18}&|f(8GtascS-M0oD2oz$OS z4EyL;CWA+Nxp3ZpBPDCOMc&w3@akv+T<8$wu$O$J? zEYWYGP|=$w#*2S#uAJTaF>y54=MKW@*gXIgGj@f5^@!zsvI6zJYJER2q@kO_wwcWK z`YL<_-eNnD$|Hn{gA5wyL(lCwvjq` z%EJdh35iMb1sMj0aul0a(RnH>ttdDHLFD6AE$3as6~@v+T?1MbMl&<8NaIAkodB!H z6L8x*Qb`6tK}w|40Zvi>m^ zBVe(C0gh+ry9q!Tqf@&nvNdX|h_Qm)H!^}IC@=p&R;x*e+K25jDJD1a#N852GTr#m z+F(*#5@Y_)Gfwc-H|v`D>b26geRK61MDR8rk^W3oa6(y4QqT0t0AX#JJ1_+5o!v<=97kY-<4f|N8Ws7^l8BvdU``P{!N?{&8aZ3HLd+ zqEmzGoV}^=ZRb--V`nOWd1m{@znrUCx)?Px9*9dp4ek!h+a~y$i_rd4&GwVvOOQuCiNdKc8*B>biYCoj$&cG#_XKfxGxYWI zN&6luijJU?sSQi`GAg*IImVrmL_MbS2v}FJlY0htC z!x+Xr`R;ab))L{lU&lwxqN%v>0}i69j<6PMu@0l;EFbb<-VUzpT)h*$^STzT zxW4i8$&I5!T@j5Fu&L{c{b3^YC-}B@5+J0t^fc;*=c59lIEuB6|2Z+MHff5GYcPCJz()jvP5{=dIDg1pt?t_*V4JM>$BXmukOC zmJxJYVCl{L{+BPo!`0@J=-Nuj(1Gd6%xl4a<-!1J@N3ji2ca8}GrX;%%=?QI111q~ zw#7s|VisLpXu#Q^pm{Rzdoo{#1&!Fc!Y*9Y2JLHn(tljy|9!>yDm)rPkGK~d7VbRc zsUQY~6_szF>?dIpf;aM(d*g7z^?e*I$hJ<^&rT*!R!SLI*Jq0-nlW}pmAG&N;TUPASNnuKLQOfLO{iXaQcZQudJquV+=i)hle|z#aaX| zPd8H<=0~&McLy(sFwNG~WZHn!?y}DqdR*oCSIc zUK@z#aa2z^n+HJkjVM%FmQi(vA5S0UGjSL$dmdJm&=6q~v#`wA-YeUe+Q-&q^h+a& z5f31j?j5JiqYA8%k-3uV@`bTK|Rj_-ku58w#13btB3j-Q1M=yL%1lG0L%ER%Wa?I{QG!V`D#!trgc-$-T^hvY^ zph4`9+U-40ipH*`j-al$=zBCtR*i;!&T6?J1E9}i-J;x+en7KbdfP`owASG^^GH@a z5KGJ4M4Rbt*%%S)Ai)O>kPkQ|qc{d9`!Qc*>4;T#YsZIs%k6SV5n|gkMQ2?H8$HS*0k3*7e-UXG^L{Wv|CoT2hckb&CuL> zZ|Ci*>JY>yp+332y%w{aAM2(lrtw3=`4t15@fCE=zgw>d%qMj@*ZjY`9@C332U10Zh6JyY-2h*a zEaKifxB`gW!U4)^^h1AGIBA(jug8=B&Dt?Bk68HW1z>sZSD9KR6sa@-h7aAtYZlt0 z5}^AYX#Ngh?}MxkgmPzVGW)ZxA*n)hG5&#dy2X{t zZUwEuRi==@W=q7ko70(!+`-J8ghyOvhA&ywM(1q3rPJULYVEfV?i zzOozbikkvNQH}F}%8MQFdhxBKHu#X;T%EBCs~z3|J<5CTO!tKs3t!g168+i-H4s-) zRrDYtdhnKUOrqZDJ3usydB)+4c`A8G2@;2VD0}`;nNXEb@{uI!=zgl!OQGI=&np+L zzT)*GoiUS_Q9hB$zI#-I)r7l$+H;L^Q7+xmvk|J#x;`oiLehIuNYi z0?GQ)5%5AfGFLfA=27-cgKL_g(?)5%o@ijp!4+y=2j*5;&lgx58_n^nE1)Hzl5NXz z@ode$xbww+KT{j)W7VlbE!Cy=fZr%{x^Gj2X!oaT7L>$p53;D^uwAYVYP~LGnk@J0 zIC^rgg6R7cR0AFTk2iYSlU(7|R9o2`ErSktU#>Nqfv2FB-$Mh-lxVP>Wkbl`Yn zxhrsbHFkgA9Eq_ROWx2`o(YC-k?w5o0Vy66sCiv&Ya(xVYxCmfszou)>nN|8EtBc# z`u6egSplogLae1-iDPkvsgHIHqn?cMP%>k>SI1uzV?SjJQU{67-&Qx{WPg0MFtoE0l82lvEadDjiKF9gTS@Q9WR{B@`ZK8) z=I$duOZ;-YK8Fc#9;a@7fGg4N(qQ-vA3pMPMN}c4 zBSWvz#uAv4vaPC)b9Cem%^+`dX}f%(U-b7G}=B}&Fa)T1D(Z_{&GW?A;lV=`nJ znb7I8X7{!9`T7^Ttthy{={@&TLaCm62QK;Lc~eJMHbI8PgqlPmTs~zbgO3MVosT=8 zYN?Gr3qV%*4aHb#!%Rf=!!k{JBLx=w|0*m{Eo&s_v?#n>^b<*>e#f^K7nFWRzd4(3 zoCl1W<@N24o6r0bwmCkVbQg6DrGGTPf0Tu3P0$!K@9jzuGkq-nnvz2-Sav7$$mlQG zTgCEHc&+2Q&p0CfHkQWPEq*0vz^``?Hg0!n3BAyPym+3C5WBl(zIZB_I=%B6a(oc& ztbThCiFUXl!gBTv)1Sc6UII)O>bN0?;&DtrfU3)`k#uY@|vkL6R&mw4Yppf$iO;k); z<)4Ob4*S_#HfU&T5!8Tk>iasGIp8LRE+!m+@HLluo5h8ZD@$M=Ko+e@0*9f zFJVEg4g1zA1?okQJx%e}08;Snq^+U`Yit=G))wGnlTj;DW>oLg--yzw_6i{9xBKuW z1yJJ^DlR-gK`ST&zqhcDmUcSX;GC@4|Emd*Wz`hK>3b-h@g(e;T=pk`CUQr?em^Vv zIo-d=aZ+zG?7X9)?R4mmEyDN}0@h?`FaJ%D@@aNLdh3T2wf&gYT&)(ir?tO+pk~3R z-sK_Q0ZIV}RUVtMIjtbP1mpf#``E0+wz~X<^dF+8h)Q%^>M^8bg5&ytDjtE`ZhHV` zdu_{8GTP}wK;x_wPH|T+zb-;@%IVRMTfGD9V`ArRL>5$qZ{MXNO!V9r<sA>3%?PVbjwGj1>@ok;RrR2)n0F-n46;CZ5&nup26^ z52WJLI$XR5wTjnw3l)(?HIDB#M|8 z_g!5(S{y9;7T-d=UVp0|z-XX7N#Za}w*2@BUcEb1_3cW;=hlnuaHVDe0tGnNwH>|) z{vF>7s^_zkB^Z4+3g3s}dv5)~d&sY+^`JZ7dcF4-)fr8|Vd|%koV65J#m8?6YAybJ&Go}ft z*xqDAT1TsV5ZIq#7;AGJSP)YA=rmp{dbR1-SbnMD6jM);|YiH_rl zxsNuxo)SZtTPwfLy0Cc4 z{%q=V4JpR^0e4?myg}nLnd7^K(#q}7xI`2O1d_IaQY?BCSJ=FJN|a)d@)+K-T?y zx{jV+ZzsBRD-6WeWPZN>uQ-NJW_{Ub^I15*kdvm1cFxyHU)f_6NrhMBsKj)Q2a|#K z?ob$_!)!gIinLk;3>vhQs*JSMYD|b?1{~SlhM6^aia{##^dM~KU6Eh-aT&?bw1ND< z(|u~yE#^5 zujJBrtF~rxiafwe4$i-)VIEI`EL>NEh+TU`-eYi0t|6Gp4Q~E5=A))qWv^L-9m|Gh z>+wSwpI4`ct7wmms_EDh?-ayt=Cs1Smx9jdd(pX#1s@j=q&JDa7%m39#cy^LYV?j8 zA&U@K`0gOd5LaIZ=2@{G%`fh2Vx-e4bRrJDSv3tptfX*Fn;>dkK3jL}&0{}C2>lgC z#+{p|^0E%Hlb)JXW2I&ZD;{4J2reRv4x+gu>WhoA>}p97PeDY{NCMgnM+m=`YC%kPBrg%=&ThiTKPm2aTHGzKboylMHzyja8>Z5WpC z5VK`*{L^`;_Nj0qsb3Epe8gwA&|TIuCQ-z)UiQt>?1t9o+DxOI{9?y7=}eMCe3P8M z$q9<%_1urkDLuTUZ!6Kq`I6s=zCv<)nQh6C{MJ2R4c`>p7xjzrbDYYtbdBMcHe&r$ zHa^d&Ki6Ic^vLW#pXEf>`_m&E^f{^byP7dg zvG?6?5zDEFgA=2K;tww3 z{ia>m-t?+}z0DwGkPys>q>e%GGj#F&~-Oerp_h_u|(U;PQNgT=P0@3NhS)thmL#MURy z+PclOZO*WAXHV8l?#+B@alJC5P6MGK*))isE?iDBwnYq?b-A@+-y-KhCEP

M$^a^8jm41-_{C#PI0dj6faq1`ClUL&ToH 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 DsM0JO literal 0 HcmV?d00001 From ab85d9694dff12eb5646e5468cdcc820a647b396 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 14:13:03 +0100 Subject: [PATCH 425/908] Fix spec --- spec/features/builds_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 5213ce1099..1f99a808f8 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -19,7 +19,7 @@ describe "Builds" do end it { expect(page).to have_content 'Running' } - it { expect(page).to have_content 'Cancel all' } + it { expect(page).to have_content 'Cancel running' } it { expect(page).to have_content @build.short_sha } it { expect(page).to have_content @build.ref } it { expect(page).to have_content @build.name } @@ -32,7 +32,7 @@ describe "Builds" do end it { expect(page).to have_content 'No builds to show' } - it { expect(page).to have_content 'Cancel all' } + it { expect(page).to have_content 'Cancel running' } end context "All builds" do @@ -45,7 +45,7 @@ describe "Builds" do it { expect(page).to have_content @build.short_sha } it { expect(page).to have_content @build.ref } it { expect(page).to have_content @build.name } - it { expect(page).to_not have_content 'Cancel all' } + it { expect(page).to_not have_content 'Cancel running' } end end @@ -53,11 +53,11 @@ describe "Builds" do before do @build.run! visit namespace_project_builds_path(@gl_project.namespace, @gl_project) - click_link "Cancel all" + click_link "Cancel running" end it { expect(page).to have_content 'No builds to show' } - it { expect(page).to_not have_content 'Cancel all' } + it { expect(page).to_not have_content 'Cancel running' } end describe "GET /:project/builds/:id" do From 5beacba038c097e513b46f24ee26d5065ad419c9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 14:13:57 +0100 Subject: [PATCH 426/908] Fix spec --- features/steps/project/commits/commits.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index e5b3f27135..0d6a9a8fc6 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -118,6 +118,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps step 'I see builds list' do expect(page).to have_content "build: pending" - expect(page).to have_content "Latest builds" + expect(page).to have_content "1 build" end end From 41a4785b855a082197b3c22004cb8af96e5453ee Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 14:41:19 +0100 Subject: [PATCH 427/908] Fix signin with OmniAuth providers --- config/initializers/omniauth.rb | 2 +- lib/omni_auth/request_forgery_protection.rb | 63 +++------------------ 2 files changed, 10 insertions(+), 55 deletions(-) diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 70ed10e827..4c164119ff 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -16,7 +16,7 @@ OmniAuth.config.allowed_request_methods = [:post] #In case of auto sign-in, the GET method is used (users don't get to click on a button) OmniAuth.config.allowed_request_methods << :get if Gitlab.config.omniauth.auto_sign_in_with_provider.present? OmniAuth.config.before_request_phase do |env| - OmniAuth::RequestForgeryProtection.new(env).call + OmniAuth::RequestForgeryProtection.call(env) end if Gitlab.config.omniauth.enabled diff --git a/lib/omni_auth/request_forgery_protection.rb b/lib/omni_auth/request_forgery_protection.rb index 3557522d3c..69155131d8 100644 --- a/lib/omni_auth/request_forgery_protection.rb +++ b/lib/omni_auth/request_forgery_protection.rb @@ -1,66 +1,21 @@ # Protects OmniAuth request phase against CSRF. module OmniAuth - # Based on ActionController::RequestForgeryProtection. - class RequestForgeryProtection - def initialize(env) - @env = env - end + module RequestForgeryProtection + class Controller < ActionController::Base + protect_from_forgery with: :exception - def request - @request ||= ActionDispatch::Request.new(@env) - end - - def session - request.session - end - - def reset_session - request.reset_session - end - - def params - request.params - end - - def call - verify_authenticity_token - end - - def verify_authenticity_token - if !verified_request? - Rails.logger.warn "Can't verify CSRF token authenticity" if Rails.logger - handle_unverified_request + def index + head :ok end end - private - - def protect_against_forgery? - ApplicationController.allow_forgery_protection + def self.app + @app ||= Controller.action(:index) end - def request_forgery_protection_token - ApplicationController.request_forgery_protection_token - end - - def forgery_protection_strategy - ApplicationController.forgery_protection_strategy - end - - def verified_request? - !protect_against_forgery? || request.get? || request.head? || - form_authenticity_token == params[request_forgery_protection_token] || - form_authenticity_token == request.headers['X-CSRF-Token'] - end - - def handle_unverified_request - forgery_protection_strategy.new(self).handle_unverified_request - end - - # Sets the token value for the current session. - def form_authenticity_token - session[:_csrf_token] ||= SecureRandom.base64(32) + def self.call(env) + app.call(env) end end end From f3ca92a062424e0cda2c077d9c30a4edbd6bf4c8 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 8 Dec 2015 15:08:22 +0100 Subject: [PATCH 428/908] Add 'resume' capability to parallel-rsync-repos --- bin/parallel-rsync-repos | 43 +++++++++++++------- doc/operations/moving_repositories.md | 56 ++++++++++++++++++++++----- 2 files changed, 75 insertions(+), 24 deletions(-) diff --git a/bin/parallel-rsync-repos b/bin/parallel-rsync-repos index b777056c95..21921148fa 100755 --- a/bin/parallel-rsync-repos +++ b/bin/parallel-rsync-repos @@ -1,31 +1,33 @@ -#!/bin/sh -# this script should run as the 'git' user, not root, because of mkdir +#!/usr/bin/env bash +# this script should run as the 'git' user, not root, because 'root' should not +# own intermediate directories created by rsync. # # Example invocation: # find /var/opt/gitlab/git-data/repositories -maxdepth 2 | \ -# parallel-rsync-repos /var/opt/gitlab/git-data/repositories /mnt/gitlab/repositories +# parallel-rsync-repos transfer-success.log /var/opt/gitlab/git-data/repositories /mnt/gitlab/repositories # # You can also rsync to a remote destination. # -# parallel-rsync-repos /var/opt/gitlab/git-data/repositories user@host:/mnt/gitlab/repositories +# parallel-rsync-repos transfer-success.log /var/opt/gitlab/git-data/repositories user@host:/mnt/gitlab/repositories # # If you need to pass extra options to rsync, set the RSYNC variable # -# env RSYNC='rsync --rsh="foo bar"' parallel-rsync-repos /src dest +# env RSYNC='rsync --rsh="foo bar"' parallel-rsync-repos transfer-success.log /src dest # -SRC=$1 -DEST=$2 +LOGFILE=$1 +SRC=$2 +DEST=$3 + +if [ -z "$LOGFILE" ] || [ -z "$SRC" ] || [ -z "$DEST" ] ; then + echo "Usage: $0 LOGFILE SRC DEST" + exit 1 +fi if [ -z "$JOBS" ] ; then JOBS=10 fi -if [ -z "$SRC" ] || [ -z "$DEST" ] ; then - echo "Usage: $0 SRC DEST" - exit 1 -fi - if [ -z "$RSYNC" ] ; then RSYNC=rsync fi @@ -35,5 +37,18 @@ if ! cd $SRC ; then exit 1 fi -sed "s|$SRC|./|" |\ - parallel -j$JOBS --progress "mkdir -p $DEST/{} && $RSYNC --delete -a {}/. $DEST/{}/" +rsyncjob() { + relative_dir="./${1#$SRC}" + + if ! $RSYNC --delete --relative -a "$relative_dir" "$DEST" ; then + echo "rsync $1 failed" + return 1 + fi + + echo "$1" >> $LOGFILE +} + +export LOGFILE SRC DEST RSYNC +export -f rsyncjob + +parallel -j$JOBS --progress rsyncjob diff --git a/doc/operations/moving_repositories.md b/doc/operations/moving_repositories.md index a89602b367..39086b7a25 100644 --- a/doc/operations/moving_repositories.md +++ b/doc/operations/moving_repositories.md @@ -96,25 +96,59 @@ after switching to the new repository storage directory. ### Parallel rsync for all repositories known to GitLab -This will sync repositories with 10 rsync processes at a time. +This will sync repositories with 10 rsync processes at a time. We keep +track of progress so that the transfer can be restarted if necessary. + +First we create a new directory, owned by 'git', to hold transfer +logs. We assume the directory is empty before we start the transfer +procedure, and that we are the only ones writing files in it. ``` # Omnibus -sudo gitlab-rake gitlab:list_repos |\ - sudo -u git \ - /usr/bin/env JOBS=10 \ - /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repoos \ - /var/opt/gitlab/git-data/repositories \ - /mnt/gitlab/repositories +sudo mkdir /var/opt/gitlab/transfer-logs +sudo chown git:git /var/opt/gitlab/transfer-logs + +# Source +sudo -u git -H mkdir /home/git/transfer-logs +``` + +We seed the process with a list of the directories we want to copy. + +``` +# Omnibus +sudo -u git sh -c 'gitlab-rake gitlab:list_repos > /var/opt/gitlab/transfer-logs/all-repos-$(date +%s).txt' # Source cd /home/git/gitlab -sudo -u git -H bundle exec rake gitlab:list_repos |\ - sudo -u git -H \ +sudo -u git -H sh -c 'bundle exec rake gitlab:list_repos > /home/git/transfer-logs/all-repos-$(date +%s).txt' +``` + +Now we can start the transfer. The command below is idempotent, and +the number of jobs done by GNU Parallel should converge to zero. If it +does not some repositories listed in all-repos-1234.txt may have been +deleted/renamed before they could be copied. + +``` +# Omnibus +sudo -u git sh -c ' +cat /var/opt/gitlab/transfer-logs/* | sort | uniq -u |\ + /usr/bin/env JOBS=10 \ + /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \ + /var/opt/gitlab/transfer-logs/succes-$(date +%s).log \ + /var/opt/gitlab/git-data/repositories \ + /mnt/gitlab/repositories +' + +# Source +cd /home/git/gitlab +sudo -u git -H sh -c ' +cat /home/git/transfer-logs/* | sort | uniq -u |\ /usr/bin/env JOBS=10 \ bin/parallel-rsync-repos \ + /home/git/transfer-logs/succes-$(date +%s).log \ /home/git/repositories \ /mnt/gitlab/repositories +` ``` ### Parallel rsync only for repositories with recent activity @@ -129,7 +163,8 @@ gitlab:list_repos' to only print repositories with recent activity. sudo gitlab-rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\ sudo -u git \ /usr/bin/env JOBS=10 \ - /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repoos \ + /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \ + succes-$(date +%s).log \ /var/opt/gitlab/git-data/repositories \ /mnt/gitlab/repositories @@ -139,6 +174,7 @@ sudo -u git -H bundle exec rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\ sudo -u git -H \ /usr/bin/env JOBS=10 \ bin/parallel-rsync-repos \ + succes-$(date +%s).log \ /home/git/repositories \ /mnt/gitlab/repositories ``` From 23f383ef69889c9829ad36afa53b5abfbf4b5511 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 8 Dec 2015 16:06:06 +0100 Subject: [PATCH 429/908] Detect project and namespace changes in list:repos --- lib/tasks/gitlab/list_repos.rake | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/tasks/gitlab/list_repos.rake b/lib/tasks/gitlab/list_repos.rake index 1377e1ea91..c7596e7abc 100644 --- a/lib/tasks/gitlab/list_repos.rake +++ b/lib/tasks/gitlab/list_repos.rake @@ -3,9 +3,10 @@ namespace :gitlab do scope = Project if ENV['SINCE'] date = Time.parse(ENV['SINCE']) - warn "Listing repositories with activity since #{date}" - project_ids = Project.where(['last_activity_at > ?', date]).pluck(:id) - scope = scope.where(id: project_ids) + warn "Listing repositories with activity or changes since #{date}" + project_ids = Project.where('last_activity_at > ? OR updated_at > ?', date, date).pluck(:id).sort + namespace_ids = Namespace.where(['updated_at > ?', date]).pluck(:id).sort + scope = scope.where('id IN (?) OR namespace_id in (?)', project_ids, namespace_ids) end scope.find_each do |project| base = File.join(Gitlab.config.gitlab_shell.repos_path, project.path_with_namespace) From d00d3444a45f81a5e79a1161acb44fca2df3c1c7 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Tue, 8 Dec 2015 18:23:30 +0300 Subject: [PATCH 430/908] Remove the prepended v on GitLab Workhorse upgrade doc --- doc/update/patch_versions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index 957354decb..c19ee49f9e 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -47,7 +47,7 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`ca ```bash cd /home/git/gitlab-workhorse sudo -u git -H git fetch -sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` -b v`cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` +sudo -u git -H git checkout `cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` -b `cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` ``` ### 5. Install libs, migrations, etc. From df6750d3d6b562c8a6a0a57c12dfd694da38a0e8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 16:42:10 +0100 Subject: [PATCH 431/908] Default target branch to patch-n when editing file in protected branch --- app/controllers/projects/blob_controller.rb | 12 ++++++++++-- app/helpers/branches_helper.rb | 2 +- app/helpers/tree_helper.rb | 13 ++++++++++--- app/models/repository.rb | 11 +++++++++++ app/views/shared/_new_commit_form.html.haml | 2 +- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 31a33bfd23..6216368293 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -162,12 +162,20 @@ class Projects::BlobController < Projects::ApplicationController end def sanitized_new_branch_name - @new_branch ||= sanitize(strip_tags(params[:new_branch])) + sanitize(strip_tags(params[:new_branch])) end def editor_variables @current_branch = @ref - @new_branch = params[:new_branch].present? ? sanitized_new_branch_name : @ref + + @new_branch = + if params[:new_branch].present? + sanitized_new_branch_name + elsif ::Gitlab::GitAccess.new(current_user, @project).can_push_to_branch?(@ref) + @ref + else + @repository.next_patch_branch + end @file_path = if action_name.to_s == 'create' diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index d6eaa7d57b..e39548e17e 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -11,7 +11,7 @@ module BranchesHelper def can_push_branch?(project, branch_name) return false unless project.repository.branch_names.include?(branch_name) - + ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name) end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 6afa1aacc5..ac75c9a14b 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -48,10 +48,17 @@ module TreeHelper def allowed_tree_edit?(project = nil, ref = nil) project ||= @project - ref ||= @ref - return false unless project.repository.branch_names.include?(ref) + can?(current_user, :push_code, project) + end - ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) + def tree_edit_branch(project = @project, ref = @ref) + if allowed_tree_edit? + if can_push_branch?(project, ref) + ref + else + project.repository.next_patch_branch + end + end end def can_delete_or_replace?(blob) diff --git a/app/models/repository.rb b/app/models/repository.rb index 1d43307e1e..1edec52c09 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -329,6 +329,17 @@ class Repository commit(sha) end + def next_patch_branch + patch_branch_ids = self.branch_names.map do |n| + result = n.match(/\Apatch-([0-9]+)\z/) + result[1].to_i if result + end.compact + + highest_patch_branch_id = patch_branch_ids.max || 0 + + "patch-#{highest_patch_branch_id + 1}" + end + # Remove archives older than 2 hours def branches_sorted_by(value) case value diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 31b02ed93d..074080e44c 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -4,7 +4,7 @@ .form-group.branch = label_tag 'new_branch', 'Target branch', class: 'control-label' .col-sm-10 - = text_field_tag 'new_branch', @new_branch || @ref, required: true, class: "form-control js-new-branch" + = text_field_tag 'new_branch', @new_branch || tree_edit_branch, required: true, class: "form-control js-new-branch" .form-group.js-create-merge-request-form-group .col-sm-offset-2.col-sm-10 From 3d654cc0faa67e5aee41fda31de095060714ecc4 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 8 Dec 2015 05:19:39 -0800 Subject: [PATCH 432/908] Fix broken group avatar upload Closes #3918 --- CHANGELOG | 1 + app/assets/javascripts/dispatcher.js.coffee | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f4f9332b50..c3aaa186ce 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.3.0 (unreleased) - Bump gollum-lib to 4.1.0 (Stan Hu) + - Fix broken group avatar upload under "New group" (Stan Hu) - Fix API setting of 'public' attribute to false will make a project private (Stan Hu) - Handle and report SSL errors in Web hook test (Stan Hu) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 4059fc39c6..599b4c4954 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -83,7 +83,7 @@ class Dispatcher when 'projects:project_members:index' new ProjectMembers() new UsersSelect() - when 'groups:new', 'groups:edit', 'admin:groups:edit' + when 'groups:new', 'groups:edit', 'admin:groups:edit', 'admin:groups:new' new GroupAvatar() when 'projects:tree:show' new TreeView() From 82ddd738b672afd4ed28f12fb58f512fa117d2be Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 17:10:07 +0100 Subject: [PATCH 433/908] Show tooltip when trying to edit file when not on branch --- app/helpers/blob_helper.rb | 38 +++++++++---------- app/helpers/tree_helper.rb | 13 ++++--- app/services/files/base_service.rb | 2 +- app/views/projects/blob/_actions.html.haml | 14 ++++--- app/views/projects/blob/show.html.haml | 2 +- .../projects/tree/_tree_header.html.haml | 4 ++ 6 files changed, 41 insertions(+), 32 deletions(-) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 4a3d971f7c..65bd543bf1 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -30,26 +30,24 @@ module BlobHelper nil end - if blob_viewable?(blob) - text = 'Edit' - after = options[:after] || '' - from_mr = options[:from_merge_request_id] - link_opts = {} - link_opts[:from_merge_request_id] = from_mr if from_mr - cls = 'btn btn-small' - if allowed_tree_edit?(project, ref) - link_to(text, - namespace_project_edit_blob_path(project.namespace, project, - tree_join(ref, path), - link_opts), - class: cls - ) - else - content_tag :span, text, class: cls + ' disabled' - end + after.html_safe - else - '' - end + return unless blob && blob_editable?(blob) + + text = 'Edit' + after = options[:after] || '' + from_mr = options[:from_merge_request_id] + link_opts = {} + link_opts[:from_merge_request_id] = from_mr if from_mr + cls = 'btn btn-small' + link_to(text, + namespace_project_edit_blob_path(project.namespace, project, + tree_join(ref, path), + link_opts), + class: cls + ) + after.html_safe + end + + def blob_editable?(blob, project = @project, ref = @ref) + allowed_tree_edit?(project, ref) && !blob.lfs_pointer? end def leave_edit_message diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index ac75c9a14b..886a1e734b 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -46,13 +46,20 @@ module TreeHelper File.join(*args) end + def on_top_of_branch?(project = @project, ref = @ref) + project.repository.branch_names.include?(ref) + end + def allowed_tree_edit?(project = nil, ref = nil) project ||= @project + ref ||= @ref + return false unless on_top_of_branch?(project, ref) + can?(current_user, :push_code, project) end def tree_edit_branch(project = @project, ref = @ref) - if allowed_tree_edit? + if allowed_tree_edit?(project, ref) if can_push_branch?(project, ref) ref else @@ -61,10 +68,6 @@ module TreeHelper end end - def can_delete_or_replace?(blob) - allowed_tree_edit? && !blob.lfs_pointer? - end - def tree_breadcrumbs(tree, max_links = 2) if @path.present? part_path = "" diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index f50aaf2eb5..309abcd6d0 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -53,7 +53,7 @@ module Files unless project.empty_repo? unless repository.branch_names.include?(@current_branch) - raise_error("You can only create files if you are on top of a branch") + raise_error("You can only create or edit files when you are on top of a branch") end if @current_branch != @target_branch diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index 0e54e59e95..41d8765623 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -1,5 +1,4 @@ .btn-group.tree-btn-group - = edit_blob_link(@project, @ref, @path) = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), class: 'btn btn-sm', target: '_blank' -# only show normal/blame view links for text files @@ -12,11 +11,16 @@ class: 'btn btn-sm' unless @blob.empty? = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'btn btn-sm' - - if @ref != @commit.sha - = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, - tree_join(@commit.sha, @path)), class: 'btn btn-sm' + = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, + tree_join(@commit.sha, @path)), class: 'btn btn-sm' -- if can_delete_or_replace?(@blob) +- if blob_editable?(@blob) .btn-group{ role: "group" } + = edit_blob_link(@project, @ref, @path) %button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Delete +- elsif !on_top_of_branch? + .btn-group{ role: "group" } + %button.btn.btn-default.disabled.has_tooltip{title: "You can only edit files when you are on top of a branch.", data: {container: 'body'}} Edit + %button.btn.btn-default.disabled.has_tooltip{title: "You can only replace files when you are on top of a branch.", data: {container: 'body'}} Replace + %button.btn.btn-remove.disabled.has_tooltip{title: "You can only delete files when you are on top of a branch.", data: {container: 'body'}} Delete diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index 09d6fc18e3..3f8d11ed8c 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -6,7 +6,7 @@ %div#tree-holder.tree-holder = render 'blob', blob: @blob -- if can_delete_or_replace?(@blob) +- if blob_editable?(@blob) = render 'projects/blob/remove' - title = "Replace #{@blob.name}" diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 1115ca6b4c..4767f824ac 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -30,3 +30,7 @@ = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do = icon('folder fw') New directory + - elsif !on_top_of_branch? + %li + %span.btn.add-to-tree.disabled.has_tooltip{href: '#', title: "You can only add files when you are on top of a branch.", data: {container: 'body'}} + = icon('plus') From 7aeb7077cb906933c59e5b890fd4027005e34d3d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 17:11:23 +0100 Subject: [PATCH 434/908] Only show Edit button for text blobs. --- app/helpers/blob_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 65bd543bf1..68e5d5be60 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -30,7 +30,7 @@ module BlobHelper nil end - return unless blob && blob_editable?(blob) + return unless blob && blob.text? && blob_editable?(blob) text = 'Edit' after = options[:after] || '' @@ -47,7 +47,7 @@ module BlobHelper end def blob_editable?(blob, project = @project, ref = @ref) - allowed_tree_edit?(project, ref) && !blob.lfs_pointer? + !blob.lfs_pointer? && allowed_tree_edit?(project, ref) end def leave_edit_message From 4f074aaa14faa8a866f18a80f58b66cd023a141f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 25 Nov 2015 14:41:14 +0100 Subject: [PATCH 435/908] Introduce CI documentation for services and languages --- doc/README.md | 12 ++ doc/ci/README.md | 12 ++ doc/ci/docker/using_docker_images.md | 115 +++++++---------- doc/ci/languages/README.md | 3 + doc/ci/languages/php.md | 178 +++++++++++++++++++++++++++ doc/ci/services/README.md | 6 + doc/ci/services/docker-services.md | 5 + doc/ci/services/mysql.md | 72 +++++++++++ doc/ci/services/postgres.md | 70 +++++++++++ doc/ci/services/redis.md | 40 ++++++ doc/ci/ssh_keys/README.md | 114 +++++++++++++++++ 11 files changed, 559 insertions(+), 68 deletions(-) create mode 100644 doc/ci/languages/README.md create mode 100644 doc/ci/languages/php.md create mode 100644 doc/ci/services/README.md create mode 100644 doc/ci/services/docker-services.md create mode 100644 doc/ci/services/mysql.md create mode 100644 doc/ci/services/postgres.md create mode 100644 doc/ci/services/redis.md create mode 100644 doc/ci/ssh_keys/README.md diff --git a/doc/README.md b/doc/README.md index 58ab5dd08e..a7025f7af1 100644 --- a/doc/README.md +++ b/doc/README.md @@ -24,9 +24,21 @@ - [Using Docker Images](ci/docker/using_docker_images.md) - [Using Docker Build](ci/docker/using_docker_build.md) - [Using Variables](ci/variables/README.md) +- [Using SSH keys](ci/ssh_keys/README.md) - [User permissions](ci/permissions/README.md) - [API](ci/api/README.md) +### CI Languages + ++ [Testing PHP](ci/languages/php.md) + +## CI Services + ++ [Using MySQL](ci/services/mysql.md) ++ [Using PostgreSQL](ci/services/postgres.md) ++ [Using Redis](ci/services/redis.md) ++ [Using Other Services](ci/docker/using_docker_images.html#how-to-use-other-images-as-services) + ### CI Examples - [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md) diff --git a/doc/ci/README.md b/doc/ci/README.md index 97325069ce..ae921b6f98 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -9,6 +9,18 @@ + [Using Docker Images](docker/using_docker_images.md) + [Using Docker Build](docker/using_docker_build.md) + [Using Variables](variables/README.md) ++ [Using SSH keys](ssh_keys/README.md) + +### Languages + ++ [Testing PHP](languages/php.md) + +### Services + ++ [Using MySQL](services/mysql.md) ++ [Using PostgreSQL](services/postgres.md) ++ [Using Redis](services/redis.md) ++ [Using Other Services](docker/using_docker_images.html#how-to-use-other-images-as-services) ### Examples diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 1feae62b1c..2f0ca19cd0 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -42,7 +42,44 @@ So, **to access your database service you have to connect to host: `mysql` inste ### How to use other images as services? You are not limited to have only database services. -You can hand modify `config.toml` to add any image as service found at [Docker Hub](https://registry.hub.docker.com/). +You can add the services to `.gitlab-ci.yml` or hand modify the `config.toml`. +You can use any image as service found at [Docker Hub](https://registry.hub.docker.com/). + +### Define image and services from `.gitlab-ci.yml` +You can simply define image or list services that you want to use for the build time. +``` +image: ruby:2.2 +services: + - postgres:9.3 +before_install: + - bundle install + +test: + script: + - bundle exec rake spec +``` + +It's possible to define image and service per-job: +``` +before_install: + - bundle install + +test:2.1: + image: ruby:2.1 + services: + - postgres:9.3 + script: + - bundle exec rake spec + +test:2.2: + image: ruby:2.2 + services: + - postgres:9.4 + script: + - bundle exec rake spec +``` + +### Define image and services in `config.toml` Look for `[runners.docker]` section: ``` [runners.docker] @@ -50,13 +87,16 @@ Look for `[runners.docker]` section: services = ["mysql:latest", "postgres:latest"] ``` +The image and services defined these way will be added to all builds run by that runner. + +### Accessing the services For example you need `wordpress` instance to test some API integration with `Wordpress`. -You can for example use this image: [tutum/wordpress](https://registry.hub.docker.com/u/tutum/wordpress/). -This is image that have fully preconfigured `wordpress` and have `MySQL` server built-in: +You can for example use this image: [tutum/wordpress](https://registry.hub.docker.com/u/tutum/wordpress/). + ``` -[runners.docker] - image = "ruby:2.1" - services = ["mysql:latest", "postgres:latest", "tutum/wordpress:latest"] +# .gitlab-ci.yml +services: +- tutum/wordpress:latest ``` Next time when you run your application the `tutum/wordpress` will be started @@ -64,7 +104,7 @@ and you will have access to it from your build container under hostname: `tutum_ Alias hostname for the service is made from the image name: 1. Everything after `:` is stripped, -2. '/' is replaced with `__`. +2. '/' is replaced to `__`. ### Configuring services Many services accept environment variables, which allow you to easily change database names or set account names depending on the environment. @@ -99,67 +139,6 @@ or README page for any other Docker image. **Note: All variables will passed to all service containers. It's not designed to distinguish which variable should go where.** -### Overwrite image and services -It's possible to overwrite `docker-image` and specify services from `.gitlab-ci.yml`. -If you add to your YAML the `image` and the `services` these parameters -be used instead of the ones that were specified during runner's registration. -``` -image: ruby:2.2 -services: - - postgres:9.3 -before_install: - - bundle install - -test: - script: - - bundle exec rake spec -``` - -It's possible to define image and service per-job: -``` -before_install: - - bundle install - -test:2.1: - image: ruby:2.1 - services: - - postgres:9.3 - script: - - bundle exec rake spec - -test:2.2: - image: ruby:2.2 - services: - - postgres:9.4 - script: - - bundle exec rake spec -``` - -#### How to enable overwriting? -To enable overwriting you have to **enable it first** (it's disabled by default for security reasons). -You can do that by hand modifying runner configuration: `config.toml`. -Please go to section where is `[runners.docker]` definition for your runner. -Add `allowed_images` and `allowed_services` to specify what images are allowed to be picked from `.gitlab-ci.yml`: -``` -[runners.docker] - image = "ruby:2.1" - allowed_images = ["ruby:*", "python:*"] - allowed_services = ["mysql:*", "redis:*"] -``` -This enables you to use in your `.gitlab-ci.yml` any image that matches above wildcards. -You will be able to pick only `ruby` and `python` images. -The same rule can be applied to limit services. - -If you are courageous enough, you can make it fully open and accept everything: -``` -[runners.docker] - image = "ruby:2.1" - allowed_images = ["*", "*/*"] - allowed_services = ["*", "*/*"] -``` - -**It the feature is not enabled, or image isn't allowed the error message will be put into the build log.** - ### How Docker integration works 1. Create any service container: `mysql`, `postgresql`, `mongodb`, `redis`. 1. Create cache container to store all volumes as defined in `config.toml` and `Dockerfile` of build image (`ruby:2.1` as in above example). diff --git a/doc/ci/languages/README.md b/doc/ci/languages/README.md new file mode 100644 index 0000000000..375adf58d1 --- /dev/null +++ b/doc/ci/languages/README.md @@ -0,0 +1,3 @@ +### Languages + ++ [Testing PHP](php.md) diff --git a/doc/ci/languages/php.md b/doc/ci/languages/php.md new file mode 100644 index 0000000000..e058918200 --- /dev/null +++ b/doc/ci/languages/php.md @@ -0,0 +1,178 @@ +## Testing PHP projects + +This guide covers basic of building PHP projects. + +Is it possible to test PHP apps on any system. +However, it will require manual configuration. +The simplest is to use Docker executor as described below. + +### PHP projects on Docker executor +It's possible to official [PHP](https://hub.docker.com/_/php/) repositories on Docker Hub. +They allow to test PHP projects against different versions of the runtime. +However, they require additional configuration. + +To build PHP project you need to create valid `.gitlab-ci.yml` describing the build environment: +1. First you need to specify PHP image as described here: http://doc.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-image. To your `.gitlab-ci.yml` add: + + image: php:5.6 + +2. The official images are great, but they are lacking a few useful tools for testing. We need to install them first in build environment. Create `ci/docker_install.sh` file with following content: + + #!/bin/bash + + # We need to install dependencies only for Docker + [[ ! -e /.dockerinit ]] && exit 0 + + set -xe + + # Install git (the php image doesn't have it) which is required by composer + apt-get update -yqq + apt-get install git -yqq + + # Install phpunit, the tool that we will use for testing + curl -o /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar + chmod +x /usr/local/bin/phpunit + + # Install mysql driver + # Here you can install any other extension that you need + docker-php-ext-install pdo_mysql + +3. From your `.gitlab-ci.yml` run the created script: + + before_script: + - bash ci/docker_install.sh > /dev/null + +4. Now you can run your tests. Usually it will be `phpunit` with arguments: + + test:app: + script: + - phpunit --configuration phpunit_myapp.xml --coverage-text + +5. Commit your files, and push them to GitLab to see if it works. With GitLab Runner 1.0 you can also test the changes locally. From your terminal execute: + + # Check using docker executor + gitlab-runner exec docker test:app + + # Check using shell executor + gitlab-runner exec shell test:app + +The final `.gitlab-ci.yml` should look similar to this: + + # Select image from https://hub.docker.com/_/php/ + image: php:5.6 + + before_script: + # Install dependencies + - ci/docker_install.sh > /dev/null + + test:app: + script: + - phpunit --configuration phpunit_myapp.xml --coverage-text + +#### Test against different PHP versions in Docker builds + +You can also test against multiple version of PHP runtime: + + before_script: + # Install dependencies + - ci/docker_install.sh > /dev/null + + # We test PHP5.6 + test:5.6: + image: php:5.6 + script: + - phpunit --configuration phpunit_myapp.xml --coverage-text + + # We test PHP7.0 + test:7.0: + image: php:7.0 + script: + - phpunit --configuration phpunit_myapp.xml --coverage-text + +#### Custom PHP configuration in Docker builds + +You can customise your PHP environment by putting your .ini file into `/usr/local/etc/php/conf.d/`: + + before_script: + - cp my_php.ini /usr/local/etc/php/conf.d/test.ini + +### Test PHP projects using Shell + +Shell executor runs your builds in terminal session of your server. Thus in order to test your projects you need to have all dependencies installed as root. + +1. Install PHP dependencies: + + sudo apt-get update -qy + sudo apt-get install phpunit php5-mysql -y + + This will install the PHP version available for your distribution. + +2. Now you can run your tests. Usually it will be `phpunit` with arguments: + + test:app: + script: + - phpunit --configuration phpunit_myapp.xml --coverage-text + +#### Test against different PHP versions in Shell builds + +The [phpenv](https://github.com/phpenv/phpenv) allows you to easily manage different PHP with they own configs. +This is specially usefull when testing PHP project with Shell executor. + +Login as `gitlab-runner` user and follow [the installation guide](https://github.com/phpenv/phpenv#installation). + +Using phpenv also allows to easily configure PHP environment with: `phpenv config-add my_config.ini`. + +#### Install custom extensions + +Since we have pretty bare installation of our PHP environment you may need some extensions that are not present on your installation. + +To install additional extensions simply execute.: + + pecl install + + It's not advised to add this to the `.gitlab-ci.yml`. + You should execute this command once, only to setup the build environment. + +### Extend your tests + +#### Using atoum + +Instead of PHPUnit, you can use any other tool to run unit tests. For example [atoum](https://github.com/atoum/atoum): + + before_script: + - wget http://downloads.atoum.org/nightly/mageekguy.atoum.phar + + test:atoum: + script: + - php mageekguy.atoum.phar + +#### Using Composer + +Majority of the PHP projects use Composer for managing the packages. +It's very simple to execute the Composer before running your tests. +To your `.gitlab-ci.yml` add: + + # The composer stores all downloaded packages in vendor/ + # Remove it if you committed the vendor/ directory + cache: + paths: + - vendor/ + + before_script: + # Install composer dependencies + - curl -sS https://getcomposer.org/installer | php + - php composer.phar install + +### Access private packages / dependencies + +You need to configure [the SSH keys](../ssh_keys/README.md) in order to checkout the repositories. + +### Use databases or other services + +Please checkout the docs about configuring [the CI services](../services/README.md). + +### Example project + +You maybe interested in our [Example Project](https://gitlab.com/gitlab-examples/php) that runs on [GitLab.com](https://gitlab.com) using our publically available shared runners. + +Want to hack it? Simply fork it, commit and push changes. Within a few moments the changes will be picked and rebuilt by public runners. diff --git a/doc/ci/services/README.md b/doc/ci/services/README.md new file mode 100644 index 0000000000..0550e9435a --- /dev/null +++ b/doc/ci/services/README.md @@ -0,0 +1,6 @@ +## GitLab CI Services + ++ [Using MySQL](mysql.md) ++ [Using PostgreSQL](postgres.md) ++ [Using Redis](redis.md) ++ [Using Other Services](../docker/using_docker_images.html#how-to-use-other-images-as-services) diff --git a/doc/ci/services/docker-services.md b/doc/ci/services/docker-services.md new file mode 100644 index 0000000000..df36ebaf7d --- /dev/null +++ b/doc/ci/services/docker-services.md @@ -0,0 +1,5 @@ +## GitLab CI Services + ++ [Using MySQL](mysql.md) ++ [Using PostgreSQL](postgres.md) ++ [Using Redis](redis.md) diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md new file mode 100644 index 0000000000..3155af6b3e --- /dev/null +++ b/doc/ci/services/mysql.md @@ -0,0 +1,72 @@ +## Using MySQL + +It's possible to use MySQL database test your apps during builds. + +### Use MySQL with Docker executor + +If you are using our Docker integration you basically have everything already. + +1. Add this to your `.gitlab-ci.yml`: + + services: + - mysql + + variables: + # Configure mysql service (https://hub.docker.com/_/mysql/) + MYSQL_DATABASE: hello_world_test + MYSQL_ROOT_PASSWORD: mysql + +2. Configure your application to use the database: + + Host: mysql + User: root + Password: mysql + Database: hello_world_test + +3. You can also use any other available on [DockerHub](https://hub.docker.com/_/mysql/). For example: `mysql:5.5`. + +Example: https://gitlab.com/gitlab-examples/mysql/blob/master/.gitlab-ci.yml + +### Use MySQL with Shell executor + +It's possible to use MySQL on manually configured servers that are using GitLab Runner with Shell executor. + +1. First install the MySQL server: + + sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev + + # Pick a MySQL root password (can be anything), type it and press enter + # Retype the MySQL root password and press enter + +2. Create an user: + + mysql -u root -p + + # Create a user which will be used by your apps + # do not type the 'mysql>', this is part of the prompt + # change $password in the command below to a real password you pick + mysql> CREATE USER 'runner'@'localhost' IDENTIFIED BY '$password'; + + # Ensure you can use the InnoDB engine which is necessary to support long indexes + # If this fails, check your MySQL config files (e.g. `/etc/mysql/*.cnf`, `/etc/mysql/conf.d/*`) for the setting "innodb = off" + mysql> SET storage_engine=INNODB; + + # Create the database + mysql> CREATE DATABASE IF NOT EXISTS `hello_world_test` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`; + + # Grant necessary permissions on the database + mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES ON `hello_world_test`.* TO 'runner'@'localhost'; + + # Quit the database session + mysql> \q + +3. Try to connect to database: + + sudo -u gitlab-runner -H mysql -u runner -p -D hello_world_test + +4. Configure your application to use the database: + + Host: localhost + User: runner + Password: $password + Database: hello_world_test diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md new file mode 100644 index 0000000000..e57f8c5944 --- /dev/null +++ b/doc/ci/services/postgres.md @@ -0,0 +1,70 @@ +## Using PostgreSQL + +It's possible to use PostgreSQL database test your apps during builds. + +### Use PostgreSQL with Docker executor + +If you are using our Docker integration you basically have everything already. + +1. Add this to your `.gitlab-ci.yml`: + + services: + - postgres + + variables: + # Configure postgres service (https://hub.docker.com/_/postgres/) + POSTGRES_DB: hello_world_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: "" + +2. Configure your application to use the database: + + Host: postgres + User: postgres + Password: postgres + Database: hello_world_test + +3. You can also use any other available on [DockerHub](https://hub.docker.com/_/postgres/). For example: `postgres:9.3`. + +Example: https://gitlab.com/gitlab-examples/postgres/blob/master/.gitlab-ci.yml + +### Use PostgreSQL with Shell executor + +It's possible to use PostgreSQL on manually configured servers that are using GitLab Runner with Shell executor. + +1. First install the PostgreSQL server: + + sudo apt-get install -y postgresql postgresql-client libpq-dev + +2. Create an user: + + # Install the database packages + sudo apt-get install -y postgresql postgresql-client libpq-dev + + # Login to PostgreSQL + sudo -u postgres psql -d template1 + + # Create a user for runner + # Do not type the 'template1=#', this is part of the prompt + template1=# CREATE USER runner CREATEDB; + + # Create the database & grant all privileges on database + template1=# CREATE DATABASE hello_world_test OWNER runner; + + # Quit the database session + template1=# \q + +3. Try to connect to database: + + # Try connecting to the new database with the new user + sudo -u gitlab-runner -H psql -d hello_world_test + + # Quit the database session + hello_world_test> \q + +4. Configure your application to use the database: + + Host: localhost + User: runner + Password: + Database: hello_world_test diff --git a/doc/ci/services/redis.md b/doc/ci/services/redis.md new file mode 100644 index 0000000000..523634a457 --- /dev/null +++ b/doc/ci/services/redis.md @@ -0,0 +1,40 @@ +## Using Redis + +It's possible to use Redis database test your apps during builds. + +### Use Redis with Docker executor + +If you are using our Docker integration you basically have everything already. + +1. Add this to your `.gitlab-ci.yml`: + + services: + - redis + +2. Configure your application to use the database: + + Host: redis + +3. You can also use any other available on [DockerHub](https://hub.docker.com/_/redis/). For example: `redis:2.6`. + +Example: https://gitlab.com/gitlab-examples/redis/blob/master/.gitlab-ci.yml + +### Use Redis with Shell executor + +It's possible to use Redis on manually configured servers that are using GitLab Runner with Shell executor. + +1. First install the Redis server: + + sudo apt-get install redis-server + +2. Try to connect to the server: + + # Try connecting the the Redis server + sudo -u gitlab-runner -H redis-cli + + # Quit the session + 127.0.0.1:6379> quit + +4. Configure your application to use the database: + + Host: localhost diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md new file mode 100644 index 0000000000..515194e5f5 --- /dev/null +++ b/doc/ci/ssh_keys/README.md @@ -0,0 +1,114 @@ +# Using SSH keys + +GitLab currently doesn't have built-in support for SSH keys in build environment. + +The SSH keys can be useful when: +1. You want to checkout internal submodules, +2. You want to download private packages using your package manager (ie. bundler), +3. You want to deploy your app (ex. to Heroku or own server), +4. You want to execute ssh commands from build environment on remote server, +5. You want to rsync files from your build to remote server. + +If anyone of the above holds true, then you most likely need SSH key. + +There are two possibilities to add SSH keys to build environment. + +## Inject keys in your build environment +The most widely supported is to inject SSH key into your build environment by extending your .gitlab-ci.yml. +This is the universal solution which works with any type of executor (docker, shell, etc.). + +### How it works? +1. We create a new SSH private key with [ssh-keygen](http://linux.die.net/man/1/ssh-keygen). +2. We add the private key as the Secure Variable to project. +3. We run the [ssh-agent](http://linux.die.net/man/1/ssh-agent) during build to load the private key. + +The example [.gitlab-ci.yml](https://gitlab.com/gitlab-examples/ssh-private-key/blob/master/.gitlab-ci.yml) looks like this. + +### Make it work? +1. First, go to terminal and generate a new SSH key: +```bash +$ ssh-keygen -t rsa -f my_key + +Generating public/private rsa key pair. +Enter passphrase (empty for no passphrase): +Enter same passphrase again: +Your identification has been saved in my_key. +Your public key has been saved in my_key.pub. +The key fingerprint is: +SHA256:tBJEfyJUGTMNmPCiPg4UHywHs67MxlM2iEBAlI/W+TY fingeprint +The key's randomart image is: ++---[RSA 2048]----+ +|=*. .o++*= | +|..= +o..o. | +|.+++o + + . | +|+o*=.. + + | +|o+.=. . S | +|*.o .E . | +|o*o . . | +|.o.. | +| . | ++----[SHA256]-----+ +``` + +2. Create a new **Secure Variable** in your project settings on GitLab and name it: `SSH_PRIVATE_KEY`. + +3. Copy the content of `my_key` and paste it as a **Value** of **SSH_PRIVATE_KEY**. + +4. Next you need to modify your `.gitlab-ci.yml` and at the top of the file add: +``` +before_script: +# install ssh-agent (it is required for Docker, change apt-get to yum if you use CentOS-based image) +- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' + +# run ssh-agent (in build environment) +- eval $(ssh-agent -s) + +# add ssh key stored in SSH_PRIVATE_KEY variable to the agent store +- ssh-add <(echo "$SSH_PRIVATE_KEY") + +# for Docker builds disable host key checking, by adding that you are suspectible to man-in-the-middle attack +- mkdir -p ~/.ssh +- '[[ -f /.dockerinit ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config` +``` + +5. Add the public key from `my_key.pub` to services that you want to have an access from build. + +6. If your builds are run using `shell` executor, you may need to login to server and execute the `ssh ` to store the fingerprint of remote server. + +## SSH keys when using Shell executor +If use `shell`, not `docker` it can be easier to have the SSH key. + +We can generate the SSH key for the machine that holds `gitlab-runner` and use that key for all projects that are run on this machine. + +1. First, login to server that runs your builds. + +2. From terminal login as `gitlab-runner` user and generate the SSH private key: +```bash +$ ssh-keygen -t rsa +Generating public/private rsa key pair. +Enter passphrase (empty for no passphrase): +Enter same passphrase again: +Your identification has been saved in ~/.ssh/id_rsa. +Your public key has been saved in ~/.ssh/id_rsa.pub. +The key fingerprint is: +SHA256:tBJEfyJUGTMNmPCiPg4UHywHs67MxlM2iEBAlI/W+TY fingeprint +The key's randomart image is: ++---[RSA 2048]----+ +|=*. .o++*= | +|..= +o..o. | +|.+++o + + . | +|+o*=.. + + | +|o+.=. . S | +|*.o .E . | +|o*o . . | +|.o.. | +| . | ++----[SHA256]-----+ +``` + +3. Add the public key from `~/.ssh/id_rsa.pub` to services that you want to have an access from build. + +4. Try to login for the first time and accept fingerprint: +```bash +ssh Date: Wed, 2 Dec 2015 17:47:18 +0200 Subject: [PATCH 436/908] Fix heading --- doc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README.md b/doc/README.md index a7025f7af1..f85a464a4f 100644 --- a/doc/README.md +++ b/doc/README.md @@ -32,7 +32,7 @@ + [Testing PHP](ci/languages/php.md) -## CI Services +### CI Services + [Using MySQL](ci/services/mysql.md) + [Using PostgreSQL](ci/services/postgres.md) From dcdc49fe429c0e8a762f1a411e1d5c0a7294c91f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 2 Dec 2015 19:24:15 +0200 Subject: [PATCH 437/908] Use .md instead of .html --- doc/README.md | 2 +- doc/ci/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/README.md b/doc/README.md index f85a464a4f..a309809421 100644 --- a/doc/README.md +++ b/doc/README.md @@ -37,7 +37,7 @@ + [Using MySQL](ci/services/mysql.md) + [Using PostgreSQL](ci/services/postgres.md) + [Using Redis](ci/services/redis.md) -+ [Using Other Services](ci/docker/using_docker_images.html#how-to-use-other-images-as-services) ++ [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services) ### CI Examples diff --git a/doc/ci/README.md b/doc/ci/README.md index ae921b6f98..5d9d7a81db 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -20,7 +20,7 @@ + [Using MySQL](services/mysql.md) + [Using PostgreSQL](services/postgres.md) + [Using Redis](services/redis.md) -+ [Using Other Services](docker/using_docker_images.html#how-to-use-other-images-as-services) ++ [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services) ### Examples From 05267b64d224c19f1b6843a715f666aea2bd106a Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 3 Dec 2015 00:36:45 +0200 Subject: [PATCH 438/908] Clean up using_docker_images.md --- doc/ci/docker/using_docker_images.md | 272 ++++++++++++++++++--------- 1 file changed, 187 insertions(+), 85 deletions(-) diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 2f0ca19cd0..6551d47b69 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -1,16 +1,25 @@ # Using Docker Images -GitLab CI can use [Docker Engine](https://www.docker.com/) to build projects. -Docker is an open-source project that allows to use predefined images to run applications -in independent "containers" that are run within a single Linux instance. -[Docker Hub](https://registry.hub.docker.com/) have rich database of built images that can be used to build applications. +GitLab CI can use [Docker Engine](https://www.docker.com/) to build projects. -Docker when used with GitLab CI runs each build in separate and isolated container using predefined image and always from scratch. -It makes it easier to have simple and reproducible build environment that can also be run on your workstation. -This allows you to test all commands from your shell, rather than having to test them on a CI server. +Docker is an open-source project that allows to use predefined images to run +applications in independent "containers" that are run within a single Linux +instance. [Docker Hub][hub] has a rich database of prebuilt images that can be +used to test and build your applications. -### Register Docker runner -To use GitLab Runner with Docker you need to register new runner to use `docker` executor: +Docker, when used with GitLab CI, runs each build in a separate and isolated +container using the predefined image that is set up in `.gitlab-ci.yml`. The +container is always euphemeral which means you get only one container per build. + +This makes it easier to have a simple and reproducible build environment that +can also run on your workstation. The added benefit is that you can test all +the commands that we will explore later from your shell, rather than having to +test them on a CI server. + +### Register docker runner + +To use GitLab Runner with docker you need to register a new runner to use the +`docker` executor: ```bash gitlab-ci-multi-runner register \ @@ -23,45 +32,70 @@ gitlab-ci-multi-runner register \ --docker-mysql latest ``` -**The registered runner will use `ruby:2.1` image and will run two services (`postgres:latest` and `mysql:latest`) that will be accessible for time of the build.** +The registered runner will use the `ruby:2.1` docker image and will run two +services, `postgres:latest` and `mysql:latest`, both of which will be +accessible during the build process. -### What is image? -The image is the name of any repository that is present in local Docker Engine or any repository that can be found at [Docker Hub](https://registry.hub.docker.com/). -For more information about the image and Docker Hub please read the [Docker Fundamentals](https://docs.docker.com/introduction/understanding-docker/). +### What is image -### What is service? -Service is just another image that is run for time of your build and is linked to your build. This allows you to access the service image during build time. -The service image can run any application, but most common use case is to run some database container, ie.: `mysql`. -It's easier and faster to use existing image, run it as additional container than install `mysql` every time project is built. +The `image` keyword is the name of the docker image that is present in the +local Docker Engine (list all images with `docker images`) or any image that +can be found at [Docker Hub][hub]. For more information about images and Docker +Hub please read the [Docker Fundamentals][] documentation. -#### How is service linked to the build? -There's good document that describes how Docker linking works: [Linking containers together](https://docs.docker.com/userguide/dockerlinks/). -To summarize: if you add `mysql` as service to your application, the image will be used to create container that is linked to build container. -The service container for MySQL will be accessible under hostname `mysql`. -So, **to access your database service you have to connect to host: `mysql` instead of socket or `localhost`**. +In short, with `image` we refer to the docker image, which will be used to +create a container on which your build will run. -### How to use other images as services? -You are not limited to have only database services. -You can add the services to `.gitlab-ci.yml` or hand modify the `config.toml`. -You can use any image as service found at [Docker Hub](https://registry.hub.docker.com/). +### What is service + +The `service` keyword defines just another docker image that is run during +your build and is linked to the docker image that the `image` keyword defines. +This allows you to access the service image during build time. + +The service image can run any application, but the most common use case is to +run a database container, eg. `mysql`. It's easier and faster to use an +existing image and run it as an additional container than install `mysql` every +time the project is built. + +#### How is service linked to the build + +To better undestand how the container linking works, read +[Linking containers together](https://docs.docker.com/userguide/dockerlinks/). + +To summarize, if you add `mysql` as service to your application, the image will +then be used to create a container that is linked to the build container. + +The service container for MySQL will be accessible under the hostname `mysql`. +So, in order to access your database service you have to connect to the host +named `mysql` instead of a socket or `localhost`. + +### How to use other images as services + +You are not limited to have only database services. You can add as many +services you need to `.gitlab-ci.yml` or manually modify `config.toml`. +Any image found at [Docker Hub][hub] can be used as a service. ### Define image and services from `.gitlab-ci.yml` -You can simply define image or list services that you want to use for the build time. -``` + +You can simply define an image that will be used for all jobs and a list of +services that you want to use during build time. + +```yaml image: ruby:2.2 services: - postgres:9.3 -before_install: +before_script: - bundle install - + test: script: - bundle exec rake spec ``` -It's possible to define image and service per-job: -``` -before_install: +It is also possible to define different images and services per job: + +```yaml +before_script: - bundle install test:2.1: @@ -80,68 +114,106 @@ test:2.2: ``` ### Define image and services in `config.toml` -Look for `[runners.docker]` section: + +Look for the `[runners.docker]` section: + ``` +... + [runners.docker] image = "ruby:2.1" services = ["mysql:latest", "postgres:latest"] + +... ``` -The image and services defined these way will be added to all builds run by that runner. +The image and services defined this way will be added to all builds run by +that runner. ### Accessing the services -For example you need `wordpress` instance to test some API integration with `Wordpress`. -You can for example use this image: [tutum/wordpress](https://registry.hub.docker.com/u/tutum/wordpress/). -``` -# .gitlab-ci.yml +Let's say that you need a Wordpress instance to test some API integration with +your application. + +You can then use for example the [tutum/wordpress][] image in your +`.gitlab-ci.yml`: + +```yaml +... + services: - tutum/wordpress:latest + +... ``` -Next time when you run your application the `tutum/wordpress` will be started -and you will have access to it from your build container under hostname: `tutum__wordpress`. +When the build is run, `tutum/wordpress` will be started and you will have +access to it from your build container under the hostname `tutum_wordpress`. -Alias hostname for the service is made from the image name: -1. Everything after `:` is stripped, -2. '/' is replaced to `__`. +The alias hostname for the service is made from the image name following these +rules: + +1. Everything after `:` is stripped +2. Backslash (`/`) is replaced with double underscores (`__`) ### Configuring services -Many services accept environment variables, which allow you to easily change database names or set account names depending on the environment. -GitLab Runner 0.5.0 and up passes all YAML-defined variables to created service containers. +Many services accept environment variables which allow you to easily change +database names or set account names depending on the environment. -1. To configure database name for [postgres](https://registry.hub.docker.com/u/library/postgres/) service, -you need to set POSTGRES_DB. +GitLab Runner 0.5.0 and up passes all YAML-defined variables to the created +service containers. - ```yaml - services: - - postgres - - variables: - POSTGRES_DB: gitlab - ``` +For all possible configuration variables check the documentation of each image +provided in their corresponding Docker hub page. -1. To use [mysql](https://registry.hub.docker.com/u/library/mysql/) service with empty password for time of build, -you need to set MYSQL_ALLOW_EMPTY_PASSWORD. +*Note: All variables will be passed to all service containers. It's not designed + to distinguish which variable should go where.* - ```yaml - services: - - mysql - - variables: - MYSQL_ALLOW_EMPTY_PASSWORD: "yes" - ``` +#### PostgreSQL service example -For other possible configuration variables check the -https://registry.hub.docker.com/u/library/mysql/ or https://registry.hub.docker.com/u/library/postgres/ -or README page for any other Docker image. +To configure the database name for [postgres][postgres-hub] service, you need +to set the `POSTGRES_DB` variable: -**Note: All variables will passed to all service containers. It's not designed to distinguish which variable should go where.** +```yaml +... + +services: +- postgres + +variables: + POSTGRES_DB: gitlab + +... +``` + +For a real example visit . + +#### MySQL service example + +To use the [mysql][mysql-hub] service with an empty password during the time of +build, you need to set the `MYSQL_ALLOW_EMPTY_PASSWORD` variable: + +```yaml +... + +services: +- mysql + +variables: + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + +... +``` ### How Docker integration works + +Below is a high level overview of the steps performed by docker during build +time. + 1. Create any service container: `mysql`, `postgresql`, `mongodb`, `redis`. -1. Create cache container to store all volumes as defined in `config.toml` and `Dockerfile` of build image (`ruby:2.1` as in above example). +1. Create cache container to store all volumes as defined in `config.toml` and + `Dockerfile` of build image (`ruby:2.1` as in above example). 1. Create build container and link any service container to build container. 1. Start build container and send build script to the container. 1. Run build script. @@ -151,32 +223,62 @@ or README page for any other Docker image. 1. Remove build container and all created service containers. ### How to debug a build locally -1. Create a file with build script: + +*Note: The following commands are run without root privileges. You should be +able to run docker with your regular user account.* + +First start with creating a file named `build script`: + ```bash -$ cat < build_script +cat < build_script git clone https://gitlab.com/gitlab-org/gitlab-ci-multi-runner.git /builds/gitlab-org/gitlab-ci-multi-runner cd /builds/gitlab-org/gitlab-ci-multi-runner -make <- or any other build step +make EOF ``` -1. Create service containers: -``` -$ docker run -d -n service-mysql mysql:latest -$ docker run -d -n service-postgres postgres:latest -``` -This will create two service containers (MySQL and PostgreSQL). +Here we use as an example the GitLab Runner repository which contains a +Makefile, so running `make` will execute the commands defined in the Makefile. +Your mileage may vary, so instead of `make` you could run the command which +is specific to your project. -1. Create a build container and execute script in its context: +Then create some service containers: + ``` -$ docker run --name build -i --link=service-mysql:mysql --link=service-postgres:postgres ruby:2.1 /bin/bash < build_script +docker run -d -n service-mysql mysql:latest +docker run -d -n service-postgres postgres:latest ``` -This will create build container that has two service containers linked. -The build_script is piped using STDIN to bash interpreter which executes the build script in container. + +This will create two service containers, named `service-mysql` and +`service-postgres` which use the latest MySQL and PostgreSQL images +respectively. They will both run in the background (`-d`). + +Finally, create a build container by executing the `build_script` file we +created earlier: + +``` +docker run --name build -i --link=service-mysql:mysql --link=service-postgres:postgres ruby:2.1 /bin/bash < build_script +``` + +The above command will create a container named `build` that is spawned from +the `ruby:2.1` image and has two services linked to it. The `build_script` is +piped using STDIN to the bash interpreter which in turn executes the +`build_script` in the `build` container. + +When you finish testing and no longer need the containers, you can remove them +with: -1. At the end remove all containers: ``` docker rm -f -v build service-mysql service-postgres ``` -This will forcefully (the `-f` switch) remove build container and service containers -and all volumes (the `-v` switch) that were created with the container creation. + +This will forcefully (`-f`) remove the `build` container, the two service +containers as well as all volumes (`-v`) that were created with the container +creation. + +[Docker Fundamentals]: https://docs.docker.com/engine/introduction/understanding-docker/ +[hub]: https://hub.docker.com/ +[linking-containers]: https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/ +[tutum/wordpress]: https://registry.hub.docker.com/u/tutum/wordpress/ +[postgres-hub]: https://registry.hub.docker.com/u/library/postgres/ +[mysql-hub]: https://registry.hub.docker.com/u/library/mysql/ From 223a02757909b406c1d5ebe42ca4bc3340318a2e Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 3 Dec 2015 15:43:06 +0200 Subject: [PATCH 439/908] Bring back removed heading and point to other section --- doc/ci/docker/using_docker_images.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 6551d47b69..63cfa43633 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -69,6 +69,10 @@ The service container for MySQL will be accessible under the hostname `mysql`. So, in order to access your database service you have to connect to the host named `mysql` instead of a socket or `localhost`. +### Overwrite image and services + +See the section below. + ### How to use other images as services You are not limited to have only database services. You can add as many From 71519d650d0317785f86ed935508d1dd5966cd6e Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 3 Dec 2015 15:43:29 +0200 Subject: [PATCH 440/908] Fix wrong example --- doc/ci/docker/using_docker_images.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 63cfa43633..488479418b 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -152,7 +152,7 @@ services: ``` When the build is run, `tutum/wordpress` will be started and you will have -access to it from your build container under the hostname `tutum_wordpress`. +access to it from your build container under the hostname `tutum__wordpress`. The alias hostname for the service is made from the image name following these rules: From bb75dfe38b481607952234212a15d29cd17b7cef Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 3 Dec 2015 15:46:27 +0200 Subject: [PATCH 441/908] Add intro about languages --- doc/ci/languages/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/ci/languages/README.md b/doc/ci/languages/README.md index 375adf58d1..54b2343e08 100644 --- a/doc/ci/languages/README.md +++ b/doc/ci/languages/README.md @@ -1,3 +1,7 @@ ### Languages +This is a list of languages you can test with GitLab CI. Each section has +comprehensive documentation and comes with a test repository hosted on +GitLab.com + + [Testing PHP](php.md) From a08cc70232b1c5071f199106a582c77ba9541a83 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 3 Dec 2015 18:13:19 +0200 Subject: [PATCH 442/908] Clean up PHP CI example [ci skip] --- doc/ci/languages/php.md | 325 ++++++++++++++++++++++++++-------------- 1 file changed, 211 insertions(+), 114 deletions(-) diff --git a/doc/ci/languages/php.md b/doc/ci/languages/php.md index e058918200..c71ae40786 100644 --- a/doc/ci/languages/php.md +++ b/doc/ci/languages/php.md @@ -1,178 +1,275 @@ -## Testing PHP projects +# Testing PHP projects -This guide covers basic of building PHP projects. +This guide covers basic building instructions for PHP projects. -Is it possible to test PHP apps on any system. -However, it will require manual configuration. -The simplest is to use Docker executor as described below. +There are covered two cases: testing using the Docker executor and testing +using the Shell executor. -### PHP projects on Docker executor -It's possible to official [PHP](https://hub.docker.com/_/php/) repositories on Docker Hub. -They allow to test PHP projects against different versions of the runtime. -However, they require additional configuration. +## Test PHP projects using the Docker executor -To build PHP project you need to create valid `.gitlab-ci.yml` describing the build environment: -1. First you need to specify PHP image as described here: http://doc.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-image. To your `.gitlab-ci.yml` add: +While it is possible to test PHP apps on any system, this would require manual +configuration from the developer. To overcome this we will be using the +official [PHP docker image][php-hub] that can be found in Docker Hub. - image: php:5.6 +This will allow us to test PHP projects against different versions of PHP. +However, not everything is plug 'n' play, you still need to onfigure some +things manually. -2. The official images are great, but they are lacking a few useful tools for testing. We need to install them first in build environment. Create `ci/docker_install.sh` file with following content: +As with every build, you need to create a valid `.gitlab-ci.yml` describing the +build environment. - #!/bin/bash +Let's first specify the PHP image that will be used for the build process +(you can read more about what an image means in the Runner's lingo reading +about [Using Docker images](../docker/using_docker_images.md#what-is-image)). - # We need to install dependencies only for Docker - [[ ! -e /.dockerinit ]] && exit 0 +Start by adding the image to your `.gitlab-ci.yml`: - set -xe +```yaml +image: php:5.6 +``` - # Install git (the php image doesn't have it) which is required by composer - apt-get update -yqq - apt-get install git -yqq +The official images are great, but they lack a few useful tools for testing. +We need to first prepare the build environment. A way to overcome this is to +create a script which installs all prerequisites prior the actual testing is +done. - # Install phpunit, the tool that we will use for testing - curl -o /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar - chmod +x /usr/local/bin/phpunit +Let's create a `ci/docker_install.sh` file in the root directory of our +repository with the following content: - # Install mysql driver - # Here you can install any other extension that you need - docker-php-ext-install pdo_mysql +```bash +#!/bin/bash -3. From your `.gitlab-ci.yml` run the created script: +# We need to install dependencies only for Docker +[[ ! -e /.dockerinit ]] && exit 0 - before_script: - - bash ci/docker_install.sh > /dev/null +set -xe -4. Now you can run your tests. Usually it will be `phpunit` with arguments: +# Install git (the php image doesn't have it) which is required by composer +apt-get update -yqq +apt-get install git -yqq - test:app: - script: - - phpunit --configuration phpunit_myapp.xml --coverage-text +# Install phpunit, the tool that we will use for testing +curl -o /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar +chmod +x /usr/local/bin/phpunit -5. Commit your files, and push them to GitLab to see if it works. With GitLab Runner 1.0 you can also test the changes locally. From your terminal execute: +# Install mysql driver +# Here you can install any other extension that you need +docker-php-ext-install pdo_mysql +``` - # Check using docker executor - gitlab-runner exec docker test:app +You might wonder what `docker-php-ext-install` is. In short, it is a script +provided by the official php docker image that you can use to easilly install +extensions. For more information read the the documentation at +. - # Check using shell executor - gitlab-runner exec shell test:app +Now that we created the script that contains all prerequisites for our build +environment, let's add it in `.gitlab-ci.yml`: + +```yaml +... + +before_script: +- bash ci/docker_install.sh > /dev/null + +... +``` + +Last step, run the actual tests using `phpunit`: + +```yaml +... + +test:app: + script: + - phpunit --configuration phpunit_myapp.xml + +... +``` + +Finally, commit your files and push them to GitLab to see your build succeeding +(or failing). The final `.gitlab-ci.yml` should look similar to this: - # Select image from https://hub.docker.com/_/php/ - image: php:5.6 +```yaml +# Select image from https://hub.docker.com/_/php/ +image: php:5.6 - before_script: - # Install dependencies - - ci/docker_install.sh > /dev/null +before_script: +# Install dependencies +- ci/docker_install.sh > /dev/null - test:app: - script: - - phpunit --configuration phpunit_myapp.xml --coverage-text +test:app: + script: + - phpunit --configuration phpunit_myapp.xml +``` -#### Test against different PHP versions in Docker builds +### Test against different PHP versions in Docker builds -You can also test against multiple version of PHP runtime: +Testing against multiple versions of PHP is super easy. Just add another job +with a different docker image version and the runner will do the rest: - before_script: - # Install dependencies - - ci/docker_install.sh > /dev/null +```yaml +before_script: +# Install dependencies +- ci/docker_install.sh > /dev/null - # We test PHP5.6 - test:5.6: - image: php:5.6 - script: - - phpunit --configuration phpunit_myapp.xml --coverage-text +# We test PHP5.6 +test:5.6: + image: php:5.6 + script: + - phpunit --configuration phpunit_myapp.xml - # We test PHP7.0 - test:7.0: - image: php:7.0 - script: - - phpunit --configuration phpunit_myapp.xml --coverage-text +# We test PHP7.0 (good luck with that) +test:7.0: + image: php:7.0 + script: + - phpunit --configuration phpunit_myapp.xml +``` -#### Custom PHP configuration in Docker builds +### Custom PHP configuration in Docker builds -You can customise your PHP environment by putting your .ini file into `/usr/local/etc/php/conf.d/`: +There are times where you will need to customise your PHP environment by +putting your `.ini` file into `/usr/local/etc/php/conf.d/`. For that purpose +add a `before_script` action: - before_script: - - cp my_php.ini /usr/local/etc/php/conf.d/test.ini +```yaml +before_script: +- cp my_php.ini /usr/local/etc/php/conf.d/test.ini +``` -### Test PHP projects using Shell +Of course, `my_php.ini` must be present in the root directory of your repository. -Shell executor runs your builds in terminal session of your server. Thus in order to test your projects you need to have all dependencies installed as root. +## Test PHP projects using the Shell executor -1. Install PHP dependencies: +The shell executor runs your builds in a terminal session on your server. +Thus, in order to test your projects you first need to make sure that all +dependencies are installed. - sudo apt-get update -qy - sudo apt-get install phpunit php5-mysql -y +For example, in a VM running Debian 8 we first update the cache, then we +install `phpunit` and `php5-mysql`: - This will install the PHP version available for your distribution. +```bash +sudo apt-get update -y +sudo apt-get install -y phpunit php5-mysql +``` -2. Now you can run your tests. Usually it will be `phpunit` with arguments: +Next, add the following snippet to your `.gitlab-ci.yml`: - test:app: - script: - - phpunit --configuration phpunit_myapp.xml --coverage-text +```yaml +test:app: + script: + - phpunit --configuration phpunit_myapp.xml +``` -#### Test against different PHP versions in Shell builds +Finally, push to GitLab and let the tests begin! -The [phpenv](https://github.com/phpenv/phpenv) allows you to easily manage different PHP with they own configs. -This is specially usefull when testing PHP project with Shell executor. +### Test against different PHP versions in Shell builds -Login as `gitlab-runner` user and follow [the installation guide](https://github.com/phpenv/phpenv#installation). +The [phpenv][] project allows you to easily manage different versions of PHP +each with its own config. This is specially usefull when testing PHP projects +with the Shell executor. -Using phpenv also allows to easily configure PHP environment with: `phpenv config-add my_config.ini`. +You will have to install it on your build machine under the `gitlab-runner` +user following [the upstream installation guide][phpenv-installation]. -#### Install custom extensions +Using phpenv also allows to easily configure the PHP environment with: -Since we have pretty bare installation of our PHP environment you may need some extensions that are not present on your installation. +``` +phpenv config-add my_config.ini +``` -To install additional extensions simply execute.: +### Install custom extensions - pecl install +Since this is a pretty bare installation of the PHP environment, you may need +some extensions that are not currently present on the build machine. - It's not advised to add this to the `.gitlab-ci.yml`. - You should execute this command once, only to setup the build environment. +To install additional extensions simply execute: -### Extend your tests +```bash +pecl install +``` -#### Using atoum +It's not advised to add this to `.gitlab-ci.yml`. You should execute this +command once, only to setup the build environment. -Instead of PHPUnit, you can use any other tool to run unit tests. For example [atoum](https://github.com/atoum/atoum): +## Extend your tests - before_script: - - wget http://downloads.atoum.org/nightly/mageekguy.atoum.phar +### Using atoum - test:atoum: - script: - - php mageekguy.atoum.phar +Instead of PHPUnit, you can use any other tool to run unit tests. For example +you can use [atoum](https://github.com/atoum/atoum): -#### Using Composer +```yaml +before_script: +- wget http://downloads.atoum.org/nightly/mageekguy.atoum.phar -Majority of the PHP projects use Composer for managing the packages. -It's very simple to execute the Composer before running your tests. -To your `.gitlab-ci.yml` add: +test:atoum: + script: + - php mageekguy.atoum.phar +``` - # The composer stores all downloaded packages in vendor/ - # Remove it if you committed the vendor/ directory - cache: - paths: - - vendor/ +### Using Composer - before_script: - # Install composer dependencies - - curl -sS https://getcomposer.org/installer | php - - php composer.phar install +The majority of the PHP projects use Composer for managing their PHP packages. +In order to execute Composer before running your tests, simply add the +following in your `.gitlab-ci.yml`: -### Access private packages / dependencies +```yaml +... -You need to configure [the SSH keys](../ssh_keys/README.md) in order to checkout the repositories. +# Composer stores all downloaded packages in the vendor/ directory. +# Do not use the following if the vendor/ directory is commited to +# your git repository. +cache: + paths: + - vendor/ -### Use databases or other services +before_script: +# Install composer dependencies +- curl -sS https://getcomposer.org/installer | php +- php composer.phar install -Please checkout the docs about configuring [the CI services](../services/README.md). +... +``` -### Example project +## Access private packages / dependencies -You maybe interested in our [Example Project](https://gitlab.com/gitlab-examples/php) that runs on [GitLab.com](https://gitlab.com) using our publically available shared runners. +If your test suite needs to access a private repository, you need to configure +[the SSH keys](../ssh_keys/README.md) in order to be able to clone it. -Want to hack it? Simply fork it, commit and push changes. Within a few moments the changes will be picked and rebuilt by public runners. +## Use databases or other services + +Most of the time you will need a running database in order for your tests to +run. If you are using the Docker executor you can leverage Docker's ability to +connect to other containers. In GitLab Runner lingo, this can be achieved by +defining a `service`. + +This functionality is covered in [the CI services](../services/README.md) +documentation. + +## Testing things locally + +With GitLab Runner 1.0 you can also test any changes locally. From your +terminal execute: + +```bash +# Check using docker executor +gitlab-runner exec docker test:app + +# Check using shell executor +gitlab-runner exec shell test:app +``` + +## Example project + +We have set up an [Example PHP Project](https://gitlab.com/gitlab-examples/php) +for your convenience that runs on [GitLab.com](https://gitlab.com) using our +publicly available [shared runners](../runners/README.md). + +Want to hack it? Simply fork it, commit and push your changes. Within a few +moments the changes will be picked by a public runner and the build will begin. + +[php-hub]: https://hub.docker.com/_/php/ +[phpenv]: https://github.com/phpenv/phpenv +[phpenv-installation]: https://github.com/phpenv/phpenv#installation From 9b8babb601ac6bdb3b7301b017a8ce20c5dc4814 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 3 Dec 2015 21:18:34 +0200 Subject: [PATCH 443/908] Use link instead of connect to be more Docker friendly --- doc/ci/languages/php.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/languages/php.md b/doc/ci/languages/php.md index c71ae40786..589253cfc4 100644 --- a/doc/ci/languages/php.md +++ b/doc/ci/languages/php.md @@ -242,7 +242,7 @@ If your test suite needs to access a private repository, you need to configure Most of the time you will need a running database in order for your tests to run. If you are using the Docker executor you can leverage Docker's ability to -connect to other containers. In GitLab Runner lingo, this can be achieved by +link to other containers. In GitLab Runner lingo, this can be achieved by defining a `service`. This functionality is covered in [the CI services](../services/README.md) From d13d43aca9c486365fbe6eab8d30cb1e005c3f61 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 4 Dec 2015 11:46:08 +0200 Subject: [PATCH 444/908] Clean up CI ssh keys docs [ci skip] --- doc/ci/ssh_keys/README.md | 163 ++++++++++++++++++-------------------- 1 file changed, 79 insertions(+), 84 deletions(-) diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index 515194e5f5..210f9c3e84 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -1,114 +1,109 @@ # Using SSH keys -GitLab currently doesn't have built-in support for SSH keys in build environment. +GitLab currently doesn't have built-in support for managing SSH keys in a build +environment. The SSH keys can be useful when: -1. You want to checkout internal submodules, -2. You want to download private packages using your package manager (ie. bundler), -3. You want to deploy your app (ex. to Heroku or own server), -4. You want to execute ssh commands from build environment on remote server, -5. You want to rsync files from your build to remote server. -If anyone of the above holds true, then you most likely need SSH key. +1. You want to checkout internal submodules +2. You want to download private packages using your package manager (eg. bundler) +3. You want to deploy your application to eg. Heroku or your own server +4. You want to execute SSH commands from the build server to the remote server +5. You want to rsync files from your build server to the remote server -There are two possibilities to add SSH keys to build environment. +If anything of the above rings a bell, then you most likely need an SSH key. -## Inject keys in your build environment -The most widely supported is to inject SSH key into your build environment by extending your .gitlab-ci.yml. -This is the universal solution which works with any type of executor (docker, shell, etc.). +## Inject keys in your build server -### How it works? -1. We create a new SSH private key with [ssh-keygen](http://linux.die.net/man/1/ssh-keygen). -2. We add the private key as the Secure Variable to project. -3. We run the [ssh-agent](http://linux.die.net/man/1/ssh-agent) during build to load the private key. +The most widely supported method is to inject an SSH key into your build +environment by extending your `.gitlab-ci.yml`. -The example [.gitlab-ci.yml](https://gitlab.com/gitlab-examples/ssh-private-key/blob/master/.gitlab-ci.yml) looks like this. +This is the universal solution which works with any type of executor +(docker, shell, etc.). -### Make it work? -1. First, go to terminal and generate a new SSH key: -```bash -$ ssh-keygen -t rsa -f my_key +### How it works -Generating public/private rsa key pair. -Enter passphrase (empty for no passphrase): -Enter same passphrase again: -Your identification has been saved in my_key. -Your public key has been saved in my_key.pub. -The key fingerprint is: -SHA256:tBJEfyJUGTMNmPCiPg4UHywHs67MxlM2iEBAlI/W+TY fingeprint -The key's randomart image is: -+---[RSA 2048]----+ -|=*. .o++*= | -|..= +o..o. | -|.+++o + + . | -|+o*=.. + + | -|o+.=. . S | -|*.o .E . | -|o*o . . | -|.o.. | -| . | -+----[SHA256]-----+ -``` +1. Create a new SSH key pair with [ssh-keygen][] +2. Add the private key as a **Secret Variable** to the project +3. Run the [ssh-agent][] during build to load the private key. -2. Create a new **Secure Variable** in your project settings on GitLab and name it: `SSH_PRIVATE_KEY`. +## SSH keys when using the Docker executor -3. Copy the content of `my_key` and paste it as a **Value** of **SSH_PRIVATE_KEY**. +You will first need to create an SSH key pair. For more information, follow the +instructions to [generate an SSH key](../ssh/README.md). + +Then, create a new **Secret Variable** in your project settings on GitLab +following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY` +and in the **Value** field paste the content of your _private_ key that you +created earlier. + +Next you need to modify your `.gitlab-ci.yml` with a `before_script` action. +Add it to the top: -4. Next you need to modify your `.gitlab-ci.yml` and at the top of the file add: ``` before_script: -# install ssh-agent (it is required for Docker, change apt-get to yum if you use CentOS-based image) -- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' + # Install ssh-agent if not already installed, it is required by Docker. + # (change apt-get to yum if you use a CentOS-based image) + - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' -# run ssh-agent (in build environment) -- eval $(ssh-agent -s) + # Run ssh-agent (inside the build environment) + - eval $(ssh-agent -s) -# add ssh key stored in SSH_PRIVATE_KEY variable to the agent store -- ssh-add <(echo "$SSH_PRIVATE_KEY") + # Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store + - ssh-add <(echo "$SSH_PRIVATE_KEY") -# for Docker builds disable host key checking, by adding that you are suspectible to man-in-the-middle attack -- mkdir -p ~/.ssh -- '[[ -f /.dockerinit ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config` + # For Docker builds disable host key checking. Be aware that by adding that + # you are suspectible to man-in-the-middle attacks. + # WARNING: Use this only with the Docker executor, if you use it with shell + # you will overwrite your user's SSH config. + - mkdir -p ~/.ssh + - '[[ -f /.dockerinit ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config` ``` -5. Add the public key from `my_key.pub` to services that you want to have an access from build. +As a final step, add the _public_ key from the one you created earlier to the +services that you want to have an access to from within the build environment. +If you are accessing a private GitLab repository you need to add it as a +[deploy key](../ssh/README.md#deploy-keys). -6. If your builds are run using `shell` executor, you may need to login to server and execute the `ssh ` to store the fingerprint of remote server. +That's it! You can now have access to private servers or repositories in your +build environment. -## SSH keys when using Shell executor -If use `shell`, not `docker` it can be easier to have the SSH key. +## SSH keys when using the Shell executor -We can generate the SSH key for the machine that holds `gitlab-runner` and use that key for all projects that are run on this machine. +If you are using the Shell executor and not Docker, it is easier to set up an +SSH key. -1. First, login to server that runs your builds. +You can generate the SSH key from the machine that GitLab Runner is installed +on, and use that key for all projects that are run on this machine. + +First, you need to login to the server that runs your builds. + +Then from the terminal login as the `gitlab-runner` user and generate the SSH +key pair as described in the [SSH keys documentation](../ssh/README.md). + +As a final step, add the _public_ key from the one you created earlier to the +services that you want to have an access to from within the build environment. +If you are accessing a private GitLab repository you need to add it as a +[deploy key](../ssh/README.md#deploy-keys). + +Once done, try to login to the remote server in order to accept the fingerprint: -2. From terminal login as `gitlab-runner` user and generate the SSH private key: ```bash -$ ssh-keygen -t rsa -Generating public/private rsa key pair. -Enter passphrase (empty for no passphrase): -Enter same passphrase again: -Your identification has been saved in ~/.ssh/id_rsa. -Your public key has been saved in ~/.ssh/id_rsa.pub. -The key fingerprint is: -SHA256:tBJEfyJUGTMNmPCiPg4UHywHs67MxlM2iEBAlI/W+TY fingeprint -The key's randomart image is: -+---[RSA 2048]----+ -|=*. .o++*= | -|..= +o..o. | -|.+++o + + . | -|+o*=.. + + | -|o+.=. . S | -|*.o .E . | -|o*o . . | -|.o.. | -| . | -+----[SHA256]-----+ +ssh ``` -3. Add the public key from `~/.ssh/id_rsa.pub` to services that you want to have an access from build. +For accessing repositories on GitLab.com, the `` would be +`git@gitlab.com`. -4. Try to login for the first time and accept fingerprint: -```bash -ssh Date: Fri, 4 Dec 2015 11:47:02 +0200 Subject: [PATCH 445/908] Move markdown link to the bottom [ci skip] --- doc/ci/languages/php.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/ci/languages/php.md b/doc/ci/languages/php.md index 589253cfc4..60079c090d 100644 --- a/doc/ci/languages/php.md +++ b/doc/ci/languages/php.md @@ -263,13 +263,14 @@ gitlab-runner exec shell test:app ## Example project -We have set up an [Example PHP Project](https://gitlab.com/gitlab-examples/php) -for your convenience that runs on [GitLab.com](https://gitlab.com) using our -publicly available [shared runners](../runners/README.md). +We have set up an [Example PHP Project][php-example-repo] for your convenience +that runs on [GitLab.com](https://gitlab.com) using our publicly available +[shared runners](../runners/README.md). -Want to hack it? Simply fork it, commit and push your changes. Within a few +Want to hack on it? Simply fork it, commit and push your changes. Within a few moments the changes will be picked by a public runner and the build will begin. [php-hub]: https://hub.docker.com/_/php/ [phpenv]: https://github.com/phpenv/phpenv [phpenv-installation]: https://github.com/phpenv/phpenv#installation +[php-example-repo]: https://gitlab.com/gitlab-examples/php From 4fc9e6944e363e3167fede07c0d6898ff6c7e19f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 4 Dec 2015 11:47:50 +0200 Subject: [PATCH 446/908] Add an intro to CI services documentation [ci skip] --- doc/ci/services/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/ci/services/README.md b/doc/ci/services/README.md index 0550e9435a..0856b7679c 100644 --- a/doc/ci/services/README.md +++ b/doc/ci/services/README.md @@ -1,6 +1,9 @@ ## GitLab CI Services +GitLab CI uses the `service` keyword to define what docker containers should be +linked with your base image. Below is a list of examples you may use. + + [Using MySQL](mysql.md) + [Using PostgreSQL](postgres.md) + [Using Redis](redis.md) -+ [Using Other Services](../docker/using_docker_images.html#how-to-use-other-images-as-services) ++ [Using Other Services](../docker/using_docker_images.md#how-to-use-other-images-as-services) From 4236861df03ff86fc1b2bb8a6a0835b0f3a03244 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 7 Dec 2015 09:39:14 +0200 Subject: [PATCH 447/908] Clean up Redis CI service example [ci skip] --- doc/ci/services/redis.md | 72 ++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/doc/ci/services/redis.md b/doc/ci/services/redis.md index 523634a457..b0d04f9540 100644 --- a/doc/ci/services/redis.md +++ b/doc/ci/services/redis.md @@ -1,40 +1,68 @@ -## Using Redis +# Using Redis -It's possible to use Redis database test your apps during builds. +As many applications depend on Redis as their key-value store, you will +eventually need it in order for your tests to run. Below you are guided how to +do this with the Docker and Shell executors of GitLab Runner. -### Use Redis with Docker executor +## Use Redis with Docker executor -If you are using our Docker integration you basically have everything already. +If you are using our Docker integration you basically have everything set up +already. -1. Add this to your `.gitlab-ci.yml`: +First, in your `.gitlab-ci.yml` add: - services: - - redis +```yaml +services: + - redis:latest +``` -2. Configure your application to use the database: +Then you need to configure your application to use the Redis database, for +example: - Host: redis +```bash +Host: redis +``` -3. You can also use any other available on [DockerHub](https://hub.docker.com/_/redis/). For example: `redis:2.6`. +And that's it. Redis will now be available to be used within your testing +framework. -Example: https://gitlab.com/gitlab-examples/redis/blob/master/.gitlab-ci.yml +If you want to use any other version of Redis, check the available versions +on [Docker Hub](https://hub.docker.com/_/redis/). -### Use Redis with Shell executor +## Use Redis with Shell executor -It's possible to use Redis on manually configured servers that are using GitLab Runner with Shell executor. +Redis can also be used on manually configured servers that are using GitLab +Runner with the Shell executor. -1. First install the Redis server: +In your build machine install the Redis server: - sudo apt-get install redis-server +```bash +sudo apt-get install redis-server +``` -2. Try to connect to the server: +Verify that you can connect to the server with the `gitlab-runner` user: - # Try connecting the the Redis server - sudo -u gitlab-runner -H redis-cli +```bash +# Try connecting the the Redis server +sudo -u gitlab-runner -H redis-cli - # Quit the session - 127.0.0.1:6379> quit +# Quit the session +127.0.0.1:6379> quit +``` -4. Configure your application to use the database: +Finally, configure your application to use the database, for example: - Host: localhost +```bash +Host: localhost +``` + +## Example project + +We have set up an [Example Redis Project][redis-example-repo] for your convenience +that runs on [GitLab.com](https://gitlab.com) using our publicly available +[shared runners](../runners/README.md). + +Want to hack on it? Simply fork it, commit and push your changes. Within a few +moments the changes will be picked by a public runner and the build will begin. + +[redis-example-repo]: https://gitlab.com/gitlab-examples/redis From 149c934a1d38662358b48cdf844430de7845d1c4 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 8 Dec 2015 15:59:41 +0200 Subject: [PATCH 448/908] More cleanup on Redis example --- doc/ci/services/redis.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/ci/services/redis.md b/doc/ci/services/redis.md index b0d04f9540..e6add57ca7 100644 --- a/doc/ci/services/redis.md +++ b/doc/ci/services/redis.md @@ -4,10 +4,10 @@ As many applications depend on Redis as their key-value store, you will eventually need it in order for your tests to run. Below you are guided how to do this with the Docker and Shell executors of GitLab Runner. -## Use Redis with Docker executor +## Use Redis with the Docker executor -If you are using our Docker integration you basically have everything set up -already. +If you are using GitLab's Runner Docker integration you basically have +everything set up already. First, in your `.gitlab-ci.yml` add: @@ -29,7 +29,7 @@ framework. If you want to use any other version of Redis, check the available versions on [Docker Hub](https://hub.docker.com/_/redis/). -## Use Redis with Shell executor +## Use Redis with the Shell executor Redis can also be used on manually configured servers that are using GitLab Runner with the Shell executor. From 06b86de996232b956129f2bb5dde4a7647f94f69 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 8 Dec 2015 18:22:45 +0200 Subject: [PATCH 449/908] Clean up postgres CI example [ci skip] --- doc/ci/services/postgres.md | 116 ++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 45 deletions(-) diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md index e57f8c5944..f82a828756 100644 --- a/doc/ci/services/postgres.md +++ b/doc/ci/services/postgres.md @@ -1,70 +1,96 @@ -## Using PostgreSQL +# Using PostgreSQL -It's possible to use PostgreSQL database test your apps during builds. +As many applications depend on PostgreSQL as their database, you will +eventually need it in order for your tests to run. Below you are guided how to +do this with the Docker and Shell executors of GitLab Runner. -### Use PostgreSQL with Docker executor +## Use PostgreSQL with the Docker executor -If you are using our Docker integration you basically have everything already. +If you are using GitLab's Runner with the Docker executor you basically have +everything set up already. -1. Add this to your `.gitlab-ci.yml`: +First, in your `.gitlab-ci.yml` add: - services: - - postgres +```yaml +services: + - postgres - variables: - # Configure postgres service (https://hub.docker.com/_/postgres/) - POSTGRES_DB: hello_world_test - POSTGRES_USER: postgres - POSTGRES_PASSWORD: "" +variables: + POSTGRES_DB: nice_marmot + POSTGRES_USER: gitlab_runner + POSTGRES_PASSWORD: "" +``` -2. Configure your application to use the database: +And then configure your application to use PostgreSQL, for example: - Host: postgres - User: postgres - Password: postgres - Database: hello_world_test +```yaml +Host: localhost +User: gitlab_runner +Password: +Database: nice_marmot +``` -3. You can also use any other available on [DockerHub](https://hub.docker.com/_/postgres/). For example: `postgres:9.3`. +You can also use any other docker image available on [Docker Hub][hub-pg]. +For example, to use PostgreSQL 9.3 the service becomes `postgres:9.3`. -Example: https://gitlab.com/gitlab-examples/postgres/blob/master/.gitlab-ci.yml +The `postgres` image can accept some environment variables. For more details +check the documentation on [Docker Hub][hub-pg]. -### Use PostgreSQL with Shell executor +## Use PostgreSQL with the Shell executor -It's possible to use PostgreSQL on manually configured servers that are using GitLab Runner with Shell executor. +You can also use PostgreSQL on manually configured servers that are using +GitLab Runner with the Shell executor. -1. First install the PostgreSQL server: +First install the PostgreSQL server: - sudo apt-get install -y postgresql postgresql-client libpq-dev +```bash +sudo apt-get install -y postgresql postgresql-client libpq-dev +``` -2. Create an user: +Then create a user: - # Install the database packages - sudo apt-get install -y postgresql postgresql-client libpq-dev +```bash +# Login to PostgreSQL +sudo -u postgres psql -d template1 - # Login to PostgreSQL - sudo -u postgres psql -d template1 +# Create a user for GitLab Runner that can create databases +# Do not type the 'template1=#', this is part of the prompt +template1=# CREATE USER gitlab_runner CREATEDB; - # Create a user for runner - # Do not type the 'template1=#', this is part of the prompt - template1=# CREATE USER runner CREATEDB; +# Create the database & grant all privileges on database +template1=# CREATE DATABASE nice_marmot OWNER gitlab_runner; - # Create the database & grant all privileges on database - template1=# CREATE DATABASE hello_world_test OWNER runner; +# Quit the database session +template1=# \q +``` - # Quit the database session - template1=# \q +Try to connect to database: -3. Try to connect to database: +```bash +# Try connecting to the new database with the new user +sudo -u gitlab_runner -H psql -d nice_marmot - # Try connecting to the new database with the new user - sudo -u gitlab-runner -H psql -d hello_world_test +# Quit the database session +nice_marmot> \q +``` - # Quit the database session - hello_world_test> \q +Finally, configure your application to use the database: -4. Configure your application to use the database: +```bash +Host: localhost +User: gitlab_runner +Password: +Database: nice_marmot +``` - Host: localhost - User: runner - Password: - Database: hello_world_test +## Example project + +We have set up an [Example PostgreSQL Project][postgres-example-repo] for your +convenience that runs on [GitLab.com](https://gitlab.com) using our publicly +available [shared runners](../runners/README.md). + +Want to hack on it? Simply fork it, commit and push your changes. Within a few +moments the changes will be picked by a public runner and the build will begin. + +[hub-pg]: https://hub.docker.com/_/postgres/ +[postgres-example-repo]: https://gitlab.com/gitlab-examples/postgres From 1d410ac96a96462d9f48ec4e43a4e819bbffdeee Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 7 Dec 2015 23:01:53 -0800 Subject: [PATCH 450/908] Update project repository size and commit count during import:repos task Closes #3848 --- CHANGELOG | 1 + lib/tasks/gitlab/import.rake | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 52c5941a3a..7c0c94f487 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) - Bump gollum-lib to 4.1.0 (Stan Hu) - Fix broken group avatar upload under "New group" (Stan Hu) + - Update project repositorize size and commit count during import:repos task (Stan Hu) - Fix API setting of 'public' attribute to false will make a project private (Stan Hu) - Handle and report SSL errors in Web hook test (Stan Hu) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index c1ee271ae2..1c04f47f08 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -64,6 +64,8 @@ namespace :gitlab do if project.persisted? puts " * Created #{project.name} (#{repo_path})".green + project.update_repository_size + project.update_commit_count else puts " * Failed trying to create #{project.name} (#{repo_path})".red puts " Errors: #{project.errors.messages}".red From bf5683f8892c4aefc4c996812ece6291b701dada Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Tue, 8 Dec 2015 09:47:42 -0600 Subject: [PATCH 451/908] Block LDAP user when they are no longer found in the LDAP server --- CHANGELOG | 1 + doc/integration/ldap.md | 8 +++++++- lib/gitlab/ldap/access.rb | 4 +++- spec/lib/gitlab/ldap/access_spec.rb | 5 +++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7c0c94f487..67f70e676c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -23,6 +23,7 @@ v 8.3.0 (unreleased) - Run custom Git hooks when branch is created or deleted. - Fix bug when simultaneously accepting multiple MRs results in MRs that are of "merged" status, but not merged to the target branch - Add languages page to graphs + - Block LDAP user when they are no longer found in the LDAP server v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index 7e2920b886..845f588f91 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -13,6 +13,12 @@ An LDAP user who is allowed to change their email on the LDAP server can [take o We recommend against using GitLab LDAP integration if your LDAP users are allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on the LDAP server. +If a user is deleted from the LDAP server, they will be blocked in GitLab as well. +Users will be immediately blocked from logging in. However, there is an LDAP check +cache time of one hour. The means users that are already logged in or are using Git +over SSH will still be able to access GitLab for up to one hour. Manually block +the user in the GitLab Admin area to immediately block all access. + ## Configuring GitLab for LDAP integration To enable GitLab LDAP integration you need to add your LDAP server settings in `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. @@ -192,4 +198,4 @@ Not supported by GitLab's configuration options. When setting `method: ssl`, the underlying authentication method used by `omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with the LDAP server before any LDAP-protocol data is exchanged but no validation of -the LDAP server's SSL certificate is performed. \ No newline at end of file +the LDAP server's SSL certificate is performed. diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index 16ff03c38d..c438a3d167 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -37,13 +37,15 @@ module Gitlab # Block user in GitLab if he/she was blocked in AD if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter) - user.block unless user.blocked? + user.block false else user.activate if user.blocked? && !ldap_config.block_auto_created_users true end else + # Block the user if they no longer exist in LDAP/AD + user.block false end rescue diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index c38f212b40..960547a0ad 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -13,6 +13,11 @@ describe Gitlab::LDAP::Access do end it { is_expected.to be_falsey } + + it 'should block user in GitLab' do + access.allowed? + expect(user).to be_blocked + end end context 'when the user is found' do From 47e81da8c2746196ad0506d30720c775a538ebdb Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 8 Dec 2015 21:11:11 +0200 Subject: [PATCH 452/908] Clean up mysql CI example --- doc/ci/services/mysql.md | 137 +++++++++++++++++++++++++-------------- 1 file changed, 90 insertions(+), 47 deletions(-) diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md index 3155af6b3e..7f50b4d76c 100644 --- a/doc/ci/services/mysql.md +++ b/doc/ci/services/mysql.md @@ -1,72 +1,115 @@ -## Using MySQL +# Using MySQL -It's possible to use MySQL database test your apps during builds. +As many applications depend on MySQL as their database, you will eventually +need it in order for your tests to run. Below you are guided how to do this +with the Docker and Shell executors of GitLab Runner. -### Use MySQL with Docker executor +## Use MySQL with the Docker executor -If you are using our Docker integration you basically have everything already. +If you are using [GitLab Runner](../runners/README.md) with the Docker executor +you basically have everything set up already. -1. Add this to your `.gitlab-ci.yml`: +First, in your `.gitlab-ci.yml` add: - services: - - mysql +```yaml +services: + - mysql - variables: - # Configure mysql service (https://hub.docker.com/_/mysql/) - MYSQL_DATABASE: hello_world_test - MYSQL_ROOT_PASSWORD: mysql +variables: + # Configure mysql environment variables (https://hub.docker.com/_/mysql/) + MYSQL_DATABASE: el_duderino + MYSQL_ROOT_PASSWORD: mysql_strong_password +``` -2. Configure your application to use the database: +And then configure your application to use the database, for example: - Host: mysql - User: root - Password: mysql - Database: hello_world_test +```yaml +Host: localhost +User: root +Password: mysql_strong_password +Database: el_duderino +``` -3. You can also use any other available on [DockerHub](https://hub.docker.com/_/mysql/). For example: `mysql:5.5`. +You can also use any other docker image available on [Docker Hub][hub-mysql]. +For example, to use MySQL 5.5 the service becomes `mysql:5.5`. -Example: https://gitlab.com/gitlab-examples/mysql/blob/master/.gitlab-ci.yml +The `mysql` image can accept some environment variables. For more details +check the documentation on [Docker Hub][hub-mysql]. -### Use MySQL with Shell executor +## Use MySQL with the Shell executor -It's possible to use MySQL on manually configured servers that are using GitLab Runner with Shell executor. +You can also use MySQL on manually configured servers that are using +GitLab Runner with the Shell executor. -1. First install the MySQL server: - - sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev +First install the MySQL server: - # Pick a MySQL root password (can be anything), type it and press enter - # Retype the MySQL root password and press enter +```bash +sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev +``` -2. Create an user: +Pick a MySQL root password (can be anything), and type it twice when asked. - mysql -u root -p +*Note: As a security measure you can run `mysql_secure_installation` to +remove anonymous users, drop the test database and disable remote logins with +the root user.* - # Create a user which will be used by your apps - # do not type the 'mysql>', this is part of the prompt - # change $password in the command below to a real password you pick - mysql> CREATE USER 'runner'@'localhost' IDENTIFIED BY '$password'; +The next step is to create a user, so login to MySQL as root: - # Ensure you can use the InnoDB engine which is necessary to support long indexes - # If this fails, check your MySQL config files (e.g. `/etc/mysql/*.cnf`, `/etc/mysql/conf.d/*`) for the setting "innodb = off" - mysql> SET storage_engine=INNODB; +```bash +mysql -u root -p +``` - # Create the database - mysql> CREATE DATABASE IF NOT EXISTS `hello_world_test` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`; +Then create a user (in our case `runner`) which will be used by your +application. Change `$password` in the command below to a real strong password. - # Grant necessary permissions on the database - mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES ON `hello_world_test`.* TO 'runner'@'localhost'; +*Note: Do not type `mysql>`, this is part of the MySQL prompt.* - # Quit the database session - mysql> \q +```bash +mysql> CREATE USER 'runner'@'localhost' IDENTIFIED BY '$password'; +``` -3. Try to connect to database: +Create the database: - sudo -u gitlab-runner -H mysql -u runner -p -D hello_world_test +```bash +mysql> CREATE DATABASE IF NOT EXISTS `el_duderino` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`; +``` -4. Configure your application to use the database: +Grant the necessary permissions on the database: - Host: localhost - User: runner - Password: $password - Database: hello_world_test +```bash +mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES ON `el_duderino`.* TO 'runner'@'localhost'; +``` + +If all went well you can now quit the database session: + +```bash +mysql> \q +``` + +Now, try to connect to the newly created database to check that everything is +in place: + +```bash +mysql -u runner -p -D el_duderino +``` + +As a final step, configure your application to use the database, for example: + +```bash +Host: localhost +User: runner +Password: $password +Database: el_duderino +``` + +## Example project + +We have set up an [Example MySQL Project][mysql-example-repo] for your +convenience that runs on [GitLab.com](https://gitlab.com) using our publicly +available [shared runners](../runners/README.md). + +Want to hack on it? Simply fork it, commit and push your changes. Within a few +moments the changes will be picked by a public runner and the build will begin. + +[hub-mysql]: https://hub.docker.com/_/mysql/ +[mysql-example-repo]: https://gitlab.com/gitlab-examples/mysql From 9dc91f46df377d6220928d6292dac73bc6bae295 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 8 Dec 2015 21:11:27 +0200 Subject: [PATCH 453/908] Add link to runners doc [ci skip] --- doc/ci/services/postgres.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md index f82a828756..b8436f4f7c 100644 --- a/doc/ci/services/postgres.md +++ b/doc/ci/services/postgres.md @@ -6,8 +6,8 @@ do this with the Docker and Shell executors of GitLab Runner. ## Use PostgreSQL with the Docker executor -If you are using GitLab's Runner with the Docker executor you basically have -everything set up already. +If you are using [GitLab Runner](../runners/README.md) with the Docker executor +you basically have everything set up already. First, in your `.gitlab-ci.yml` add: @@ -21,7 +21,7 @@ variables: POSTGRES_PASSWORD: "" ``` -And then configure your application to use PostgreSQL, for example: +And then configure your application to use the database, for example: ```yaml Host: localhost From cf44dc510786775a1303384d858eb0a7bf33c34f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 8 Dec 2015 21:35:08 +0200 Subject: [PATCH 454/908] Add note about the various phpenv tools --- doc/ci/languages/php.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/ci/languages/php.md b/doc/ci/languages/php.md index 60079c090d..dacb67fa3f 100644 --- a/doc/ci/languages/php.md +++ b/doc/ci/languages/php.md @@ -179,6 +179,14 @@ Using phpenv also allows to easily configure the PHP environment with: phpenv config-add my_config.ini ``` +*__Important note:__ It seems `phpenv/phpenv` + [is abandoned](https://github.com/phpenv/phpenv/issues/57). There is a fork + at [madumlao/phpenv](https://github.com/madumlao/phpenv) that tries to bring + the project back to life. [CHH/phpenv](https://github.com/CHH/phpenv) also + seems like a good alternative. Picking any of the mentioned tools will work + with the basic phpenv commands. Guiding you to choose the right phpenv is out + of the scope of this tutorial.* + ### Install custom extensions Since this is a pretty bare installation of the PHP environment, you may need From 4ea0f06470620064c5d23e28c778f83296aeb551 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 8 Dec 2015 21:35:25 +0200 Subject: [PATCH 455/908] Fix wrong naming of services keyword [ci skip] --- doc/ci/services/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/services/README.md b/doc/ci/services/README.md index 0856b7679c..1ebb0a4a25 100644 --- a/doc/ci/services/README.md +++ b/doc/ci/services/README.md @@ -1,6 +1,6 @@ ## GitLab CI Services -GitLab CI uses the `service` keyword to define what docker containers should be +GitLab CI uses the `services` keyword to define what docker containers should be linked with your base image. Below is a list of examples you may use. + [Using MySQL](mysql.md) From 0c22635aa2036ab3394c7e7bd99fd61ddd99c43c Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 8 Dec 2015 21:41:56 +0200 Subject: [PATCH 456/908] More redis CI example clean up --- doc/ci/services/redis.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/ci/services/redis.md b/doc/ci/services/redis.md index e6add57ca7..b281e8f9f6 100644 --- a/doc/ci/services/redis.md +++ b/doc/ci/services/redis.md @@ -6,8 +6,8 @@ do this with the Docker and Shell executors of GitLab Runner. ## Use Redis with the Docker executor -If you are using GitLab's Runner Docker integration you basically have -everything set up already. +If you are using [GitLab Runner](../runners/README.md) with the Docker executor +you basically have everything set up already. First, in your `.gitlab-ci.yml` add: @@ -19,15 +19,15 @@ services: Then you need to configure your application to use the Redis database, for example: -```bash +```yaml Host: redis ``` And that's it. Redis will now be available to be used within your testing framework. -If you want to use any other version of Redis, check the available versions -on [Docker Hub](https://hub.docker.com/_/redis/). +You can also use any other docker image available on [Docker Hub][hub-redis]. +For example, to use Redis 2.8 the service becomes `redis:2.8`. ## Use Redis with the Shell executor @@ -52,7 +52,7 @@ sudo -u gitlab-runner -H redis-cli Finally, configure your application to use the database, for example: -```bash +```yaml Host: localhost ``` @@ -65,4 +65,5 @@ that runs on [GitLab.com](https://gitlab.com) using our publicly available Want to hack on it? Simply fork it, commit and push your changes. Within a few moments the changes will be picked by a public runner and the build will begin. +[hub-redis]: https://hub.docker.com/_/redis/ [redis-example-repo]: https://gitlab.com/gitlab-examples/redis From ebee5077f2437d0828784ff23b19ed470bed534d Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 8 Dec 2015 14:47:28 -0500 Subject: [PATCH 457/908] Make tab target selectors less naive Prior, any of the specified IDs could have been hijacked by a table of contents header, breaking the tab functionality. For example, a `## Notes` header would get the id `notes` and prevent the Discussion tab from being activated. Closes #3908 --- app/views/projects/merge_requests/_new_submit.html.haml | 4 ++-- app/views/projects/merge_requests/_show.html.haml | 6 +++--- spec/javascripts/fixtures/merge_request_tabs.html.haml | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 156922cea4..0bcc826e8d 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -20,11 +20,11 @@ .mr-compare.merge-request %ul.merge-request-tabs.center-top-menu.no-top.no-bottom %li.commits-tab - = link_to url_for(params), data: {target: '#commits', action: 'commits', toggle: 'tab'} do + = link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits %span.badge= @commits.size %li.diffs-tab.active - = link_to url_for(params), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do + = link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do Changes %span.badge= @diffs.size diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index f5aff0877e..6a89df3823 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -44,15 +44,15 @@ - if @commits.present? %ul.merge-request-tabs.center-top-menu.no-top.no-bottom %li.notes-tab - = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#notes', action: 'notes', toggle: 'tab'} do + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do Discussion %span.badge= @merge_request.mr_and_commit_notes.user.count %li.commits-tab - = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#commits', action: 'commits', toggle: 'tab'} do + = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits %span.badge= @commits.size %li.diffs-tab - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do Changes %span.badge= @merge_request.diffs.size diff --git a/spec/javascripts/fixtures/merge_request_tabs.html.haml b/spec/javascripts/fixtures/merge_request_tabs.html.haml index 7624a71394..68678c3d7e 100644 --- a/spec/javascripts/fixtures/merge_request_tabs.html.haml +++ b/spec/javascripts/fixtures/merge_request_tabs.html.haml @@ -1,12 +1,12 @@ %ul.nav.nav-tabs.merge-request-tabs %li.notes-tab - %a{href: '/foo/bar/merge_requests/1', data: {target: '#notes', action: 'notes', toggle: 'tab'}} + %a{href: '/foo/bar/merge_requests/1', data: {target: 'div#notes', action: 'notes', toggle: 'tab'}} Discussion %li.commits-tab - %a{href: '/foo/bar/merge_requests/1/commits', data: {target: '#commits', action: 'commits', toggle: 'tab'}} + %a{href: '/foo/bar/merge_requests/1/commits', data: {target: 'div#commits', action: 'commits', toggle: 'tab'}} Commits %li.diffs-tab - %a{href: '/foo/bar/merge_requests/1/diffs', data: {target: '#diffs', action: 'diffs', toggle: 'tab'}} + %a{href: '/foo/bar/merge_requests/1/diffs', data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'}} Diffs .tab-content From bbe652cdaf11dae7fa917bac1ffa4a02f2099026 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 8 Dec 2015 22:26:41 +0200 Subject: [PATCH 458/908] More postgres CI service example cleanup --- doc/ci/services/postgres.md | 57 +++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md index b8436f4f7c..db5be070ee 100644 --- a/doc/ci/services/postgres.md +++ b/doc/ci/services/postgres.md @@ -13,11 +13,11 @@ First, in your `.gitlab-ci.yml` add: ```yaml services: - - postgres + - postgres:latest variables: POSTGRES_DB: nice_marmot - POSTGRES_USER: gitlab_runner + POSTGRES_USER: runner POSTGRES_PASSWORD: "" ``` @@ -25,7 +25,7 @@ And then configure your application to use the database, for example: ```yaml Host: localhost -User: gitlab_runner +User: runner Password: Database: nice_marmot ``` @@ -47,39 +47,54 @@ First install the PostgreSQL server: sudo apt-get install -y postgresql postgresql-client libpq-dev ``` -Then create a user: +The next step is to create a user, so login to PostgreSQL: ```bash -# Login to PostgreSQL sudo -u postgres psql -d template1 +``` -# Create a user for GitLab Runner that can create databases -# Do not type the 'template1=#', this is part of the prompt -template1=# CREATE USER gitlab_runner CREATEDB; +Then create a user (in our case `runner`) which will be used by your +application. Change `$password` in the command below to a real strong password. -# Create the database & grant all privileges on database -template1=# CREATE DATABASE nice_marmot OWNER gitlab_runner; +*__Note:__ Do not type `template1=#`, this is part of the PostgreSQL prompt.* -# Quit the database session +```bash +template1=# CREATE USER runner WITH PASSWORD '$password' CREATEDB; +``` + +*__Note:__ Notice that we created the user with the privilege to be able to +create databases (`CREATEDB`). In the following steps we will create a database +explicitly for that user but having that privilege can be useful if in your +testing framework you have tools that drop and create databases.* + +Create the database and grant all privileges on it for the user `runner`: + +```bash +template1=# CREATE DATABASE nice_marmot OWNER runner; +``` + +If all went well you can now quit the database session: + +```bash template1=# \q ``` -Try to connect to database: +Now, try to connect to the newly created database with the user `runner` to +check that everything is in place. ```bash -# Try connecting to the new database with the new user -sudo -u gitlab_runner -H psql -d nice_marmot - -# Quit the database session -nice_marmot> \q +psql -U runner -h localhost -d nice_marmot -W ``` -Finally, configure your application to use the database: +*__Note:__ We are explicitly telling `psql` to connect to localhost in order +to use the md5 authentication. If you omit this step you will be denied access.* -```bash +Finally, configure your application to use the database, for example: + +```yaml Host: localhost -User: gitlab_runner -Password: +User: runner +Password: $password Database: nice_marmot ``` From d55dcd11d59ebfadf49290746c5f30c2e4780c6b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 8 Dec 2015 22:27:23 +0200 Subject: [PATCH 459/908] Use the latest docker image in mysql CI example [ci skip] --- doc/ci/services/mysql.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md index 7f50b4d76c..44bc613e6f 100644 --- a/doc/ci/services/mysql.md +++ b/doc/ci/services/mysql.md @@ -13,7 +13,7 @@ First, in your `.gitlab-ci.yml` add: ```yaml services: - - mysql + - mysql:latest variables: # Configure mysql environment variables (https://hub.docker.com/_/mysql/) From 813c9b2aeb9e5d44c99f02a2373ee0e007d0e87d Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 8 Dec 2015 15:29:46 -0500 Subject: [PATCH 460/908] Make cross-project reference's clipboard target less naive See !2023 --- app/views/projects/issues/_discussion.html.haml | 2 +- app/views/projects/merge_requests/_discussion.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 21eee70b42..f2011542ca 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -17,7 +17,7 @@ .input-group.cross-project-reference %span#cross-project-reference.slead.has_tooltip{title: 'Cross-project reference'} = cross_project_reference(@project, @issue) - = clipboard_button(clipboard_target: '#cross-project-reference') + = clipboard_button(clipboard_target: 'span#cross-project-reference') .row %section.col-md-9 diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 3d7bd78dce..d64b19ae91 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -17,7 +17,7 @@ .input-group.cross-project-reference %span#cross-project-reference.slead.has_tooltip{title: 'Cross-project reference'} = cross_project_reference(@project, @merge_request) - = clipboard_button(clipboard_target: '#cross-project-reference') + = clipboard_button(clipboard_target: 'span#cross-project-reference') .row %section.col-md-9 From f66454beaa880d283ef527c3a32a3076fedf6551 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 21:43:11 +0100 Subject: [PATCH 461/908] Add indication to merge request list item that MR cannot be merged automatically --- app/assets/stylesheets/framework/lists.scss | 18 ++++++++ app/models/merge_request.rb | 4 ++ app/views/projects/issues/_issue.html.haml | 25 ++++++----- .../merge_requests/_merge_request.html.haml | 41 +++++++++++-------- 4 files changed, 61 insertions(+), 27 deletions(-) diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index a798ae812e..2404f8898f 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -124,6 +124,24 @@ ul.content-list { padding: 10px 14px; } } + + ul.controls { + list-style: none; + + li { + float: left; + padding-right: 10px; + + .author_link { + display: inline-block; + + .avatar-inline { + margin-left: 0; + margin-right: 0; + } + } + } + } } } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 080b7f7fb8..1f84dc2a57 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -484,4 +484,8 @@ class MergeRequest < ActiveRecord::Base source_project.ci_commit(last_commit.id) end end + + def broken? + self.commits.blank? || branch_missing? || cannot_be_merged? + end end diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 1eb71990e5..7d9fc8d2c8 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -6,23 +6,26 @@ .issue-title %span.issue-title-text = link_to_gfm issue.title, issue_path(issue), class: "row_title" - .pull-right.light + %ul.controls.light - if issue.closed? - %span + %li CLOSED + - if issue.assignee - = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name") + %li + = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name") + - note_count = issue.notes.user.count - if note_count > 0 -   - = link_to issue_path(issue) + "#notes" do - = icon('comments') - = note_count + %li + = link_to issue_path(issue) + "#notes" do + = icon('comments') + = note_count - else -   - = link_to issue_path(issue) + "#notes", class: "issue-no-comments" do - = icon('comments') - = 0 + %li + = link_to issue_path(issue) + "#notes", class: "issue-no-comments" do + = icon('comments') + = notes_count .issue-info #{issue.to_reference} · diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 1d4c9b66c4..5ee7738e9c 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -3,31 +3,40 @@ .merge-request-title %span.merge-request-title-text = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" - .pull-right.light - - if ci_commit - = render_ci_status(ci_commit) + %ul.controls.light - if merge_request.merged? - %span + %li = icon('check') MERGED - elsif merge_request.closed? - %span + %li = icon('ban') CLOSED - - note_count = merge_request.mr_and_commit_notes.user.count + + - if ci_commit + %li + = render_ci_status(ci_commit) + + - if merge_request.open? && merge_request.broken? + %li + = link_to merge_request_path(merge_request), class: "has_tooltip", title: "Cannot be merged automatically", data: {container: 'body'} do + = icon('exclamation-triangle') + - if merge_request.assignee -   - = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name") + %li + = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name") + + - note_count = merge_request.mr_and_commit_notes.user.count - if note_count > 0 -   - = link_to merge_request_path(merge_request) + "#notes" do - = icon('comments') - = note_count + %li + = link_to merge_request_path(merge_request) + "#notes" do + = icon('comments') + = note_count - else -   - = link_to merge_request_path(merge_request) + "#notes", class: "merge-request-no-comments" do - = icon('comments') - = 0 + %li + = link_to merge_request_path(merge_request) + "#notes", class: "merge-request-no-comments" do + = icon('comments') + = note_count .merge-request-info \##{merge_request.iid} · From 065375ca2d482faac80897e42d82b3afdcb08d99 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 21:43:52 +0100 Subject: [PATCH 462/908] Use "Build passed" in tooltip instead of "Build status: passed" --- app/helpers/ci_status_helper.rb | 2 +- features/steps/dashboard/dashboard.rb | 2 +- features/steps/project/merge_requests.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 0ecf77bb45..599a9adc31 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -46,7 +46,7 @@ module CiStatusHelper def render_ci_status(ci_commit) link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}", - title: "Build status: #{ci_commit.status}", + title: "Build #{ci_commit.status}", data: { toggle: 'tooltip', placement: 'left' } do ci_status_icon(ci_commit) end diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index f0fbd8a826..63f0ec2b6e 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -12,7 +12,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps end step 'I should see "Shop" project CI status' do - expect(page).to have_link "Build status: skipped" + expect(page).to have_link "Build skipped" end step 'I should see last push widget' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 822cf0ffe1..0107d9d848 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -367,7 +367,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see merge request "Bug NS-05" with CI status' do page.within ".mr-list" do - expect(page).to have_link "Build status: pending" + expect(page).to have_link "Build pending" end end From 23b6a98de00b966728f6b5ed3747b0d2e078165f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 22:28:28 +0100 Subject: [PATCH 463/908] Move Builds tab to the end --- .../javascripts/merge_request_tabs.js.coffee | 26 +++++++++---------- .../merge_requests/_new_submit.html.haml | 16 ++++++------ .../projects/merge_requests/_show.html.haml | 12 ++++----- config/routes.rb | 2 +- .../merge_request_reference_filter.rb | 4 +-- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 69a12fdd04..b0eeb1db53 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -68,10 +68,10 @@ class @MergeRequestTabs if action == 'commits' @loadCommits($target.attr('href')) - else if action == 'builds' - @loadBuilds($target.attr('href')) else if action == 'diffs' @loadDiff($target.attr('href')) + else if action == 'builds' + @loadBuilds($target.attr('href')) @setCurrentAction(action) @@ -110,7 +110,7 @@ class @MergeRequestTabs action = 'notes' if action == 'show' # Remove a trailing '/commits' or '/diffs' - new_state = @_location.pathname.replace(/\/(commits|builds|diffs)(\.html)?\/?$/, '') + new_state = @_location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, '') # Append the new action if we're on a tab other than 'notes' unless action == 'notes' @@ -138,6 +138,16 @@ class @MergeRequestTabs @commitsLoaded = true @scrollToElement("#commits") + loadDiff: (source) -> + return if @diffsLoaded + + @_get + url: "#{source}.json" + @_location.search + success: (data) => + document.getElementById('diffs').innerHTML = data.html + @diffsLoaded = true + @scrollToElement("#diffs") + loadBuilds: (source) -> return if @buildsLoaded @@ -149,16 +159,6 @@ class @MergeRequestTabs @buildsLoaded = true @scrollToElement("#builds") - loadDiff: (source) -> - return if @diffsLoaded - - @_get - url: "#{source}.json" + @_location.search - success: (data) => - document.getElementById('diffs').innerHTML = data.html - @diffsLoaded = true - @scrollToElement("#diffs") - # Show or hide the loading spinner # # status - Boolean, true to show, false to hide diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 3f6e421fcd..4172d5a4e8 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -23,22 +23,19 @@ = link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits %span.badge= @commits.size - - if @ci_commit - %li.builds-tab.active - = link_to url_for(params), data: {target: '#builds', action: 'builds', toggle: 'tab'} do - Builds - %span.badge= @statuses.size %li.diffs-tab.active = link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do Changes %span.badge= @diffs.size + - if @ci_commit + %li.builds-tab.active + = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do + Builds + %span.badge= @statuses.size .tab-content #commits.commits.tab-pane = render "projects/merge_requests/show/commits" - - if @ci_commit - #builds.builds.tab-pane - = render "projects/merge_requests/show/builds" #diffs.diffs.tab-pane.active - if @diffs.present? = render "projects/diffs/diffs", diffs: @diffs, project: @project @@ -50,6 +47,9 @@ .alert.alert-danger %h4 This comparison includes a huge diff. %p To preserve performance the line changes are not shown. + - if @ci_commit + #builds.builds.tab-pane + = render "projects/merge_requests/show/builds" :javascript $('.assign-to-me-link').on('click', function(e){ diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 686a9a2c75..960d1561e7 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -50,25 +50,25 @@ = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits %span.badge= @commits.size + %li.diffs-tab + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do + Changes + %span.badge= @merge_request.diffs.size - if @ci_commit %li.builds-tab = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do Builds %span.badge= @statuses.size - %li.diffs-tab - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do - Changes - %span.badge= @merge_request.diffs.size .tab-content #notes.notes.tab-pane.voting_notes = render "projects/merge_requests/discussion" #commits.commits.tab-pane - # This tab is always loaded via AJAX - #builds.builds.tab-pane - - # This tab is always loaded via AJAX #diffs.diffs.tab-pane - # This tab is always loaded via AJAX + #builds.builds.tab-pane + - # This tab is always loaded via AJAX .mr-loading-status = spinner diff --git a/config/routes.rb b/config/routes.rb index 359cd08e13..046e180023 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -570,9 +570,9 @@ Rails.application.routes.draw do resources :merge_requests, constraints: { id: /\d+/ }, except: [:destroy] do member do + get :commits get :diffs get :builds - get :commits post :merge get :merge_check get :ci_status diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb index 79d67870b1..2eb77c46da 100644 --- a/lib/gitlab/markdown/merge_request_reference_filter.rb +++ b/lib/gitlab/markdown/merge_request_reference_filter.rb @@ -28,10 +28,10 @@ module Gitlab case path when '/diffs' extras.unshift "diffs" - when '/builds' - extras.unshift "builds" when '/commits' extras.unshift "commits" + when '/builds' + extras.unshift "builds" end extras From b312edaf440b729dad02749259577f6e0589c061 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 8 Dec 2015 23:29:49 +0200 Subject: [PATCH 464/908] More clean up to the CI example on how to use docker images [ci skip] --- doc/ci/docker/using_docker_images.md | 100 ++++++++++----------------- 1 file changed, 36 insertions(+), 64 deletions(-) diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 488479418b..8d4bd44053 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -1,28 +1,29 @@ # Using Docker Images -GitLab CI can use [Docker Engine](https://www.docker.com/) to build projects. +GitLab CI in conjuction with [GitLab Runner](../runners/README.md) can use +[Docker Engine](https://www.docker.com/) to test and build any application. -Docker is an open-source project that allows to use predefined images to run -applications in independent "containers" that are run within a single Linux +Docker is an open-source project that allows you to use predefined images to +run applications in independent "containers" that are run within a single Linux instance. [Docker Hub][hub] has a rich database of prebuilt images that can be used to test and build your applications. Docker, when used with GitLab CI, runs each build in a separate and isolated -container using the predefined image that is set up in `.gitlab-ci.yml`. The -container is always euphemeral which means you get only one container per build. +container using the predefined image that is set up in +[`.gitlab-ci.yml`](../yaml/README.md). This makes it easier to have a simple and reproducible build environment that can also run on your workstation. The added benefit is that you can test all the commands that we will explore later from your shell, rather than having to -test them on a CI server. +test them on a dedicated CI server. -### Register docker runner +## Register docker runner To use GitLab Runner with docker you need to register a new runner to use the `docker` executor: ```bash -gitlab-ci-multi-runner register \ +gitlab-runner register \ --url "https://gitlab.com/" \ --registration-token "PROJECT_REGISTRATION_TOKEN" \ --description "docker-ruby-2.1" \ @@ -36,7 +37,7 @@ The registered runner will use the `ruby:2.1` docker image and will run two services, `postgres:latest` and `mysql:latest`, both of which will be accessible during the build process. -### What is image +## What is image The `image` keyword is the name of the docker image that is present in the local Docker Engine (list all images with `docker images`) or any image that @@ -46,9 +47,9 @@ Hub please read the [Docker Fundamentals][] documentation. In short, with `image` we refer to the docker image, which will be used to create a container on which your build will run. -### What is service +## What is service -The `service` keyword defines just another docker image that is run during +The `services` keyword defines just another docker image that is run during your build and is linked to the docker image that the `image` keyword defines. This allows you to access the service image during build time. @@ -57,9 +58,12 @@ run a database container, eg. `mysql`. It's easier and faster to use an existing image and run it as an additional container than install `mysql` every time the project is built. -#### How is service linked to the build +You can see some widely used services examples in the relevant documentation of +[CI services examples](../services/README.md). -To better undestand how the container linking works, read +### How is service linked to the build + +To better understand how the container linking works, read [Linking containers together](https://docs.docker.com/userguide/dockerlinks/). To summarize, if you add `mysql` as service to your application, the image will @@ -69,25 +73,27 @@ The service container for MySQL will be accessible under the hostname `mysql`. So, in order to access your database service you have to connect to the host named `mysql` instead of a socket or `localhost`. -### Overwrite image and services +## Overwrite image and services -See the section below. +See [How to use other images as services](#how-to-use-other-images-as-services). -### How to use other images as services +## How to use other images as services You are not limited to have only database services. You can add as many services you need to `.gitlab-ci.yml` or manually modify `config.toml`. Any image found at [Docker Hub][hub] can be used as a service. -### Define image and services from `.gitlab-ci.yml` +## Define image and services from `.gitlab-ci.yml` You can simply define an image that will be used for all jobs and a list of services that you want to use during build time. ```yaml image: ruby:2.2 + services: - postgres:9.3 + before_script: - bundle install @@ -117,24 +123,20 @@ test:2.2: - bundle exec rake spec ``` -### Define image and services in `config.toml` +## Define image and services in `config.toml` Look for the `[runners.docker]` section: ``` -... - [runners.docker] image = "ruby:2.1" services = ["mysql:latest", "postgres:latest"] - -... ``` The image and services defined this way will be added to all builds run by that runner. -### Accessing the services +## Accessing the services Let's say that you need a Wordpress instance to test some API integration with your application. @@ -143,12 +145,8 @@ You can then use for example the [tutum/wordpress][] image in your `.gitlab-ci.yml`: ```yaml -... - services: - tutum/wordpress:latest - -... ``` When the build is run, `tutum/wordpress` will be started and you will have @@ -160,7 +158,7 @@ rules: 1. Everything after `:` is stripped 2. Backslash (`/`) is replaced with double underscores (`__`) -### Configuring services +## Configuring services Many services accept environment variables which allow you to easily change database names or set account names depending on the environment. @@ -171,46 +169,20 @@ service containers. For all possible configuration variables check the documentation of each image provided in their corresponding Docker hub page. -*Note: All variables will be passed to all service containers. It's not designed - to distinguish which variable should go where.* +*Note: All variables will be passed to all services containers. It's not +designed to distinguish which variable should go where.* -#### PostgreSQL service example +### PostgreSQL service example -To configure the database name for [postgres][postgres-hub] service, you need -to set the `POSTGRES_DB` variable: +See the specific documentation for +[using PostgreSQL as a service](../services/postgres.md). -```yaml -... +### MySQL service example -services: -- postgres +See the specific documentation for +[using MySQL as a service](../services/mysql.md). -variables: - POSTGRES_DB: gitlab - -... -``` - -For a real example visit . - -#### MySQL service example - -To use the [mysql][mysql-hub] service with an empty password during the time of -build, you need to set the `MYSQL_ALLOW_EMPTY_PASSWORD` variable: - -```yaml -... - -services: -- mysql - -variables: - MYSQL_ALLOW_EMPTY_PASSWORD: "yes" - -... -``` - -### How Docker integration works +## How Docker integration works Below is a high level overview of the steps performed by docker during build time. @@ -226,7 +198,7 @@ time. 1. Check exit status of build script. 1. Remove build container and all created service containers. -### How to debug a build locally +## How to debug a build locally *Note: The following commands are run without root privileges. You should be able to run docker with your regular user account.* From 57d71520bdc2ba79ba8182802cd944d4fb42a192 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 22:30:40 +0100 Subject: [PATCH 465/908] Make tooltip less confusing --- app/controllers/projects/application_controller.rb | 2 +- app/services/files/base_service.rb | 2 +- app/views/projects/blob/_actions.html.haml | 6 +++--- app/views/projects/tree/_tree_header.html.haml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index d3f926b62b..eea41dbeeb 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -21,7 +21,7 @@ class Projects::ApplicationController < ApplicationController unless @repository.branch_names.include?(@ref) redirect_to( namespace_project_tree_path(@project.namespace, @project, @ref), - notice: "This action is not allowed unless you are on top of a branch" + notice: "This action is not allowed unless you are on a branch" ) end end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 309abcd6d0..9a67b16094 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -53,7 +53,7 @@ module Files unless project.empty_repo? unless repository.branch_names.include?(@current_branch) - raise_error("You can only create or edit files when you are on top of a branch") + raise_error("You can only create or edit files when you are on a branch") end if @current_branch != @target_branch diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index 41d8765623..b1df8d1993 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -21,6 +21,6 @@ %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Delete - elsif !on_top_of_branch? .btn-group{ role: "group" } - %button.btn.btn-default.disabled.has_tooltip{title: "You can only edit files when you are on top of a branch.", data: {container: 'body'}} Edit - %button.btn.btn-default.disabled.has_tooltip{title: "You can only replace files when you are on top of a branch.", data: {container: 'body'}} Replace - %button.btn.btn-remove.disabled.has_tooltip{title: "You can only delete files when you are on top of a branch.", data: {container: 'body'}} Delete + %button.btn.btn-default.disabled.has_tooltip{title: "You can only edit files when you are on a branch.", data: {container: 'body'}} Edit + %button.btn.btn-default.disabled.has_tooltip{title: "You can only replace files when you are on a branch.", data: {container: 'body'}} Replace + %button.btn.btn-remove.disabled.has_tooltip{title: "You can only delete files when you are on a branch.", data: {container: 'body'}} Delete diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 4767f824ac..2baa5ecd97 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -32,5 +32,5 @@ New directory - elsif !on_top_of_branch? %li - %span.btn.add-to-tree.disabled.has_tooltip{href: '#', title: "You can only add files when you are on top of a branch.", data: {container: 'body'}} + %span.btn.add-to-tree.disabled.has_tooltip{title: "You can only add files when you are on a branch.", data: {container: 'body'}} = icon('plus') From 0f89e6905db6fe821332013e8ddb2707079e0ba4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 22:31:40 +0100 Subject: [PATCH 466/908] Fix spec --- features/project/source/browse_files.feature | 6 ------ features/steps/project/source/browse_files.rb | 4 ---- 2 files changed, 10 deletions(-) diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 37f99b3761..439787f264 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -110,12 +110,6 @@ Feature: Project Source Browse Files Given I visit a binary file in the repo Then I cannot see the edit button - Scenario: If I don't have edit permission the edit link is disabled - Given public project "Community" - And I visit project "Community" source page - And I click on ".gitignore" file in repo - Then The edit button is disabled - @javascript Scenario: I can edit and commit file Given I click on ".gitignore" file in repo diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 2792174cc9..f2b9576426 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -53,10 +53,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(page).not_to have_link 'edit' end - step 'The edit button is disabled' do - expect(page).to have_css '.disabled', text: 'Edit' - end - step 'I can edit code' do set_new_content expect(evaluate_script('blob.editor.getValue()')).to eq new_gitignore_content From 1464a69c76cf492ad8b9674e24260e917dc7d2ef Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 22:37:07 +0100 Subject: [PATCH 467/908] Tweak text of documentation --- .gitignore | 1 - doc/workflow/merge_when_build_succeeds.md | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 881b3fb81a..f5b6427ca0 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,6 @@ public/assets/ public/uploads.* public/uploads/ shared/artifacts/ -TODO rails_best_practices_output.html /tags tmp/ diff --git a/doc/workflow/merge_when_build_succeeds.md b/doc/workflow/merge_when_build_succeeds.md index 3c055650c4..75e1fdff2b 100644 --- a/doc/workflow/merge_when_build_succeeds.md +++ b/doc/workflow/merge_when_build_succeeds.md @@ -1,12 +1,12 @@ # Merge When Build Succeeds -When reviewing a merge request that looks ready to merge but still has one or more CI builds running, you can set it to be merged automatically when the build succeeds. This way, you don't have to wait for the build to finish and remember to merge the merge request then. +When reviewing a merge request that looks ready to merge but still has one or more CI builds running, you can set it to be merged automatically when all builds succeed. This way, you don't have to wait for the builds to finish and remember to merge the request manually. ![Enable](merge_when_build_succeeds/enable.png) -When you hit the "Merge When Build Succeeds" button, the status of the Merge Request will be updated to represent the impending merge. If you cannot wait for the build to succeed and want to build immediately, this option is available in the dropdown menu on the right of the main button. +When you hit the "Merge When Build Succeeds" button, the status of the merge request will be updated to represent the impending merge. If you cannot wait for the build to succeed and want to merge immediately, this option is available in the dropdown menu on the right of the main button. -Both team developers and the author of the merge request have the option to cancel the automatic merge when they find a reason it shouldn't be merged after all. +Both team developers and the author of the merge request have the option to cancel the automatic merge if they find a reason why it shouldn't be merged after all. ![Status](merge_when_build_succeeds/status.png) From 31b4960eca490994bd192f53c4b3f5283fe962d5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 22:39:26 +0100 Subject: [PATCH 468/908] Fix specs --- app/views/projects/issues/_issue.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 7d9fc8d2c8..f9cf4910df 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -25,7 +25,7 @@ %li = link_to issue_path(issue) + "#notes", class: "issue-no-comments" do = icon('comments') - = notes_count + = note_count .issue-info #{issue.to_reference} · From 1e7156ed701945349bef484d35c7f53f2ee4474b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 8 Dec 2015 23:49:05 +0200 Subject: [PATCH 469/908] Use the name of the linked containers instead of localhost https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1917#note_2833978 --- doc/ci/services/mysql.md | 5 ++++- doc/ci/services/postgres.md | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md index 44bc613e6f..c66d77122b 100644 --- a/doc/ci/services/mysql.md +++ b/doc/ci/services/mysql.md @@ -24,12 +24,15 @@ variables: And then configure your application to use the database, for example: ```yaml -Host: localhost +Host: mysql User: root Password: mysql_strong_password Database: el_duderino ``` +If you are wondering why we used `mysql` for the `Host`, read more at +[How is service linked to the build](../docker/using_docker_images.md#how-is-service-linked-to-the-build). + You can also use any other docker image available on [Docker Hub][hub-mysql]. For example, to use MySQL 5.5 the service becomes `mysql:5.5`. diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md index db5be070ee..17d21dbda1 100644 --- a/doc/ci/services/postgres.md +++ b/doc/ci/services/postgres.md @@ -24,12 +24,15 @@ variables: And then configure your application to use the database, for example: ```yaml -Host: localhost +Host: postgres User: runner Password: Database: nice_marmot ``` +If you are wondering why we used `postgres` for the `Host`, read more at +[How is service linked to the build](../docker/using_docker_images.md#how-is-service-linked-to-the-build). + You can also use any other docker image available on [Docker Hub][hub-pg]. For example, to use PostgreSQL 9.3 the service becomes `postgres:9.3`. From 53fbb0ecda36be783a46413f59551f13090ea9df Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 8 Dec 2015 16:53:48 -0500 Subject: [PATCH 470/908] Improve styling for mixed list styles Tasks in an ordered list will now also show their numbers. Closes #2488 Related to #3921 --- app/assets/stylesheets/framework/lists.scss | 10 ++++++++-- app/assets/stylesheets/pages/notes.scss | 10 +++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 927641216e..f770e535b3 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -88,8 +88,14 @@ ul.bordered-list { } } -li.task-list-item { - list-style-type: none; +ul.task-list { + li.task-list-item { + list-style-type: none; + } + + ul:not(.task-list) { + padding-left: 1.3em; + } } ul.content-list { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 1980fe0d45..4dff87abaa 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -109,13 +109,9 @@ ul.notes { } } - // Reduce left padding of first task list ul element - ul.task-list:first-child { - padding-left: 10px; - - // sub-tasks should be padded normally - ul { - padding-left: 20px; + ul.task-list { + ul:not(.task-list) { + padding-left: 1.3em; } } From 23522a9b34c30d61e950820a197b6bd353ba1914 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 8 Dec 2015 23:28:28 +0100 Subject: [PATCH 471/908] Fix specs --- spec/features/issues_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 0af5e6fc1a..3d6d87e764 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -293,10 +293,10 @@ describe 'Issues', feature: true do end def first_issue - page.all('ul.issues-list li').first.text + page.all('ul.issues-list > li').first.text end def last_issue - page.all('ul.issues-list li').last.text + page.all('ul.issues-list > li').last.text end end From 56f25c3e615752817446d5aedb7d66d25d815641 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 7 Dec 2015 13:37:01 +0100 Subject: [PATCH 472/908] Improve text indication visibility on snippets --- CHANGELOG | 1 + app/helpers/visibility_level_helper.rb | 26 +++--- .../application_settings/_form.html.haml | 4 +- app/views/shared/snippets/_header.html.haml | 2 +- spec/helpers/visibility_level_helper_spec.rb | 89 ++++++++----------- 5 files changed, 55 insertions(+), 67 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 67f70e676c..c291d4b6de 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,7 @@ v 8.3.0 (unreleased) - Fix bug when simultaneously accepting multiple MRs results in MRs that are of "merged" status, but not merged to the target branch - Add languages page to graphs - Block LDAP user when they are no longer found in the LDAP server + - Improve wording on project visibility levels (Zeger-Jan van de Weg) v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 72c65030f9..2e69ce923a 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -12,22 +12,22 @@ module VisibilityLevelHelper # Return the description for the +level+ argument. # - # +level+ One of the Gitlab::VisibilityLevel constants - # +form_model+ Either a model object (Project, Snippet, etc.) or the name of - # a Project or Snippet class. + # +level+ One of the Gitlab::VisibilityLevel constants + # +form_model+ Either a model object (Project, Snippet, etc.) or the name of + # a Project or Snippet class. def visibility_level_description(level, form_model) - case form_model.is_a?(String) ? form_model : form_model.class.name - when 'PersonalSnippet', 'ProjectSnippet', 'Snippet' - snippet_visibility_level_description(level) - when 'Project' + case form_model + when Project project_visibility_level_description(level) + when Snippet + snippet_visibility_level_description(level, form_model) end end def project_visibility_level_description(level) case level when Gitlab::VisibilityLevel::PRIVATE - "Project access must be granted explicitly for each user." + "Project access must be granted explicitly to each user." when Gitlab::VisibilityLevel::INTERNAL "The project can be cloned by any logged in user." when Gitlab::VisibilityLevel::PUBLIC @@ -35,12 +35,16 @@ module VisibilityLevelHelper end end - def snippet_visibility_level_description(level) + def snippet_visibility_level_description(level, snippet = nil) case level when Gitlab::VisibilityLevel::PRIVATE - "The snippet is visible only for me." + if snippet.is_a? ProjectSnippet + "The snippet is visible only to project members." + else + "The snippet is visible only to me." + end when Gitlab::VisibilityLevel::INTERNAL - "The snippet is visible for any logged in user." + "The snippet is visible to any logged in user." when Gitlab::VisibilityLevel::PUBLIC "The snippet can be accessed without any authentication." end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index ddaf0e0e8f..6c35536694 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -14,11 +14,11 @@ .form-group.project-visibility-level-holder = f.label :default_project_visibility, class: 'control-label col-sm-2' .col-sm-10 - = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: 'Project') + = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project) .form-group.project-visibility-level-holder = f.label :default_snippet_visibility, class: 'control-label col-sm-2' .col-sm-10 - = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: 'Snippet') + = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: PersonalSnippet) .form-group = f.label :restricted_visibility_levels, class: 'control-label col-sm-2' .col-sm-10 diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 89c1d7122b..eb0fd21c2d 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -1,6 +1,6 @@ .issuable-details .page-title - .snippet-box.has_tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level), data: { container: 'body' }} + .snippet-box.has_tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: 'body' }} = visibility_level_icon(@snippet.visibility_level, fw: false) = visibility_level_label(@snippet.visibility_level) Snippet ##{@snippet.id} diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb index c4f7693329..aafc24397a 100644 --- a/spec/helpers/visibility_level_helper_spec.rb +++ b/spec/helpers/visibility_level_helper_spec.rb @@ -7,69 +7,52 @@ describe VisibilityLevelHelper do init_haml_helpers end - let(:project) { create(:project) } + let(:project) { build(:project) } + let(:personal_snippet) { build(:personal_snippet) } + let(:project_snippet) { build(:project_snippet) } describe 'visibility_level_description' do - shared_examples 'a visibility level description' do - let(:desc) do - visibility_level_description(Gitlab::VisibilityLevel::PRIVATE, - form_model) - end - - let(:expected_class) do - class_name = case form_model.class.name - when 'String' - form_model - else - form_model.class.name - end - - class_name.match(/(project|snippet)$/i)[0] - end - - it 'should refer to the correct class' do - expect(desc).to match(/#{expected_class}/i) + context 'used with a Project' do + it 'delegates projects to #project_visibility_level_description' do + expect(visibility_level_description(Gitlab::VisibilityLevel::PRIVATE, project)) + .to match /project/i end end - context 'form_model argument is a String' do - context 'model object is a personal snippet' do - it_behaves_like 'a visibility level description' do - let(:form_model) { 'PersonalSnippet' } - end - end - - context 'model object is a project snippet' do - it_behaves_like 'a visibility level description' do - let(:form_model) { 'ProjectSnippet' } - end - end - - context 'model object is a project' do - it_behaves_like 'a visibility level description' do - let(:form_model) { 'Project' } - end + context 'called with a Snippet' do + it 'delegates snippets to #snippet_visibility_level_description' do + expect(visibility_level_description(Gitlab::VisibilityLevel::INTERNAL, project_snippet)) + .to match /snippet/i end end + end - context 'form_model argument is a model object' do - context 'model object is a personal snippet' do - it_behaves_like 'a visibility level description' do - let(:form_model) { create(:personal_snippet) } - end - end + describe "#project_visibility_level_description" do + it "describes private projects" do + expect(project_visibility_level_description(Gitlab::VisibilityLevel::PRIVATE)) + .to eq "Project access must be granted explicitly to each user." + end - context 'model object is a project snippet' do - it_behaves_like 'a visibility level description' do - let(:form_model) { create(:project_snippet, project: project) } - end - end + it "describes public projects" do + expect(project_visibility_level_description(Gitlab::VisibilityLevel::PUBLIC)) + .to eq "The project can be cloned without any authentication." + end + end - context 'model object is a project' do - it_behaves_like 'a visibility level description' do - let(:form_model) { project } - end - end + describe "#snippet_visibility_level_description" do + it 'describes visibility only for me' do + expect(snippet_visibility_level_description(Gitlab::VisibilityLevel::PRIVATE, personal_snippet)) + .to eq "The snippet is visible only to me." + end + + it 'describes visibility for project members' do + expect(snippet_visibility_level_description(Gitlab::VisibilityLevel::PRIVATE, project_snippet)) + .to eq "The snippet is visible only to project members." + end + + it 'defaults to personal snippet' do + expect(snippet_visibility_level_description(Gitlab::VisibilityLevel::PRIVATE)) + .to eq "The snippet is visible only to me." end end From 430aa23a5b82b34912dc86c323087fabd2849b7c Mon Sep 17 00:00:00 2001 From: Eirik Lygre Date: Wed, 9 Dec 2015 00:20:51 +0100 Subject: [PATCH 473/908] Add spinach tests for the clone protocol selection. --- features/explore/projects.feature | 21 +++++++++++++++++++-- features/steps/explore/projects.rb | 8 ++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/features/explore/projects.feature b/features/explore/projects.feature index 5d3870827f..63b0ac65c4 100644 --- a/features/explore/projects.feature +++ b/features/explore/projects.feature @@ -31,8 +31,17 @@ Feature: Explore Projects Then I should see empty public project details And I should see empty public project details with http clone info - Scenario: I visit an empty public project page as user + Scenario: I visit an empty public project page as user with no ssh-keys Given I sign in as a user + And I have no ssh keys + And public empty project "Empty Public Project" + When I visit empty project page + Then I should see empty public project details + And I should see empty public project details with http clone info + + Scenario: I visit an empty public project page as user with an ssh-key + Given I sign in as a user + And I have ssh key "ssh-rsa Work" And public empty project "Empty Public Project" When I visit empty project page Then I should see empty public project details @@ -57,8 +66,16 @@ Feature: Explore Projects Then I should see project "Community" home page And I should see an http link to the repository - Scenario: I visit public project page as user + Scenario: I visit public project page as user with no ssh-keys Given I sign in as a user + And I have no ssh keys + When I visit project "Community" page + Then I should see project "Community" home page + And I should see an http link to the repository + + Scenario: I visit public project page as user with an ssh-key + Given I sign in as a user + And I have ssh key "ssh-rsa Work" When I visit project "Community" page Then I should see project "Community" home page And I should see an ssh link to the repository diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb index 8b498e7b4a..82cb9e2406 100644 --- a/features/steps/explore/projects.rb +++ b/features/steps/explore/projects.rb @@ -144,4 +144,12 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps def public_merge_request @public_merge_request ||= MergeRequest.find_by!(title: 'Bug fix for public project') end + + step 'I have ssh key "ssh-rsa Work"' do + create(:key, user: @user, title: "ssh-rsa Work", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+L3TbFegm3k8QjejSwemk4HhlRh+DuN679Pc5ckqE/MPhVtE/+kZQDYCTB284GiT2aIoGzmZ8ee9TkaoejAsBwlA+Wz2Q3vhz65X6sMgalRwpdJx8kSEUYV8ZPV3MZvPo8KdNg993o4jL6G36GDW4BPIyO6FPZhfsawdf6liVD0Xo5kibIK7B9VoE178cdLQtLpS2YolRwf5yy6XR6hbbBGQR+6xrGOdP16eGZDb1CE2bMvvJijjloFqPscGktWOqW+nfh5txwFfBzlfARDTBsS8WZtg3Yoj1kn33kPsWRlgHfNutFRAIynDuDdQzQq8tTtVwm+Yi75RfcPHW8y3P Work") + end + + step 'I have no ssh keys' do + Key.delete_all + end end From 9dd26749634648e2cd27b223eea5d291c0a78f37 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 9 Dec 2015 01:43:57 +0100 Subject: [PATCH 474/908] Fix only 20 group members showing on project member page Signed-off-by: Dmitriy Zaporozhets --- app/controllers/projects/project_members_controller.rb | 2 +- app/views/projects/project_members/_group_members.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 07eb94e4f4..8364fc293b 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -23,7 +23,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController @group_members = @group_members.where(user_id: users) end - @group_members = @group_members.order('access_level DESC').limit(20) + @group_members = @group_members.order('access_level DESC') end @project_member = @project.project_members.new diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index d2810f9707..1c2458fa14 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -10,7 +10,7 @@ = icon('pencil-square-o') Manage group members %ul.content-list - - members.each do |member| + - members.limit(20).each do |member| = render 'groups/group_members/group_member', member: member, show_controls: false - if members.count > 20 %li From 94dc9ef9e1a85b8a4506358479a549dc3a1306b6 Mon Sep 17 00:00:00 2001 From: Eirik Lygre Date: Wed, 9 Dec 2015 02:02:53 +0100 Subject: [PATCH 475/908] Move ssh-key steps into SharedUser. Change references in "Explore Projects" feature. Apply ssh-key requirement to "Project Create" feature. --- features/explore/projects.feature | 4 ++-- features/project/create.feature | 2 ++ features/steps/explore/projects.rb | 9 +-------- features/steps/project/create.rb | 1 + features/steps/shared/user.rb | 8 ++++++++ 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/features/explore/projects.feature b/features/explore/projects.feature index 63b0ac65c4..629859e960 100644 --- a/features/explore/projects.feature +++ b/features/explore/projects.feature @@ -41,7 +41,7 @@ Feature: Explore Projects Scenario: I visit an empty public project page as user with an ssh-key Given I sign in as a user - And I have ssh key "ssh-rsa Work" + And I have an ssh key And public empty project "Empty Public Project" When I visit empty project page Then I should see empty public project details @@ -75,7 +75,7 @@ Feature: Explore Projects Scenario: I visit public project page as user with an ssh-key Given I sign in as a user - And I have ssh key "ssh-rsa Work" + And I have an ssh key When I visit project "Community" page Then I should see project "Community" home page And I should see an ssh link to the repository diff --git a/features/project/create.feature b/features/project/create.feature index e9dc4fe6b3..a86079143e 100644 --- a/features/project/create.feature +++ b/features/project/create.feature @@ -7,6 +7,7 @@ Feature: Project Create Scenario: User create a project Given I sign in as a user When I visit new project page + And I have an ssh key And fill project form with valid data Then I should see project page And I should see empty project instuctions @@ -14,6 +15,7 @@ Feature: Project Create @javascript Scenario: Empty project instructions Given I sign in as a user + And I have an ssh key When I visit new project page And fill project form with valid data Then I see empty project instuctions diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb index 82cb9e2406..f819dec219 100644 --- a/features/steps/explore/projects.rb +++ b/features/steps/explore/projects.rb @@ -2,6 +2,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps include SharedAuthentication include SharedPaths include SharedProject + include SharedUser step 'I should see project "Empty Public Project"' do expect(page).to have_content "Empty Public Project" @@ -144,12 +145,4 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps def public_merge_request @public_merge_request ||= MergeRequest.find_by!(title: 'Bug fix for public project') end - - step 'I have ssh key "ssh-rsa Work"' do - create(:key, user: @user, title: "ssh-rsa Work", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+L3TbFegm3k8QjejSwemk4HhlRh+DuN679Pc5ckqE/MPhVtE/+kZQDYCTB284GiT2aIoGzmZ8ee9TkaoejAsBwlA+Wz2Q3vhz65X6sMgalRwpdJx8kSEUYV8ZPV3MZvPo8KdNg993o4jL6G36GDW4BPIyO6FPZhfsawdf6liVD0Xo5kibIK7B9VoE178cdLQtLpS2YolRwf5yy6XR6hbbBGQR+6xrGOdP16eGZDb1CE2bMvvJijjloFqPscGktWOqW+nfh5txwFfBzlfARDTBsS8WZtg3Yoj1kn33kPsWRlgHfNutFRAIynDuDdQzQq8tTtVwm+Yi75RfcPHW8y3P Work") - end - - step 'I have no ssh keys' do - Key.delete_all - end end diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb index 0d39e1997b..f90218f379 100644 --- a/features/steps/project/create.rb +++ b/features/steps/project/create.rb @@ -1,6 +1,7 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps include SharedAuthentication include SharedPaths + include SharedUser step 'fill project form with valid data' do fill_in 'project_path', with: 'Empty' diff --git a/features/steps/shared/user.rb b/features/steps/shared/user.rb index 250cc5b94f..33c146f6db 100644 --- a/features/steps/shared/user.rb +++ b/features/steps/shared/user.rb @@ -18,4 +18,12 @@ module SharedUser def user_exists(name, options = {}) User.find_by(name: name) || create(:user, { name: name, admin: false }.merge(options)) end + + step 'I have an ssh key' do + create(:key, user: @user, title: "An ssh-key", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+L3TbFegm3k8QjejSwemk4HhlRh+DuN679Pc5ckqE/MPhVtE/+kZQDYCTB284GiT2aIoGzmZ8ee9TkaoejAsBwlA+Wz2Q3vhz65X6sMgalRwpdJx8kSEUYV8ZPV3MZvPo8KdNg993o4jL6G36GDW4BPIyO6FPZhfsawdf6liVD0Xo5kibIK7B9VoE178cdLQtLpS2YolRwf5yy6XR6hbbBGQR+6xrGOdP16eGZDb1CE2bMvvJijjloFqPscGktWOqW+nfh5txwFfBzlfARDTBsS8WZtg3Yoj1kn33kPsWRlgHfNutFRAIynDuDdQzQq8tTtVwm+Yi75RfcPHW8y3P Work") + end + + step 'I have no ssh keys' do + Key.delete_all + end end From 1d250b48922ef88b14ad8ce2af157e2f2d256773 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 9 Dec 2015 02:50:46 +0100 Subject: [PATCH 476/908] Move Network page from separate tab to sub tab of Commits Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/views/layouts/nav/_project.html.haml | 16 ++++++++-------- app/views/projects/commits/_head.html.haml | 6 ++++++ app/views/projects/network/_head.html.haml | 2 +- app/views/projects/network/show.html.haml | 3 ++- features/project/active_tab.feature | 11 ++++++----- features/steps/project/active_tab.rb | 4 ++++ features/steps/shared/project_tab.rb | 4 ---- 8 files changed, 28 insertions(+), 19 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c291d4b6de..747afc82aa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -25,6 +25,7 @@ v 8.3.0 (unreleased) - Add languages page to graphs - Block LDAP user when they are no longer found in the LDAP server - Improve wording on project visibility levels (Zeger-Jan van de Weg) + - Make Network page as sub tab of Commits v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 87a7707b09..2fcba7bd67 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -32,7 +32,7 @@ Files - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories tags branches releases)) do + = nav_link(controller: %w(commit commits compare repositories tags branches releases network)) do = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do = icon('history fw') %span @@ -46,13 +46,6 @@ Builds %span.count.builds_counter= @project.ci_builds.running_or_pending.count(:all) - - if project_nav_tab? :network - = nav_link(controller: %w(network)) do - = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do - = icon('code-fork fw') - %span - Network - - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do @@ -118,3 +111,10 @@ = icon('cogs fw') %span Settings + + -# Global shortcut to network page for compatibility + - if project_nav_tab? :network + %li.hidden + = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do + Network + diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index f11a41cfd7..2f604c82f1 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -3,6 +3,12 @@ = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do Commits %span.badge= number_with_delimiter(@repository.commit_count) + + - if project_nav_tab? :network + = nav_link(controller: %w(network)) do + = link_to namespace_project_network_path(@project.namespace, @project, current_ref), class: 'shortcuts-network' do + Network + = nav_link(controller: :compare) do = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do Compare diff --git a/app/views/projects/network/_head.html.haml b/app/views/projects/network/_head.html.haml index 9e0e0dc6bb..28a617538b 100644 --- a/app/views/projects/network/_head.html.haml +++ b/app/views/projects/network/_head.html.haml @@ -1,4 +1,4 @@ -.gray-content-block.top-block.append-bottom-default +.gray-content-block.append-bottom-default .tree-ref-holder = render partial: 'shared/ref_switcher', locals: {destination: 'graph'} diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 16005161df..8065663ca2 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,5 +1,6 @@ - page_title "Network", @ref -= header_title project_title(@project, "Network", namespace_project_network_path(@project.namespace, @project, current_ref)) += render "projects/commits/header_title" += render "projects/commits/head" = render "head" .project-network .controls diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index 8661ea98c2..2fd097d100 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -20,11 +20,6 @@ Feature: Project Active Tab Then the active main tab should be Commits And no other main tabs should be active - Scenario: On Project Network - Given I visit my project's network page - Then the active main tab should be Network - And no other main tabs should be active - Scenario: On Project Issues Given I visit my project's issues page Then the active main tab should be Issues @@ -83,6 +78,12 @@ Feature: Project Active Tab And no other sub tabs should be active And the active main tab should be Commits + Scenario: On Project Commits/Network + Given I visit my project's network page + Then the active sub tab should be Network + And no other sub tabs should be active + And the active main tab should be Commits + Scenario: On Project Commits/Compare Given I visit my project's commits page And I click the "Compare" tab diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 9e96fa5ba4..94a5dd744d 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -67,6 +67,10 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps ensure_active_sub_tab('Commits') end + step 'the active sub tab should be Network' do + ensure_active_sub_tab('Network') + end + step 'the active sub tab should be Compare' do ensure_active_sub_tab('Compare') end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index 33ff7084e3..3bad28dc28 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -16,10 +16,6 @@ module SharedProjectTab ensure_active_main_tab('Commits') end - step 'the active main tab should be Network' do - ensure_active_main_tab('Network') - end - step 'the active main tab should be Graphs' do ensure_active_main_tab('Graphs') end From 9dbc768db8304004c08957710d423c0b2b54510a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 8 Dec 2015 21:00:01 -0800 Subject: [PATCH 477/908] Update annotations --- app/models/lfs_object.rb | 12 ++++++++++++ app/models/lfs_objects_project.rb | 11 +++++++++++ app/models/note.rb | 1 + app/models/project.rb | 1 + app/models/user.rb | 1 + spec/factories/lfs_objects.rb | 12 ++++++++++++ spec/factories/lfs_objects_projects.rb | 11 +++++++++++ spec/factories/notes.rb | 1 + spec/factories/projects.rb | 1 + spec/models/note_spec.rb | 1 + spec/models/project_spec.rb | 1 + spec/models/user_spec.rb | 1 + 12 files changed, 54 insertions(+) diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 18657c3e1c..86b1b7e2f9 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -1,3 +1,15 @@ +# == Schema Information +# +# Table name: lfs_objects +# +# id :integer not null, primary key +# oid :string(255) not null +# size :integer not null +# created_at :datetime +# updated_at :datetime +# file :string(255) +# + class LfsObject < ActiveRecord::Base has_many :lfs_objects_projects, dependent: :destroy has_many :projects, through: :lfs_objects_projects diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb index 0fd5f089db..890736bfc8 100644 --- a/app/models/lfs_objects_project.rb +++ b/app/models/lfs_objects_project.rb @@ -1,3 +1,14 @@ +# == Schema Information +# +# Table name: lfs_objects_projects +# +# id :integer not null, primary key +# lfs_object_id :integer not null +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# + class LfsObjectsProject < ActiveRecord::Base belongs_to :project belongs_to :lfs_object diff --git a/app/models/note.rb b/app/models/note.rb index 8d433c57ce..98c29ddc4c 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -16,6 +16,7 @@ # system :boolean default(FALSE), not null # st_diff :text # updated_by_id :integer +# is_award :boolean default(FALSE), not null # require 'carrierwave/orm/activerecord' diff --git a/app/models/project.rb b/app/models/project.rb index af034a6692..cb965ce1b9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -28,6 +28,7 @@ # import_type :string(255) # import_source :string(255) # commit_count :integer default(0) +# import_error :text # require 'carrierwave/orm/activerecord' diff --git a/app/models/user.rb b/app/models/user.rb index cfed797e72..7155dd2bea 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -56,6 +56,7 @@ # project_view :integer default(0) # consumed_timestep :integer # layout :integer default(0) +# hide_project_limit :boolean default(FALSE) # require 'carrierwave/orm/activerecord' diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb index 7fb2d77ca3..2da107ba24 100644 --- a/spec/factories/lfs_objects.rb +++ b/spec/factories/lfs_objects.rb @@ -1,3 +1,15 @@ +# == Schema Information +# +# Table name: lfs_objects +# +# id :integer not null, primary key +# oid :string(255) not null +# size :integer not null +# created_at :datetime +# updated_at :datetime +# file :string(255) +# + # Read about factories at https://github.com/thoughtbot/factory_girl FactoryGirl.define do diff --git a/spec/factories/lfs_objects_projects.rb b/spec/factories/lfs_objects_projects.rb index 93de6607df..3772236a77 100644 --- a/spec/factories/lfs_objects_projects.rb +++ b/spec/factories/lfs_objects_projects.rb @@ -1,3 +1,14 @@ +# == Schema Information +# +# Table name: lfs_objects_projects +# +# id :integer not null, primary key +# lfs_object_id :integer not null +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# + # Read about factories at https://github.com/thoughtbot/factory_girl FactoryGirl.define do diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 9d777ddfcc..35a20adeef 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -16,6 +16,7 @@ # system :boolean default(FALSE), not null # st_diff :text # updated_by_id :integer +# is_award :boolean default(FALSE), not null # require_relative '../support/repo_helpers' diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 1d500a11ad..112213377f 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -28,6 +28,7 @@ # import_type :string(255) # import_source :string(255) # commit_count :integer default(0) +# import_error :text # FactoryGirl.define do diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index f347f53755..e7e8887baf 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -16,6 +16,7 @@ # system :boolean default(FALSE), not null # st_diff :text # updated_by_id :integer +# is_award :boolean default(FALSE), not null # require 'spec_helper' diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 06a02c13bf..dc703d8095 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -28,6 +28,7 @@ # import_type :string(255) # import_source :string(255) # commit_count :integer default(0) +# import_error :text # require 'spec_helper' diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a0f78d3b33..1aad37fa02 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -56,6 +56,7 @@ # project_view :integer default(0) # consumed_timestep :integer # layout :integer default(0) +# hide_project_limit :boolean default(FALSE) # require 'spec_helper' From 7b0ac5b6b49675790491dcf8764ebfb17b93012c Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 8 Dec 2015 21:31:22 -0800 Subject: [PATCH 478/908] Remove default_branch from project API creation since an empty repository has no branches to start. Closes #3937 --- doc/api/projects.md | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/api/projects.md b/doc/api/projects.md index 42919a312a..43a50a9a81 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -398,7 +398,6 @@ Parameters: - `user_id` (required) - user_id of owner - `name` (required) - new project name - `description` (optional) - short project description -- `default_branch` (optional) - 'master' by default - `issues_enabled` (optional) - `merge_requests_enabled` (optional) - `builds_enabled` (optional) From dd980b46f1833ba72207846c66995e517b63b50d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 9 Dec 2015 09:08:53 +0100 Subject: [PATCH 479/908] Use up and down --- db/migrate/20140122112253_create_merge_request_diffs.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/db/migrate/20140122112253_create_merge_request_diffs.rb b/db/migrate/20140122112253_create_merge_request_diffs.rb index 47bee269f9..f34e30925d 100644 --- a/db/migrate/20140122112253_create_merge_request_diffs.rb +++ b/db/migrate/20140122112253_create_merge_request_diffs.rb @@ -1,5 +1,5 @@ class CreateMergeRequestDiffs < ActiveRecord::Migration - def change + def up create_table :merge_request_diffs do |t| t.string :state, null: false, default: 'collected' t.text :st_commits, null: true @@ -14,4 +14,8 @@ class CreateMergeRequestDiffs < ActiveRecord::Migration change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647 end end + + def down + drop_table :merge_request_diffs + end end From 28351806aa1f89f584d6905365fd34f5f0e8bbc7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 9 Dec 2015 09:59:19 +0100 Subject: [PATCH 480/908] Give merge request widget the vars it desires --- app/controllers/projects/merge_requests_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 3240c77e99..530f3d3dcb 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -7,6 +7,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] before_action :define_show_vars, only: [:show, :diffs, :commits, :builds] + before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds] # Allow read any merge_request @@ -301,6 +302,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def define_widget_vars + @ci_commit = @merge_request.ci_commit + end + def invalid_mr # Render special view for MR with removed source or target branch render 'invalid' From 903151141da36ed9b8c2333a9b66f5c611e4efb6 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 9 Dec 2015 09:59:39 +0100 Subject: [PATCH 481/908] Tweak specs --- .../merge_requests/merge_when_build_succeeds_spec.rb | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index ee2fb25e9e..a674124aab 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -43,20 +43,14 @@ feature 'Merge When Build Succeeds', feature: true, js: true do context 'When it is enabled' do let(:merge_request) do - create(:merge_request_with_diffs, source_project: project, author: user, - merge_user: user, title: "MepMep", merge_when_build_succeeds: true) + create(:merge_request_with_diffs, :simple, source_project: project, author: user, + merge_user: user, title: "MepMep", merge_when_build_succeeds: true) end let!(:ci_commit) { create(:ci_commit, gl_project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } let!(:ci_build) { create(:ci_build, commit: ci_commit) } before do - merge_request.source_project.team << [user, :master] - merge_request.source_branch = "feature" - merge_request.target_branch = "master" - merge_request.save! - - login_as user visit_merge_request(merge_request) end From 20b55981bc09c82dcbc523123cbd81a5bf2ae593 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 9 Dec 2015 10:50:28 +0100 Subject: [PATCH 482/908] Tag feature specs --- features/project/commits/branches.feature | 1 + features/project/commits/comments.feature | 1 + features/project/commits/commits.feature | 1 + features/project/commits/diff_comments.feature | 1 + features/project/commits/tags.feature | 1 + features/project/commits/user_lookup.feature | 1 + features/project/issues/award_emoji.feature | 1 + features/project/issues/filter_labels.feature | 1 + features/project/issues/issues.feature | 2 +- features/project/issues/labels.feature | 1 + features/project/issues/milestones.feature | 1 + features/project/merge_requests.feature | 1 + features/project/merge_requests/accept.feature | 1 + 13 files changed, 13 insertions(+), 1 deletion(-) diff --git a/features/project/commits/branches.feature b/features/project/commits/branches.feature index 65d8e48b9b..5103ca1294 100644 --- a/features/project/commits/branches.feature +++ b/features/project/commits/branches.feature @@ -1,3 +1,4 @@ +@project_commits Feature: Project Commits Branches Background: Given I sign in as a user diff --git a/features/project/commits/comments.feature b/features/project/commits/comments.feature index 320f008abb..fafb54b183 100644 --- a/features/project/commits/comments.feature +++ b/features/project/commits/comments.feature @@ -1,3 +1,4 @@ +@project_commits Feature: Project Commits Comments Background: Given I sign in as a user diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index e4beeb59ad..21367fd76a 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -1,3 +1,4 @@ +@project_commits Feature: Project Commits Background: Given I sign in as a user diff --git a/features/project/commits/diff_comments.feature b/features/project/commits/diff_comments.feature index d6e0c84537..2bde4c8a99 100644 --- a/features/project/commits/diff_comments.feature +++ b/features/project/commits/diff_comments.feature @@ -1,3 +1,4 @@ +@project_commits Feature: Project Commits Diff Comments Background: Given I sign in as a user diff --git a/features/project/commits/tags.feature b/features/project/commits/tags.feature index 56ee091acc..a4be39b2d4 100644 --- a/features/project/commits/tags.feature +++ b/features/project/commits/tags.feature @@ -1,3 +1,4 @@ +@project_commits Feature: Project Commits Tags Background: Given I sign in as a user diff --git a/features/project/commits/user_lookup.feature b/features/project/commits/user_lookup.feature index db51d4a6cf..c18f4e070f 100644 --- a/features/project/commits/user_lookup.feature +++ b/features/project/commits/user_lookup.feature @@ -1,3 +1,4 @@ +@project_commits Feature: Project Commits User Lookup Background: Given I sign in as a user diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature index 2609f129d0..0ce99e855c 100644 --- a/features/project/issues/award_emoji.feature +++ b/features/project/issues/award_emoji.feature @@ -1,3 +1,4 @@ +@project_issues Feature: Award Emoji Background: Given I sign in as a user diff --git a/features/project/issues/filter_labels.feature b/features/project/issues/filter_labels.feature index e316f51986..e07f8053fb 100644 --- a/features/project/issues/filter_labels.feature +++ b/features/project/issues/filter_labels.feature @@ -1,3 +1,4 @@ +@project_issues Feature: Project Issues Filter Labels Background: Given I sign in as a user diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 28cc43ef71..f08b30e0b8 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -1,3 +1,4 @@ +@project_issues Feature: Project Issues Background: Given I sign in as a user @@ -196,4 +197,3 @@ Feature: Project Issues And I should not see labels field And I submit new issue "500 error on profile" Then I should see issue "500 error on profile" - diff --git a/features/project/issues/labels.feature b/features/project/issues/labels.feature index 039a7d83cb..45de57f18e 100644 --- a/features/project/issues/labels.feature +++ b/features/project/issues/labels.feature @@ -1,3 +1,4 @@ +@project_issues Feature: Project Issues Labels Background: Given I sign in as a user diff --git a/features/project/issues/milestones.feature b/features/project/issues/milestones.feature index c1a20e9b48..1af05b3c32 100644 --- a/features/project/issues/milestones.feature +++ b/features/project/issues/milestones.feature @@ -1,3 +1,4 @@ +@project_issues Feature: Project Issues Milestones Background: Given I sign in as a user diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 6cd081c868..35ace94888 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -1,3 +1,4 @@ +@project_merge_requests Feature: Project Merge Requests Background: Given I sign in as a user diff --git a/features/project/merge_requests/accept.feature b/features/project/merge_requests/accept.feature index 9bc2b7c8ec..d5e4f2b0bd 100644 --- a/features/project/merge_requests/accept.feature +++ b/features/project/merge_requests/accept.feature @@ -1,3 +1,4 @@ +@project_merge_requests Feature: Project Merge Requests Acceptance Background: Given There is an open Merge Request From 9ebdee09673d79b57c07c7332ee87791229b6f72 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 9 Dec 2015 10:50:38 +0100 Subject: [PATCH 483/908] Split up feature specs more --- .gitlab-ci.yml | 11 +++++++++-- lib/tasks/spinach.rake | 26 +++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e8290fb36b..527ccb08ab 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -39,9 +39,16 @@ spec:other: - ruby - mysql -spinach:project: +spinach:project:half: script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half + tags: + - ruby + - mysql + +spinach:project:rest: + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest tags: - ruby - mysql diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake index d5a96fd38f..3acfc6e207 100644 --- a/lib/tasks/spinach.rake +++ b/lib/tasks/spinach.rake @@ -1,11 +1,31 @@ Rake::Task["spinach"].clear if Rake::Task.task_defined?('spinach') namespace :spinach do + namespace :project do + desc "GitLab | Spinach | Run project commits, issues and merge requests spinach features" + task :half do + cmds = [ + %W(rake gitlab:setup), + %W(spinach --tags @project_commits,@project_issues,@project_merge_requests), + ] + run_commands(cmds) + end + + desc "GitLab | Spinach | Run remaining project spinach features" + task :rest do + cmds = [ + %W(rake gitlab:setup), + %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests), + ] + run_commands(cmds) + end + end + desc "GitLab | Spinach | Run project spinach features" task :project do cmds = [ %W(rake gitlab:setup), - %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets,~@commits), + %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets), ] run_commands(cmds) end @@ -14,7 +34,7 @@ namespace :spinach do task :other do cmds = [ %W(rake gitlab:setup), - %W(spinach --tags @admin,@dashboard,@profile,@public,@snippets,@commits), + %W(spinach --tags @admin,@dashboard,@profile,@public,@snippets), ] run_commands(cmds) end @@ -33,4 +53,4 @@ def run_commands(cmds) cmds.each do |cmd| system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!") end -end \ No newline at end of file +end From 1e8d703a854b372b12a2ad41b5e54d2abc028f74 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 9 Dec 2015 10:50:51 +0100 Subject: [PATCH 484/908] Tag model specs --- spec/models/broadcast_message_spec.rb | 2 +- spec/models/build_spec.rb | 2 +- spec/models/ci/commit_spec.rb | 2 +- spec/models/ci/project_services/hip_chat_message_spec.rb | 2 +- spec/models/ci/project_services/hip_chat_service_spec.rb | 2 +- spec/models/ci/project_services/mail_service_spec.rb | 2 +- spec/models/ci/project_services/slack_message_spec.rb | 2 +- spec/models/ci/project_services/slack_service_spec.rb | 2 +- spec/models/ci/project_spec.rb | 2 +- spec/models/ci/runner_project_spec.rb | 2 +- spec/models/ci/runner_spec.rb | 2 +- spec/models/ci/service_spec.rb | 2 +- spec/models/ci/trigger_spec.rb | 2 +- spec/models/ci/variable_spec.rb | 2 +- spec/models/ci/web_hook_spec.rb | 2 +- spec/models/commit_range_spec.rb | 2 +- spec/models/commit_spec.rb | 2 +- spec/models/commit_status_spec.rb | 2 +- spec/models/concerns/case_sensitivity_spec.rb | 2 +- spec/models/deploy_key_spec.rb | 2 +- spec/models/deploy_keys_project_spec.rb | 2 +- spec/models/event_spec.rb | 2 +- spec/models/external_issue_spec.rb | 2 +- spec/models/external_wiki_service_spec.rb | 2 +- spec/models/generic_commit_status_spec.rb | 2 +- spec/models/global_milestone_spec.rb | 2 +- spec/models/group_spec.rb | 2 +- spec/models/hooks/project_hook_spec.rb | 2 +- spec/models/hooks/service_hook_spec.rb | 2 +- spec/models/hooks/system_hook_spec.rb | 2 +- spec/models/hooks/web_hook_spec.rb | 2 +- spec/models/issue_spec.rb | 2 +- spec/models/key_spec.rb | 2 +- spec/models/label_link_spec.rb | 2 +- spec/models/label_spec.rb | 2 +- spec/models/member_spec.rb | 2 +- spec/models/members/group_member_spec.rb | 2 +- spec/models/members/project_member_spec.rb | 2 +- spec/models/merge_request_spec.rb | 2 +- spec/models/milestone_spec.rb | 2 +- spec/models/namespace_spec.rb | 2 +- spec/models/note_spec.rb | 2 +- spec/models/project_security_spec.rb | 2 +- spec/models/project_services/buildkite_service_spec.rb | 2 +- spec/models/project_services/drone_ci_service_spec.rb | 2 +- spec/models/project_services/flowdock_service_spec.rb | 2 +- spec/models/project_services/gemnasium_service_spec.rb | 2 +- spec/models/project_services/gitlab_ci_service_spec.rb | 2 +- .../project_services/gitlab_issue_tracker_service_spec.rb | 2 +- spec/models/project_services/hipchat_service_spec.rb | 2 +- spec/models/project_services/irker_service_spec.rb | 2 +- spec/models/project_services/jira_service_spec.rb | 2 +- spec/models/project_services/pushover_service_spec.rb | 2 +- .../models/project_services/slack_service/issue_message_spec.rb | 2 +- .../models/project_services/slack_service/merge_message_spec.rb | 2 +- spec/models/project_services/slack_service/note_message_spec.rb | 2 +- spec/models/project_services/slack_service/push_message_spec.rb | 2 +- spec/models/project_services/slack_service_spec.rb | 2 +- spec/models/project_snippet_spec.rb | 2 +- spec/models/project_spec.rb | 2 +- spec/models/project_team_spec.rb | 2 +- spec/models/project_wiki_spec.rb | 2 +- spec/models/protected_branch_spec.rb | 2 +- spec/models/repository_spec.rb | 2 +- spec/models/service_spec.rb | 2 +- spec/models/snippet_spec.rb | 2 +- spec/models/user_spec.rb | 2 +- spec/models/wiki_page_spec.rb | 2 +- 68 files changed, 68 insertions(+), 68 deletions(-) diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index 2b325f44f6..e4cac10511 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -15,7 +15,7 @@ require 'spec_helper' -describe BroadcastMessage do +describe BroadcastMessage, models: true do subject { create(:broadcast_message) } it { is_expected.to be_valid } diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 839b4c6b16..70c831b7cb 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -25,7 +25,7 @@ require 'spec_helper' -describe Ci::Build do +describe Ci::Build, models: true do let(:project) { FactoryGirl.create :ci_project } let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index a13f6458ca..89813cdf7f 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -18,7 +18,7 @@ require 'spec_helper' -describe Ci::Commit do +describe Ci::Commit, models: true do let(:project) { FactoryGirl.create :ci_project } let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } diff --git a/spec/models/ci/project_services/hip_chat_message_spec.rb b/spec/models/ci/project_services/hip_chat_message_spec.rb index e23d6ae2c2..7d54b6cf84 100644 --- a/spec/models/ci/project_services/hip_chat_message_spec.rb +++ b/spec/models/ci/project_services/hip_chat_message_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::HipChatMessage do +describe Ci::HipChatMessage, models: true do subject { Ci::HipChatMessage.new(build) } let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) } diff --git a/spec/models/ci/project_services/hip_chat_service_spec.rb b/spec/models/ci/project_services/hip_chat_service_spec.rb index d9ccc855ed..714f1e17e0 100644 --- a/spec/models/ci/project_services/hip_chat_service_spec.rb +++ b/spec/models/ci/project_services/hip_chat_service_spec.rb @@ -15,7 +15,7 @@ require 'spec_helper' -describe Ci::HipChatService do +describe Ci::HipChatService, models: true do describe "Validations" do diff --git a/spec/models/ci/project_services/mail_service_spec.rb b/spec/models/ci/project_services/mail_service_spec.rb index c03be3ef75..638d9a4a62 100644 --- a/spec/models/ci/project_services/mail_service_spec.rb +++ b/spec/models/ci/project_services/mail_service_spec.rb @@ -14,7 +14,7 @@ require 'spec_helper' -describe Ci::MailService do +describe Ci::MailService, models: true do describe "Associations" do it { is_expected.to belong_to :project } end diff --git a/spec/models/ci/project_services/slack_message_spec.rb b/spec/models/ci/project_services/slack_message_spec.rb index 8adda6c86c..226032b4cd 100644 --- a/spec/models/ci/project_services/slack_message_spec.rb +++ b/spec/models/ci/project_services/slack_message_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::SlackMessage do +describe Ci::SlackMessage, models: true do subject { Ci::SlackMessage.new(commit) } let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) } diff --git a/spec/models/ci/project_services/slack_service_spec.rb b/spec/models/ci/project_services/slack_service_spec.rb index 1ac7dfe568..e7d7d5d6f4 100644 --- a/spec/models/ci/project_services/slack_service_spec.rb +++ b/spec/models/ci/project_services/slack_service_spec.rb @@ -14,7 +14,7 @@ require 'spec_helper' -describe Ci::SlackService do +describe Ci::SlackService, models: true do describe "Associations" do it { is_expected.to belong_to :project } end diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb index ac7e38bbcb..346471aa9b 100644 --- a/spec/models/ci/project_spec.rb +++ b/spec/models/ci/project_spec.rb @@ -27,7 +27,7 @@ require 'spec_helper' -describe Ci::Project do +describe Ci::Project, models: true do let(:project) { FactoryGirl.create :ci_project } let(:gl_project) { project.gl_project } subject { project } diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb index 37682c6ea0..da8491357a 100644 --- a/spec/models/ci/runner_project_spec.rb +++ b/spec/models/ci/runner_project_spec.rb @@ -11,6 +11,6 @@ require 'spec_helper' -describe Ci::RunnerProject do +describe Ci::RunnerProject, models: true do pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 9a1233b909..6ebb5e8686 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -19,7 +19,7 @@ require 'spec_helper' -describe Ci::Runner do +describe Ci::Runner, models: true do describe '#display_name' do it 'should return the description if it has a value' do runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448') diff --git a/spec/models/ci/service_spec.rb b/spec/models/ci/service_spec.rb index 36cda988eb..34e3af7f81 100644 --- a/spec/models/ci/service_spec.rb +++ b/spec/models/ci/service_spec.rb @@ -14,7 +14,7 @@ require 'spec_helper' -describe Ci::Service do +describe Ci::Service, models: true do describe "Associations" do it { is_expected.to belong_to :project } diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb index b8aa3c1e77..61eb3c0829 100644 --- a/spec/models/ci/trigger_spec.rb +++ b/spec/models/ci/trigger_spec.rb @@ -12,7 +12,7 @@ require 'spec_helper' -describe Ci::Trigger do +describe Ci::Trigger, models: true do let(:project) { FactoryGirl.create :ci_project } describe 'before_validation' do diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index a515f5881f..31b56953a1 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -13,7 +13,7 @@ require 'spec_helper' -describe Ci::Variable do +describe Ci::Variable, models: true do subject { Ci::Variable.new } let(:secret_value) { 'secret' } diff --git a/spec/models/ci/web_hook_spec.rb b/spec/models/ci/web_hook_spec.rb index 2865482a21..1a4edec9d4 100644 --- a/spec/models/ci/web_hook_spec.rb +++ b/spec/models/ci/web_hook_spec.rb @@ -11,7 +11,7 @@ require 'spec_helper' -describe Ci::WebHook do +describe Ci::WebHook, models: true do describe "Associations" do it { is_expected.to belong_to :project } end diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index 3c1009a2eb..9307d97e21 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe CommitRange do +describe CommitRange, models: true do describe 'modules' do subject { described_class } diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 38a3dc1f4a..ecf37b40c5 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Commit do +describe Commit, models: true do let(:project) { create(:project) } let(:commit) { project.commit } diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index dca0715eed..5e311ead28 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -33,7 +33,7 @@ require 'spec_helper' -describe CommitStatus do +describe CommitStatus, models: true do let(:commit) { FactoryGirl.create :ci_commit } let(:commit_status) { FactoryGirl.create :commit_status, commit: commit } diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb index f7ed30f819..25b3f4e50d 100644 --- a/spec/models/concerns/case_sensitivity_spec.rb +++ b/spec/models/concerns/case_sensitivity_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe CaseSensitivity do +describe CaseSensitivity, models: true do describe '.iwhere' do let(:connection) { ActiveRecord::Base.connection } let(:model) { Class.new { include CaseSensitivity } } diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb index 9572993245..64ba778afe 100644 --- a/spec/models/deploy_key_spec.rb +++ b/spec/models/deploy_key_spec.rb @@ -15,7 +15,7 @@ require 'spec_helper' -describe DeployKey do +describe DeployKey, models: true do let(:project) { create(:project) } let(:deploy_key) { create(:deploy_key, projects: [project]) } diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb index 0eb22599d1..8aedbfb863 100644 --- a/spec/models/deploy_keys_project_spec.rb +++ b/spec/models/deploy_keys_project_spec.rb @@ -11,7 +11,7 @@ require 'spec_helper' -describe DeployKeysProject do +describe DeployKeysProject, models: true do describe "Associations" do it { is_expected.to belong_to(:deploy_key) } it { is_expected.to belong_to(:project) } diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index ae53f7a536..071582b028 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -16,7 +16,7 @@ require 'spec_helper' -describe Event do +describe Event, models: true do describe "Associations" do it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:target) } diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb index 7744610db7..6ec6b9037a 100644 --- a/spec/models/external_issue_spec.rb +++ b/spec/models/external_issue_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe ExternalIssue do +describe ExternalIssue, models: true do let(:project) { double('project', to_reference: 'namespace1/project1') } let(:issue) { described_class.new('EXT-1234', project) } diff --git a/spec/models/external_wiki_service_spec.rb b/spec/models/external_wiki_service_spec.rb index 4bd5b0be61..b198aa7752 100644 --- a/spec/models/external_wiki_service_spec.rb +++ b/spec/models/external_wiki_service_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' -describe ExternalWikiService do +describe ExternalWikiService, models: true do include ExternalWikiHelper describe "Associations" do it { should belong_to :project } diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index c86314c454..d61c1c96bd 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -33,7 +33,7 @@ require 'spec_helper' -describe GenericCommitStatus do +describe GenericCommitStatus, models: true do let(:commit) { FactoryGirl.create :ci_commit } let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, commit: commit } diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb index 6eeff30b20..ba03e6aabd 100644 --- a/spec/models/global_milestone_spec.rb +++ b/spec/models/global_milestone_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe GlobalMilestone do +describe GlobalMilestone, models: true do let(:user) { create(:user) } let(:user2) { create(:user) } let(:group) { create(:group) } diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 6f166b5ab7..646f767e6f 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -16,7 +16,7 @@ require 'spec_helper' -describe Group do +describe Group, models: true do let!(:group) { create(:group) } describe 'associations' do diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb index a2dc66fce3..645ee0b929 100644 --- a/spec/models/hooks/project_hook_spec.rb +++ b/spec/models/hooks/project_hook_spec.rb @@ -18,7 +18,7 @@ require 'spec_helper' -describe ProjectHook do +describe ProjectHook, models: true do describe '.push_hooks' do it 'should return hooks for push events only' do hook = create(:project_hook, push_events: true) diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb index 16641c1212..1455661485 100644 --- a/spec/models/hooks/service_hook_spec.rb +++ b/spec/models/hooks/service_hook_spec.rb @@ -18,7 +18,7 @@ require "spec_helper" -describe ServiceHook do +describe ServiceHook, models: true do describe "Associations" do it { is_expected.to belong_to :service } end diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 02d2cc2c77..138b87a9a0 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -18,7 +18,7 @@ require "spec_helper" -describe SystemHook do +describe SystemHook, models: true do describe "execute" do before(:each) do @system_hook = create(:system_hook) diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 35042788c6..2d90b0793c 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -18,7 +18,7 @@ require 'spec_helper' -describe ProjectHook do +describe ProjectHook, models: true do describe "Associations" do it { is_expected.to belong_to :project } end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index c9aa1b063c..52271c7c8c 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' -describe Issue do +describe Issue, models: true do describe "Associations" do it { is_expected.to belong_to(:milestone) } end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 2f819f60cb..d7fe01976d 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -15,7 +15,7 @@ require 'spec_helper' -describe Key do +describe Key, models: true do describe "Associations" do it { is_expected.to belong_to(:user) } end diff --git a/spec/models/label_link_spec.rb b/spec/models/label_link_spec.rb index 8c24082658..dc7510b1de 100644 --- a/spec/models/label_link_spec.rb +++ b/spec/models/label_link_spec.rb @@ -12,7 +12,7 @@ require 'spec_helper' -describe LabelLink do +describe LabelLink, models: true do let(:label) { create(:label_link) } it { expect(label).to be_valid } diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 511ee8cbd9..696fbf7e0a 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -13,7 +13,7 @@ require 'spec_helper' -describe Label do +describe Label, models: true do let(:label) { create(:label) } describe 'associations' do diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 57f840c1e9..2aedca20df 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -19,7 +19,7 @@ require 'spec_helper' -describe Member do +describe Member, models: true do describe "Associations" do it { is_expected.to belong_to(:user) } end diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index 652026729b..5424c9b9cb 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -19,7 +19,7 @@ require 'spec_helper' -describe GroupMember do +describe GroupMember, models: true do context 'notification' do describe "#after_create" do it "should send email to user" do diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index ee912bf12a..9f26d9eb5c 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -19,7 +19,7 @@ require 'spec_helper' -describe ProjectMember do +describe ProjectMember, models: true do describe :import_team do before do @abilities = Six.new diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 567c911425..6a456ca5ea 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -25,7 +25,7 @@ require 'spec_helper' -describe MergeRequest do +describe MergeRequest, models: true do subject { create(:merge_request) } describe 'associations' do diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 77c5862732..30a71987d8 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -15,7 +15,7 @@ require 'spec_helper' -describe Milestone do +describe Milestone, models: true do describe "Associations" do it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:issues) } diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index a98b9cb732..4fa2d2bc4d 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -16,7 +16,7 @@ require 'spec_helper' -describe Namespace do +describe Namespace, models: true do let!(:namespace) { create(:namespace) } it { is_expected.to have_many :projects } diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index e7e8887baf..cd3c868ecc 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -21,7 +21,7 @@ require 'spec_helper' -describe Note do +describe Note, models: true do describe 'associations' do it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:noteable) } diff --git a/spec/models/project_security_spec.rb b/spec/models/project_security_spec.rb index f600a240c4..3643ad1b05 100644 --- a/spec/models/project_security_spec.rb +++ b/spec/models/project_security_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Project do +describe Project, models: true do describe :authorization do before do @p1 = create(:project) diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb index 230807ea67..88cd624877 100644 --- a/spec/models/project_services/buildkite_service_spec.rb +++ b/spec/models/project_services/buildkite_service_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' -describe BuildkiteService do +describe BuildkiteService, models: true do describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb index e9967f5fe0..a2cf68a9e3 100644 --- a/spec/models/project_services/drone_ci_service_spec.rb +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' -describe DroneCiService do +describe DroneCiService, models: true do describe 'associations' do it { is_expected.to belong_to(:project) } it { is_expected.to have_one(:service_hook) } diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb index 16296607a9..ff7fbcaa00 100644 --- a/spec/models/project_services/flowdock_service_spec.rb +++ b/spec/models/project_services/flowdock_service_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' -describe FlowdockService do +describe FlowdockService, models: true do describe "Associations" do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb index 9e15647231..ecb3ccb167 100644 --- a/spec/models/project_services/gemnasium_service_spec.rb +++ b/spec/models/project_services/gemnasium_service_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' -describe GemnasiumService do +describe GemnasiumService, models: true do describe "Associations" do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb index b9006b693b..835bf36405 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' -describe GitlabCiService do +describe GitlabCiService, models: true do describe 'associations' do it { is_expected.to belong_to(:project) } it { is_expected.to have_one(:service_hook) } diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb index e34ca09bff..3518dbd172 100644 --- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb +++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' -describe GitlabIssueTrackerService do +describe GitlabIssueTrackerService, models: true do describe "Associations" do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index f67d7b3098..c96ab54814 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' -describe HipchatService do +describe HipchatService, models: true do describe "Associations" do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb index 7d483a44c5..b783b1a576 100644 --- a/spec/models/project_services/irker_service_spec.rb +++ b/spec/models/project_services/irker_service_spec.rb @@ -22,7 +22,7 @@ require 'spec_helper' require 'socket' require 'json' -describe IrkerService do +describe IrkerService, models: true do describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 576f5fc79e..7d91ebe9ce 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' -describe JiraService do +describe JiraService, models: true do describe "Associations" do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb index ac10ffbd39..96039f9491 100644 --- a/spec/models/project_services/pushover_service_spec.rb +++ b/spec/models/project_services/pushover_service_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' -describe PushoverService do +describe PushoverService, models: true do describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb index b78d92f23a..97e6f03e30 100644 --- a/spec/models/project_services/slack_service/issue_message_spec.rb +++ b/spec/models/project_services/slack_service/issue_message_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe SlackService::IssueMessage do +describe SlackService::IssueMessage, models: true do subject { SlackService::IssueMessage.new(args) } let(:args) do diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb index 581c50d6c8..dae8bd9092 100644 --- a/spec/models/project_services/slack_service/merge_message_spec.rb +++ b/spec/models/project_services/slack_service/merge_message_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe SlackService::MergeMessage do +describe SlackService::MergeMessage, models: true do subject { SlackService::MergeMessage.new(args) } let(:args) do diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb index 21fb575480..ebf8837570 100644 --- a/spec/models/project_services/slack_service/note_message_spec.rb +++ b/spec/models/project_services/slack_service/note_message_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe SlackService::NoteMessage do +describe SlackService::NoteMessage, models: true do let(:color) { '#345' } before do diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/slack_service/push_message_spec.rb index ddc290820d..cda9ee670b 100644 --- a/spec/models/project_services/slack_service/push_message_spec.rb +++ b/spec/models/project_services/slack_service/push_message_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe SlackService::PushMessage do +describe SlackService::PushMessage, models: true do subject { SlackService::PushMessage.new(args) } let(:args) do diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 97b60e19e4..a9e0afad90 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' -describe SlackService do +describe SlackService, models: true do describe "Associations" do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb index 3e8f106d27..cc92eb0bd9 100644 --- a/spec/models/project_snippet_spec.rb +++ b/spec/models/project_snippet_spec.rb @@ -17,7 +17,7 @@ require 'spec_helper' -describe ProjectSnippet do +describe ProjectSnippet, models: true do describe "Associations" do it { is_expected.to belong_to(:project) } end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index dc703d8095..6ddb0e2b8f 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -33,7 +33,7 @@ require 'spec_helper' -describe Project do +describe Project, models: true do describe 'associations' do it { is_expected.to belong_to(:group) } it { is_expected.to belong_to(:namespace) } diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 26e8fdae47..5cd5ae327b 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe ProjectTeam do +describe ProjectTeam, models: true do let(:master) { create(:user) } let(:reporter) { create(:user) } let(:guest) { create(:user) } diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 3b88914444..876b927eae 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe ProjectWiki do +describe ProjectWiki, models: true do let(:project) { create(:empty_project) } let(:repository) { project.repository } let(:user) { project.owner } diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb index 1e6937b536..7e956cf677 100644 --- a/spec/models/protected_branch_spec.rb +++ b/spec/models/protected_branch_spec.rb @@ -12,7 +12,7 @@ require 'spec_helper' -describe ProtectedBranch do +describe ProtectedBranch, models: true do describe 'Associations' do it { is_expected.to belong_to(:project) } end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index fa261e64c3..e6c415da26 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Repository do +describe Repository, models: true do include RepoHelpers let(:repository) { create(:project).repository } diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 692e5fda3b..0ca82365b9 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' -describe Service do +describe Service, models: true do describe "Associations" do it { is_expected.to belong_to :project } diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 8158183867..eb2dbbdc5a 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -17,7 +17,7 @@ require 'spec_helper' -describe Snippet do +describe Snippet, models: true do describe 'modules' do subject { described_class } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 1aad37fa02..daa9d1087b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -61,7 +61,7 @@ require 'spec_helper' -describe User do +describe User, models: true do include Gitlab::CurrentSettings describe 'modules' do diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index d7802d1734..c1b03838aa 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe WikiPage do +describe WikiPage, models: true do let(:project) { create(:empty_project) } let(:user) { project.owner } let(:wiki) { ProjectWiki.new(project, user) } From ae0b9017315b38dd42b4a1c9b6fb1daa78fee28a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 9 Dec 2015 10:51:01 +0100 Subject: [PATCH 485/908] Split up specs more --- .gitlab-ci.yml | 9 ++++++++- lib/tasks/spec.rake | 11 ++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 527ccb08ab..943e40d530 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,6 +24,13 @@ spec:api: - ruby - mysql +spec:models: + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models + tags: + - ruby + - mysql + spec:benchmark: script: - RAILS_ENV=test bundle exec rake spec:benchmark @@ -96,7 +103,7 @@ flay: - mysql bundler:audit: - script: + script: - "bundle exec bundle-audit update" - "bundle exec bundle-audit check" tags: diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake index 365ff2defd..049e17e6c1 100644 --- a/lib/tasks/spec.rake +++ b/lib/tasks/spec.rake @@ -19,6 +19,15 @@ namespace :spec do run_commands(cmds) end + desc 'GitLab | Rspec | Run model specs' + task :models do + cmds = [ + %W(rake gitlab:setup), + %W(rspec spec --tag @models) + ] + run_commands(cmds) + end + desc 'GitLab | Rspec | Run benchmark specs' task :benchmark do cmds = [ @@ -32,7 +41,7 @@ namespace :spec do task :other do cmds = [ %W(rake gitlab:setup), - %W(rspec spec --tag ~@api --tag ~@feature --tag ~@benchmark) + %W(rspec spec --tag ~@api,~@feature,~@models,~@benchmark) ] run_commands(cmds) end From 7a5e77c0a0591ff5d121f93deced27b798ddbee3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 9 Dec 2015 11:09:25 +0100 Subject: [PATCH 486/908] Fix rspec tag syntax --- lib/tasks/spec.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake index 049e17e6c1..343e4b6352 100644 --- a/lib/tasks/spec.rake +++ b/lib/tasks/spec.rake @@ -41,7 +41,7 @@ namespace :spec do task :other do cmds = [ %W(rake gitlab:setup), - %W(rspec spec --tag ~@api,~@feature,~@models,~@benchmark) + %W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@benchmark) ] run_commands(cmds) end From 905090dbe9d140ef950995c63204a864a161dbdb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 9 Dec 2015 11:18:21 +0100 Subject: [PATCH 487/908] Remove RedCloth and no longer allow bundle-audit to fail --- .gitlab-ci.yml | 3 +-- Gemfile | 1 - Gemfile.lock | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e8290fb36b..c6d1e622bd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -89,10 +89,9 @@ flay: - mysql bundler:audit: - script: + script: - "bundle exec bundle-audit update" - "bundle exec bundle-audit check" tags: - ruby - mysql - allow_failure: true diff --git a/Gemfile b/Gemfile index 91ad4b6fb4..473770f53e 100644 --- a/Gemfile +++ b/Gemfile @@ -93,7 +93,6 @@ gem 'html-pipeline', '~> 1.11.0' gem 'task_list', '~> 1.0.2', require: 'task_list/railtie' gem 'github-markup', '~> 1.3.1' gem 'redcarpet', '~> 3.3.3' -gem 'RedCloth', '~> 4.2.9' gem 'rdoc', '~>3.6' gem 'org-ruby', '~> 0.9.12' gem 'creole', '~> 0.5.0' diff --git a/Gemfile.lock b/Gemfile.lock index 3979418ed4..37ba22b7ce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,6 @@ GEM remote: https://rubygems.org/ specs: CFPropertyList (2.3.2) - RedCloth (4.2.9) ace-rails-ap (2.0.1) actionmailer (4.2.4) actionpack (= 4.2.4) @@ -826,7 +825,6 @@ PLATFORMS ruby DEPENDENCIES - RedCloth (~> 4.2.9) ace-rails-ap (~> 2.0.1) activerecord-deprecated_finders (~> 1.0.3) activerecord-session_store (~> 0.1.0) From 13d6bab177d2a703d19b2fc5f28271ed083273c3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 9 Dec 2015 11:55:36 +0100 Subject: [PATCH 488/908] Tag lib specs --- spec/lib/ci/ansi2html_spec.rb | 2 +- spec/lib/ci/charts_spec.rb | 2 +- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 2 +- spec/lib/disable_email_interceptor_spec.rb | 2 +- spec/lib/extracts_path_spec.rb | 2 +- spec/lib/file_size_validator_spec.rb | 2 +- spec/lib/git_ref_validator_spec.rb | 2 +- spec/lib/gitlab/asciidoc_spec.rb | 2 +- spec/lib/gitlab/auth_spec.rb | 2 +- spec/lib/gitlab/backend/grack_auth_spec.rb | 2 +- spec/lib/gitlab/backend/shell_spec.rb | 4 ++-- spec/lib/gitlab/bitbucket_import/client_spec.rb | 2 +- spec/lib/gitlab/bitbucket_import/project_creator_spec.rb | 2 +- spec/lib/gitlab/closing_issue_extractor_spec.rb | 2 +- spec/lib/gitlab/color_schemes_spec.rb | 2 +- spec/lib/gitlab/database_spec.rb | 2 +- spec/lib/gitlab/diff/file_spec.rb | 2 +- spec/lib/gitlab/diff/parser_spec.rb | 2 +- spec/lib/gitlab/email/attachment_uploader_spec.rb | 2 +- spec/lib/gitlab/email/receiver_spec.rb | 2 +- spec/lib/gitlab/email/reply_parser_spec.rb | 2 +- spec/lib/gitlab/git_access_spec.rb | 2 +- spec/lib/gitlab/git_access_wiki_spec.rb | 2 +- spec/lib/gitlab/github_import/client_spec.rb | 2 +- spec/lib/gitlab/github_import/project_creator_spec.rb | 2 +- spec/lib/gitlab/gitlab_import/client_spec.rb | 2 +- spec/lib/gitlab/gitlab_import/project_creator_spec.rb | 2 +- spec/lib/gitlab/gitorious_import/project_creator_spec.rb | 2 +- spec/lib/gitlab/google_code_import/client_spec.rb | 2 +- spec/lib/gitlab/google_code_import/importer_spec.rb | 2 +- spec/lib/gitlab/google_code_import/project_creator_spec.rb | 2 +- spec/lib/gitlab/incoming_email_spec.rb | 2 +- spec/lib/gitlab/inline_diff_spec.rb | 2 +- spec/lib/gitlab/key_fingerprint_spec.rb | 2 +- spec/lib/gitlab/ldap/access_spec.rb | 2 +- spec/lib/gitlab/ldap/adapter_spec.rb | 2 +- spec/lib/gitlab/ldap/auth_hash_spec.rb | 2 +- spec/lib/gitlab/ldap/authentication_spec.rb | 2 +- spec/lib/gitlab/ldap/config_spec.rb | 2 +- spec/lib/gitlab/ldap/user_spec.rb | 2 +- spec/lib/gitlab/lfs/lfs_router_spec.rb | 2 +- spec/lib/gitlab/markdown/autolink_filter_spec.rb | 2 +- .../lib/gitlab/markdown/commit_range_reference_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/commit_reference_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/cross_project_reference_spec.rb | 2 +- spec/lib/gitlab/markdown/emoji_filter_spec.rb | 2 +- .../gitlab/markdown/external_issue_reference_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/external_link_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/issue_reference_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/label_reference_filter_spec.rb | 2 +- .../gitlab/markdown/merge_request_reference_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/redactor_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/relative_link_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/sanitization_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/task_list_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/upload_link_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/user_reference_filter_spec.rb | 2 +- spec/lib/gitlab/markup_helper_spec.rb | 2 +- spec/lib/gitlab/note_data_builder_spec.rb | 2 +- spec/lib/gitlab/o_auth/auth_hash_spec.rb | 2 +- spec/lib/gitlab/o_auth/user_spec.rb | 2 +- spec/lib/gitlab/popen_spec.rb | 2 +- spec/lib/gitlab/project_search_results_spec.rb | 2 +- spec/lib/gitlab/push_data_builder_spec.rb | 2 +- spec/lib/gitlab/reference_extractor_spec.rb | 2 +- spec/lib/gitlab/regex_spec.rb | 2 +- spec/lib/gitlab/sherlock/collection_spec.rb | 2 +- spec/lib/gitlab/sherlock/file_sample_spec.rb | 2 +- spec/lib/gitlab/sherlock/line_profiler_spec.rb | 2 +- spec/lib/gitlab/sherlock/line_sample_spec.rb | 2 +- spec/lib/gitlab/sherlock/location_spec.rb | 2 +- spec/lib/gitlab/sherlock/middleware_spec.rb | 2 +- spec/lib/gitlab/sherlock/query_spec.rb | 2 +- spec/lib/gitlab/sherlock/transaction_spec.rb | 2 +- spec/lib/gitlab/sql/union_spec.rb | 2 +- spec/lib/gitlab/themes_spec.rb | 2 +- spec/lib/gitlab/upgrader_spec.rb | 2 +- spec/lib/gitlab/uploads_transfer_spec.rb | 2 +- spec/lib/gitlab/url_builder_spec.rb | 2 +- spec/lib/gitlab/version_info_spec.rb | 2 +- spec/lib/repository_cache_spec.rb | 2 +- 85 files changed, 86 insertions(+), 86 deletions(-) diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb index 75c023bbc4..3a2b568f4c 100644 --- a/spec/lib/ci/ansi2html_spec.rb +++ b/spec/lib/ci/ansi2html_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::Ansi2html do +describe Ci::Ansi2html, lib: true do subject { Ci::Ansi2html } it "prints non-ansi as-is" do diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb index 83e2ad220b..50a77308cd 100644 --- a/spec/lib/ci/charts_spec.rb +++ b/spec/lib/ci/charts_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Charts" do +describe Ci::Charts, lib: true do context "build_times" do before do diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 6f287719ba..ce4a5244bd 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' module Ci - describe GitlabCiYamlProcessor do + describe GitlabCiYamlProcessor, lib: true do let(:path) { 'path' } describe "#builds_for_ref" do diff --git a/spec/lib/disable_email_interceptor_spec.rb b/spec/lib/disable_email_interceptor_spec.rb index a9624e9a2b..c2a7b20b84 100644 --- a/spec/lib/disable_email_interceptor_spec.rb +++ b/spec/lib/disable_email_interceptor_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe DisableEmailInterceptor do +describe DisableEmailInterceptor, lib: true do before do ActionMailer::Base.register_interceptor(DisableEmailInterceptor) end diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index 48bc60eed1..f38fadda9b 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe ExtractsPath do +describe ExtractsPath, lib: true do include ExtractsPath include RepoHelpers include Gitlab::Application.routes.url_helpers diff --git a/spec/lib/file_size_validator_spec.rb b/spec/lib/file_size_validator_spec.rb index 12ccc051c7..fda6f9a6c8 100644 --- a/spec/lib/file_size_validator_spec.rb +++ b/spec/lib/file_size_validator_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Gitlab::FileSizeValidatorSpec' do +describe FileSizeValidator, lib: true do let(:validator) { FileSizeValidator.new(options) } let(:attachment) { AttachmentUploader.new } let(:note) { create(:note) } diff --git a/spec/lib/git_ref_validator_spec.rb b/spec/lib/git_ref_validator_spec.rb index 4633b6f393..dc57e94f19 100644 --- a/spec/lib/git_ref_validator_spec.rb +++ b/spec/lib/git_ref_validator_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::GitRefValidator do +describe Gitlab::GitRefValidator, lib: true do it { expect(Gitlab::GitRefValidator.validate('feature/new')).to be_truthy } it { expect(Gitlab::GitRefValidator.validate('implement_@all')).to be_truthy } it { expect(Gitlab::GitRefValidator.validate('my_new_feature')).to be_truthy } diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 03e36fd355..7d985fb6f1 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' require 'nokogiri' module Gitlab - describe Asciidoc do + describe Asciidoc, lib: true do let(:input) { 'ascii' } let(:context) { {} } diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 72806bebe1..aad291c03c 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Auth do +describe Gitlab::Auth, lib: true do let(:gl_auth) { Gitlab::Auth.new } describe :find do diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb index dfa0e10318..73458735ad 100644 --- a/spec/lib/gitlab/backend/grack_auth_spec.rb +++ b/spec/lib/gitlab/backend/grack_auth_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe Grack::Auth do +describe Grack::Auth, lib: true do let(:user) { create(:user) } let(:project) { create(:project) } diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb index b60e23454d..fd869f48b5 100644 --- a/spec/lib/gitlab/backend/shell_spec.rb +++ b/spec/lib/gitlab/backend/shell_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Shell do +describe Gitlab::Shell, lib: true do let(:project) { double('Project', id: 7, path: 'diaspora') } let(:gitlab_shell) { Gitlab::Shell.new } @@ -16,7 +16,7 @@ describe Gitlab::Shell do it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") } - describe Gitlab::Shell::KeyAdder do + describe Gitlab::Shell::KeyAdder, lib: true do describe '#add_key' do it 'normalizes space characters in the key' do io = spy diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb index dfe58637ee..aa0699f2eb 100644 --- a/spec/lib/gitlab/bitbucket_import/client_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/client_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::BitbucketImport::Client do +describe Gitlab::BitbucketImport::Client, lib: true do let(:token) { '123456' } let(:secret) { 'secret' } let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) } diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb index 0e826a319e..e1c60e07b4 100644 --- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::BitbucketImport::ProjectCreator do +describe Gitlab::BitbucketImport::ProjectCreator, lib: true do let(:user) { create(:user) } let(:repo) do { diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index fe1b94a484..99288da1e4 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ClosingIssueExtractor do +describe Gitlab::ClosingIssueExtractor, lib: true do let(:project) { create(:project) } let(:project2) { create(:project) } let(:issue) { create(:issue, project: project) } diff --git a/spec/lib/gitlab/color_schemes_spec.rb b/spec/lib/gitlab/color_schemes_spec.rb index c7be45dbcd..0a1ec66f19 100644 --- a/spec/lib/gitlab/color_schemes_spec.rb +++ b/spec/lib/gitlab/color_schemes_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ColorSchemes do +describe Gitlab::ColorSchemes, lib: true do describe '.body_classes' do it 'returns a space-separated list of class names' do css = described_class.body_classes diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 7cdebdf209..8461e8ce50 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Database do +describe Gitlab::Database, lib: true do # These are just simple smoke tests to check if the methods work (regardless # of what they may return). describe '.mysql?' do diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index 8b7946f311..c7cdf8691d 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Diff::File do +describe Gitlab::Diff::File, lib: true do include RepoHelpers let(:project) { create(:project) } diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb index 4d5d143168..ba577bd28e 100644 --- a/spec/lib/gitlab/diff/parser_spec.rb +++ b/spec/lib/gitlab/diff/parser_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Diff::Parser do +describe Gitlab::Diff::Parser, lib: true do include RepoHelpers let(:project) { create(:project) } diff --git a/spec/lib/gitlab/email/attachment_uploader_spec.rb b/spec/lib/gitlab/email/attachment_uploader_spec.rb index 8fb432367b..476a21bf99 100644 --- a/spec/lib/gitlab/email/attachment_uploader_spec.rb +++ b/spec/lib/gitlab/email/attachment_uploader_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe Gitlab::Email::AttachmentUploader do +describe Gitlab::Email::AttachmentUploader, lib: true do describe "#execute" do let(:project) { build(:project) } let(:message_raw) { fixture_file("emails/attachment.eml") } diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index e470b7cd5f..b535413bbd 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe Gitlab::Email::Receiver do +describe Gitlab::Email::Receiver, lib: true do before do stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") end diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb index 7cae1da805..6f8e9a4be6 100644 --- a/spec/lib/gitlab/email/reply_parser_spec.rb +++ b/spec/lib/gitlab/email/reply_parser_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" # Inspired in great part by Discourse's Email::Receiver -describe Gitlab::Email::ReplyParser do +describe Gitlab::Email::ReplyParser, lib: true do describe '#execute' do def test_parse_body(mail_string) described_class.new(Mail::Message.new(mail_string)).execute diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index c7291689e3..9b3a0e3a75 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::GitAccess do +describe Gitlab::GitAccess, lib: true do let(:access) { Gitlab::GitAccess.new(actor, project) } let(:project) { create(:project) } let(:user) { create(:user) } diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 4cb91094cb..77ecfce6f1 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::GitAccessWiki do +describe Gitlab::GitAccessWiki, lib: true do let(:access) { Gitlab::GitAccessWiki.new(user, project) } let(:project) { create(:project) } let(:user) { create(:user) } diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb index 2661812031..49d8cdf431 100644 --- a/spec/lib/gitlab/github_import/client_spec.rb +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::GithubImport::Client do +describe Gitlab::GithubImport::Client, lib: true do let(:token) { '123456' } let(:client) { Gitlab::GithubImport::Client.new(token) } diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb index ca61d3c523..c93a3ebdae 100644 --- a/spec/lib/gitlab/github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/github_import/project_creator_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::GithubImport::ProjectCreator do +describe Gitlab::GithubImport::ProjectCreator, lib: true do let(:user) { create(:user) } let(:repo) do OpenStruct.new( diff --git a/spec/lib/gitlab/gitlab_import/client_spec.rb b/spec/lib/gitlab/gitlab_import/client_spec.rb index c511c51547..e6831e7c38 100644 --- a/spec/lib/gitlab/gitlab_import/client_spec.rb +++ b/spec/lib/gitlab/gitlab_import/client_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::GitlabImport::Client do +describe Gitlab::GitlabImport::Client, lib: true do let(:token) { '123456' } let(:client) { Gitlab::GitlabImport::Client.new(token) } diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb index 2d8923d14b..483f65cd05 100644 --- a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb +++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::GitlabImport::ProjectCreator do +describe Gitlab::GitlabImport::ProjectCreator, lib: true do let(:user) { create(:user) } let(:repo) do { diff --git a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb index c1125ca635..946712ca38 100644 --- a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb +++ b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::GitoriousImport::ProjectCreator do +describe Gitlab::GitoriousImport::ProjectCreator, lib: true do let(:user) { create(:user) } let(:repo) { Gitlab::GitoriousImport::Repository.new('foo/bar-baz-qux') } let(:namespace){ create(:group, owner: user) } diff --git a/spec/lib/gitlab/google_code_import/client_spec.rb b/spec/lib/gitlab/google_code_import/client_spec.rb index 37985c062b..85949ae8dc 100644 --- a/spec/lib/gitlab/google_code_import/client_spec.rb +++ b/spec/lib/gitlab/google_code_import/client_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe Gitlab::GoogleCodeImport::Client do +describe Gitlab::GoogleCodeImport::Client, lib: true do let(:raw_data) { JSON.parse(fixture_file("GoogleCodeProjectHosting.json")) } subject { described_class.new(raw_data) } diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb index 65ad7524cc..647631271e 100644 --- a/spec/lib/gitlab/google_code_import/importer_spec.rb +++ b/spec/lib/gitlab/google_code_import/importer_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe Gitlab::GoogleCodeImport::Importer do +describe Gitlab::GoogleCodeImport::Importer, lib: true do let(:mapped_user) { create(:user, username: "thilo123") } let(:raw_data) { JSON.parse(fixture_file("GoogleCodeProjectHosting.json")) } let(:client) { Gitlab::GoogleCodeImport::Client.new(raw_data) } diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb index 35549b4868..499a896ee7 100644 --- a/spec/lib/gitlab/google_code_import/project_creator_spec.rb +++ b/spec/lib/gitlab/google_code_import/project_creator_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::GoogleCodeImport::ProjectCreator do +describe Gitlab::GoogleCodeImport::ProjectCreator, lib: true do let(:user) { create(:user) } let(:repo) do Gitlab::GoogleCodeImport::Repository.new( diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index 5fdb9c723b..bcdba8d4c1 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe Gitlab::IncomingEmail do +describe Gitlab::IncomingEmail, lib: true do describe "self.enabled?" do context "when reply by email is enabled" do before do diff --git a/spec/lib/gitlab/inline_diff_spec.rb b/spec/lib/gitlab/inline_diff_spec.rb index 2e0a05088c..c690c19511 100644 --- a/spec/lib/gitlab/inline_diff_spec.rb +++ b/spec/lib/gitlab/inline_diff_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::InlineDiff do +describe Gitlab::InlineDiff, lib: true do describe '#processing' do let(:diff) do < Date: Wed, 9 Dec 2015 11:55:49 +0100 Subject: [PATCH 489/908] Tag service specs --- spec/services/archive_repository_service_spec.rb | 2 +- spec/services/ci/create_commit_service_spec.rb | 2 +- spec/services/ci/create_trigger_request_service_spec.rb | 2 +- spec/services/ci/event_service_spec.rb | 2 +- spec/services/ci/image_for_build_service_spec.rb | 2 +- spec/services/ci/register_build_service_spec.rb | 2 +- spec/services/ci/web_hook_service_spec.rb | 2 +- spec/services/create_release_service_spec.rb | 2 +- spec/services/create_snippet_service_spec.rb | 2 +- spec/services/destroy_group_service_spec.rb | 2 +- spec/services/event_create_service_spec.rb | 2 +- spec/services/git_hooks_service_spec.rb | 2 +- spec/services/git_push_service_spec.rb | 2 +- spec/services/git_tag_push_service_spec.rb | 2 +- spec/services/issues/bulk_update_service_spec.rb | 2 +- spec/services/issues/close_service_spec.rb | 2 +- spec/services/issues/create_service_spec.rb | 2 +- spec/services/issues/update_service_spec.rb | 2 +- spec/services/merge_requests/close_service_spec.rb | 2 +- spec/services/merge_requests/create_service_spec.rb | 2 +- spec/services/merge_requests/merge_service_spec.rb | 2 +- spec/services/merge_requests/refresh_service_spec.rb | 2 +- spec/services/merge_requests/reopen_service_spec.rb | 2 +- spec/services/merge_requests/update_service_spec.rb | 2 +- spec/services/milestones/close_service_spec.rb | 2 +- spec/services/milestones/create_service_spec.rb | 2 +- spec/services/notes/create_service_spec.rb | 2 +- spec/services/notification_service_spec.rb | 2 +- spec/services/projects/create_service_spec.rb | 2 +- spec/services/projects/destroy_service_spec.rb | 2 +- spec/services/projects/download_service_spec.rb | 2 +- spec/services/projects/fork_service_spec.rb | 2 +- spec/services/projects/transfer_service_spec.rb | 2 +- spec/services/projects/update_service_spec.rb | 2 +- spec/services/projects/upload_service_spec.rb | 2 +- spec/services/search_service_spec.rb | 2 +- spec/services/system_hooks_service_spec.rb | 2 +- spec/services/system_note_service_spec.rb | 2 +- spec/services/test_hook_service_spec.rb | 2 +- spec/services/update_release_service_spec.rb | 2 +- spec/services/update_snippet_service_spec.rb | 2 +- 41 files changed, 41 insertions(+), 41 deletions(-) diff --git a/spec/services/archive_repository_service_spec.rb b/spec/services/archive_repository_service_spec.rb index f7a36cd967..bd871605c6 100644 --- a/spec/services/archive_repository_service_spec.rb +++ b/spec/services/archive_repository_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe ArchiveRepositoryService do +describe ArchiveRepositoryService, services: true do let(:project) { create(:project) } subject { ArchiveRepositoryService.new(project, "master", "zip") } diff --git a/spec/services/ci/create_commit_service_spec.rb b/spec/services/ci/create_commit_service_spec.rb index e0ede1d58b..c2fafca2ad 100644 --- a/spec/services/ci/create_commit_service_spec.rb +++ b/spec/services/ci/create_commit_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' module Ci - describe CreateCommitService do + describe CreateCommitService, services: true do let(:service) { CreateCommitService.new } let(:project) { FactoryGirl.create(:ci_project) } let(:user) { nil } diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index 2ef4bb50a5..c80cb58163 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::CreateTriggerRequestService do +describe Ci::CreateTriggerRequestService, services: true do let(:service) { Ci::CreateTriggerRequestService.new } let(:gl_project) { create(:project) } let(:project) { gl_project.ensure_gitlab_ci_project } diff --git a/spec/services/ci/event_service_spec.rb b/spec/services/ci/event_service_spec.rb index 1264e17ff5..32516c75cf 100644 --- a/spec/services/ci/event_service_spec.rb +++ b/spec/services/ci/event_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::EventService do +describe Ci::EventService, services: true do let(:project) { FactoryGirl.create :ci_project } let(:user) { double(username: "root", id: 1) } diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index cda7d0c4a5..b43cabb4ee 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' module Ci - describe ImageForBuildService do + describe ImageForBuildService, services: true do let(:service) { ImageForBuildService.new } let(:project) { FactoryGirl.create(:ci_project) } let(:gl_project) { FactoryGirl.create(:project, gitlab_ci_project: project) } diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index b370dfbe11..379e07982f 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' module Ci - describe RegisterBuildService do + describe RegisterBuildService, services: true do let!(:service) { RegisterBuildService.new } let!(:gl_project) { FactoryGirl.create :empty_project } let!(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } diff --git a/spec/services/ci/web_hook_service_spec.rb b/spec/services/ci/web_hook_service_spec.rb index aa48fcbcbf..e7d8ab3065 100644 --- a/spec/services/ci/web_hook_service_spec.rb +++ b/spec/services/ci/web_hook_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::WebHookService do +describe Ci::WebHookService, services: true do let(:project) { FactoryGirl.create :ci_project } let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } diff --git a/spec/services/create_release_service_spec.rb b/spec/services/create_release_service_spec.rb index 26d7f365bb..61e5ae72f5 100644 --- a/spec/services/create_release_service_spec.rb +++ b/spec/services/create_release_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe CreateReleaseService do +describe CreateReleaseService, services: true do let(:project) { create(:project) } let(:user) { create(:user) } let(:tag_name) { project.repository.tag_names.first } diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb index 8edabe9450..c800dea04f 100644 --- a/spec/services/create_snippet_service_spec.rb +++ b/spec/services/create_snippet_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe CreateSnippetService do +describe CreateSnippetService, services: true do before do @user = create :user @admin = create :user, admin: true diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/destroy_group_service_spec.rb index e28564b386..afa89b8417 100644 --- a/spec/services/destroy_group_service_spec.rb +++ b/spec/services/destroy_group_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe DestroyGroupService do +describe DestroyGroupService, services: true do let!(:user) { create(:user) } let!(:group) { create(:group) } let!(:project) { create(:project, namespace: group) } diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index 7756b973ec..f6dc9d4008 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe EventCreateService do +describe EventCreateService, services: true do let(:service) { EventCreateService.new } describe 'Issues' do diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb index 7e018d3c9f..2bb9c3b3db 100644 --- a/spec/services/git_hooks_service_spec.rb +++ b/spec/services/git_hooks_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe GitHooksService do +describe GitHooksService, services: true do include RepoHelpers let(:user) { create :user } diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 17015d29e5..a04c242cf0 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe GitPushService do +describe GitPushService, services: true do include RepoHelpers let(:user) { create :user } diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index eed50c7eba..e2d15f1a83 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe GitTagPushService do +describe GitTagPushService, services: true do include RepoHelpers let(:user) { create :user } diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index 4c62fbafd7..6a7ea4b2f4 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Issues::BulkUpdateService do +describe Issues::BulkUpdateService, services: true do let(:issue) { create(:issue, project: @project) } before do diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index d711e3da10..3a8daf28f5 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Issues::CloseService do +describe Issues::CloseService, services: true do let(:user) { create(:user) } let(:user2) { create(:user) } let(:issue) { create(:issue, assignee: user2) } diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 7f1ebcb319..2148d091a5 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Issues::CreateService do +describe Issues::CreateService, services: true do let(:project) { create(:empty_project) } let(:user) { create(:user) } diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 73d0b7f7ab..87da0e9618 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Issues::UpdateService do +describe Issues::UpdateService, services: true do let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 272f389793..50d0c28879 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MergeRequests::CloseService do +describe MergeRequests::CloseService, services: true do let(:user) { create(:user) } let(:user2) { create(:user) } let(:merge_request) { create(:merge_request, assignee: user2) } diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index cc64d69361..be8f1676ee 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MergeRequests::CreateService do +describe MergeRequests::CreateService, services: true do let(:project) { create(:project) } let(:user) { create(:user) } diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index c0961ceb11..f2160567f0 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MergeRequests::MergeService do +describe MergeRequests::MergeService, services: true do let(:user) { create(:user) } let(:user2) { create(:user) } let(:merge_request) { create(:merge_request, assignee: user2) } diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 7ee4488521..b892f44af0 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MergeRequests::RefreshService do +describe MergeRequests::RefreshService, services: true do let(:project) { create(:project) } let(:user) { create(:user) } let(:service) { MergeRequests::RefreshService } diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index 05146bf43f..ac0221998f 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MergeRequests::ReopenService do +describe MergeRequests::ReopenService, services: true do let(:user) { create(:user) } let(:user2) { create(:user) } let(:merge_request) { create(:merge_request, assignee: user2) } diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index d899b1f01d..2e9e6e0870 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MergeRequests::UpdateService do +describe MergeRequests::UpdateService, services: true do let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb index 034c0f22e1..1cd6eb2ab3 100644 --- a/spec/services/milestones/close_service_spec.rb +++ b/spec/services/milestones/close_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Milestones::CloseService do +describe Milestones::CloseService, services: true do let(:user) { create(:user) } let(:project) { create(:project) } let(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) } diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb index 757c9a226d..c793026e30 100644 --- a/spec/services/milestones/create_service_spec.rb +++ b/spec/services/milestones/create_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Milestones::CreateService do +describe Milestones::CreateService, services: true do let(:project) { create(:empty_project) } let(:user) { create(:user) } diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index cc38d25779..a797a2fe4a 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Notes::CreateService do +describe Notes::CreateService, services: true do let(:project) { create(:empty_project) } let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 35fa412ed8..d7a898e85f 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe NotificationService do +describe NotificationService, services: true do let(:notification) { NotificationService.new } around(:each) do |example| diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index e81c4edb7d..2d8c316e38 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::CreateService do +describe Projects::CreateService, services: true do describe :create_by_user do before do @user = create :user diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index e83eef0b1a..1ec2707771 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::DestroyService do +describe Projects::DestroyService, services: true do let!(:user) { create(:user) } let!(:project) { create(:project, namespace: user.namespace) } let!(:path) { project.repository.path_to_repo } diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb index ddee2e62df..5ceed5af9a 100644 --- a/spec/services/projects/download_service_spec.rb +++ b/spec/services/projects/download_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::DownloadService do +describe Projects::DownloadService, services: true do describe 'File service' do before do @user = create :user diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 1feba6ce04..d1ee60a0ae 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::ForkService do +describe Projects::ForkService, services: true do describe :fork_by_user do before do @from_namespace = create(:namespace) diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 47755bfc99..c46259431a 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::TransferService do +describe Projects::TransferService, services: true do let(:user) { create(:user) } let(:group) { create(:group) } let(:project) { create(:project, namespace: user.namespace) } diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index b347fa15f8..c36d458198 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::UpdateService do +describe Projects::UpdateService, services: true do describe :update_by_user do before do @user = create :user diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb index 1b1a80d1fe..9268a9fb1a 100644 --- a/spec/services/projects/upload_service_spec.rb +++ b/spec/services/projects/upload_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::UploadService do +describe Projects::UploadService, services: true do describe 'File service' do before do @user = create :user diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index f57bfaea87..7b3a9a75d7 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Search::GlobalService' do +describe 'Search::GlobalService', services: true do let(:user) { create(:user) } let(:public_user) { create(:user) } let(:internal_user) { create(:user) } diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index a31fc1e4b0..febc78d278 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe SystemHooksService do +describe SystemHooksService, services: true do let(:user) { create :user } let(:project) { create :project } let(:project_member) { create :project_member } diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index a45130bd47..19f6332318 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe SystemNoteService do +describe SystemNoteService, services: true do let(:project) { create(:project) } let(:author) { create(:user) } let(:noteable) { create(:issue, project: project) } diff --git a/spec/services/test_hook_service_spec.rb b/spec/services/test_hook_service_spec.rb index 226196eeda..f034f251ba 100644 --- a/spec/services/test_hook_service_spec.rb +++ b/spec/services/test_hook_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe TestHookService do +describe TestHookService, services: true do let(:user) { create :user } let(:project) { create :project } let(:hook) { create :project_hook, project: project } diff --git a/spec/services/update_release_service_spec.rb b/spec/services/update_release_service_spec.rb index 93368c45b8..bba211089a 100644 --- a/spec/services/update_release_service_spec.rb +++ b/spec/services/update_release_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe UpdateReleaseService do +describe UpdateReleaseService, services: true do let(:project) { create(:project) } let(:user) { create(:user) } let(:tag_name) { project.repository.tag_names.first } diff --git a/spec/services/update_snippet_service_spec.rb b/spec/services/update_snippet_service_spec.rb index d7c516e393..124bb76e67 100644 --- a/spec/services/update_snippet_service_spec.rb +++ b/spec/services/update_snippet_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe UpdateSnippetService do +describe UpdateSnippetService, services: true do before do @user = create :user @admin = create :user, admin: true From 6c6fb1d8bb2c6aa44553ebf35496d64ff6b202d2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 9 Dec 2015 11:56:23 +0100 Subject: [PATCH 490/908] Split up spec:other even more --- .gitlab-ci.yml | 14 ++++++++++++++ lib/tasks/spec.rake | 20 +++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 943e40d530..aec9936da2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -31,6 +31,20 @@ spec:models: - ruby - mysql +spec:lib: + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib + tags: + - ruby + - mysql + +spec:services: + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services + tags: + - ruby + - mysql + spec:benchmark: script: - RAILS_ENV=test bundle exec rake spec:benchmark diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake index 343e4b6352..0985ef3a66 100644 --- a/lib/tasks/spec.rake +++ b/lib/tasks/spec.rake @@ -28,6 +28,24 @@ namespace :spec do run_commands(cmds) end + desc 'GitLab | Rspec | Run service specs' + task :services do + cmds = [ + %W(rake gitlab:setup), + %W(rspec spec --tag @services) + ] + run_commands(cmds) + end + + desc 'GitLab | Rspec | Run lib specs' + task :lib do + cmds = [ + %W(rake gitlab:setup), + %W(rspec spec --tag @lib) + ] + run_commands(cmds) + end + desc 'GitLab | Rspec | Run benchmark specs' task :benchmark do cmds = [ @@ -41,7 +59,7 @@ namespace :spec do task :other do cmds = [ %W(rake gitlab:setup), - %W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@benchmark) + %W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services --tag ~@benchmark) ] run_commands(cmds) end From 3fd1a36d74c6af0ff040580f014505568d0b3aab Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 9 Dec 2015 12:00:26 +0100 Subject: [PATCH 491/908] Fix spec --- spec/features/atom/users_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index dc41be8246..5213455633 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -79,6 +79,6 @@ describe "User Feed", feature: true do end def safe_name - html_escape(user.name) + CGI.escapeHTML(user.name) end end From 64eaccf10a7ac70c3dc2f9565a638e9f45a8359c Mon Sep 17 00:00:00 2001 From: Felix Eckhofer Date: Wed, 9 Dec 2015 12:26:18 +0100 Subject: [PATCH 492/908] Update init script only once The init script is already being updated in section 6. --- doc/update/8.1-to-8.2.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md index 7b228d6a22..b08a79ca0a 100644 --- a/doc/update/8.1-to-8.2.md +++ b/doc/update/8.1-to-8.2.md @@ -85,11 +85,10 @@ sudo -u git -H git checkout 0.4.2 sudo -u git -H make ``` -Update the GitLab init script and 'default' file. +Update the GitLab 'default' file. ``` cd /home/git/gitlab -sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab test -e /etc/default/gitlab && \ sudo sed -i.pre-8.2 's/^\([^=]*\)gitlab_git_http_server/\1gitlab_workhorse/' /etc/default/gitlab ``` From f7ff146aaac158ce2099583af5d5620204ec9b93 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 9 Dec 2015 13:52:13 +0100 Subject: [PATCH 493/908] Fix size of disabled 'New file' button --- app/views/projects/tree/_tree_header.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 0e1f707660..cefe33e581 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -32,5 +32,5 @@ New directory - elsif !on_top_of_branch? %li - %span.btn.add-to-tree.disabled.has_tooltip{title: "You can only add files when you are on a branch.", data: {container: 'body'}} + %span.btn.btn-sm.add-to-tree.disabled.has_tooltip{title: "You can only add files when you are on a branch.", data: {container: 'body'}} = icon('plus') From 9e5ac7285cc6b9b641d863457086b2e5292699e1 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 9 Dec 2015 11:30:21 -0200 Subject: [PATCH 494/908] Ensure notes are replaced on a merge request diff when they're updated --- app/assets/javascripts/notes.js.coffee | 2 +- features/project/merge_requests.feature | 10 ++++++++++ features/steps/project/merge_requests.rb | 25 ++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 533d00bfb0..5fe0318de6 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -286,7 +286,7 @@ class @Notes $html.find('.js-task-list-container').taskList('enable') # Find the note's `li` element by ID and replace it with the updated HTML - $note_li = $("#note_#{note.id}") + $note_li = $('.note-row-' + note.id) $note_li.replaceWith($html) ### diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 6cd081c868..5a7f7f7c64 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -83,6 +83,16 @@ Feature: Project Merge Requests And I switch to the merge request's comments tab Then I should see a discussion has started on diff + @javascript + Scenario: I edit a comment on a merge request diff + Given project "Shop" have "Bug NS-05" open merge request with diffs inside + And I visit merge request page "Bug NS-05" + And I click on the Changes tab + And I leave a comment like "Line is wrong" on diff + And I change the comment "Line is wrong" to "Typo, please fix" on diff + Then I should not see a diff comment saying "Line is wrong" + And I should see a diff comment saying "Typo, please fix" + @javascript Scenario: I comment on a line of a commit in merge request Given project "Shop" have "Bug NS-05" open merge request with diffs inside diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 822cf0ffe1..74539d0b01 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -186,6 +186,31 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps leave_comment "Line is wrong" end + step 'I change the comment "Line is wrong" to "Typo, please fix" on diff' do + page.within('.diff-file:nth-of-type(5) .note') do + find('.js-note-edit').click + + page.within('.current-note-edit-form', visible: true) do + fill_in 'note_note', with: 'Typo, please fix' + click_button 'Save Comment' + end + + expect(page).not_to have_button 'Save Comment', disabled: true, visible: true + end + end + + step 'I should not see a diff comment saying "Line is wrong"' do + page.within('.diff-file:nth-of-type(5) .note') do + expect(page).not_to have_visible_content 'Line is wrong' + end + end + + step 'I should see a diff comment saying "Typo, please fix"' do + page.within('.diff-file:nth-of-type(5) .note') do + expect(page).to have_visible_content 'Typo, please fix' + end + end + step 'I should see a discussion has started on diff' do page.within(".notes .discussion") do page.should have_content "#{current_user.name} started a discussion" From 05f06955bad652fa68901d620c32b30969237c69 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 9 Dec 2015 11:59:32 -0200 Subject: [PATCH 495/908] Ensure existing notes are highlighted properly on a merge request diff --- app/assets/javascripts/merge_request_tabs.js.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index b0eeb1db53..1f0b6d9ced 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -144,7 +144,9 @@ class @MergeRequestTabs @_get url: "#{source}.json" + @_location.search success: (data) => - document.getElementById('diffs').innerHTML = data.html + html = $(data.html) + html.syntaxHighlight() + $('#diffs').html(html) @diffsLoaded = true @scrollToElement("#diffs") From 429932a8b40540faa4d5cbe39119bfeccddd7bab Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 9 Dec 2015 12:16:24 -0200 Subject: [PATCH 496/908] Ensure new notes are highlighted properly on a merge request diff --- app/assets/javascripts/notes.js.coffee | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 5fe0318de6..b1df56b24f 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -148,6 +148,8 @@ class @Notes @note_ids.push(note.id) form = $("form[rel='" + note.discussion_id + "']") row = form.closest("tr") + note_html = $(note.html) + note_html.syntaxHighlight() # is this the first note of discussion? if row.is(".js-temp-notes-holder") @@ -158,14 +160,16 @@ class @Notes row.next().find(".note").remove() # Add note to 'Changes' page discussions - $(".notes[rel='" + note.discussion_id + "']").append note.html + $(".notes[rel='" + note.discussion_id + "']").append note_html # Init discussion on 'Discussion' page if it is merge request page if $('body').attr('data-page').indexOf('projects:merge_request') == 0 - $('ul.main-notes-list').append(note.discussion_with_diff_html) + discussion_html = $(note.discussion_with_diff_html) + discussion_html.syntaxHighlight() + $('ul.main-notes-list').append(discussion_html) else # append new note to all matching discussions - $(".notes[rel='" + note.discussion_id + "']").append note.html + $(".notes[rel='" + note.discussion_id + "']").append note_html # cleanup after successfully creating a diff/discussion note @removeDiscussionNoteForm(form) From 37199719c7da301fae368348df52f90f2af95863 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 9 Dec 2015 16:19:59 +0100 Subject: [PATCH 497/908] Make sure everyone has shared/lfs-objects --- shared/lfs-objects/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 shared/lfs-objects/.gitkeep diff --git a/shared/lfs-objects/.gitkeep b/shared/lfs-objects/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 From 7e8fc4822758057ed8a2239659cdd8f49099613d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Fri, 4 Dec 2015 16:54:25 -0500 Subject: [PATCH 498/908] Normalize email when looking for GitLab users from commit info. #3854 --- app/models/commit.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index 8ae5325d16..fa88a408fa 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -175,11 +175,11 @@ class Commit end def author - @author ||= User.find_by_any_email(author_email) + @author ||= User.find_by_any_email(author_email.downcase) end def committer - @committer ||= User.find_by_any_email(committer_email) + @committer ||= User.find_by_any_email(committer_email.downcase) end def parents From 2cb542b9d365558923ed125f76c5005188dbee77 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 9 Dec 2015 17:39:13 +0100 Subject: [PATCH 499/908] remove unnecessary code Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/commits/_head.html.haml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 2f604c82f1..fcccb002d7 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -4,10 +4,9 @@ Commits %span.badge= number_with_delimiter(@repository.commit_count) - - if project_nav_tab? :network - = nav_link(controller: %w(network)) do - = link_to namespace_project_network_path(@project.namespace, @project, current_ref), class: 'shortcuts-network' do - Network + = nav_link(controller: %w(network)) do + = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do + Network = nav_link(controller: :compare) do = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do From 2c61e3c057af02068bfb57f1c3f921d9f0c87b1d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 9 Dec 2015 17:47:45 +0100 Subject: [PATCH 500/908] Fix test for network shortcut Signed-off-by: Dmitriy Zaporozhets --- features/project/shortcuts.feature | 3 ++- features/steps/project/active_tab.rb | 4 ---- features/steps/shared/project_tab.rb | 4 ++++ 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature index 0f71c32380..10e7c23461 100644 --- a/features/project/shortcuts.feature +++ b/features/project/shortcuts.feature @@ -19,7 +19,8 @@ Feature: Project Shortcuts @javascript Scenario: Navigate to network tab Given I press "g" and "n" - Then the active main tab should be Network + Then the active sub tab should be Network + And the active main tab should be Commits @javascript Scenario: Navigate to graphs tab diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 94a5dd744d..9e96fa5ba4 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -67,10 +67,6 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps ensure_active_sub_tab('Commits') end - step 'the active sub tab should be Network' do - ensure_active_sub_tab('Network') - end - step 'the active sub tab should be Compare' do ensure_active_sub_tab('Compare') end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index 3bad28dc28..4fc2ece79f 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -49,4 +49,8 @@ module SharedProjectTab step 'the active main tab should be Activity' do ensure_active_main_tab('Activity') end + + step 'the active sub tab should be Network' do + ensure_active_sub_tab('Network') + end end From 364041e7ddf56a16a90f35ec85e35c05e5758621 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 9 Dec 2015 14:48:00 -0200 Subject: [PATCH 501/908] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 59fe30746c..08a40d8d4d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 8.3.0 (unreleased) - Add languages page to graphs - Block LDAP user when they are no longer found in the LDAP server - Improve wording on project visibility levels (Zeger-Jan van de Weg) + - Fix editing notes on a merge request diff v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) From 3d4b4683ebca56973a0b5f56a558d9cac3c67ed9 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 9 Dec 2015 12:08:44 -0500 Subject: [PATCH 502/908] Add CHANGELOG entry for !1998 [ci skip] --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 3017c52838..b9a746c780 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 8.3.0 (unreleased) - Add languages page to graphs - Block LDAP user when they are no longer found in the LDAP server - Improve wording on project visibility levels (Zeger-Jan van de Weg) + - Automatically select default clone protocol based on user preferences (Eirik Lygre) v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) From 7a2a860adcda179578098afb9b84894c1670f2c7 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 9 Dec 2015 12:09:00 -0500 Subject: [PATCH 503/908] Simplify shared User SSH key steps --- features/steps/shared/user.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/steps/shared/user.rb b/features/steps/shared/user.rb index 33c146f6db..f0721094ee 100644 --- a/features/steps/shared/user.rb +++ b/features/steps/shared/user.rb @@ -20,10 +20,10 @@ module SharedUser end step 'I have an ssh key' do - create(:key, user: @user, title: "An ssh-key", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+L3TbFegm3k8QjejSwemk4HhlRh+DuN679Pc5ckqE/MPhVtE/+kZQDYCTB284GiT2aIoGzmZ8ee9TkaoejAsBwlA+Wz2Q3vhz65X6sMgalRwpdJx8kSEUYV8ZPV3MZvPo8KdNg993o4jL6G36GDW4BPIyO6FPZhfsawdf6liVD0Xo5kibIK7B9VoE178cdLQtLpS2YolRwf5yy6XR6hbbBGQR+6xrGOdP16eGZDb1CE2bMvvJijjloFqPscGktWOqW+nfh5txwFfBzlfARDTBsS8WZtg3Yoj1kn33kPsWRlgHfNutFRAIynDuDdQzQq8tTtVwm+Yi75RfcPHW8y3P Work") + create(:personal_key, user: @user) end step 'I have no ssh keys' do - Key.delete_all + @user.keys.delete_all end end From 8d5527007a3cca93dabe7324f8fffbabdc7664d0 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 8 Dec 2015 20:52:13 -0800 Subject: [PATCH 504/908] Provide better diagnostic message upon project creation errors Prevents an Error 500 when project fails to create due to invalid parameters. See #3937 --- CHANGELOG | 1 + app/services/projects/create_service.rb | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3017c52838..dd1cdc7549 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.3.0 (unreleased) - Merge when build succeeds (Zeger-Jan van de Weg) + - Provide better diagnostic message upon project creation errors (Stan Hu) - Bump gollum-lib to 4.1.0 (Stan Hu) - Fix broken group avatar upload under "New group" (Stan Hu) - Update project repositorize size and commit count during import:repos task (Stan Hu) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 700a1db04d..a6820183be 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -64,8 +64,10 @@ module Projects after_create_actions if @project.persisted? @project - rescue - @project.errors.add(:base, "Can't save project. Please try again later") + rescue => e + message = "Unable to save project: #{e.message}" + Rails.logger.error(message) + @project.errors.add(:base, message) if @project @project end From 99fd78d6db9ca444853fc9903bdd203f6113b78b Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 8 Dec 2015 21:49:24 -0800 Subject: [PATCH 505/908] Add spec for invalid options in project creation --- spec/services/projects/create_service_spec.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 2d8c316e38..5d0b18558b 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -49,6 +49,13 @@ describe Projects::CreateService, services: true do it { expect(@project.namespace).to eq(@group) } end + context 'error handling' do + it 'handles invalid options' do + @opts.merge!({ default_branch: 'master' } ) + expect(create_project(@user, @opts)).to eq(nil) + end + end + context 'wiki_enabled creates repository directory' do context 'wiki_enabled true creates wiki repository directory' do before do From 86aa535562ea627ccea93bcdf7e63468a4a148bf Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 9 Dec 2015 13:35:20 -0500 Subject: [PATCH 506/908] Run db:reset before db:create on CI --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 39d8f59cf0..c614e14e24 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,7 +8,7 @@ before_script: - touch log/application.log - touch log/test.log - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - - bundle exec rake db:create RAILS_ENV=test + - bundle exec rake db:reset db:create RAILS_ENV=test spec:feature: script: From 98958decce9e3a5764de609f0c730c7c5fde65d6 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 9 Dec 2015 13:55:31 -0500 Subject: [PATCH 507/908] Add number_with_delimiter to build counts --- app/views/projects/builds/index.html.haml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 742676305a..fbf2c293db 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -12,17 +12,20 @@ %li{class: ('active' if @scope.nil?)} = link_to project_builds_path(@project) do Running - %span.badge.js-running-count= @all_builds.running_or_pending.count(:id) + %span.badge.js-running-count + = number_with_delimiter(@all_builds.running_or_pending.count(:id)) %li{class: ('active' if @scope == 'finished')} = link_to project_builds_path(@project, scope: :finished) do Finished - %span.badge.js-running-count= @all_builds.finished.count(:id) + %span.badge.js-running-count + = number_with_delimiter(@all_builds.finished.count(:id)) %li{class: ('active' if @scope == 'all')} = link_to project_builds_path(@project, scope: :all) do All - %span.badge.js-totalbuilds-count= @all_builds.count(:id) + %span.badge.js-totalbuilds-count + = number_with_delimiter(@all_builds.count(:id)) .gray-content-block #{(@scope || 'running').capitalize} builds from this project From 7b50965e9990bcb88f56b771d47514cbeb5316e5 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 9 Dec 2015 14:21:08 -0500 Subject: [PATCH 508/908] Update CHANGELOG [ci skip] --- CHANGELOG | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b9a746c780..01f4c386e1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,7 +20,6 @@ v 8.3.0 (unreleased) - Add API endpoint to fetch merge request commits list - Expose events API with comment information and author info - Fix: Ensure "Remove Source Branch" button is not shown when branch is being deleted. #3583 - - Fix 500 error when creating a merge request that removes a submodule - Run custom Git hooks when branch is created or deleted. - Fix bug when simultaneously accepting multiple MRs results in MRs that are of "merged" status, but not merged to the target branch - Add languages page to graphs @@ -31,9 +30,10 @@ v 8.3.0 (unreleased) v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) - Fix Error 500s when creating global milestones with Unicode characters (Stan Hu) - -v 8.2.3 + - Update documentation for "Guest" permissions + - Properly convert Emoji-only comments into Award Emojis - Webhook payload has an added, modified and removed properties for each commit + - Fix 500 error when creating a merge request that removes a submodule v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) @@ -94,7 +94,6 @@ v 8.2.0 - Add email notification to former assignee upon unassignment (Adam Lieskovský) - New design for project graphs page - Remove deprecated dumped yaml file generated from previous job definitions - - Fix incoming email config defaults - Show specific runners from projects where user is master or owner - MR target branch is now visible on a list view when it is different from project's default one - Improve Continuous Integration graphs page @@ -252,7 +251,6 @@ v 8.0.2 - Allow AWS S3 Server-Side Encryption with Amazon S3-Managed Keys for backups (Paul Beattie) v 8.0.1 - - Remove git refs used internally by GitLab from network graph (Stan Hu) - Improve CI migration procedure and documentation v 8.0.0 From c5dacce4d7e47a0504975fbb3bfaf478b95f1065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Wed, 9 Dec 2015 19:50:00 +0000 Subject: [PATCH 509/908] Use YAML.safe_load --- lib/ci/gitlab_ci_yaml_processor.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 3beafcad11..7f54f5f072 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -10,7 +10,7 @@ module Ci attr_reader :before_script, :image, :services, :variables, :path, :cache def initialize(config, path = nil) - @config = YAML.load(config) + @config = YAML.safe_load(config) @path = path unless @config.is_a? Hash @@ -250,4 +250,4 @@ module Ci end end end -end +end \ No newline at end of file From cbac31d923c39d71b162250f893d716073cc08e0 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 8 Dec 2015 17:43:03 -0500 Subject: [PATCH 510/908] Add copy-to-clipboard button to Snippets#show Closes #3701 --- CHANGELOG | 1 + app/assets/stylesheets/pages/snippets.scss | 6 ++++++ app/views/projects/snippets/show.html.haml | 4 ++-- app/views/shared/_file_highlight.html.haml | 5 +++-- app/views/snippets/show.html.haml | 4 ++-- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 01f4c386e1..a9ee1435e4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 8.3.0 (unreleased) - Block LDAP user when they are no longer found in the LDAP server - Improve wording on project visibility levels (Zeger-Jan van de Weg) - Automatically select default clone protocol based on user preferences (Eirik Lygre) + - Add copy-to-clipboard button for Snippets v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index bb74e50151..1430d01859 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -32,6 +32,12 @@ .file-holder { border-top: 0; } + + .file-actions { + .btn-clipboard { + @extend .btn; + } + } } .snippet-box { diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 5d706942f2..7c599563ce 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -10,8 +10,8 @@ %strong = @snippet.file_name .file-actions.hidden-xs - .btn-group.tree-btn-group - = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" = render 'shared/snippets/blob' diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index 57c3aff3e1..2bc98983d6 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -8,5 +8,6 @@ %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} %i.fa.fa-link = i - :preserve - #{highlight(blob.name, blob.data)} + .blob-content{data: {blob_id: blob.id}} + :preserve + #{highlight(blob.name, blob.data)} diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 69d8899d4c..a2b3656877 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -9,6 +9,6 @@ %strong = @snippet.file_name .file-actions.hidden-xs - .btn-group.tree-btn-group - = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" = render 'shared/snippets/blob' From b4b9df277bb1490ba04a976fb2a59f2a0f603173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Wed, 9 Dec 2015 20:58:53 +0000 Subject: [PATCH 511/908] Allow [Symbol] when loading YAML --- lib/ci/gitlab_ci_yaml_processor.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 7f54f5f072..e7fb1b79b7 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -10,7 +10,7 @@ module Ci attr_reader :before_script, :image, :services, :variables, :path, :cache def initialize(config, path = nil) - @config = YAML.safe_load(config) + @config = YAML.safe_load(config, [Symbol]) @path = path unless @config.is_a? Hash @@ -250,4 +250,4 @@ module Ci end end end -end \ No newline at end of file +end From 48bf0a34e2771096b175555b47133b2573faa4d8 Mon Sep 17 00:00:00 2001 From: Andrew Tomaka Date: Wed, 9 Dec 2015 16:11:21 -0500 Subject: [PATCH 512/908] "No ssh" message should be same on project page --- app/views/projects/empty.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 950ab33825..503d156661 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,4 +1,4 @@ -.alert_holder += content_for :flash_message do - if current_user && can?(current_user, :download_code, @project) = render 'shared/no_ssh' = render 'shared/no_password' From ab8ab9b10c6bda35d3a82e0cf52f45c1cd81eb09 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 10 Dec 2015 01:24:10 +0100 Subject: [PATCH 513/908] Fix list with controls display Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/lists.scss | 44 ++++++++++++--------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 481e3f33a2..cc48f8c816 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -7,7 +7,7 @@ padding: 0; list-style: none; - li { + > li { padding: 10px 15px; min-height: 20px; border-bottom: 1px solid #eee; @@ -123,24 +123,6 @@ ul.content-list { padding: 10px 14px; } } - - ul.controls { - list-style: none; - - li { - float: left; - padding-right: 10px; - - .author_link { - display: inline-block; - - .avatar-inline { - margin-left: 0; - margin-right: 0; - } - } - } - } } } @@ -149,3 +131,27 @@ ul.content-list { margin: 0; } } + +ul.controls { + padding-top: 1px; + float: right; + list-style: none; + + .btn { + padding: 10px 14px; + } + + > li { + float: left; + padding-right: 10px; + + .author_link { + display: inline-block; + + .avatar-inline { + margin-left: 0; + margin-right: 0; + } + } + } +} From f4ec906e90b2f8dbf18b359b773e3b31f5da89ff Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Wed, 9 Dec 2015 11:45:26 -0600 Subject: [PATCH 514/908] Use devise paranoid mode and ensure the same message is returned every time Skipped CI because it has already passed. Had to rebase due to CHANGELOG. --- CHANGELOG | 3 +++ app/controllers/passwords_controller.rb | 6 ++++-- config/initializers/devise.rb | 2 +- config/locales/devise.en.yml | 1 - spec/features/password_reset_spec.rb | 26 ++++++++++++------------- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 01f4c386e1..03574b1e77 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,6 +32,9 @@ v 8.2.3 - Fix Error 500s when creating global milestones with Unicode characters (Stan Hu) - Update documentation for "Guest" permissions - Properly convert Emoji-only comments into Award Emojis + - Enable devise paranoid mode to prevent user enumeration attack + +v 8.2.3 - Webhook payload has an added, modified and removed properties for each commit - Fix 500 error when creating a merge request that removes a submodule diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 2025158d06..f74daff3bd 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -40,7 +40,9 @@ class PasswordsController < Devise::PasswordsController def throttle_reset return unless resource && resource.recently_sent_password_reset? - redirect_to new_password_path(resource_name), - alert: I18n.t('devise.passwords.recently_reset') + # Throttle reset attempts, but return a normal message to + # avoid user enumeration attack. + redirect_to new_user_session_path, + notice: I18n.t('devise.passwords.send_paranoid_instructions') end end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 29506970af..5fb43a86e1 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -60,7 +60,7 @@ Devise.setup do |config| # It will change confirmation, password recovery and other workflows # to behave the same regardless if the e-mail provided was right or wrong. # Does not affect registerable. - # config.paranoid = true + config.paranoid = true # ==> Configuration for :database_authenticatable # For bcrypt, this is the cost for hashing the password and defaults to 10. If diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 22070e37f0..bd4c3ebc69 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -30,7 +30,6 @@ en: success: "Successfully authenticated from %{kind} account." passwords: no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." - recently_reset: "Instructions about how to reset your password have already been sent recently. Please wait a few minutes to try again." send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." updated: "Your password has been changed successfully. You are now signed in." diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb index 85e70b4d47..257d363438 100644 --- a/spec/features/password_reset_spec.rb +++ b/spec/features/password_reset_spec.rb @@ -3,11 +3,12 @@ require 'spec_helper' feature 'Password reset', feature: true do describe 'throttling' do it 'sends reset instructions when not previously sent' do - visit root_path - forgot_password(create(:user)) + user = create(:user) + forgot_password(user) - expect(page).to have_content(I18n.t('devise.passwords.send_instructions')) + expect(page).to have_content(I18n.t('devise.passwords.send_paranoid_instructions')) expect(current_path).to eq new_user_session_path + expect(user.recently_sent_password_reset?).to be_truthy end it 'sends reset instructions when previously sent more than a minute ago' do @@ -15,26 +16,25 @@ feature 'Password reset', feature: true do user.send_reset_password_instructions user.update_attribute(:reset_password_sent_at, 5.minutes.ago) - visit root_path - forgot_password(user) - - expect(page).to have_content(I18n.t('devise.passwords.send_instructions')) + expect{ forgot_password(user) }.to change{ user.reset_password_sent_at } + expect(page).to have_content(I18n.t('devise.passwords.send_paranoid_instructions')) expect(current_path).to eq new_user_session_path end - it "throttles multiple resets in a short timespan" do + it 'throttles multiple resets in a short timespan' do user = create(:user) user.send_reset_password_instructions + # Reload because PG handles datetime less precisely than Ruby/Rails + user.reload - visit root_path - forgot_password(user) - - expect(page).to have_content(I18n.t('devise.passwords.recently_reset')) - expect(current_path).to eq new_user_password_path + expect{ forgot_password(user) }.not_to change{ user.reset_password_sent_at } + expect(page).to have_content(I18n.t('devise.passwords.send_paranoid_instructions')) + expect(current_path).to eq new_user_session_path end end def forgot_password(user) + visit root_path click_on 'Forgot your password?' fill_in 'Email', with: user.email click_button 'Reset password' From e3ee46a13b91a6cefb0efb1841fb24afed37b674 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 10 Dec 2015 14:36:31 +0200 Subject: [PATCH 515/908] Don't allow to edit award emoji comments --- CHANGELOG | 1 + app/models/note.rb | 2 +- spec/models/note_spec.rb | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 144b348771..776f86c0e0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ v 8.3.0 (unreleased) - Improve wording on project visibility levels (Zeger-Jan van de Weg) - Automatically select default clone protocol based on user preferences (Eirik Lygre) - Make Network page as sub tab of Commits + - Prevent possible XSS attack with award-emoji v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) diff --git a/app/models/note.rb b/app/models/note.rb index 98c29ddc4c..0f7efc2f2a 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -350,7 +350,7 @@ class Note < ActiveRecord::Base end def editable? - !system? + !system? && !is_award end # Checks if note is an award added as a comment diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index cd3c868ecc..5b6f177ebb 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -142,4 +142,21 @@ describe Note, models: true do expect(Note.grouped_awards.first.last).to match_array(Note.all) end end + + describe "editable?" do + it "returns true" do + note = build(:note) + expect(note.editable?).to be_truthy + end + + it "returns false" do + note = build(:note, system: true) + expect(note.editable?).to be_falsy + end + + it "returns false" do + note = build(:note, is_award: true, note: "smiley") + expect(note.editable?).to be_falsy + end + end end From 57af1231f2e99ab525d8c1fda2e0888e41a873a5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 10 Dec 2015 15:46:29 +0100 Subject: [PATCH 516/908] Add missing changelog items --- CHANGELOG | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index fc1cb1a996..18a9d317cc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) - - Merge when build succeeds (Zeger-Jan van de Weg) + - Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg) - Bump gollum-lib to 4.1.0 (Stan Hu) - Fix broken group avatar upload under "New group" (Stan Hu) - Update project repositorize size and commit count during import:repos task (Stan Hu) @@ -28,6 +28,13 @@ v 8.3.0 (unreleased) - Automatically select default clone protocol based on user preferences (Eirik Lygre) - Make Network page as sub tab of Commits - Add copy-to-clipboard button for Snippets + - Add indication to merge request list item that MR cannot be merged automatically + - Default target branch to patch-n when editing file in protected branch + - Add Builds tab to merge request detail page + - Allow milestones, issues and MRs to be created from dashboard and group indexes + - Use new style for wiki + - Use new style for milestone detail page + - Fix sidebar tooltips when collapsed v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) From 2988e1fbf50b3c9e803a9358933e3e969e64dcc3 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 7 Dec 2015 13:23:23 +0100 Subject: [PATCH 517/908] Migrate CI::Services and CI::WebHooks to Services and WebHooks --- CHANGELOG | 1 + .../projects/ci_services_controller.rb | 49 ----- .../projects/ci_web_hooks_controller.rb | 45 ----- app/controllers/projects/hooks_controller.rb | 3 +- .../projects/services_controller.rb | 4 +- app/mailers/ci/emails/builds.rb | 17 -- app/mailers/ci/notify.rb | 46 ----- app/mailers/emails/builds.rb | 15 ++ app/mailers/notify.rb | 1 + app/models/ci/build.rb | 20 +- app/models/ci/commit.rb | 4 + app/models/ci/project.rb | 41 ---- app/models/ci/service.rb | 105 ----------- app/models/ci/web_hook.rb | 43 ----- app/models/hooks/project_hook.rb | 1 + app/models/hooks/web_hook.rb | 1 + app/models/project.rb | 1 + .../project_services/builds_email_service.rb | 81 ++++++++ .../project_services/ci/hip_chat_message.rb | 73 -------- .../project_services/ci/hip_chat_service.rb | 93 --------- .../project_services/ci/mail_service.rb | 84 --------- .../project_services/ci/slack_message.rb | 92 --------- .../project_services/ci/slack_service.rb | 81 -------- .../project_services/hipchat_service.rb | 42 ++++- app/models/project_services/slack_service.rb | 22 ++- .../slack_service/base_message.rb | 3 + .../slack_service/build_message.rb | 82 ++++++++ app/models/service.rb | 18 ++ app/services/ci/create_builds_service.rb | 3 +- .../layouts/nav/_project_settings.html.haml | 10 - .../notify/build_fail_email.html.haml | 2 +- .../{ci => }/notify/build_fail_email.text.erb | 0 .../notify/build_success_email.html.haml | 2 +- .../notify/build_success_email.text.erb | 0 .../projects/ci_services/_form.html.haml | 54 ------ app/views/projects/ci_services/edit.html.haml | 2 - .../projects/ci_services/index.html.haml | 23 --- .../projects/ci_web_hooks/index.html.haml | 94 ---------- app/views/projects/hooks/index.html.haml | 9 +- app/views/shared/_service_settings.html.haml | 9 + app/workers/build_email_worker.rb | 19 ++ app/workers/ci/hip_chat_notifier_worker.rb | 19 -- app/workers/ci/slack_notifier_worker.rb | 10 - app/workers/ci/web_hook_worker.rb | 9 - config/routes.rb | 11 -- ...1203162134_add_build_events_to_services.rb | 6 + db/schema.rb | 4 +- features/project/service.feature | 8 +- features/steps/admin/settings.rb | 2 + features/steps/project/services.rb | 8 +- lib/api/entities.rb | 5 +- lib/api/project_hooks.rb | 2 + lib/ci/api/projects.rb | 38 ---- lib/gitlab/build_data_builder.rb | 64 +++++++ spec/factories/ci/web_hook.rb | 6 - spec/features/ci_web_hooks_spec.rb | 27 --- spec/lib/gitlab/build_data_builder_spec.rb | 20 ++ spec/mailers/ci/notify_spec.rb | 35 ---- spec/mailers/notify_spec.rb | 29 +++ .../project_services/hip_chat_message_spec.rb | 39 ---- .../project_services/hip_chat_service_spec.rb | 73 -------- .../ci/project_services/mail_service_spec.rb | 177 ------------------ .../ci/project_services/slack_message_spec.rb | 43 ----- .../ci/project_services/slack_service_spec.rb | 57 ------ spec/models/ci/project_spec.rb | 2 - spec/models/ci/service_spec.rb | 48 ----- spec/models/ci/web_hook_spec.rb | 63 ------- .../project_services/hipchat_service_spec.rb | 49 +++++ .../slack_service/build_message_spec.rb | 46 +++++ spec/requests/api/project_hooks_spec.rb | 12 +- spec/requests/ci/api/projects_spec.rb | 73 -------- spec/services/ci/web_hook_service_spec.rb | 37 ---- spec/workers/build_email_worker_spec.rb | 35 ++++ 73 files changed, 602 insertions(+), 1750 deletions(-) delete mode 100644 app/controllers/projects/ci_services_controller.rb delete mode 100644 app/controllers/projects/ci_web_hooks_controller.rb delete mode 100644 app/mailers/ci/emails/builds.rb delete mode 100644 app/mailers/ci/notify.rb create mode 100644 app/mailers/emails/builds.rb delete mode 100644 app/models/ci/service.rb delete mode 100644 app/models/ci/web_hook.rb create mode 100644 app/models/project_services/builds_email_service.rb delete mode 100644 app/models/project_services/ci/hip_chat_message.rb delete mode 100644 app/models/project_services/ci/hip_chat_service.rb delete mode 100644 app/models/project_services/ci/mail_service.rb delete mode 100644 app/models/project_services/ci/slack_message.rb delete mode 100644 app/models/project_services/ci/slack_service.rb create mode 100644 app/models/project_services/slack_service/build_message.rb rename app/views/{ci => }/notify/build_fail_email.html.haml (96%) rename app/views/{ci => }/notify/build_fail_email.text.erb (100%) rename app/views/{ci => }/notify/build_success_email.html.haml (95%) rename app/views/{ci => }/notify/build_success_email.text.erb (100%) delete mode 100644 app/views/projects/ci_services/_form.html.haml delete mode 100644 app/views/projects/ci_services/edit.html.haml delete mode 100644 app/views/projects/ci_services/index.html.haml delete mode 100644 app/views/projects/ci_web_hooks/index.html.haml create mode 100644 app/workers/build_email_worker.rb delete mode 100644 app/workers/ci/hip_chat_notifier_worker.rb delete mode 100644 app/workers/ci/slack_notifier_worker.rb delete mode 100644 app/workers/ci/web_hook_worker.rb create mode 100644 db/migrate/20151203162134_add_build_events_to_services.rb create mode 100644 lib/gitlab/build_data_builder.rb delete mode 100644 spec/factories/ci/web_hook.rb delete mode 100644 spec/features/ci_web_hooks_spec.rb create mode 100644 spec/lib/gitlab/build_data_builder_spec.rb delete mode 100644 spec/mailers/ci/notify_spec.rb delete mode 100644 spec/models/ci/project_services/hip_chat_message_spec.rb delete mode 100644 spec/models/ci/project_services/hip_chat_service_spec.rb delete mode 100644 spec/models/ci/project_services/mail_service_spec.rb delete mode 100644 spec/models/ci/project_services/slack_message_spec.rb delete mode 100644 spec/models/ci/project_services/slack_service_spec.rb delete mode 100644 spec/models/ci/service_spec.rb delete mode 100644 spec/models/ci/web_hook_spec.rb create mode 100644 spec/models/project_services/slack_service/build_message_spec.rb delete mode 100644 spec/services/ci/web_hook_service_spec.rb create mode 100644 spec/workers/build_email_worker_spec.rb diff --git a/CHANGELOG b/CHANGELOG index fc1cb1a996..2f94eb6c66 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 8.3.0 (unreleased) - Fire update hook from GitLab - Style warning about mentioning many people in a comment - Fix: sort milestones by due date once again (Greg Smethells) + - Migrate all CI::Services and CI::WebHooks to Services and WebHooks - Don't show project fork event as "imported" - Add API endpoint to fetch merge request commits list - Expose events API with comment information and author info diff --git a/app/controllers/projects/ci_services_controller.rb b/app/controllers/projects/ci_services_controller.rb deleted file mode 100644 index 550a019e8e..0000000000 --- a/app/controllers/projects/ci_services_controller.rb +++ /dev/null @@ -1,49 +0,0 @@ -class Projects::CiServicesController < Projects::ApplicationController - before_action :ci_project - before_action :authorize_admin_project! - - layout "project_settings" - - def index - @ci_project.build_missing_services - @services = @ci_project.services.reload - end - - def edit - service - end - - def update - if service.update_attributes(service_params) - redirect_to edit_namespace_project_ci_service_path(@project.namespace, @project, service.to_param) - else - render 'edit' - end - end - - def test - last_build = @project.ci_builds.last - - if service.execute(last_build) - message = { notice: 'We successfully tested the service' } - else - message = { alert: 'We tried to test the service but error occurred' } - end - - redirect_back_or_default(options: message) - end - - private - - def service - @service ||= @ci_project.services.find { |service| service.to_param == params[:id] } - end - - def service_params - params.require(:service).permit( - :type, :active, :webhook, :notify_only_broken_builds, - :email_recipients, :email_only_broken_builds, :email_add_pusher, - :hipchat_token, :hipchat_room, :hipchat_server - ) - end -end diff --git a/app/controllers/projects/ci_web_hooks_controller.rb b/app/controllers/projects/ci_web_hooks_controller.rb deleted file mode 100644 index a2d470d4a6..0000000000 --- a/app/controllers/projects/ci_web_hooks_controller.rb +++ /dev/null @@ -1,45 +0,0 @@ -class Projects::CiWebHooksController < Projects::ApplicationController - before_action :ci_project - before_action :authorize_admin_project! - - layout "project_settings" - - def index - @web_hooks = @ci_project.web_hooks - @web_hook = Ci::WebHook.new - end - - def create - @web_hook = @ci_project.web_hooks.new(web_hook_params) - @web_hook.save - - if @web_hook.valid? - redirect_to namespace_project_ci_web_hooks_path(@project.namespace, @project) - else - @web_hooks = @ci_project.web_hooks.select(&:persisted?) - render :index - end - end - - def test - Ci::TestHookService.new.execute(hook, current_user) - - redirect_back_or_default(default: { action: 'index' }) - end - - def destroy - hook.destroy - - redirect_to namespace_project_ci_web_hooks_path(@project.namespace, @project) - end - - private - - def hook - @web_hook ||= @ci_project.web_hooks.find(params[:id]) - end - - def web_hook_params - params.require(:web_hook).permit(:url) - end -end diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 6a62880cb7..5fd4f855de 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -53,6 +53,7 @@ class Projects::HooksController < Projects::ApplicationController def hook_params params.require(:hook).permit(:url, :push_events, :issues_events, - :merge_requests_events, :tag_push_events, :note_events, :enable_ssl_verification) + :merge_requests_events, :tag_push_events, :note_events, + :build_events, :enable_ssl_verification) end end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 42dbb497e0..6e7590260f 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -6,7 +6,9 @@ class Projects::ServicesController < Projects::ApplicationController :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, :colorize_messages, :channels, :push_events, :issues_events, :merge_requests_events, :tag_push_events, - :note_events, :send_from_committer_email, :disable_diffs, :external_wiki_url, + :note_events, :build_events, + :notify_only_broken_builds, :add_pusher, + :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, :server_host, :server_port, :default_irc_uri, :enable_ssl_verification] diff --git a/app/mailers/ci/emails/builds.rb b/app/mailers/ci/emails/builds.rb deleted file mode 100644 index 6fb4fba85e..0000000000 --- a/app/mailers/ci/emails/builds.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Ci - module Emails - module Builds - def build_fail_email(build_id, to) - @build = Ci::Build.find(build_id) - @project = @build.project - mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha)) - end - - def build_success_email(build_id, to) - @build = Ci::Build.find(build_id) - @project = @build.project - mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha)) - end - end - end -end diff --git a/app/mailers/ci/notify.rb b/app/mailers/ci/notify.rb deleted file mode 100644 index 404842cf21..0000000000 --- a/app/mailers/ci/notify.rb +++ /dev/null @@ -1,46 +0,0 @@ -module Ci - class Notify < ActionMailer::Base - include Ci::Emails::Builds - - add_template_helper Ci::GitlabHelper - - default_url_options[:host] = Gitlab.config.gitlab.host - default_url_options[:protocol] = Gitlab.config.gitlab.protocol - default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port? - default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root - - default from: Gitlab.config.gitlab.email_from - - # Just send email with 3 seconds delay - def self.delay - delay_for(2.seconds) - end - - private - - # Formats arguments into a String suitable for use as an email subject - # - # extra - Extra Strings to be inserted into the subject - # - # Examples - # - # >> subject('Lorem ipsum') - # => "GitLab-CI | Lorem ipsum" - # - # # Automatically inserts Project name when @project is set - # >> @project = Project.last - # => # - # >> subject('Lorem ipsum') - # => "GitLab-CI | Ruby on Rails | Lorem ipsum " - # - # # Accepts multiple arguments - # >> subject('Lorem ipsum', 'Dolor sit amet') - # => "GitLab-CI | Lorem ipsum | Dolor sit amet" - def subject(*extra) - subject = "GitLab-CI" - subject << (@project ? " | #{@project.name}" : "") - subject << " | " + extra.join(' | ') if extra.present? - subject - end - end -end diff --git a/app/mailers/emails/builds.rb b/app/mailers/emails/builds.rb new file mode 100644 index 0000000000..d58609a2de --- /dev/null +++ b/app/mailers/emails/builds.rb @@ -0,0 +1,15 @@ +module Emails + module Builds + def build_fail_email(build_id, to) + @build = Ci::Build.find(build_id) + @project = @build.project + mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha)) + end + + def build_success_email(build_id, to) + @build = Ci::Build.find(build_id) + @project = @build.project + mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha)) + end + end +end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 50a409c375..874769e239 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -7,6 +7,7 @@ class Notify < BaseMailer include Emails::Projects include Emails::Profile include Emails::Groups + include Emails::Builds add_template_helper MergeRequestsHelper add_template_helper EmailsHelper diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 52ce1b920f..564041e321 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -96,21 +96,21 @@ module Ci end state_machine :status, initial: :pending do + after_transition pending: :running do |build, transition| + build.execute_hooks + end + after_transition any => [:success, :failed, :canceled] do |build, transition| return unless build.gl_project project = build.project - if project.web_hooks? - Ci::WebHookService.new.build_end(build) - end - - build.commit.create_next_builds(build) - project.execute_services(build) - if project.coverage_enabled? build.update_coverage end + + build.commit.create_next_builds(build) + build.execute_hooks end end @@ -275,6 +275,12 @@ module Ci end end + def execute_hooks + build_data = Gitlab::BuildDataBuilder.build(self) + gl_project.execute_hooks(build_data.dup, :build_hooks) + gl_project.execute_services(build_data.dup, :build_hooks) + end + private def yaml_variables diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 75465685e9..e63f779094 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -178,6 +178,10 @@ module Ci duration_array.reduce(:+).to_i end + def started_at + @started_at ||= statuses.order('started_at ASC').first.try(:started_at) + end + def finished_at @finished_at ||= statuses.order('finished_at DESC').first.try(:finished_at) end diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb index 669ee1cc0d..79ff7e1dcd 100644 --- a/app/models/ci/project.rb +++ b/app/models/ci/project.rb @@ -35,17 +35,10 @@ module Ci has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' has_many :runners, through: :runner_projects, class_name: 'Ci::Runner' - has_many :web_hooks, dependent: :destroy, class_name: 'Ci::WebHook' has_many :events, dependent: :destroy, class_name: 'Ci::Event' has_many :variables, dependent: :destroy, class_name: 'Ci::Variable' has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger' - # Project services - has_many :services, dependent: :destroy, class_name: 'Ci::Service' - has_one :hip_chat_service, dependent: :destroy, class_name: 'Ci::HipChatService' - has_one :slack_service, dependent: :destroy, class_name: 'Ci::SlackService' - has_one :mail_service, dependent: :destroy, class_name: 'Ci::MailService' - accepts_nested_attributes_for :variables, allow_destroy: true delegate :name_with_namespace, :path_with_namespace, :web_url, :http_url_to_repo, :ssh_url_to_repo, to: :gl_project @@ -122,14 +115,6 @@ module Ci email_add_pusher || email_recipients.present? end - def web_hooks? - web_hooks.any? - end - - def services? - services.any? - end - def timeout_in_minutes timeout / 60 end @@ -151,32 +136,6 @@ module Ci end end - def available_services_names - %w(slack mail hip_chat) - end - - def build_missing_services - available_services_names.each do |service_name| - service = services.find { |service| service.to_param == service_name } - - # If service is available but missing in db - # we should create an instance. Ex `create_gitlab_ci_service` - self.send :"create_#{service_name}_service" if service.nil? - end - end - - def execute_services(data) - services.each do |service| - - # Call service hook only if it is active - begin - service.execute(data) if service.active && service.can_execute?(data) - rescue => e - logger.error(e) - end - end - end - def setup_finished? commits.any? end diff --git a/app/models/ci/service.rb b/app/models/ci/service.rb deleted file mode 100644 index 8063c51e82..0000000000 --- a/app/models/ci/service.rb +++ /dev/null @@ -1,105 +0,0 @@ -# == Schema Information -# -# Table name: ci_services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -# To add new service you should build a class inherited from Service -# and implement a set of methods -module Ci - class Service < ActiveRecord::Base - extend Ci::Model - - serialize :properties, JSON - - default_value_for :active, false - - after_initialize :initialize_properties - - belongs_to :project, class_name: 'Ci::Project' - - validates :project_id, presence: true - - def activated? - active - end - - def category - :common - end - - def initialize_properties - self.properties = {} if properties.nil? - end - - def title - # implement inside child - end - - def description - # implement inside child - end - - def help - # implement inside child - end - - def to_param - # implement inside child - end - - def fields - # implement inside child - [] - end - - def can_test? - project.builds.any? - end - - def can_execute?(build) - true - end - - def execute(build) - # implement inside child - end - - # Provide convenient accessor methods - # for each serialized property. - def self.prop_accessor(*args) - args.each do |arg| - class_eval %{ - def #{arg} - (properties || {})['#{arg}'] - end - - def #{arg}=(value) - self.properties ||= {} - self.properties['#{arg}'] = value - end - } - end - end - - def self.boolean_accessor(*args) - self.prop_accessor(*args) - - args.each do |arg| - class_eval %{ - def #{arg}? - ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg}) - end - } - end - end - end -end diff --git a/app/models/ci/web_hook.rb b/app/models/ci/web_hook.rb deleted file mode 100644 index 0dc15eb668..0000000000 --- a/app/models/ci/web_hook.rb +++ /dev/null @@ -1,43 +0,0 @@ -# == Schema Information -# -# Table name: ci_web_hooks -# -# id :integer not null, primary key -# url :string(255) not null -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# - -module Ci - class WebHook < ActiveRecord::Base - extend Ci::Model - - include HTTParty - - belongs_to :project, class_name: 'Ci::Project' - - # HTTParty timeout - default_timeout 10 - - validates :url, presence: true, url: true - - def execute(data) - parsed_url = URI.parse(url) - if parsed_url.userinfo.blank? - Ci::WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" }, verify: false) - else - post_url = url.gsub("#{parsed_url.userinfo}@", "") - auth = { - username: URI.decode(parsed_url.user), - password: URI.decode(parsed_url.password), - } - Ci::WebHook.post(post_url, - body: data.to_json, - headers: { "Content-Type" => "application/json" }, - verify: false, - basic_auth: auth) - end - end - end -end diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index 337b309712..2263805777 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -25,4 +25,5 @@ class ProjectHook < WebHook scope :issue_hooks, -> { where(issues_events: true) } scope :note_hooks, -> { where(note_events: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true) } + scope :build_hooks, -> { where(build_events: true) } end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 715ec5908b..40eb0e20b4 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -26,6 +26,7 @@ class WebHook < ActiveRecord::Base default_value_for :note_events, false default_value_for :merge_requests_events, false default_value_for :tag_push_events, false + default_value_for :build_events, false default_value_for :enable_ssl_verification, true # HTTParty timeout diff --git a/app/models/project.rb b/app/models/project.rb index e78868af1c..60ca2cad6a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -81,6 +81,7 @@ class Project < ActiveRecord::Base has_one :campfire_service, dependent: :destroy has_one :drone_ci_service, dependent: :destroy has_one :emails_on_push_service, dependent: :destroy + has_one :builds_email_service, dependent: :destroy has_one :irker_service, dependent: :destroy has_one :pivotaltracker_service, dependent: :destroy has_one :hipchat_service, dependent: :destroy diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb new file mode 100644 index 0000000000..9c9b5a4d08 --- /dev/null +++ b/app/models/project_services/builds_email_service.rb @@ -0,0 +1,81 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +class BuildsEmailService < Service + prop_accessor :recipients + boolean_accessor :add_pusher + boolean_accessor :notify_only_broken_builds + validates :recipients, presence: true, if: :activated? + + def title + 'Builds emails' + end + + def description + 'Email the builds status to a list of recipients.' + end + + def to_param + 'builds_email' + end + + def supported_events + %w(build) + end + + def execute(push_data) + return unless supported_events.include?(push_data[:object_kind]) + + if should_build_be_notified?(push_data) + BuildEmailWorker.perform_async( + push_data[:build_id], + all_recipients(push_data), + push_data, + ) + end + end + + def fields + [ + { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by whitespace' }, + { type: 'checkbox', name: 'add_pusher', label: 'Add pusher to recipients list' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + ] + end + + def should_build_be_notified?(data) + case data[:build_status] + when 'success' + !notify_only_broken_builds? + when 'failed' + true + else + false + end + end + + def all_recipients(data) + if add_pusher? && data[:user][:email] + recipients + " #{data[:user][:email]}" + else + recipients + end + end +end diff --git a/app/models/project_services/ci/hip_chat_message.rb b/app/models/project_services/ci/hip_chat_message.rb deleted file mode 100644 index d89466b689..0000000000 --- a/app/models/project_services/ci/hip_chat_message.rb +++ /dev/null @@ -1,73 +0,0 @@ -module Ci - class HipChatMessage - include Gitlab::Application.routes.url_helpers - - attr_reader :build - - def initialize(build) - @build = build - end - - def to_s - lines = Array.new - lines.push("#{project.name} - ") - lines.push("Commit ##{commit.id}
") - lines.push("#{commit.short_sha} #{commit.git_author_name} - #{commit.git_commit_message}
") - lines.push("#{humanized_status(commit_status)} in #{commit.duration} second(s).") - lines.join('') - end - - def status_color(build_or_commit=nil) - build_or_commit ||= commit_status - case build_or_commit - when :success - 'green' - when :failed, :canceled - 'red' - else # :pending, :running or unknown - 'yellow' - end - end - - def notify? - [:failed, :canceled].include?(commit_status) - end - - - private - - def commit - build.commit - end - - def project - commit.project - end - - def build_status - build.status.to_sym - end - - def commit_status - commit.status.to_sym - end - - def humanized_status(build_or_commit=nil) - build_or_commit ||= commit_status - case build_or_commit - when :pending - "Pending" - when :running - "Running" - when :failed - "Failed" - when :success - "Successful" - when :canceled - "Canceled" - else - "Unknown" - end - end - end -end diff --git a/app/models/project_services/ci/hip_chat_service.rb b/app/models/project_services/ci/hip_chat_service.rb deleted file mode 100644 index 0df03890ef..0000000000 --- a/app/models/project_services/ci/hip_chat_service.rb +++ /dev/null @@ -1,93 +0,0 @@ -# == Schema Information -# -# Table name: ci_services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -module Ci - class HipChatService < Ci::Service - prop_accessor :hipchat_token, :hipchat_room, :hipchat_server - boolean_accessor :notify_only_broken_builds - validates :hipchat_token, presence: true, if: :activated? - validates :hipchat_room, presence: true, if: :activated? - default_value_for :notify_only_broken_builds, true - - def title - "HipChat" - end - - def description - "Private group chat, video chat, instant messaging for teams" - end - - def help - end - - def to_param - 'hip_chat' - end - - def fields - [ - { type: 'text', name: 'hipchat_token', label: 'Token', placeholder: '' }, - { type: 'text', name: 'hipchat_room', label: 'Room', placeholder: '' }, - { type: 'text', name: 'hipchat_server', label: 'Server', placeholder: 'https://hipchat.example.com', help: 'Leave blank for default' }, - { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' } - ] - end - - def can_execute?(build) - return if build.allow_failure? - - commit = build.commit - return unless commit - return unless commit.latest_builds.include? build - - case commit.status.to_sym - when :failed - true - when :success - true unless notify_only_broken_builds? - else - false - end - end - - def execute(build) - msg = Ci::HipChatMessage.new(build) - opts = default_options.merge( - token: hipchat_token, - room: hipchat_room, - server: server_url, - color: msg.status_color, - notify: msg.notify? - ) - Ci::HipChatNotifierWorker.perform_async(msg.to_s, opts) - end - - private - - def default_options - { - service_name: 'GitLab CI', - message_format: 'html' - } - end - - def server_url - if hipchat_server.blank? - 'https://api.hipchat.com' - else - hipchat_server - end - end - end -end diff --git a/app/models/project_services/ci/mail_service.rb b/app/models/project_services/ci/mail_service.rb deleted file mode 100644 index bb961d0697..0000000000 --- a/app/models/project_services/ci/mail_service.rb +++ /dev/null @@ -1,84 +0,0 @@ -# == Schema Information -# -# Table name: ci_services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -module Ci - class MailService < Ci::Service - delegate :email_recipients, :email_recipients=, - :email_add_pusher, :email_add_pusher=, - :email_only_broken_builds, :email_only_broken_builds=, to: :project, prefix: false - - before_save :update_project - - default_value_for :active, true - - def title - 'Mail' - end - - def description - 'Email notification' - end - - def to_param - 'mail' - end - - def fields - [ - { type: 'text', name: 'email_recipients', label: 'Recipients', help: 'Whitespace-separated list of recipient addresses' }, - { type: 'checkbox', name: 'email_add_pusher', label: 'Add pusher to recipients list' }, - { type: 'checkbox', name: 'email_only_broken_builds', label: 'Notify only broken builds' } - ] - end - - def can_execute?(build) - return if build.allow_failure? - - # it doesn't make sense to send emails for retried builds - commit = build.commit - return unless commit - return unless commit.latest_builds.include?(build) - - case build.status.to_sym - when :failed - true - when :success - true unless email_only_broken_builds - else - false - end - end - - def execute(build) - build.project_recipients.each do |recipient| - case build.status.to_sym - when :success - mailer.build_success_email(build.id, recipient).deliver_later - when :failed - mailer.build_fail_email(build.id, recipient).deliver_later - end - end - end - - private - - def update_project - project.save! - end - - def mailer - Ci::Notify - end - end -end diff --git a/app/models/project_services/ci/slack_message.rb b/app/models/project_services/ci/slack_message.rb deleted file mode 100644 index 1a6ff8e34c..0000000000 --- a/app/models/project_services/ci/slack_message.rb +++ /dev/null @@ -1,92 +0,0 @@ -require 'slack-notifier' - -module Ci - class SlackMessage - include Gitlab::Application.routes.url_helpers - - def initialize(commit) - @commit = commit - end - - def pretext - '' - end - - def color - attachment_color - end - - def fallback - format(attachment_message) - end - - def attachments - fields = [] - - commit.latest_builds.each do |build| - next if build.allow_failure? - next unless build.failed? - fields << { - title: build.name, - value: "Build <#{namespace_project_build_url(build.gl_project.namespace, build.gl_project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)." - } - end - - [{ - text: attachment_message, - color: attachment_color, - fields: fields - }] - end - - private - - attr_reader :commit - - def attachment_message - out = "<#{ci_project_url(project)}|#{project_name}>: " - out << "Commit <#{builds_namespace_project_commit_url(commit.gl_project.namespace, commit.gl_project, commit.sha)}|\##{commit.id}> " - out << "(<#{commit_sha_link}|#{commit.short_sha}>) " - out << "of <#{commit_ref_link}|#{commit.ref}> " - out << "by #{commit.git_author_name} " if commit.git_author_name - out << "#{commit_status} in " - out << "#{commit.duration} second(s)" - end - - def format(string) - Slack::Notifier::LinkFormatter.format(string) - end - - def project - commit.project - end - - def project_name - project.name - end - - def commit_sha_link - "#{project.gitlab_url}/commit/#{commit.sha}" - end - - def commit_ref_link - "#{project.gitlab_url}/commits/#{commit.ref}" - end - - def attachment_color - if commit.success? - 'good' - else - 'danger' - end - end - - def commit_status - if commit.success? - 'succeeded' - else - 'failed' - end - end - end -end diff --git a/app/models/project_services/ci/slack_service.rb b/app/models/project_services/ci/slack_service.rb deleted file mode 100644 index 7064bfe78d..0000000000 --- a/app/models/project_services/ci/slack_service.rb +++ /dev/null @@ -1,81 +0,0 @@ -# == Schema Information -# -# Table name: ci_services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -module Ci - class SlackService < Ci::Service - prop_accessor :webhook - boolean_accessor :notify_only_broken_builds - validates :webhook, presence: true, if: :activated? - - default_value_for :notify_only_broken_builds, true - - def title - 'Slack' - end - - def description - 'A team communication tool for the 21st century' - end - - def to_param - 'slack' - end - - def help - 'Visit https://www.slack.com/services/new/incoming-webhook. Then copy link and save project!' unless webhook.present? - end - - def fields - [ - { type: 'text', name: 'webhook', label: 'Webhook URL', placeholder: '' }, - { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' } - ] - end - - def can_execute?(build) - return if build.allow_failure? - - commit = build.commit - return unless commit - return unless commit.latest_builds.include?(build) - - case commit.status.to_sym - when :failed - true - when :success - true unless notify_only_broken_builds? - else - false - end - end - - def execute(build) - message = Ci::SlackMessage.new(build.commit) - options = default_options.merge( - color: message.color, - fallback: message.fallback, - attachments: message.attachments - ) - Ci::SlackNotifierWorker.perform_async(webhook, message.pretext, options) - end - - private - - def default_options - { - username: 'GitLab CI' - } - end - end -end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index af2840a57f..6f96907ec1 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -22,6 +22,7 @@ class HipchatService < Service MAX_COMMITS = 3 prop_accessor :token, :room, :server, :notify, :color, :api_version + boolean_accessor :notify_only_broken_builds validates :token, presence: true, if: :activated? def title @@ -45,12 +46,13 @@ class HipchatService < Service { type: 'text', name: 'api_version', placeholder: 'Leave blank for default (v2)' }, { type: 'text', name: 'server', - placeholder: 'Leave blank for default. https://hipchat.example.com' } + placeholder: 'Leave blank for default. https://hipchat.example.com' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, ] end def supported_events - %w(push issue merge_request note tag_push) + %w(push issue merge_request note tag_push build) end def execute(data) @@ -94,6 +96,8 @@ class HipchatService < Service create_merge_request_message(data) unless is_update?(data) when "note" create_note_message(data) + when "build" + create_build_message(data) if should_build_be_notified?(data) end end @@ -235,6 +239,20 @@ class HipchatService < Service message end + def create_build_message(data) + ref_type = data[:tag] ? 'tag' : 'branch' + ref = data[:ref] + sha = data[:sha] + user_name = data[:commit][:author_name] + status = data[:commit][:status] + duration = data[:commit][:duration] + + branch_link = "#{ref}" + commit_link = "#{Commit.truncate_sha(sha)}" + + "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)" + end + def project_name project.name_with_namespace.gsub(/\s/, '') end @@ -250,4 +268,24 @@ class HipchatService < Service def is_update?(data) data[:object_attributes][:action] == 'update' end + + def humanized_status(status) + case status + when 'success' + 'passed' + else + status + end + end + + def should_build_be_notified?(data) + case data[:commit][:status] + when 'success' + !notify_only_broken_builds? + when 'failed' + true + else + false + end + end end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 7cd5e89250..3581949157 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -20,6 +20,7 @@ class SlackService < Service prop_accessor :webhook, :username, :channel + boolean_accessor :notify_only_broken_builds validates :webhook, presence: true, if: :activated? def title @@ -45,12 +46,13 @@ class SlackService < Service { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, { type: 'text', name: 'username', placeholder: 'username' }, - { type: 'text', name: 'channel', placeholder: '#channel' } + { type: 'text', name: 'channel', placeholder: '#channel' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, ] end def supported_events - %w(push issue merge_request note tag_push) + %w(push issue merge_request note tag_push build) end def execute(data) @@ -78,6 +80,8 @@ class SlackService < Service MergeMessage.new(data) unless is_update?(data) when "note" NoteMessage.new(data) + when "build" + BuildMessage.new(data) if should_build_be_notified?(data) end opt = {} @@ -86,7 +90,7 @@ class SlackService < Service if message notifier = Slack::Notifier.new(webhook, opt) - notifier.ping(message.pretext, attachments: message.attachments) + notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) end end @@ -103,9 +107,21 @@ class SlackService < Service def is_update?(data) data[:object_attributes][:action] == 'update' end + + def should_build_be_notified?(data) + case data[:commit][:status] + when 'success' + !notify_only_broken_builds? + when 'failed' + true + else + false + end + end end require "slack_service/issue_message" require "slack_service/push_message" require "slack_service/merge_message" require "slack_service/note_message" +require "slack_service/build_message" diff --git a/app/models/project_services/slack_service/base_message.rb b/app/models/project_services/slack_service/base_message.rb index aa00d6061a..f118282468 100644 --- a/app/models/project_services/slack_service/base_message.rb +++ b/app/models/project_services/slack_service/base_message.rb @@ -10,6 +10,9 @@ class SlackService format(message) end + def fallback + end + def attachments raise NotImplementedError end diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/slack_service/build_message.rb new file mode 100644 index 0000000000..c124cad4af --- /dev/null +++ b/app/models/project_services/slack_service/build_message.rb @@ -0,0 +1,82 @@ +class SlackService + class BuildMessage < BaseMessage + attr_reader :sha + attr_reader :ref_type + attr_reader :ref + attr_reader :status + attr_reader :project_name + attr_reader :project_url + attr_reader :user_name + attr_reader :duration + + def initialize(params, commit = true) + @sha = params[:sha] + @ref_type = params[:tag] ? 'tag' : 'branch' + @ref = params[:ref] + @project_name = params[:project_name] + @project_url = params[:project_url] + @status = params[:commit][:status] + @user_name = params[:commit][:author_name] + @duration = params[:commit][:duration] + end + + def pretext + '' + end + + def fallback + format(message) + end + + def attachments + [{ text: format(message), color: attachment_color }] + end + + private + + def message + "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} second(s)" + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def humanized_status + case status + when 'success' + 'passed' + else + status + end + end + + def attachment_color + if status == 'success' + 'good' + else + 'danger' + end + end + + def branch_url + "#{project_url}/commits/#{ref}" + end + + def branch_link + "[#{ref}](#{branch_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def commit_url + "#{project_url}/commit/#{sha}/builds" + end + + def commit_link + "[#{Commit.truncate_sha(sha)}](#{commit_url})" + end + end +end diff --git a/app/models/service.rb b/app/models/service.rb index d610abd168..195c4690e8 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -30,6 +30,7 @@ class Service < ActiveRecord::Base default_value_for :merge_requests_events, true default_value_for :tag_push_events, true default_value_for :note_events, true + default_value_for :build_events, true after_initialize :initialize_properties @@ -47,6 +48,7 @@ class Service < ActiveRecord::Base scope :issue_hooks, -> { where(issues_events: true, active: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) } scope :note_hooks, -> { where(note_events: true, active: true) } + scope :build_hooks, -> { where(build_events: true, active: true) } def activated? active @@ -133,6 +135,21 @@ class Service < ActiveRecord::Base end end + # Provide convenient boolean accessor methods + # for each serialized property. + # Also keep track of updated properties in a similar way as ActiveModel::Dirty + def self.boolean_accessor(*args) + self.prop_accessor(*args) + + args.each do |arg| + class_eval %{ + def #{arg}? + ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg}) + end + } + end + end + # Returns a hash of the properties that have been assigned a new value since last save, # indicating their original values (attr => original value). # ActiveRecord does not provide a mechanism to track changes in serialized keys, @@ -163,6 +180,7 @@ class Service < ActiveRecord::Base assembla bamboo buildkite + builds_email campfire custom_issue_tracker drone_ci diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index 912eb6258a..847db2d48a 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -31,7 +31,8 @@ module Ci trigger_request: trigger_request, user: user) - commit.builds.create!(build_attrs) + build = commit.builds.create!(build_attrs) + build.execute_hooks end end end diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index f0b3f27b62..4b32c631d5 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -50,18 +50,8 @@ = icon('retweet fw') %span Triggers - = nav_link path: 'ci_web_hooks#index' do - = link_to namespace_project_ci_web_hooks_path(@project.namespace, @project), title: 'CI Web Hooks' do - = icon('link fw') - %span - CI Web Hooks = nav_link path: 'ci_settings#edit' do = link_to edit_namespace_project_ci_settings_path(@project.namespace, @project), title: 'CI Settings' do = icon('building fw') %span CI Settings - = nav_link controller: 'ci_services' do - = link_to namespace_project_ci_services_path(@project.namespace, @project), title: 'CI Services' do - = icon('share fw') - %span - CI Services diff --git a/app/views/ci/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml similarity index 96% rename from app/views/ci/notify/build_fail_email.html.haml rename to app/views/notify/build_fail_email.html.haml index de6291aa91..3b251dc011 100644 --- a/app/views/ci/notify/build_fail_email.html.haml +++ b/app/views/notify/build_fail_email.html.haml @@ -1,6 +1,6 @@ - content_for :header do %h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} - GitLab CI (build failed) + GitLab (build failed) %h3 Project: = link_to ci_project_url(@project) do diff --git a/app/views/ci/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb similarity index 100% rename from app/views/ci/notify/build_fail_email.text.erb rename to app/views/notify/build_fail_email.text.erb diff --git a/app/views/ci/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml similarity index 95% rename from app/views/ci/notify/build_success_email.html.haml rename to app/views/notify/build_success_email.html.haml index 6ef1fd67d8..c23f4b7e45 100644 --- a/app/views/ci/notify/build_success_email.html.haml +++ b/app/views/notify/build_success_email.html.haml @@ -1,6 +1,6 @@ - content_for :header do %h1{style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} - GitLab CI (build successful) + GitLab (build successful) %h3 Project: diff --git a/app/views/ci/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb similarity index 100% rename from app/views/ci/notify/build_success_email.text.erb rename to app/views/notify/build_success_email.text.erb diff --git a/app/views/projects/ci_services/_form.html.haml b/app/views/projects/ci_services/_form.html.haml deleted file mode 100644 index 397832e56d..0000000000 --- a/app/views/projects/ci_services/_form.html.haml +++ /dev/null @@ -1,54 +0,0 @@ -%h3.page-title - = @service.title - = boolean_to_icon @service.activated? - -%p= @service.description - - -%hr - -= form_for(@service, as: :service, url: namespace_project_ci_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f| - - if @service.errors.any? - .alert.alert-danger - %ul - - @service.errors.full_messages.each do |msg| - %li= msg - - - if @service.help.present? - .bs-callout - = @service.help - - .form-group - = f.label :active, "Active", class: "control-label" - .col-sm-10 - = f.check_box :active - - - @service.fields.each do |field| - - name = field[:name] - - label = field[:label] || name - - value = @service.send(name) - - type = field[:type] - - placeholder = field[:placeholder] - - choices = field[:choices] - - default_choice = field[:default_choice] - - help = field[:help] - - .form-group - = f.label label, class: "control-label" - .col-sm-10 - - if type == 'text' - = f.text_field name, class: "form-control", placeholder: placeholder - - elsif type == 'textarea' - = f.text_area name, rows: 5, class: "form-control", placeholder: placeholder - - elsif type == 'checkbox' - = f.check_box name - - elsif type == 'select' - = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } - - if help - .light #{help} - - .form-actions - = f.submit 'Save', class: 'btn btn-save' -   - - if @service.valid? && @service.activated? && @service.can_test? - = link_to 'Test settings', test_namespace_project_ci_service_path(@project.namespace, @project, @service.to_param), class: 'btn' diff --git a/app/views/projects/ci_services/edit.html.haml b/app/views/projects/ci_services/edit.html.haml deleted file mode 100644 index dacb6b4f6f..0000000000 --- a/app/views/projects/ci_services/edit.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -- page_title @service.title, "CI Services" -= render 'form' diff --git a/app/views/projects/ci_services/index.html.haml b/app/views/projects/ci_services/index.html.haml deleted file mode 100644 index 3f26c7851d..0000000000 --- a/app/views/projects/ci_services/index.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -- page_title "CI Services" -%h3.page-title Project services -%p.light Project services allow you to integrate GitLab CI with other applications - -%table.table - %thead - %tr - %th - %th Service - %th Description - %th Last edit - - @services.sort_by(&:title).each do |service| - %tr - %td - = boolean_to_icon service.activated? - %td - = link_to edit_namespace_project_ci_service_path(@project.namespace, @project, service.to_param) do - %strong= service.title - %td - = service.description - %td.light - = time_ago_in_words service.updated_at - ago diff --git a/app/views/projects/ci_web_hooks/index.html.haml b/app/views/projects/ci_web_hooks/index.html.haml deleted file mode 100644 index 2998fb08ff..0000000000 --- a/app/views/projects/ci_web_hooks/index.html.haml +++ /dev/null @@ -1,94 +0,0 @@ -- page_title "CI Web Hooks" -%h3.page-title - CI Web hooks - -%p.light - Web Hooks can be used for binding events when build completed. - -%hr.clearfix - -= form_for @web_hook, url: namespace_project_ci_web_hooks_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| - -if @web_hook.errors.any? - .alert.alert-danger - - @web_hook.errors.full_messages.each do |msg| - %p= msg - .form-group - = f.label :url, "URL", class: 'control-label' - .col-sm-10 - = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json' - .form-actions - = f.submit "Add Web Hook", class: "btn btn-create" - --if @web_hooks.any? - %h4 Activated web hooks (#{@web_hooks.count}) - .table-holder - %table.table - - @web_hooks.each do |hook| - %tr - %td - .clearfix - %span.monospace= hook.url - %td - .pull-right - - if @ci_project.commits.any? - = link_to 'Test Hook', test_namespace_project_ci_web_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped" - = link_to 'Remove', namespace_project_ci_web_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" - -%h4 Web Hook data example - -:erb -

-    
-      {
-        "build_id": 2,
-        "build_name":"rspec_linux"
-        "build_status": "failed",
-        "build_started_at": "2014-05-05T18:01:02.563Z",
-        "build_finished_at": "2014-05-05T18:01:07.611Z",
-        "project_id": 1,
-        "project_name": "Brightbox \/ Brightbox Cli",
-        "gitlab_url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli",
-        "ref": "master",
-        "sha": "a26cf5de9ed9827746d4970872376b10d9325f40",
-        "before_sha": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
-        "push_data": {
-          "before": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
-          "after": "a26cf5de9ed9827746d4970872376b10d9325f40",
-          "ref": "refs\/heads\/master",
-          "user_id": 1,
-          "user_name": "Administrator",
-          "project_id": 5,
-          "repository": {
-            "name": "Brightbox Cli",
-            "url": "dzaporozhets@localhost:brightbox\/brightbox-cli.git",
-            "description": "Voluptatibus quae error consectetur voluptas dolores vel excepturi possimus.",
-            "homepage": "http:\/\/localhost:3000\/brightbox\/brightbox-cli"
-          },
-          "commits": [
-            {
-              "id": "a26cf5de9ed9827746d4970872376b10d9325f40",
-              "message": "Release v1.2.2",
-              "timestamp": "2014-04-22T16:46:42+03:00",
-              "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/a26cf5de9ed9827746d4970872376b10d9325f40",
-              "author": {
-                "name": "Paul Thornthwaite",
-                "email": "tokengeek@gmail.com"
-              }
-            },
-            {
-              "id": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
-              "message": "Fix server user data update\n\nIncorrect condition was being used so Base64 encoding option was having\nopposite effect from desired.",
-              "timestamp": "2014-04-11T18:17:26+03:00",
-              "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/34f57f6ba3ed0c21c5e361bbb041c3591411176c",
-              "author": {
-                "name": "Paul Thornthwaite",
-                "email": "tokengeek@gmail.com"
-              }
-            }
-          ],
-          "total_commits_count": 2,
-          "ci_yaml_file":"rspec_linux:\r\n  script: ls\r\n"
-        }
-      }
-    
-  
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 3702aeaecb..b18d9197d0 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -55,6 +55,13 @@ %strong Merge Request events %p.light This url will be triggered when a merge request is created + %div + = f.check_box :build_events, class: 'pull-left' + .prepend-left-20 + = f.label :build_events, class: 'list-label' do + %strong Build events + %p.light + This url will be triggered when the build status changes .form-group = f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox' .col-sm-10 @@ -78,7 +85,7 @@ .clearfix %span.monospace= hook.url %p - - %w(push_events tag_push_events issues_events note_events merge_requests_events).each do |trigger| + - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray= trigger.titleize SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 16a98a7233..28d6f421fe 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -59,6 +59,15 @@ %strong Merge Request events %p.light This url will be triggered when a merge request is created + - if @service.supported_events.include?("build") + %div + = form.check_box :build_events, class: 'pull-left' + .prepend-left-20 + = form.label :build_events, class: 'list-label' do + %strong Build events + %p.light + This url will be triggered when a build status changes + - @service.fields.each do |field| - type = field[:type] diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb new file mode 100644 index 0000000000..c5c8055bea --- /dev/null +++ b/app/workers/build_email_worker.rb @@ -0,0 +1,19 @@ +class BuildEmailWorker + include Sidekiq::Worker + + def perform(build_id, recipients, push_data) + recipients.split(' ').each do |recipient| + begin + case push_data['build_status'] + when 'success' + Notify.build_success_email(build_id, recipient).deliver_now + when 'failed' + Notify.build_fail_email(build_id, recipient).deliver_now + end + # These are input errors and won't be corrected even if Sidekiq retries + rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e + logger.info("Failed to send e-mail for project '#{push_data['project_name']}' to #{recipient}: #{e}") + end + end + end +end diff --git a/app/workers/ci/hip_chat_notifier_worker.rb b/app/workers/ci/hip_chat_notifier_worker.rb deleted file mode 100644 index ebb43570e2..0000000000 --- a/app/workers/ci/hip_chat_notifier_worker.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Ci - class HipChatNotifierWorker - include Sidekiq::Worker - - def perform(message, options={}) - room = options.delete('room') - token = options.delete('token') - server = options.delete('server') - name = options.delete('service_name') - client_opts = { - api_version: 'v2', - server_url: server - } - - client = HipChat::Client.new(token, client_opts) - client[room].send(name, message, options.symbolize_keys) - end - end -end diff --git a/app/workers/ci/slack_notifier_worker.rb b/app/workers/ci/slack_notifier_worker.rb deleted file mode 100644 index 3bbb9b4bec..0000000000 --- a/app/workers/ci/slack_notifier_worker.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Ci - class SlackNotifierWorker - include Sidekiq::Worker - - def perform(webhook_url, message, options={}) - notifier = Slack::Notifier.new(webhook_url) - notifier.ping(message, options) - end - end -end diff --git a/app/workers/ci/web_hook_worker.rb b/app/workers/ci/web_hook_worker.rb deleted file mode 100644 index 0bb8384557..0000000000 --- a/app/workers/ci/web_hook_worker.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Ci - class WebHookWorker - include Sidekiq::Worker - - def perform(hook_id, data) - Ci::WebHook.find(hook_id).execute data - end - end -end diff --git a/config/routes.rb b/config/routes.rb index 061a8fd5da..a104e686ac 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -596,17 +596,6 @@ Rails.application.routes.draw do resource :variables, only: [:show, :update] resources :triggers, only: [:index, :create, :destroy] resource :ci_settings, only: [:edit, :update, :destroy] - resources :ci_web_hooks, only: [:index, :create, :destroy] do - member do - get :test - end - end - - resources :ci_services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do - member do - get :test - end - end resources :builds, only: [:index, :show] do collection do diff --git a/db/migrate/20151203162134_add_build_events_to_services.rb b/db/migrate/20151203162134_add_build_events_to_services.rb new file mode 100644 index 0000000000..a84be7db3f --- /dev/null +++ b/db/migrate/20151203162134_add_build_events_to_services.rb @@ -0,0 +1,6 @@ +class AddBuildEventsToServices < ActiveRecord::Migration + def up + add_column :services, :build_events, :boolean, default: false, null: false + add_column :web_hooks, :build_events, :boolean, default: false, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 94b87040d8..58595f8ae5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151203162133) do +ActiveRecord::Schema.define(version: 20151203162134) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -706,6 +706,7 @@ ActiveRecord::Schema.define(version: 20151203162133) do t.boolean "merge_requests_events", default: true t.boolean "tag_push_events", default: true t.boolean "note_events", default: true, null: false + t.boolean "build_events", default: false, null: false end add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree @@ -854,6 +855,7 @@ ActiveRecord::Schema.define(version: 20151203162133) do t.boolean "tag_push_events", default: false t.boolean "note_events", default: false, null: false t.boolean "enable_ssl_verification", default: true + t.boolean "build_events", default: false, null: false end add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree diff --git a/features/project/service.feature b/features/project/service.feature index 5014b52b9f..13edc6cb2b 100644 --- a/features/project/service.feature +++ b/features/project/service.feature @@ -55,11 +55,11 @@ Feature: Project Services And I fill Pushover settings Then I should see Pushover service settings saved - Scenario: Activate email on push service + Scenario: Activate email service When I visit project "Shop" services page - And I click email on push service link - And I fill email on push settings - Then I should see email on push service settings saved + And I click email service link + And I fill email settings + Then I should see email service settings saved Scenario: Activate Irker (IRC Gateway) service When I visit project "Shop" services page diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb index 6acbf46eb2..037f7494a7 100644 --- a/features/steps/admin/settings.rb +++ b/features/steps/admin/settings.rb @@ -32,6 +32,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps page.check('Comments') page.check('Issues events') page.check('Merge Request events') + page.check('Build events') click_on 'Save' end @@ -39,6 +40,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps fill_in 'Webhook', with: 'http://localhost' fill_in 'Username', with: 'test_user' fill_in 'Channel', with: '#test_channel' + page.check('Notify only broken builds') end step 'I should see service template settings saved' do diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 1c700df0c6..2d564dac49 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -118,16 +118,16 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps expect(find_field('Restrict to branch').value).to eq 'master' end - step 'I click email on push service link' do - click_link 'Emails on push' + step 'I click email service link' do + click_link 'Emails' end - step 'I fill email on push settings' do + step 'I fill email settings' do fill_in 'Recipients', with: 'qa@company.name' click_button 'Save' end - step 'I should see email on push service settings saved' do + step 'I should see email service settings saved' do expect(find_field('Recipients').value).to eq 'qa@company.name' end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 81bf7a8222..03e3056a87 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -45,7 +45,8 @@ module API class ProjectHook < Hook expose :project_id, :push_events - expose :issues_events, :merge_requests_events, :tag_push_events, :note_events, :enable_ssl_verification + expose :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events + expose :enable_ssl_verification end class ForkedFromProject < Grape::Entity @@ -252,7 +253,7 @@ module API class ProjectService < Grape::Entity expose :id, :title, :created_at, :updated_at, :active - expose :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events + expose :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events # Expose serialized properties expose :properties do |service, options| field_names = service.fields. diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 882d1a083a..cf9938d25a 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -45,6 +45,7 @@ module API :merge_requests_events, :tag_push_events, :note_events, + :build_events, :enable_ssl_verification ] @hook = user_project.hooks.new(attrs) @@ -77,6 +78,7 @@ module API :merge_requests_events, :tag_push_events, :note_events, + :build_events, :enable_ssl_verification ] diff --git a/lib/ci/api/projects.rb b/lib/ci/api/projects.rb index d719ad9e8d..23c79f3879 100644 --- a/lib/ci/api/projects.rb +++ b/lib/ci/api/projects.rb @@ -5,30 +5,6 @@ module Ci before { authenticate! } resource :projects do - # Register new webhook for project - # - # Parameters - # project_id (required) - The ID of a project - # web_hook (required) - WebHook URL - # Example Request - # POST /projects/:project_id/webhooks - post ":project_id/webhooks" do - required_attributes! [:web_hook] - - project = Ci::Project.find(params[:project_id]) - - unauthorized! unless can?(current_user, :admin_project, project.gl_project) - - web_hook = project.web_hooks.new({ url: params[:web_hook] }) - - if web_hook.save - present web_hook, with: Entities::WebHook - else - errors = web_hook.errors.full_messages.join(", ") - render_api_error!(errors, 400) - end - end - # Retrieve all Gitlab CI projects that the user has access to # # Example Request: @@ -121,20 +97,6 @@ module Ci end end - # Remove a Gitlab CI project - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # DELETE /projects/:id - delete ":id" do - project = Ci::Project.find(params[:id]) - - unauthorized! unless can?(current_user, :admin_project, project.gl_project) - - project.destroy - end - # Link a Gitlab CI project to a runner # # Parameters: diff --git a/lib/gitlab/build_data_builder.rb b/lib/gitlab/build_data_builder.rb new file mode 100644 index 0000000000..fa2cd551ce --- /dev/null +++ b/lib/gitlab/build_data_builder.rb @@ -0,0 +1,64 @@ +module Gitlab + class BuildDataBuilder + class << self + def build(build) + project = build.gl_project + commit = build.commit + user = build.user + + data = { + object_kind: 'build', + + ref: build.ref, + tag: build.tag, + before_sha: build.before_sha, + sha: build.sha, + + # TODO: should this be not prefixed with build_? + # Leaving this way to have backward compatibility + build_id: build.id, + build_name: build.name, + build_stage: build.stage, + build_status: build.status, + build_started_at: build.started_at, + build_finished_at: build.finished_at, + build_duration: build.duration, + + # TODO: do we still need it? + project_id: project.id, + project_name: project.name_with_namespace, + + user: { + id: user.try(:id), + name: user.try(:name), + email: user.try(:email), + }, + + commit: { + id: commit.id, + sha: commit.sha, + message: commit.git_commit_message, + author_name: commit.git_author_name, + author_email: commit.git_author_email, + status: commit.status, + duration: commit.duration, + started_at: commit.started_at, + finished_at: commit.finished_at, + }, + + repository: { + name: project.name, + url: project.url_to_repo, + description: project.description, + homepage: project.web_url, + git_http_url: project.http_url_to_repo, + git_ssh_url: project.ssh_url_to_repo, + visibility_level: project.visibility_level + }, + } + + data + end + end + end +end diff --git a/spec/factories/ci/web_hook.rb b/spec/factories/ci/web_hook.rb deleted file mode 100644 index 40d878ecb3..0000000000 --- a/spec/factories/ci/web_hook.rb +++ /dev/null @@ -1,6 +0,0 @@ -FactoryGirl.define do - factory :ci_web_hook, class: Ci::WebHook do - sequence(:url) { FFaker::Internet.uri('http') } - project factory: :ci_project - end -end diff --git a/spec/features/ci_web_hooks_spec.rb b/spec/features/ci_web_hooks_spec.rb deleted file mode 100644 index efae0a42c1..0000000000 --- a/spec/features/ci_web_hooks_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'spec_helper' - -describe 'CI web hooks' do - let(:user) { create(:user) } - before { login_as(user) } - - before do - @project = FactoryGirl.create :ci_project - @gl_project = @project.gl_project - @gl_project.team << [user, :master] - visit namespace_project_ci_web_hooks_path(@gl_project.namespace, @gl_project) - end - - context 'create a trigger' do - before do - fill_in 'web_hook_url', with: 'http://example.com' - click_on 'Add Web Hook' - end - - it { expect(@project.web_hooks.count).to eq(1) } - - it 'revokes the trigger' do - click_on 'Remove' - expect(@project.web_hooks.count).to eq(0) - end - end -end diff --git a/spec/lib/gitlab/build_data_builder_spec.rb b/spec/lib/gitlab/build_data_builder_spec.rb new file mode 100644 index 0000000000..af2de207eb --- /dev/null +++ b/spec/lib/gitlab/build_data_builder_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe 'Gitlab::BuildDataBuilder' do + let(:build) { create(:ci_build) } + + describe :build do + let(:data) do + Gitlab::BuildDataBuilder.build(build) + end + + it { expect(data).to be_a(Hash) } + it { expect(data[:ref]).to eq(build.ref) } + it { expect(data[:sha]).to eq(build.sha) } + it { expect(data[:tag]).to eq(build.tag) } + it { expect(data[:build_id]).to eq(build.id) } + it { expect(data[:build_status]).to eq(build.status) } + it { expect(data[:project_id]).to eq(build.gl_project.id) } + it { expect(data[:project_name]).to eq(build.gl_project.name_with_namespace) } + end +end diff --git a/spec/mailers/ci/notify_spec.rb b/spec/mailers/ci/notify_spec.rb deleted file mode 100644 index b83fb41603..0000000000 --- a/spec/mailers/ci/notify_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'spec_helper' - -describe Ci::Notify do - include EmailSpec::Helpers - include EmailSpec::Matchers - - before do - @commit = FactoryGirl.create :ci_commit - @build = FactoryGirl.create :ci_build, commit: @commit - end - - describe 'build success' do - subject { Ci::Notify.build_success_email(@build.id, 'wow@example.com') } - - it 'has the correct subject' do - should have_subject /Build success for/ - end - - it 'contains name of project' do - should have_body_text /build successful/ - end - end - - describe 'build fail' do - subject { Ci::Notify.build_fail_email(@build.id, 'wow@example.com') } - - it 'has the correct subject' do - should have_subject /Build failed for/ - end - - it 'contains name of project' do - should have_body_text /build failed/ - end - end -end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 27e509933b..154901a2fb 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -13,6 +13,7 @@ describe Notify do let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to } let(:recipient) { create(:user, email: 'recipient@example.com') } let(:project) { create(:project) } + let(:build) { create(:ci_build) } before(:each) do ActionMailer::Base.deliveries.clear @@ -865,4 +866,32 @@ describe Notify do is_expected.to have_body_text /#{diff_path}/ end end + + describe 'build success' do + before { build.success } + + subject { Notify.build_success_email(build.id, 'wow@example.com') } + + it 'has the correct subject' do + should have_subject /Build success for/ + end + + it 'contains name of project' do + should have_body_text build.project_name + end + end + + describe 'build fail' do + before { build.drop } + + subject { Notify.build_fail_email(build.id, 'wow@example.com') } + + it 'has the correct subject' do + should have_subject /Build failed for/ + end + + it 'contains name of project' do + should have_body_text build.project_name + end + end end diff --git a/spec/models/ci/project_services/hip_chat_message_spec.rb b/spec/models/ci/project_services/hip_chat_message_spec.rb deleted file mode 100644 index 7d54b6cf84..0000000000 --- a/spec/models/ci/project_services/hip_chat_message_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'spec_helper' - -describe Ci::HipChatMessage, models: true do - subject { Ci::HipChatMessage.new(build) } - - let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) } - - let(:build) do - commit.builds.first - end - - context 'when all matrix builds succeed' do - it 'returns a successful message' do - commit.create_builds('master', false, nil) - commit.builds.update_all(status: "success") - commit.reload - - expect(subject.status_color).to eq 'green' - expect(subject.notify?).to be_falsey - expect(subject.to_s).to match(/Commit #\d+/) - expect(subject.to_s).to match(/Successful in \d+ second\(s\)\./) - end - end - - context 'when at least one matrix build fails' do - it 'returns a failure message' do - commit.create_builds('master', false, nil) - first_build = commit.builds.first - second_build = commit.builds.last - first_build.update(status: "success") - second_build.update(status: "failed") - - expect(subject.status_color).to eq 'red' - expect(subject.notify?).to be_truthy - expect(subject.to_s).to match(/Commit #\d+/) - expect(subject.to_s).to match(/Failed in \d+ second\(s\)\./) - end - end -end diff --git a/spec/models/ci/project_services/hip_chat_service_spec.rb b/spec/models/ci/project_services/hip_chat_service_spec.rb deleted file mode 100644 index 714f1e17e0..0000000000 --- a/spec/models/ci/project_services/hip_chat_service_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - - -require 'spec_helper' - -describe Ci::HipChatService, models: true do - - describe "Validations" do - - context "active" do - before do - subject.active = true - end - - it { is_expected.to validate_presence_of :hipchat_room } - it { is_expected.to validate_presence_of :hipchat_token } - - end - end - - describe "Execute" do - - let(:service) { Ci::HipChatService.new } - let(:commit) { FactoryGirl.create :ci_commit } - let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' } - let(:api_url) { 'https://api.hipchat.com/v2/room/123/notification?auth_token=a1b2c3d4e5f6' } - - before do - allow(service).to receive_messages( - project: commit.project, - project_id: commit.project_id, - notify_only_broken_builds: false, - hipchat_room: 123, - hipchat_token: 'a1b2c3d4e5f6' - ) - - WebMock.stub_request(:post, api_url) - end - - - it "should call the HipChat API" do - service.execute(build) - Ci::HipChatNotifierWorker.drain - - expect( WebMock ).to have_requested(:post, api_url).once - end - - it "calls the worker with expected arguments" do - expect( Ci::HipChatNotifierWorker ).to receive(:perform_async) \ - .with(an_instance_of(String), hash_including( - token: 'a1b2c3d4e5f6', - room: 123, - server: 'https://api.hipchat.com', - color: 'red', - notify: true - )) - - service.execute(build) - end - end -end diff --git a/spec/models/ci/project_services/mail_service_spec.rb b/spec/models/ci/project_services/mail_service_spec.rb deleted file mode 100644 index 638d9a4a62..0000000000 --- a/spec/models/ci/project_services/mail_service_spec.rb +++ /dev/null @@ -1,177 +0,0 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -require 'spec_helper' - -describe Ci::MailService, models: true do - describe "Associations" do - it { is_expected.to belong_to :project } - end - - describe "Validations" do - context "active" do - before do - subject.active = true - end - end - end - - describe 'Sends email for' do - let(:mail) { Ci::MailService.new } - let(:user) { User.new(notification_email: 'git@example.com')} - - describe 'failed build' do - let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true) } - let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } - let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: 'failed', commit: commit, user: user) } - - before do - allow(mail).to receive_messages( - project: project - ) - end - - it do - perform_enqueued_jobs do - expect{ mail.execute(build) }.to change{ ActionMailer::Base.deliveries.size }.by(1) - expect(ActionMailer::Base.deliveries.last.to).to eq(["git@example.com"]) - end - end - end - - describe 'successfull build' do - let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true, email_only_broken_builds: false) } - let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } - let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) } - - before do - allow(mail).to receive_messages( - project: project - ) - end - - it do - perform_enqueued_jobs do - expect{ mail.execute(build) }.to change{ ActionMailer::Base.deliveries.size }.by(1) - expect(ActionMailer::Base.deliveries.last.to).to eq(["git@example.com"]) - end - end - end - - describe 'successfull build and project has email_recipients' do - let(:project) do - FactoryGirl.create(:ci_project, - email_add_pusher: true, - email_only_broken_builds: false, - email_recipients: "jeroen@example.com") - end - let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } - let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) } - - before do - allow(mail).to receive_messages( - project: project - ) - end - - it do - perform_enqueued_jobs do - expect{ mail.execute(build) }.to change{ ActionMailer::Base.deliveries.size }.by(2) - expect( - ActionMailer::Base.deliveries.map(&:to).flatten - ).to include("git@example.com", "jeroen@example.com") - end - end - end - - describe 'successful build and notify only broken builds' do - let(:project) do - FactoryGirl.create(:ci_project, - email_add_pusher: true, - email_only_broken_builds: true, - email_recipients: "jeroen@example.com") - end - let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } - let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) } - - before do - allow(mail).to receive_messages( - project: project - ) - end - - it do - perform_enqueued_jobs do - expect do - mail.execute(build) if mail.can_execute?(build) - end.to_not change{ ActionMailer::Base.deliveries.size } - end - end - end - - describe 'successful build and can test service' do - let(:project) do - FactoryGirl.create(:ci_project, - email_add_pusher: true, - email_only_broken_builds: false, - email_recipients: "jeroen@example.com") - end - let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } - let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) } - - before do - allow(mail).to receive_messages( - project: project - ) - build - end - - it do - expect(mail.can_test?).to eq(true) - end - end - - describe 'retried build should not receive email' do - let(:project) do - FactoryGirl.create(:ci_project, - email_add_pusher: true, - email_only_broken_builds: true, - email_recipients: "jeroen@example.com") - end - let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } - let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: 'failed', commit: commit, user: user) } - - before do - allow(mail).to receive_messages( - project: project - ) - end - - it do - Ci::Build.retry(build) - perform_enqueued_jobs do - expect do - mail.execute(build) if mail.can_execute?(build) - end.to_not change{ ActionMailer::Base.deliveries.size } - end - end - end - end -end diff --git a/spec/models/ci/project_services/slack_message_spec.rb b/spec/models/ci/project_services/slack_message_spec.rb deleted file mode 100644 index 226032b4cd..0000000000 --- a/spec/models/ci/project_services/slack_message_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'spec_helper' - -describe Ci::SlackMessage, models: true do - subject { Ci::SlackMessage.new(commit) } - - let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) } - - context 'when all matrix builds succeeded' do - let(:color) { 'good' } - - it 'returns a message with success' do - commit.create_builds('master', false, nil) - commit.builds.update_all(status: "success") - commit.reload - - expect(subject.color).to eq(color) - expect(subject.fallback).to include('Commit') - expect(subject.fallback).to include("\##{commit.id}") - expect(subject.fallback).to include('succeeded') - expect(subject.attachments.first[:fields]).to be_empty - end - end - - context 'when one of matrix builds failed' do - let(:color) { 'danger' } - - it 'returns a message with information about failed build' do - commit.create_builds('master', false, nil) - first_build = commit.builds.first - second_build = commit.builds.last - first_build.update(status: "success") - second_build.update(status: "failed") - - expect(subject.color).to eq(color) - expect(subject.fallback).to include('Commit') - expect(subject.fallback).to include("\##{commit.id}") - expect(subject.fallback).to include('failed') - expect(subject.attachments.first[:fields].size).to eq(1) - expect(subject.attachments.first[:fields].first[:title]).to eq(second_build.name) - expect(subject.attachments.first[:fields].first[:value]).to include("\##{second_build.id}") - end - end -end diff --git a/spec/models/ci/project_services/slack_service_spec.rb b/spec/models/ci/project_services/slack_service_spec.rb deleted file mode 100644 index e7d7d5d6f4..0000000000 --- a/spec/models/ci/project_services/slack_service_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -require 'spec_helper' - -describe Ci::SlackService, models: true do - describe "Associations" do - it { is_expected.to belong_to :project } - end - - describe "Validations" do - context "active" do - before do - subject.active = true - end - - it { is_expected.to validate_presence_of :webhook } - end - end - - describe "Execute" do - let(:slack) { Ci::SlackService.new } - let(:commit) { FactoryGirl.create :ci_commit } - let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' } - let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } - let(:notify_only_broken_builds) { false } - - before do - allow(slack).to receive_messages( - project: commit.project, - project_id: commit.project_id, - webhook: webhook_url, - notify_only_broken_builds: notify_only_broken_builds - ) - - WebMock.stub_request(:post, webhook_url) - end - - it "should call Slack API" do - slack.execute(build) - Ci::SlackNotifierWorker.drain - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end -end diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb index 346471aa9b..e358aa0274 100644 --- a/spec/models/ci/project_spec.rb +++ b/spec/models/ci/project_spec.rb @@ -34,11 +34,9 @@ describe Ci::Project, models: true do it { is_expected.to have_many(:runner_projects) } it { is_expected.to have_many(:runners) } - it { is_expected.to have_many(:web_hooks) } it { is_expected.to have_many(:events) } it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:triggers) } - it { is_expected.to have_many(:services) } it { is_expected.to validate_presence_of :timeout } it { is_expected.to validate_presence_of :gitlab_id } diff --git a/spec/models/ci/service_spec.rb b/spec/models/ci/service_spec.rb deleted file mode 100644 index 34e3af7f81..0000000000 --- a/spec/models/ci/service_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# == Schema Information -# -# Table name: ci_services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -require 'spec_helper' - -describe Ci::Service, models: true do - - describe "Associations" do - it { is_expected.to belong_to :project } - end - - describe "Mass assignment" do - end - - describe "Test Button" do - before do - @service = Ci::Service.new - end - - describe "Testable" do - let(:commit) { FactoryGirl.create :ci_commit } - let(:build) { FactoryGirl.create :ci_build, commit: commit } - - before do - allow(@service).to receive_messages( - project: commit.project - ) - build - @testable = @service.can_test? - end - - describe :can_test do - it { expect(@testable).to eq(true) } - end - end - end -end diff --git a/spec/models/ci/web_hook_spec.rb b/spec/models/ci/web_hook_spec.rb deleted file mode 100644 index 1a4edec9d4..0000000000 --- a/spec/models/ci/web_hook_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -# == Schema Information -# -# Table name: ci_web_hooks -# -# id :integer not null, primary key -# url :string(255) not null -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# - -require 'spec_helper' - -describe Ci::WebHook, models: true do - describe "Associations" do - it { is_expected.to belong_to :project } - end - - describe "Validations" do - it { is_expected.to validate_presence_of(:url) } - - context "url format" do - it { is_expected.to allow_value("http://example.com").for(:url) } - it { is_expected.to allow_value("https://excample.com").for(:url) } - it { is_expected.to allow_value("http://test.com/api").for(:url) } - it { is_expected.to allow_value("http://test.com/api?key=abc").for(:url) } - it { is_expected.to allow_value("http://test.com/api?key=abc&type=def").for(:url) } - - it { is_expected.not_to allow_value("example.com").for(:url) } - it { is_expected.not_to allow_value("ftp://example.com").for(:url) } - it { is_expected.not_to allow_value("herp-and-derp").for(:url) } - end - end - - describe "execute" do - before(:each) do - @web_hook = FactoryGirl.create(:ci_web_hook) - @project = @web_hook.project - @data = { before: 'oldrev', after: 'newrev', ref: 'ref' } - - WebMock.stub_request(:post, @web_hook.url) - end - - it "POSTs to the web hook URL" do - @web_hook.execute(@data) - expect(WebMock).to have_requested(:post, @web_hook.url).once - end - - it "POSTs the data as JSON" do - json = @data.to_json - - @web_hook.execute(@data) - expect(WebMock).to have_requested(:post, @web_hook.url).with(body: json).once - end - - it "catches exceptions" do - expect(Ci::WebHook).to receive(:post).and_raise("Some HTTP Post error") - - expect{ @web_hook.execute(@data) }. - to raise_error(RuntimeError, 'Some HTTP Post error') - end - end -end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index c96ab54814..a5662b08bd 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -247,6 +247,55 @@ describe HipchatService, models: true do end end + context 'build events' do + let(:build) { create(:ci_build) } + let(:data) { Gitlab::BuildDataBuilder.build(build) } + + context 'for failed' do + before { build.drop } + + it "should call Hipchat API" do + hipchat.execute(data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "should create a build message" do + message = hipchat.send(:create_build_message, data) + + project_url = project.web_url + project_name = project.name_with_namespace.gsub(/\s/, '') + sha = data[:sha] + ref = data[:ref] + ref_type = data[:tag] ? 'tag' : 'branch' + duration = data[:commit][:duration] + + expect(message).to eq("#{project_name}: " \ + "Commit #{Commit.truncate_sha(sha)} " \ + "of #{ref} #{ref_type} " \ + "by #{data[:commit][:author_name]} failed in #{duration} second(s)") + end + end + + context 'for succeeded' do + before do + build.success + end + + it "should call Hipchat API" do + hipchat.notify_only_broken_builds = false + hipchat.execute(data) + expect(WebMock).to have_requested(:post, api_url).once + end + + it "should notify only broken" do + hipchat.notify_only_broken_builds = true + hipchat.execute(data) + expect(WebMock).to_not have_requested(:post, api_url).once + end + end + end + context "#message_options" do it "should be set to the defaults" do expect(hipchat.send(:message_options)).to eq({ notify: false, color: 'yellow' }) diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/slack_service/build_message_spec.rb new file mode 100644 index 0000000000..d64c67da93 --- /dev/null +++ b/spec/models/project_services/slack_service/build_message_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe SlackService::BuildMessage do + subject { SlackService::BuildMessage.new(args) } + + let(:args) do + { + sha: '97de212e80737a608d939f648d959671fb0a0142', + ref: 'develop', + tag: false, + + project_name: 'project_name', + project_url: 'somewhere.com', + + commit: { + status: status, + author_name: 'hacker', + duration: 10, + }, + } + end + + context 'succeeded' do + let(:status) { 'success' } + let(:color) { 'good' } + + it 'returns a message with information about succeeded build' do + message = ': Commit of branch by hacker succeeded in 10 second(s)' + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end + + context 'failed' do + let(:status) { 'failed' } + let(:color) { 'danger' } + + it 'returns a message with information about failed build' do + message = ': Commit of branch by hacker failed in 10 second(s)' + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end +end diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 606b226ad7..142b637d29 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -1,11 +1,17 @@ require 'spec_helper' -describe API::API, 'ProjectHooks', api: true do +describe API::API, 'ProjectHooks', api: true do include ApiHelpers let(:user) { create(:user) } let(:user3) { create(:user) } let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let!(:hook) { create(:project_hook, project: project, url: "http://example.com", push_events: true, merge_requests_events: true, tag_push_events: true, issues_events: true, note_events: true, enable_ssl_verification: true) } + let!(:hook) do + create(:project_hook, + project: project, url: "http://example.com", + push_events: true, merge_requests_events: true, tag_push_events: true, + issues_events: true, note_events: true, build_events: true, + enable_ssl_verification: true) + end before do project.team << [user, :master] @@ -26,6 +32,7 @@ describe API::API, 'ProjectHooks', api: true do expect(json_response.first['merge_requests_events']).to eq(true) expect(json_response.first['tag_push_events']).to eq(true) expect(json_response.first['note_events']).to eq(true) + expect(json_response.first['build_events']).to eq(true) expect(json_response.first['enable_ssl_verification']).to eq(true) end end @@ -83,6 +90,7 @@ describe API::API, 'ProjectHooks', api: true do expect(json_response['merge_requests_events']).to eq(false) expect(json_response['tag_push_events']).to eq(false) expect(json_response['note_events']).to eq(false) + expect(json_response['build_events']).to eq(false) expect(json_response['enable_ssl_verification']).to eq(true) end diff --git a/spec/requests/ci/api/projects_spec.rb b/spec/requests/ci/api/projects_spec.rb index 893fd168d3..b1fbdac597 100644 --- a/spec/requests/ci/api/projects_spec.rb +++ b/spec/requests/ci/api/projects_spec.rb @@ -58,57 +58,6 @@ describe Ci::API::API do end end - describe "POST /projects/:project_id/webhooks" do - let!(:project) { FactoryGirl.create(:ci_project) } - - context "Valid Webhook URL" do - let!(:webhook) { { web_hook: "http://example.com/sth/1/ala_ma_kota" } } - - before do - options.merge!(webhook) - end - - it "should create webhook for specified project" do - project.gl_project.team << [user, :master] - post ci_api("/projects/#{project.id}/webhooks"), options - expect(response.status).to eq(201) - expect(json_response["url"]).to eq(webhook[:web_hook]) - end - - it "fails to create webhook for non existsing project" do - post ci_api("/projects/non-existant-id/webhooks"), options - expect(response.status).to eq(404) - end - - it "non-manager is not authorized" do - post ci_api("/projects/#{project.id}/webhooks"), options - expect(response.status).to eq(401) - end - end - - context "Invalid Webhook URL" do - let!(:webhook) { { web_hook: "ala_ma_kota" } } - - before do - options.merge!(webhook) - end - - it "fails to create webhook for not valid url" do - project.gl_project.team << [user, :master] - post ci_api("/projects/#{project.id}/webhooks"), options - expect(response.status).to eq(400) - end - end - - context "Missed web_hook parameter" do - it "fails to create webhook for not provided url" do - project.gl_project.team << [user, :master] - post ci_api("/projects/#{project.id}/webhooks"), options - expect(response.status).to eq(400) - end - end - end - describe "GET /projects/:id" do let!(:project) { FactoryGirl.create(:ci_project) } @@ -158,28 +107,6 @@ describe Ci::API::API do end end - describe "DELETE /projects/:id" do - let!(:project) { FactoryGirl.create(:ci_project) } - - it "should delete a specific project" do - project.gl_project.team << [user, :master] - delete ci_api("/projects/#{project.id}"), options - expect(response.status).to eq(200) - expect { project.reload }. - to raise_error(ActiveRecord::RecordNotFound) - end - - it "non-manager is not authorized" do - delete ci_api("/projects/#{project.id}"), options - expect(response.status).to eq(401) - end - - it "is getting not found error" do - delete ci_api("/projects/not-existing_id"), options - expect(response.status).to eq(404) - end - end - describe "POST /projects/:id/runners/:id" do let(:project) { FactoryGirl.create(:ci_project) } let(:runner) { FactoryGirl.create(:ci_runner) } diff --git a/spec/services/ci/web_hook_service_spec.rb b/spec/services/ci/web_hook_service_spec.rb deleted file mode 100644 index e7d8ab3065..0000000000 --- a/spec/services/ci/web_hook_service_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'spec_helper' - -describe Ci::WebHookService, services: true do - let(:project) { FactoryGirl.create :ci_project } - let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } - let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } - let(:build) { FactoryGirl.create :ci_build, commit: commit } - let(:hook) { FactoryGirl.create :ci_web_hook, project: project } - - describe :execute do - it "should execute successfully" do - stub_request(:post, hook.url).to_return(status: 200) - expect(Ci::WebHookService.new.build_end(build)).to be_truthy - end - end - - context 'build_data' do - it "contains all needed fields" do - expect(build_data(build)).to include( - :build_id, - :project_id, - :ref, - :build_status, - :build_started_at, - :build_finished_at, - :before_sha, - :project_name, - :gitlab_url, - :build_name - ) - end - end - - def build_data(build) - Ci::WebHookService.new.send :build_data, build - end -end diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb new file mode 100644 index 0000000000..7e379502f9 --- /dev/null +++ b/spec/workers/build_email_worker_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe BuildEmailWorker do + include RepoHelpers + + let(:build) { create(:ci_build) } + let(:user) { create(:user) } + let(:data) { Gitlab::BuildDataBuilder.build(build) } + + subject { BuildEmailWorker.new } + + before do + allow(build).to receive(:execute_hooks).and_return(false) + build.success + end + + describe "#perform" do + it "sends mail" do + subject.perform(build.id, user.email, data.stringify_keys) + + email = ActionMailer::Base.deliveries.last + expect(email.subject).to include('Build success for') + expect(email.to).to eq([user.email]) + end + + it "gracefully handles an input SMTP error" do + ActionMailer::Base.deliveries.clear + allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError) + + subject.perform(build.id, user.email, data.stringify_keys) + + expect(ActionMailer::Base.deliveries.count).to eq(0) + end + end +end From d5c91bb9a601a1a344d94763654f0b0996857497 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 9 Dec 2015 16:31:42 +0100 Subject: [PATCH 518/908] Migrate CI WebHooks and Emails to new tables --- .../project_services/builds_email_service.rb | 11 +++++++---- app/workers/build_email_worker.rb | 2 +- .../20151209144329_migrate_ci_web_hooks.rb | 12 ++++++++++++ db/migrate/20151209145909_migrate_ci_emails.rb | 16 ++++++++++++++++ db/schema.rb | 2 +- lib/gitlab/database.rb | 18 ++++++++++++++++++ 6 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20151209144329_migrate_ci_web_hooks.rb create mode 100644 db/migrate/20151209145909_migrate_ci_emails.rb diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 9c9b5a4d08..4514f55cf5 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -54,7 +54,7 @@ class BuildsEmailService < Service def fields [ - { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by whitespace' }, + { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by comma' }, { type: 'checkbox', name: 'add_pusher', label: 'Add pusher to recipients list' }, { type: 'checkbox', name: 'notify_only_broken_builds' }, ] @@ -72,10 +72,13 @@ class BuildsEmailService < Service end def all_recipients(data) + all_recipients = [] + all_recipients <<= recipients.split(',') + if add_pusher? && data[:user][:email] - recipients + " #{data[:user][:email]}" - else - recipients + all_recipients << "#{data[:user][:email]}" end + + all_recipients end end diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb index c5c8055bea..1c7a04a66a 100644 --- a/app/workers/build_email_worker.rb +++ b/app/workers/build_email_worker.rb @@ -2,7 +2,7 @@ class BuildEmailWorker include Sidekiq::Worker def perform(build_id, recipients, push_data) - recipients.split(' ').each do |recipient| + recipients.each do |recipient| begin case push_data['build_status'] when 'success' diff --git a/db/migrate/20151209144329_migrate_ci_web_hooks.rb b/db/migrate/20151209144329_migrate_ci_web_hooks.rb new file mode 100644 index 0000000000..0293be3f6c --- /dev/null +++ b/db/migrate/20151209144329_migrate_ci_web_hooks.rb @@ -0,0 +1,12 @@ +class MigrateCiWebHooks < ActiveRecord::Migration + include Gitlab::Database + + def up + execute( + 'INSERT INTO web_hooks (url, project_id, type, created_at, updated_at, push_events, build_events) ' \ + "SELECT ci_web_hooks.url, projects.id, 'ProjectHook', ci_web_hooks.created_at, ci_web_hooks.updated_at, #{false_value}, #{true_value} FROM ci_web_hooks " \ + 'JOIN ci_projects ON ci_web_hooks.project_id = ci_projects.id ' \ + 'JOIN projects ON ci_projects.gitlab_id = projects.id' + ) + end +end diff --git a/db/migrate/20151209145909_migrate_ci_emails.rb b/db/migrate/20151209145909_migrate_ci_emails.rb new file mode 100644 index 0000000000..ebf0e7d264 --- /dev/null +++ b/db/migrate/20151209145909_migrate_ci_emails.rb @@ -0,0 +1,16 @@ +class MigrateCiEmails < ActiveRecord::Migration + include Gitlab::Database + + def up + execute( + 'INSERT INTO services (project_id, type, created_at, updated_at, active, push_events, issues_events, merge_requests_events, tag_push_events, note_events, build_events, properties) ' \ + "SELECT projects.id, 'BuildsEmailService', ci_services.created_at, ci_services.updated_at, #{true_value}, #{false_value}, #{false_value}, #{false_value}, #{false_value}, #{false_value}, #{true_value}, " \ + "CONCAT('{\"notify_only_broken_builds\":\"', ci_projects.email_only_broken_builds, " \ + "'\",\"add_pusher\":\"', ci_projects.email_add_pusher, '\",\"recipients\":\"', ci_projects.email_recipients, '\"}') " \ + 'FROM ci_services ' \ + 'JOIN ci_projects ON ci_services.project_id = ci_projects.id ' \ + 'JOIN projects ON ci_projects.gitlab_id = projects.id ' \ + "WHERE ci_services.type = 'Ci::MailService' AND ci_services.active" + ) + end +end diff --git a/db/schema.rb b/db/schema.rb index 58595f8ae5..50b6650ac4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151203162134) do +ActiveRecord::Schema.define(version: 20151209145909) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 71f37f1fef..de77a6fbff 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -7,5 +7,23 @@ module Gitlab def self.postgresql? ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql' end + + def true_value + case ActiveRecord::Base.connection.adapter_name.downcase + when 'postgresql' + "'t'" + else + 1 + end + end + + def false_value + case ActiveRecord::Base.connection.adapter_name.downcase + when 'postgresql' + "'f'" + else + 0 + end + end end end From 80f8074d01a310141984dad9dfe01a27b533e78a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 10 Dec 2015 14:08:09 +0100 Subject: [PATCH 519/908] Migrate SlackService and HipChat service --- .../project_services/builds_email_service.rb | 2 ++ .../project_services/hipchat_service.rb | 2 ++ app/models/project_services/slack_service.rb | 2 ++ .../20151209145909_migrate_ci_emails.rb | 6 ++++ ...20151210125232_migrate_ci_slack_service.rb | 29 ++++++++++++++++++ ...51210125927_migrate_ci_hip_chat_service.rb | 30 +++++++++++++++++++ db/schema.rb | 2 +- 7 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20151210125232_migrate_ci_slack_service.rb create mode 100644 db/migrate/20151210125927_migrate_ci_hip_chat_service.rb diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 4514f55cf5..e01648f059 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -24,6 +24,8 @@ class BuildsEmailService < Service boolean_accessor :notify_only_broken_builds validates :recipients, presence: true, if: :activated? + default_value_for :notify_only_broken_builds, true + def title 'Builds emails' end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 6f96907ec1..4045d16fa2 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -25,6 +25,8 @@ class HipchatService < Service boolean_accessor :notify_only_broken_builds validates :token, presence: true, if: :activated? + default_value_for :notify_only_broken_builds, true + def title 'HipChat' end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 3581949157..553d4b025e 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -23,6 +23,8 @@ class SlackService < Service boolean_accessor :notify_only_broken_builds validates :webhook, presence: true, if: :activated? + default_value_for :notify_only_broken_builds, true + def title 'Slack' end diff --git a/db/migrate/20151209145909_migrate_ci_emails.rb b/db/migrate/20151209145909_migrate_ci_emails.rb index ebf0e7d264..964dde841a 100644 --- a/db/migrate/20151209145909_migrate_ci_emails.rb +++ b/db/migrate/20151209145909_migrate_ci_emails.rb @@ -2,6 +2,9 @@ class MigrateCiEmails < ActiveRecord::Migration include Gitlab::Database def up + # This inserts a new service: BuildsEmailService + # It also "manually" constructs the properties (JSON-encoded) + # Migrating all ci_projects e-mail related columns execute( 'INSERT INTO services (project_id, type, created_at, updated_at, active, push_events, issues_events, merge_requests_events, tag_push_events, note_events, build_events, properties) ' \ "SELECT projects.id, 'BuildsEmailService', ci_services.created_at, ci_services.updated_at, #{true_value}, #{false_value}, #{false_value}, #{false_value}, #{false_value}, #{false_value}, #{true_value}, " \ @@ -13,4 +16,7 @@ class MigrateCiEmails < ActiveRecord::Migration "WHERE ci_services.type = 'Ci::MailService' AND ci_services.active" ) end + + def down + end end diff --git a/db/migrate/20151210125232_migrate_ci_slack_service.rb b/db/migrate/20151210125232_migrate_ci_slack_service.rb new file mode 100644 index 0000000000..4a5dfe866a --- /dev/null +++ b/db/migrate/20151210125232_migrate_ci_slack_service.rb @@ -0,0 +1,29 @@ +class MigrateCiSlackService < ActiveRecord::Migration + include Gitlab::Database + + def up + properties_query = 'SELECT properties FROM ci_services ' \ + 'JOIN ci_projects ON ci_services.project_id=ci_projects.id ' \ + 'WHERE ci_projects.gitlab_id=services.project_id' + + active_query = 'SELECT 1 FROM ci_services ' \ + 'JOIN ci_projects ON ci_services.project_id=ci_projects.id ' \ + "WHERE ci_projects.gitlab_id=services.project_id AND ci_services.type='Ci::SlackService' AND ci_services.active" + + # We update the service since services are always generated for project, even if they are inactive + # Activate service and migrate properties if currently the service is not active + execute( + "UPDATE services SET properties=(#{properties_query}), build_events=#{true_value}, active=#{true_value} " \ + "WHERE NOT services.active AND services.type='SlackService' AND (#{active_query}) IS NOT NULL" + ) + + # Tick only build_events if the service is already active + execute( + "UPDATE services SET build_events=#{true_value} " \ + "WHERE services.active AND services.type='SlackService' AND (#{active_query}) IS NOT NULL" + ) + end + + def down + end +end diff --git a/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb new file mode 100644 index 0000000000..2fdaf5fb91 --- /dev/null +++ b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb @@ -0,0 +1,30 @@ +class MigrateCiHipChatService < ActiveRecord::Migration + include Gitlab::Database + + def up + # From properties strip `hipchat_` key + properties_query = "SELECT REPLACE(properties, '\"hipchat_', '\"') FROM ci_services " \ + 'JOIN ci_projects ON ci_services.project_id=ci_projects.id ' \ + 'WHERE ci_projects.gitlab_id=services.project_id' + + active_query = 'SELECT 1 FROM ci_services ' \ + 'JOIN ci_projects ON ci_services.project_id=ci_projects.id ' \ + "WHERE ci_projects.gitlab_id=services.project_id AND ci_services.type='Ci::HipchatService' AND ci_services.active" + + # We update the service since services are always generated for project, even if they are inactive + # Activate service and migrate properties if currently the service is not active + execute( + "UPDATE services SET properties=(#{properties_query}), build_events=#{true_value}, active=#{true_value} " \ + "WHERE NOT services.active AND services.type='HipchatService' AND (#{active_query}) IS NOT NULL" + ) + + # Tick only build_events if the service is already active + execute( + "UPDATE services SET build_events=#{true_value} " \ + "WHERE services.active AND services.type='HipchatService' AND (#{active_query}) IS NOT NULL" + ) + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 50b6650ac4..6ccd440421 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151209145909) do +ActiveRecord::Schema.define(version: 20151210125927) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From c4fa894de22a6ba20f3078f490b708c81b6d6464 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 10 Dec 2015 16:16:34 +0100 Subject: [PATCH 520/908] Fix specs --- app/models/service.rb | 1 + features/project/service.feature | 8 ++++---- features/steps/project/services.rb | 8 ++++---- .../project_services/slack_service/build_message_spec.rb | 2 +- spec/workers/build_email_worker_spec.rb | 4 ++-- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/models/service.rb b/app/models/service.rb index 195c4690e8..0ccb8b410d 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -31,6 +31,7 @@ class Service < ActiveRecord::Base default_value_for :tag_push_events, true default_value_for :note_events, true default_value_for :build_events, true + default_value_for :properties, {} after_initialize :initialize_properties diff --git a/features/project/service.feature b/features/project/service.feature index 13edc6cb2b..5014b52b9f 100644 --- a/features/project/service.feature +++ b/features/project/service.feature @@ -55,11 +55,11 @@ Feature: Project Services And I fill Pushover settings Then I should see Pushover service settings saved - Scenario: Activate email service + Scenario: Activate email on push service When I visit project "Shop" services page - And I click email service link - And I fill email settings - Then I should see email service settings saved + And I click email on push service link + And I fill email on push settings + Then I should see email on push service settings saved Scenario: Activate Irker (IRC Gateway) service When I visit project "Shop" services page diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 2d564dac49..1c700df0c6 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -118,16 +118,16 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps expect(find_field('Restrict to branch').value).to eq 'master' end - step 'I click email service link' do - click_link 'Emails' + step 'I click email on push service link' do + click_link 'Emails on push' end - step 'I fill email settings' do + step 'I fill email on push settings' do fill_in 'Recipients', with: 'qa@company.name' click_button 'Save' end - step 'I should see email service settings saved' do + step 'I should see email on push service settings saved' do expect(find_field('Recipients').value).to eq 'qa@company.name' end diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/slack_service/build_message_spec.rb index d64c67da93..621c83c0cd 100644 --- a/spec/models/project_services/slack_service/build_message_spec.rb +++ b/spec/models/project_services/slack_service/build_message_spec.rb @@ -25,7 +25,7 @@ describe SlackService::BuildMessage do let(:color) { 'good' } it 'returns a message with information about succeeded build' do - message = ': Commit of branch by hacker succeeded in 10 second(s)' + message = ': Commit of branch by hacker passed in 10 second(s)' expect(subject.pretext).to be_empty expect(subject.fallback).to eq(message) expect(subject.attachments).to eq([text: message, color: color]) diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb index 7e379502f9..98deae0a58 100644 --- a/spec/workers/build_email_worker_spec.rb +++ b/spec/workers/build_email_worker_spec.rb @@ -16,7 +16,7 @@ describe BuildEmailWorker do describe "#perform" do it "sends mail" do - subject.perform(build.id, user.email, data.stringify_keys) + subject.perform(build.id, [user.email], data.stringify_keys) email = ActionMailer::Base.deliveries.last expect(email.subject).to include('Build success for') @@ -27,7 +27,7 @@ describe BuildEmailWorker do ActionMailer::Base.deliveries.clear allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError) - subject.perform(build.id, user.email, data.stringify_keys) + subject.perform(build.id, [user.email], data.stringify_keys) expect(ActionMailer::Base.deliveries.count).to eq(0) end From 5fd280f3d3264aec3656cb61cd8728f2ca4d61ce Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Thu, 12 Nov 2015 17:00:39 +0100 Subject: [PATCH 521/908] Licence also accepted as license file --- CHANGELOG | 2 +- app/models/repository.rb | 22 +++++++++++++++------- spec/models/repository_spec.rb | 11 ++++++++++- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 69a293d70d..891004f2ff 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,7 +32,7 @@ v 8.2.0 (unreleased) - Fix incoming email config defaults - MR target branch is now visible on a list view when it is different from project's default one - Improve Continuous Integration graphs page - - Accept COPYING as licence file (Zeger-Jan van de Weg) + - Accept COPYING,COPYING.lesser, and licence as license file (Zeger-Jan van de Weg) 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/models/repository.rb b/app/models/repository.rb index 622d6303bf..cc70aa2b73 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -244,16 +244,24 @@ class Repository def license cache.fetch(:license) do licenses = tree(:head).blobs.find_all do |file| - file.name =~ /\A(copying|license)/i + file.name =~ /\A(copying|license|licence)/i end - # If `licence`, `copying` and `copying.lesser` are found, return in the - # following order: licence, copying, copying.lesser - regex = [/\Alicense(\..*)?\z/i, /\Acopying(\..{0,3})?\z/i, /\Acopying.lesser/i] + preferences = [ + /\Alicen[sc]e\z/i, # LICENSE, LICENCE + /\Alicen[sc]e\./i, # LICENSE.md, LICENSE.txt + /\Acopying\z/i, # COPYING + /\Acopying\.(?!lesser)/i, # COPYING.txt + /Acopying.lesser/i # COPYING.LESSER + ] - regex.map do |re| - licenses.find { |l| l.name =~ re } - end.compact.first + license = nil + preferences.each do |r| + license = licenses.find { |l| l.name =~ r } + break if license + end + + license end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 04437d64fe..49af0229bb 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -102,13 +102,22 @@ describe Repository do end describe "#license" do - it 'test selection preference' do + before do repository.send(:cache).expire(:license) TestBlob = Struct.new(:name) + end + + it 'test selection preference' do files = [TestBlob.new('file'), TestBlob.new('license'), TestBlob.new('copying')] expect(repository.tree).to receive(:blobs).and_return(files) expect(repository.license.name).to eq('license') end + + it 'also accepts licence instead of license' do + expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('licence')]) + + expect(repository.license.name).to eq('licence') + end end end From 01cafa1d90b387abd017b83794eb55dd7e6fca3b Mon Sep 17 00:00:00 2001 From: Borja Aparicio Date: Tue, 8 Dec 2015 11:01:24 +0100 Subject: [PATCH 522/908] Aling project js with EE --- app/assets/javascripts/project.js.coffee | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index ec919f0cd6..1f221945c0 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -4,8 +4,11 @@ class @Project $('.js-protocol-switch').click -> return if $(@).hasClass('active') - # Toggle 'active' for both buttons - $('.js-protocol-switch').toggleClass('active') + + # Remove the active class for all buttons (ssh, http, kerberos if shown) + $('.active').not($(@)).removeClass('active'); + # Add the active class for the clicked button + $(@).toggleClass('active') url = $(@).data('clone') From a2798e8d06641be61e8a44682984c2d4b123e70f Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Thu, 10 Dec 2015 16:29:25 +0100 Subject: [PATCH 523/908] Target right release --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b7e86bf83d..1b0491f264 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,6 +28,7 @@ v 8.3.0 (unreleased) - Automatically select default clone protocol based on user preferences (Eirik Lygre) - Make Network page as sub tab of Commits - Add copy-to-clipboard button for Snippets + - Accept COPYING,COPYING.lesser, and licence as license file (Zeger-Jan van de Weg) v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) @@ -102,7 +103,6 @@ v 8.2.0 - Show specific runners from projects where user is master or owner - MR target branch is now visible on a list view when it is different from project's default one - Improve Continuous Integration graphs page - - Accept COPYING,COPYING.lesser, and licence as license file (Zeger-Jan van de Weg) - 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) From 0c54c9a8a3cb025d7a7aec7aaf8ab17f61bb6ed6 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 10 Dec 2015 13:42:10 -0200 Subject: [PATCH 524/908] USe innerHtml to avoid slow performance on a MR with very large diff --- app/assets/javascripts/merge_request_tabs.js.coffee | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 1f0b6d9ced..e4fa03c0ac 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -144,9 +144,8 @@ class @MergeRequestTabs @_get url: "#{source}.json" + @_location.search success: (data) => - html = $(data.html) - html.syntaxHighlight() - $('#diffs').html(html) + document.getElementById('diffs').innerHTML = data.html + $('div#diffs .js-syntax-highlight').syntaxHighlight() @diffsLoaded = true @scrollToElement("#diffs") From 67913670dcc6ca228654f2c6e14d0992636c5bb7 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 10 Dec 2015 13:50:31 -0200 Subject: [PATCH 525/908] Make selectors more specific when setting the tab content Reason: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2023 --- app/assets/javascripts/merge_request_tabs.js.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index e4fa03c0ac..79ef746d57 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -133,7 +133,7 @@ class @MergeRequestTabs @_get url: "#{source}.json" success: (data) => - document.getElementById('commits').innerHTML = data.html + document.querySelector("div#commits").innerHTML = data.html $('.js-timeago').timeago() @commitsLoaded = true @scrollToElement("#commits") @@ -144,7 +144,7 @@ class @MergeRequestTabs @_get url: "#{source}.json" + @_location.search success: (data) => - document.getElementById('diffs').innerHTML = data.html + document.querySelector("div#diffs").innerHTML = data.html $('div#diffs .js-syntax-highlight').syntaxHighlight() @diffsLoaded = true @scrollToElement("#diffs") @@ -155,7 +155,7 @@ class @MergeRequestTabs @_get url: "#{source}.json" success: (data) => - document.getElementById('builds').innerHTML = data.html + document.querySelector("div#builds").innerHTML = data.html $('.js-timeago').timeago() @buildsLoaded = true @scrollToElement("#builds") From dc4e2744ba13cd7d75147787550a1272b9d34a95 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 10 Dec 2015 17:21:06 +0100 Subject: [PATCH 526/908] Fix issue tracker service --- .../project_services/issue_tracker_service.rb | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 936e574ccc..46ba980817 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -56,16 +56,12 @@ class IssueTrackerService < Service end def initialize_properties - if properties.nil? + if new_record? if enabled_in_gitlab_config - self.properties = { - title: issues_tracker['title'], - project_url: add_issues_tracker_id(issues_tracker['project_url']), - issues_url: add_issues_tracker_id(issues_tracker['issues_url']), - new_issue_url: add_issues_tracker_id(issues_tracker['new_issue_url']) - } - else - self.properties = {} + self.title = issues_tracker['title'] + self.project_url = add_issues_tracker_id(issues_tracker['project_url']) + self.issues_url = add_issues_tracker_id(issues_tracker['issues_url']) + self.new_issue_url = add_issues_tracker_id(issues_tracker['new_issue_url']) end end end @@ -98,8 +94,8 @@ class IssueTrackerService < Service def enabled_in_gitlab_config Gitlab.config.issues_tracker && - Gitlab.config.issues_tracker.values.any? && - issues_tracker + Gitlab.config.issues_tracker.values.any? && + issues_tracker end def issues_tracker From 63a1a581e937ff6d21e7e6ca4774b7907c6a0c1b Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Thu, 10 Dec 2015 10:24:08 -0600 Subject: [PATCH 527/908] Document file upload random uuid security --- doc/security/README.md | 3 ++- doc/security/user_file_uploads.md | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 doc/security/user_file_uploads.md diff --git a/doc/security/README.md b/doc/security/README.md index 473f3632dc..fba6013d9c 100644 --- a/doc/security/README.md +++ b/doc/security/README.md @@ -4,4 +4,5 @@ - [Rack attack](rack_attack.md) - [Web Hooks and insecure internal web services](webhooks.md) - [Information exclusivity](information_exclusivity.md) -- [Reset your root password](reset_root_password.md) \ No newline at end of file +- [Reset your root password](reset_root_password.md) +- [User File Uploads](user_file_uploads.md) diff --git a/doc/security/user_file_uploads.md b/doc/security/user_file_uploads.md new file mode 100644 index 0000000000..98493d33b0 --- /dev/null +++ b/doc/security/user_file_uploads.md @@ -0,0 +1,11 @@ +# User File Uploads + +Images attached to issues, merge requests or comments do not require authentication +to be viewed if someone knows the direct URL. This direct URL contains a random +32-character ID that prevents unauthorized people from guessing the URL to an +image containing sensitive information. We don't enable authentication because +these images need to be visible in the body of notification emails, which are +often read from email clients that are not authenticated with GitLab, like +Outlook, Apple Mail, or the Mail app on your mobile device. + +Note that non-image attachments do require authentication to be viewed. From b1fc53134471f6c934bedde7ecab74996c8dc86e Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 9 Dec 2015 10:41:37 +0100 Subject: [PATCH 528/908] Fix typo --- app/views/projects/merge_requests/widget/_merged.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index 8c2b5366a0..6f52c963a5 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -33,7 +33,7 @@ .remove_source_branch_in_progress.hide %p = icon('spinner spin') - Removing source branch '#{@merge_request.source_branch}'. Please wait. This page will be automatically reload. + Removing source branch '#{@merge_request.source_branch}'. Please wait, this page will be automatically reloaded. :javascript $('.remove_source_branch').on('click', function() { From 9d6e5f0a28cda3dcbf2dcbcb8a869c0873ea3afc Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 10 Dec 2015 15:42:00 -0200 Subject: [PATCH 529/908] Make selectors more specific when scroll to element --- app/assets/javascripts/merge_request_tabs.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 79ef746d57..9e2dc1250c 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -77,7 +77,7 @@ class @MergeRequestTabs scrollToElement: (container) -> if window.location.hash - $el = $("#{container} #{window.location.hash}") + $el = $("div#{container} #{window.location.hash}") $('body').scrollTo($el.offset().top) if $el.length # Activate a tab based on the current action From cdda82757781bbae12bb06c92ad58cd9532f1b27 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 10 Dec 2015 20:45:54 +0200 Subject: [PATCH 530/908] Remove check icon from merged requests Now that GitLab CI exposes the status of each MR in the MR page it seems "redundant" to have an extra icon next to the CI statuses. --- app/views/projects/merge_requests/_merge_request.html.haml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index cf9570f7c7..105c731c7e 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -5,7 +5,6 @@ %ul.controls.light - if merge_request.merged? %li - = icon('check') MERGED - elsif merge_request.closed? %li From 3e5b24d46a7fe904a5f934a54dafbb05a01f837b Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Thu, 10 Dec 2015 15:45:36 -0200 Subject: [PATCH 531/908] Upgraded Sidekiq to 4.x --- CHANGELOG | 1 + Gemfile | 5 +++-- Gemfile.lock | 16 +++++++++------- config/initializers/sidekiq.rb | 8 ++++++++ 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 750ee1016d..83d422633b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -36,6 +36,7 @@ v 8.3.0 (unreleased) - Use new style for milestone detail page - Fix sidebar tooltips when collapsed - Prevent possible XSS attack with award-emoji + - Upgraded Sidekiq to 4.x v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) diff --git a/Gemfile b/Gemfile index 473770f53e..d1ed47d230 100644 --- a/Gemfile +++ b/Gemfile @@ -119,8 +119,9 @@ gem 'acts-as-taggable-on', '~> 3.4' # Background jobs gem 'sinatra', '~> 1.4.4', require: nil -gem 'sidekiq', '~> 3.5.0' -gem 'sidekiq-cron', '~> 0.3.0' +gem 'sidekiq', '~> 4.0' +gem 'sidekiq-cron', '~> 0.4.0' +gem 'redis-namespace' # HTTP requests gem "httparty", '~> 0.13.3' diff --git a/Gemfile.lock b/Gemfile.lock index 37ba22b7ce..20084d08d2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -147,6 +147,7 @@ GEM execjs coffee-script-source (1.10.0) colorize (0.7.7) + concurrent-ruby (1.0.0) connection_pool (2.2.0) coveralls (0.8.9) json (~> 1.8) @@ -682,15 +683,15 @@ GEM rack shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - sidekiq (3.5.3) - celluloid (~> 0.17.2) + sidekiq (4.0.1) + concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) json (~> 1.0) redis (~> 3.2, >= 3.2.1) - redis-namespace (~> 1.5, >= 1.5.2) - sidekiq-cron (0.3.1) + sidekiq-cron (0.4.0) + redis-namespace (>= 1.5.2) rufus-scheduler (>= 2.0.24) - sidekiq (>= 2.17.3) + sidekiq (>= 4.0.0) simple_oauth (0.1.9) simplecov (0.10.0) docile (~> 1.1.0) @@ -934,6 +935,7 @@ DEPENDENCIES rblineprof rdoc (~> 3.6) redcarpet (~> 3.3.3) + redis-namespace redis-rails (~> 4.0.0) request_store (~> 1.2.0) rerun (~> 0.10.0) @@ -951,8 +953,8 @@ DEPENDENCIES settingslogic (~> 2.0.9) sham_rack shoulda-matchers (~> 2.8.0) - sidekiq (~> 3.5.0) - sidekiq-cron (~> 0.3.0) + sidekiq (~> 4.0) + sidekiq-cron (~> 0.4.0) simplecov (~> 0.10.0) sinatra (~> 1.4.4) six (~> 0.2.0) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 6e5701e33d..2e3a71912e 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -23,6 +23,14 @@ Sidekiq.configure_server do |config| if File.exists?(schedule_file) Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file) end + + # Database pool should be at least `sidekiq_concurrency` + 2 + # For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md + config = ActiveRecord::Base.configurations[Rails.env] || + Rails.application.config.database_configuration[Rails.env] + config['pool'] = Sidekiq.options[:concurrency] + 2 + ActiveRecord::Base.establish_connection(config) + Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}") end Sidekiq.configure_client do |config| From 7f1b60cc66d27d1596b881884734532cdc62efd7 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 10 Dec 2015 09:02:57 -0800 Subject: [PATCH 532/908] Bump devise to 3.5.3 to fix reset token expiring after account creation Also fixes an incorrect redirect after login with relative URL root: Closes https://github.com/gitlabhq/gitlabhq/issues/8228 Closes #2750 --- CHANGELOG | 1 + Gemfile | 2 +- Gemfile.lock | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2fa1dac346..2078e2dc67 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ v 8.3.0 (unreleased) - Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg) - Merge when build succeeds (Zeger-Jan van de Weg) - Provide better diagnostic message upon project creation errors (Stan Hu) + - Bump devise to 3.5.3 to fix reset token expiring after account creation (Stan Hu) - Bump gollum-lib to 4.1.0 (Stan Hu) - Fix broken group avatar upload under "New group" (Stan Hu) - Update project repositorize size and commit count during import:repos task (Stan Hu) diff --git a/Gemfile b/Gemfile index 473770f53e..edeab0a9d5 100644 --- a/Gemfile +++ b/Gemfile @@ -18,7 +18,7 @@ gem "mysql2", '~> 0.3.16', group: :mysql gem "pg", '~> 0.18.2', group: :postgres # Authentication libraries -gem 'devise', '~> 3.5.2' +gem 'devise', '~> 3.5.3' gem 'devise-async', '~> 0.9.0' gem 'doorkeeper', '~> 2.2.0' gem 'omniauth', '~> 1.2.2' diff --git a/Gemfile.lock b/Gemfile.lock index 37ba22b7ce..c3d7536b6c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -168,7 +168,7 @@ GEM activerecord (>= 3.2.0, < 5.0) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - devise (3.5.2) + devise (3.5.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 3.2.6, < 5) @@ -858,7 +858,7 @@ DEPENDENCIES d3_rails (~> 3.5.5) database_cleaner (~> 1.4.0) default_value_for (~> 3.0.0) - devise (~> 3.5.2) + devise (~> 3.5.3) devise-async (~> 0.9.0) devise-two-factor (~> 2.0.0) diffy (~> 3.0.3) From d8b3c3274c63d5fa62c441bac6e78a16e94422c3 Mon Sep 17 00:00:00 2001 From: Corey Hinshaw Date: Thu, 14 May 2015 11:46:48 -0400 Subject: [PATCH 533/908] AuthHash should not parameterize email user --- lib/gitlab/o_auth/auth_hash.rb | 2 +- spec/lib/gitlab/o_auth/auth_hash_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb index d94b104bbf..ba31599432 100644 --- a/lib/gitlab/o_auth/auth_hash.rb +++ b/lib/gitlab/o_auth/auth_hash.rb @@ -62,7 +62,7 @@ module Gitlab # Get the first part of the email address (before @) # In addtion in removes illegal characters def generate_username(email) - email.match(/^[^@]*/)[0].parameterize + email.match(/^[^@]*/)[0].mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/,'').to_s end def generate_temporarily_email(username) diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb index a4f8b44e38..8aaeb5779d 100644 --- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb +++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::OAuth::AuthHash, lib: true do let(:uid_raw) do "CN=Onur K\xC3\xBC\xC3\xA7\xC3\xBCk,OU=Test,DC=example,DC=net" end - let(:email_raw) { "onur.k\xC3\xBC\xC3\xA7\xC3\xBCk@example.net" } + let(:email_raw) { "onur.k\xC3\xBC\xC3\xA7\xC3\xBCk_ABC-123@example.net" } let(:nickname_raw) { "ok\xC3\xBC\xC3\xA7\xC3\xBCk" } let(:first_name_raw) { 'Onur' } let(:last_name_raw) { "K\xC3\xBC\xC3\xA7\xC3\xBCk" } @@ -66,7 +66,7 @@ describe Gitlab::OAuth::AuthHash, lib: true do before { info_hash.delete(:nickname) } it 'takes the first part of the email as username' do - expect(auth_hash.username).to eql 'onur-kucuk' + expect(auth_hash.username).to eql 'onur.kucuk_ABC-123' end end From 1f893022fdb9e5d23c5006e9960c1965a9415f2d Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 10 Dec 2015 22:07:25 -0200 Subject: [PATCH 534/908] Remove note from MR discussion when deleting it in the changes tab --- app/assets/javascripts/notes.js.coffee | 24 ++++++++++++++++-------- features/project/merge_requests.feature | 10 ++++++++++ features/steps/project/merge_requests.rb | 19 +++++++++++++++++++ 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index b1df56b24f..30aaa364ba 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -350,18 +350,26 @@ class @Notes ### removeNote: -> note = $(this).closest(".note") - notes = note.closest(".notes") + note_id = note.attr('id') - # check if this is the last note for this line - if notes.find(".note").length is 1 + $('.note[id="' + note_id + '"]').each -> + note = $(this) + notes = note.closest(".notes") + count = notes.closest(".notes_holder").find(".discussion-notes-count") - # for discussions - notes.closest(".discussion").remove() + # check if this is the last note for this line + if notes.find(".note").length is 1 - # for diff lines - notes.closest("tr").remove() + # for discussions + notes.closest(".discussion").remove() - note.remove() + # for diff lines + notes.closest("tr").remove() + else + # update notes count + count.get(0).lastChild.nodeValue = " #{notes.children().length - 1}" + + note.remove() ### Called in response to clicking the delete attachment link diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 7490ad400b..aa9078b878 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -94,6 +94,16 @@ Feature: Project Merge Requests Then I should not see a diff comment saying "Line is wrong" And I should see a diff comment saying "Typo, please fix" + @javascript + Scenario: I delete a comment on a merge request diff + Given project "Shop" have "Bug NS-05" open merge request with diffs inside + And I visit merge request page "Bug NS-05" + And I click on the Changes tab + And I leave a comment like "Line is wrong" on diff + And I delete the comment "Line is wrong" on diff + And I click on the Discussion tab + Then I should not see any discussion + @javascript Scenario: I comment on a line of a commit in merge request Given project "Shop" have "Bug NS-05" open merge request with diffs inside diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 13d9d180dd..f0cf4818d0 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -211,6 +211,25 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end end + step 'I delete the comment "Line is wrong" on diff' do + page.within('.diff-file:nth-of-type(5) .note') do + find('.js-note-delete').click + end + end + + step 'I click on the Discussion tab' do + page.within '.merge-request-tabs' do + click_link 'Discussion' + end + + # Waits for load + expect(page).to have_css('.tab-content #notes.active') + end + + step 'I should not see any discussion' do + expect(page).not_to have_css('.notes .discussion') + end + step 'I should see a discussion has started on diff' do page.within(".notes .discussion") do page.should have_content "#{current_user.name} started a discussion" From 1a9d6ec71eafddf8cd6542b47b5473a6a3ba19b9 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 10 Dec 2015 22:30:06 -0200 Subject: [PATCH 535/908] [ci skip] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 2bde388d0f..8dd7e510d8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -40,6 +40,7 @@ v 8.3.0 (unreleased) - Fix sidebar tooltips when collapsed - Prevent possible XSS attack with award-emoji - Upgraded Sidekiq to 4.x + - Fix deleting notes on a merge request diff v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) From 74dcbec369aca9dfa181c9a82e1978ba2396773a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 10 Dec 2015 21:26:32 -0800 Subject: [PATCH 536/908] Update CHANGELOG for 7bb8bb85 --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 1cc780ae72..e38c8b363e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) + - Expand character set of usernames created by Omniauth (Corey Hinshaw) - Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg) - Merge when build succeeds (Zeger-Jan van de Weg) - Provide better diagnostic message upon project creation errors (Stan Hu) From 4a32b07d9e16a7926c42e11a462bf76133617f0c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 10 Dec 2015 07:32:46 +0000 Subject: [PATCH 537/908] Add `runners_registration_token` to ApplicationSettings --- app/models/application_setting.rb | 2 +- ...add_runners_registration_token_to_application_settings.rb | 5 +++++ db/schema.rb | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 1880ad9f33..764ecd4ee2 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -27,6 +27,7 @@ # admin_notification_email :string(255) # shared_runners_enabled :boolean default(TRUE), not null # max_artifacts_size :integer default(100), not null +# runners_registration_token :string(255) # class ApplicationSetting < ActiveRecord::Base @@ -128,5 +129,4 @@ class ApplicationSetting < ActiveRecord::Base /x) self.restricted_signup_domains.reject! { |d| d.empty? } end - end diff --git a/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb b/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb new file mode 100644 index 0000000000..00f88180e4 --- /dev/null +++ b/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddRunnersRegistrationTokenToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :runners_registration_token, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 94b87040d8..a3b649f3fd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151203162133) do +ActiveRecord::Schema.define(version: 20151210072243) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -49,6 +49,7 @@ ActiveRecord::Schema.define(version: 20151203162133) do t.string "admin_notification_email" t.boolean "shared_runners_enabled", default: true, null: false t.integer "max_artifacts_size", default: 100, null: false + t.string "runners_registration_token" end create_table "audit_events", force: :cascade do |t| From 9948e5bcdd9e2b9c99bba0bac119ac08daf82564 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 10 Dec 2015 10:48:37 +0100 Subject: [PATCH 538/908] Refactor `TokenAuthenticatable` to improve reusability This adds a ability to use multiple different authentication token fields in other models. From now on it is necessary to add authentication token field manually in each class that implements this mixin. --- app/models/application_setting.rb | 3 ++ app/models/concerns/token_authenticatable.rb | 42 ++++++++++++-------- app/models/user.rb | 4 +- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 764ecd4ee2..b49a5ce905 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -31,6 +31,9 @@ # class ApplicationSetting < ActiveRecord::Base + include TokenAuthenticatable + add_authentication_token_field :runners_registration_token + CACHE_KEY = 'application_setting.last' serialize :restricted_visibility_levels diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 9b88ec1cc3..46f8ec84e2 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -1,31 +1,39 @@ module TokenAuthenticatable extend ActiveSupport::Concern - module ClassMethods - def find_by_authentication_token(authentication_token = nil) - if authentication_token - where(authentication_token: authentication_token).first + class_methods do + def authentication_token_fields + @token_fields || [] + end + + private + + def add_authentication_token_field(token_field) + @token_fields = [] unless @token_fields + @token_fields << token_field + + define_singleton_method("find_by_#{token_field}") do |token| + where(token_field => token).first if token + end + + define_method("ensure_#{token_field}") do + write_attribute(token_field, generate_token_for(token_field)) if + read_attribute(token_field).blank? + end + + define_method("reset_#{token_field}!") do + write_attribute(token_field, generate_token_for(token_field)) + save end end end - def ensure_authentication_token - if authentication_token.blank? - self.authentication_token = generate_authentication_token - end - end - - def reset_authentication_token! - self.authentication_token = generate_authentication_token - save - end - private - def generate_authentication_token + def generate_token_for(token_field) loop do token = Devise.friendly_token - break token unless self.class.unscoped.where(authentication_token: token).first + break token unless self.class.unscoped.where(token_field => token).first end end end diff --git a/app/models/user.rb b/app/models/user.rb index 7155dd2bea..1a8d8f1e24 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -69,8 +69,10 @@ class User < ActiveRecord::Base include Gitlab::CurrentSettings include Referable include Sortable - include TokenAuthenticatable include CaseSensitivity + include TokenAuthenticatable + + add_authentication_token_field :authentication_token default_value_for :admin, false default_value_for :can_create_group, gitlab_config.default_can_create_group From d90d3db32bdc5f2180651297939490821e3f7fc9 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 10 Dec 2015 12:43:35 +0100 Subject: [PATCH 539/908] Use `save!` when generating new token in `TokenAuthenticatable` --- app/models/concerns/token_authenticatable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 46f8ec84e2..fe764e8da4 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -23,7 +23,7 @@ module TokenAuthenticatable define_method("reset_#{token_field}!") do write_attribute(token_field, generate_token_for(token_field)) - save + save! end end end From 30e29bb9244e6e0395a42dd966f72dd966a59a03 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 10 Dec 2015 12:44:40 +0100 Subject: [PATCH 540/908] Add specs for `TokenAuthenticatable` concern --- .../concerns/token_authenticatable_spec.rb | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 spec/models/concerns/token_authenticatable_spec.rb diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb new file mode 100644 index 0000000000..1b55317341 --- /dev/null +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +shared_examples 'TokenAuthenticatable' do + describe 'dynamically defined methods' do + it { expect(described_class).to be_private_method_defined(:generate_token_for) } + it { expect(described_class).to respond_to("find_by_#{token_field}") } + it { is_expected.to respond_to("ensure_#{token_field}") } + it { is_expected.to respond_to("reset_#{token_field}!") } + end +end + +describe User, 'TokenAuthenticatable' do + let(:token_field) { :authentication_token } + it_behaves_like 'TokenAuthenticatable' + + describe 'ensured authentication token' do + subject { create(:user).send(token_field) } + it { is_expected.to be_a String } + end +end + +describe ApplicationSetting, 'TokenAuthenticatable' do + let(:token_field) { :runners_registration_token } + it_behaves_like 'TokenAuthenticatable' + + describe 'generating new token' do + subject { described_class.new } + let(:token) { subject.send(token_field) } + + context 'token is not generated yet' do + it { expect(token).to be nil } + end + + context 'token is generated' do + before { subject.send("reset_#{token_field}!") } + it { expect(token).to be_a String } + end + end + + describe 'multiple token fields' do + before do + described_class.send(:add_authentication_token_field, :yet_another_token) + end + + describe '.token_fields' do + subject { described_class.authentication_token_fields } + it { is_expected.to include(:runners_registration_token, :yet_another_token) } + end + end +end From 5a7b94f6d1bfd7c376ecb6af45a31bb774358826 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 10 Dec 2015 13:05:59 +0100 Subject: [PATCH 541/908] Ensure that app settings contains runners registration token --- app/models/application_setting.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index b49a5ce905..faa0bdf840 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -78,6 +78,8 @@ class ApplicationSetting < ActiveRecord::Base end end + before_save :ensure_runners_registration_token + after_commit do Rails.cache.write(CACHE_KEY, self) end From 219afbad2adf0c1b7b3d4ccc116f44ba82818cf1 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 10 Dec 2015 13:06:44 +0100 Subject: [PATCH 542/908] Display runners registration token in CI runners config --- app/views/ci/admin/runners/index.html.haml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/ci/admin/runners/index.html.haml index bacaccfbff..c71710a831 100644 --- a/app/views/ci/admin/runners/index.html.haml +++ b/app/views/ci/admin/runners/index.html.haml @@ -1,6 +1,9 @@ %p.lead - %span To register a new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication. - %code #{GitlabCi::REGISTRATION_TOKEN} + %span + To register a new runner you should enter the following registration token. + With this token the runner will request a unique runner token and use that for future communication. + Registration token is + %code #{current_application_settings.runners_registration_token} .bs-callout %p From e2e43a114b5af63aad6d9badcb727c8829abb167 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 10 Dec 2015 13:50:00 +0100 Subject: [PATCH 543/908] Use new runners registration token to register CI runners --- config/initializers/4_ci_app.rb | 2 -- lib/ci/api/helpers.rb | 6 +++++- lib/ci/api/runners.rb | 2 +- spec/requests/ci/api/runners_spec.rb | 9 ++++++--- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/config/initializers/4_ci_app.rb b/config/initializers/4_ci_app.rb index cac8edb32b..d252e40310 100644 --- a/config/initializers/4_ci_app.rb +++ b/config/initializers/4_ci_app.rb @@ -1,8 +1,6 @@ module GitlabCi VERSION = Gitlab::VERSION REVISION = Gitlab::REVISION - - REGISTRATION_TOKEN = SecureRandom.hex(10) def self.config Settings diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 0250233375..12d7396189 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -6,7 +6,7 @@ module Ci UPDATE_RUNNER_EVERY = 60 def authenticate_runners! - forbidden! unless params[:token] == GitlabCi::REGISTRATION_TOKEN + forbidden! unless runner_registration_token_valid? end def authenticate_runner! @@ -22,6 +22,10 @@ module Ci forbidden! unless token && build.valid_token?(token) end + def runner_registration_token_valid? + params[:token] == current_application_settings.runners_registration_token + end + def update_runner_last_contact # Use a random threshold to prevent beating DB updates contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY) diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb index 1466fe4356..3edf067f46 100644 --- a/lib/ci/api/runners.rb +++ b/lib/ci/api/runners.rb @@ -40,7 +40,7 @@ module Ci required_attributes! [:token] runner = - if params[:token] == GitlabCi::REGISTRATION_TOKEN + if runner_registration_token_valid? # Create shared runner. Requires admin access Ci::Runner.create( description: params[:description], diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index 11dc089e1f..962d34a0f9 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -4,8 +4,11 @@ describe Ci::API::API do include ApiHelpers include StubGitlabCalls + let(:registration_token) { 'abcdefg123456' } + before do stub_gitlab_calls + stub_application_setting(runners_registration_token: registration_token) end describe "GET /runners" do @@ -33,20 +36,20 @@ describe Ci::API::API do describe "POST /runners/register" do describe "should create a runner if token provided" do - before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN } + before { post ci_api("/runners/register"), token: registration_token } it { expect(response.status).to eq(201) } end describe "should create a runner with description" do - before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN, description: "server.hostname" } + before { post ci_api("/runners/register"), token: registration_token, description: "server.hostname" } it { expect(response.status).to eq(201) } it { expect(Ci::Runner.first.description).to eq("server.hostname") } end describe "should create a runner with tags" do - before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN, tag_list: "tag1, tag2" } + before { post ci_api("/runners/register"), token: registration_token, tag_list: "tag1, tag2" } it { expect(response.status).to eq(201) } it { expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"]) } From 2da3cf314651d22f85059d99476ec7952950b44f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 11 Dec 2015 10:22:05 +0100 Subject: [PATCH 544/908] Add CI runners registration token reset button --- .../admin/application_settings_controller.rb | 6 ++++++ app/views/ci/admin/runners/index.html.haml | 13 ++++++++++++- config/routes.rb | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index a9bcfc7456..4804035938 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -13,6 +13,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end end + def reset_runners_token + @application_setting.reset_runners_registration_token! + flash[:notice] = 'New runners registration token has been generated!' + redirect_to ci_admin_runners_path + end + private def set_application_setting diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/ci/admin/runners/index.html.haml index c71710a831..95bc8ebd40 100644 --- a/app/views/ci/admin/runners/index.html.haml +++ b/app/views/ci/admin/runners/index.html.haml @@ -3,7 +3,18 @@ To register a new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication. Registration token is - %code #{current_application_settings.runners_registration_token} + %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token} + +.bs-callout.clearfix + .pull-left + %p + You can reset runners registration token by pressing a button below. + %p + = button_to reset_runners_token_admin_application_settings_path, + method: :put, class: 'btn btn-default', + data: { confirm: 'Are you sure you want to reset registration token?' } do + = icon('refresh') + Reset runners registration token .bs-callout %p diff --git a/config/routes.rb b/config/routes.rb index 061a8fd5da..54d6afe2dc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -276,6 +276,7 @@ Rails.application.routes.draw do resource :application_settings, only: [:show, :update] do resources :services + put :reset_runners_token end resources :labels From 28ad40d9740076677420612443358c2976fe0916 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 11 Dec 2015 11:14:55 +0100 Subject: [PATCH 545/908] Add specs for feature that regenerates runners token --- spec/features/ci/admin/runners_spec.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/spec/features/ci/admin/runners_spec.rb b/spec/features/ci/admin/runners_spec.rb index b83744f53a..0c5aebb3a7 100644 --- a/spec/features/ci/admin/runners_spec.rb +++ b/spec/features/ci/admin/runners_spec.rb @@ -61,4 +61,26 @@ describe "Admin Runners" do it { expect(page).not_to have_content(@project2.name_with_namespace) } end end + + describe 'runners registration token' do + let!(:token) { current_application_settings.runners_registration_token } + before { visit ci_admin_runners_path } + + it 'has a registration token' do + expect(page).to have_content("Registration token is #{token}") + expect(page).to have_selector('#runners-token', text: token) + end + + describe 'reload registration token' do + let(:page_token) { find('#runners-token').text } + + before do + click_button 'Reset runners registration token' + end + + it 'changes registration token' do + expect(page_token).to_not eq token + end + end + end end From 72b7d1f59d4fb26fb9119c5a059528f0fc951904 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 11 Dec 2015 13:10:00 +0200 Subject: [PATCH 546/908] emoji aliases problem --- CHANGELOG | 1 + app/assets/javascripts/awards_handler.coffee | 7 +++- app/models/note.rb | 3 +- app/views/votes/_votes_block.html.haml | 3 +- lib/award_emoji.rb | 35 ++++++++++++++++++++ spec/models/note_spec.rb | 9 +++++ 6 files changed, 55 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 585e6c4204..3ec005c58f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,6 +43,7 @@ v 8.3.0 (unreleased) - Prevent possible XSS attack with award-emoji - Upgraded Sidekiq to 4.x - Accept COPYING,COPYING.lesser, and licence as license file (Zeger-Jan van de Weg) + - Fix emoji aliases problem v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 96fd8f8773..3ff9ba77df 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,11 +1,13 @@ class @AwardsHandler - constructor: (@post_emoji_url, @noteable_type, @noteable_id) -> + constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) -> addAward: (emoji) -> + emoji = @normilizeEmojiName(emoji) @postEmoji emoji, => @addAwardToEmojiBar(emoji) addAwardToEmojiBar: (emoji, custom_path = '') -> + emoji = @normilizeEmojiName(emoji) if @exist(emoji) if @isActive(emoji) @decrementCounter(emoji) @@ -94,3 +96,6 @@ class @AwardsHandler $('body, html').animate({ scrollTop: $('.awards').offset().top - 80 }, 200) + + normilizeEmojiName: (emoji) -> + @aliases[emoji] || emoji diff --git a/app/models/note.rb b/app/models/note.rb index 8f0efa8d4b..04053ccc61 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -377,6 +377,7 @@ class Note < ActiveRecord::Base end def award_emoji_name - note.match(Gitlab::Markdown::EmojiFilter.emoji_pattern)[1] + original_name = note.match(Gitlab::Markdown::EmojiFilter.emoji_pattern)[1] + AwardEmoji.normilize_emoji_name(original_name) end end diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index 7eb27c12d3..6071f1484c 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -19,7 +19,8 @@ post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}" noteable_type = "#{votable.class.name.underscore}" noteable_id = "#{votable.id}" - window.awards_handler = new AwardsHandler(post_emoji_url, noteable_type, noteable_id) + aliases = #{AwardEmoji::ALIASES.to_json} + window.awards_handler = new AwardsHandler(post_emoji_url, noteable_type, noteable_id, aliases) $(".awards-menu li").click (e)-> emoji = $(this).data("emoji") diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb index d58a196c4e..4d99164bc3 100644 --- a/lib/award_emoji.rb +++ b/lib/award_emoji.rb @@ -6,7 +6,42 @@ class AwardEmoji "ambulance", "anguished", "two_hearts", "wink" ] + ALIASES = { + pout: "rage", + satisfied: "laughing", + hankey: "shit", + poop: "shit", + collision: "boom", + thumbsup: "+1", + thumbsdown: "-1", + punch: "facepunch", + raised_hand: "hand", + running: "runner", + ng_woman: "no_good", + shoe: "mans_shoe", + tshirt: "shirt", + honeybee: "bee", + flipper: "dolphin", + paw_prints: "feet", + waxing_gibbous_moon: "moon", + telephone: "phone", + knife: "hocho", + envelope: "email", + pencil: "memo", + open_book: "book", + sailboat: "boat", + red_car: "car", + lantern: "izakaya_lantern", + uk: "gb", + heavy_exclamation_mark: "exclamation", + squirrel: "shipit" + }.with_indifferent_access + def self.path_to_emoji_image(name) "emoji/#{Emoji.emoji_filename(name)}.png" end + + def self.normilize_emoji_name(name) + ALIASES[name] || name + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 5b6f177ebb..216c7dabae 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -159,4 +159,13 @@ describe Note, models: true do expect(note.editable?).to be_falsy end end + + describe "set_award!" do + let(:issue) { create :issue } + + it "converts aliases to actual name" do + note = create :note, note: ":thumbsup:", noteable: issue + expect(note.reload.note).to eq("+1") + end + end end From e365750199802a7c0b45dc88c99e4bf7eb6f111c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 11 Dec 2015 13:24:38 +0100 Subject: [PATCH 547/908] Enhance migrate CI emails --- .../20151209145909_migrate_ci_emails.rb | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/db/migrate/20151209145909_migrate_ci_emails.rb b/db/migrate/20151209145909_migrate_ci_emails.rb index 964dde841a..5ee1189358 100644 --- a/db/migrate/20151209145909_migrate_ci_emails.rb +++ b/db/migrate/20151209145909_migrate_ci_emails.rb @@ -3,13 +3,16 @@ class MigrateCiEmails < ActiveRecord::Migration def up # This inserts a new service: BuildsEmailService - # It also "manually" constructs the properties (JSON-encoded) + # It "manually" constructs the properties (JSON-encoded) # Migrating all ci_projects e-mail related columns execute( 'INSERT INTO services (project_id, type, created_at, updated_at, active, push_events, issues_events, merge_requests_events, tag_push_events, note_events, build_events, properties) ' \ - "SELECT projects.id, 'BuildsEmailService', ci_services.created_at, ci_services.updated_at, #{true_value}, #{false_value}, #{false_value}, #{false_value}, #{false_value}, #{false_value}, #{true_value}, " \ - "CONCAT('{\"notify_only_broken_builds\":\"', ci_projects.email_only_broken_builds, " \ - "'\",\"add_pusher\":\"', ci_projects.email_add_pusher, '\",\"recipients\":\"', ci_projects.email_recipients, '\"}') " \ + "SELECT projects.id, 'BuildsEmailService', ci_services.created_at, ci_services.updated_at, " \ + "#{true_value}, #{false_value}, #{false_value}, #{false_value}, #{false_value}, #{false_value}, #{true_value}, " \ + "CONCAT('{\"notify_only_broken_builds\":\"', #{convert_bool('ci_projects.email_only_broken_builds')}, " \ + "'\",\"add_pusher\":\"', #{convert_bool('ci_projects.email_add_pusher')}, " \ + "'\",\"recipients\":\"', #{escape_text('ci_projects.email_recipients')}, " \ + "'\"}') " \ 'FROM ci_services ' \ 'JOIN ci_projects ON ci_services.project_id = ci_projects.id ' \ 'JOIN projects ON ci_projects.gitlab_id = projects.id ' \ @@ -19,4 +22,20 @@ class MigrateCiEmails < ActiveRecord::Migration def down end + + # This function escapes double-quotes and slash + def escape_text(name) + "REPLACE(REPLACE(#{name}, '\\', '\\\\'), '\"', '\\\"')" + end + + # This function returns 0 or 1 for column + def convert_bool(name) + if self.postgresql? + # PostgreSQL uses BOOLEAN type + "CASE WHEN #{name} IS TRUE THEN '1' ELSE '0' END;" + else + # MySQL uses TINYINT + "#{name}" + end + end end From 71e6a93db979165073161f0dc13d3bfe7a461e4a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 11 Dec 2015 13:28:40 +0100 Subject: [PATCH 548/908] Change default values --- .../project_services/builds_email_service.rb | 7 ++++++- app/models/project_services/hipchat_service.rb | 7 ++++++- .../project_services/issue_tracker_service.rb | 18 +++++++++++------- app/models/project_services/slack_service.rb | 7 ++++++- app/models/service.rb | 1 - 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index e01648f059..b8bd48aca6 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -24,7 +24,12 @@ class BuildsEmailService < Service boolean_accessor :notify_only_broken_builds validates :recipients, presence: true, if: :activated? - default_value_for :notify_only_broken_builds, true + def initialize_properties + if properties.nil? + self.properties = {} + self.notify_only_broken_builds = true + end + end def title 'Builds emails' diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 4045d16fa2..1e1686a11c 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -25,7 +25,12 @@ class HipchatService < Service boolean_accessor :notify_only_broken_builds validates :token, presence: true, if: :activated? - default_value_for :notify_only_broken_builds, true + def initialize_properties + if properties.nil? + self.properties = {} + self.notify_only_broken_builds = true + end + end def title 'HipChat' diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 46ba980817..936e574ccc 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -56,12 +56,16 @@ class IssueTrackerService < Service end def initialize_properties - if new_record? + if properties.nil? if enabled_in_gitlab_config - self.title = issues_tracker['title'] - self.project_url = add_issues_tracker_id(issues_tracker['project_url']) - self.issues_url = add_issues_tracker_id(issues_tracker['issues_url']) - self.new_issue_url = add_issues_tracker_id(issues_tracker['new_issue_url']) + self.properties = { + title: issues_tracker['title'], + project_url: add_issues_tracker_id(issues_tracker['project_url']), + issues_url: add_issues_tracker_id(issues_tracker['issues_url']), + new_issue_url: add_issues_tracker_id(issues_tracker['new_issue_url']) + } + else + self.properties = {} end end end @@ -94,8 +98,8 @@ class IssueTrackerService < Service def enabled_in_gitlab_config Gitlab.config.issues_tracker && - Gitlab.config.issues_tracker.values.any? && - issues_tracker + Gitlab.config.issues_tracker.values.any? && + issues_tracker end def issues_tracker diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 553d4b025e..375b4534d0 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -23,7 +23,12 @@ class SlackService < Service boolean_accessor :notify_only_broken_builds validates :webhook, presence: true, if: :activated? - default_value_for :notify_only_broken_builds, true + def initialize_properties + if properties.nil? + self.properties = {} + self.notify_only_broken_builds = true + end + end def title 'Slack' diff --git a/app/models/service.rb b/app/models/service.rb index 0ccb8b410d..195c4690e8 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -31,7 +31,6 @@ class Service < ActiveRecord::Base default_value_for :tag_push_events, true default_value_for :note_events, true default_value_for :build_events, true - default_value_for :properties, {} after_initialize :initialize_properties From d1211e0ada61896c3a7a02c4fe7138705b086022 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 11 Dec 2015 13:03:09 +0100 Subject: [PATCH 549/908] Fix award-emojis alert flash message This adds a new feature to `Flash` that allows to pin it after a specified selector. This removes a fixed position from such award-emoji alert, and makes it responsive by design of selector that this alert is pinned to. Closes #3996 --- app/assets/javascripts/flash.js.coffee | 4 ++-- app/assets/javascripts/notes.js.coffee | 2 +- app/assets/stylesheets/framework/flash.scss | 10 ---------- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee index 9b59d4e57f..5de012e409 100644 --- a/app/assets/javascripts/flash.js.coffee +++ b/app/assets/javascripts/flash.js.coffee @@ -12,5 +12,5 @@ class @Flash @flash.click -> $(@).fadeOut() @flash.show() - pin: -> - @flash.addClass('flash-pinned flash-raised') + pinTo: (selector) -> + @flash.detach().appendTo(selector) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index b1df56b24f..a7d720fff4 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -114,7 +114,7 @@ class @Notes unless note.valid if note.award flash = new Flash('You have already used this award emoji!', 'alert') - flash.pin() + flash.pinTo('.header-content') return # render note if it not present in loaded list diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index 1b723021d7..82eb50ad4b 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -15,13 +15,3 @@ @extend .alert-danger; } } - -.flash-pinned { - position: fixed; - top: 80px; - width: 80%; -} - -.flash-raised { - z-index: 1000; -} From ef37041ab6ac41ed1de3fc62c193566d779b105a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 11 Dec 2015 13:23:59 +0100 Subject: [PATCH 550/908] Update changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 3ec005c58f..61068d3efb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -44,6 +44,7 @@ v 8.3.0 (unreleased) - Upgraded Sidekiq to 4.x - Accept COPYING,COPYING.lesser, and licence as license file (Zeger-Jan van de Weg) - Fix emoji aliases problem + - Fix award-emojis Flash alert's width v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) From cbeb06eb420f294b3a406f869f11de554048e93d Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 11 Dec 2015 13:00:24 +0000 Subject: [PATCH 551/908] Mix url helpers in into `RepositoryPush` --- lib/gitlab/email/message/repository_push.rb | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index c53148d2ed..a2eb7a70bd 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -5,6 +5,8 @@ module Gitlab attr_accessor :recipient attr_reader :author_id, :ref, :action + include Gitlab::Application.routes.url_helpers + delegate :namespace, :name_with_namespace, to: :project, prefix: :project delegate :name, to: :author, prefix: :author @@ -16,7 +18,6 @@ module Gitlab @project_id = project_id @recipient = recipient @opts = opts.dup - @urls = Gitlab::Application.routes.url_helpers @author_id = @opts.delete(:author_id) @ref = @opts.delete(:ref) @@ -86,18 +87,18 @@ module Gitlab def target_url if @action == :push && commits if commits.length > 1 - @urls.namespace_project_compare_url(project_namespace, - project, - from: Commit.new(compare.base, project), - to: Commit.new(compare.head, project)) + namespace_project_compare_url(project_namespace, + project, + from: Commit.new(compare.base, project), + to: Commit.new(compare.head, project)) else - @urls.namespace_project_commit_url(project_namespace, - project, commits.first) + namespace_project_commit_url(project_namespace, + project, commits.first) end else unless @action == :delete - @urls.namespace_project_tree_url(project_namespace, - project, ref_name) + namespace_project_tree_url(project_namespace, + project, ref_name) end end end From 917effb7379f8e871dbc113d7c7dab89473d4bc8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 11 Dec 2015 13:37:54 +0000 Subject: [PATCH 552/908] Make sure that token `ensure_*` method always returns a token --- app/models/concerns/token_authenticatable.rb | 8 ++++++-- spec/models/concerns/token_authenticatable_spec.rb | 9 ++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index fe764e8da4..56d38fe825 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -17,8 +17,12 @@ module TokenAuthenticatable end define_method("ensure_#{token_field}") do - write_attribute(token_field, generate_token_for(token_field)) if - read_attribute(token_field).blank? + current_token = read_attribute(token_field) + if current_token.blank? + write_attribute(token_field, generate_token_for(token_field)) + else + current_token + end end define_method("reset_#{token_field}!") do diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index 1b55317341..a9b0b64e5d 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -13,7 +13,7 @@ describe User, 'TokenAuthenticatable' do let(:token_field) { :authentication_token } it_behaves_like 'TokenAuthenticatable' - describe 'ensured authentication token' do + describe 'ensures authentication token' do subject { create(:user).send(token_field) } it { is_expected.to be_a String } end @@ -29,6 +29,13 @@ describe ApplicationSetting, 'TokenAuthenticatable' do context 'token is not generated yet' do it { expect(token).to be nil } + + describe 'ensured token' do + subject { described_class.new.send("ensure_#{token_field}") } + + it { is_expected.to be_a String } + it { is_expected.to_not be_blank } + end end context 'token is generated' do From 6b0c0d5bcc7940c7d84b8af965b2c4f9ceeb4175 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 11 Dec 2015 14:43:44 +0100 Subject: [PATCH 553/908] Ensure that runners registration token is present --- app/views/ci/admin/runners/index.html.haml | 2 +- lib/ci/api/helpers.rb | 2 +- spec/features/ci/admin/runners_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/ci/admin/runners/index.html.haml index 95bc8ebd40..0574ed3801 100644 --- a/app/views/ci/admin/runners/index.html.haml +++ b/app/views/ci/admin/runners/index.html.haml @@ -3,7 +3,7 @@ To register a new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication. Registration token is - %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token} + %code{ id: 'runners-token' } #{current_application_settings.ensure_runners_registration_token} .bs-callout.clearfix .pull-left diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 12d7396189..abd1869efc 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -23,7 +23,7 @@ module Ci end def runner_registration_token_valid? - params[:token] == current_application_settings.runners_registration_token + params[:token] == current_application_settings.ensure_runners_registration_token end def update_runner_last_contact diff --git a/spec/features/ci/admin/runners_spec.rb b/spec/features/ci/admin/runners_spec.rb index 0c5aebb3a7..fd1967d069 100644 --- a/spec/features/ci/admin/runners_spec.rb +++ b/spec/features/ci/admin/runners_spec.rb @@ -63,7 +63,7 @@ describe "Admin Runners" do end describe 'runners registration token' do - let!(:token) { current_application_settings.runners_registration_token } + let!(:token) { current_application_settings.ensure_runners_registration_token } before { visit ci_admin_runners_path } it 'has a registration token' do From bc1cfd384f4f972ad887b0501280b9f94d4c94ac Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 11 Dec 2015 15:23:43 +0100 Subject: [PATCH 554/908] Stub also `ensure_*` method in CI runners API specs --- spec/requests/ci/api/runners_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index 962d34a0f9..ef40d75766 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -8,6 +8,7 @@ describe Ci::API::API do before do stub_gitlab_calls + stub_application_setting(ensure_runners_registration_token: registration_token) stub_application_setting(runners_registration_token: registration_token) end From 5983e9bdf2aebb455aa7395541bb9d93d6d4de38 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Fri, 11 Dec 2015 16:06:17 +0100 Subject: [PATCH 555/908] Minor fix in flow 'Merge when build succeeds' When a user, which is not the merge user, want to removes the source branch after the automatic merge this might fail because of checks in the DeleteBranchService --- .../widget/open/_merge_when_build_succeeds.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index 08af124274..2168294c68 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -12,7 +12,7 @@ - else The source branch will not be removed. - - remove_source_branch_button = @merge_request.can_remove_source_branch?(current_user) && !should_remove_source_branch + - remove_source_branch_button = @merge_request.can_remove_source_branch?(current_user) && !should_remove_source_branch && @merge_request.merge_user == current_user - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_build_succeeds?(current_user) - if remove_source_branch_button || user_can_cancel_automatic_merge .clearfix.prepend-top-10 From eec4751bd0865498a925e89930465b40ea849ea2 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 11 Dec 2015 16:34:07 +0200 Subject: [PATCH 556/908] Add hover-state for emoji in emoji-picker --- CHANGELOG | 1 + app/assets/stylesheets/pages/issuable.scss | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 61068d3efb..155048b95c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -45,6 +45,7 @@ v 8.3.0 (unreleased) - Accept COPYING,COPYING.lesser, and licence as license file (Zeger-Jan van de Weg) - Fix emoji aliases problem - Fix award-emojis Flash alert's width + - Emoji picker improvements v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 957da5c182..5b5e73e465 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -129,10 +129,18 @@ border-color: $border-color; cursor: pointer; + &:hover { + background-color: #dce0e5; + } + &.active { border-color: $border-gray-light; background-color: $gray-light; + &:hover { + background-color: #dce0e5; + } + .counter { font-weight: bold; } @@ -170,7 +178,18 @@ > li { cursor: pointer; - margin: 5px; + width: 30px; + height: 30px; + text-align: center; + @include border-radius(5px); + + img { + margin-bottom: 2px; + } + + &:hover { + background-color: #ccc; + } } } } From 22999a6fa6b44a835536307da8e5d6a6b349cb49 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 11 Dec 2015 16:16:07 +0100 Subject: [PATCH 557/908] Revert "Merge branch 'remove-redcloth' into 'master' " This reverts commit e426c027b0a2a3aa0dea1d833008f2bfd814f483, reversing changes made to c3676aa156981092b7f03f1a3e74bb819cfa2fc3. --- .gitlab-ci.yml | 3 ++- Gemfile | 1 + Gemfile.lock | 2 ++ spec/features/atom/users_spec.rb | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c614e14e24..a8da3de83f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -117,9 +117,10 @@ flay: - mysql bundler:audit: - script: + script: - "bundle exec bundle-audit update" - "bundle exec bundle-audit check" tags: - ruby - mysql + allow_failure: true diff --git a/Gemfile b/Gemfile index 582a1624f0..7298e21ce6 100644 --- a/Gemfile +++ b/Gemfile @@ -93,6 +93,7 @@ gem 'html-pipeline', '~> 1.11.0' gem 'task_list', '~> 1.0.2', require: 'task_list/railtie' gem 'github-markup', '~> 1.3.1' gem 'redcarpet', '~> 3.3.3' +gem 'RedCloth', '~> 4.2.9' gem 'rdoc', '~>3.6' gem 'org-ruby', '~> 0.9.12' gem 'creole', '~> 0.5.0' diff --git a/Gemfile.lock b/Gemfile.lock index 0db1c6760f..ff57460f5b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,6 +2,7 @@ GEM remote: https://rubygems.org/ specs: CFPropertyList (2.3.2) + RedCloth (4.2.9) ace-rails-ap (2.0.1) actionmailer (4.2.4) actionpack (= 4.2.4) @@ -826,6 +827,7 @@ PLATFORMS ruby DEPENDENCIES + RedCloth (~> 4.2.9) ace-rails-ap (~> 2.0.1) activerecord-deprecated_finders (~> 1.0.3) activerecord-session_store (~> 0.1.0) diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index 5213455633..dc41be8246 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -79,6 +79,6 @@ describe "User Feed", feature: true do end def safe_name - CGI.escapeHTML(user.name) + html_escape(user.name) end end From d597a0a21ab7a589eb8c959e3062632f472ee099 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 11 Dec 2015 16:42:40 +0100 Subject: [PATCH 558/908] Pass all requests from NGINX to gitlab-workhorse --- lib/support/nginx/gitlab | 148 +--------------------------------- lib/support/nginx/gitlab-ssl | 149 +---------------------------------- 2 files changed, 5 insertions(+), 292 deletions(-) diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index 2a79fbdcf9..fc5475c4ee 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -10,34 +10,12 @@ ## If you change this file in a Merge Request, please also create ## a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests ## -################################## -## CHUNKED TRANSFER ## -################################## -## -## It is a known issue that Git-over-HTTP requires chunked transfer encoding [0] -## which is not supported by Nginx < 1.3.9 [1]. As a result, pushing a large object -## with Git (i.e. a single large file) can lead to a 411 error. In theory you can get -## around this by tweaking this configuration file and either: -## - installing an old version of Nginx with the chunkin module [2] compiled in, or -## - using a newer version of Nginx. -## -## At the time of writing we do not know if either of these theoretical solutions works. -## As a workaround users can use Git over SSH to push large files. -## -## [0] https://git.kernel.org/cgit/git/git.git/tree/Documentation/technical/http-protocol.txt#n99 -## [1] https://github.com/agentzh/chunkin-nginx-module#status -## [2] https://github.com/agentzh/chunkin-nginx-module -## ################################### ## configuration ## ################################### ## ## See installation.md#using-https for additional HTTPS configuration details. -upstream gitlab { - server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0; -} - upstream gitlab-workhorse { server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0; } @@ -54,10 +32,6 @@ server { server_tokens off; ## Don't show the nginx version number, a security best practice root /home/git/gitlab/public; - ## Increase this if you want to upload large attachments - ## Or if you want to accept large git objects over http - client_max_body_size 20m; - ## See app/controllers/application_controller.rb for headers set ## Individual nginx logs for this GitLab vhost @@ -65,16 +39,8 @@ server { error_log /var/log/nginx/gitlab_error.log; location / { - ## Serve static files from defined root folder. - ## @gitlab is a named location for the upstream fallback, see below. - try_files $uri /index.html $uri.html @gitlab; - } - - ## We route uploads through GitLab to prevent XSS and enforce access control. - location /uploads/ { - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. - # gzip off; + client_max_body_size 0; + gzip off; ## https://github.com/gitlabhq/gitlabhq/issues/694 ## Some requests take more than 30 seconds. @@ -82,101 +48,7 @@ server { proxy_connect_timeout 300; proxy_redirect off; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Frame-Options SAMEORIGIN; - - proxy_pass http://gitlab; - } - - ## If a file, which is not found in the root folder is requested, - ## then the proxy passes the request to the upsteam (gitlab unicorn). - location @gitlab { - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. - # gzip off; - - ## https://github.com/gitlabhq/gitlabhq/issues/694 - ## Some requests take more than 30 seconds. - proxy_read_timeout 300; - proxy_connect_timeout 300; - proxy_redirect off; - - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Frame-Options SAMEORIGIN; - - proxy_pass http://gitlab; - } - - location ~ ^/[\w\.-]+/[\w\.-]+/gitlab-lfs/objects { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location ~ ^/api/v3/projects/.*/repository/archive { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - # Build artifacts should be submitted to this location - location ~ ^/[\w\.-]+/[\w\.-]+/builds/download { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - # Build artifacts should be submitted to this location - location ~ /ci/api/v1/builds/[0-9]+/artifacts { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location @gitlab-workhorse { - client_max_body_size 0; - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. - # gzip off; - - ## https://github.com/gitlabhq/gitlabhq/issues/694 - ## Some requests take more than 30 seconds. - proxy_read_timeout 300; - proxy_connect_timeout 300; - proxy_redirect off; - - # Do not buffer Git HTTP responses - proxy_buffering off; - - # The following settings only work with NGINX 1.7.11 or newer - # - # # Pass chunked request bodies to gitlab-workhorse as-is - # proxy_request_buffering off; - # proxy_http_version 1.1; + proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; @@ -185,18 +57,4 @@ server { proxy_pass http://gitlab-workhorse; } - - ## Enable gzip compression as per rails guide: - ## http://guides.rubyonrails.org/asset_pipeline.html#gzip-compression - ## WARNING: If you are using relative urls remove the block below - ## See config/application.rb under "Relative url support" for the list of - ## other files that need to be changed for relative url support - location ~ ^/(assets)/ { - root /home/git/gitlab/public; - gzip_static on; # to serve pre-gzipped version - expires max; - add_header Cache-Control public; - } - - error_page 502 /502.html; } diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 79fe147482..1e5f85413e 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -14,34 +14,12 @@ ## If you change this file in a Merge Request, please also create ## a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests ## -################################## -## CHUNKED TRANSFER ## -################################## -## -## It is a known issue that Git-over-HTTP requires chunked transfer encoding [0] -## which is not supported by Nginx < 1.3.9 [1]. As a result, pushing a large object -## with Git (i.e. a single large file) can lead to a 411 error. In theory you can get -## around this by tweaking this configuration file and either: -## - installing an old version of Nginx with the chunkin module [2] compiled in, or -## - using a newer version of Nginx. -## -## At the time of writing we do not know if either of these theoretical solutions works. -## As a workaround users can use Git over SSH to push large files. -## -## [0] https://git.kernel.org/cgit/git/git.git/tree/Documentation/technical/http-protocol.txt#n99 -## [1] https://github.com/agentzh/chunkin-nginx-module#status -## [2] https://github.com/agentzh/chunkin-nginx-module -## ################################### ## configuration ## ################################### ## ## See installation.md#using-https for additional HTTPS configuration details. -upstream gitlab { - server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0; -} - upstream gitlab-workhorse { server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0; } @@ -61,7 +39,6 @@ server { error_log /var/log/nginx/gitlab_error.log; } - ## HTTPS host server { listen 0.0.0.0:443 ssl; @@ -70,10 +47,6 @@ server { server_tokens off; ## Don't show the nginx version number, a security best practice root /home/git/gitlab/public; - ## Increase this if you want to upload large attachments - ## Or if you want to accept large git objects over http - client_max_body_size 20m; - ## Strong SSL Security ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/ ssl on; @@ -110,15 +83,7 @@ server { error_log /var/log/nginx/gitlab_error.log; location / { - ## Serve static files from defined root folder. - ## @gitlab is a named location for the upstream fallback, see below. - try_files $uri /index.html $uri.html @gitlab; - } - - ## We route uploads through GitLab to prevent XSS and enforce access control. - location /uploads/ { - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. + client_max_body_size 0; gzip off; ## https://github.com/gitlabhq/gitlabhq/issues/694 @@ -127,103 +92,7 @@ server { proxy_connect_timeout 300; proxy_redirect off; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Ssl on; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Frame-Options SAMEORIGIN; - - proxy_pass http://gitlab; - } - - ## If a file, which is not found in the root folder is requested, - ## then the proxy passes the request to the upsteam (gitlab unicorn). - location @gitlab { - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. - gzip off; - - ## https://github.com/gitlabhq/gitlabhq/issues/694 - ## Some requests take more than 30 seconds. - proxy_read_timeout 300; - proxy_connect_timeout 300; - proxy_redirect off; - - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Ssl on; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Frame-Options SAMEORIGIN; - - proxy_pass http://gitlab; - } - - location ~ ^/[\w\.-]+/[\w\.-]+/gitlab-lfs/objects { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location ~ ^/api/v3/projects/.*/repository/archive { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - # Build artifacts should be submitted to this location - location ~ ^/[\w\.-]+/[\w\.-]+/builds/download { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - # Build artifacts should be submitted to this location - location ~ /ci/api/v1/builds/[0-9]+/artifacts { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location @gitlab-workhorse { - client_max_body_size 0; - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. - gzip off; - - ## https://github.com/gitlabhq/gitlabhq/issues/694 - ## Some requests take more than 30 seconds. - proxy_read_timeout 300; - proxy_connect_timeout 300; - proxy_redirect off; - - # Do not buffer Git HTTP responses - proxy_buffering off; - - # The following settings only work with NGINX 1.7.11 or newer - # - # # Pass chunked request bodies to gitlab-workhorse as-is - # proxy_request_buffering off; - # proxy_http_version 1.1; + proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; @@ -232,18 +101,4 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://gitlab-workhorse; } - - ## Enable gzip compression as per rails guide: - ## http://guides.rubyonrails.org/asset_pipeline.html#gzip-compression - ## WARNING: If you are using relative urls remove the block below - ## See config/application.rb under "Relative url support" for the list of - ## other files that need to be changed for relative url support - location ~ ^/(assets)/ { - root /home/git/gitlab/public; - gzip_static on; # to serve pre-gzipped version - expires max; - add_header Cache-Control public; - } - - error_page 502 /502.html; } From 0272f27401d25faed97419611a78a968f801a42f Mon Sep 17 00:00:00 2001 From: Greg Smethells Date: Fri, 4 Dec 2015 13:00:07 -0600 Subject: [PATCH 559/908] display referenced merge requests in issue description with CI status --- app/assets/stylesheets/pages/issues.scss | 20 +++++++++++++++ app/controllers/projects/issues_controller.rb | 1 + app/helpers/ci_status_helper.rb | 2 +- app/models/issue.rb | 8 ++++++ .../projects/issues/_merge_requests.html.haml | 25 +++++++++++++++++++ app/views/projects/issues/show.html.haml | 4 +++ 6 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 app/views/projects/issues/_merge_requests.html.haml diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index f5548c5b88..a652b65502 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -60,6 +60,26 @@ form.edit-issue { margin: 0; } +.merge-requests-title { + font-size: 16px; + font-weight: 600; +} + +.merge-request-id { + display: inline-block; + width: 3em; +} + +.merge-request-info { + padding-left: 5px; +} + +.merge-request-status { + color: $gl-gray; + font-size: 15px; + font-weight: bold; +} + .merge-request, .issue { &.today { diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index ae474cf8d6..cf617d53ed 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -62,6 +62,7 @@ class Projects::IssuesController < Projects::ApplicationController @note = @project.notes.new(noteable: @issue) @notes = @issue.notes.nonawards.with_associations.fresh @noteable = @issue + @merge_requests = @issue.referenced_merge_requests respond_with(@issue) end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 8e1f8f9ba6..f8f2cbf131 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -52,7 +52,7 @@ module CiStatusHelper 'circle' end - icon(icon_name) + icon(icon_name + ' fw') end def render_ci_status(ci_commit) diff --git a/app/models/issue.rb b/app/models/issue.rb index 187b6482b6..e04035b3af 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -83,6 +83,14 @@ class Issue < ActiveRecord::Base reference end + def referenced_merge_requests + references = [self, *notes].flat_map do |note| + note.all_references(load_lazy_references: false).merge_requests + end.uniq + + Gitlab::Markdown::ReferenceFilter::LazyReference.load(references).uniq.sort_by(&:iid) + end + # Reset issue events cache # # Since we do cache @event we need to reset cache in special cases: diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml new file mode 100644 index 0000000000..fe856ac991 --- /dev/null +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -0,0 +1,25 @@ +-if @merge_requests.any? + %h2.merge-requests-title + = pluralize(@merge_requests.count, 'Related Merge Request') + %ul.bordered-list + - has_any_ci = @merge_requests.any?(&:ci_commit) + - @merge_requests.each do |merge_request| + %li + %span.merge-request-ci-status + - if merge_request.ci_commit + = render_ci_status(merge_request.ci_commit) + - elsif has_any_ci + = icon('blank fw') + %span.merge-request-id + \##{merge_request.iid} + %span.merge-request-info + %strong + = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" + in + - project = merge_request.target_project + = link_to project.name_with_namespace, namespace_project_path(project.namespace, project) + %span.merge-request-status.prepend-left-10 + - if merge_request.merged? + MERGED + - elsif merge_request.closed? + CLOSED diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index a78d20cc07..38254403fa 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -47,6 +47,10 @@ = markdown(@issue.description, cache_key: [@issue, "description"]) %textarea.hidden.js-task-list-field = @issue.description + + .merge-requests + = render 'merge_requests' + - if @closed_by_merge_requests.present? = render 'projects/issues/closed_by_box' .issue-discussion From 8b4cdc50fca816b4f56f8579e17c4dba836ec797 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 11 Dec 2015 18:01:57 +0100 Subject: [PATCH 560/908] Fix indentation and BuildsEmailService --- app/models/project_services/builds_email_service.rb | 3 +-- app/models/service.rb | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index b8bd48aca6..8247c79fc3 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -79,8 +79,7 @@ class BuildsEmailService < Service end def all_recipients(data) - all_recipients = [] - all_recipients <<= recipients.split(',') + all_recipients = recipients.split(',') if add_pusher? && data[:user][:email] all_recipients << "#{data[:user][:email]}" diff --git a/app/models/service.rb b/app/models/service.rb index 195c4690e8..4159e367d8 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -143,10 +143,10 @@ class Service < ActiveRecord::Base args.each do |arg| class_eval %{ - def #{arg}? - ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg}) - end - } + def #{arg}? + ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg}) + end + } end end From e80e3f5372d6bcad1fbe04a85b3086bb66794828 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 4 Dec 2015 12:55:23 +0100 Subject: [PATCH 561/908] Migrate CI::Project to Project --- app/controllers/admin/builds_controller.rb | 23 ++ .../admin/runner_projects_controller.rb | 35 +++ app/controllers/admin/runners_controller.rb | 63 +++++ .../ci/admin/application_controller.rb | 10 - .../admin/application_settings_controller.rb | 31 --- app/controllers/ci/admin/builds_controller.rb | 18 -- app/controllers/ci/admin/events_controller.rb | 9 - .../ci/admin/projects_controller.rb | 19 -- .../ci/admin/runner_projects_controller.rb | 34 --- .../ci/admin/runners_controller.rb | 73 ------ app/controllers/ci/application_controller.rb | 12 +- app/controllers/ci/lints_controller.rb | 2 +- app/controllers/ci/projects_controller.rb | 12 +- .../ci/runner_projects_controller.rb | 34 --- .../projects/application_controller.rb | 4 - app/controllers/projects/builds_controller.rb | 7 +- .../projects/ci_settings_controller.rb | 36 --- app/controllers/projects/commit_controller.rb | 1 - app/controllers/projects/graphs_controller.rb | 10 +- .../projects/runner_projects_controller.rb | 26 ++ .../projects/runners_controller.rb | 15 +- .../projects/triggers_controller.rb | 9 +- .../projects/variables_controller.rb | 7 +- app/controllers/projects_controller.rb | 4 +- app/helpers/ci/gitlab_helper.rb | 17 -- app/helpers/ci/projects_helper.rb | 36 --- app/helpers/ci_badge_helper.rb | 11 + app/helpers/ci_status_helper.rb | 7 +- app/helpers/graph_helper.rb | 10 + app/helpers/runners_helper.rb | 2 +- app/helpers/yaml_helper.rb | 11 + app/models/ci/application_setting.rb | 38 --- app/models/ci/build.rb | 51 ++-- app/models/ci/commit.rb | 22 +- app/models/ci/event.rb | 27 -- app/models/ci/project.rb | 151 ----------- app/models/ci/project_status.rb | 31 --- app/models/ci/runner.rb | 8 +- app/models/ci/runner_project.rb | 4 +- app/models/ci/trigger.rb | 2 +- app/models/ci/variable.rb | 4 +- app/models/commit_status.rb | 4 +- app/models/project.rb | 72 ++++-- .../project_services/gitlab_ci_service.rb | 73 +----- app/models/service.rb | 3 +- app/models/user.rb | 5 +- app/services/ci/create_builds_service.rb | 3 +- app/services/ci/create_commit_service.rb | 2 +- .../ci/create_trigger_request_service.rb | 6 +- app/services/ci/event_service.rb | 31 --- app/services/ci/image_for_build_service.rb | 4 +- app/services/ci/register_build_service.rb | 9 +- app/services/ci/test_hook_service.rb | 7 - app/services/projects/fork_service.rb | 15 +- app/views/admin/builds/_build.html.haml | 73 ++++++ app/views/admin/builds/index.html.haml | 50 ++++ .../{ci => }/admin/runners/_runner.html.haml | 12 +- .../{ci => }/admin/runners/index.html.haml | 4 +- .../{ci => }/admin/runners/show.html.haml | 39 ++- .../{ci => }/admin/runners/update.js.haml | 0 .../application_settings/_form.html.haml | 24 -- .../admin/application_settings/show.html.haml | 3 - app/views/ci/admin/builds/_build.html.haml | 34 --- app/views/ci/admin/builds/index.html.haml | 28 -- app/views/ci/admin/events/index.html.haml | 18 -- .../ci/admin/projects/_project.html.haml | 29 --- app/views/ci/admin/projects/index.html.haml | 16 -- .../ci/admin/runner_projects/index.html.haml | 57 ---- app/views/ci/commits/_commit.html.haml | 5 +- app/views/ci/shared/_guide.html.haml | 2 +- app/views/ci/user_sessions/new.html.haml | 7 - app/views/layouts/ci/_nav_admin.html.haml | 35 --- app/views/layouts/ci/_nav_project.html.haml | 12 - app/views/layouts/ci/admin.html.haml | 11 - app/views/layouts/ci/application.html.haml | 11 - app/views/layouts/nav/_admin.html.haml | 15 +- .../layouts/nav/_project_settings.html.haml | 5 - app/views/notify/build_fail_email.html.haml | 4 +- app/views/notify/build_fail_email.text.erb | 2 +- .../notify/build_success_email.html.haml | 4 +- app/views/notify/build_success_email.text.erb | 2 +- app/views/projects/builds/index.html.haml | 4 +- app/views/projects/builds/show.html.haml | 4 +- .../projects/ci_settings/_form.html.haml | 120 --------- .../ci_settings/_no_runners.html.haml | 8 - app/views/projects/ci_settings/edit.html.haml | 6 - app/views/projects/commit/_builds.html.haml | 18 +- .../commit_statuses/_commit_status.html.haml | 2 +- app/views/projects/edit.html.haml | 56 ++++ app/views/projects/graphs/ci/_overall.haml | 11 +- app/views/projects/runners/_runner.html.haml | 6 +- .../runners/_shared_runners.html.haml | 6 +- .../runners/_specific_runners.html.haml | 2 +- app/views/projects/triggers/index.html.haml | 6 +- app/views/projects/variables/show.html.haml | 10 +- config/routes.rb | 55 ++-- .../20151203162135_add_ci_to_project.rb | 10 + .../20151204110124_add_project_id_to_ci.rb | 8 + .../20151204110613_migrate_ci_to_project.rb | 36 +++ .../20151204110832_add_index_to_ci_tables.rb | 11 + .../20151204123933_drop_null_for_ci_tables.rb | 9 + db/schema.rb | 29 ++- doc/api/projects.md | 3 - features/project/service.feature | 6 - features/steps/project/commits/commits.rb | 2 +- features/steps/project/merge_requests.rb | 2 +- features/steps/project/services.rb | 10 - features/steps/shared/project.rb | 2 +- lib/api/commit_statuses.rb | 2 +- lib/api/entities.rb | 1 + lib/api/helpers.rb | 7 +- lib/api/projects.rb | 6 + lib/ci/api/api.rb | 2 - lib/ci/api/commits.rb | 66 ----- lib/ci/api/projects.rb | 157 ----------- lib/ci/api/runners.rb | 15 +- lib/ci/api/triggers.rb | 2 +- lib/ci/charts.rb | 6 +- lib/ci/current_settings.rb | 22 -- lib/ci/scheduler.rb | 16 -- lib/gitlab/backend/grack_auth.rb | 4 +- lib/gitlab/build_data_builder.rb | 2 +- lib/tasks/ci/schedule_builds.rake | 6 - spec/factories/ci/builds.rb | 4 + spec/factories/ci/commits.rb | 2 +- spec/factories/ci/events.rb | 24 -- spec/factories/ci/projects.rb | 50 ---- spec/factories/ci/runner_projects.rb | 2 +- .../{ci/admin => atom}/builds_spec.rb | 35 ++- .../{ci/admin => atom}/runners_spec.rb | 10 +- spec/features/builds_spec.rb | 24 +- spec/features/ci/admin/events_spec.rb | 20 -- spec/features/ci/admin/projects_spec.rb | 19 -- spec/features/ci_settings_spec.rb | 22 -- spec/features/commits_spec.rb | 3 +- spec/features/{ci => }/lint_spec.rb | 0 spec/features/runners_spec.rb | 38 +-- spec/features/triggers_spec.rb | 13 +- spec/features/variables_spec.rb | 9 +- spec/lib/gitlab/backend/grack_auth_spec.rb | 9 +- spec/lib/gitlab/build_data_builder_spec.rb | 4 +- spec/models/application_setting_spec.rb | 22 -- spec/models/build_spec.rb | 89 +++---- spec/models/ci/commit_spec.rb | 28 +- spec/models/ci/project_spec.rb | 244 ------------------ spec/models/ci/runner_spec.rb | 14 +- spec/models/ci/trigger_spec.rb | 2 +- spec/models/commit_status_spec.rb | 2 +- .../gitlab_ci_service_spec.rb | 57 ---- spec/models/project_spec.rb | 89 ++++++- spec/requests/api/projects_spec.rb | 12 - spec/requests/ci/api/builds_spec.rb | 28 +- spec/requests/ci/api/commits_spec.rb | 65 ----- spec/requests/ci/api/projects_spec.rb | 159 ------------ spec/requests/ci/api/runners_spec.rb | 27 +- spec/requests/ci/api/triggers_spec.rb | 27 +- .../services/ci/create_commit_service_spec.rb | 4 +- .../ci/create_trigger_request_service_spec.rb | 5 +- spec/services/ci/event_service_spec.rb | 34 --- .../ci/image_for_build_service_spec.rb | 8 +- .../ci/register_build_service_spec.rb | 10 +- 161 files changed, 972 insertions(+), 2660 deletions(-) create mode 100644 app/controllers/admin/builds_controller.rb create mode 100644 app/controllers/admin/runner_projects_controller.rb create mode 100644 app/controllers/admin/runners_controller.rb delete mode 100644 app/controllers/ci/admin/application_controller.rb delete mode 100644 app/controllers/ci/admin/application_settings_controller.rb delete mode 100644 app/controllers/ci/admin/builds_controller.rb delete mode 100644 app/controllers/ci/admin/events_controller.rb delete mode 100644 app/controllers/ci/admin/projects_controller.rb delete mode 100644 app/controllers/ci/admin/runner_projects_controller.rb delete mode 100644 app/controllers/ci/admin/runners_controller.rb delete mode 100644 app/controllers/ci/runner_projects_controller.rb delete mode 100644 app/controllers/projects/ci_settings_controller.rb create mode 100644 app/controllers/projects/runner_projects_controller.rb delete mode 100644 app/helpers/ci/gitlab_helper.rb delete mode 100644 app/helpers/ci/projects_helper.rb create mode 100644 app/helpers/ci_badge_helper.rb create mode 100644 app/helpers/yaml_helper.rb delete mode 100644 app/models/ci/application_setting.rb delete mode 100644 app/models/ci/event.rb delete mode 100644 app/models/ci/project.rb delete mode 100644 app/models/ci/project_status.rb delete mode 100644 app/services/ci/event_service.rb delete mode 100644 app/services/ci/test_hook_service.rb create mode 100644 app/views/admin/builds/_build.html.haml create mode 100644 app/views/admin/builds/index.html.haml rename app/views/{ci => }/admin/runners/_runner.html.haml (62%) rename app/views/{ci => }/admin/runners/index.html.haml (90%) rename app/views/{ci => }/admin/runners/show.html.haml (71%) rename app/views/{ci => }/admin/runners/update.js.haml (100%) delete mode 100644 app/views/ci/admin/application_settings/_form.html.haml delete mode 100644 app/views/ci/admin/application_settings/show.html.haml delete mode 100644 app/views/ci/admin/builds/_build.html.haml delete mode 100644 app/views/ci/admin/builds/index.html.haml delete mode 100644 app/views/ci/admin/events/index.html.haml delete mode 100644 app/views/ci/admin/projects/_project.html.haml delete mode 100644 app/views/ci/admin/projects/index.html.haml delete mode 100644 app/views/ci/admin/runner_projects/index.html.haml delete mode 100644 app/views/ci/user_sessions/new.html.haml delete mode 100644 app/views/layouts/ci/_nav_admin.html.haml delete mode 100644 app/views/layouts/ci/_nav_project.html.haml delete mode 100644 app/views/layouts/ci/admin.html.haml delete mode 100644 app/views/layouts/ci/application.html.haml delete mode 100644 app/views/projects/ci_settings/_form.html.haml delete mode 100644 app/views/projects/ci_settings/_no_runners.html.haml delete mode 100644 app/views/projects/ci_settings/edit.html.haml create mode 100644 db/migrate/20151203162135_add_ci_to_project.rb create mode 100644 db/migrate/20151204110124_add_project_id_to_ci.rb create mode 100644 db/migrate/20151204110613_migrate_ci_to_project.rb create mode 100644 db/migrate/20151204110832_add_index_to_ci_tables.rb create mode 100644 db/migrate/20151204123933_drop_null_for_ci_tables.rb delete mode 100644 lib/ci/api/commits.rb delete mode 100644 lib/ci/api/projects.rb delete mode 100644 lib/ci/current_settings.rb delete mode 100644 lib/ci/scheduler.rb delete mode 100644 lib/tasks/ci/schedule_builds.rake delete mode 100644 spec/factories/ci/events.rb delete mode 100644 spec/factories/ci/projects.rb rename spec/features/{ci/admin => atom}/builds_spec.rb (69%) rename spec/features/{ci/admin => atom}/runners_spec.rb (87%) delete mode 100644 spec/features/ci/admin/events_spec.rb delete mode 100644 spec/features/ci/admin/projects_spec.rb delete mode 100644 spec/features/ci_settings_spec.rb rename spec/features/{ci => }/lint_spec.rb (100%) delete mode 100644 spec/models/ci/project_spec.rb delete mode 100644 spec/models/project_services/gitlab_ci_service_spec.rb delete mode 100644 spec/requests/ci/api/commits_spec.rb delete mode 100644 spec/requests/ci/api/projects_spec.rb delete mode 100644 spec/services/ci/event_service_spec.rb diff --git a/app/controllers/admin/builds_controller.rb b/app/controllers/admin/builds_controller.rb new file mode 100644 index 0000000000..83d9684c70 --- /dev/null +++ b/app/controllers/admin/builds_controller.rb @@ -0,0 +1,23 @@ +class Admin::BuildsController < Admin::ApplicationController + def index + @scope = params[:scope] + @all_builds = Ci::Build + @builds = @all_builds.order('created_at DESC') + @builds = + case @scope + when 'all' + @builds + when 'finished' + @builds.finished + else + @builds.running_or_pending.reverse_order + end + @builds = @builds.page(params[:page]).per(30) + end + + def cancel_all + Ci::Build.running_or_pending.each(&:cancel) + + redirect_to admin_builds_path + end +end diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb new file mode 100644 index 0000000000..20d621742f --- /dev/null +++ b/app/controllers/admin/runner_projects_controller.rb @@ -0,0 +1,35 @@ +class Admin::RunnerProjectsController < Admin::ApplicationController + before_action :project, only: [:create] + + def index + @runner_projects = project.ci_runner_projects.all + @runner_project = project.ci_runner_projects.new + end + + def create + @runner = Ci::Runner.find(params[:runner_project][:runner_id]) + + if @runner.assign_to(@project, current_user) + redirect_to admin_runner_path(@runner) + else + redirect_to admin_runner_path(@runner), alert: 'Failed adding runner to project' + end + end + + def destroy + rp = Ci::RunnerProject.find(params[:id]) + runner = rp.runner + rp.destroy + + redirect_to admin_runner_path(runner) + end + + private + + def project + @project = Project.find_with_namespace( + [params[:namespace_id], '/', params[:project_id]].join('') + ) + @project || render_404 + end +end diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb new file mode 100644 index 0000000000..a701d49b84 --- /dev/null +++ b/app/controllers/admin/runners_controller.rb @@ -0,0 +1,63 @@ +class Admin::RunnersController < Admin::ApplicationController + before_action :runner, except: :index + + def index + @runners = Ci::Runner.order('id DESC') + @runners = @runners.search(params[:search]) if params[:search].present? + @runners = @runners.page(params[:page]).per(30) + @active_runners_cnt = Ci::Runner.online.count + end + + def show + @builds = @runner.builds.order('id DESC').first(30) + @projects = + if params[:search].present? + ::Project.search(params[:search]) + else + Project.all + end + @projects = @projects.where.not(id: @runner.projects.select(:id)) if @runner.projects.any? + @projects = @projects.page(params[:page]).per(30) + end + + def update + @runner.update_attributes(runner_params) + + respond_to do |format| + format.js + format.html { redirect_to admin_runner_path(@runner) } + end + end + + def destroy + @runner.destroy + + redirect_to admin_runners_path + end + + def resume + if @runner.update_attributes(active: true) + redirect_to admin_runners_path, notice: 'Runner was successfully updated.' + else + redirect_to admin_runners_path, alert: 'Runner was not updated.' + end + end + + def pause + if @runner.update_attributes(active: false) + redirect_to admin_runners_path, notice: 'Runner was successfully updated.' + else + redirect_to admin_runners_path, alert: 'Runner was not updated.' + end + end + + private + + def runner + @runner ||= Ci::Runner.find(params[:id]) + end + + def runner_params + params.require(:runner).permit(:token, :description, :tag_list, :active) + end +end diff --git a/app/controllers/ci/admin/application_controller.rb b/app/controllers/ci/admin/application_controller.rb deleted file mode 100644 index 4ec2dc9c2c..0000000000 --- a/app/controllers/ci/admin/application_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Ci - module Admin - class ApplicationController < Ci::ApplicationController - before_action :authenticate_user! - before_action :authenticate_admin! - - layout "ci/admin" - end - end -end diff --git a/app/controllers/ci/admin/application_settings_controller.rb b/app/controllers/ci/admin/application_settings_controller.rb deleted file mode 100644 index 71e253fac6..0000000000 --- a/app/controllers/ci/admin/application_settings_controller.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Ci - class Admin::ApplicationSettingsController < Ci::Admin::ApplicationController - before_action :set_application_setting - - def show - end - - def update - if @application_setting.update_attributes(application_setting_params) - redirect_to ci_admin_application_settings_path, - notice: 'Application settings saved successfully' - else - render :show - end - end - - private - - def set_application_setting - @application_setting = Ci::ApplicationSetting.current - @application_setting ||= Ci::ApplicationSetting.create_from_defaults - end - - def application_setting_params - params.require(:application_setting).permit( - :all_broken_builds, - :add_pusher, - ) - end - end -end diff --git a/app/controllers/ci/admin/builds_controller.rb b/app/controllers/ci/admin/builds_controller.rb deleted file mode 100644 index 38abfdeafb..0000000000 --- a/app/controllers/ci/admin/builds_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Ci - class Admin::BuildsController < Ci::Admin::ApplicationController - def index - @scope = params[:scope] - @builds = Ci::Build.order('created_at DESC').page(params[:page]).per(30) - - @builds = - case @scope - when "pending" - @builds.pending - when "running" - @builds.running - else - @builds - end - end - end -end diff --git a/app/controllers/ci/admin/events_controller.rb b/app/controllers/ci/admin/events_controller.rb deleted file mode 100644 index 5939efff98..0000000000 --- a/app/controllers/ci/admin/events_controller.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Ci - class Admin::EventsController < Ci::Admin::ApplicationController - EVENTS_PER_PAGE = 50 - - def index - @events = Ci::Event.admin.order('created_at DESC').page(params[:page]).per(EVENTS_PER_PAGE) - end - end -end diff --git a/app/controllers/ci/admin/projects_controller.rb b/app/controllers/ci/admin/projects_controller.rb deleted file mode 100644 index 5bbd0ce739..0000000000 --- a/app/controllers/ci/admin/projects_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Ci - class Admin::ProjectsController < Ci::Admin::ApplicationController - def index - @projects = Ci::Project.ordered_by_last_commit_date.page(params[:page]).per(30) - end - - def destroy - project.destroy - - redirect_to ci_projects_url - end - - protected - - def project - @project ||= Ci::Project.find(params[:id]) - end - end -end diff --git a/app/controllers/ci/admin/runner_projects_controller.rb b/app/controllers/ci/admin/runner_projects_controller.rb deleted file mode 100644 index e7de6eb12c..0000000000 --- a/app/controllers/ci/admin/runner_projects_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Ci - class Admin::RunnerProjectsController < Ci::Admin::ApplicationController - layout 'ci/project' - - def index - @runner_projects = project.runner_projects.all - @runner_project = project.runner_projects.new - end - - def create - @runner = Ci::Runner.find(params[:runner_project][:runner_id]) - - if @runner.assign_to(project, current_user) - redirect_to ci_admin_runner_path(@runner) - else - redirect_to ci_admin_runner_path(@runner), alert: 'Failed adding runner to project' - end - end - - def destroy - rp = Ci::RunnerProject.find(params[:id]) - runner = rp.runner - rp.destroy - - redirect_to ci_admin_runner_path(runner) - end - - private - - def project - @project ||= Ci::Project.find(params[:project_id]) - end - end -end diff --git a/app/controllers/ci/admin/runners_controller.rb b/app/controllers/ci/admin/runners_controller.rb deleted file mode 100644 index 0cafad2741..0000000000 --- a/app/controllers/ci/admin/runners_controller.rb +++ /dev/null @@ -1,73 +0,0 @@ -module Ci - class Admin::RunnersController < Ci::Admin::ApplicationController - before_action :runner, except: :index - - def index - @runners = Ci::Runner.order('id DESC') - @runners = @runners.search(params[:search]) if params[:search].present? - @runners = @runners.page(params[:page]).per(30) - @active_runners_cnt = Ci::Runner.online.count - end - - def show - @builds = @runner.builds.order('id DESC').first(30) - @projects = Ci::Project.all - if params[:search].present? - @gl_projects = ::Project.search(params[:search]) - @projects = @projects.where(gitlab_id: @gl_projects.select(:id)) - end - @projects = @projects.where("ci_projects.id NOT IN (?)", @runner.projects.pluck(:id)) if @runner.projects.any? - @projects = @projects.joins(:gl_project) - @projects = @projects.page(params[:page]).per(30) - end - - def update - @runner.update_attributes(runner_params) - - respond_to do |format| - format.js - format.html { redirect_to ci_admin_runner_path(@runner) } - end - end - - def destroy - @runner.destroy - - redirect_to ci_admin_runners_path - end - - def resume - if @runner.update_attributes(active: true) - redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.' - else - redirect_to ci_admin_runners_path, alert: 'Runner was not updated.' - end - end - - def pause - if @runner.update_attributes(active: false) - redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.' - else - redirect_to ci_admin_runners_path, alert: 'Runner was not updated.' - end - end - - def assign_all - Ci::Project.unassigned(@runner).all.each do |project| - @runner.assign_to(project, current_user) - end - - redirect_to ci_admin_runner_path(@runner), notice: "Runner was assigned to all projects" - end - - private - - def runner - @runner ||= Ci::Runner.find(params[:id]) - end - - def runner_params - params.require(:runner).permit(:token, :description, :tag_list, :active) - end - end -end diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb index 848f2b4e31..bc7f48b3c8 100644 --- a/app/controllers/ci/application_controller.rb +++ b/app/controllers/ci/application_controller.rb @@ -4,8 +4,6 @@ module Ci "app/helpers/ci" end - helper_method :gl_project - private def authenticate_token! @@ -15,13 +13,13 @@ module Ci end def authorize_access_project! - unless can?(current_user, :read_project, gl_project) + unless can?(current_user, :read_project, project) return page_404 end end def authorize_manage_builds! - unless can?(current_user, :manage_builds, gl_project) + unless can?(current_user, :manage_builds, project) return page_404 end end @@ -31,7 +29,7 @@ module Ci end def authorize_manage_project! - unless can?(current_user, :admin_project, gl_project) + unless can?(current_user, :admin_project, project) return page_404 end end @@ -58,9 +56,5 @@ module Ci count: count } end - - def gl_project - ::Project.find(@project.gitlab_id) - end end end diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index a4f6aff49b..7ed78ff8e9 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -1,5 +1,5 @@ module Ci - class LintsController < Ci::ApplicationController + class LintsController < ApplicationController before_action :authenticate_user! def show diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 8406399fb6..7e62320bf2 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -3,13 +3,12 @@ module Ci before_action :project, except: [:index] before_action :authenticate_user!, except: [:index, :build, :badge] before_action :authorize_access_project!, except: [:index, :badge] - before_action :authorize_manage_project!, only: [:toggle_shared_runners, :dumped_yaml] before_action :no_cache, only: [:badge] protect_from_forgery def show # Temporary compatibility with CI badges pointing to CI project page - redirect_to namespace_project_path(project.gl_project.namespace, project.gl_project) + redirect_to namespace_project_path(project.namespace, project) end # Project status badge @@ -20,16 +19,11 @@ module Ci send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml" end - def toggle_shared_runners - project.toggle!(:shared_runners_enabled) - - redirect_to namespace_project_runners_path(project.gl_project.namespace, project.gl_project) - end - protected def project - @project ||= Ci::Project.find(params[:id]) + # TODO: what to do here? + @project ||= Project.find_by_ci_id(params[:id]) end def no_cache diff --git a/app/controllers/ci/runner_projects_controller.rb b/app/controllers/ci/runner_projects_controller.rb deleted file mode 100644 index 9d55531336..0000000000 --- a/app/controllers/ci/runner_projects_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Ci - class RunnerProjectsController < Ci::ApplicationController - before_action :authenticate_user! - before_action :project - before_action :authorize_manage_project! - - def create - @runner = Ci::Runner.find(params[:runner_project][:runner_id]) - - return head(403) unless current_user.ci_authorized_runners.include?(@runner) - - path = runners_path(@project.gl_project) - - if @runner.assign_to(project, current_user) - redirect_to path - else - redirect_to path, alert: 'Failed adding runner to project' - end - end - - def destroy - runner_project = project.runner_projects.find(params[:id]) - runner_project.destroy - - redirect_to runners_path(@project.gl_project) - end - - private - - def project - @project ||= Ci::Project.find(params[:project_id]) - end - end -end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 7d0d57858e..dd32d50919 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -31,8 +31,4 @@ class Projects::ApplicationController < ApplicationController def builds_enabled return render_404 unless @project.builds_enabled? end - - def ci_project - @ci_project ||= @project.ensure_gitlab_ci_project - end end diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 4638f77b88..e7e2ab4313 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -1,5 +1,4 @@ class Projects::BuildsController < Projects::ApplicationController - before_action :ci_project before_action :build, except: [:index, :cancel_all] before_action :authorize_manage_builds!, except: [:index, :show, :status] @@ -30,7 +29,7 @@ class Projects::BuildsController < Projects::ApplicationController end def show - @builds = @ci_project.commits.find_by_sha(@build.sha).builds.order('id DESC') + @builds = @project.ci_commits.find_by_sha(@build.sha).builds.order('id DESC') @builds = @builds.where("id not in (?)", @build.id) @commit = @build.commit @@ -77,7 +76,7 @@ class Projects::BuildsController < Projects::ApplicationController private def build - @build ||= ci_project.builds.unscoped.find_by!(id: params[:id]) + @build ||= project.ci_builds.unscoped.find_by!(id: params[:id]) end def artifacts_file @@ -85,7 +84,7 @@ class Projects::BuildsController < Projects::ApplicationController end def build_path(build) - namespace_project_build_path(build.gl_project.namespace, build.gl_project, build) + namespace_project_build_path(build.project.namespace, build.project, build) end def authorize_manage_builds! diff --git a/app/controllers/projects/ci_settings_controller.rb b/app/controllers/projects/ci_settings_controller.rb deleted file mode 100644 index a263242a85..0000000000 --- a/app/controllers/projects/ci_settings_controller.rb +++ /dev/null @@ -1,36 +0,0 @@ -class Projects::CiSettingsController < Projects::ApplicationController - before_action :ci_project - before_action :authorize_admin_project! - - layout "project_settings" - - def edit - end - - def update - if ci_project.update_attributes(project_params) - Ci::EventService.new.change_project_settings(current_user, ci_project) - - redirect_to edit_namespace_project_ci_settings_path(project.namespace, project), notice: 'Project was successfully updated.' - else - render action: "edit" - end - end - - def destroy - ci_project.destroy - Ci::EventService.new.remove_project(current_user, ci_project) - project.gitlab_ci_service.update_attributes(active: false) - - redirect_to project_path(project), notice: "CI was disabled for this project" - end - - protected - - def project_params - params.require(:project).permit(:path, :timeout, :timeout_in_minutes, :default_ref, :always_build, - :polling_interval, :public, :ssh_url_to_repo, :allow_git_fetch, :email_recipients, - :email_add_pusher, :email_only_broken_builds, :coverage_regex, :shared_runners_enabled, :token, - { variables_attributes: [:id, :key, :value, :_destroy] }) - end -end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index e8af205b78..0aaba3792b 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -31,7 +31,6 @@ class Projects::CommitController < Projects::ApplicationController end def builds - @ci_project = @project.gitlab_ci_project end def cancel_builds diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index a8f47069bb..d13ea9f34b 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -25,13 +25,11 @@ class Projects::GraphsController < Projects::ApplicationController end def ci - ci_project = @project.gitlab_ci_project - @charts = {} - @charts[:week] = Ci::Charts::WeekChart.new(ci_project) - @charts[:month] = Ci::Charts::MonthChart.new(ci_project) - @charts[:year] = Ci::Charts::YearChart.new(ci_project) - @charts[:build_times] = Ci::Charts::BuildTime.new(ci_project) + @charts[:week] = Ci::Charts::WeekChart.new(project) + @charts[:month] = Ci::Charts::MonthChart.new(project) + @charts[:year] = Ci::Charts::YearChart.new(project) + @charts[:build_times] = Ci::Charts::BuildTime.new(project) end def languages diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb new file mode 100644 index 0000000000..6986338735 --- /dev/null +++ b/app/controllers/projects/runner_projects_controller.rb @@ -0,0 +1,26 @@ +class Projects::RunnerProjectsController < Projects::ApplicationController + before_action :authorize_admin_project! + + layout 'project_settings' + + def create + @runner = Ci::Runner.find(params[:runner_project][:runner_id]) + + return head(403) unless current_user.ci_authorized_runners.include?(@runner) + + path = runners_path(project) + + if @runner.assign_to(project, current_user) + redirect_to path + else + redirect_to path, alert: 'Failed adding runner to project' + end + end + + def destroy + runner_project = project.ci_runner_projects.find(params[:id]) + runner_project.destroy + + redirect_to runners_path(project) + end +end diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index bfbcf2567f..863c5d131a 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -1,14 +1,13 @@ class Projects::RunnersController < Projects::ApplicationController - before_action :ci_project before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show] before_action :authorize_admin_project! layout 'project_settings' def index - @runners = @ci_project.runners.ordered + @runners = project.ci_runners.ordered @specific_runners = current_user.ci_authorized_runners. - where.not(id: @ci_project.runners). + where.not(id: project.ci_runners). ordered.page(params[:page]).per(20) @shared_runners = Ci::Runner.shared.active @shared_runners_count = @shared_runners.count(:all) @@ -26,7 +25,7 @@ class Projects::RunnersController < Projects::ApplicationController end def destroy - if @runner.only_for?(@ci_project) + if @runner.only_for?(project) @runner.destroy end @@ -52,10 +51,16 @@ class Projects::RunnersController < Projects::ApplicationController def show end + def toggle_shared_runners + project.toggle!(:shared_runners_enabled) + + redirect_to namespace_project_runners_path(project.namespace, project) + end + protected def set_runner - @runner ||= @ci_project.runners.find(params[:id]) + @runner ||= project.ci_runners.find(params[:id]) end def runner_params diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb index 782ebd01b0..421e648a2d 100644 --- a/app/controllers/projects/triggers_controller.rb +++ b/app/controllers/projects/triggers_controller.rb @@ -1,22 +1,21 @@ class Projects::TriggersController < Projects::ApplicationController - before_action :ci_project before_action :authorize_admin_project! layout 'project_settings' def index - @triggers = @ci_project.triggers + @triggers = project.ci_triggers @trigger = Ci::Trigger.new end def create - @trigger = @ci_project.triggers.new + @trigger = project.ci_triggers.new @trigger.save if @trigger.valid? redirect_to namespace_project_triggers_path(@project.namespace, @project) else - @triggers = @ci_project.triggers.select(&:persisted?) + @triggers = project.ci_triggers.select(&:persisted?) render :index end end @@ -30,6 +29,6 @@ class Projects::TriggersController < Projects::ApplicationController private def trigger - @trigger ||= @ci_project.triggers.find(params[:id]) + @trigger ||= project.ci_triggers.find(params[:id]) end end diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index d6561a45a7..1dab978f46 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -1,5 +1,4 @@ class Projects::VariablesController < Projects::ApplicationController - before_action :ci_project before_action :authorize_admin_project! layout 'project_settings' @@ -8,9 +7,7 @@ class Projects::VariablesController < Projects::ApplicationController end def update - if ci_project.update_attributes(project_params) - Ci::EventService.new.change_project_settings(current_user, ci_project) - + if project.update_attributes(project_params) redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.' else render action: 'show' @@ -20,6 +17,6 @@ class Projects::VariablesController < Projects::ApplicationController private def project_params - params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] }) + params.require(:project).permit({ ci_variables_attributes: [:id, :key, :value, :_destroy] }) end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 10c75370d7..e9917109f3 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -210,10 +210,10 @@ class ProjectsController < ApplicationController def project_params params.require(:project).permit( - :name, :path, :description, :issues_tracker, :tag_list, + :name, :path, :description, :issues_tracker, :tag_list, :token, :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, - :builds_enabled + :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, ) end diff --git a/app/helpers/ci/gitlab_helper.rb b/app/helpers/ci/gitlab_helper.rb deleted file mode 100644 index e34c8be1df..0000000000 --- a/app/helpers/ci/gitlab_helper.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Ci - module GitlabHelper - def no_turbolink - { :"data-no-turbolink" => "data-no-turbolink" } - end - - def yaml_web_editor_link(project) - commits = project.commits - - if commits.any? && commits.last.ci_yaml_file - "#{project.gitlab_url}/edit/master/.gitlab-ci.yml" - else - "#{project.gitlab_url}/new/master" - end - end - end -end diff --git a/app/helpers/ci/projects_helper.rb b/app/helpers/ci/projects_helper.rb deleted file mode 100644 index fd991a4165..0000000000 --- a/app/helpers/ci/projects_helper.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Ci - module ProjectsHelper - def ref_tab_class ref = nil - 'active' if ref == @ref - end - - def success_ratio(success_builds, failed_builds) - failed_builds = failed_builds.count(:all) - success_builds = success_builds.count(:all) - - return 100 if failed_builds.zero? - - ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100 - ratio.to_i - end - - def markdown_badge_code(project, ref) - url = status_ci_project_url(project, ref: ref, format: 'png') - "[![build status](#{url})](#{ci_project_url(project, ref: ref)})" - end - - def html_badge_code(project, ref) - url = status_ci_project_url(project, ref: ref, format: 'png') - "" - end - - def project_uses_specific_runner?(project) - project.runners.any? - end - - def no_runners_for_project?(project) - project.runners.blank? && - Ci::Runner.shared.blank? - end - end -end diff --git a/app/helpers/ci_badge_helper.rb b/app/helpers/ci_badge_helper.rb new file mode 100644 index 0000000000..a81edbcb41 --- /dev/null +++ b/app/helpers/ci_badge_helper.rb @@ -0,0 +1,11 @@ +module CiBadgeHelper + def markdown_badge_code(project, ref) + url = status_ci_project_url(project, ref: ref, format: 'png') + "[![build status](#{url})](#{ci_project_url(project, ref: ref)})" + end + + def html_badge_code(project, ref) + url = status_ci_project_url(project, ref: ref, format: 'png') + "" + end +end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 8e1f8f9ba6..7feeaa1730 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -1,6 +1,6 @@ module CiStatusHelper def ci_status_path(ci_commit) - project = ci_commit.gl_project + project = ci_commit.project builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha) end @@ -63,4 +63,9 @@ module CiStatusHelper ci_status_icon(ci_commit) end end + + def no_runners_for_project?(project) + project.ci_runners.blank? && + Ci::Runner.shared.blank? + end end diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb index 1e372d5631..c2ab80f2e0 100644 --- a/app/helpers/graph_helper.rb +++ b/app/helpers/graph_helper.rb @@ -16,4 +16,14 @@ module GraphHelper ids = parents.map { |p| p.id } ids.zip(parent_spaces) end + + def success_ratio(success_builds, failed_builds) + failed_builds = failed_builds.count(:all) + success_builds = success_builds.count(:all) + + return 100 if failed_builds.zero? + + ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100 + ratio.to_i + end end diff --git a/app/helpers/runners_helper.rb b/app/helpers/runners_helper.rb index 46eb82a354..9fb42487a7 100644 --- a/app/helpers/runners_helper.rb +++ b/app/helpers/runners_helper.rb @@ -19,7 +19,7 @@ module RunnersHelper id = "\##{runner.id}" if current_user && current_user.admin - link_to ci_admin_runner_path(runner) do + link_to admin_runner_path(runner) do display_name + id end else diff --git a/app/helpers/yaml_helper.rb b/app/helpers/yaml_helper.rb new file mode 100644 index 0000000000..17990e1f47 --- /dev/null +++ b/app/helpers/yaml_helper.rb @@ -0,0 +1,11 @@ +module YamlHelper + def yaml_web_editor_link(project) + commits = project.ci_commits + + if commits.any? && commits.last.ci_yaml_file + "#{project.gitlab_url}/edit/master/.gitlab-ci.yml" + else + "#{project.gitlab_url}/new/master" + end + end +end diff --git a/app/models/ci/application_setting.rb b/app/models/ci/application_setting.rb deleted file mode 100644 index 7f5df8ce6c..0000000000 --- a/app/models/ci/application_setting.rb +++ /dev/null @@ -1,38 +0,0 @@ -# == Schema Information -# -# Table name: ci_application_settings -# -# id :integer not null, primary key -# all_broken_builds :boolean -# add_pusher :boolean -# created_at :datetime -# updated_at :datetime -# - -module Ci - class ApplicationSetting < ActiveRecord::Base - extend Ci::Model - CACHE_KEY = 'ci_application_setting.last' - - after_commit do - Rails.cache.write(CACHE_KEY, self) - end - - def self.expire - Rails.cache.delete(CACHE_KEY) - end - - def self.current - Rails.cache.fetch(CACHE_KEY) do - Ci::ApplicationSetting.last - end - end - - def self.create_from_defaults - create( - all_broken_builds: Settings.gitlab_ci['all_broken_builds'], - add_pusher: Settings.gitlab_ci['add_pusher'], - ) - end - end -end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 564041e321..43ed8eb518 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -84,6 +84,7 @@ module Ci new_build.options = build.options new_build.commands = build.commands new_build.tag_list = build.tag_list + new_build.gl_project_id = build.gl_project_id new_build.commit_id = build.commit_id new_build.name = build.name new_build.allow_failure = build.allow_failure @@ -101,14 +102,9 @@ module Ci end after_transition any => [:success, :failed, :canceled] do |build, transition| - return unless build.gl_project - - project = build.project - - if project.coverage_enabled? - build.update_coverage - end + return unless build.project + build.update_coverage build.commit.create_next_builds(build) build.execute_hooks end @@ -119,7 +115,7 @@ module Ci end def retryable? - commands.present? + project.builds_enabled? && commands.present? end def retried? @@ -132,7 +128,7 @@ module Ci end def timeout - project.timeout + project.build_timeout end def variables @@ -151,26 +147,21 @@ module Ci project.name end - def project_recipients - recipients = project.email_recipients.split(' ') - - if project.email_add_pusher? && user.present? && user.notification_email.present? - recipients << user.notification_email - end - - recipients.uniq - end - def repo_url - project.repo_url_with_auth + auth = "gitlab-ci-token:#{token}@" + project.http_url_to_repo.sub(/^https?:\/\//) do |prefix| + prefix + auth + end end def allow_git_fetch - project.allow_git_fetch + project.build_allow_git_fetch end def update_coverage - coverage = extract_coverage(trace, project.coverage_regex) + coverage_regex = project.build_coverage_regex + return unless coverage_regex + coverage = extract_coverage(trace, coverage_regex) if coverage.is_a? Numeric update_attributes(coverage: coverage) @@ -239,20 +230,20 @@ module Ci def target_url Gitlab::Application.routes.url_helpers. - namespace_project_build_url(gl_project.namespace, gl_project, self) + namespace_project_build_url(project.namespace, project, self) end def cancel_url if active? Gitlab::Application.routes.url_helpers. - cancel_namespace_project_build_path(gl_project.namespace, gl_project, self) + cancel_namespace_project_build_path(project.namespace, project, self) end end def retry_url if retryable? Gitlab::Application.routes.url_helpers. - retry_namespace_project_build_path(gl_project.namespace, gl_project, self) + retry_namespace_project_build_path(project.namespace, project, self) end end @@ -271,16 +262,18 @@ module Ci def download_url if artifacts_file.exists? Gitlab::Application.routes.url_helpers. - download_namespace_project_build_path(gl_project.namespace, gl_project, self) + download_namespace_project_build_path(project.namespace, project, self) end end def execute_hooks build_data = Gitlab::BuildDataBuilder.build(self) - gl_project.execute_hooks(build_data.dup, :build_hooks) - gl_project.execute_services(build_data.dup, :build_hooks) + project.execute_hooks(build_data.dup, :build_hooks) + project.execute_services(build_data.dup, :build_hooks) end + + private def yaml_variables @@ -294,7 +287,7 @@ module Ci end def project_variables - project.variables.map do |variable| + project.ci_variables.map do |variable| { key: variable.key, value: variable.value, public: false } end end diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index e63f779094..7919334454 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -20,8 +20,8 @@ module Ci class Commit < ActiveRecord::Base extend Ci::Model - belongs_to :gl_project, class_name: '::Project', foreign_key: :gl_project_id - has_many :statuses, dependent: :destroy, class_name: 'CommitStatus' + belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id + has_many :statuses, class_name: 'CommitStatus' has_many :builds, class_name: 'Ci::Build' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' @@ -38,10 +38,6 @@ module Ci sha end - def project - @project ||= gl_project.ensure_gitlab_ci_project - end - def project_id project.id end @@ -79,7 +75,7 @@ module Ci end def commit_data - @commit ||= gl_project.commit(sha) + @commit ||= project.commit(sha) rescue nil end @@ -187,11 +183,9 @@ module Ci end def coverage - if project.coverage_enabled? - coverage_array = latest_builds.map(&:coverage).compact - if coverage_array.size >= 1 - '%.2f' % (coverage_array.reduce(:+) / coverage_array.size) - end + coverage_array = latest_builds.map(&:coverage).compact + if coverage_array.size >= 1 + '%.2f' % (coverage_array.reduce(:+) / coverage_array.size) end end @@ -201,7 +195,7 @@ module Ci def config_processor return nil unless ci_yaml_file - @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, gl_project.path_with_namespace) + @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace) rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e save_yaml_error(e.message) nil @@ -211,7 +205,7 @@ module Ci end def ci_yaml_file - @ci_yaml_file ||= gl_project.repository.blob_at(sha, '.gitlab-ci.yml').data + @ci_yaml_file ||= project.repository.blob_at(sha, '.gitlab-ci.yml').data rescue nil end diff --git a/app/models/ci/event.rb b/app/models/ci/event.rb deleted file mode 100644 index 8c39be4267..0000000000 --- a/app/models/ci/event.rb +++ /dev/null @@ -1,27 +0,0 @@ -# == Schema Information -# -# Table name: ci_events -# -# id :integer not null, primary key -# project_id :integer -# user_id :integer -# is_admin :integer -# description :text -# created_at :datetime -# updated_at :datetime -# - -module Ci - class Event < ActiveRecord::Base - extend Ci::Model - - belongs_to :project, class_name: 'Ci::Project' - - validates :description, - presence: true, - length: { in: 5..200 } - - scope :admin, ->(){ where(is_admin: true) } - scope :project_wide, ->(){ where(is_admin: false) } - end -end diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb deleted file mode 100644 index 79ff7e1dcd..0000000000 --- a/app/models/ci/project.rb +++ /dev/null @@ -1,151 +0,0 @@ -# == Schema Information -# -# Table name: ci_projects -# -# id :integer not null, primary key -# name :string(255) -# timeout :integer default(3600), not null -# created_at :datetime -# updated_at :datetime -# token :string(255) -# default_ref :string(255) -# path :string(255) -# always_build :boolean default(FALSE), not null -# polling_interval :integer -# public :boolean default(FALSE), not null -# ssh_url_to_repo :string(255) -# gitlab_id :integer -# allow_git_fetch :boolean default(TRUE), not null -# email_recipients :string(255) default(""), not null -# email_add_pusher :boolean default(TRUE), not null -# email_only_broken_builds :boolean default(TRUE), not null -# skip_refs :string(255) -# coverage_regex :string(255) -# shared_runners_enabled :boolean default(FALSE) -# generated_yaml_config :text -# - -module Ci - class Project < ActiveRecord::Base - extend Ci::Model - - include Ci::ProjectStatus - - belongs_to :gl_project, class_name: '::Project', foreign_key: :gitlab_id - - has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' - has_many :runners, through: :runner_projects, class_name: 'Ci::Runner' - has_many :events, dependent: :destroy, class_name: 'Ci::Event' - has_many :variables, dependent: :destroy, class_name: 'Ci::Variable' - has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger' - - accepts_nested_attributes_for :variables, allow_destroy: true - - delegate :name_with_namespace, :path_with_namespace, :web_url, :http_url_to_repo, :ssh_url_to_repo, to: :gl_project - - # - # Validations - # - validates_presence_of :timeout, :token, :default_ref, :gitlab_id - - validates_uniqueness_of :gitlab_id - - validates :polling_interval, - presence: true, - if: ->(project) { project.always_build.present? } - - before_validation :set_default_values - - class << self - include Ci::CurrentSettings - - def unassigned(runner) - joins("LEFT JOIN #{Ci::RunnerProject.table_name} ON #{Ci::RunnerProject.table_name}.project_id = #{Ci::Project.table_name}.id " \ - "AND #{Ci::RunnerProject.table_name}.runner_id = #{runner.id}"). - where("#{Ci::RunnerProject.table_name}.project_id" => nil) - end - - def ordered_by_last_commit_date - last_commit_subquery = "(SELECT gl_project_id, MAX(committed_at) committed_at FROM #{Ci::Commit.table_name} GROUP BY gl_project_id)" - joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.gitlab_id = last_commit.gl_project_id"). - joins(:gl_project). - order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC") - end - end - - def name - name_with_namespace - end - - def path - path_with_namespace - end - - def gitlab_url - web_url - end - - def any_runners?(&block) - if runners.active.any?(&block) - return true - end - - shared_runners_enabled && Ci::Runner.shared.active.any?(&block) - end - - def set_default_values - self.token = SecureRandom.hex(15) if self.token.blank? - self.default_ref ||= 'master' - end - - def tracked_refs - @tracked_refs ||= default_ref.split(",").map { |ref| ref.strip } - end - - def valid_token? token - self.token && self.token == token - end - - def no_running_builds? - # Get running builds not later than 3 days ago to ignore hangs - builds.running.where("updated_at > ?", 3.days.ago).empty? - end - - def email_notification? - email_add_pusher || email_recipients.present? - end - - def timeout_in_minutes - timeout / 60 - end - - def timeout_in_minutes=(value) - self.timeout = value.to_i * 60 - end - - def coverage_enabled? - coverage_regex.present? - end - - # Build a clone-able repo url - # using http and basic auth - def repo_url_with_auth - auth = "gitlab-ci-token:#{token}@" - http_url_to_repo.sub(/^https?:\/\//) do |prefix| - prefix + auth - end - end - - def setup_finished? - commits.any? - end - - def commits - gl_project.ci_commits.ordered - end - - def builds - gl_project.ci_builds - end - end -end diff --git a/app/models/ci/project_status.rb b/app/models/ci/project_status.rb deleted file mode 100644 index 2d35aeac22..0000000000 --- a/app/models/ci/project_status.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Ci - module ProjectStatus - def status - last_commit.status if last_commit - end - - def broken? - last_commit.failed? if last_commit - end - - def success? - last_commit.success? if last_commit - end - - def broken_or_success? - broken? || success? - end - - def last_commit - @last_commit ||= commits.last if commits.any? - end - - def last_commit_date - last_commit.try(:created_at) - end - - def human_status - status - end - end -end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 8971048581..aa445db7eb 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -25,7 +25,7 @@ module Ci has_many :builds, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' - has_many :projects, through: :runner_projects, class_name: 'Ci::Project' + has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build' @@ -45,10 +45,6 @@ module Ci query: "%#{query.try(:downcase)}%") end - def gl_projects_ids - projects.select(:gitlab_id) - end - def set_default_values self.token = SecureRandom.hex(15) if self.token.blank? end @@ -56,7 +52,7 @@ module Ci def assign_to(project, current_user = nil) self.is_shared = false if shared? self.save - project.runner_projects.create!(runner_id: self.id) + project.ci_runner_projects.create!(runner_id: self.id) end def display_name diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb index 3f4fc43873..93d9be144e 100644 --- a/app/models/ci/runner_project.rb +++ b/app/models/ci/runner_project.rb @@ -14,8 +14,8 @@ module Ci extend Ci::Model belongs_to :runner, class_name: 'Ci::Runner' - belongs_to :project, class_name: 'Ci::Project' + belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id - validates_uniqueness_of :runner_id, scope: :project_id + validates_uniqueness_of :runner_id, scope: :gl_project_id end end diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index b73c35d5ae..23516709a4 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -16,7 +16,7 @@ module Ci acts_as_paranoid - belongs_to :project, class_name: 'Ci::Project' + belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' validates_presence_of :token diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index b3d2b809e0..56759d3e50 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -15,10 +15,10 @@ module Ci class Variable < ActiveRecord::Base extend Ci::Model - belongs_to :project, class_name: 'Ci::Project' + belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id validates_presence_of :key - validates_uniqueness_of :key, scope: :project_id + validates_uniqueness_of :key, scope: :gl_project_id attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index ff619965a5..579b638706 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -30,6 +30,7 @@ class CommitStatus < ActiveRecord::Base self.table_name = 'ci_builds' + belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id belongs_to :commit, class_name: 'Ci::Commit' belongs_to :user @@ -49,6 +50,7 @@ class CommitStatus < ActiveRecord::Base scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) } scope :ordered, -> { order(:ref, :stage_idx, :name) } scope :for_ref, ->(ref) { where(ref: ref) } + scope :has_coverage?, -> { where.not(coverage: nil).any? } state_machine :status, initial: :pending do event :run do @@ -86,7 +88,7 @@ class CommitStatus < ActiveRecord::Base state :canceled, value: 'canceled' end - delegate :sha, :short_sha, :gl_project, + delegate :sha, :short_sha, :project, to: :commit, prefix: false # TODO: this should be removed with all references diff --git a/app/models/project.rb b/app/models/project.rb index 60ca2cad6a..e3eee36c25 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -56,6 +56,7 @@ class Project < ActiveRecord::Base default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :wall_enabled, false default_value_for :snippets_enabled, gitlab_config_features.snippets + default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } # set last_activity_at to the same as created_at after_create :set_last_activity_at @@ -77,7 +78,6 @@ class Project < ActiveRecord::Base # Project services has_many :services - has_one :gitlab_ci_service, dependent: :destroy has_one :campfire_service, dependent: :destroy has_one :drone_ci_service, dependent: :destroy has_one :emails_on_push_service, dependent: :destroy @@ -122,14 +122,21 @@ class Project < ActiveRecord::Base has_many :deploy_keys, through: :deploy_keys_projects has_many :users_star_projects, dependent: :destroy has_many :starrers, through: :users_star_projects, source: :user - has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id - has_many :ci_builds, through: :ci_commits, source: :builds, dependent: :destroy, class_name: 'Ci::Build' has_many :releases, dependent: :destroy has_many :lfs_objects_projects, dependent: :destroy has_many :lfs_objects, through: :lfs_objects_projects has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" - has_one :gitlab_ci_project, dependent: :destroy, class_name: "Ci::Project", foreign_key: :gitlab_id + + has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id + has_many :ci_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id + has_many :ci_builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the ci_statuses + has_many :ci_runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id + has_many :ci_runners, through: :ci_runner_projects, source: :runner, class_name: 'Ci::Runner' + has_many :ci_variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id + has_many :ci_triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id + + accepts_nested_attributes_for :ci_variables, allow_destroy: true delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true @@ -162,6 +169,11 @@ class Project < ActiveRecord::Base if: ->(project) { project.avatar.present? && project.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } + before_validation :set_random_token + def set_random_token + self.token = SecureRandom.hex(15) if self.token.blank? + end + mount_uploader :avatar, AvatarUploader # Scopes @@ -257,6 +269,12 @@ class Project < ActiveRecord::Base projects.iwhere('projects.path' => project_path).take end + def find_by_ci_id(id) + ci_projects = Arel::Table.new(:ci_projects) + gitlab_id = ci_projects.where(ci_projects[:id].eq(id)).project(ci_projects[:gitlab_id]) + find_by("id=(#{gitlab_id.to_sql})") + end + def visibility_levels Gitlab::VisibilityLevel.options end @@ -791,28 +809,6 @@ class Project < ActiveRecord::Base ci_commit(sha) || ci_commits.create(sha: sha) end - def ensure_gitlab_ci_project - gitlab_ci_project || create_gitlab_ci_project( - shared_runners_enabled: current_application_settings.shared_runners_enabled - ) - end - - # TODO: this should be migrated to Project table, - # the same as issues_enabled - def builds_enabled - gitlab_ci_service && gitlab_ci_service.active - end - - def builds_enabled? - builds_enabled - end - - def builds_enabled=(value) - service = gitlab_ci_service || create_gitlab_ci_service - service.active = value - service.save - end - def enable_ci self.builds_enabled = true end @@ -826,4 +822,28 @@ class Project < ActiveRecord::Base forked_project_link.destroy end end + + def any_runners?(&block) + if ci_runners.active.any?(&block) + return true + end + + shared_runners_enabled? && Ci::Runner.shared.active.any?(&block) + end + + def valid_token? token + self.token && self.token == token + end + + def build_coverage_enabled? + build_coverage_regex.present? + end + + def build_timeout_in_minutes + build_timeout / 60 + end + + def build_timeout_in_minutes=(value) + self.build_timeout = value.to_i * 60 + end end diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 234e8e8b58..d73182d40a 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -19,76 +19,5 @@ # class GitlabCiService < CiService - include Gitlab::Application.routes.url_helpers - - after_save :compose_service_hook, if: :activated? - after_save :ensure_gitlab_ci_project, if: :activated? - - def compose_service_hook - hook = service_hook || build_service_hook - hook.save - end - - def ensure_gitlab_ci_project - return unless project - project.ensure_gitlab_ci_project - end - - def supported_events - %w(push tag_push) - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - ci_project = project.gitlab_ci_project - if ci_project - current_user = User.find_by(id: data[:user_id]) - Ci::CreateCommitService.new.execute(ci_project, current_user, data) - end - end - - def token - if project.gitlab_ci_project.present? - project.gitlab_ci_project.token - end - end - - def get_ci_commit(sha, ref) - Ci::Project.find(project.gitlab_ci_project.id).commits.find_by_sha!(sha) - end - - def commit_status(sha, ref) - get_ci_commit(sha, ref).status - rescue ActiveRecord::RecordNotFound - :error - end - - def commit_coverage(sha, ref) - get_ci_commit(sha, ref).coverage - rescue ActiveRecord::RecordNotFound - :error - end - - def build_page(sha, ref) - if project.gitlab_ci_project.present? - builds_namespace_project_commit_url(project.namespace, project, sha) - end - end - - def title - 'GitLab CI' - end - - def description - 'Continuous integration server from GitLab' - end - - def to_param - 'gitlab_ci' - end - - def fields - [] - end + # this is no longer used end diff --git a/app/models/service.rb b/app/models/service.rb index 4159e367d8..d3bf7f0ebd 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -41,7 +41,7 @@ class Service < ActiveRecord::Base validates :project_id, presence: true, unless: Proc.new { |service| service.template? } - scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } + scope :visible, -> { where.not(type: ['GitlabIssueTrackerService', 'GitlabCiService']) } scope :push_hooks, -> { where(push_events: true, active: true) } scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) } @@ -188,7 +188,6 @@ class Service < ActiveRecord::Base external_wiki flowdock gemnasium - gitlab_ci hipchat irker jira diff --git a/app/models/user.rb b/app/models/user.rb index 7155dd2bea..da06b6f3ad 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -769,10 +769,9 @@ class User < ActiveRecord::Base def ci_authorized_runners @ci_authorized_runners ||= begin - runner_ids = Ci::RunnerProject.joins(:project). - where("ci_projects.gitlab_id IN (#{ci_projects_union.to_sql})"). + runner_ids = Ci::RunnerProject. + where("ci_runner_projects.gl_project_id IN (#{ci_projects_union.to_sql})"). select(:runner_id) - Ci::Runner.specific.where(id: runner_ids) end end diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index 847db2d48a..ba7c4632f4 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -29,7 +29,8 @@ module Ci build_attrs.merge!(ref: ref, tag: tag, trigger_request: trigger_request, - user: user) + user: user, + gl_project_id: commit.gl_project_id) build = commit.builds.create!(build_attrs) build.execute_hooks diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb index 479a2d6def..6401ce3619 100644 --- a/app/services/ci/create_commit_service.rb +++ b/app/services/ci/create_commit_service.rb @@ -16,7 +16,7 @@ module Ci end tag = origin_ref.start_with?('refs/tags/') - commit = project.gl_project.ensure_ci_commit(sha) + commit = project.ensure_ci_commit(sha) unless commit.skip_ci? commit.update_committed! commit.create_builds(ref, tag, user) diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index 4b86cb0a1f..b3dfc70722 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -1,13 +1,13 @@ module Ci class CreateTriggerRequestService def execute(project, trigger, ref, variables = nil) - commit = project.gl_project.commit(ref) + commit = project.commit(ref) return unless commit # check if ref is tag - tag = project.gl_project.repository.find_tag(ref).present? + tag = project.repository.find_tag(ref).present? - ci_commit = project.gl_project.ensure_ci_commit(commit.sha) + ci_commit = project.ensure_ci_commit(commit.sha) trigger_request = trigger.trigger_requests.create!( variables: variables, diff --git a/app/services/ci/event_service.rb b/app/services/ci/event_service.rb deleted file mode 100644 index 3f4e02dd26..0000000000 --- a/app/services/ci/event_service.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Ci - class EventService - def remove_project(user, project) - create( - description: "Project \"#{project.name}\" has been removed by #{user.username}", - user_id: user.id, - is_admin: true - ) - end - - def create_project(user, project) - create( - description: "Project \"#{project.name}\" has been created by #{user.username}", - user_id: user.id, - is_admin: true - ) - end - - def change_project_settings(user, project) - create( - project_id: project.id, - user_id: user.id, - description: "User \"#{user.username}\" updated projects settings" - ) - end - - def create(*args) - Ci::Event.create!(*args) - end - end -end diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index b8d2419303..f469b13e90 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -4,10 +4,10 @@ module Ci sha = params[:sha] sha ||= if params[:ref] - project.gl_project.commit(params[:ref]).try(:sha) + project.commit(params[:ref]).try(:sha) end - commit = project.commits.ordered.find_by(sha: sha) + commit = project.ci_commits.ordered.find_by(sha: sha) image_name = image_for_commit(commit) image_path = Rails.root.join('public/ci', image_name) diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb index 7beb098659..eba602da99 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_build_service.rb @@ -8,10 +8,10 @@ module Ci builds = if current_runner.shared? # don't run projects which have not enables shared runners - builds.joins(commit: { gl_project: :gitlab_ci_project }).where(ci_projects: { shared_runners_enabled: true }) + builds.joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }) else # do run projects which are only assigned to this runner - builds.joins(:commit).where(ci_commits: { gl_project_id: current_runner.gl_projects_ids }) + builds.where(project: current_runner.projects) end builds = builds.order('created_at ASC') @@ -20,10 +20,9 @@ module Ci build.can_be_served?(current_runner) end - if build # In case when 2 runners try to assign the same build, second runner will be declined - # with StateMachine::InvalidTransition in run! method. + # with StateMachines::InvalidTransition in run! method. build.with_lock do build.runner_id = current_runner.id build.save! @@ -33,7 +32,7 @@ module Ci build - rescue StateMachine::InvalidTransition + rescue StateMachines::InvalidTransition nil end end diff --git a/app/services/ci/test_hook_service.rb b/app/services/ci/test_hook_service.rb deleted file mode 100644 index 3a17596aae..0000000000 --- a/app/services/ci/test_hook_service.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Ci - class TestHookService - def execute(hook, current_user) - Ci::WebHookService.new.build_end(hook.project.commits.last.last_build) - end - end -end diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 5da1c7afd9..0577ae778d 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -7,6 +7,8 @@ module Projects description: @project.description, name: @project.name, path: @project.path, + shared_runners_enabled: @project.shared_runners_enabled, + builds_enabled: @project.builds_enabled, namespace_id: @params[:namespace].try(:id) || current_user.namespace.id } @@ -15,19 +17,6 @@ module Projects end new_project = CreateService.new(current_user, new_params).execute - - if new_project.persisted? - if @project.builds_enabled? - new_project.enable_ci - - settings = @project.gitlab_ci_project.attributes.select do |attr_name, value| - ["public", "shared_runners_enabled", "allow_git_fetch"].include? attr_name - end - - new_project.gitlab_ci_project.update(settings) - end - end - new_project end end diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml new file mode 100644 index 0000000000..6936e61434 --- /dev/null +++ b/app/views/admin/builds/_build.html.haml @@ -0,0 +1,73 @@ +- project = build.project +%tr.build + %td.status + = ci_status_with_icon(build.status) + + %td.build-link + - if build.target_url + = link_to build.target_url do + %strong Build ##{build.id} + - else + %strong Build ##{build.id} + + - if build.show_warning? + %i.fa.fa-warning.text-warning + + %td + - if project + = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace" + + %td + = link_to build.short_sha, namespace_project_commit_path(project.namespace, project, build.sha), class: "monospace" + + %td + - if build.ref + = link_to build.ref, namespace_project_commits_path(project.namespace, project, build.ref) + - else + .light none + + %td + - if build.try(:runner) + = runner_link(build.runner) + - else + .light none + + %td + #{build.stage} / #{build.name} + + .pull-right + - if build.tags.any? + - build.tags.each do |tag| + %span.label.label-primary + = tag + - if build.try(:trigger_request) + %span.label.label-info triggered + - if build.try(:allow_failure) + %span.label.label-danger allowed to fail + + %td.duration + - if build.duration + #{duration_in_words(build.finished_at, build.started_at)} + + %td.timestamp + - if build.finished_at + %span #{time_ago_with_tooltip(build.finished_at)} + + - if defined?(coverage) && coverage + %td.coverage + - if build.try(:coverage) + #{build.coverage}% + + %td + .pull-right + - if current_user && can?(current_user, :download_build_artifacts, project) && build.download_url + = link_to build.download_url, title: 'Download artifacts' do + %i.fa.fa-download + - if current_user && can?(current_user, :manage_builds, build.project) + - if build.active? + - if build.cancel_url + = link_to build.cancel_url, method: :post, title: 'Cancel' do + %i.fa.fa-remove.cred + - elsif defined?(allow_retry) && allow_retry && build.retry_url + = link_to build.retry_url, method: :post, title: 'Retry' do + %i.fa.fa-repeat diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml new file mode 100644 index 0000000000..55da06a7fe --- /dev/null +++ b/app/views/admin/builds/index.html.haml @@ -0,0 +1,50 @@ +.project-issuable-filter + .controls + .pull-left.hidden-xs + - if @all_builds.running_or_pending.any? + = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post + + %ul.center-top-menu + %li{class: ('active' if @scope.nil?)} + = link_to admin_builds_path do + Running + %span.badge.js-running-count= @all_builds.running_or_pending.count(:id) + + %li{class: ('active' if @scope == 'finished')} + = link_to admin_builds_path(scope: :finished) do + Finished + %span.badge.js-running-count= @all_builds.finished.count(:id) + + %li{class: ('active' if @scope == 'all')} + = link_to admin_builds_path(scope: :all) do + All + %span.badge.js-totalbuilds-count= @all_builds.count(:id) + +.gray-content-block + #{(@scope || 'running').capitalize} builds + +%ul.content-list + - if @builds.blank? + %li + .nothing-here-block No builds to show + - else + .table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Project + %th Commit + %th Ref + %th Runner + %th Name + %th Duration + %th Finished at + %th + + - @builds.each do |build| + = render "admin/builds/build", build: build + + = paginate @builds, theme: 'gitlab' + diff --git a/app/views/ci/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml similarity index 62% rename from app/views/ci/admin/runners/_runner.html.haml rename to app/views/admin/runners/_runner.html.haml index 701782d26b..6745e58dec 100644 --- a/app/views/ci/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -8,14 +8,14 @@ %span.label.label-danger paused %td - = link_to ci_admin_runner_path(runner) do + = link_to admin_runner_path(runner) do = runner.short_sha %td .runner-description = runner.description %span (#{link_to 'edit', '#', class: 'edit-runner-link'}) .runner-description-form.hide - = form_for [:ci, :admin, runner], remote: true, html: { class: 'form-inline' } do |f| + = form_for [:admin, runner], remote: true, html: { class: 'form-inline' } do |f| .form-group = f.text_field :description, class: 'form-control' = f.submit 'Save', class: 'btn' @@ -38,11 +38,11 @@ Never %td .pull-right - = link_to 'Edit', ci_admin_runner_path(runner), class: 'btn btn-sm' + = link_to 'Edit', admin_runner_path(runner), class: 'btn btn-sm'   - if runner.active? - = link_to 'Pause', [:pause, :ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :get, class: 'btn btn-danger btn-sm' + = link_to 'Pause', [:pause, :admin, runner], data: { confirm: "Are you sure?" }, method: :get, class: 'btn btn-danger btn-sm' - else - = link_to 'Resume', [:resume, :ci, :admin, runner], method: :get, class: 'btn btn-success btn-sm' - = link_to 'Remove', [:ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + = link_to 'Resume', [:resume, :admin, runner], method: :get, class: 'btn btn-success btn-sm' + = link_to 'Remove', [:admin, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml similarity index 90% rename from app/views/ci/admin/runners/index.html.haml rename to app/views/admin/runners/index.html.haml index bacaccfbff..26b180b405 100644 --- a/app/views/ci/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -25,7 +25,7 @@ .append-bottom-20.clearfix .pull-left - = form_tag ci_admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do + = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do .form-group = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false = submit_tag 'Search', class: 'btn' @@ -49,5 +49,5 @@ %th - @runners.each do |runner| - = render "ci/admin/runners/runner", runner: runner + = render "admin/runners/runner", runner: runner = paginate @runners diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml similarity index 71% rename from app/views/ci/admin/runners/show.html.haml rename to app/views/admin/runners/show.html.haml index fd3d33d657..aeb484a797 100644 --- a/app/views/ci/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -22,7 +22,7 @@ %h4 This runner will process builds only from ASSIGNED projects %p You can't make this a shared runner. %hr -= form_for @runner, url: ci_admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f| += form_for @runner, url: admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f| .form-group = label_tag :token, class: 'control-label' do Token @@ -53,29 +53,24 @@ %th - @runner.runner_projects.each do |runner_project| - project = runner_project.project - - if project.gl_project + - if project %tr.alert-info %td %strong - = project.name + = project.name_with_namespace %td .pull-right - = link_to 'Disable', [:ci, :admin, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' + = link_to 'Disable', [:admin, project.namespace, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' %table.table %thead %tr %th Project %th - .pull-right - = link_to 'Assign to all', assign_all_ci_admin_runner_path(@runner), - class: 'btn btn-sm assign-all-runner', - title: 'Assign runner to all projects', - method: :put %tr %td - = form_tag ci_admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do + = form_tag admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do .form-group = search_field_tag :search, params[:search], class: 'form-control', spellcheck: false = submit_tag 'Search', class: 'btn' @@ -84,44 +79,44 @@ - @projects.each do |project| %tr %td - = project.name + = project.name_with_namespace %td .pull-right - = form_for [:ci, :admin, project, project.runner_projects.new] do |f| + = form_for [:admin, project.namespace, project, project.ci_runner_projects.new] do |f| = f.hidden_field :runner_id, value: @runner.id = f.submit 'Enable', class: 'btn btn-xs' = paginate @projects .col-md-6 %h4 Recent builds served by this runner - %table.builds.runner-builds + %table.table.builds.runner-builds %thead %tr - %th Build ID + %th Build %th Status %th Project %th Commit %th Finished at - @builds.each do |build| - - gl_project = build.gl_project + - project = build.project %tr.build %td.id - - if gl_project - = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do - = build.id + - if project + = link_to namespace_project_build_path(project.namespace, project, build) do + %strong ##{build.id} - else - = build.id + %strong ##{build.id} %td.status = ci_status_with_icon(build.status) %td.status - - if gl_project - = gl_project.name_with_namespace + - if project + = project.name_with_namespace %td.build-link - - if gl_project + - if project = link_to ci_status_path(build.commit) do %strong #{build.commit.short_sha} diff --git a/app/views/ci/admin/runners/update.js.haml b/app/views/admin/runners/update.js.haml similarity index 100% rename from app/views/ci/admin/runners/update.js.haml rename to app/views/admin/runners/update.js.haml diff --git a/app/views/ci/admin/application_settings/_form.html.haml b/app/views/ci/admin/application_settings/_form.html.haml deleted file mode 100644 index 634c9daa47..0000000000 --- a/app/views/ci/admin/application_settings/_form.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -= form_for @application_setting, url: ci_admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| - - if @application_setting.errors.any? - #error_explanation - .alert.alert-danger - - @application_setting.errors.full_messages.each do |msg| - %p= msg - - %fieldset - %legend Default Project Settings - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :all_broken_builds do - = f.check_box :all_broken_builds - Send emails only on broken builds - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :add_pusher do - = f.check_box :add_pusher - Add pusher to recipients list - - .form-actions - = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/ci/admin/application_settings/show.html.haml b/app/views/ci/admin/application_settings/show.html.haml deleted file mode 100644 index 7ef0aa89ed..0000000000 --- a/app/views/ci/admin/application_settings/show.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -%h3.page-title Settings -%hr -= render 'form' diff --git a/app/views/ci/admin/builds/_build.html.haml b/app/views/ci/admin/builds/_build.html.haml deleted file mode 100644 index 2df5871321..0000000000 --- a/app/views/ci/admin/builds/_build.html.haml +++ /dev/null @@ -1,34 +0,0 @@ -- gl_project = build.project.gl_project -- if build.commit && build.project - %tr.build - %td.build-link - = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do - %strong #{build.id} - - %td.status - = ci_status_with_icon(build.status) - - %td.commit-link - = link_to ci_status_path(build.commit) do - %strong #{build.commit.short_sha} - - %td.runner - - if build.runner - = link_to build.runner.id, ci_admin_runner_path(build.runner) - - %td.build-project - = truncate build.project.name, length: 30 - - %td.build-message - %span= truncate(build.commit.git_commit_message, length: 30) - - %td.build-branch - %span= truncate(build.ref, length: 25) - - %td.duration - - if build.duration - #{duration_in_words(build.finished_at, build.started_at)} - - %td.timestamp - - if build.finished_at - %span #{time_ago_in_words build.finished_at} ago diff --git a/app/views/ci/admin/builds/index.html.haml b/app/views/ci/admin/builds/index.html.haml deleted file mode 100644 index d23119162c..0000000000 --- a/app/views/ci/admin/builds/index.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -%ul.nav.nav-tabs.append-bottom-20 - %li{class: ("active" if @scope.nil?)} - = link_to 'All builds', ci_admin_builds_path - - %li{class: ("active" if @scope == "pending")} - = link_to "Pending", ci_admin_builds_path(scope: :pending) - - %li{class: ("active" if @scope == "running")} - = link_to "Running", ci_admin_builds_path(scope: :running) - - -%table.builds - %thead - %tr - %th Build - %th Status - %th Commit - %th Runner - %th Project - %th Message - %th Branch - %th Duration - %th Finished at - - - @builds.each do |build| - = render "ci/admin/builds/build", build: build - -= paginate @builds diff --git a/app/views/ci/admin/events/index.html.haml b/app/views/ci/admin/events/index.html.haml deleted file mode 100644 index 5a5b4dc7c3..0000000000 --- a/app/views/ci/admin/events/index.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -.table-holder - %table.table - %thead - %tr - %th User ID - %th Description - %th When - - @events.each do |event| - %tr - %td - = event.user_id - %td - = event.description - %td.light - = time_ago_in_words event.updated_at - ago - -= paginate @events diff --git a/app/views/ci/admin/projects/_project.html.haml b/app/views/ci/admin/projects/_project.html.haml deleted file mode 100644 index a342d6e1cf..0000000000 --- a/app/views/ci/admin/projects/_project.html.haml +++ /dev/null @@ -1,29 +0,0 @@ -- last_commit = project.commits.last -%tr - %td - = project.id - %td - = link_to [:ci, project] do - %strong= project.name - %td - - if last_commit - = ci_status_with_icon(last_commit.status) - - if project.last_commit_date - · - = time_ago_in_words project.last_commit_date - ago - - else - No builds yet - %td - - if project.public - %i.fa.fa-globe - Public - - else - %i.fa.fa-lock - Private - %td - = project.commits.count - %td - = link_to [:ci, :admin, project], method: :delete, class: 'btn btn-danger btn-sm' do - %i.fa.fa-remove - Remove diff --git a/app/views/ci/admin/projects/index.html.haml b/app/views/ci/admin/projects/index.html.haml deleted file mode 100644 index 0da8547924..0000000000 --- a/app/views/ci/admin/projects/index.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -.table-holder - %table.table - %thead - %tr - %th ID - %th Name - %th Last build - %th Access - %th Builds - %th - - - @projects.each do |project| - = render "ci/admin/projects/project", project: project - -= paginate @projects - diff --git a/app/views/ci/admin/runner_projects/index.html.haml b/app/views/ci/admin/runner_projects/index.html.haml deleted file mode 100644 index 6b4e3b2cb3..0000000000 --- a/app/views/ci/admin/runner_projects/index.html.haml +++ /dev/null @@ -1,57 +0,0 @@ -%p.lead - To register a new runner visit #{link_to 'this page ', ci_runners_path} - -.row - .col-md-8 - %h5 Activated: - %table.table - %tr - %th Runner ID - %th Runner Description - %th Last build - %th Builds Stats - %th Registered - %th - - - @runner_projects.each do |runner_project| - - runner = runner_project.runner - - builds = runner.builds.where(project_id: @project.id) - %tr - %td - %span.badge.badge-info= runner.id - %td - = runner.display_name - %td - - last_build = builds.last - - if last_build - = link_to last_build.short_sha, [last_build.project, last_build] - - else - unknown - %td - %span.badge.badge-success - #{builds.success.count} - %span / - %span.badge.badge-important - #{builds.failed.count} - %td - #{time_ago_in_words(runner_project.created_at)} ago - %td - = link_to 'Disable', [:ci, @project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm right' - .col-md-4 - %h5 Available - %table.table - %tr - %th ID - %th Token - %th - - - (Ci::Runner.all - @project.runners).each do |runner| - %tr - %td - = runner.id - %td - = runner.token - %td - = form_for [:ci, @project, @runner_project] do |f| - = f.hidden_field :runner_id, value: runner.id - = f.submit 'Add', class: 'btn btn-sm' diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml index b24a3b826c..11163813f3 100644 --- a/app/views/ci/commits/_commit.html.haml +++ b/app/views/ci/commits/_commit.html.haml @@ -27,7 +27,6 @@ - if commit.finished_at %span #{time_ago_in_words commit.finished_at} ago - - if commit.project.coverage_enabled? + - if commit.coverage %td.coverage - - if commit.coverage - #{commit.coverage}% + #{commit.coverage}% diff --git a/app/views/ci/shared/_guide.html.haml b/app/views/ci/shared/_guide.html.haml index db2d7f2f4b..ec76885866 100644 --- a/app/views/ci/shared/_guide.html.haml +++ b/app/views/ci/shared/_guide.html.haml @@ -4,7 +4,7 @@ %ol %li Add at least one runner to the project. - Go to #{link_to 'Runners page', runners_path(@project.gl_project), target: :blank} for instructions. + Go to #{link_to 'Runners page', runners_path(@project), target: :blank} for instructions. %li Put the .gitlab-ci.yml in the root of your repository. Examples can be found in #{link_to "Configuring project (.gitlab-ci.yml)", "http://doc.gitlab.com/ci/yaml/README.html", target: :blank}. You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} diff --git a/app/views/ci/user_sessions/new.html.haml b/app/views/ci/user_sessions/new.html.haml deleted file mode 100644 index b8d9a1d708..0000000000 --- a/app/views/ci/user_sessions/new.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -.login-block - %h2 Login using GitLab account - %p.light - Make sure you have an account on the GitLab server - = link_to GitlabCi.config.gitlab_server.url, GitlabCi.config.gitlab_server.url, no_turbolink - %hr - = link_to "Login with GitLab", auth_ci_user_sessions_path(state: params[:state]), no_turbolink.merge( class: 'btn btn-login btn-success' ) diff --git a/app/views/layouts/ci/_nav_admin.html.haml b/app/views/layouts/ci/_nav_admin.html.haml deleted file mode 100644 index dcda04a463..0000000000 --- a/app/views/layouts/ci/_nav_admin.html.haml +++ /dev/null @@ -1,35 +0,0 @@ -%ul.nav.nav-sidebar - = nav_link do - = link_to admin_root_path, title: 'Back to admin', data: {placement: 'right'}, class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Back to admin - - %li.separate-item - = nav_link path: 'projects#index' do - = link_to ci_admin_projects_path do - = icon('list-alt fw') - %span - Projects - = nav_link path: 'events#index' do - = link_to ci_admin_events_path do - = icon('book fw') - %span - Events - = nav_link path: ['runners#index', 'runners#show'] do - = link_to ci_admin_runners_path do - = icon('cog fw') - %span - Runners - %span.count= Ci::Runner.count(:all) - = nav_link path: 'builds#index' do - = link_to ci_admin_builds_path do - = icon('link fw') - %span - Builds - %span.count= Ci::Build.count(:all) - = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do - = link_to ci_admin_application_settings_path do - = icon('cogs fw') - %span - Settings diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml deleted file mode 100644 index f094edbfa8..0000000000 --- a/app/views/layouts/ci/_nav_project.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%ul.nav.nav-sidebar - = nav_link do - = link_to project_path(@project.gl_project), title: 'Back to project', data: {placement: 'right'}, class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Back to project - %li.separate-item - = nav_link path: 'events#index' do - = link_to ci_project_events_path(@project) do - = icon('book fw') - %span - Events diff --git a/app/views/layouts/ci/admin.html.haml b/app/views/layouts/ci/admin.html.haml deleted file mode 100644 index c8cb185d28..0000000000 --- a/app/views/layouts/ci/admin.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render 'layouts/head' - %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} - - header_title = "Admin area" - - if current_user - = render "layouts/header/default", title: header_title - - else - = render "layouts/header/public", title: header_title - - = render 'layouts/ci/page', sidebar: 'nav_admin' diff --git a/app/views/layouts/ci/application.html.haml b/app/views/layouts/ci/application.html.haml deleted file mode 100644 index 38023468d0..0000000000 --- a/app/views/layouts/ci/application.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render 'layouts/head' - %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} - - header_title = "Continuous Integration" - - if current_user - = render "layouts/header/default", title: header_title - - else - = render "layouts/header/public", title: header_title - - = render 'layouts/ci/page' diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index d04a3d1f22..c60ac5eefa 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -24,11 +24,18 @@ = icon('key fw') %span Deploy Keys - = nav_link do - = link_to ci_admin_projects_path, title: 'Continuous Integration' do - = icon('building fw') + = nav_link path: ['runners#index', 'runners#show'] do + = link_to admin_runners_path do + = icon('cog fw') %span - Continuous Integration + Runners + %span.count= Ci::Runner.count(:all) + = nav_link path: 'builds#index' do + = link_to admin_builds_path do + = icon('link fw') + %span + Builds + %span.count= Ci::Build.count(:all) = nav_link(controller: :logs) do = link_to admin_logs_path, title: 'Logs' do = icon('file-text fw') diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 4b32c631d5..970da78a5c 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -50,8 +50,3 @@ = icon('retweet fw') %span Triggers - = nav_link path: 'ci_settings#edit' do - = link_to edit_namespace_project_ci_settings_path(@project.namespace, @project), title: 'CI Settings' do - = icon('building fw') - %span - CI Settings diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml index 3b251dc011..f4e9749e5c 100644 --- a/app/views/notify/build_fail_email.html.haml +++ b/app/views/notify/build_fail_email.html.haml @@ -7,7 +7,7 @@ = @project.name %p - Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.gl_project.namespace, @build.gl_project, @build.sha)} + Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)} %p Author: #{@build.commit.git_author_name} %p @@ -20,4 +20,4 @@ Message: #{@build.commit.git_commit_message} %p - Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build)} + Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb index 17a3b9b1d3..675acea60a 100644 --- a/app/views/notify/build_fail_email.text.erb +++ b/app/views/notify/build_fail_email.text.erb @@ -8,4 +8,4 @@ Stage: <%= @build.stage %> Job: <%= @build.name %> Message: <%= @build.commit.git_commit_message %> -Url: <%= namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build) %> +Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %> diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml index c23f4b7e45..8b004d34cc 100644 --- a/app/views/notify/build_success_email.html.haml +++ b/app/views/notify/build_success_email.html.haml @@ -8,7 +8,7 @@ = @project.name %p - Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.gl_project.namespace, @build.gl_project, @build.sha)} + Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)} %p Author: #{@build.commit.git_author_name} %p @@ -21,4 +21,4 @@ Message: #{@build.commit.git_commit_message} %p - Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build)} + Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb index bc8b978c3d..747da44aca 100644 --- a/app/views/notify/build_success_email.text.erb +++ b/app/views/notify/build_success_email.text.erb @@ -8,4 +8,4 @@ Stage: <%= @build.stage %> Job: <%= @build.name %> Message: <%= @build.commit.git_commit_message %> -Url: <%= namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build) %> +Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %> diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index fbf2c293db..1a26908ab1 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -3,7 +3,7 @@ .project-issuable-filter .controls - - if @ci_project && can?(current_user, :manage_builds, @project) + - if can?(current_user, :manage_builds, @project) .pull-left.hidden-xs - if @all_builds.running_or_pending.any? = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post @@ -40,7 +40,7 @@ %thead %tr %th Status - %th Build ID + %th Runner %th Commit %th Ref %th Stage diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index d5e81f84b5..20a5b6a66e 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -56,7 +56,7 @@ %br Go to - = link_to namespace_project_runners_path(@build.gl_project.namespace, @build.gl_project) do + = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do Runners page .row.prepend-top-default @@ -113,7 +113,7 @@ %p %span.attr-name Runner: - if @build.runner && current_user && current_user.admin - = link_to "##{@build.runner.id}", ci_admin_runner_path(@build.runner.id) + = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id) - elsif @build.runner \##{@build.runner.id} diff --git a/app/views/projects/ci_settings/_form.html.haml b/app/views/projects/ci_settings/_form.html.haml deleted file mode 100644 index ee6b8885e2..0000000000 --- a/app/views/projects/ci_settings/_form.html.haml +++ /dev/null @@ -1,120 +0,0 @@ -%h3.page-title - CI settings -%hr -.bs-callout.help-callout - %p - If you want to test your .gitlab-ci.yml, you can use special tool - #{link_to "Lint", ci_lint_path} - %p - Edit your - #{link_to ".gitlab-ci.yml using web-editor", yaml_web_editor_link(@ci_project)} - -- unless @project.empty_repo? - %p - Paste build status image for #{@repository.root_ref} with next link - = link_to '#', class: 'badge-codes-toggle btn btn-default btn-xs' do - Status Badge - .badge-codes-block.bs-callout.bs-callout-info.hide - %p - Status badge for - %span.label.label-info #{@ref} - branch - %div - %label Markdown: - = text_field_tag 'badge_md', markdown_badge_code(@ci_project, @repository.root_ref), readonly: true, class: 'form-control' - %label Html: - = text_field_tag 'badge_html', html_badge_code(@ci_project, @repository.root_ref), readonly: true, class: 'form-control' - -= nested_form_for @ci_project, url: namespace_project_ci_settings_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| - - if @ci_project.errors.any? - #error_explanation - %p.lead= "#{pluralize(@ci_project.errors.count, "error")} prohibited this project from being saved:" - .alert.alert-error - %ul - - @ci_project.errors.full_messages.each do |msg| - %li= msg - - %fieldset - %legend Build settings - .form-group - = label_tag nil, class: 'control-label' do - Get code - .col-sm-10 - %p Get recent application code using the following command: - .radio - = label_tag do - = f.radio_button :allow_git_fetch, 'false' - %strong git clone - .light Slower but makes sure you have a clean dir before every build - .radio - = label_tag do - = f.radio_button :allow_git_fetch, 'true' - %strong git fetch - .light Faster - .form-group - = f.label :timeout_in_minutes, 'Timeout', class: 'control-label' - .col-sm-10 - = f.number_field :timeout_in_minutes, class: 'form-control', min: '0' - .light per build in minutes - - - %fieldset - %legend Build Schedule - .form-group - = f.label :always_build, 'Schedule build', class: 'control-label' - .col-sm-10 - .checkbox - = f.label :always_build do - = f.check_box :always_build - %span.light Repeat last build after X hours if no builds - .form-group - = f.label :polling_interval, "Build interval", class: 'control-label' - .col-sm-10 - = f.number_field :polling_interval, placeholder: '5', min: '0', class: 'form-control' - .light In hours - - %fieldset - %legend Project settings - .form-group - = f.label :default_ref, "Make tabs for the following branches", class: 'control-label' - .col-sm-10 - = f.text_field :default_ref, class: 'form-control', placeholder: 'master, stable' - .light You will be able to filter builds by the following branches - .form-group - = f.label :public, 'Public mode', class: 'control-label' - .col-sm-10 - .checkbox - = f.label :public do - = f.check_box :public - %span.light Anyone can see project and builds - .form-group - = f.label :coverage_regex, "Test coverage parsing", class: 'control-label' - .col-sm-10 - .input-group - %span.input-group-addon / - = f.text_field :coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' - %span.input-group-addon / - .light We will use this regular expression to find test coverage output in build trace. Leave blank if you want to disable this feature - .bs-callout.bs-callout-info - %p Below are examples of regex for existing tools: - %ul - %li - Simplecov (Ruby) - - %code \(\d+.\d+\%\) covered - %li - pytest-cov (Python) - - %code \d+\%\s*$ - %li - phpunit --coverage-text --colors=never (PHP) - - %code ^\s*Lines:\s*\d+.\d+\% - - %fieldset - %legend Advanced settings - .form-group - = f.label :token, "CI token", class: 'control-label' - .col-sm-10 - = f.text_field :token, class: 'form-control', placeholder: 'xEeFCaDAB89' - - .form-actions - = f.submit 'Save changes', class: 'btn btn-save' - - unless @ci_project.new_record? - = link_to 'Remove Project', ci_project_path(@ci_project), method: :delete, data: { confirm: 'Project will be removed. Are you sure?' }, class: 'btn btn-danger pull-right' diff --git a/app/views/projects/ci_settings/_no_runners.html.haml b/app/views/projects/ci_settings/_no_runners.html.haml deleted file mode 100644 index 1374e6680f..0000000000 --- a/app/views/projects/ci_settings/_no_runners.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.alert.alert-danger - %p - There are NO runners to build this project. - %br - You can add Specific runner for this project on Runners page - - - if current_user.admin - or add Shared runner for whole application in admin area. diff --git a/app/views/projects/ci_settings/edit.html.haml b/app/views/projects/ci_settings/edit.html.haml deleted file mode 100644 index acc912d459..0000000000 --- a/app/views/projects/ci_settings/edit.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -- page_title "CI Settings" - -- if no_runners_for_project?(@ci_project) - = render 'no_runners' - -= render 'form' diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index e4d81182c1..952f33184f 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -1,17 +1,17 @@ .gray-content-block.middle-block .pull-right - - if @ci_project && can?(current_user, :manage_builds, @ci_commit.gl_project) + - if @ci_project && can?(current_user, :manage_builds, @ci_commit.project) - if @ci_commit.builds.latest.failed.any?(&:retryable?) - = link_to "Retry failed", retry_builds_namespace_project_commit_path(@ci_commit.gl_project.namespace, @ci_commit.gl_project, @ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post + = link_to "Retry failed", retry_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post - if @ci_commit.builds.running_or_pending.any? - = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@ci_commit.gl_project.namespace, @ci_commit.gl_project, @ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post + = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post .oneline = pluralize @statuses.count(:id), "build" - if defined?(link_to_commit) && link_to_commit for commit - = link_to @ci_commit.short_sha, namespace_project_commit_path(@ci_commit.gl_project.namespace, @ci_commit.gl_project, @ci_commit.sha), class: "monospace" + = link_to @ci_commit.short_sha, namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: "monospace" - if @ci_commit.duration > 0 in = time_interval_in_words @ci_commit.duration @@ -23,7 +23,7 @@ - @ci_commit.yaml_errors.split(",").each do |error| %li= error -- if @ci_commit.gl_project.builds_enabled? && !@ci_commit.ci_yaml_file +- if @ci_commit.project.builds_enabled? && !@ci_commit.ci_yaml_file .bs-callout.bs-callout-warning \.gitlab-ci.yml not found in this commit @@ -38,12 +38,12 @@ %th Name %th Duration %th Finished at - - if @ci_project && @ci_project.coverage_enabled? + - if @ci_commit.project.coverage_enabled? %th Coverage %th - @ci_commit.refs.each do |ref| = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered, - locals: { coverage: @ci_project.try(:coverage_enabled?), stage: true, allow_retry: true } + locals: { coverage: @ci_commit.project.coverage_enabled?, stage: true, allow_retry: true } - if @ci_commit.retried.any? .gray-content-block.second-block @@ -60,8 +60,8 @@ %th Name %th Duration %th Finished at - - if @ci_project && @ci_project.coverage_enabled? + - if @ci_commit.project.coverage_enabled? %th Coverage %th = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, - locals: { coverage: @ci_project.try(:coverage_enabled?), stage: true } + locals: { coverage: @ci_commit.project.coverage_enabled?, stage: true } diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index a527bb2f84..45a00e4d25 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -69,7 +69,7 @@ - if current_user && can?(current_user, :download_build_artifacts, @project) && commit_status.download_url = link_to commit_status.download_url, title: 'Download artifacts' do %i.fa.fa-download - - if current_user && can?(current_user, :manage_builds, commit_status.gl_project) + - if current_user && can?(current_user, :manage_builds, commit_status.project) - if commit_status.active? - if commit_status.cancel_url = link_to commit_status.cancel_url, method: :post, title: 'Cancel' do diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index d865d29913..a7ab9b44e7 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -112,6 +112,62 @@ %hr = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" + %fieldset.features + %legend + Continuous Integration + .form-group + .col-sm-offset-2.col-sm-10 + %p Get recent application code using the following command: + .radio + = f.label :build_allow_git_fetch do + = f.radio_button :build_allow_git_fetch, 'false' + %strong git clone + %br + %span.descr Slower but makes sure you have a clean dir before every build + .radio + = f.label :build_allow_git_fetch do + = f.radio_button :build_allow_git_fetch, 'true' + %strong git fetch + %br + %span.descr Faster + .form-group + = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label' + .col-sm-10 + = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0' + %p.help-block per build in minutes + .form-group + = f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label' + .col-sm-10 + .input-group + %span.input-group-addon / + = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' + %span.input-group-addon / + %p.help-block + We will use this regular expression to find test coverage output in build trace. + Leave blank if you want to disable this feature + .bs-callout.bs-callout-info + %p Below are examples of regex for existing tools: + %ul + %li + Simplecov (Ruby) - + %code \(\d+.\d+\%\) covered + %li + pytest-cov (Python) - + %code \d+\%\s*$ + %li + phpunit --coverage-text --colors=never (PHP) - + %code ^\s*Lines:\s*\d+.\d+\% + + + %fieldset.features + %legend + Advanced settings + .form-group + = f.label :token, "CI token", class: 'control-label' + .col-sm-10 + = f.text_field :token, class: "form-control", placeholder: 'xEeFCaDAB89' + %p.help-block The secure token used to checkout project. + .form-actions = f.submit 'Save changes', class: "btn btn-save" diff --git a/app/views/projects/graphs/ci/_overall.haml b/app/views/projects/graphs/ci/_overall.haml index cf4285a267..4105d68311 100644 --- a/app/views/projects/graphs/ci/_overall.haml +++ b/app/views/projects/graphs/ci/_overall.haml @@ -1,20 +1,19 @@ -- ci_project = @project.gitlab_ci_project %h4 Overall stats %ul %li Total: - %strong= pluralize ci_project.builds.count(:all), 'build' + %strong= pluralize @project.ci_builds.count(:all), 'build' %li Successful: - %strong= pluralize ci_project.builds.success.count(:all), 'build' + %strong= pluralize @project.ci_builds.success.count(:all), 'build' %li Failed: - %strong= pluralize ci_project.builds.failed.count(:all), 'build' + %strong= pluralize @project.ci_builds.failed.count(:all), 'build' %li Success ratio: %strong - #{success_ratio(ci_project.builds.success, ci_project.builds.failed)}% + #{success_ratio(@project.ci_builds.success, @project.ci_builds.failed)}% %li Commits covered: %strong - = ci_project.commits.count(:all) + = @project.ci_commits.count(:all) diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index e6b8a2e6fe..1a61cb07b5 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -15,10 +15,10 @@ - if runner.belongs_to_one_project? = link_to 'Remove runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' - else - - runner_project = @ci_project.runner_projects.find_by(runner_id: runner) - = link_to 'Disable for this project', [:ci, @ci_project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + - runner_project = @project.ci_runner_projects.find_by(runner_id: runner) + = link_to 'Disable for this project', namespace_project_runner_project_path(@project.namespace, @project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' - elsif runner.specific? - = form_for [:ci, @ci_project, @ci_project.runner_projects.new] do |f| + = form_for [@project.namespace, @project, @project.ci_runner_projects.new] do |f| = f.hidden_field :runner_id, value: runner.id = f.submit 'Enable for this project', class: 'btn btn-sm' .pull-right diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index 316ea747b1..7e4ffc884a 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -3,11 +3,11 @@ .bs-callout.bs-callout-warning GitLab Runners do not offer secure isolation between projects that they do builds for. You are TRUSTING all GitLab users who can push code to project A, B or C to run shell scripts on the machine hosting runner X. %hr - - if @ci_project.shared_runners_enabled - = link_to toggle_shared_runners_ci_project_path(@ci_project), class: 'btn btn-warning', method: :post do + - if @project.shared_runners_enabled? + = link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-warning', method: :post do Disable shared runners - else - = link_to toggle_shared_runners_ci_project_path(@ci_project), class: 'btn btn-success', method: :post do + = link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-success', method: :post do Enable shared runners   for this project diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index c13625c7e4..67767f3e34 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -12,7 +12,7 @@ %code #{ci_root_url(only_path: false)} %li Use the following registration token during setup: - %code #{@ci_project.token} + %code #{@project.token} %li Start runner! diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index b3ad79a200..147cda51d5 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -36,7 +36,7 @@ :plain curl -X POST \ -F token=TOKEN \ - #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')} + #{ci_build_trigger_url(@project.id, 'REF_NAME')} %h3 Use .gitlab-ci.yml @@ -51,7 +51,7 @@ trigger: type: deploy script: - - "curl -X POST -F token=TOKEN #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')}" + - "curl -X POST -F token=TOKEN #{ci_build_trigger_url(@project.id, 'REF_NAME')}" %h3 Pass build variables @@ -66,4 +66,4 @@ curl -X POST \ -F token=TOKEN \ -F "variables[RUN_NIGHTLY_BUILD]=true" \ - #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')} + #{ci_build_trigger_url(@project.id, 'REF_NAME')} diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml index e052da1ac4..e7bc866d6b 100644 --- a/app/views/projects/variables/show.html.haml +++ b/app/views/projects/variables/show.html.haml @@ -10,16 +10,16 @@ %hr -= nested_form_for @ci_project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f| += nested_form_for @project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f| - if @project.errors.any? #error_explanation - %p.lead= "#{pluralize(@ci_project.errors.count, "error")} prohibited this project from being saved:" + %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:" .alert.alert-error %ul - - @ci_project.errors.full_messages.each do |msg| + - @project.errors.full_messages.each do |msg| %li= msg - = f.fields_for :variables do |variable_form| + = f.fields_for :ci_variables do |variable_form| .form-group = variable_form.label :key, 'Key', class: 'control-label' .col-sm-10 @@ -34,7 +34,7 @@ %hr %p .clearfix - = f.link_to_add "Add a variable", :variables, class: 'btn btn-success pull-right' + = f.link_to_add "Add a variable", :ci_variables, class: 'btn btn-success pull-right' .form-actions = f.submit 'Save changes', class: 'btn btn-save', return_to: request.original_url diff --git a/config/routes.rb b/config/routes.rb index a104e686ac..50836d63fa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,43 +24,10 @@ Rails.application.routes.draw do resource :lint, only: [:show, :create] resources :projects do - collection do - post :add - get :disabled - end - member do get :status, to: 'projects#badge' get :integration - post :toggle_shared_runners end - - resources :runner_projects, only: [:create, :destroy] - end - - resource :user_sessions do - get :auth - get :callback - end - - namespace :admin do - resources :runners, only: [:index, :show, :update, :destroy] do - member do - put :assign_all - get :resume - get :pause - end - end - - resources :events, only: [:index] - - resources :projects do - resources :runner_projects - end - - resources :builds, only: :index - - resource :application_settings, only: [:show, :update] end root to: 'projects#index' @@ -271,6 +238,8 @@ Rails.application.routes.draw do member do put :transfer end + + resources :runner_projects end end @@ -280,6 +249,19 @@ Rails.application.routes.draw do resources :labels + resources :runners, only: [:index, :show, :update, :destroy] do + member do + get :resume + get :pause + end + end + + resources :builds, only: :index do + collection do + post :cancel_all + end + end + root to: 'dashboard#index' end @@ -595,7 +577,6 @@ Rails.application.routes.draw do resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } resource :variables, only: [:show, :update] resources :triggers, only: [:index, :create, :destroy] - resource :ci_settings, only: [:edit, :update, :destroy] resources :builds, only: [:index, :show] do collection do @@ -674,7 +655,13 @@ Rails.application.routes.draw do get :resume get :pause end + + collection do + post :toggle_shared_runners + end end + + resources :runner_projects, only: [:create, :destroy] end end end diff --git a/db/migrate/20151203162135_add_ci_to_project.rb b/db/migrate/20151203162135_add_ci_to_project.rb new file mode 100644 index 0000000000..e95942666c --- /dev/null +++ b/db/migrate/20151203162135_add_ci_to_project.rb @@ -0,0 +1,10 @@ +class AddCiToProject < ActiveRecord::Migration + def up + add_column :projects, :builds_enabled, :boolean, default: true, null: false + add_column :projects, :shared_runners_enabled, :boolean, default: true, null: false + add_column :projects, :token, :string + add_column :projects, :build_coverage_regex, :string + add_column :projects, :build_allow_git_fetch, :boolean, default: true, null: false + add_column :projects, :build_timeout, :integer, default: 3600, null: false + end +end diff --git a/db/migrate/20151204110124_add_project_id_to_ci.rb b/db/migrate/20151204110124_add_project_id_to_ci.rb new file mode 100644 index 0000000000..5d1cf54357 --- /dev/null +++ b/db/migrate/20151204110124_add_project_id_to_ci.rb @@ -0,0 +1,8 @@ +class AddProjectIdToCi < ActiveRecord::Migration + def up + add_column :ci_builds, :gl_project_id, :integer + add_column :ci_runner_projects, :gl_project_id, :integer + add_column :ci_triggers, :gl_project_id, :integer + add_column :ci_variables, :gl_project_id, :integer + end +end diff --git a/db/migrate/20151204110613_migrate_ci_to_project.rb b/db/migrate/20151204110613_migrate_ci_to_project.rb new file mode 100644 index 0000000000..1777b6170b --- /dev/null +++ b/db/migrate/20151204110613_migrate_ci_to_project.rb @@ -0,0 +1,36 @@ +class MigrateCiToProject < ActiveRecord::Migration + def up + migrate_project_id_for_table('ci_runner_projects') + migrate_project_id_for_table('ci_triggers') + migrate_project_id_for_table('ci_variables') + migrate_project_id_for_builds + + migrate_project_column('shared_runners_enabled') + migrate_project_column('token') + migrate_project_column('coverage_regex', 'build_coverage_regex') + migrate_project_column('allow_git_fetch', 'build_allow_git_fetch') + migrate_project_column('timeout', 'build_timeout') + migrate_ci_service + end + + def migrate_project_id_for_table(table) + subquery = "SELECT gitlab_id FROM ci_projects WHERE ci_projects.id = #{table}.project_id" + execute("UPDATE #{table} SET gl_project_id=(#{subquery}) WHERE gl_project_id IS NULL") + end + + def migrate_project_id_for_builds + subquery = 'SELECT gl_project_id FROM ci_commits WHERE ci_commits.id = ci_builds.commit_id' + execute("UPDATE ci_builds SET gl_project_id=(#{subquery}) WHERE gl_project_id IS NULL") + end + + def migrate_project_column(column, new_column = nil) + new_column ||= column + subquery = "SELECT #{column} FROM ci_projects WHERE projects.id = ci_projects.gitlab_id" + execute("UPDATE projects SET #{new_column}=(#{subquery}) WHERE #{new_column} IS NULL AND (#{subquery}) IS NOT NULL") + end + + def migrate_ci_service + subquery = "SELECT active FROM services WHERE projects.id = services.project_id AND type='GitlabCiService'" + execute("UPDATE projects SET builds_enabled=(#{subquery}) WHERE builds_enabled IS NULL AND (#{subquery}) IS NOT NULL") + end +end diff --git a/db/migrate/20151204110832_add_index_to_ci_tables.rb b/db/migrate/20151204110832_add_index_to_ci_tables.rb new file mode 100644 index 0000000000..b95931334c --- /dev/null +++ b/db/migrate/20151204110832_add_index_to_ci_tables.rb @@ -0,0 +1,11 @@ +class AddIndexToCiTables < ActiveRecord::Migration + def up + add_index :ci_builds, :gl_project_id + add_index :ci_runner_projects, :gl_project_id + add_index :ci_triggers, :gl_project_id + add_index :ci_variables, :gl_project_id + add_index :projects, :token + add_index :projects, :builds_enabled + add_index :projects, [:builds_enabled, :shared_runners_enabled] + end +end diff --git a/db/migrate/20151204123933_drop_null_for_ci_tables.rb b/db/migrate/20151204123933_drop_null_for_ci_tables.rb new file mode 100644 index 0000000000..0b007430b0 --- /dev/null +++ b/db/migrate/20151204123933_drop_null_for_ci_tables.rb @@ -0,0 +1,9 @@ +class DropNullForCiTables < ActiveRecord::Migration + def up + remove_index :ci_variables, :project_id + remove_index :ci_runner_projects, :project_id + change_column_null :ci_triggers, :project_id, true + change_column_null :ci_variables, :project_id, true + change_column_null :ci_runner_projects, :project_id, true + end +end diff --git a/db/schema.rb b/db/schema.rb index 6ccd440421..fd9004faa2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151210125927) do +ActiveRecord::Schema.define(version: 20151204123933) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -110,6 +110,7 @@ ActiveRecord::Schema.define(version: 20151210125927) do t.string "target_url" t.string "description" t.text "artifacts_file" + t.integer "gl_project_id" end add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree @@ -117,6 +118,7 @@ ActiveRecord::Schema.define(version: 20151210125927) do add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree + add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree @@ -201,13 +203,14 @@ ActiveRecord::Schema.define(version: 20151210125927) do add_index "ci_projects", ["shared_runners_enabled"], name: "index_ci_projects_on_shared_runners_enabled", using: :btree create_table "ci_runner_projects", force: :cascade do |t| - t.integer "runner_id", null: false - t.integer "project_id", null: false + t.integer "runner_id", null: false + t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" + t.integer "gl_project_id" end - add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree + add_index "ci_runner_projects", ["gl_project_id"], name: "index_ci_runner_projects_on_gl_project_id", using: :btree add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree create_table "ci_runners", force: :cascade do |t| @@ -277,24 +280,27 @@ ActiveRecord::Schema.define(version: 20151210125927) do create_table "ci_triggers", force: :cascade do |t| t.string "token" - t.integer "project_id", null: false + t.integer "project_id" t.datetime "deleted_at" t.datetime "created_at" t.datetime "updated_at" + t.integer "gl_project_id" end add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree + add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree create_table "ci_variables", force: :cascade do |t| - t.integer "project_id", null: false + t.integer "project_id" t.string "key" t.text "value" t.text "encrypted_value" t.string "encrypted_value_salt" t.string "encrypted_value_iv" + t.integer "gl_project_id" end - add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree + add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree create_table "ci_web_hooks", force: :cascade do |t| t.string "url", null: false @@ -649,14 +655,23 @@ ActiveRecord::Schema.define(version: 20151210125927) do t.string "import_source" t.integer "commit_count", default: 0 t.text "import_error" + t.boolean "builds_enabled", default: true, null: false + t.boolean "shared_runners_enabled", default: true, null: false + t.string "token" + t.string "build_coverage_regex" + t.boolean "build_allow_git_fetch", default: true, null: false + t.integer "build_timeout", default: 3600, null: false end + add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree + add_index "projects", ["builds_enabled"], name: "index_projects_on_builds_enabled", using: :btree add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree add_index "projects", ["path"], name: "index_projects_on_path", using: :btree add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree + add_index "projects", ["token"], name: "index_projects_on_token", using: :btree add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree create_table "protected_branches", force: :cascade do |t| diff --git a/doc/api/projects.md b/doc/api/projects.md index 43a50a9a81..1a52440062 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -32,7 +32,6 @@ Parameters: - `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` - `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` - `search` (optional) - Return list of authorized projects according to a search criteria -- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first ```json [ @@ -137,7 +136,6 @@ Parameters: - `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` - `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` - `search` (optional) - Return list of authorized projects according to a search criteria -- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first ### List ALL projects @@ -153,7 +151,6 @@ Parameters: - `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` - `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` - `search` (optional) - Return list of authorized projects according to a search criteria -- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first ### Get single project diff --git a/features/project/service.feature b/features/project/service.feature index 5014b52b9f..ff3e7a0b38 100644 --- a/features/project/service.feature +++ b/features/project/service.feature @@ -7,12 +7,6 @@ Feature: Project Services When I visit project "Shop" services page Then I should see list of available services - Scenario: Activate gitlab-ci service - When I visit project "Shop" services page - And I click gitlab-ci service link - And I fill gitlab-ci settings - Then I should see service settings saved - Scenario: Activate hipchat service When I visit project "Shop" services page And I click hipchat service link diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 0d6a9a8fc6..101eb408ba 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -104,7 +104,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps step 'commit has ci status' do @project.enable_ci - ci_commit = create :ci_commit, gl_project: @project, sha: sample_commit.id + ci_commit = create :ci_commit, project: @project, sha: sample_commit.id create :ci_build, commit: ci_commit end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 0107d9d848..c4c6225242 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -361,7 +361,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step '"Bug NS-05" has CI status' do project = merge_request.source_project project.enable_ci - ci_commit = create :ci_commit, gl_project: project, sha: merge_request.last_commit.id + ci_commit = create :ci_commit, project: project, sha: merge_request.last_commit.id create :ci_build, commit: ci_commit end diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 1c700df0c6..ed3957ca87 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -11,7 +11,6 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps expect(page).to have_content 'Project services' expect(page).to have_content 'Campfire' expect(page).to have_content 'HipChat' - expect(page).to have_content 'GitLab CI' expect(page).to have_content 'Assembla' expect(page).to have_content 'Pushover' expect(page).to have_content 'Atlassian Bamboo' @@ -20,15 +19,6 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps expect(page).to have_content 'Irker (IRC gateway)' end - step 'I click gitlab-ci service link' do - click_link 'GitLab CI' - end - - step 'I fill gitlab-ci settings' do - check 'Active' - click_button 'Save' - end - step 'I should see service settings saved' do expect(find_field('Active').value).to eq '1' end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 7021fac5fe..da643bf3ba 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -204,7 +204,7 @@ module SharedProject step 'project "Shop" has CI build' do project = Project.find_by(name: "Shop") - create :ci_commit, gl_project: project, sha: project.commit.sha + create :ci_commit, project: project, sha: project.commit.sha end step 'I should see last commit with CI status' do diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 2c0596c9df..1162271f5f 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -53,7 +53,7 @@ module API name = params[:name] || params[:context] status = GenericCommitStatus.running_or_pending.find_by(commit: ci_commit, name: name, ref: params[:ref]) - status ||= GenericCommitStatus.new(commit: ci_commit, user: current_user) + status ||= GenericCommitStatus.new(project: @project, commit: ci_commit, user: current_user) status.update(attrs) case params[:state].to_s diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 03e3056a87..381babe291 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -64,6 +64,7 @@ module API expose :name, :name_with_namespace expose :path, :path_with_namespace expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :created_at, :last_activity_at + expose :shared_runners_enabled expose :creator_id expose :namespace expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? } diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 92540ccf2b..a4df810e75 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -266,12 +266,7 @@ module API projects = projects.search(params[:search]) end - if params[:ci_enabled_first].present? - projects.includes(:gitlab_ci_service). - reorder("services.active DESC, projects.#{project_order_by} #{project_sort}") - else - projects.reorder(project_order_by => project_sort) - end + projects.reorder(project_order_by => project_sort) end def project_order_by diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 6928fe0eb9..bdf4b77596 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -82,6 +82,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # shared_runners_enabled (optional) # namespace_id (optional) - defaults to user namespace # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - 0 by default @@ -98,6 +99,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, + :shared_runners_enabled, :namespace_id, :public, :visibility_level, @@ -126,6 +128,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) # import_url (optional) @@ -142,6 +145,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, + :shared_runners_enabled, :public, :visibility_level, :import_url] @@ -183,6 +187,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - visibility level of a project # Example Request @@ -197,6 +202,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, + :shared_runners_enabled, :public, :visibility_level] attrs = map_public_to_visibility_level(attrs) diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index 07e68216d7..5c347e432b 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -30,9 +30,7 @@ module Ci helpers Gitlab::CurrentSettings mount Builds - mount Commits mount Runners - mount Projects mount Triggers end end diff --git a/lib/ci/api/commits.rb b/lib/ci/api/commits.rb deleted file mode 100644 index a60769d830..0000000000 --- a/lib/ci/api/commits.rb +++ /dev/null @@ -1,66 +0,0 @@ -module Ci - module API - class Commits < Grape::API - resource :commits do - # Get list of commits per project - # - # Parameters: - # project_id (required) - The ID of a project - # project_token (requires) - Project token - # page (optional) - # per_page (optional) - items per request (default is 20) - # - get do - required_attributes! [:project_id, :project_token] - project = Ci::Project.find(params[:project_id]) - authenticate_project_token!(project) - - commits = project.commits.page(params[:page]).per(params[:per_page] || 20) - present commits, with: Entities::CommitWithBuilds - end - - # Create a commit - # - # Parameters: - # project_id (required) - The ID of a project - # project_token (requires) - Project token - # data (required) - GitLab push data - # - # Sample GitLab push data: - # { - # "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", - # "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", - # "ref": "refs/heads/master", - # "commits": [ - # { - # "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", - # "message": "Update Catalan translation to e38cb41.", - # "timestamp": "2011-12-12T14:27:31+02:00", - # "url": "http://localhost/diaspora/commits/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", - # "author": { - # "name": "Jordi Mallach", - # "email": "jordi@softcatala.org", - # } - # }, .... more commits - # ] - # } - # - # Example Request: - # POST /commits - post do - required_attributes! [:project_id, :data, :project_token] - project = Ci::Project.find(params[:project_id]) - authenticate_project_token!(project) - commit = Ci::CreateCommitService.new.execute(project, current_user, params[:data]) - - if commit.persisted? - present commit, with: Entities::CommitWithBuilds - else - errors = commit.errors.full_messages.join(", ") - render_api_error!(errors, 400) - end - end - end - end - end -end diff --git a/lib/ci/api/projects.rb b/lib/ci/api/projects.rb deleted file mode 100644 index 23c79f3879..0000000000 --- a/lib/ci/api/projects.rb +++ /dev/null @@ -1,157 +0,0 @@ -module Ci - module API - # Projects API - class Projects < Grape::API - before { authenticate! } - - resource :projects do - # Retrieve all Gitlab CI projects that the user has access to - # - # Example Request: - # GET /projects - get do - gitlab_projects = current_user.authorized_projects - gitlab_projects = filter_projects(gitlab_projects) - gitlab_projects = paginate gitlab_projects - - ids = gitlab_projects.map { |project| project.id } - - projects = Ci::Project.where("gitlab_id IN (?)", ids).load - present projects, with: Entities::Project - end - - # Retrieve all Gitlab CI projects that the user owns - # - # Example Request: - # GET /projects/owned - get "owned" do - gitlab_projects = current_user.owned_projects - gitlab_projects = filter_projects(gitlab_projects) - gitlab_projects = paginate gitlab_projects - - ids = gitlab_projects.map { |project| project.id } - - projects = Ci::Project.where("gitlab_id IN (?)", ids).load - present projects, with: Entities::Project - end - - # Retrieve info for a Gitlab CI project - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id - get ":id" do - project = Ci::Project.find(params[:id]) - unauthorized! unless can?(current_user, :read_project, project.gl_project) - - present project, with: Entities::Project - end - - # Create Gitlab CI project using Gitlab project info - # - # Parameters: - # gitlab_id (required) - The gitlab id of the project - # default_ref - The branch to run against (defaults to `master`) - # Example Request: - # POST /projects - post do - required_attributes! [:gitlab_id] - - filtered_params = { - gitlab_id: params[:gitlab_id], - # we accept gitlab_url for backward compatibility for a while (added to 7.11) - default_ref: params[:default_ref] || 'master' - } - - project = Ci::Project.new(filtered_params) - project.build_missing_services - - if project.save - present project, with: Entities::Project - else - errors = project.errors.full_messages.join(", ") - render_api_error!(errors, 400) - end - end - - # Update a Gitlab CI project - # - # Parameters: - # id (required) - The ID of a project - # default_ref - The branch to run against (defaults to `master`) - # Example Request: - # PUT /projects/:id - put ":id" do - project = Ci::Project.find(params[:id]) - - unauthorized! unless can?(current_user, :admin_project, project.gl_project) - - attrs = attributes_for_keys [:default_ref] - - if project.update_attributes(attrs) - present project, with: Entities::Project - else - errors = project.errors.full_messages.join(", ") - render_api_error!(errors, 400) - end - end - - # Link a Gitlab CI project to a runner - # - # Parameters: - # id (required) - The ID of a CI project - # runner_id (required) - The ID of a runner - # Example Request: - # POST /projects/:id/runners/:runner_id - post ":id/runners/:runner_id" do - project = Ci::Project.find(params[:id]) - runner = Ci::Runner.find(params[:runner_id]) - - unauthorized! unless can?(current_user, :admin_project, project.gl_project) - - options = { - project_id: project.id, - runner_id: runner.id - } - - runner_project = Ci::RunnerProject.new(options) - - if runner_project.save - present runner_project, with: Entities::RunnerProject - else - errors = project.errors.full_messages.join(", ") - render_api_error!(errors, 400) - end - end - - # Remove a Gitlab CI project from a runner - # - # Parameters: - # id (required) - The ID of a CI project - # runner_id (required) - The ID of a runner - # Example Request: - # DELETE /projects/:id/runners/:runner_id - delete ":id/runners/:runner_id" do - project = Ci::Project.find(params[:id]) - runner = Ci::Runner.find(params[:runner_id]) - - unauthorized! unless can?(current_user, :admin_project, project.gl_project) - - options = { - project_id: project.id, - runner_id: runner.id - } - - runner_project = Ci::RunnerProject.find_by(options) - - if runner_project.present? - runner_project.destroy - else - not_found! - end - end - end - end - end -end diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb index 1466fe4356..dd77bd6586 100644 --- a/lib/ci/api/runners.rb +++ b/lib/ci/api/runners.rb @@ -3,17 +3,6 @@ module Ci # Runners API class Runners < Grape::API resource :runners do - # Get list of all available runners - # - # Example Request: - # GET /runners - get do - authenticate! - runners = Ci::Runner.all - - present runners, with: Entities::Runner - end - # Delete runner # Parameters: # token (required) - The unique token of runner @@ -47,9 +36,9 @@ module Ci tag_list: params[:tag_list], is_shared: true ) - elsif project = Ci::Project.find_by(token: params[:token]) + elsif project = Project.find_by(token: params[:token]) # Create a specific runner for project. - project.runners.create( + project.ci_runners.create( description: params[:description], tag_list: params[:tag_list] ) diff --git a/lib/ci/api/triggers.rb b/lib/ci/api/triggers.rb index 40907d6db5..6d2cdd8c68 100644 --- a/lib/ci/api/triggers.rb +++ b/lib/ci/api/triggers.rb @@ -14,7 +14,7 @@ module Ci post ":id/refs/:ref/trigger" do required_attributes! [:token] - project = Ci::Project.find(params[:id]) + project = Project.find_by_ci_id(params[:id]) trigger = Ci::Trigger.find_by_token(params[:token].to_s) not_found! unless project && trigger unauthorized! unless trigger.project == project diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb index 5ff7407c6f..912ccff5f9 100644 --- a/lib/ci/charts.rb +++ b/lib/ci/charts.rb @@ -16,10 +16,10 @@ module Ci def push(from, to, format) @labels << from.strftime(format) - @total << project.builds. + @total << project.ci_builds. where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from). count(:all) - @success << project.builds. + @success << project.ci_builds. where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from). success.count(:all) end @@ -60,7 +60,7 @@ module Ci class BuildTime < Chart def collect - commits = project.commits.last(30) + commits = project.ci_commits.last(30) commits.each do |commit| @labels << commit.short_sha diff --git a/lib/ci/current_settings.rb b/lib/ci/current_settings.rb deleted file mode 100644 index fd78b02497..0000000000 --- a/lib/ci/current_settings.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Ci - module CurrentSettings - def current_application_settings - key = :ci_current_application_settings - - RequestStore.store[key] ||= begin - if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('ci_application_settings') - Ci::ApplicationSetting.current || Ci::ApplicationSetting.create_from_defaults - else - fake_application_settings - end - end - end - - def fake_application_settings - OpenStruct.new( - all_broken_builds: Ci::Settings.gitlab_ci['all_broken_builds'], - add_pusher: Ci::Settings.gitlab_ci['add_pusher'], - ) - end - end -end diff --git a/lib/ci/scheduler.rb b/lib/ci/scheduler.rb deleted file mode 100644 index ee0958f4be..0000000000 --- a/lib/ci/scheduler.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Ci - class Scheduler - def perform - projects = Ci::Project.where(always_build: true).all - projects.each do |project| - last_commit = project.commits.last - next unless last_commit && last_commit.last_build - - interval = project.polling_interval - if (last_commit.last_build.created_at + interval.hours) < Time.now - last_commit.retry - end - end - end - end -end diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 0d156047ff..5a032b572a 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -77,7 +77,9 @@ module Grack if project && matched_login.present? && git_cmd == 'git-upload-pack' underscored_service = matched_login['s'].underscore - if Service.available_services_names.include?(underscored_service) + if underscored_service == 'gitlab_ci' + return project && project.builds_enabled? && project.valid_token?(password) + elsif Service.available_services_names.include?(underscored_service) service_method = "#{underscored_service}_service" service = project.send(service_method) diff --git a/lib/gitlab/build_data_builder.rb b/lib/gitlab/build_data_builder.rb index fa2cd551ce..86bfa0a437 100644 --- a/lib/gitlab/build_data_builder.rb +++ b/lib/gitlab/build_data_builder.rb @@ -2,7 +2,7 @@ module Gitlab class BuildDataBuilder class << self def build(build) - project = build.gl_project + project = build.project commit = build.commit user = build.user diff --git a/lib/tasks/ci/schedule_builds.rake b/lib/tasks/ci/schedule_builds.rake deleted file mode 100644 index 49435504c6..0000000000 --- a/lib/tasks/ci/schedule_builds.rake +++ /dev/null @@ -1,6 +0,0 @@ -namespace :ci do - desc "GitLab CI | Clean running builds" - task schedule_builds: :environment do - Ci::Scheduler.new.perform - end -end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 2fcd70182b..f76e826f13 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -42,6 +42,10 @@ FactoryGirl.define do commit factory: :ci_commit + after(:build) do |build, evaluator| + build.project = build.commit.project + end + factory :ci_not_started_build do started_at nil finished_at nil diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb index 70e3fa319c..b42cafa518 100644 --- a/spec/factories/ci/commits.rb +++ b/spec/factories/ci/commits.rb @@ -21,7 +21,7 @@ FactoryGirl.define do factory :ci_empty_commit, class: Ci::Commit do sha '97de212e80737a608d939f648d959671fb0a0142' - gl_project factory: :empty_project + project factory: :empty_project factory :ci_commit_without_jobs do after(:build) do |commit| diff --git a/spec/factories/ci/events.rb b/spec/factories/ci/events.rb deleted file mode 100644 index 9638618a40..0000000000 --- a/spec/factories/ci/events.rb +++ /dev/null @@ -1,24 +0,0 @@ -# == Schema Information -# -# Table name: events -# -# id :integer not null, primary key -# project_id :integer -# user_id :integer -# is_admin :integer -# description :text -# created_at :datetime -# updated_at :datetime -# - -FactoryGirl.define do - factory :ci_event, class: Ci::Event do - sequence :description do |n| - "updated project settings#{n}" - end - - factory :ci_admin_event do - is_admin true - end - end -end diff --git a/spec/factories/ci/projects.rb b/spec/factories/ci/projects.rb deleted file mode 100644 index 11cb8c9eea..0000000000 --- a/spec/factories/ci/projects.rb +++ /dev/null @@ -1,50 +0,0 @@ -# == Schema Information -# -# Table name: projects -# -# id :integer not null, primary key -# name :string(255) not null -# timeout :integer default(3600), not null -# created_at :datetime -# updated_at :datetime -# token :string(255) -# default_ref :string(255) -# path :string(255) -# always_build :boolean default(FALSE), not null -# polling_interval :integer -# public :boolean default(FALSE), not null -# ssh_url_to_repo :string(255) -# gitlab_id :integer -# allow_git_fetch :boolean default(TRUE), not null -# email_recipients :string(255) default(""), not null -# email_add_pusher :boolean default(TRUE), not null -# email_only_broken_builds :boolean default(TRUE), not null -# skip_refs :string(255) -# coverage_regex :string(255) -# shared_runners_enabled :boolean default(FALSE) -# generated_yaml_config :text -# - -# Read about factories at https://github.com/thoughtbot/factory_girl - -FactoryGirl.define do - factory :ci_project_without_token, class: Ci::Project do - default_ref 'master' - - shared_runners_enabled false - - factory :ci_project do - token 'iPWx6WM4lhHNedGfBpPJNP' - end - - initialize_with do - # TODO: - # this is required, because builds_enabled is initialized when Project is created - # and this create gitlab_ci_project if builds is set to true - # here we take created gitlab_ci_project and update it's attributes - ci_project = create(:empty_project).ensure_gitlab_ci_project - ci_project.update_attributes(attributes) - ci_project - end - end -end diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb index 3aa14ca434..008d1c5d96 100644 --- a/spec/factories/ci/runner_projects.rb +++ b/spec/factories/ci/runner_projects.rb @@ -14,6 +14,6 @@ FactoryGirl.define do factory :ci_runner_project, class: Ci::RunnerProject do runner_id 1 - project_id 1 + gl_project_id 1 end end diff --git a/spec/features/ci/admin/builds_spec.rb b/spec/features/atom/builds_spec.rb similarity index 69% rename from spec/features/ci/admin/builds_spec.rb rename to spec/features/atom/builds_spec.rb index 623d466c67..72764b1629 100644 --- a/spec/features/ci/admin/builds_spec.rb +++ b/spec/features/atom/builds_spec.rb @@ -5,17 +5,16 @@ describe "Admin Builds" do let(:build) { FactoryGirl.create :ci_build, commit: commit } before do - skip_ci_admin_auth - login_as :user + login_as :admin end describe "GET /admin/builds" do before do build - visit ci_admin_builds_path + visit admin_builds_path end - it { expect(page).to have_content "All builds" } + it { expect(page).to have_content "Running" } it { expect(page).to have_content build.short_sha } end @@ -26,43 +25,43 @@ describe "Admin Builds" do FactoryGirl.create :ci_build, commit: commit, status: "success" FactoryGirl.create :ci_build, commit: commit, status: "failed" - visit ci_admin_builds_path + visit admin_builds_path + + within ".center-top-menu" do + click_on "All" + end expect(page.all(".build-link").size).to eq(4) end - it "shows pending builds" do + it "shows finished builds" do build = FactoryGirl.create :ci_build, commit: commit, status: "pending" build1 = FactoryGirl.create :ci_build, commit: commit, status: "running" build2 = FactoryGirl.create :ci_build, commit: commit, status: "success" - build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed" - visit ci_admin_builds_path + visit admin_builds_path - within ".nav.nav-tabs" do - click_on "Pending" + within ".center-top-menu" do + click_on "Finished" end - expect(page.find(".build-link")).to have_content(build.id) + expect(page.find(".build-link")).not_to have_content(build.id) expect(page.find(".build-link")).not_to have_content(build1.id) - expect(page.find(".build-link")).not_to have_content(build2.id) - expect(page.find(".build-link")).not_to have_content(build3.id) + expect(page.find(".build-link")).to have_content(build2.id) end it "shows running builds" do build = FactoryGirl.create :ci_build, commit: commit, status: "pending" - build1 = FactoryGirl.create :ci_build, commit: commit, status: "running" build2 = FactoryGirl.create :ci_build, commit: commit, status: "success" build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed" - visit ci_admin_builds_path + visit admin_builds_path - within ".nav.nav-tabs" do + within ".center-top-menu" do click_on "Running" end - expect(page.find(".build-link")).to have_content(build1.id) - expect(page.find(".build-link")).not_to have_content(build.id) + expect(page.find(".build-link")).to have_content(build.id) expect(page.find(".build-link")).not_to have_content(build2.id) expect(page.find(".build-link")).not_to have_content(build3.id) end diff --git a/spec/features/ci/admin/runners_spec.rb b/spec/features/atom/runners_spec.rb similarity index 87% rename from spec/features/ci/admin/runners_spec.rb rename to spec/features/atom/runners_spec.rb index b83744f53a..b1f2d40104 100644 --- a/spec/features/ci/admin/runners_spec.rb +++ b/spec/features/atom/runners_spec.rb @@ -10,7 +10,7 @@ describe "Admin Runners" do runner = FactoryGirl.create(:ci_runner) commit = FactoryGirl.create(:ci_commit) FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) - visit ci_admin_runners_path + visit admin_runners_path end it { page.has_text? "Manage Runners" } @@ -36,9 +36,9 @@ describe "Admin Runners" do let(:runner) { FactoryGirl.create :ci_runner } before do - @project1 = FactoryGirl.create(:ci_project) - @project2 = FactoryGirl.create(:ci_project) - visit ci_admin_runner_path(runner) + @project1 = FactoryGirl.create(:empty_project) + @project2 = FactoryGirl.create(:empty_project) + visit admin_runner_path(runner) end describe 'runner info' do @@ -53,7 +53,7 @@ describe "Admin Runners" do describe 'search' do before do search_form = find('#runner-projects-search') - search_form.fill_in 'search', with: @project1.gl_project.name + search_form.fill_in 'search', with: @project1.name search_form.click_button 'Search' end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 1f99a808f8..61d81cb0c8 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -7,15 +7,15 @@ describe "Builds" do login_as(:user) @commit = FactoryGirl.create :ci_commit @build = FactoryGirl.create :ci_build, commit: @commit - @gl_project = @commit.project.gl_project - @gl_project.team << [@user, :master] + @project = @commit.project + @project.team << [@user, :master] end describe "GET /:project/builds" do context "Running scope" do before do @build.run! - visit namespace_project_builds_path(@gl_project.namespace, @gl_project) + visit namespace_project_builds_path(@project.namespace, @project) end it { expect(page).to have_content 'Running' } @@ -28,7 +28,7 @@ describe "Builds" do context "Finished scope" do before do @build.run! - visit namespace_project_builds_path(@gl_project.namespace, @gl_project, scope: :finished) + visit namespace_project_builds_path(@project.namespace, @project, scope: :finished) end it { expect(page).to have_content 'No builds to show' } @@ -37,8 +37,8 @@ describe "Builds" do context "All builds" do before do - @gl_project.ci_builds.running_or_pending.each(&:success) - visit namespace_project_builds_path(@gl_project.namespace, @gl_project, scope: :all) + @project.ci_builds.running_or_pending.each(&:success) + visit namespace_project_builds_path(@project.namespace, @project, scope: :all) end it { expect(page).to have_content 'All' } @@ -52,7 +52,7 @@ describe "Builds" do describe "POST /:project/builds/:id/cancel_all" do before do @build.run! - visit namespace_project_builds_path(@gl_project.namespace, @gl_project) + visit namespace_project_builds_path(@project.namespace, @project) click_link "Cancel running" end @@ -62,7 +62,7 @@ describe "Builds" do describe "GET /:project/builds/:id" do before do - visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + visit namespace_project_build_path(@project.namespace, @project, @build) end it { expect(page).to have_content @commit.sha[0..7] } @@ -72,7 +72,7 @@ describe "Builds" do context "Download artifacts" do before do @build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + visit namespace_project_build_path(@project.namespace, @project, @build) end it { expect(page).to have_content 'Download artifacts' } @@ -82,7 +82,7 @@ describe "Builds" do describe "POST /:project/builds/:id/cancel" do before do @build.run! - visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + visit namespace_project_build_path(@project.namespace, @project, @build) click_link "Cancel" end @@ -93,7 +93,7 @@ describe "Builds" do describe "POST /:project/builds/:id/retry" do before do @build.run! - visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + visit namespace_project_build_path(@project.namespace, @project, @build) click_link "Cancel" click_link 'Retry' end @@ -105,7 +105,7 @@ describe "Builds" do describe "GET /:project/builds/:id/download" do before do @build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + visit namespace_project_build_path(@project.namespace, @project, @build) click_link 'Download artifacts' end diff --git a/spec/features/ci/admin/events_spec.rb b/spec/features/ci/admin/events_spec.rb deleted file mode 100644 index a7e75cc4f6..0000000000 --- a/spec/features/ci/admin/events_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe "Admin Events" do - let(:event) { FactoryGirl.create :ci_admin_event } - - before do - skip_ci_admin_auth - login_as :user - end - - describe "GET /admin/events" do - before do - event - visit ci_admin_events_path - end - - it { expect(page).to have_content "Events" } - it { expect(page).to have_content event.description } - end -end diff --git a/spec/features/ci/admin/projects_spec.rb b/spec/features/ci/admin/projects_spec.rb deleted file mode 100644 index b88f55a680..0000000000 --- a/spec/features/ci/admin/projects_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'spec_helper' - -describe "Admin Projects" do - let(:project) { FactoryGirl.create :ci_project } - - before do - skip_ci_admin_auth - login_as :user - end - - describe "GET /admin/projects" do - before do - project - visit ci_admin_projects_path - end - - it { expect(page).to have_content "Projects" } - end -end diff --git a/spec/features/ci_settings_spec.rb b/spec/features/ci_settings_spec.rb deleted file mode 100644 index 7e25e88301..0000000000 --- a/spec/features/ci_settings_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'spec_helper' - -describe "CI settings" do - let(:user) { create(:user) } - before { login_as(user) } - - before do - @project = FactoryGirl.create :ci_project - @gl_project = @project.gl_project - @gl_project.team << [user, :master] - visit edit_namespace_project_ci_settings_path(@gl_project.namespace, @gl_project) - end - - it { expect(page).to have_content 'Build Schedule' } - - it "updates configuration" do - fill_in 'Timeout', with: '70' - click_button 'Save changes' - expect(page).to have_content 'was successfully updated' - expect(find_field('Timeout').value).to eq '70' - end -end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 90739cd6a2..cc0d4c150f 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -9,8 +9,7 @@ describe "Commits" do before do login_as :user project.team << [@user, :master] - @ci_project = project.ensure_gitlab_ci_project - @commit = FactoryGirl.create :ci_commit, gl_project: project, sha: project.commit.sha + @commit = FactoryGirl.create :ci_commit, project: project, sha: project.commit.sha @build = FactoryGirl.create :ci_build, commit: @commit @generic_status = FactoryGirl.create :generic_commit_status, commit: @commit end diff --git a/spec/features/ci/lint_spec.rb b/spec/features/lint_spec.rb similarity index 100% rename from spec/features/ci/lint_spec.rb rename to spec/features/lint_spec.rb diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index b025902663..a4562f2fae 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -8,24 +8,24 @@ describe "Runners" do describe "specific runners" do before do - @project = FactoryGirl.create :ci_project - @project.gl_project.team << [user, :master] + @project = FactoryGirl.create :empty_project, shared_runners_enabled: false + @project.team << [user, :master] - @project2 = FactoryGirl.create :ci_project - @project2.gl_project.team << [user, :master] + @project2 = FactoryGirl.create :empty_project + @project2.team << [user, :master] - @project3 = FactoryGirl.create :ci_project - @project3.gl_project.team << [user, :developer] + @project3 = FactoryGirl.create :empty_project + @project3.team << [user, :developer] @shared_runner = FactoryGirl.create :ci_shared_runner @specific_runner = FactoryGirl.create :ci_specific_runner @specific_runner2 = FactoryGirl.create :ci_specific_runner @specific_runner3 = FactoryGirl.create :ci_specific_runner - @project.runners << @specific_runner - @project2.runners << @specific_runner2 - @project3.runners << @specific_runner3 + @project.ci_runners << @specific_runner + @project2.ci_runners << @specific_runner2 + @project3.ci_runners << @specific_runner3 - visit runners_path(@project.gl_project) + visit runners_path(@project) end before do @@ -48,8 +48,8 @@ describe "Runners" do end it "disables specific runner for project" do - @project2.runners << @specific_runner - visit runners_path(@project.gl_project) + @project2.ci_runners << @specific_runner + visit runners_path(@project) within ".activated-specific-runners" do click_on "Disable for this project" @@ -69,9 +69,9 @@ describe "Runners" do describe "shared runners" do before do - @project = FactoryGirl.create :ci_project - @project.gl_project.team << [user, :master] - visit runners_path(@project.gl_project) + @project = FactoryGirl.create :empty_project, shared_runners_enabled: false + @project.team << [user, :master] + visit runners_path(@project) end it "enables shared runners" do @@ -82,14 +82,14 @@ describe "Runners" do describe "show page" do before do - @project = FactoryGirl.create :ci_project - @project.gl_project.team << [user, :master] + @project = FactoryGirl.create :empty_project + @project.team << [user, :master] @specific_runner = FactoryGirl.create :ci_specific_runner - @project.runners << @specific_runner + @project.ci_runners << @specific_runner end it "shows runner information" do - visit runners_path(@project.gl_project) + visit runners_path(@project) click_on @specific_runner.short_sha expect(page).to have_content(@specific_runner.platform) end diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index 69492d5887..b0705a45ae 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -5,25 +5,24 @@ describe 'Triggers' do before { login_as(user) } before do - @project = FactoryGirl.create :ci_project - @gl_project = @project.gl_project - @gl_project.team << [user, :master] - visit namespace_project_triggers_path(@gl_project.namespace, @gl_project) + @project = FactoryGirl.create :empty_project + @project.team << [user, :master] + visit namespace_project_triggers_path(@project.namespace, @project) end context 'create a trigger' do before do click_on 'Add Trigger' - expect(@project.triggers.count).to eq(1) + expect(@project.ci_triggers.count).to eq(1) end it 'contains trigger token' do - expect(page).to have_content(@project.triggers.first.token) + expect(page).to have_content(@project.ci_triggers.first.token) end it 'revokes the trigger' do click_on 'Revoke' - expect(@project.triggers.count).to eq(0) + expect(@project.ci_triggers.count).to eq(0) end end end diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb index adb602f3ed..a6e68eeefb 100644 --- a/spec/features/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -6,20 +6,19 @@ describe "Variables" do describe "specific runners" do before do - @project = FactoryGirl.create :ci_project - @gl_project = @project.gl_project - @gl_project.team << [user, :master] + @project = FactoryGirl.create :empty_project + @project.team << [user, :master] end it "creates variable", js: true do - visit namespace_project_variables_path(@gl_project.namespace, @gl_project) + visit namespace_project_variables_path(@project.namespace, @project) click_on "Add a variable" fill_in "Key", with: "SECRET_KEY" fill_in "Value", with: "SECRET_VALUE" click_on "Save changes" expect(page).to have_content("Variables were successfully updated.") - expect(@project.variables.count).to eq(1) + expect(@project.ci_variables.count).to eq(1) end end end diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb index 73458735ad..2dd27464de 100644 --- a/spec/lib/gitlab/backend/grack_auth_spec.rb +++ b/spec/lib/gitlab/backend/grack_auth_spec.rb @@ -191,15 +191,10 @@ describe Grack::Auth, lib: true do context "when a gitlab ci token is provided" do let(:token) { "123" } - let(:gitlab_ci_project) { FactoryGirl.create :ci_project, token: token } + let(:project) { FactoryGirl.create :empty_project, token: token } before do - project.gitlab_ci_project = gitlab_ci_project - project.save - - gitlab_ci_service = project.build_gitlab_ci_service - gitlab_ci_service.active = true - gitlab_ci_service.save + project.update_attributes(token: token, builds_enabled: true) env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials("gitlab-ci-token", token) end diff --git a/spec/lib/gitlab/build_data_builder_spec.rb b/spec/lib/gitlab/build_data_builder_spec.rb index af2de207eb..839b30f1ff 100644 --- a/spec/lib/gitlab/build_data_builder_spec.rb +++ b/spec/lib/gitlab/build_data_builder_spec.rb @@ -14,7 +14,7 @@ describe 'Gitlab::BuildDataBuilder' do it { expect(data[:tag]).to eq(build.tag) } it { expect(data[:build_id]).to eq(build.id) } it { expect(data[:build_status]).to eq(build.status) } - it { expect(data[:project_id]).to eq(build.gl_project.id) } - it { expect(data[:project_name]).to eq(build.gl_project.name_with_namespace) } + it { expect(data[:project_id]).to eq(build.project.id) } + it { expect(data[:project_name]).to eq(build.project.name_with_namespace) } end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index b67b84959d..5f64453a35 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -73,26 +73,4 @@ describe ApplicationSetting, models: true do expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com']) end end - - context 'shared runners' do - let(:gl_project) { create(:empty_project) } - - before do - allow_any_instance_of(Project).to receive(:current_application_settings).and_return(setting) - end - - subject { gl_project.ensure_gitlab_ci_project.shared_runners_enabled } - - context 'enabled' do - before { setting.update_attributes(shared_runners_enabled: true) } - - it { is_expected.to be_truthy } - end - - context 'disabled' do - before { setting.update_attributes(shared_runners_enabled: false) } - - it { is_expected.to be_falsey } - end - end end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 70c831b7cb..70a63c05b2 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -26,9 +26,8 @@ require 'spec_helper' describe Ci::Build, models: true do - let(:project) { FactoryGirl.create :ci_project } - let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } - let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } + let(:project) { FactoryGirl.create :empty_project } + let(:commit) { FactoryGirl.create :ci_commit, project: project } let(:build) { FactoryGirl.create :ci_build, commit: commit } it { is_expected.to validate_presence_of :ref } @@ -120,11 +119,12 @@ describe Ci::Build, models: true do end end - describe :timeout do - subject { build.timeout } - - it { is_expected.to eq(commit.project.timeout) } - end + # TODO: build timeout + # describe :timeout do + # subject { build.timeout } + # + # it { is_expected.to eq(commit.project.timeout) } + # end describe :options do let(:options) do @@ -140,11 +140,12 @@ describe Ci::Build, models: true do it { is_expected.to eq(options) } end - describe :allow_git_fetch do - subject { build.allow_git_fetch } - - it { is_expected.to eq(project.allow_git_fetch) } - end + # TODO: allow_git_fetch + # describe :allow_git_fetch do + # subject { build.allow_git_fetch } + # + # it { is_expected.to eq(project.allow_git_fetch) } + # end describe :project do subject { build.project } @@ -164,12 +165,6 @@ describe Ci::Build, models: true do it { is_expected.to eq(project.name) } end - describe :repo_url do - subject { build.repo_url } - - it { is_expected.to eq(project.repo_url_with_auth) } - end - describe :extract_coverage do context 'valid content & regex' do subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') } @@ -237,7 +232,7 @@ describe Ci::Build, models: true do end before do - build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') + build.project.ci_variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') end it { is_expected.to eq(predefined_variables + yaml_variables + secure_variables) } @@ -266,44 +261,10 @@ describe Ci::Build, models: true do end end - describe :project_recipients do - let(:pusher_email) { 'pusher@gitlab.test' } - let(:user) { User.new(notification_email: pusher_email) } - subject { build.project_recipients } - - before do - build.update_attributes(user: user) - end - - it 'should return pusher_email as only recipient when no additional recipients are given' do - project.update_attributes(email_add_pusher: true, - email_recipients: '') - is_expected.to eq([pusher_email]) - end - - it 'should return pusher_email and additional recipients' do - project.update_attributes(email_add_pusher: true, - email_recipients: 'rec1 rec2') - is_expected.to eq(['rec1', 'rec2', pusher_email]) - end - - it 'should return recipients' do - project.update_attributes(email_add_pusher: false, - email_recipients: 'rec1 rec2') - is_expected.to eq(['rec1', 'rec2']) - end - - it 'should return unique recipients only' do - project.update_attributes(email_add_pusher: true, - email_recipients: "rec1 rec1 #{pusher_email}") - is_expected.to eq(['rec1', pusher_email]) - end - end - describe :can_be_served? do let(:runner) { FactoryGirl.create :ci_specific_runner } - before { build.project.runners << runner } + before { build.project.ci_runners << runner } context 'runner without tags' do it 'can handle builds without tags' do @@ -346,7 +307,7 @@ describe Ci::Build, models: true do let(:runner) { FactoryGirl.create :ci_specific_runner } before do - build.project.runners << runner + build.project.ci_runners << runner runner.update_attributes(contacted_at: 1.second.ago) end @@ -383,7 +344,7 @@ describe Ci::Build, models: true do let(:runner) { FactoryGirl.create :ci_specific_runner, contacted_at: 1.second.ago } before do - build.project.runners << runner + build.project.ci_runners << runner runner.save end @@ -415,4 +376,18 @@ describe Ci::Build, models: true do is_expected.to_not be_nil end end + + describe :repo_url do + let(:build) { FactoryGirl.create :ci_build } + let(:project) { build.project } + + subject { build.repo_url } + + it { is_expected.to be_a(String) } + it { is_expected.to end_with(".git") } + it { is_expected.to start_with(project.web_url[0..6]) } + it { is_expected.to include(project.token) } + it { is_expected.to include('gitlab-ci-token') } + it { is_expected.to include(project.web_url[7..-1]) } + end end diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 89813cdf7f..ac61c8fb52 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -13,17 +13,16 @@ # tag :boolean default(FALSE) # yaml_errors :text # committed_at :datetime -# gl_project_id :integer +# project_id :integer # require 'spec_helper' describe Ci::Commit, models: true do - let(:project) { FactoryGirl.create :ci_project } - let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } - let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } + let(:project) { FactoryGirl.create :empty_project } + let(:commit) { FactoryGirl.create :ci_commit, project: project } - it { is_expected.to belong_to(:gl_project) } + it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:statuses) } it { is_expected.to have_many(:trigger_requests) } it { is_expected.to have_many(:builds) } @@ -37,16 +36,16 @@ describe Ci::Commit, models: true do let(:project) { FactoryGirl.create :empty_project } it 'returns ordered list of commits' do - commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project - commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project + commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project + commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: project expect(project.ci_commits.ordered).to eq([commit2, commit1]) end it 'returns commits ordered by committed_at and id, with nulls last' do - commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project - commit2 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project - commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project - commit4 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project + commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project + commit2 = FactoryGirl.create :ci_commit, committed_at: nil, project: project + commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: project + commit4 = FactoryGirl.create :ci_commit, committed_at: nil, project: project expect(project.ci_commits.ordered).to eq([commit2, commit4, commit3, commit1]) end end @@ -162,7 +161,7 @@ describe Ci::Commit, models: true do end describe :create_builds do - let!(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } + let!(:commit) { FactoryGirl.create :ci_commit, project: project } def create_builds(trigger_request = nil) commit.create_builds('master', false, nil, trigger_request) @@ -390,9 +389,8 @@ describe Ci::Commit, models: true do end describe "coverage" do - let(:project) { FactoryGirl.create :ci_project, coverage_regex: "/.*/" } - let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } - let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } + let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" } + let(:commit) { FactoryGirl.create :ci_commit, project: project } it "calculates average when there are two builds with coverage" do FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb deleted file mode 100644 index e358aa0274..0000000000 --- a/spec/models/ci/project_spec.rb +++ /dev/null @@ -1,244 +0,0 @@ -# == Schema Information -# -# Table name: ci_projects -# -# id :integer not null, primary key -# name :string(255) -# timeout :integer default(3600), not null -# created_at :datetime -# updated_at :datetime -# token :string(255) -# default_ref :string(255) -# path :string(255) -# always_build :boolean default(FALSE), not null -# polling_interval :integer -# public :boolean default(FALSE), not null -# ssh_url_to_repo :string(255) -# gitlab_id :integer -# allow_git_fetch :boolean default(TRUE), not null -# email_recipients :string(255) default(""), not null -# email_add_pusher :boolean default(TRUE), not null -# email_only_broken_builds :boolean default(TRUE), not null -# skip_refs :string(255) -# coverage_regex :string(255) -# shared_runners_enabled :boolean default(FALSE) -# generated_yaml_config :text -# - -require 'spec_helper' - -describe Ci::Project, models: true do - let(:project) { FactoryGirl.create :ci_project } - let(:gl_project) { project.gl_project } - subject { project } - - it { is_expected.to have_many(:runner_projects) } - it { is_expected.to have_many(:runners) } - it { is_expected.to have_many(:events) } - it { is_expected.to have_many(:variables) } - it { is_expected.to have_many(:triggers) } - - it { is_expected.to validate_presence_of :timeout } - it { is_expected.to validate_presence_of :gitlab_id } - - describe 'before_validation' do - it 'should set an random token if none provided' do - project = FactoryGirl.create :ci_project_without_token - expect(project.token).not_to eq("") - end - - it 'should not set an random toke if one provided' do - project = FactoryGirl.create :ci_project - expect(project.token).to eq("iPWx6WM4lhHNedGfBpPJNP") - end - end - - describe :name_with_namespace do - subject { project.name_with_namespace } - - it { is_expected.to eq(project.name) } - it { is_expected.to eq(gl_project.name_with_namespace) } - end - - describe :path_with_namespace do - subject { project.path_with_namespace } - - it { is_expected.to eq(project.path) } - it { is_expected.to eq(gl_project.path_with_namespace) } - end - - describe :path_with_namespace do - subject { project.web_url } - - it { is_expected.to eq(gl_project.web_url) } - end - - describe :web_url do - subject { project.web_url } - - it { is_expected.to eq(project.gitlab_url) } - it { is_expected.to eq(gl_project.web_url) } - end - - describe :http_url_to_repo do - subject { project.http_url_to_repo } - - it { is_expected.to eq(gl_project.http_url_to_repo) } - end - - describe :ssh_url_to_repo do - subject { project.ssh_url_to_repo } - - it { is_expected.to eq(gl_project.ssh_url_to_repo) } - end - - describe :commits do - subject { project.commits } - - before do - FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: gl_project - end - - it { is_expected.to eq(gl_project.ci_commits) } - end - - describe :builds do - subject { project.builds } - - before do - commit = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: gl_project - FactoryGirl.create :ci_build, commit: commit - end - - it { is_expected.to eq(gl_project.ci_builds) } - end - - describe "ordered_by_last_commit_date" do - it "returns ordered projects" do - newest_project = FactoryGirl.create :empty_project - newest_ci_project = newest_project.ensure_gitlab_ci_project - oldest_project = FactoryGirl.create :empty_project - oldest_ci_project = oldest_project.ensure_gitlab_ci_project - project_without_commits = FactoryGirl.create :empty_project - ci_project_without_commits = project_without_commits.ensure_gitlab_ci_project - - FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: newest_project - FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: oldest_project - - expect(Ci::Project.ordered_by_last_commit_date).to eq([newest_ci_project, oldest_ci_project, ci_project_without_commits]) - end - end - - context :valid_project do - let(:commit) { FactoryGirl.create(:ci_commit) } - - context :project_with_commit_and_builds do - let(:project) { commit.project } - - before do - FactoryGirl.create(:ci_build, commit: commit) - end - - it { expect(project.status).to eq('pending') } - it { expect(project.last_commit).to be_kind_of(Ci::Commit) } - it { expect(project.human_status).to eq('pending') } - end - end - - describe '#email_notification?' do - it do - project = FactoryGirl.create :ci_project, email_add_pusher: true - expect(project.email_notification?).to eq(true) - end - - it do - project = FactoryGirl.create :ci_project, email_add_pusher: false, email_recipients: 'test tesft' - expect(project.email_notification?).to eq(true) - end - - it do - project = FactoryGirl.create :ci_project, email_add_pusher: false, email_recipients: '' - expect(project.email_notification?).to eq(false) - end - end - - describe '#broken_or_success?' do - it do - project = FactoryGirl.create :ci_project, email_add_pusher: true - allow(project).to receive(:broken?).and_return(true) - allow(project).to receive(:success?).and_return(true) - expect(project.broken_or_success?).to eq(true) - end - - it do - project = FactoryGirl.create :ci_project, email_add_pusher: true - allow(project).to receive(:broken?).and_return(true) - allow(project).to receive(:success?).and_return(false) - expect(project.broken_or_success?).to eq(true) - end - - it do - project = FactoryGirl.create :ci_project, email_add_pusher: true - allow(project).to receive(:broken?).and_return(false) - allow(project).to receive(:success?).and_return(true) - expect(project.broken_or_success?).to eq(true) - end - - it do - project = FactoryGirl.create :ci_project, email_add_pusher: true - allow(project).to receive(:broken?).and_return(false) - allow(project).to receive(:success?).and_return(false) - expect(project.broken_or_success?).to eq(false) - end - end - - describe :repo_url_with_auth do - let(:project) { FactoryGirl.create :ci_project } - subject { project.repo_url_with_auth } - - it { is_expected.to be_a(String) } - it { is_expected.to end_with(".git") } - it { is_expected.to start_with(project.gitlab_url[0..6]) } - it { is_expected.to include(project.token) } - it { is_expected.to include('gitlab-ci-token') } - it { is_expected.to include(project.gitlab_url[7..-1]) } - end - - describe :any_runners do - it "there are no runners available" do - project = FactoryGirl.create(:ci_project) - expect(project.any_runners?).to be_falsey - end - - it "there is a specific runner" do - project = FactoryGirl.create(:ci_project) - project.runners << FactoryGirl.create(:ci_specific_runner) - expect(project.any_runners?).to be_truthy - end - - it "there is a shared runner" do - project = FactoryGirl.create(:ci_project, shared_runners_enabled: true) - FactoryGirl.create(:ci_shared_runner) - expect(project.any_runners?).to be_truthy - end - - it "there is a shared runner, but they are prohibited to use" do - project = FactoryGirl.create(:ci_project) - FactoryGirl.create(:ci_shared_runner) - expect(project.any_runners?).to be_falsey - end - - it "checks the presence of specific runner" do - project = FactoryGirl.create(:ci_project) - specific_runner = FactoryGirl.create(:ci_specific_runner) - project.runners << specific_runner - expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy - end - - it "checks the presence of shared runner" do - project = FactoryGirl.create(:ci_project, shared_runners_enabled: true) - shared_runner = FactoryGirl.create(:ci_shared_runner) - expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy - end - end -end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 6ebb5e8686..33388f9782 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -38,7 +38,7 @@ describe Ci::Runner, models: true do end describe :assign_to do - let!(:project) { FactoryGirl.create :ci_project } + let!(:project) { FactoryGirl.create :empty_project } let!(:shared_runner) { FactoryGirl.create(:ci_shared_runner) } before { shared_runner.assign_to(project) } @@ -116,18 +116,18 @@ describe Ci::Runner, models: true do describe "belongs_to_one_project?" do it "returns false if there are two projects runner assigned to" do runner = FactoryGirl.create(:ci_specific_runner) - project = FactoryGirl.create(:ci_project) - project1 = FactoryGirl.create(:ci_project) - project.runners << runner - project1.runners << runner + project = FactoryGirl.create(:empty_project) + project1 = FactoryGirl.create(:empty_project) + project.ci_runners << runner + project1.ci_runners << runner expect(runner.belongs_to_one_project?).to be_falsey end it "returns true" do runner = FactoryGirl.create(:ci_specific_runner) - project = FactoryGirl.create(:ci_project) - project.runners << runner + project = FactoryGirl.create(:empty_project) + project.ci_runners << runner expect(runner.belongs_to_one_project?).to be_truthy end diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb index 61eb3c0829..cb2f51e201 100644 --- a/spec/models/ci/trigger_spec.rb +++ b/spec/models/ci/trigger_spec.rb @@ -13,7 +13,7 @@ require 'spec_helper' describe Ci::Trigger, models: true do - let(:project) { FactoryGirl.create :ci_project } + let(:project) { FactoryGirl.create :empty_project } describe 'before_validation' do it 'should set an random token if none provided' do diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 5e311ead28..6131191f0f 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -44,7 +44,7 @@ describe CommitStatus, models: true do it { is_expected.to delegate_method(:sha).to(:commit) } it { is_expected.to delegate_method(:short_sha).to(:commit) } - it { is_expected.to delegate_method(:gl_project).to(:commit) } + it { is_expected.to delegate_method(:project).to(:commit) } it { is_expected.to respond_to :success? } it { is_expected.to respond_to :failed? } diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb deleted file mode 100644 index 835bf36405..0000000000 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# - -require 'spec_helper' - -describe GitlabCiService, models: true do - describe 'associations' do - it { is_expected.to belong_to(:project) } - it { is_expected.to have_one(:service_hook) } - end - - describe 'commits methods' do - before do - @ci_project = create(:ci_project) - @service = GitlabCiService.new - allow(@service).to receive_messages( - service_hook: true, - project_url: 'http://ci.gitlab.org/projects/2', - token: 'verySecret', - project: @ci_project.gl_project - ) - end - - describe :build_page do - it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://localhost/#{@ci_project.gl_project.path_with_namespace}/commit/2ab7834c/builds")} - end - - describe "execute" do - let(:user) { create(:user, username: 'username') } - let(:project) { create(:project, name: 'project') } - let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } - - it "calls CreateCommitService" do - expect_any_instance_of(Ci::CreateCommitService).to receive(:execute).with(@ci_project, user, push_sample_data) - - @service.execute(push_sample_data) - end - end - end -end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 6ddb0e2b8f..ee106b11fb 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -54,6 +54,13 @@ describe Project, models: true do it { is_expected.to have_one(:slack_service).dependent(:destroy) } it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) } + it { is_expected.to have_many(:ci_commits) } + it { is_expected.to have_many(:ci_statuses) } + it { is_expected.to have_many(:ci_builds) } + it { is_expected.to have_many(:ci_runner_projects) } + it { is_expected.to have_many(:ci_runners) } + it { is_expected.to have_many(:ci_variables) } + it { is_expected.to have_many(:ci_triggers) } end describe 'modules' do @@ -88,6 +95,18 @@ describe Project, models: true do expect(project2.errors[:limit_reached].first).to match(/Your project limit is 0/) end end + + describe 'project token' do + it 'should set an random token if none provided' do + project = FactoryGirl.create :empty_project, token: '' + expect(project.token).not_to eq('') + end + + it 'should not set an random toke if one provided' do + project = FactoryGirl.create :empty_project, token: 'my-token' + expect(project.token).to eq('my-token') + end + end describe 'Respond to' do it { is_expected.to respond_to(:url_to_repo) } @@ -395,12 +414,7 @@ describe Project, models: true do describe :ci_commit do let(:project) { create :project } - let(:commit) { create :ci_commit, gl_project: project } - - before do - project.ensure_gitlab_ci_project - project.create_gitlab_ci_service(active: true) - end + let(:commit) { create :ci_commit, project: project } it { expect(project.ci_commit(commit.sha)).to eq(commit) } end @@ -412,9 +426,7 @@ describe Project, models: true do subject { project.builds_enabled } - it { is_expected.to eq(project.gitlab_ci_service.active) } it { expect(project.builds_enabled?).to be_truthy } - it { expect(project.gitlab_ci_project).to be_a(Ci::Project) } end describe '.trending' do @@ -475,4 +487,65 @@ describe Project, models: true do it { is_expected.to eq([]) } end end + + context 'shared runners by default' do + let(:project) { create(:empty_project) } + + subject { project.shared_runners_enabled } + + context 'are enabled' do + before { stub_application_setting(shared_runners_enabled: true) } + + it { is_expected.to be_truthy } + end + + context 'are disabled' do + before { stub_application_setting(shared_runners_enabled: false) } + + it { is_expected.to be_falsey } + end + end + + describe :any_runners do + let(:project) { create(:empty_project, shared_runners_enabled: shared_runners_enabled) } + let(:specific_runner) { create(:ci_specific_runner) } + let(:shared_runner) { create(:ci_shared_runner) } + + context 'for shared runners disabled' do + let(:shared_runners_enabled) { false } + + it 'there are no runners available' do + expect(project.any_runners?).to be_falsey + end + + it 'there is a specific runner' do + project.ci_runners << specific_runner + expect(project.any_runners?).to be_truthy + end + + it 'there is a shared runner, but they are prohibited to use' do + shared_runner + expect(project.any_runners?).to be_falsey + end + + it 'checks the presence of specific runner' do + project.ci_runners << specific_runner + expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy + end + end + + context 'for shared runners enabled' do + let(:shared_runners_enabled) { true } + + it 'there is a shared runner' do + shared_runner + expect(project.any_runners?).to be_truthy + end + + it 'checks the presence of shared runner' do + shared_runner + expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy + end + end + end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 24b765f497..e784b7d1f2 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -86,18 +86,6 @@ describe API::API, api: true do expect(json_response).to be_an Array expect(json_response.first['id']).to eq(project3.id) end - - it 'returns projects in the correct order when ci_enabled_first parameter is passed' do - [project, project2, project3].each do |project| - project.builds_enabled = false - project.build_missing_services - end - project2.builds_enabled = true - get api('/projects', user), { ci_enabled_first: 'true' } - expect(response.status).to eq(200) - expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(project2.id) - end end end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index c2be045099..0aa9981ea8 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -4,8 +4,7 @@ describe Ci::API::API do include ApiHelpers let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) } - let(:project) { FactoryGirl.create(:ci_project) } - let(:gl_project) { project.gl_project } + let(:project) { FactoryGirl.create(:empty_project) } before do stub_ci_commit_to_return_yaml_file @@ -13,16 +12,15 @@ describe Ci::API::API do describe "Builds API for runners" do let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") } - let(:shared_project) { FactoryGirl.create(:ci_project, name: "SharedProject") } - let(:shared_gl_project) { shared_project.gl_project } + let(:shared_project) { FactoryGirl.create(:empty_project, name: "SharedProject") } before do - FactoryGirl.create :ci_runner_project, project_id: project.id, runner_id: runner.id + FactoryGirl.create :ci_runner_project, project: project, runner: runner end describe "POST /builds/register" do it "should start a build" do - commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) + commit = FactoryGirl.create(:ci_commit, project: project) commit.create_builds('master', false, nil) build = commit.builds.first @@ -40,7 +38,7 @@ describe Ci::API::API do end it "should return 404 error if no builds for specific runner" do - commit = FactoryGirl.create(:ci_commit, gl_project: shared_gl_project) + commit = FactoryGirl.create(:ci_commit, project: shared_project) FactoryGirl.create(:ci_build, commit: commit, status: 'pending') post ci_api("/builds/register"), token: runner.token @@ -49,7 +47,7 @@ describe Ci::API::API do end it "should return 404 error if no builds for shared runner" do - commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) + commit = FactoryGirl.create(:ci_commit, project: project) FactoryGirl.create(:ci_build, commit: commit, status: 'pending') post ci_api("/builds/register"), token: shared_runner.token @@ -58,7 +56,7 @@ describe Ci::API::API do end it "returns options" do - commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) + commit = FactoryGirl.create(:ci_commit, project: project) commit.create_builds('master', false, nil) post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -68,9 +66,9 @@ describe Ci::API::API do end it "returns variables" do - commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) + commit = FactoryGirl.create(:ci_commit, project: project) commit.create_builds('master', false, nil) - project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") + project.ci_variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -85,11 +83,11 @@ describe Ci::API::API do it "returns variables for triggers" do trigger = FactoryGirl.create(:ci_trigger, project: project) - commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) + commit = FactoryGirl.create(:ci_commit, project: project) trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger) commit.create_builds('master', false, nil, trigger_request) - project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") + project.ci_variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -106,7 +104,7 @@ describe Ci::API::API do end describe "PUT /builds/:id" do - let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project)} + let(:commit) { FactoryGirl.create(:ci_commit, project: project)} let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) } it "should update a running build" do @@ -126,7 +124,7 @@ describe Ci::API::API do context "Artifacts" do let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } - let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } + let(:commit) { FactoryGirl.create(:ci_commit, project: project) } let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) } let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") } let(:post_url) { ci_api("/builds/#{build.id}/artifacts") } diff --git a/spec/requests/ci/api/commits_spec.rb b/spec/requests/ci/api/commits_spec.rb deleted file mode 100644 index aa51ba95bc..0000000000 --- a/spec/requests/ci/api/commits_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'spec_helper' - -describe Ci::API::API, 'Commits' do - include ApiHelpers - - let(:project) { FactoryGirl.create(:ci_project) } - let(:gl_project) { project.gl_project } - let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - - let(:options) do - { - project_token: project.token, - project_id: project.id - } - end - - describe "GET /commits" do - before { commit } - - it "should return commits per project" do - get ci_api("/commits"), options - - expect(response.status).to eq(200) - expect(json_response.count).to eq(1) - expect(json_response.first["project_id"]).to eq(project.id) - expect(json_response.first["sha"]).to eq(commit.sha) - end - end - - describe "POST /commits" do - let(:data) do - { - "before" => "95790bf891e76fee5e1747ab589903a6a1f80f22", - "after" => "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", - "ref" => "refs/heads/master", - "commits" => [ - { - "id" => "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", - "message" => "Update Catalan translation to e38cb41.", - "timestamp" => "2011-12-12T14:27:31+02:00", - "url" => "http://localhost/diaspora/commits/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", - "author" => { - "name" => "Jordi Mallach", - "email" => "jordi@softcatala.org", - } - } - ] - } - end - - it "should create a build" do - post ci_api("/commits"), options.merge(data: data) - - expect(response.status).to eq(201) - expect(json_response['sha']).to eq("da1560886d4f094c3e6c9ef40349f7d38b5d27d7") - end - - it "should return 400 error if no data passed" do - post ci_api("/commits"), options - - expect(response.status).to eq(400) - expect(json_response['message']).to eq("400 (Bad request) \"data\" not given") - end - end -end diff --git a/spec/requests/ci/api/projects_spec.rb b/spec/requests/ci/api/projects_spec.rb deleted file mode 100644 index b1fbdac597..0000000000 --- a/spec/requests/ci/api/projects_spec.rb +++ /dev/null @@ -1,159 +0,0 @@ -require 'spec_helper' - -describe Ci::API::API do - include ApiHelpers - - let(:gitlab_url) { GitlabCi.config.gitlab_ci.url } - let(:user) { create(:user) } - let(:private_token) { user.private_token } - - let(:options) do - { - private_token: private_token, - url: gitlab_url - } - end - - before do - stub_gitlab_calls - end - - context "requests for scoped projects" do - # NOTE: These ids are tied to the actual projects on demo.gitlab.com - describe "GET /projects" do - let!(:project1) { FactoryGirl.create(:ci_project) } - let!(:project2) { FactoryGirl.create(:ci_project) } - - before do - project1.gl_project.team << [user, :developer] - project2.gl_project.team << [user, :developer] - end - - it "should return all projects on the CI instance" do - get ci_api("/projects"), options - expect(response.status).to eq(200) - expect(json_response.count).to eq(2) - expect(json_response.first["id"]).to eq(project1.id) - expect(json_response.last["id"]).to eq(project2.id) - end - end - - describe "GET /projects/owned" do - let!(:gl_project1) {FactoryGirl.create(:empty_project, namespace: user.namespace)} - let!(:gl_project2) {FactoryGirl.create(:empty_project, namespace: user.namespace)} - let!(:project1) { gl_project1.ensure_gitlab_ci_project } - let!(:project2) { gl_project2.ensure_gitlab_ci_project } - - before do - project1.gl_project.team << [user, :developer] - project2.gl_project.team << [user, :developer] - end - - it "should return all projects on the CI instance" do - get ci_api("/projects/owned"), options - - expect(response.status).to eq(200) - expect(json_response.count).to eq(2) - end - end - end - - describe "GET /projects/:id" do - let!(:project) { FactoryGirl.create(:ci_project) } - - before do - project.gl_project.team << [user, :developer] - end - - context "with an existing project" do - it "should retrieve the project info" do - get ci_api("/projects/#{project.id}"), options - expect(response.status).to eq(200) - expect(json_response['id']).to eq(project.id) - end - end - - context "with a non-existing project" do - it "should return 404 error if project not found" do - get ci_api("/projects/non_existent_id"), options - expect(response.status).to eq(404) - end - end - end - - describe "PUT /projects/:id" do - let!(:project) { FactoryGirl.create(:ci_project) } - let!(:project_info) { { default_ref: "develop" } } - - before do - options.merge!(project_info) - end - - it "should update a specific project's information" do - project.gl_project.team << [user, :master] - put ci_api("/projects/#{project.id}"), options - expect(response.status).to eq(200) - expect(json_response["default_ref"]).to eq(project_info[:default_ref]) - end - - it "fails to update a non-existing project" do - put ci_api("/projects/non-existant-id"), options - expect(response.status).to eq(404) - end - - it "non-manager is not authorized" do - put ci_api("/projects/#{project.id}"), options - expect(response.status).to eq(401) - end - end - - describe "POST /projects/:id/runners/:id" do - let(:project) { FactoryGirl.create(:ci_project) } - let(:runner) { FactoryGirl.create(:ci_runner) } - - it "should add the project to the runner" do - project.gl_project.team << [user, :master] - post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options - expect(response.status).to eq(201) - - project.reload - expect(project.runners.first.id).to eq(runner.id) - end - - it "should fail if it tries to link a non-existing project or runner" do - post ci_api("/projects/#{project.id}/runners/non-existing"), options - expect(response.status).to eq(404) - - post ci_api("/projects/non-existing/runners/#{runner.id}"), options - expect(response.status).to eq(404) - end - - it "non-manager is not authorized" do - allow_any_instance_of(User).to receive(:can_manage_project?).and_return(false) - post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options - expect(response.status).to eq(401) - end - end - - describe "DELETE /projects/:id/runners/:id" do - let(:project) { FactoryGirl.create(:ci_project) } - let(:runner) { FactoryGirl.create(:ci_runner) } - - it "should remove the project from the runner" do - project.gl_project.team << [user, :master] - post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options - - expect(project.runners).to be_present - delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options - expect(response.status).to eq(200) - - project.reload - expect(project.runners).to be_empty - end - - it "non-manager is not authorized" do - delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options - expect(response.status).to eq(401) - end - end -end diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index 11dc089e1f..ef2f759fda 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -8,29 +8,6 @@ describe Ci::API::API do stub_gitlab_calls end - describe "GET /runners" do - let(:gitlab_url) { GitlabCi.config.gitlab_ci.url } - let(:private_token) { create(:user).private_token } - let(:options) do - { - private_token: private_token, - url: gitlab_url - } - end - - before do - 5.times { FactoryGirl.create(:ci_runner) } - end - - it "should retrieve a list of all runners" do - get ci_api("/runners", nil), options - expect(response.status).to eq(200) - expect(json_response.count).to eq(5) - expect(json_response.last).to have_key("id") - expect(json_response.last).to have_key("token") - end - end - describe "POST /runners/register" do describe "should create a runner if token provided" do before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN } @@ -53,11 +30,11 @@ describe Ci::API::API do end describe "should create a runner if project token provided" do - let(:project) { FactoryGirl.create(:ci_project) } + let(:project) { FactoryGirl.create(:empty_project) } before { post ci_api("/runners/register"), token: project.token } it { expect(response.status).to eq(201) } - it { expect(project.runners.size).to eq(1) } + it { expect(project.ci_runners.size).to eq(1) } end it "should return 403 error if token is invalid" do diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb index a2b436d581..cd33a96e1a 100644 --- a/spec/requests/ci/api/triggers_spec.rb +++ b/spec/requests/ci/api/triggers_spec.rb @@ -5,15 +5,16 @@ describe Ci::API::API do describe 'POST /projects/:project_id/refs/:ref/trigger' do let!(:trigger_token) { 'secure token' } - let!(:gl_project) { FactoryGirl.create(:project) } - let!(:project) { gl_project.ensure_gitlab_ci_project } - let!(:project2) { FactoryGirl.create(:ci_project) } + let!(:project) { FactoryGirl.create(:project) } + let!(:project2) { FactoryGirl.create(:empty_project) } let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) } let(:options) do { token: trigger_token } end + let(:project_ci_id) { create_ci_id(project) } + let(:project2_ci_id) { create_ci_id(project2) } before do stub_ci_commit_to_return_yaml_file @@ -21,7 +22,7 @@ describe Ci::API::API do context 'Handles errors' do it 'should return bad request if token is missing' do - post ci_api("/projects/#{project.id}/refs/master/trigger") + post ci_api("/projects/#{project_ci_id}/refs/master/trigger") expect(response.status).to eq(400) end @@ -31,23 +32,23 @@ describe Ci::API::API do end it 'should return unauthorized if token is for different project' do - post ci_api("/projects/#{project2.id}/refs/master/trigger"), options + post ci_api("/projects/#{project2_ci_id}/refs/master/trigger"), options expect(response.status).to eq(401) end end context 'Have a commit' do - let(:commit) { project.commits.last } + let(:commit) { project.ci_commits.last } it 'should create builds' do - post ci_api("/projects/#{project.id}/refs/master/trigger"), options + post ci_api("/projects/#{project_ci_id}/refs/master/trigger"), options expect(response.status).to eq(201) commit.builds.reload expect(commit.builds.size).to eq(2) end it 'should return bad request with no builds created if there\'s no commit for that ref' do - post ci_api("/projects/#{project.id}/refs/other-branch/trigger"), options + post ci_api("/projects/#{project_ci_id}/refs/other-branch/trigger"), options expect(response.status).to eq(400) expect(json_response['message']).to eq('No builds created') end @@ -58,19 +59,19 @@ describe Ci::API::API do end it 'should validate variables to be a hash' do - post ci_api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: 'value') + post ci_api("/projects/#{project_ci_id}/refs/master/trigger"), options.merge(variables: 'value') expect(response.status).to eq(400) expect(json_response['message']).to eq('variables needs to be a hash') end it 'should validate variables needs to be a map of key-valued strings' do - post ci_api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) }) + post ci_api("/projects/#{project_ci_id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) }) expect(response.status).to eq(400) expect(json_response['message']).to eq('variables needs to be a map of key-valued strings') end it 'create trigger request with variables' do - post ci_api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: variables) + post ci_api("/projects/#{project_ci_id}/refs/master/trigger"), options.merge(variables: variables) expect(response.status).to eq(201) commit.builds.reload expect(commit.builds.first.trigger_request.variables).to eq(variables) @@ -78,4 +79,8 @@ describe Ci::API::API do end end end + + def create_ci_id(project) + ActiveRecord::Base.connection.insert("INSERT INTO ci_projects (gitlab_id) VALUES(#{project.id})") + end end diff --git a/spec/services/ci/create_commit_service_spec.rb b/spec/services/ci/create_commit_service_spec.rb index c2fafca2ad..7e2dcc503e 100644 --- a/spec/services/ci/create_commit_service_spec.rb +++ b/spec/services/ci/create_commit_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' module Ci describe CreateCommitService, services: true do let(:service) { CreateCommitService.new } - let(:project) { FactoryGirl.create(:ci_project) } + let(:project) { FactoryGirl.create(:empty_project) } let(:user) { nil } before do @@ -24,7 +24,7 @@ module Ci it { expect(commit).to be_kind_of(Commit) } it { expect(commit).to be_valid } it { expect(commit).to be_persisted } - it { expect(commit).to eq(project.commits.last) } + it { expect(commit).to eq(project.ci_commits.last) } it { expect(commit.builds.first).to be_kind_of(Build) } end diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index c80cb58163..dbdc5370bd 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -2,8 +2,7 @@ require 'spec_helper' describe Ci::CreateTriggerRequestService, services: true do let(:service) { Ci::CreateTriggerRequestService.new } - let(:gl_project) { create(:project) } - let(:project) { gl_project.ensure_gitlab_ci_project } + let(:project) { create(:project) } let(:trigger) { create(:ci_trigger, project: project) } before do @@ -29,7 +28,7 @@ describe Ci::CreateTriggerRequestService, services: true do before do stub_ci_commit_yaml_file('{}') - FactoryGirl.create :ci_commit, gl_project: gl_project + FactoryGirl.create :ci_commit, project: project end it { expect(subject).to be_nil } diff --git a/spec/services/ci/event_service_spec.rb b/spec/services/ci/event_service_spec.rb deleted file mode 100644 index 32516c75cf..0000000000 --- a/spec/services/ci/event_service_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'spec_helper' - -describe Ci::EventService, services: true do - let(:project) { FactoryGirl.create :ci_project } - let(:user) { double(username: "root", id: 1) } - - before do - Event.destroy_all - end - - describe :remove_project do - it "creates event" do - Ci::EventService.new.remove_project(user, project) - - expect(Ci::Event.admin.last.description).to eq("Project \"#{project.name_with_namespace}\" has been removed by root") - end - end - - describe :create_project do - it "creates event" do - Ci::EventService.new.create_project(user, project) - - expect(Ci::Event.admin.last.description).to eq("Project \"#{project.name_with_namespace}\" has been created by root") - end - end - - describe :change_project_settings do - it "creates event" do - Ci::EventService.new.change_project_settings(user, project) - - expect(Ci::Event.last.description).to eq("User \"root\" updated projects settings") - end - end -end diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index b43cabb4ee..870861ad20 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -3,16 +3,16 @@ require 'spec_helper' module Ci describe ImageForBuildService, services: true do let(:service) { ImageForBuildService.new } - let(:project) { FactoryGirl.create(:ci_project) } - let(:gl_project) { FactoryGirl.create(:project, gitlab_ci_project: project) } - let(:commit_sha) { gl_project.commit('master').sha } - let(:commit) { gl_project.ensure_ci_commit(commit_sha) } + let(:project) { FactoryGirl.create(:empty_project) } + let(:commit_sha) { '01234567890123456789' } + let(:commit) { project.ensure_ci_commit(commit_sha) } let(:build) { FactoryGirl.create(:ci_build, commit: commit) } describe :execute do before { build } context 'branch name' do + before { allow(project).to receive(:commit).and_return(OpenStruct.new(sha: commit_sha)) } before { build.run! } let(:image) { service.execute(project, ref: 'master') } diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index 379e07982f..e81f9e757a 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -3,14 +3,14 @@ require 'spec_helper' module Ci describe RegisterBuildService, services: true do let!(:service) { RegisterBuildService.new } - let!(:gl_project) { FactoryGirl.create :empty_project } - let!(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } + let!(:project) { FactoryGirl.create :empty_project, shared_runners_enabled: false } + let!(:commit) { FactoryGirl.create :ci_commit, project: project } let!(:pending_build) { FactoryGirl.create :ci_build, commit: commit } let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) } let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) } before do - specific_runner.assign_to(gl_project.ensure_gitlab_ci_project) + specific_runner.assign_to(project) end describe :execute do @@ -47,7 +47,7 @@ module Ci context 'allow shared runners' do before do - gl_project.gitlab_ci_project.update(shared_runners_enabled: true) + project.update(shared_runners_enabled: true) end context 'shared runner' do @@ -71,7 +71,7 @@ module Ci context 'disallow shared runners' do before do - gl_project.gitlab_ci_project.update(shared_runners_enabled: false) + project.update(shared_runners_enabled: false) end context 'shared runner' do From 8cdd54cc0696b76daa2baf463d02d944b50bac6a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 10 Dec 2015 17:29:44 +0100 Subject: [PATCH 562/908] Add runners token --- app/controllers/ci/application_controller.rb | 6 ------ app/controllers/ci/projects_controller.rb | 3 +-- app/models/ci/build.rb | 2 +- app/models/project.rb | 16 +++++++++------- db/migrate/20151203162135_add_ci_to_project.rb | 3 ++- .../20151204110613_migrate_ci_to_project.rb | 7 ++++--- .../20151204110832_add_index_to_ci_tables.rb | 3 ++- lib/ci/api/helpers.rb | 4 ---- lib/ci/api/runners.rb | 2 +- lib/ci/api/triggers.rb | 2 +- lib/gitlab/backend/grack_auth.rb | 2 +- spec/models/project_spec.rb | 2 +- 12 files changed, 23 insertions(+), 29 deletions(-) diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb index bc7f48b3c8..c420b59c3a 100644 --- a/app/controllers/ci/application_controller.rb +++ b/app/controllers/ci/application_controller.rb @@ -6,12 +6,6 @@ module Ci private - def authenticate_token! - unless project.valid_token?(params[:token]) - return head(403) - end - end - def authorize_access_project! unless can?(current_user, :read_project, project) return page_404 diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 7e62320bf2..3004c2d27f 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -22,8 +22,7 @@ module Ci protected def project - # TODO: what to do here? - @project ||= Project.find_by_ci_id(params[:id]) + @project ||= Project.find_by(ci_id: params[:id].to_i) end def no_cache diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 43ed8eb518..fac1d1c4c2 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -225,7 +225,7 @@ module Ci end def valid_token? token - project.valid_token? token + project.valid_runners_token? token end def target_url diff --git a/app/models/project.rb b/app/models/project.rb index e3eee36c25..a11bc9c4bd 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -169,9 +169,9 @@ class Project < ActiveRecord::Base if: ->(project) { project.avatar.present? && project.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } - before_validation :set_random_token - def set_random_token - self.token = SecureRandom.hex(15) if self.token.blank? + before_validation :set_runners_token_token + def set_runners_token_token + self.runners_token = SecureRandom.hex(15) if self.runners_token.blank? end mount_uploader :avatar, AvatarUploader @@ -270,9 +270,7 @@ class Project < ActiveRecord::Base end def find_by_ci_id(id) - ci_projects = Arel::Table.new(:ci_projects) - gitlab_id = ci_projects.where(ci_projects[:id].eq(id)).project(ci_projects[:gitlab_id]) - find_by("id=(#{gitlab_id.to_sql})") + find_by(ci_id: id.to_i) end def visibility_levels @@ -831,7 +829,11 @@ class Project < ActiveRecord::Base shared_runners_enabled? && Ci::Runner.shared.active.any?(&block) end - def valid_token? token + def valid_runners_token? token + self.token && self.token == token + end + + def valid_build_token? token self.token && self.token == token end diff --git a/db/migrate/20151203162135_add_ci_to_project.rb b/db/migrate/20151203162135_add_ci_to_project.rb index e95942666c..8a65abab63 100644 --- a/db/migrate/20151203162135_add_ci_to_project.rb +++ b/db/migrate/20151203162135_add_ci_to_project.rb @@ -1,8 +1,9 @@ class AddCiToProject < ActiveRecord::Migration def up + add_column :projects, :ci_id, :integer add_column :projects, :builds_enabled, :boolean, default: true, null: false add_column :projects, :shared_runners_enabled, :boolean, default: true, null: false - add_column :projects, :token, :string + add_column :projects, :runners_token, :string add_column :projects, :build_coverage_regex, :string add_column :projects, :build_allow_git_fetch, :boolean, default: true, null: false add_column :projects, :build_timeout, :integer, default: 3600, null: false diff --git a/db/migrate/20151204110613_migrate_ci_to_project.rb b/db/migrate/20151204110613_migrate_ci_to_project.rb index 1777b6170b..d17b2a425f 100644 --- a/db/migrate/20151204110613_migrate_ci_to_project.rb +++ b/db/migrate/20151204110613_migrate_ci_to_project.rb @@ -5,8 +5,9 @@ class MigrateCiToProject < ActiveRecord::Migration migrate_project_id_for_table('ci_variables') migrate_project_id_for_builds - migrate_project_column('shared_runners_enabled') - migrate_project_column('token') + migrate_project_column('id', 'ci_id') + migrate_project_column('shared_runners_enabled', 'shared_runners_enabled') + migrate_project_column('token', 'runners_token') migrate_project_column('coverage_regex', 'build_coverage_regex') migrate_project_column('allow_git_fetch', 'build_allow_git_fetch') migrate_project_column('timeout', 'build_timeout') @@ -25,7 +26,7 @@ class MigrateCiToProject < ActiveRecord::Migration def migrate_project_column(column, new_column = nil) new_column ||= column - subquery = "SELECT #{column} FROM ci_projects WHERE projects.id = ci_projects.gitlab_id" + subquery = "SELECT ci_projects.#{column} FROM ci_projects WHERE projects.id = ci_projects.gitlab_id" execute("UPDATE projects SET #{new_column}=(#{subquery}) WHERE #{new_column} IS NULL AND (#{subquery}) IS NOT NULL") end diff --git a/db/migrate/20151204110832_add_index_to_ci_tables.rb b/db/migrate/20151204110832_add_index_to_ci_tables.rb index b95931334c..9fedb5d612 100644 --- a/db/migrate/20151204110832_add_index_to_ci_tables.rb +++ b/db/migrate/20151204110832_add_index_to_ci_tables.rb @@ -4,8 +4,9 @@ class AddIndexToCiTables < ActiveRecord::Migration add_index :ci_runner_projects, :gl_project_id add_index :ci_triggers, :gl_project_id add_index :ci_variables, :gl_project_id - add_index :projects, :token + add_index :projects, :runners_token add_index :projects, :builds_enabled add_index :projects, [:builds_enabled, :shared_runners_enabled] + add_index :projects, [:ci_id] end end diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 0250233375..9891b5e38e 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -13,10 +13,6 @@ module Ci forbidden! unless current_runner end - def authenticate_project_token!(project) - forbidden! unless project.valid_token?(params[:project_token]) - end - def authenticate_build_token!(build) token = (params[BUILD_TOKEN_PARAM] || env[BUILD_TOKEN_HEADER]).to_s forbidden! unless token && build.valid_token?(token) diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb index dd77bd6586..1e738a7315 100644 --- a/lib/ci/api/runners.rb +++ b/lib/ci/api/runners.rb @@ -36,7 +36,7 @@ module Ci tag_list: params[:tag_list], is_shared: true ) - elsif project = Project.find_by(token: params[:token]) + elsif project = Project.find_by(runners_token: params[:token]) # Create a specific runner for project. project.ci_runners.create( description: params[:description], diff --git a/lib/ci/api/triggers.rb b/lib/ci/api/triggers.rb index 6d2cdd8c68..63b4211351 100644 --- a/lib/ci/api/triggers.rb +++ b/lib/ci/api/triggers.rb @@ -14,7 +14,7 @@ module Ci post ":id/refs/:ref/trigger" do required_attributes! [:token] - project = Project.find_by_ci_id(params[:id]) + project = Project.find_by(ci_id: params[:id].to_i) trigger = Ci::Trigger.find_by_token(params[:token].to_s) not_found! unless project && trigger unauthorized! unless trigger.project == project diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 5a032b572a..d854c1c868 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -78,7 +78,7 @@ module Grack underscored_service = matched_login['s'].underscore if underscored_service == 'gitlab_ci' - return project && project.builds_enabled? && project.valid_token?(password) + return project && project.builds_enabled? && project.valid_build_token?(password) elsif Service.available_services_names.include?(underscored_service) service_method = "#{underscored_service}_service" service = project.send(service_method) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index ee106b11fb..9c9266455c 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -55,7 +55,7 @@ describe Project, models: true do it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) } it { is_expected.to have_many(:ci_commits) } - it { is_expected.to have_many(:ci_statuses) } + it { is_expected.to have_many(:commit_statuses) } it { is_expected.to have_many(:ci_builds) } it { is_expected.to have_many(:ci_runner_projects) } it { is_expected.to have_many(:ci_runners) } From 64bfd9d71a4017e0b5336a2c1565926f4b8beedd Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 10 Dec 2015 17:44:06 +0100 Subject: [PATCH 563/908] Remove ci_ prefix from all ci related things --- .../admin/runner_projects_controller.rb | 4 +- app/controllers/projects/builds_controller.rb | 6 +- .../projects/runner_projects_controller.rb | 2 +- .../projects/runners_controller.rb | 6 +- .../projects/triggers_controller.rb | 8 +- .../projects/variables_controller.rb | 2 +- app/helpers/ci_status_helper.rb | 2 +- app/models/ci/build.rb | 2 +- app/models/ci/runner.rb | 2 +- app/models/project.rb | 16 +- app/models/user.rb | 2 +- app/services/ci/create_commit_service.rb | 28 --- app/services/create_commit_builds_service.rb | 29 +++ app/services/git_push_service.rb | 1 + app/services/git_tag_push_service.rb | 1 + app/views/admin/runners/show.html.haml | 2 +- app/views/layouts/nav/_project.html.haml | 2 +- app/views/projects/graphs/ci/_overall.haml | 8 +- app/views/projects/runners/_runner.html.haml | 4 +- app/views/projects/variables/show.html.haml | 4 +- lib/ci/api/runners.rb | 2 +- lib/ci/charts.rb | 4 +- spec/features/builds_spec.rb | 2 +- spec/features/runners_spec.rb | 10 +- spec/features/triggers_spec.rb | 6 +- spec/features/variables_spec.rb | 2 +- spec/models/build_spec.rb | 8 +- spec/models/ci/runner_spec.rb | 6 +- spec/models/project_spec.rb | 16 +- spec/requests/ci/api/builds_spec.rb | 4 +- spec/requests/ci/api/runners_spec.rb | 2 +- .../services/ci/create_commit_service_spec.rb | 172 ------------------ .../create_commit_builds_service_spec.rb | 171 +++++++++++++++++ 33 files changed, 269 insertions(+), 267 deletions(-) delete mode 100644 app/services/ci/create_commit_service.rb create mode 100644 app/services/create_commit_builds_service.rb delete mode 100644 spec/services/ci/create_commit_service_spec.rb create mode 100644 spec/services/create_commit_builds_service_spec.rb diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb index 20d621742f..d25619d94e 100644 --- a/app/controllers/admin/runner_projects_controller.rb +++ b/app/controllers/admin/runner_projects_controller.rb @@ -2,8 +2,8 @@ class Admin::RunnerProjectsController < Admin::ApplicationController before_action :project, only: [:create] def index - @runner_projects = project.ci_runner_projects.all - @runner_project = project.ci_runner_projects.new + @runner_projects = project.runner_projects.all + @runner_project = project.runner_projects.new end def create diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index e7e2ab4313..26ba12520c 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -8,7 +8,7 @@ class Projects::BuildsController < Projects::ApplicationController def index @scope = params[:scope] - @all_builds = project.ci_builds + @all_builds = project.builds @builds = @all_builds.order('created_at DESC') @builds = case @scope @@ -23,7 +23,7 @@ class Projects::BuildsController < Projects::ApplicationController end def cancel_all - @project.ci_builds.running_or_pending.each(&:cancel) + @project.builds.running_or_pending.each(&:cancel) redirect_to namespace_project_builds_path(project.namespace, project) end @@ -76,7 +76,7 @@ class Projects::BuildsController < Projects::ApplicationController private def build - @build ||= project.ci_builds.unscoped.find_by!(id: params[:id]) + @build ||= project.builds.unscoped.find_by!(id: params[:id]) end def artifacts_file diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb index 6986338735..e2785caa2f 100644 --- a/app/controllers/projects/runner_projects_controller.rb +++ b/app/controllers/projects/runner_projects_controller.rb @@ -18,7 +18,7 @@ class Projects::RunnerProjectsController < Projects::ApplicationController end def destroy - runner_project = project.ci_runner_projects.find(params[:id]) + runner_project = project.runner_projects.find(params[:id]) runner_project.destroy redirect_to runners_path(project) diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 863c5d131a..4993b2648a 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -5,9 +5,9 @@ class Projects::RunnersController < Projects::ApplicationController layout 'project_settings' def index - @runners = project.ci_runners.ordered + @runners = project.runners.ordered @specific_runners = current_user.ci_authorized_runners. - where.not(id: project.ci_runners). + where.not(id: project.runners). ordered.page(params[:page]).per(20) @shared_runners = Ci::Runner.shared.active @shared_runners_count = @shared_runners.count(:all) @@ -60,7 +60,7 @@ class Projects::RunnersController < Projects::ApplicationController protected def set_runner - @runner ||= project.ci_runners.find(params[:id]) + @runner ||= project.runners.find(params[:id]) end def runner_params diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb index 421e648a2d..30adfad1da 100644 --- a/app/controllers/projects/triggers_controller.rb +++ b/app/controllers/projects/triggers_controller.rb @@ -4,18 +4,18 @@ class Projects::TriggersController < Projects::ApplicationController layout 'project_settings' def index - @triggers = project.ci_triggers + @triggers = project.triggers @trigger = Ci::Trigger.new end def create - @trigger = project.ci_triggers.new + @trigger = project.triggers.new @trigger.save if @trigger.valid? redirect_to namespace_project_triggers_path(@project.namespace, @project) else - @triggers = project.ci_triggers.select(&:persisted?) + @triggers = project.triggers.select(&:persisted?) render :index end end @@ -29,6 +29,6 @@ class Projects::TriggersController < Projects::ApplicationController private def trigger - @trigger ||= project.ci_triggers.find(params[:id]) + @trigger ||= project.triggers.find(params[:id]) end end diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 1dab978f46..10efafea9d 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -17,6 +17,6 @@ class Projects::VariablesController < Projects::ApplicationController private def project_params - params.require(:project).permit({ ci_variables_attributes: [:id, :key, :value, :_destroy] }) + params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] }) end end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 7feeaa1730..02ae8177df 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -65,7 +65,7 @@ module CiStatusHelper end def no_runners_for_project?(project) - project.ci_runners.blank? && + project.runners.blank? && Ci::Runner.shared.blank? end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index fac1d1c4c2..401e08115d 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -287,7 +287,7 @@ module Ci end def project_variables - project.ci_variables.map do |variable| + project.variables.map do |variable| { key: variable.key, value: variable.value, public: false } end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index aa445db7eb..38b20cd7fa 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -52,7 +52,7 @@ module Ci def assign_to(project, current_user = nil) self.is_shared = false if shared? self.save - project.ci_runner_projects.create!(runner_id: self.id) + project.runner_projects.create!(runner_id: self.id) end def display_name diff --git a/app/models/project.rb b/app/models/project.rb index a11bc9c4bd..ba04470c64 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -128,15 +128,15 @@ class Project < ActiveRecord::Base has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" + has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id - has_many :ci_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id - has_many :ci_builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the ci_statuses - has_many :ci_runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id - has_many :ci_runners, through: :ci_runner_projects, source: :runner, class_name: 'Ci::Runner' - has_many :ci_variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id - has_many :ci_triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id + has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses + has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id + has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' + has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id + has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id - accepts_nested_attributes_for :ci_variables, allow_destroy: true + accepts_nested_attributes_for :variables, allow_destroy: true delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true @@ -822,7 +822,7 @@ class Project < ActiveRecord::Base end def any_runners?(&block) - if ci_runners.active.any?(&block) + if runners.active.any?(&block) return true end diff --git a/app/models/user.rb b/app/models/user.rb index da06b6f3ad..87d46a4943 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -134,7 +134,7 @@ class User < ActiveRecord::Base has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest" has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy has_one :abuse_report, dependent: :destroy - has_many :ci_builds, dependent: :nullify, class_name: 'Ci::Build' + has_many :builds, dependent: :nullify, class_name: 'Ci::Build' # diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb deleted file mode 100644 index 6401ce3619..0000000000 --- a/app/services/ci/create_commit_service.rb +++ /dev/null @@ -1,28 +0,0 @@ -module Ci - class CreateCommitService - def execute(project, user, params) - sha = params[:checkout_sha] || params[:after] - origin_ref = params[:ref] - - unless origin_ref && sha.present? - return false - end - - ref = origin_ref.gsub(/\Arefs\/(tags|heads)\//, '') - - # Skip branch removal - if sha == Ci::Git::BLANK_SHA - return false - end - - tag = origin_ref.start_with?('refs/tags/') - commit = project.ensure_ci_commit(sha) - unless commit.skip_ci? - commit.update_committed! - commit.create_builds(ref, tag, user) - end - - commit - end - end -end diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb new file mode 100644 index 0000000000..886bd88d20 --- /dev/null +++ b/app/services/create_commit_builds_service.rb @@ -0,0 +1,29 @@ +class CreateCommitBuildsService + def execute(project, user, params) + return false unless project.builds_enabled? + + sha = params[:checkout_sha] || params[:after] + origin_ref = params[:ref] + + unless origin_ref && sha.present? + return false + end + + ref = origin_ref.gsub(/\Arefs\/(tags|heads)\//, '') + + # Skip branch removal + if sha == Ci::Git::BLANK_SHA + return false + end + + tag = origin_ref.start_with?('refs/tags/') + commit = project.ensure_ci_commit(sha) + unless commit.skip_ci? + commit.update_committed! + commit.create_builds(ref, tag, user) + end + + commit + end +end + diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index f11690aa3f..d7ea30bc31 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -61,6 +61,7 @@ class GitPushService EventCreateService.new.push(project, user, @push_data) project.execute_hooks(@push_data.dup, :push_hooks) project.execute_services(@push_data.dup, :push_hooks) + CreateCommitBuildsService.new.execute(project, @user, @push_data) ProjectCacheWorker.perform_async(project.id) end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 1cc42b0b0a..4144c7111d 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -10,6 +10,7 @@ class GitTagPushService EventCreateService.new.push(project, user, @push_data) project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_services(@push_data.dup, :tag_push_hooks) + CreateCommitBuildsService.new.execute(project, @user, @push_data) ProjectCacheWorker.perform_async(project.id) true diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index aeb484a797..cd0cc2d177 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -82,7 +82,7 @@ = project.name_with_namespace %td .pull-right - = form_for [:admin, project.namespace, project, project.ci_runner_projects.new] do |f| + = form_for [:admin, project.namespace, project, project.runner_projects.new] do |f| = f.hidden_field :runner_id, value: @runner.id = f.submit 'Enable', class: 'btn btn-xs' = paginate @projects diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 2fcba7bd67..c0d6202863 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -44,7 +44,7 @@ = icon('cubes fw') %span Builds - %span.count.builds_counter= @project.ci_builds.running_or_pending.count(:all) + %span.count.builds_counter= @project.builds.running_or_pending.count(:all) - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do diff --git a/app/views/projects/graphs/ci/_overall.haml b/app/views/projects/graphs/ci/_overall.haml index 4105d68311..4b12e5f2da 100644 --- a/app/views/projects/graphs/ci/_overall.haml +++ b/app/views/projects/graphs/ci/_overall.haml @@ -2,17 +2,17 @@ %ul %li Total: - %strong= pluralize @project.ci_builds.count(:all), 'build' + %strong= pluralize @project.builds.count(:all), 'build' %li Successful: - %strong= pluralize @project.ci_builds.success.count(:all), 'build' + %strong= pluralize @project.builds.success.count(:all), 'build' %li Failed: - %strong= pluralize @project.ci_builds.failed.count(:all), 'build' + %strong= pluralize @project.builds.failed.count(:all), 'build' %li Success ratio: %strong - #{success_ratio(@project.ci_builds.success, @project.ci_builds.failed)}% + #{success_ratio(@project.builds.success, @project.builds.failed)}% %li Commits covered: %strong diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 1a61cb07b5..4d95afc28b 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -15,10 +15,10 @@ - if runner.belongs_to_one_project? = link_to 'Remove runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' - else - - runner_project = @project.ci_runner_projects.find_by(runner_id: runner) + - runner_project = @project.runner_projects.find_by(runner_id: runner) = link_to 'Disable for this project', namespace_project_runner_project_path(@project.namespace, @project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' - elsif runner.specific? - = form_for [@project.namespace, @project, @project.ci_runner_projects.new] do |f| + = form_for [@project.namespace, @project, @project.runner_projects.new] do |f| = f.hidden_field :runner_id, value: runner.id = f.submit 'Enable for this project', class: 'btn btn-sm' .pull-right diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml index e7bc866d6b..e80dffc1ce 100644 --- a/app/views/projects/variables/show.html.haml +++ b/app/views/projects/variables/show.html.haml @@ -19,7 +19,7 @@ - @project.errors.full_messages.each do |msg| %li= msg - = f.fields_for :ci_variables do |variable_form| + = f.fields_for :variables do |variable_form| .form-group = variable_form.label :key, 'Key', class: 'control-label' .col-sm-10 @@ -34,7 +34,7 @@ %hr %p .clearfix - = f.link_to_add "Add a variable", :ci_variables, class: 'btn btn-success pull-right' + = f.link_to_add "Add a variable", :variables, class: 'btn btn-success pull-right' .form-actions = f.submit 'Save changes', class: 'btn btn-save', return_to: request.original_url diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb index 1e738a7315..8704917b9e 100644 --- a/lib/ci/api/runners.rb +++ b/lib/ci/api/runners.rb @@ -38,7 +38,7 @@ module Ci ) elsif project = Project.find_by(runners_token: params[:token]) # Create a specific runner for project. - project.ci_runners.create( + project.runners.create( description: params[:description], tag_list: params[:tag_list] ) diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb index 912ccff5f9..d53bdcbd0f 100644 --- a/lib/ci/charts.rb +++ b/lib/ci/charts.rb @@ -16,10 +16,10 @@ module Ci def push(from, to, format) @labels << from.strftime(format) - @total << project.ci_builds. + @total << project.builds. where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from). count(:all) - @success << project.ci_builds. + @success << project.builds. where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from). success.count(:all) end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 61d81cb0c8..f0031a0a24 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -37,7 +37,7 @@ describe "Builds" do context "All builds" do before do - @project.ci_builds.running_or_pending.each(&:success) + @project.builds.running_or_pending.each(&:success) visit namespace_project_builds_path(@project.namespace, @project, scope: :all) end diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index a4562f2fae..d97831aae1 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -21,9 +21,9 @@ describe "Runners" do @specific_runner = FactoryGirl.create :ci_specific_runner @specific_runner2 = FactoryGirl.create :ci_specific_runner @specific_runner3 = FactoryGirl.create :ci_specific_runner - @project.ci_runners << @specific_runner - @project2.ci_runners << @specific_runner2 - @project3.ci_runners << @specific_runner3 + @project.runners << @specific_runner + @project2.runners << @specific_runner2 + @project3.runners << @specific_runner3 visit runners_path(@project) end @@ -48,7 +48,7 @@ describe "Runners" do end it "disables specific runner for project" do - @project2.ci_runners << @specific_runner + @project2.runners << @specific_runner visit runners_path(@project) within ".activated-specific-runners" do @@ -85,7 +85,7 @@ describe "Runners" do @project = FactoryGirl.create :empty_project @project.team << [user, :master] @specific_runner = FactoryGirl.create :ci_specific_runner - @project.ci_runners << @specific_runner + @project.runners << @specific_runner end it "shows runner information" do diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index b0705a45ae..3cbc8253ad 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -13,16 +13,16 @@ describe 'Triggers' do context 'create a trigger' do before do click_on 'Add Trigger' - expect(@project.ci_triggers.count).to eq(1) + expect(@project.triggers.count).to eq(1) end it 'contains trigger token' do - expect(page).to have_content(@project.ci_triggers.first.token) + expect(page).to have_content(@project.triggers.first.token) end it 'revokes the trigger' do click_on 'Revoke' - expect(@project.ci_triggers.count).to eq(0) + expect(@project.triggers.count).to eq(0) end end end diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb index a6e68eeefb..afea1840cd 100644 --- a/spec/features/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -18,7 +18,7 @@ describe "Variables" do click_on "Save changes" expect(page).to have_content("Variables were successfully updated.") - expect(@project.ci_variables.count).to eq(1) + expect(@project.variables.count).to eq(1) end end end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 70a63c05b2..91af694fc0 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -232,7 +232,7 @@ describe Ci::Build, models: true do end before do - build.project.ci_variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') + build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') end it { is_expected.to eq(predefined_variables + yaml_variables + secure_variables) } @@ -264,7 +264,7 @@ describe Ci::Build, models: true do describe :can_be_served? do let(:runner) { FactoryGirl.create :ci_specific_runner } - before { build.project.ci_runners << runner } + before { build.project.runners << runner } context 'runner without tags' do it 'can handle builds without tags' do @@ -307,7 +307,7 @@ describe Ci::Build, models: true do let(:runner) { FactoryGirl.create :ci_specific_runner } before do - build.project.ci_runners << runner + build.project.runners << runner runner.update_attributes(contacted_at: 1.second.ago) end @@ -344,7 +344,7 @@ describe Ci::Build, models: true do let(:runner) { FactoryGirl.create :ci_specific_runner, contacted_at: 1.second.ago } before do - build.project.ci_runners << runner + build.project.runners << runner runner.save end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 33388f9782..232760dfeb 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -118,8 +118,8 @@ describe Ci::Runner, models: true do runner = FactoryGirl.create(:ci_specific_runner) project = FactoryGirl.create(:empty_project) project1 = FactoryGirl.create(:empty_project) - project.ci_runners << runner - project1.ci_runners << runner + project.runners << runner + project1.runners << runner expect(runner.belongs_to_one_project?).to be_falsey end @@ -127,7 +127,7 @@ describe Ci::Runner, models: true do it "returns true" do runner = FactoryGirl.create(:ci_specific_runner) project = FactoryGirl.create(:empty_project) - project.ci_runners << runner + project.runners << runner expect(runner.belongs_to_one_project?).to be_truthy end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 9c9266455c..b962f66ba2 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -54,13 +54,13 @@ describe Project, models: true do it { is_expected.to have_one(:slack_service).dependent(:destroy) } it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) } - it { is_expected.to have_many(:ci_commits) } it { is_expected.to have_many(:commit_statuses) } - it { is_expected.to have_many(:ci_builds) } - it { is_expected.to have_many(:ci_runner_projects) } - it { is_expected.to have_many(:ci_runners) } - it { is_expected.to have_many(:ci_variables) } - it { is_expected.to have_many(:ci_triggers) } + it { is_expected.to have_many(:ci_commits) } + it { is_expected.to have_many(:builds) } + it { is_expected.to have_many(:runner_projects) } + it { is_expected.to have_many(:runners) } + it { is_expected.to have_many(:variables) } + it { is_expected.to have_many(:triggers) } end describe 'modules' do @@ -519,7 +519,7 @@ describe Project, models: true do end it 'there is a specific runner' do - project.ci_runners << specific_runner + project.runners << specific_runner expect(project.any_runners?).to be_truthy end @@ -529,7 +529,7 @@ describe Project, models: true do end it 'checks the presence of specific runner' do - project.ci_runners << specific_runner + project.runners << specific_runner expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 0aa9981ea8..550d74d158 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -68,7 +68,7 @@ describe Ci::API::API do it "returns variables" do commit = FactoryGirl.create(:ci_commit, project: project) commit.create_builds('master', false, nil) - project.ci_variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") + project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -87,7 +87,7 @@ describe Ci::API::API do trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger) commit.create_builds('master', false, nil, trigger_request) - project.ci_variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") + project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index ef2f759fda..944d910d05 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -34,7 +34,7 @@ describe Ci::API::API do before { post ci_api("/runners/register"), token: project.token } it { expect(response.status).to eq(201) } - it { expect(project.ci_runners.size).to eq(1) } + it { expect(project.runners.size).to eq(1) } end it "should return 403 error if token is invalid" do diff --git a/spec/services/ci/create_commit_service_spec.rb b/spec/services/ci/create_commit_service_spec.rb deleted file mode 100644 index 7e2dcc503e..0000000000 --- a/spec/services/ci/create_commit_service_spec.rb +++ /dev/null @@ -1,172 +0,0 @@ -require 'spec_helper' - -module Ci - describe CreateCommitService, services: true do - let(:service) { CreateCommitService.new } - let(:project) { FactoryGirl.create(:empty_project) } - let(:user) { nil } - - before do - stub_ci_commit_to_return_yaml_file - end - - describe :execute do - context 'valid params' do - let(:commit) do - service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: [ { message: "Message" } ] - ) - end - - it { expect(commit).to be_kind_of(Commit) } - it { expect(commit).to be_valid } - it { expect(commit).to be_persisted } - it { expect(commit).to eq(project.ci_commits.last) } - it { expect(commit.builds.first).to be_kind_of(Build) } - end - - context "skip tag if there is no build for it" do - it "creates commit if there is appropriate job" do - result = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: [ { message: "Message" } ] - ) - expect(result).to be_persisted - end - - it "creates commit if there is no appropriate job but deploy job has right ref setting" do - config = YAML.dump({ deploy: { deploy: "ls", only: ["0_1"] } }) - stub_ci_commit_yaml_file(config) - - result = service.execute(project, user, - ref: 'refs/heads/0_1', - before: '00000000', - after: '31das312', - commits: [ { message: "Message" } ] - ) - expect(result).to be_persisted - end - end - - it 'skips commits without .gitlab-ci.yml' do - stub_ci_commit_yaml_file(nil) - result = service.execute(project, user, - ref: 'refs/heads/0_1', - before: '00000000', - after: '31das312', - commits: [ { message: 'Message' } ] - ) - expect(result).to be_persisted - expect(result.builds.any?).to be_falsey - expect(result.status).to eq('skipped') - expect(result.yaml_errors).to be_nil - end - - it 'skips commits if yaml is invalid' do - message = 'message' - allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message } - stub_ci_commit_yaml_file('invalid: file: file') - commits = [{ message: message }] - commit = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(commit.builds.any?).to be false - expect(commit.status).to eq('failed') - expect(commit.yaml_errors).to_not be_nil - end - - describe :ci_skip? do - let(:message) { "some message[ci skip]" } - - before do - allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message } - end - - it "skips builds creation if there is [ci skip] tag in commit message" do - commits = [{ message: message }] - commit = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(commit.builds.any?).to be false - expect(commit.status).to eq("skipped") - end - - it "does not skips builds creation if there is no [ci skip] tag in commit message" do - allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { "some message" } - - commits = [{ message: "some message" }] - commit = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(commit.builds.first.name).to eq("staging") - end - - it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do - stub_ci_commit_yaml_file('invalid: file: fiile') - commits = [{ message: message }] - commit = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(commit.builds.any?).to be false - expect(commit.status).to eq("skipped") - expect(commit.yaml_errors).to be_nil - end - end - - it "skips build creation if there are already builds" do - allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { gitlab_ci_yaml } - - commits = [{ message: "message" }] - commit = service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(commit.builds.count(:all)).to eq(2) - - commit = service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(commit.builds.count(:all)).to eq(2) - end - - it "creates commit with failed status if yaml is invalid" do - stub_ci_commit_yaml_file('invalid: file') - - commits = [{ message: "some message" }] - - commit = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(commit.status).to eq("failed") - expect(commit.builds.any?).to be false - end - end - end -end diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb new file mode 100644 index 0000000000..70e28f6c52 --- /dev/null +++ b/spec/services/create_commit_builds_service_spec.rb @@ -0,0 +1,171 @@ +require 'spec_helper' + +describe CreateCommitBuildsService, services: true do + let(:service) { CreateCommitBuildsService.new } + let(:project) { FactoryGirl.create(:empty_project) } + let(:user) { nil } + + before do + stub_ci_commit_to_return_yaml_file + end + + describe :execute do + context 'valid params' do + let(:commit) do + service.execute(project, user, + ref: 'refs/heads/master', + before: '00000000', + after: '31das312', + commits: [{ message: "Message" }] + ) + end + + it { expect(commit).to be_kind_of(Commit) } + it { expect(commit).to be_valid } + it { expect(commit).to be_persisted } + it { expect(commit).to eq(project.ci_commits.last) } + it { expect(commit.builds.first).to be_kind_of(Build) } + end + + context "skip tag if there is no build for it" do + it "creates commit if there is appropriate job" do + result = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: [{ message: "Message" }] + ) + expect(result).to be_persisted + end + + it "creates commit if there is no appropriate job but deploy job has right ref setting" do + config = YAML.dump({ deploy: { deploy: "ls", only: ["0_1"] } }) + stub_ci_commit_yaml_file(config) + + result = service.execute(project, user, + ref: 'refs/heads/0_1', + before: '00000000', + after: '31das312', + commits: [{ message: "Message" }] + ) + expect(result).to be_persisted + end + end + + it 'skips commits without .gitlab-ci.yml' do + stub_ci_commit_yaml_file(nil) + result = service.execute(project, user, + ref: 'refs/heads/0_1', + before: '00000000', + after: '31das312', + commits: [{ message: 'Message' }] + ) + expect(result).to be_persisted + expect(result.builds.any?).to be_falsey + expect(result.status).to eq('skipped') + expect(result.yaml_errors).to be_nil + end + + it 'skips commits if yaml is invalid' do + message = 'message' + allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message } + stub_ci_commit_yaml_file('invalid: file: file') + commits = [{ message: message }] + commit = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(commit.builds.any?).to be false + expect(commit.status).to eq('failed') + expect(commit.yaml_errors).to_not be_nil + end + + describe :ci_skip? do + let(:message) { "some message[ci skip]" } + + before do + allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message } + end + + it "skips builds creation if there is [ci skip] tag in commit message" do + commits = [{ message: message }] + commit = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(commit.builds.any?).to be false + expect(commit.status).to eq("skipped") + end + + it "does not skips builds creation if there is no [ci skip] tag in commit message" do + allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { "some message" } + + commits = [{ message: "some message" }] + commit = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + + expect(commit.builds.first.name).to eq("staging") + end + + it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do + stub_ci_commit_yaml_file('invalid: file: fiile') + commits = [{ message: message }] + commit = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(commit.builds.any?).to be false + expect(commit.status).to eq("skipped") + expect(commit.yaml_errors).to be_nil + end + end + + it "skips build creation if there are already builds" do + allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { gitlab_ci_yaml } + + commits = [{ message: "message" }] + commit = service.execute(project, user, + ref: 'refs/heads/master', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(commit.builds.count(:all)).to eq(2) + + commit = service.execute(project, user, + ref: 'refs/heads/master', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(commit.builds.count(:all)).to eq(2) + end + + it "creates commit with failed status if yaml is invalid" do + stub_ci_commit_yaml_file('invalid: file') + + commits = [{ message: "some message" }] + + commit = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + + expect(commit.status).to eq("failed") + expect(commit.builds.any?).to be false + end + end +end + From 3d9ce37a48615032c786913acdde1eedba351361 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 10 Dec 2015 18:04:40 +0100 Subject: [PATCH 564/908] Reimplement Trigger API --- app/helpers/triggers_helper.rb | 4 +- app/views/projects/triggers/index.html.haml | 8 ++- lib/api/api.rb | 1 + lib/api/entities.rb | 4 ++ lib/api/triggers.rb | 48 +++++++++++++ spec/requests/api/triggers_spec.rb | 80 +++++++++++++++++++++ 6 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 lib/api/triggers.rb create mode 100644 spec/requests/api/triggers_spec.rb diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb index 2a3a7e80fc..8cad994d10 100644 --- a/app/helpers/triggers_helper.rb +++ b/app/helpers/triggers_helper.rb @@ -1,5 +1,5 @@ module TriggersHelper - def ci_build_trigger_url(project_id, ref_name) - "#{Settings.gitlab_ci.url}/ci/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger" + def builds_trigger_url(project_id) + "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds" end end diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index 147cda51d5..fb3794764c 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -36,7 +36,8 @@ :plain curl -X POST \ -F token=TOKEN \ - #{ci_build_trigger_url(@project.id, 'REF_NAME')} + -F ref=REF_NAME \ + #{builds_trigger_url(@project.id)} %h3 Use .gitlab-ci.yml @@ -51,7 +52,7 @@ trigger: type: deploy script: - - "curl -X POST -F token=TOKEN #{ci_build_trigger_url(@project.id, 'REF_NAME')}" + - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}" %h3 Pass build variables @@ -65,5 +66,6 @@ :plain curl -X POST \ -F token=TOKEN \ + -F "ref=REF_NAME" \ -F "variables[RUN_NIGHTLY_BUILD]=true" \ - #{ci_build_trigger_url(@project.id, 'REF_NAME')} + #{builds_trigger_url(@project.id, 'TOKEN')} diff --git a/lib/api/api.rb b/lib/api/api.rb index fe1bf8a481..7834262d61 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -53,5 +53,6 @@ module API mount Settings mount Keys mount Tags + mount Triggers end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 381babe291..b1cd80bdf6 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -361,5 +361,9 @@ module API end end end + + class TriggerRequest < Grape::Entity + expose :id, :variables + end end end diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb new file mode 100644 index 0000000000..b713c00e82 --- /dev/null +++ b/lib/api/triggers.rb @@ -0,0 +1,48 @@ +module API + # Triggers API + class Triggers < Grape::API + resource :projects do + # Trigger a GitLab project build + # + # Parameters: + # id (required) - The ID of a CI project + # ref (required) - The name of project's branch or tag + # token (required) - The uniq token of trigger + # variables (optional) - The list of variables to be injected into build + # Example Request: + # POST /projects/:id/trigger/builds + post ":id/trigger/builds" do + required_attributes! [:ref, :token] + + project = Project.find_with_namespace(id) || Project.find_by(id: params[:id]) + trigger = Ci::Trigger.find_by_token(params[:token].to_s) + not_found! unless project && trigger + unauthorized! unless trigger.project == project + + # validate variables + variables = params[:variables] + if variables + unless variables.is_a?(Hash) + render_api_error!('variables needs to be a hash', 400) + end + + unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) } + render_api_error!('variables needs to be a map of key-valued strings', 400) + end + + # convert variables from Mash to Hash + variables = variables.to_h + end + + # create request and trigger builds + trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables) + if trigger_request + present trigger_request, with: Entities::TriggerRequest + else + errors = 'No builds created' + render_api_error!(errors, 400) + end + end + end + end +end diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb new file mode 100644 index 0000000000..899458e619 --- /dev/null +++ b/spec/requests/api/triggers_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe API::API do + include ApiHelpers + + describe 'POST /projects/:project_id/trigger' do + let!(:trigger_token) { 'secure token' } + let!(:project) { FactoryGirl.create(:project) } + let!(:project2) { FactoryGirl.create(:empty_project) } + let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) } + let(:options) do + { + token: trigger_token + } + end + + before do + stub_ci_commit_to_return_yaml_file + end + + context 'Handles errors' do + it 'should return bad request if token is missing' do + post api("/projects/#{project.id}/trigger/builds"), ref: 'master' + expect(response.status).to eq(400) + end + + it 'should return not found if project is not found' do + post api('/projects/0/refs/master/trigger/builds'), options.merge(ref: 'master') + expect(response.status).to eq(404) + end + + it 'should return unauthorized if token is for different project' do + post api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master') + expect(response.status).to eq(401) + end + end + + context 'Have a commit' do + let(:commit) { project.ci_commits.last } + + it 'should create builds' do + post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master') + expect(response.status).to eq(201) + commit.builds.reload + expect(commit.builds.size).to eq(2) + end + + it 'should return bad request with no builds created if there\'s no commit for that ref' do + post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch') + expect(response.status).to eq(400) + expect(json_response['message']).to eq('No builds created') + end + + context 'Validates variables' do + let(:variables) do + { 'TRIGGER_KEY' => 'TRIGGER_VALUE' } + end + + it 'should validate variables to be a hash' do + post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', refs: 'master') + expect(response.status).to eq(400) + expect(json_response['message']).to eq('variables needs to be a hash') + end + + it 'should validate variables needs to be a map of key-valued strings' do + post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, refs: 'master') + expect(response.status).to eq(400) + expect(json_response['message']).to eq('variables needs to be a map of key-valued strings') + end + + it 'create trigger request with variables' do + post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, refs: 'master') + expect(response.status).to eq(201) + commit.builds.reload + expect(commit.builds.first.trigger_request.variables).to eq(variables) + end + end + end + end +end From 91cdb08d5fa83a86f5f18088bf148e832732bed9 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 10 Dec 2015 18:08:59 +0100 Subject: [PATCH 565/908] Rename columns and rename migrations --- ..._to_project.rb => 20151210125928_add_ci_to_project.rb} | 0 ...id_to_ci.rb => 20151210125929_add_project_id_to_ci.rb} | 0 ...project.rb => 20151210125930_migrate_ci_to_project.rb} | 0 ...tables.rb => 20151210125931_add_index_to_ci_tables.rb} | 0 ...ables.rb => 20151210125932_drop_null_for_ci_tables.rb} | 0 db/schema.rb | 8 +++++--- 6 files changed, 5 insertions(+), 3 deletions(-) rename db/migrate/{20151203162135_add_ci_to_project.rb => 20151210125928_add_ci_to_project.rb} (100%) rename db/migrate/{20151204110124_add_project_id_to_ci.rb => 20151210125929_add_project_id_to_ci.rb} (100%) rename db/migrate/{20151204110613_migrate_ci_to_project.rb => 20151210125930_migrate_ci_to_project.rb} (100%) rename db/migrate/{20151204110832_add_index_to_ci_tables.rb => 20151210125931_add_index_to_ci_tables.rb} (100%) rename db/migrate/{20151204123933_drop_null_for_ci_tables.rb => 20151210125932_drop_null_for_ci_tables.rb} (100%) diff --git a/db/migrate/20151203162135_add_ci_to_project.rb b/db/migrate/20151210125928_add_ci_to_project.rb similarity index 100% rename from db/migrate/20151203162135_add_ci_to_project.rb rename to db/migrate/20151210125928_add_ci_to_project.rb diff --git a/db/migrate/20151204110124_add_project_id_to_ci.rb b/db/migrate/20151210125929_add_project_id_to_ci.rb similarity index 100% rename from db/migrate/20151204110124_add_project_id_to_ci.rb rename to db/migrate/20151210125929_add_project_id_to_ci.rb diff --git a/db/migrate/20151204110613_migrate_ci_to_project.rb b/db/migrate/20151210125930_migrate_ci_to_project.rb similarity index 100% rename from db/migrate/20151204110613_migrate_ci_to_project.rb rename to db/migrate/20151210125930_migrate_ci_to_project.rb diff --git a/db/migrate/20151204110832_add_index_to_ci_tables.rb b/db/migrate/20151210125931_add_index_to_ci_tables.rb similarity index 100% rename from db/migrate/20151204110832_add_index_to_ci_tables.rb rename to db/migrate/20151210125931_add_index_to_ci_tables.rb diff --git a/db/migrate/20151204123933_drop_null_for_ci_tables.rb b/db/migrate/20151210125932_drop_null_for_ci_tables.rb similarity index 100% rename from db/migrate/20151204123933_drop_null_for_ci_tables.rb rename to db/migrate/20151210125932_drop_null_for_ci_tables.rb diff --git a/db/schema.rb b/db/schema.rb index fd9004faa2..d17930b84b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151204123933) do +ActiveRecord::Schema.define(version: 20151210125932) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -655,9 +655,10 @@ ActiveRecord::Schema.define(version: 20151204123933) do t.string "import_source" t.integer "commit_count", default: 0 t.text "import_error" + t.integer "ci_id" t.boolean "builds_enabled", default: true, null: false t.boolean "shared_runners_enabled", default: true, null: false - t.string "token" + t.string "runners_token" t.string "build_coverage_regex" t.boolean "build_allow_git_fetch", default: true, null: false t.integer "build_timeout", default: 3600, null: false @@ -665,13 +666,14 @@ ActiveRecord::Schema.define(version: 20151204123933) do add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree add_index "projects", ["builds_enabled"], name: "index_projects_on_builds_enabled", using: :btree + add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree add_index "projects", ["path"], name: "index_projects_on_path", using: :btree + add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree - add_index "projects", ["token"], name: "index_projects_on_token", using: :btree add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree create_table "protected_branches", force: :cascade do |t| From 3578153d3e838da92587fed88a608d9a5458c37c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 10 Dec 2015 18:12:19 +0100 Subject: [PATCH 566/908] Fix triggers tests --- lib/api/triggers.rb | 2 +- spec/requests/api/triggers_spec.rb | 8 ++++---- spec/requests/ci/api/triggers_spec.rb | 24 +++++++++--------------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index b713c00e82..2781f1cf19 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -14,7 +14,7 @@ module API post ":id/trigger/builds" do required_attributes! [:ref, :token] - project = Project.find_with_namespace(id) || Project.find_by(id: params[:id]) + project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id]) trigger = Ci::Trigger.find_by_token(params[:token].to_s) not_found! unless project && trigger unauthorized! unless trigger.project == project diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 899458e619..314bd7ddc5 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -25,7 +25,7 @@ describe API::API do end it 'should return not found if project is not found' do - post api('/projects/0/refs/master/trigger/builds'), options.merge(ref: 'master') + post api('/projects/0/trigger/builds'), options.merge(ref: 'master') expect(response.status).to eq(404) end @@ -57,19 +57,19 @@ describe API::API do end it 'should validate variables to be a hash' do - post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', refs: 'master') + post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master') expect(response.status).to eq(400) expect(json_response['message']).to eq('variables needs to be a hash') end it 'should validate variables needs to be a map of key-valued strings' do - post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, refs: 'master') + post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master') expect(response.status).to eq(400) expect(json_response['message']).to eq('variables needs to be a map of key-valued strings') end it 'create trigger request with variables' do - post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, refs: 'master') + post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master') expect(response.status).to eq(201) commit.builds.reload expect(commit.builds.first.trigger_request.variables).to eq(variables) diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb index cd33a96e1a..0ef03f9371 100644 --- a/spec/requests/ci/api/triggers_spec.rb +++ b/spec/requests/ci/api/triggers_spec.rb @@ -5,16 +5,14 @@ describe Ci::API::API do describe 'POST /projects/:project_id/refs/:ref/trigger' do let!(:trigger_token) { 'secure token' } - let!(:project) { FactoryGirl.create(:project) } - let!(:project2) { FactoryGirl.create(:empty_project) } + let!(:project) { FactoryGirl.create(:project, ci_id: 10) } + let!(:project2) { FactoryGirl.create(:empty_project, ci_id: 11) } let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) } let(:options) do { token: trigger_token } end - let(:project_ci_id) { create_ci_id(project) } - let(:project2_ci_id) { create_ci_id(project2) } before do stub_ci_commit_to_return_yaml_file @@ -22,7 +20,7 @@ describe Ci::API::API do context 'Handles errors' do it 'should return bad request if token is missing' do - post ci_api("/projects/#{project_ci_id}/refs/master/trigger") + post ci_api("/projects/#{project.ci_id}/refs/master/trigger") expect(response.status).to eq(400) end @@ -32,7 +30,7 @@ describe Ci::API::API do end it 'should return unauthorized if token is for different project' do - post ci_api("/projects/#{project2_ci_id}/refs/master/trigger"), options + post ci_api("/projects/#{project2.ci_id}/refs/master/trigger"), options expect(response.status).to eq(401) end end @@ -41,14 +39,14 @@ describe Ci::API::API do let(:commit) { project.ci_commits.last } it 'should create builds' do - post ci_api("/projects/#{project_ci_id}/refs/master/trigger"), options + post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options expect(response.status).to eq(201) commit.builds.reload expect(commit.builds.size).to eq(2) end it 'should return bad request with no builds created if there\'s no commit for that ref' do - post ci_api("/projects/#{project_ci_id}/refs/other-branch/trigger"), options + post ci_api("/projects/#{project.ci_id}/refs/other-branch/trigger"), options expect(response.status).to eq(400) expect(json_response['message']).to eq('No builds created') end @@ -59,19 +57,19 @@ describe Ci::API::API do end it 'should validate variables to be a hash' do - post ci_api("/projects/#{project_ci_id}/refs/master/trigger"), options.merge(variables: 'value') + post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: 'value') expect(response.status).to eq(400) expect(json_response['message']).to eq('variables needs to be a hash') end it 'should validate variables needs to be a map of key-valued strings' do - post ci_api("/projects/#{project_ci_id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) }) + post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) }) expect(response.status).to eq(400) expect(json_response['message']).to eq('variables needs to be a map of key-valued strings') end it 'create trigger request with variables' do - post ci_api("/projects/#{project_ci_id}/refs/master/trigger"), options.merge(variables: variables) + post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: variables) expect(response.status).to eq(201) commit.builds.reload expect(commit.builds.first.trigger_request.variables).to eq(variables) @@ -79,8 +77,4 @@ describe Ci::API::API do end end end - - def create_ci_id(project) - ActiveRecord::Base.connection.insert("INSERT INTO ci_projects (gitlab_id) VALUES(#{project.id})") - end end From 1e2a4895c803f4881ab63c1816141462fbfa6d2b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 10 Dec 2015 18:47:22 +0100 Subject: [PATCH 567/908] Finishing touches --- app/helpers/yaml_helper.rb | 11 ----------- app/models/commit_status.rb | 4 +--- app/services/ci/create_builds_service.rb | 2 +- app/views/ci/shared/_guide.html.haml | 6 ++---- app/views/projects/commit/_builds.html.haml | 1 + 5 files changed, 5 insertions(+), 19 deletions(-) delete mode 100644 app/helpers/yaml_helper.rb diff --git a/app/helpers/yaml_helper.rb b/app/helpers/yaml_helper.rb deleted file mode 100644 index 17990e1f47..0000000000 --- a/app/helpers/yaml_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -module YamlHelper - def yaml_web_editor_link(project) - commits = project.ci_commits - - if commits.any? && commits.last.ci_yaml_file - "#{project.gitlab_url}/edit/master/.gitlab-ci.yml" - else - "#{project.gitlab_url}/new/master" - end - end -end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 579b638706..77c3f776aa 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -50,7 +50,6 @@ class CommitStatus < ActiveRecord::Base scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) } scope :ordered, -> { order(:ref, :stage_idx, :name) } scope :for_ref, ->(ref) { where(ref: ref) } - scope :has_coverage?, -> { where.not(coverage: nil).any? } state_machine :status, initial: :pending do event :run do @@ -88,8 +87,7 @@ class CommitStatus < ActiveRecord::Base state :canceled, value: 'canceled' end - delegate :sha, :short_sha, :project, - to: :commit, prefix: false + delegate :sha, :short_sha, to: :commit, prefix: false # TODO: this should be removed with all references def before_sha diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index ba7c4632f4..ad901f2da5 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -30,7 +30,7 @@ module Ci tag: tag, trigger_request: trigger_request, user: user, - gl_project_id: commit.gl_project_id) + project: commit.project) build = commit.builds.create!(build_attrs) build.execute_hooks diff --git a/app/views/ci/shared/_guide.html.haml b/app/views/ci/shared/_guide.html.haml index ec76885866..09e7e65352 100644 --- a/app/views/ci/shared/_guide.html.haml +++ b/app/views/ci/shared/_guide.html.haml @@ -6,10 +6,8 @@ Add at least one runner to the project. Go to #{link_to 'Runners page', runners_path(@project), target: :blank} for instructions. %li - Put the .gitlab-ci.yml in the root of your repository. Examples can be found in #{link_to "Configuring project (.gitlab-ci.yml)", "http://doc.gitlab.com/ci/yaml/README.html", target: :blank}. + Put the .gitlab-ci.yml in the root of your repository. Examples can be found in + #{link_to "Configuring project (.gitlab-ci.yml)", "http://doc.gitlab.com/ci/yaml/README.html", target: :blank}. You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} - %li - Visit #{link_to 'GitLab project settings', @project.gitlab_url + "/services/gitlab_ci/edit", target: :blank} - and press the "Test settings" button. %li Return to this page and refresh it, it should show a new build. diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index 952f33184f..c87b06d2f0 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -22,6 +22,7 @@ %ul - @ci_commit.yaml_errors.split(",").each do |error| %li= error + You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} - if @ci_commit.project.builds_enabled? && !@ci_commit.ci_yaml_file .bs-callout.bs-callout-warning From 2b7a75ceaf96830e482fc5c59460fe2dd6ff0a28 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 11 Dec 2015 13:25:42 +0100 Subject: [PATCH 568/908] Update badge --- app/helpers/ci_badge_helper.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/helpers/ci_badge_helper.rb b/app/helpers/ci_badge_helper.rb index a81edbcb41..27386133e3 100644 --- a/app/helpers/ci_badge_helper.rb +++ b/app/helpers/ci_badge_helper.rb @@ -1,11 +1,13 @@ module CiBadgeHelper def markdown_badge_code(project, ref) url = status_ci_project_url(project, ref: ref, format: 'png') - "[![build status](#{url})](#{ci_project_url(project, ref: ref)})" + link = namespace_project_commits_path(project.namespace, project, ref) + "[![build status](#{url})](#{link})" end def html_badge_code(project, ref) url = status_ci_project_url(project, ref: ref, format: 'png') - "" + link = namespace_project_commits_path(project.namespace, project, ref) + "" end end From 73b04bebad23ce6750d7747c821a93cfeb73a9d2 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 11 Dec 2015 13:34:11 +0100 Subject: [PATCH 569/908] Fix errors --- app/controllers/projects/merge_requests_controller.rb | 3 --- app/models/commit_status.rb | 2 +- app/services/create_commit_builds_service.rb | 1 - app/views/projects/commit/_builds.html.haml | 2 +- .../features/merge_requests/merge_when_build_succeeds_spec.rb | 4 ++-- spec/services/create_commit_builds_service_spec.rb | 1 - .../merge_requests/merge_when_build_succeeds_service_spec.rb | 2 +- 7 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 530f3d3dcb..3ae4c5b099 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -81,8 +81,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def builds - @ci_project = @merge_request.source_project.gitlab_ci_project - respond_to do |format| format.html { render 'show' } format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_builds') } } @@ -106,7 +104,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController @first_commit = @merge_request.first_commit @diffs = @merge_request.compare_diffs - @ci_project = @source_project.gitlab_ci_project @ci_commit = @merge_request.ci_commit @statuses = @ci_commit.statuses if @ci_commit diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 77c3f776aa..21c5c87bc3 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -77,7 +77,7 @@ class CommitStatus < ActiveRecord::Base end after_transition [:pending, :running] => :success do |build, transition| - MergeRequests::MergeWhenBuildSucceedsService.new(build.commit.gl_project, nil).trigger(build) + MergeRequests::MergeWhenBuildSucceedsService.new(build.commit.project, nil).trigger(build) end state :pending, value: 'pending' diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb index 886bd88d20..392fe21eec 100644 --- a/app/services/create_commit_builds_service.rb +++ b/app/services/create_commit_builds_service.rb @@ -26,4 +26,3 @@ class CreateCommitBuildsService commit end end - diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index c87b06d2f0..e65a5a9c2d 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -1,6 +1,6 @@ .gray-content-block.middle-block .pull-right - - if @ci_project && can?(current_user, :manage_builds, @ci_commit.project) + - if can?(current_user, :manage_builds, @ci_commit.project) - if @ci_commit.builds.latest.failed.any?(&:retryable?) = link_to "Retry failed", retry_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index a674124aab..28a46a0725 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -12,7 +12,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do end context "Active build for Merge Request" do - let!(:ci_commit) { create(:ci_commit, gl_project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } let!(:ci_build) { create(:ci_build, commit: ci_commit) } before do @@ -47,7 +47,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do merge_user: user, title: "MepMep", merge_when_build_succeeds: true) end - let!(:ci_commit) { create(:ci_commit, gl_project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } let!(:ci_build) { create(:ci_build, commit: ci_commit) } before do diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb index 70e28f6c52..ae56f0b3a9 100644 --- a/spec/services/create_commit_builds_service_spec.rb +++ b/spec/services/create_commit_builds_service_spec.rb @@ -168,4 +168,3 @@ describe CreateCommitBuildsService, services: true do end end end - diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index 188fda6211..449cecaa78 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -11,7 +11,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do end let(:project) { create(:project) } - let(:ci_commit) { create(:ci_commit_with_one_job, ref: mr_merge_if_green_enabled.source_branch, gl_project: project) } + let(:ci_commit) { create(:ci_commit_with_one_job, ref: mr_merge_if_green_enabled.source_branch, project: project) } let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, commit_message: 'Awesome message') } describe "#execute" do From 513d551c8f7078e263d31ef2c30a1f72cbab3fae Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 11 Dec 2015 13:39:43 +0100 Subject: [PATCH 570/908] Fix after column rename --- app/controllers/projects_controller.rb | 2 +- app/models/ci/build.rb | 4 ++-- app/views/projects/edit.html.haml | 4 ++-- lib/ci/api/entities.rb | 9 --------- spec/lib/gitlab/backend/grack_auth_spec.rb | 4 ++-- spec/models/build_spec.rb | 2 +- spec/models/project_spec.rb | 8 ++++---- spec/requests/ci/api/builds_spec.rb | 16 ++++++++-------- spec/requests/ci/api/runners_spec.rb | 2 +- 9 files changed, 21 insertions(+), 30 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index e9917109f3..bf5e25ff89 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -210,7 +210,7 @@ class ProjectsController < ApplicationController def project_params params.require(:project).permit( - :name, :path, :description, :issues_tracker, :tag_list, :token, + :name, :path, :description, :issues_tracker, :tag_list, :runners_token, :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 401e08115d..6d9cdb9529 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -194,7 +194,7 @@ module Ci def trace trace = raw_trace if project && trace.present? - trace.gsub(project.token, 'xxxxxx') + trace.gsub(project.runners_token, 'xxxxxx') else trace end @@ -221,7 +221,7 @@ module Ci end def token - project.token + project.runners_token end def valid_token? token diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index a7ab9b44e7..650629ef1b 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -163,9 +163,9 @@ %legend Advanced settings .form-group - = f.label :token, "CI token", class: 'control-label' + = f.label :runners_token, "CI token", class: 'control-label' .col-sm-10 - = f.text_field :token, class: "form-control", placeholder: 'xEeFCaDAB89' + = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89' %p.help-block The secure token used to checkout project. .form-actions diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb index 750f421872..e4ac0545ea 100644 --- a/lib/ci/api/entities.rb +++ b/lib/ci/api/entities.rb @@ -37,15 +37,6 @@ module Ci expose :id, :token end - class Project < Grape::Entity - expose :id, :name, :token, :default_ref, :gitlab_url, :path, - :always_build, :polling_interval, :public, :ssh_url_to_repo, :gitlab_id - - expose :timeout do |model| - model.timeout - end - end - class RunnerProject < Grape::Entity expose :id, :project_id, :runner_id end diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb index 2dd27464de..cd26dca099 100644 --- a/spec/lib/gitlab/backend/grack_auth_spec.rb +++ b/spec/lib/gitlab/backend/grack_auth_spec.rb @@ -191,10 +191,10 @@ describe Grack::Auth, lib: true do context "when a gitlab ci token is provided" do let(:token) { "123" } - let(:project) { FactoryGirl.create :empty_project, token: token } + let(:project) { FactoryGirl.create :empty_project } before do - project.update_attributes(token: token, builds_enabled: true) + project.update_attributes(runners_token: token, builds_enabled: true) env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials("gitlab-ci-token", token) end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 91af694fc0..1fd475d205 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -111,7 +111,7 @@ describe Ci::Build, models: true do let(:token) { 'my_secret_token' } before do - build.project.update_attributes(token: token) + build.project.update_attributes(runners_token: token) build.update_attributes(trace: token) end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index b962f66ba2..87582e0749 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -98,13 +98,13 @@ describe Project, models: true do describe 'project token' do it 'should set an random token if none provided' do - project = FactoryGirl.create :empty_project, token: '' - expect(project.token).not_to eq('') + project = FactoryGirl.create :empty_project, runners_token: '' + expect(project.runners_token).not_to eq('') end it 'should not set an random toke if one provided' do - project = FactoryGirl.create :empty_project, token: 'my-token' - expect(project.token).to eq('my-token') + project = FactoryGirl.create :empty_project, runners_token: 'my-token' + expect(project.runners_token).to eq('my-token') end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 550d74d158..c27e87c4ac 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -131,7 +131,7 @@ describe Ci::API::API do let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") } let(:get_url) { ci_api("/builds/#{build.id}/artifacts") } let(:headers) { { "GitLab-Workhorse" => "1.0" } } - let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.project.token) } + let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token) } describe "POST /builds/:id/artifacts/authorize" do context "should authorize posting artifact to running build" do @@ -140,7 +140,7 @@ describe Ci::API::API do end it "using token as parameter" do - post authorize_url, { token: build.project.token }, headers + post authorize_url, { token: build.token }, headers expect(response.status).to eq(200) expect(json_response["TempPath"]).to_not be_nil end @@ -159,7 +159,7 @@ describe Ci::API::API do it "using token as parameter" do stub_application_setting(max_artifacts_size: 0) - post authorize_url, { token: build.project.token, filesize: 100 }, headers + post authorize_url, { token: build.token, filesize: 100 }, headers expect(response.status).to eq(413) end @@ -239,7 +239,7 @@ describe Ci::API::API do end it do - post post_url, { token: build.project.token }, {} + post post_url, { token: build.token }, {} expect(response.status).to eq(403) end end @@ -279,12 +279,12 @@ describe Ci::API::API do describe "DELETE /builds/:id/artifacts" do before do build.run! - post delete_url, token: build.project.token, file: file_upload + post delete_url, token: build.token, file: file_upload end it "should delete artifact build" do build.success - delete delete_url, token: build.project.token + delete delete_url, token: build.token expect(response.status).to eq(200) end end @@ -296,12 +296,12 @@ describe Ci::API::API do it "should download artifact" do build.update_attributes(artifacts_file: file_upload) - get get_url, token: build.project.token + get get_url, token: build.token expect(response.status).to eq(200) end it "should fail to download if no artifact uploaded" do - get get_url, token: build.project.token + get get_url, token: build.token expect(response.status).to eq(404) end end diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index 944d910d05..b98c4d506c 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -31,7 +31,7 @@ describe Ci::API::API do describe "should create a runner if project token provided" do let(:project) { FactoryGirl.create(:empty_project) } - before { post ci_api("/runners/register"), token: project.token } + before { post ci_api("/runners/register"), token: project.runners_token } it { expect(response.status).to eq(201) } it { expect(project.runners.size).to eq(1) } From dd8102f2d1acd803793777b26c84d7c6e977b8e2 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 11 Dec 2015 14:37:16 +0100 Subject: [PATCH 571/908] Fix specs --- app/models/project.rb | 6 ++++-- app/views/projects/commit/_builds.html.haml | 8 ++++---- app/views/projects/triggers/index.html.haml | 2 +- lib/gitlab/backend/grack_auth.rb | 2 +- spec/models/build_spec.rb | 2 +- spec/models/commit_status_spec.rb | 3 ++- spec/services/create_commit_builds_service_spec.rb | 4 ++-- 7 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index ba04470c64..e1f7bf971e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -830,11 +830,13 @@ class Project < ActiveRecord::Base end def valid_runners_token? token - self.token && self.token == token + self.runners_token && self.runners_token == token end + # TODO (ayufan): For now we use runners_token (backward compatibility) + # In 8.4 every build will have its own individual token valid for time of build def valid_build_token? token - self.token && self.token == token + self.builds_enabled? && self.runners_token && self.runners_token == token end def build_coverage_enabled? diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index e65a5a9c2d..329aaa0bb8 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -39,12 +39,12 @@ %th Name %th Duration %th Finished at - - if @ci_commit.project.coverage_enabled? + - if @ci_commit.project.build_coverage_enabled? %th Coverage %th - @ci_commit.refs.each do |ref| = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered, - locals: { coverage: @ci_commit.project.coverage_enabled?, stage: true, allow_retry: true } + locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true } - if @ci_commit.retried.any? .gray-content-block.second-block @@ -61,8 +61,8 @@ %th Name %th Duration %th Finished at - - if @ci_commit.project.coverage_enabled? + - if @ci_commit.project.build_coverage_enabled? %th Coverage %th = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, - locals: { coverage: @ci_commit.project.coverage_enabled?, stage: true } + locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true } diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index fb3794764c..bd346c4b8e 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -68,4 +68,4 @@ -F token=TOKEN \ -F "ref=REF_NAME" \ -F "variables[RUN_NIGHTLY_BUILD]=true" \ - #{builds_trigger_url(@project.id, 'TOKEN')} + #{builds_trigger_url(@project.id)} diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index d854c1c868..cdcaae8094 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -78,7 +78,7 @@ module Grack underscored_service = matched_login['s'].underscore if underscored_service == 'gitlab_ci' - return project && project.builds_enabled? && project.valid_build_token?(password) + return project && project.valid_build_token?(password) elsif Service.available_services_names.include?(underscored_service) service_method = "#{underscored_service}_service" service = project.send(service_method) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 1fd475d205..96b6f1dbca 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -386,7 +386,7 @@ describe Ci::Build, models: true do it { is_expected.to be_a(String) } it { is_expected.to end_with(".git") } it { is_expected.to start_with(project.web_url[0..6]) } - it { is_expected.to include(project.token) } + it { is_expected.to include(build.token) } it { is_expected.to include('gitlab-ci-token') } it { is_expected.to include(project.web_url[7..-1]) } end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 6131191f0f..b8f901b343 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -39,12 +39,13 @@ describe CommitStatus, models: true do it { is_expected.to belong_to(:commit) } it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:project) } + it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_inclusion_of(:status).in_array(%w(pending running failed success canceled)) } it { is_expected.to delegate_method(:sha).to(:commit) } it { is_expected.to delegate_method(:short_sha).to(:commit) } - it { is_expected.to delegate_method(:project).to(:commit) } it { is_expected.to respond_to :success? } it { is_expected.to respond_to :failed? } diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb index ae56f0b3a9..798c480b81 100644 --- a/spec/services/create_commit_builds_service_spec.rb +++ b/spec/services/create_commit_builds_service_spec.rb @@ -20,11 +20,11 @@ describe CreateCommitBuildsService, services: true do ) end - it { expect(commit).to be_kind_of(Commit) } + it { expect(commit).to be_kind_of(Ci::Commit) } it { expect(commit).to be_valid } it { expect(commit).to be_persisted } it { expect(commit).to eq(project.ci_commits.last) } - it { expect(commit.builds.first).to be_kind_of(Build) } + it { expect(commit.builds.first).to be_kind_of(Ci::Build) } end context "skip tag if there is no build for it" do From 1c0683da7b593bb12e37d46e89a0764a93a0da95 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 11 Dec 2015 17:44:00 +0100 Subject: [PATCH 572/908] Fix last specs --- app/views/projects/runners/_specific_runners.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 67767f3e34..30cd1263a1 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -12,7 +12,7 @@ %code #{ci_root_url(only_path: false)} %li Use the following registration token during setup: - %code #{@project.token} + %code #{@project.runners_token} %li Start runner! From c9ac38a074423b30b5627553bfdbf0ba15737d8e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 11 Dec 2015 17:57:04 +0100 Subject: [PATCH 573/908] Use Gitlab::Git instead of Ci::Git --- app/models/ci/commit.rb | 2 +- app/services/create_commit_builds_service.rb | 6 +++--- lib/ci/git.rb | 5 ----- 3 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 lib/ci/git.rb diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 7919334454..d2a2923694 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -53,7 +53,7 @@ module Ci end def valid_commit_sha - if self.sha == Ci::Git::BLANK_SHA + if self.sha == Gitlab::Git::BLANK_SHA self.errors.add(:sha, " cant be 00000000 (branch removal)") end end diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb index 392fe21eec..759c334ebe 100644 --- a/app/services/create_commit_builds_service.rb +++ b/app/services/create_commit_builds_service.rb @@ -9,14 +9,14 @@ class CreateCommitBuildsService return false end - ref = origin_ref.gsub(/\Arefs\/(tags|heads)\//, '') + ref = Gitlab::Git.ref_name(origin_ref) # Skip branch removal - if sha == Ci::Git::BLANK_SHA + if sha == Gitlab::Git::BLANK_SHA return false end - tag = origin_ref.start_with?('refs/tags/') + tag = Gitlab::Git.tag_ref?(origin_ref) commit = project.ensure_ci_commit(sha) unless commit.skip_ci? commit.update_committed! diff --git a/lib/ci/git.rb b/lib/ci/git.rb deleted file mode 100644 index 7acc3f38ed..0000000000 --- a/lib/ci/git.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Ci - module Git - BLANK_SHA = '0' * 40 - end -end From fc1e2f89864a74d78d67ca27d99bd43407c703df Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 11 Dec 2015 18:03:48 +0100 Subject: [PATCH 574/908] Run builds from projects with enabled CI --- app/services/ci/register_build_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb index eba602da99..4ff268a6f0 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_build_service.rb @@ -11,7 +11,7 @@ module Ci builds.joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }) else # do run projects which are only assigned to this runner - builds.where(project: current_runner.projects) + builds.where(project: current_runner.projects.where(builds_enabled: true)) end builds = builds.order('created_at ASC') From baa97175abd25eb566d7273bc188a6459639abf3 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Fri, 11 Dec 2015 11:09:34 -0600 Subject: [PATCH 575/908] Clarify cache behavior --- doc/ci/yaml/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 3dbf1afc7a..7e2edb945d 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -113,6 +113,9 @@ The YAML-defined variables are also set to all created service containers, thus ### cache `cache` is used to specify list of files and directories which should be cached between builds. +Caches are stored according to the branch/ref and the job name. Caches are not +currently shared between different job names or between branches/refs. This means +caching will benefit you if you push subsequent commits to an existing feature branch. **The global setting allows to specify default cached files for all jobs.** From 722f14816c85bc5996caac33adb4e4c54e6efdfc Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 11 Dec 2015 13:27:28 -0500 Subject: [PATCH 576/908] Update CHANGELOG [ci skip] --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 4c8d8dcd4d..cc6d19063f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -46,6 +46,7 @@ v 8.3.0 (unreleased) - Fix emoji aliases problem - Fix award-emojis Flash alert's width - Fix deleting notes on a merge request diff + - Display referenced merge request statuses in the issue description (Greg Smethells) v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) From 456ddb5eb7f905899bd1a6258617eb505f4dc289 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 11 Dec 2015 13:57:49 -0500 Subject: [PATCH 577/908] Fix time_ago_with_tooltip for activity feed Closes #4002 --- app/helpers/application_helper.rb | 8 ++++++-- spec/helpers/application_helper_spec.rb | 7 ++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 21f962df20..39d3017162 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -204,12 +204,16 @@ module ApplicationHelper # Returns an HTML-safe String def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false) element = content_tag :time, time.to_s, - class: "#{html_class} js-timeago", + class: "#{html_class} js-timeago js-timeago-pending", datetime: time.getutc.iso8601, title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'), data: { toggle: 'tooltip', placement: placement, container: 'body' } - element += javascript_tag "$('.js-timeago').last().timeago()" unless skip_js + unless skip_js + element << javascript_tag( + "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()" + ) + end element end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 5568f06639..68527c3a4f 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -263,11 +263,12 @@ describe ApplicationHelper do end it 'includes a default js-timeago class' do - expect(element.attr('class')).to eq 'time_ago js-timeago' + expect(element.attr('class')).to eq 'time_ago js-timeago js-timeago-pending' end it 'accepts a custom html_class' do - expect(element(html_class: 'custom_class').attr('class')).to eq 'custom_class js-timeago' + expect(element(html_class: 'custom_class').attr('class')). + to eq 'custom_class js-timeago js-timeago-pending' end it 'accepts a custom tooltip placement' do @@ -278,7 +279,7 @@ describe ApplicationHelper do el = element.next_element expect(el.name).to eq 'script' - expect(el.text).to include "$('.js-timeago').last().timeago()" + expect(el.text).to include "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()" end it 'allows the script tag to be excluded' do From f7bba47f8ab35f22882dc232229cbc5d36ead002 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 10 Dec 2015 19:01:49 +0100 Subject: [PATCH 578/908] Implement issuable sidebar partial Signed-off-by: Dmitriy Zaporozhets --- app/views/shared/issuable/_sidebar.html.haml | 80 ++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 app/views/shared/issuable/_sidebar.html.haml diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml new file mode 100644 index 0000000000..10f1aaf8c3 --- /dev/null +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -0,0 +1,80 @@ +.issuable-sidebar.issuable-affix + = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| + .block + .title + Cross-project reference + .cross-project-reference + %span#cross-project-reference.has_tooltip{title: 'Cross-project reference'} + = cross_project_reference(@project, issuable) + = clipboard_button(clipboard_target: 'span#cross-project-reference') + + .block + .title + %label + Assignee: + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + .pull-right + = link_to 'Edit', '#', class: 'edit-link' + .value + - if issuable.assignee + %strong= link_to_member(@project, issuable.assignee, size: 24) + - else + none + + .selectbox + = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true) + + .block + .title + %label + Milestone: + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + .pull-right + = link_to 'Edit', '#', class: 'edit-link' + .value + - if issuable.milestone + %span.back-to-milestone + = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do + %strong + = icon('clock-o') + = issuable.milestone.title + - else + none + .selectbox + = f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }}) + = hidden_field_tag :issuable_context + = f.submit class: 'btn hide' + + - if issuable.labels.any? + .block + .title + %label Labels + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + .pull-right + = link_to 'Edit', '#', class: 'edit-link' + .value.issuable-show-labels + - issuable.labels.each do |label| + = link_to_label(label) + .selectbox + = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, + { selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" } + + = render "shared/issuable/participants", participants: issuable.participants(current_user) + + - if current_user + - subscribed = issuable.subscribed?(current_user) + .block.light + .title + %label.light Notifications + - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' + %button.btn.btn-block.btn-gray.subscribe-button{:type => 'button'} + %span= subscribed ? 'Unsubscribe' : 'Subscribe' + .subscription-status{data: {status: subscribtion_status}} + .unsubscribed{class: ( 'hidden' if subscribed )} + You're not receiving notifications from this thread. + .subscribed{class: ( 'hidden' unless subscribed )} + You're receiving notifications because you're subscribed to this thread. + + :javascript + new Subscription("#{toggle_subscription_path(issuable)}"); + new IssuableContext(); From 76ff8eac26ef949b5bc99d99878cc8b43c4b4326 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 10 Dec 2015 19:02:14 +0100 Subject: [PATCH 579/908] Move awards css to separate file Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/awards.scss | 68 ++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 app/assets/stylesheets/pages/awards.scss diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss new file mode 100644 index 0000000000..895bafebce --- /dev/null +++ b/app/assets/stylesheets/pages/awards.scss @@ -0,0 +1,68 @@ +.awards { + @include clearfix; + line-height: 34px; + margin-top: $gl-padding; + + .award { + @include border-radius(5px); + + border: 1px solid; + padding: 0px 10px; + float: left; + margin-right: 5px; + border-color: $border-color; + cursor: pointer; + + &.active { + border-color: $border-gray-light; + background-color: $gray-light; + + .counter { + font-weight: bold; + } + } + + .icon { + float: left; + margin-right: 10px; + } + + .counter { + float: left; + } + } + + .awards-controls { + margin-left: 10px; + float: left; + + .add-award { + font-size: 24px; + color: $gl-gray; + position: relative; + top: 2px; + + &:hover, + &:link { + text-decoration: none; + } + } + + .awards-menu { + padding: $gl-padding; + min-width: 214px; + + > li { + cursor: pointer; + margin: 5px; + } + } + } + + .awards-menu{ + li { + float: left; + margin: 3px; + } + } +} From b0591ff03a11eff7d7bee9aab67cd73b80fe9105 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 10 Dec 2015 19:03:48 +0100 Subject: [PATCH 580/908] Redesign issue page for new sidebar Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/issuable.scss | 166 +++++++----------- .../projects/issues/_discussion.html.haml | 23 +-- app/views/projects/issues/show.html.haml | 47 +++-- app/views/projects/issues/update.js.haml | 4 +- app/views/shared/issuable/_context.html.haml | 57 ------ .../shared/issuable/_participants.html.haml | 8 +- 6 files changed, 102 insertions(+), 203 deletions(-) delete mode 100644 app/views/shared/issuable/_context.html.haml diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 957da5c182..35af92e7df 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -24,20 +24,6 @@ } } -.issuable-context-title { - margin-bottom: 5px; - - .avatar { - margin-left: 0; - } - - label { - color: $gl-gray; - font-weight: normal; - margin-right: 4px; - } -} - .project-issuable-filter { .controls { float: right; @@ -50,23 +36,6 @@ } .issuable-details { - .page-title { - margin-top: -$gl-padding; - padding: 7px 0; - margin-bottom: 0; - color: #5c5d5e; - font-size: 16px; - line-height: 42px; - - .author { - color: #5c5d5e; - } - - .issue-id { - color: #5c5d5e; - } - } - .issue-title { margin: 0; font-size: 23px; @@ -80,6 +49,20 @@ margin-bottom: 0; } } + + section { + border-right: 1px solid #ECEEF1; + + > .tab-content { + margin-right: 1px; + } + + > .gray-content-block { + margin-top: 0; + border-top: none; + margin-right: -15px; + } + } } .issuable-filter-count { @@ -101,84 +84,67 @@ } } -.cross-project-reference { - text-align: center; - width: 100%; +.issuable-sidebar { + .block { + @include clearfix; + padding: $gl-padding 0; + border-bottom: 1px solid #F0F0F0; - .slead { - padding: 5px; - } - - span, button { - background-color: $background-color; - } -} - -.awards { - @include clearfix; - line-height: 34px; - margin: 2px 0; - - .award { - @include border-radius(5px); - - border: 1px solid; - padding: 0px 10px; - float: left; - margin: 0 5px; - border-color: $border-color; - cursor: pointer; - - &.active { - border-color: $border-gray-light; - background-color: $gray-light; - - .counter { - font-weight: bold; - } - } - - .icon { - float: left; - margin-right: 10px; - } - - .counter { - float: left; + &:last-child { + border: none; } } - .awards-controls { - margin-left: 10px; - float: left; + .title { + color: $gl-text-color; + margin-bottom: 8px; - .add-award { - font-size: 24px; + .avatar { + margin-left: 0; + } + + label { + font-weight: normal; + margin-right: 4px; + } + + .edit-link { color: $gl-gray; - position: relative; - top: 2px; - - &:hover, - &:link { - text-decoration: none; - } - } - - .awards-menu { - padding: $gl-padding; - min-width: 214px; - - > li { - cursor: pointer; - margin: 5px; - } } } - .awards-menu{ - li { - float: left; - margin: 3px; + .cross-project-reference { + font-weight: bold; + color: $gl-link-color; + + button { + float: right; } } + + .selectbox { + display: none + } + + .btn-clipboard { + color: $gl-gray; + } +} + +.issuable-title { + margin: -$gl-padding; + padding: 7px $gl-padding; + margin-bottom: 0px; + border-bottom: 1px solid $border-color; + color: #5c5d5e; + font-size: 16px; + line-height: 42px; + + .author { + color: #5c5d5e; + } + + .issuable-id { + color: #5c5d5e; + } } diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index f2011542ca..86d3dc546b 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -5,24 +5,5 @@ - else = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close js-note-target-close', title: 'Close Issue' -= render 'shared/show_aside' - -.gray-content-block.second-block.oneline-block - .row - .col-md-9 - .votes-holder.pull-right - #votes= render 'votes/votes_block', votable: @issue - = render "shared/issuable/participants" - .col-md-3 - .input-group.cross-project-reference - %span#cross-project-reference.slead.has_tooltip{title: 'Cross-project reference'} - = cross_project_reference(@project, @issue) - = clipboard_button(clipboard_target: 'span#cross-project-reference') - -.row - %section.col-md-9 - .voting_notes#notes= render 'projects/notes/notes_with_form' - %aside.col-md-3 - .issuable-affix - .context - = render 'shared/issuable/context', issuable: @issue +#notes + = render 'projects/notes/notes_with_form' diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 38254403fa..79c9ef6b3c 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -3,13 +3,13 @@ .issue .issue-details.issuable-details - .page-title + .issuable-title .issue-box{ class: issue_box_class(@issue) } - if @issue.closed? Closed - else Open - %span.issue-id Issue ##{@issue.iid} + %span.issuable-id Issue ##{@issue.iid} %span.creator · opened by #{link_to_member(@project, @issue.author, size: 24)} @@ -36,22 +36,31 @@ = icon('pencil-square-o') Edit - .gray-content-block.middle-block - %h2.issue-title - = markdown escape_once(@issue.title), pipeline: :single_line - %div - - if @issue.description.present? - .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''} - .wiki - = preserve do - = markdown(@issue.description, cache_key: [@issue, "description"]) - %textarea.hidden.js-task-list-field - = @issue.description + .row + %section.col-md-9 + .gray-content-block + %h2.issue-title + = markdown escape_once(@issue.title), pipeline: :single_line + %div + - if @issue.description.present? + .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''} + .wiki + = preserve do + = markdown(@issue.description, cache_key: [@issue, "description"]) + %textarea.hidden.js-task-list-field + = @issue.description - .merge-requests - = render 'merge_requests' + .merge-requests + = render 'merge_requests' - - if @closed_by_merge_requests.present? - = render 'projects/issues/closed_by_box' - .issue-discussion - = render 'projects/issues/discussion' + = render 'votes/votes_block', votable: @issue + + - if @closed_by_merge_requests.present? + = render 'projects/issues/closed_by_box' + .issue-discussion + = render 'projects/issues/discussion' + + %aside.col-md-3 + = render 'shared/issuable/sidebar', issuable: @issue + + = render 'shared/show_aside' diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml index b7735aaf3c..2f0f3fcfb0 100644 --- a/app/views/projects/issues/update.js.haml +++ b/app/views/projects/issues/update.js.haml @@ -1,3 +1,3 @@ -$('.context').html("#{escape_javascript(render 'shared/issuable/context', issuable: @issue)}"); -$('.context').effect('highlight') +$('.issuable-sidebar').html("#{escape_javascript(render 'shared/issuable/sidebar', issuable: @issue)}"); +$('.issuable-sidebar').parent().effect('highlight') new Issue(); diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml deleted file mode 100644 index 2aa4666261..0000000000 --- a/app/views/shared/issuable/_context.html.haml +++ /dev/null @@ -1,57 +0,0 @@ -= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| - %div.prepend-top-default - .issuable-context-title - %label - Assignee: - - if issuable.assignee - %strong= link_to_member(@project, issuable.assignee, size: 24) - - else - none - .issuable-context-selectbox - - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true) - - %div.prepend-top-default.clearfix - .issuable-context-title - %label - Milestone: - - if issuable.milestone - %span.back-to-milestone - = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do - %strong - = icon('clock-o') - = issuable.milestone.title - - else - none - .issuable-context-selectbox - - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - = f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }}) - = hidden_field_tag :issuable_context - = f.submit class: 'btn hide' - - - if issuable.labels.any? - %div.prepend-top-default.clearfix - .issuable-context-title - %label Labels - .issuable-show-labels - - issuable.labels.each do |label| - = link_to_label(label) - - - if current_user - - subscribed = issuable.subscribed?(current_user) - %div.prepend-top-default.clearfix - .issuable-context-title - %label Subscription - - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' - .subscription-status{data: {status: subscribtion_status}} - .description-block.unsubscribed{class: ( 'hidden' if subscribed )} - You're not receiving notifications from this thread. - .description-block.subscribed{class: ( 'hidden' unless subscribed )} - You're receiving notifications because you're subscribed to this thread. - %button.btn.btn-block.subscribe-button{:type => 'button'} - = icon('eye') - %span= subscribed ? 'Unsubscribe' : 'Subscribe' - -:javascript - new Subscription("#{toggle_subscription_path(issuable)}"); - new IssuableContext(); diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml index b4e0def48b..da6bacbb74 100644 --- a/app/views/shared/issuable/_participants.html.haml +++ b/app/views/shared/issuable/_participants.html.haml @@ -1,5 +1,5 @@ -.participants - %span - = pluralize @participants.count, "participant" - - @participants.each do |participant| +.block.participants + .title + = pluralize participants.count, "participant" + - participants.each do |participant| = link_to_member(@project, participant, name: false, size: 24) From aa5bf4097b97bbb8d644c87e552d2a60a860a3f6 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 10 Dec 2015 19:04:34 +0100 Subject: [PATCH 581/908] Make edit link on issuable sidebar works Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/issuable_context.js.coffee | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index c4d3e619f5..02232698bc 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -5,9 +5,9 @@ class @IssuableContext new UsersSelect() $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) - $(".context .inline-update").on "change", "select", -> + $(".issuable-sidebar .inline-update").on "change", "select", -> $(this).submit() - $(".context .inline-update").on "change", ".js-assignee", -> + $(".issuable-sidebar .inline-update").on "change", ".js-assignee", -> $(this).submit() $('.issuable-details').waitForImages -> @@ -21,3 +21,9 @@ class @IssuableContext @top = ($('.issuable-affix').offset().top - 70) bottom: -> @bottom = $('.footer').outerHeight(true) + + $(".edit-link").click (e) -> + block = $(@).parents('.block') + block.find('.selectbox').show() + block.find('.value').hide() + block.find('.js-select2').select2("open") From c6c244315a4a0959894cf24aa931e4f027d02c3b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 10 Dec 2015 19:04:58 +0100 Subject: [PATCH 582/908] Implement new sidebar for merge request page Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/common.scss | 6 + app/assets/stylesheets/framework/selects.scss | 2 +- .../stylesheets/pages/merge_requests.scss | 4 - app/controllers/projects/issues_controller.rb | 1 - .../projects/merge_requests_controller.rb | 2 - .../merge_requests/_discussion.html.haml | 22 +-- .../projects/merge_requests/_show.html.haml | 127 ++++++++++-------- .../merge_requests/show/_mr_box.html.haml | 2 + .../merge_requests/show/_mr_title.html.haml | 4 +- .../projects/merge_requests/update.js.haml | 4 +- 10 files changed, 82 insertions(+), 92 deletions(-) diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 2e8515668f..88da799ee2 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -461,3 +461,9 @@ table { visibility: hidden; } } + +.content-separator { + margin-left: -$gl-padding; + margin-right: -$gl-padding; + border-top: 1px solid $border-color; +} diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index c01e1e32e4..af145191bc 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -181,4 +181,4 @@ .ajax-users-dropdown { min-width: 250px !important; -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index fc8c716199..502e9552ac 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -121,10 +121,6 @@ } } -.merge-request-details { - margin-bottom: $gl-padding; -} - .mr_source_commit, .mr_target_commit { .commit { diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index cf617d53ed..b59b52291f 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -58,7 +58,6 @@ class Projects::IssuesController < Projects::ApplicationController end def show - @participants = @issue.participants(current_user) @note = @project.notes.new(noteable: @issue) @notes = @issue.notes.nonawards.with_associations.fresh @noteable = @issue diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 530f3d3dcb..e8fa10fafb 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -279,8 +279,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def define_show_vars - @participants = @merge_request.participants(current_user) - # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) @notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index d64b19ae91..399e9cc1e1 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -5,24 +5,4 @@ - if @merge_request.closed? = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" -= render 'shared/show_aside' - -.gray-content-block.middle-block.oneline-block - .row - .col-md-9 - .votes-holder.pull-right - #votes= render 'votes/votes_block', votable: @merge_request - = render "shared/issuable/participants" - .col-md-3 - .input-group.cross-project-reference - %span#cross-project-reference.slead.has_tooltip{title: 'Cross-project reference'} - = cross_project_reference(@project, @merge_request) - = clipboard_button(clipboard_target: 'span#cross-project-reference') - -.row - %section.col-md-9 - .voting_notes#notes= render "projects/notes/notes_with_form" - %aside.col-md-3 - .issuable-affix - .context - = render 'shared/issuable/context', issuable: @merge_request +#notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 960d1561e7..b7b2859f95 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -7,71 +7,80 @@ .merge-request{'data-url' => merge_request_path(@merge_request)} .merge-request-details.issuable-details = render "projects/merge_requests/show/mr_title" - = render "projects/merge_requests/show/mr_box" - .append-bottom-default.mr-source-target.prepend-top-default - - if @merge_request.open? - .pull-right - - if @merge_request.source_branch_exists? - = link_to "#modal_merge_info", class: "btn btn-sm", "data-toggle" => "modal" do - = icon('cloud-download fw') - Check out branch + .row + %section.col-md-9 + = render "projects/merge_requests/show/mr_box" + .append-bottom-default.mr-source-target.prepend-top-default + - if @merge_request.open? + .pull-right + - if @merge_request.source_branch_exists? + = link_to "#modal_merge_info", class: "btn btn-sm", "data-toggle" => "modal" do + = icon('cloud-download fw') + Check out branch - %span.dropdown - %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } - = icon('download') - Download as - %span.caret - %ul.dropdown-menu - %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) - %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) - .normal - %span Request to merge - %span.label-branch= source_branch_with_namespace(@merge_request) - %span into - = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do - = @merge_request.target_branch + %span.dropdown + %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } + = icon('download') + Download as + %span.caret + %ul.dropdown-menu + %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) + %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) + .normal + %span Request to merge + %span.label-branch= source_branch_with_namespace(@merge_request) + %span into + = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do + = @merge_request.target_branch - = render "projects/merge_requests/show/how_to_merge" - = render "projects/merge_requests/widget/show.html.haml" + = render "projects/merge_requests/show/how_to_merge" + = render "projects/merge_requests/widget/show.html.haml" - - if @merge_request.open? && @merge_request.source_branch_exists? && @merge_request.can_be_merged? && @merge_request.can_be_merged_by?(current_user) - .light.prepend-top-default - You can also accept this merge request manually using the - = succeed '.' do - = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" + - if @merge_request.open? && @merge_request.source_branch_exists? && @merge_request.can_be_merged? && @merge_request.can_be_merged_by?(current_user) + .light.prepend-top-default + You can also accept this merge request manually using the + = succeed '.' do + = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - - if @commits.present? - %ul.merge-request-tabs.center-top-menu.no-top.no-bottom - %li.notes-tab - = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do - Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.count - %li.commits-tab - = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do - Commits - %span.badge= @commits.size - %li.diffs-tab - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do - Changes - %span.badge= @merge_request.diffs.size - - if @ci_commit - %li.builds-tab - = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do - Builds - %span.badge= @statuses.size + - if @commits.present? + %ul.merge-request-tabs.center-top-menu.no-top.no-bottom + %li.notes-tab + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do + Discussion + %span.badge= @merge_request.mr_and_commit_notes.user.count + %li.commits-tab + = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do + Commits + %span.badge= @commits.size + %li.diffs-tab + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do + Changes + %span.badge= @merge_request.diffs.size + - if @ci_commit + %li.builds-tab + = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do + Builds + %span.badge= @statuses.size - .tab-content - #notes.notes.tab-pane.voting_notes - = render "projects/merge_requests/discussion" - #commits.commits.tab-pane - - # This tab is always loaded via AJAX - #diffs.diffs.tab-pane - - # This tab is always loaded via AJAX - #builds.builds.tab-pane - - # This tab is always loaded via AJAX + .tab-content + #notes.notes.tab-pane.voting_notes + .content-separator + = render "projects/merge_requests/discussion" + #commits.commits.tab-pane + - # This tab is always loaded via AJAX + #diffs.diffs.tab-pane + - # This tab is always loaded via AJAX + #builds.builds.tab-pane + - # This tab is always loaded via AJAX + + .mr-loading-status + = spinner + + %aside.col-md-3 + = render 'shared/issuable/sidebar', issuable: @merge_request + + = render 'shared/show_aside' - .mr-loading-status - = spinner :javascript var merge_request; diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index 9bfe202589..66433c6d4f 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -10,3 +10,5 @@ = markdown(@merge_request.description, cache_key: [@merge_request, "description"]) %textarea.hidden.js-task-list-field = @merge_request.description + + = render 'votes/votes_block', votable: @merge_request diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 4dfe46e2b8..d65c3b1661 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,7 +1,7 @@ -.page-title +.issuable-title .issue-box{ class: issue_box_class(@merge_request) } = @merge_request.state_human_name - %span.issue-id Merge Request ##{@merge_request.iid} + %span.issuable-id Merge Request ##{@merge_request.iid} %span.creator · opened by #{link_to_member(@project, @merge_request.author, size: 24)} diff --git a/app/views/projects/merge_requests/update.js.haml b/app/views/projects/merge_requests/update.js.haml index 25583b2cc6..93db65ddf7 100644 --- a/app/views/projects/merge_requests/update.js.haml +++ b/app/views/projects/merge_requests/update.js.haml @@ -1,3 +1,3 @@ -$('.context').html("#{escape_javascript(render 'shared/issuable/context', issuable: @merge_request)}"); -$('.context').effect('highlight') +$('.issuable-sidebar').html("#{escape_javascript(render 'shared/issuable/sidebar', issuable: @merge_request)}"); +$('.issuable-sidebar').parent().effect('highlight') merge_request = new MergeRequest(); From 6117ae98da6fe9d3c5cf055546c82a97b46c87a6 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 10 Dec 2015 19:05:39 +0100 Subject: [PATCH 583/908] Update changelog Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index cc6d19063f..09e3382bec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -47,6 +47,7 @@ v 8.3.0 (unreleased) - Fix award-emojis Flash alert's width - Fix deleting notes on a merge request diff - Display referenced merge request statuses in the issue description (Greg Smethells) + - Implement new sidebar for issue and merge request pages v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) From 617ba01322b0d2d8fbe6a543b2e2393b8178089e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 10 Dec 2015 21:06:26 +0100 Subject: [PATCH 584/908] Fix tests for new issuable sidebar Signed-off-by: Dmitriy Zaporozhets --- app/views/shared/issuable/_sidebar.html.haml | 17 +++++++----- spec/features/issues_spec.rb | 27 +++++++++++++++----- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 10f1aaf8c3..cbd01c7d4f 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -8,7 +8,7 @@ = cross_project_reference(@project, issuable) = clipboard_button(clipboard_target: 'span#cross-project-reference') - .block + .block.assignee .title %label Assignee: @@ -19,12 +19,12 @@ - if issuable.assignee %strong= link_to_member(@project, issuable.assignee, size: 24) - else - none + None .selectbox = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true) - .block + .block.milestone .title %label Milestone: @@ -39,13 +39,13 @@ = icon('clock-o') = issuable.milestone.title - else - none + None .selectbox = f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }}) = hidden_field_tag :issuable_context = f.submit class: 'btn hide' - - if issuable.labels.any? + - if issuable.project.labels.any? .block .title %label Labels @@ -53,8 +53,11 @@ .pull-right = link_to 'Edit', '#', class: 'edit-link' .value.issuable-show-labels - - issuable.labels.each do |label| - = link_to_label(label) + - if issuable.labels.any? + - issuable.labels.each do |label| + = link_to_label(label) + - else + None .selectbox = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, { selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" } diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 3d6d87e764..0cad8c78ad 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -69,7 +69,10 @@ describe 'Issues', feature: true do click_button 'Save changes' - expect(page).to have_content 'Assignee: none' + page.within('.assignee') do + expect(page).to have_content 'None' + end + expect(issue.reload.assignee).to be_nil end end @@ -202,7 +205,7 @@ describe 'Issues', feature: true do it 'with dropdown menu' do visit namespace_project_issue_path(project.namespace, project, issue) - find('.context #issue_assignee_id'). + find('.issuable-sidebar #issue_assignee_id'). set project.team.members.first.id click_button 'Update Issue' @@ -241,12 +244,16 @@ describe 'Issues', feature: true do it 'with dropdown menu' do visit namespace_project_issue_path(project.namespace, project, issue) - find('.context'). + find('.issuable-sidebar'). select(milestone.title, from: 'issue_milestone_id') click_button 'Update Issue' expect(page).to have_content "Milestone changed to #{milestone.title}" - expect(page).to have_content "Milestone: #{milestone.title}" + + page.within('.milestone') do + expect(page).to have_content milestone.title + end + has_select?('issue_assignee_id', selected: milestone.title) end end @@ -279,13 +286,19 @@ describe 'Issues', feature: true do it 'allows user to remove assignee', js: true do visit namespace_project_issue_path(project.namespace, project, issue) - expect(page).to have_content "Assignee: #{user2.name}" - first('#s2id_issue_assignee_id').click + page.within('.assignee') do + expect(page).to have_content user2.name + end + + find('.assignee .edit-link').click sleep 2 # wait for ajax stuff to complete first('.user-result').click - expect(page).to have_content 'Assignee: none' + page.within('.assignee') do + expect(page).to have_content 'None' + end + sleep 2 # wait for ajax stuff to complete expect(issue.reload.assignee).to be_nil end From b4ea6ad16a63acfcf581f1e6e2f04f811a5efc90 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 11 Dec 2015 19:38:18 +0100 Subject: [PATCH 585/908] Few UI improvements to new sidebar implementation Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/issuable_context.js.coffee | 2 +- app/assets/stylesheets/pages/issuable.scss | 7 ++++++- app/views/shared/issuable/_sidebar.html.haml | 12 ++++++------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index 02232698bc..01bd515cc0 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -18,7 +18,7 @@ class @IssuableContext $('.issuable-affix').affix offset: top: -> - @top = ($('.issuable-affix').offset().top - 70) + @top = ($('.issuable-affix').offset().top - 60) bottom: -> @bottom = $('.footer').outerHeight(true) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 35af92e7df..1f1d720ae4 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -18,7 +18,7 @@ &.affix { position: fixed; - top: 70px; + top: 60px; margin-right: 35px; } } @@ -129,6 +129,11 @@ .btn-clipboard { color: $gl-gray; } + + .participants .avatar { + margin-top: 6px; + margin-right: 2px; + } } .issuable-title { diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index cbd01c7d4f..0019f739b8 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -4,14 +4,14 @@ .title Cross-project reference .cross-project-reference - %span#cross-project-reference.has_tooltip{title: 'Cross-project reference'} + %span#cross-project-reference = cross_project_reference(@project, issuable) = clipboard_button(clipboard_target: 'span#cross-project-reference') .block.assignee .title %label - Assignee: + Assignee - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .pull-right = link_to 'Edit', '#', class: 'edit-link' @@ -19,7 +19,7 @@ - if issuable.assignee %strong= link_to_member(@project, issuable.assignee, size: 24) - else - None + .light None .selectbox = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true) @@ -27,7 +27,7 @@ .block.milestone .title %label - Milestone: + Milestone - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .pull-right = link_to 'Edit', '#', class: 'edit-link' @@ -39,7 +39,7 @@ = icon('clock-o') = issuable.milestone.title - else - None + .light None .selectbox = f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }}) = hidden_field_tag :issuable_context @@ -57,7 +57,7 @@ - issuable.labels.each do |label| = link_to_label(label) - else - None + .light None .selectbox = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, { selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" } From 1911811438606e3ca8c6629ec2900afc5ba1e11a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 11 Dec 2015 19:48:11 +0100 Subject: [PATCH 586/908] Move awards back to gray panel and few improvements to sidebar Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/awards.scss | 1 - app/assets/stylesheets/pages/issuable.scss | 1 + app/views/projects/issues/_discussion.html.haml | 3 +++ app/views/projects/issues/show.html.haml | 2 -- app/views/projects/merge_requests/_discussion.html.haml | 3 +++ app/views/projects/merge_requests/_show.html.haml | 1 - app/views/projects/merge_requests/show/_mr_box.html.haml | 2 -- 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 895bafebce..f6d8522148 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -1,7 +1,6 @@ .awards { @include clearfix; line-height: 34px; - margin-top: $gl-padding; .award { @include border-radius(5px); diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 1f1d720ae4..797a0af372 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -57,6 +57,7 @@ margin-right: 1px; } + .issue-discussion > .gray-content-block, > .gray-content-block { margin-top: 0; border-top: none; diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 86d3dc546b..405bae1bbb 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -5,5 +5,8 @@ - else = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close js-note-target-close', title: 'Close Issue' +.gray-content-block.second-block.oneline-block + = render 'votes/votes_block', votable: @issue + #notes = render 'projects/notes/notes_with_form' diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 79c9ef6b3c..cc2cf8c871 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -53,8 +53,6 @@ .merge-requests = render 'merge_requests' - = render 'votes/votes_block', votable: @issue - - if @closed_by_merge_requests.present? = render 'projects/issues/closed_by_box' .issue-discussion diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 399e9cc1e1..7a7428d35c 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -5,4 +5,7 @@ - if @merge_request.closed? = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" +.gray-content-block.second-block.oneline-block + = render 'votes/votes_block', votable: @merge_request + #notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index b7b2859f95..04f8fd7442 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -64,7 +64,6 @@ .tab-content #notes.notes.tab-pane.voting_notes - .content-separator = render "projects/merge_requests/discussion" #commits.commits.tab-pane - # This tab is always loaded via AJAX diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index 66433c6d4f..9bfe202589 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -10,5 +10,3 @@ = markdown(@merge_request.description, cache_key: [@merge_request, "description"]) %textarea.hidden.js-task-list-field = @merge_request.description - - = render 'votes/votes_block', votable: @merge_request From 7cbcc21dcc4e38285dd9d097007df0979653bfe4 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 11 Dec 2015 20:03:58 +0100 Subject: [PATCH 587/908] Fix tests Signed-off-by: Dmitriy Zaporozhets --- spec/features/issues_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 0cad8c78ad..a2fb3e4c75 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -209,7 +209,7 @@ describe 'Issues', feature: true do set project.team.members.first.id click_button 'Update Issue' - expect(page).to have_content 'Assignee:' + expect(page).to have_content 'Assignee' has_select?('issue_assignee_id', selected: project.team.members.first.name) end From d86e2f33a8b80c5da30f26a320f542c1ecc5c540 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 11 Dec 2015 20:33:24 +0100 Subject: [PATCH 588/908] Increase fixed layout width to 1280px Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/variables.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 91954683c3..2ef40a6e51 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -19,7 +19,7 @@ $border-color: #dce0e6; $table-border-color: #eef0f2; $background-color: #F7F8FA; $header-height: 58px; -$fixed-layout-width: 1200px; +$fixed-layout-width: 1280px; $gl-gray: #7f8fa4; $gl-padding: 16px; $gl-avatar-size: 46px; From 51c8d037d37f19fdbf4ad89e1a1c39da9ca4f150 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 11 Dec 2015 20:39:26 +0100 Subject: [PATCH 589/908] Make profile navigation full wide Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/common.scss | 5 +++++ app/views/users/show.html.haml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 88da799ee2..7562ef6d24 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -401,6 +401,11 @@ table { border-bottom: 1px solid $border-color; height: 57px; } + + &.wide { + margin-left: -$gl-padding; + margin-right: -$gl-padding; + } } .center-middle-menu { diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index a0a6e2d981..b7a7eb4e6f 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -73,7 +73,7 @@ .user-calendar-activities -%ul.center-top-menu.no-top.no-bottom.bottom-border +%ul.center-top-menu.no-top.no-bottom.bottom-border.wide %li.active = link_to "#activity", 'data-toggle' => 'tab' do Activity From e6e27f03d77fa57b82f9189f45be7eec1ffe37ac Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Fri, 11 Dec 2015 23:16:37 +0000 Subject: [PATCH 590/908] add details on how to change saml button label --- doc/integration/saml.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/integration/saml.md b/doc/integration/saml.md index 1b8c28dd0f..1632e42f70 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -38,7 +38,8 @@ First configure SAML 2.0 support in GitLab, then register the GitLab application idp_sso_target_url: 'https://login.example.com/idp', issuer: 'https://gitlab.example.com', name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' - } + }, + "label" => "Company Login" # optional label for SAML login button, defaults to "Saml" } ] ``` @@ -79,4 +80,4 @@ On the sign in page there should now be a SAML button below the regular sign in If you see a "500 error" in GitLab when you are redirected back from the SAML sign in page, this likely indicates that GitLab could not get the email address for the SAML user. -Make sure the IdP provides a claim containing the user's email address, using claim name 'email' or 'mail'. The email will be used to automatically generate the GitLab username. +Make sure the IdP provides a claim containing the user's email address, using claim name 'email' or 'mail'. The email will be used to automatically generate the GitLab username. \ No newline at end of file From 3efae53bd79db118463bfaeceb209bc91f63bd0b Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 11 Dec 2015 23:17:36 -0800 Subject: [PATCH 591/908] Add open_issues_count to project API This is needed to support Huboard and a generally useful value. --- CHANGELOG | 1 + app/models/project.rb | 4 ++++ doc/api/projects.md | 3 +++ lib/api/entities.rb | 1 + spec/models/project_spec.rb | 6 +++++- spec/requests/api/projects_spec.rb | 16 ++++++++++++++++ 6 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e38c8b363e..ae544c08c7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.3.0 (unreleased) + - Add open_issues_count to project API (Stan Hu) - Expand character set of usernames created by Omniauth (Corey Hinshaw) - Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg) - Merge when build succeeds (Zeger-Jan van de Weg) diff --git a/app/models/project.rb b/app/models/project.rb index e78868af1c..6756e45caa 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -825,4 +825,8 @@ class Project < ActiveRecord::Base forked_project_link.destroy end end + + def open_issues_count + issues.opened.count + end end diff --git a/doc/api/projects.md b/doc/api/projects.md index 43a50a9a81..15956fe6df 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -59,6 +59,7 @@ Parameters: "path": "diaspora-client", "path_with_namespace": "diaspora/diaspora-client", "issues_enabled": true, + "open_issues_count": 1, "merge_requests_enabled": true, "builds_enabled": true, "wiki_enabled": true, @@ -101,6 +102,7 @@ Parameters: "path": "puppet", "path_with_namespace": "brightbox/puppet", "issues_enabled": true, + "open_issues_count": 1, "merge_requests_enabled": true, "builds_enabled": true, "wiki_enabled": true, @@ -192,6 +194,7 @@ Parameters: "path": "diaspora-project-site", "path_with_namespace": "diaspora/diaspora-project-site", "issues_enabled": true, + "open_issues_count": 1, "merge_requests_enabled": true, "builds_enabled": true, "wiki_enabled": true, diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 81bf7a8222..014116ef13 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -68,6 +68,7 @@ module API expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? } expose :avatar_url expose :star_count, :forks_count + expose :open_issues_count, if: lambda { | project, options | project.issues_enabled? && project.default_issues_tracker? } end class ProjectMember < UserBasic diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 6ddb0e2b8f..37ac049515 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -153,13 +153,17 @@ describe Project, models: true do describe '#get_issue' do let(:project) { create(:empty_project) } - let(:issue) { create(:issue, project: project) } + let!(:issue) { create(:issue, project: project) } context 'with default issues tracker' do it 'returns an issue' do expect(project.get_issue(issue.iid)).to eq issue end + it 'returns count of open issues' do + expect(project.open_issues_count).to eq(1) + end + it 'returns nil when no issue found' do expect(project.get_issue(999)).to be_nil end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 24b765f497..55a7b1a95f 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -65,6 +65,22 @@ describe API::API, api: true do expect(json_response.first.keys).to include('tag_list') end + it 'should include open_issues_count' do + get api('/projects', user) + expect(response.status).to eq 200 + expect(json_response).to be_an Array + expect(json_response.first.keys).to include('open_issues_count') + end + + it 'should not include open_issues_count' do + project.update_attributes( { issues_enabled: false } ) + + get api('/projects', user) + expect(response.status).to eq 200 + expect(json_response).to be_an Array + expect(json_response.first.keys).not_to include('open_issues_count') + end + context 'and using search' do it 'should return searched project' do get api('/projects', user), { search: project.name } From 0b60723d1cdef3804ae052defcb77e2641f22dcb Mon Sep 17 00:00:00 2001 From: Pelle Date: Sat, 12 Dec 2015 09:48:32 +0000 Subject: [PATCH 592/908] Fix wording on runner setup page --- app/views/projects/runners/_shared_runners.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index 316ea747b1..8be3d37982 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -12,8 +12,8 @@   for this project - if @shared_runners_count.zero? - This application has no shared runners yet. - Please use specific runners or ask administrator to create one + This GitLab server does not provide any shared runners yet. + Please use specific runners or ask the administrator to create one. - else %h4.underlined-title Available shared runners - #{@shared_runners_count} %ul.bordered-list.available-shared-runners From 9c5d8079a367ac24d04466f03f6b9abf5c333f59 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 24 Nov 2015 08:28:18 -0800 Subject: [PATCH 593/908] Bump Redis requirement to 2.8 for Sidekiq 4 requirements Closes #3649 [ci skip] --- CHANGELOG | 1 + README.md | 2 +- doc/install/installation.md | 22 +++++++++++++++++++--- lib/tasks/gitlab/check.rake | 2 +- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 09e3382bec..7359bda421 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ v 8.3.0 (unreleased) - Update project repositorize size and commit count during import:repos task (Stan Hu) - Fix API setting of 'public' attribute to false will make a project private (Stan Hu) - Handle and report SSL errors in Web hook test (Stan Hu) + - Bump Redis requirement to 2.8 for Sidekiq 4 (Stan Hu) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - Add rake tasks for git repository maintainance (Zeger-Jan van de Weg) - Fix 500 error when update group member permission diff --git a/README.md b/README.md index c459e67baa..3ec1d4a776 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ GitLab is a Ruby on Rails application that runs on the following software: - Ubuntu/Debian/CentOS/RHEL - Ruby (MRI) 2.1 - Git 1.7.10+ -- Redis 2.4+ +- Redis 2.8+ - MySQL or PostgreSQL For more information please see the [architecture documentation](http://doc.gitlab.com/ce/development/architecture.html). diff --git a/doc/install/installation.md b/doc/install/installation.md index 618391e16d..0a19a060a9 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -62,7 +62,7 @@ up-to-date and install it. Install the required packages (needed to compile Ruby and native extensions to Ruby gems): - sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake nodejs + sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake nodejs If you want to use Kerberos for user authentication, then install libkrb5-dev: @@ -174,7 +174,23 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da ## 6. Redis - sudo apt-get install redis-server +As of this writing, most Debian/Ubuntu distributions ship with Redis 2.2 or +2.4. GitLab requires at least Redis 2.8. If your platform doesn't provide +this, the following instructions cover building and installing Redis from +scratch. + +Ubuntu users [can also use a PPA](https://launchpad.net/~chris-lea/+archive/ubuntu/redis-server) +to install a recent version of Redis. + + # Build Redis + wget http://download.redis.io/releases/redis-2.8.23.tar.gz + tar xzf redis-2.8.23.tar.gz + cd redis-2.8.23 + make + + # Install Redis + cd utils + sudo ./install_server.sh # Configure redis to use sockets sudo cp /etc/redis/redis.conf /etc/redis/redis.conf.orig @@ -197,7 +213,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da fi # Activate the changes to redis.conf - sudo service redis-server restart + sudo service redis_6379 start # Add git to the redis group sudo usermod -aG redis git diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index a25fac62cf..b5af3d88b4 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -331,7 +331,7 @@ namespace :gitlab do end def check_redis_version - min_redis_version = "2.4.0" + min_redis_version = "2.8.0" print "Redis version >= #{min_redis_version}? ... " redis_version = run(%W(redis-cli --version)) From 118d96906ae3923206ca91ca9ccd3c5bc6c2fd3a Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 12 Dec 2015 12:38:12 -0500 Subject: [PATCH 594/908] Fix note polling Closes #4032 --- app/controllers/projects/notes_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 88b949a27a..ae6e9f6fd3 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -13,7 +13,8 @@ class Projects::NotesController < Projects::ApplicationController @notes.each do |note| notes_json[:notes] << { id: note.id, - html: note_to_html(note) + html: note_to_html(note), + valid: note.valid? } end From 3084c8c37064d7309ac4ca7b9ecb24ca8c625d24 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 12 Dec 2015 13:18:51 -0500 Subject: [PATCH 595/908] Define CI status icon colors in SCSS instead of a helper --- app/assets/stylesheets/pages/status.scss | 17 +++++++++++++++++ app/helpers/ci_status_helper.rb | 22 ++++------------------ spec/helpers/ci_status_helper_spec.rb | 11 +++-------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index a7d3b2197f..4b6ef03567 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -35,3 +35,20 @@ border-color: $gl-warning; } } + +.ci-status-icon-success { + @extend .cgreen; +} +.ci-status-icon-failed { + @extend .cred; +} +.ci-status-icon-running, +.ci-status-icon-pending { + // These are standard text color +} +.ci-status-icon-canceled, +.ci-status-icon-disabled, +.ci-status-icon-not-found, +.ci-status-icon-skipped { + @extend .cgray; +} diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index f8f2cbf131..fe5cad54a3 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -12,19 +12,6 @@ module CiStatusHelper ci_label_for_status(ci_commit.status) end - def ci_status_color(ci_commit) - case ci_commit.status - when 'success' - 'green' - when 'failed' - 'red' - when 'running', 'pending' - 'yellow' - else - 'gray' - end - end - def ci_status_with_icon(status) content_tag :span, class: "ci-status ci-#{status}" do ci_icon_for_status(status) + ' '.html_safe + ci_label_for_status(status) @@ -56,11 +43,10 @@ module CiStatusHelper end def render_ci_status(ci_commit) - link_to ci_status_path(ci_commit), - class: "c#{ci_status_color(ci_commit)}", + link_to ci_status_icon(ci_commit), + ci_status_path(ci_commit), + class: "ci-status-icon-#{ci_commit.status.dasherize}", title: "Build #{ci_status_label(ci_commit)}", - data: { toggle: 'tooltip', placement: 'left' } do - ci_status_icon(ci_commit) - end + data: { toggle: 'tooltip', placement: 'left' } end end diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb index 7fc53eb147..4f8d9c6726 100644 --- a/spec/helpers/ci_status_helper_spec.rb +++ b/spec/helpers/ci_status_helper_spec.rb @@ -6,13 +6,8 @@ describe CiStatusHelper do let(:success_commit) { double("Ci::Commit", status: 'success') } let(:failed_commit) { double("Ci::Commit", status: 'failed') } - describe 'ci_status_color' do - it { expect(ci_status_icon(success_commit)).to include('fa-check') } - it { expect(ci_status_icon(failed_commit)).to include('fa-close') } - end - - describe 'ci_status_color' do - it { expect(ci_status_color(success_commit)).to eq('green') } - it { expect(ci_status_color(failed_commit)).to eq('red') } + describe 'ci_status_icon' do + it { expect(helper.ci_status_icon(success_commit)).to include('fa-check') } + it { expect(helper.ci_status_icon(failed_commit)).to include('fa-close') } end end From 08e67983bef013e0aeeff14741c0fc1b01c470e9 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Sun, 13 Dec 2015 01:20:38 -0200 Subject: [PATCH 596/908] Update rerun to remove celluloid as dependency --- Gemfile | 2 +- Gemfile.lock | 29 ++++------------------------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/Gemfile b/Gemfile index 7298e21ce6..3391bd38dd 100644 --- a/Gemfile +++ b/Gemfile @@ -215,7 +215,7 @@ group :development do gem "annotate", "~> 2.6.0" gem "letter_opener", '~> 1.1.2' gem 'quiet_assets', '~> 1.0.2' - gem 'rerun', '~> 0.10.0' + gem 'rerun', '~> 0.11.0' gem 'bullet', require: false gem 'rblineprof', platform: :mri, require: false gem 'web-console', '~> 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index ff57460f5b..7dbf346654 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -117,23 +117,6 @@ GEM activemodel (>= 3.2.0) activesupport (>= 3.2.0) json (>= 1.7) - celluloid (0.17.2) - celluloid-essentials - celluloid-extras - celluloid-fsm - celluloid-pool - celluloid-supervision - timers (>= 4.1.1) - celluloid-essentials (0.20.5) - timers (>= 4.1.1) - celluloid-extras (0.20.5) - timers (>= 4.1.1) - celluloid-fsm (0.20.5) - timers (>= 4.1.1) - celluloid-pool (0.20.5) - timers (>= 4.1.1) - celluloid-supervision (0.20.5) - timers (>= 4.1.1) charlock_holmes (0.7.3) chunky_png (1.3.5) cliver (0.3.2) @@ -369,7 +352,6 @@ GEM hipchat (1.5.2) httparty mimemagic - hitimes (1.2.3) html-pipeline (1.11.0) activesupport (>= 2) nokogiri (~> 1.4) @@ -410,8 +392,7 @@ GEM addressable (~> 2.3) letter_opener (1.1.2) launchy (~> 2.2) - listen (2.9.0) - celluloid (>= 0.15.2) + listen (3.0.5) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) loofah (2.0.3) @@ -601,8 +582,8 @@ GEM redis-store (1.1.7) redis (>= 2.2) request_store (1.2.1) - rerun (0.10.0) - listen (~> 2.7, >= 2.7.3) + rerun (0.11.0) + listen (~> 3.0) responders (2.1.0) railties (>= 4.2.0, < 5) rest-client (1.8.0) @@ -758,8 +739,6 @@ GEM thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) - timers (4.1.1) - hitimes timfel-krb5-auth (0.8.3) tinder (1.10.1) eventmachine (~> 1.0) @@ -940,7 +919,7 @@ DEPENDENCIES redis-namespace redis-rails (~> 4.0.0) request_store (~> 1.2.0) - rerun (~> 0.10.0) + rerun (~> 0.11.0) responders (~> 2.0) rouge (~> 1.10.1) rqrcode-rails3 (~> 0.1.7) From 41f5bb035bd648c6fb0023bfbfb5ae40551cb180 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 14 Dec 2015 00:15:29 -0800 Subject: [PATCH 597/908] Rename mention of gitlab-git-http-server to gitlab-workhorse [ci skip] Closes https://github.com/gitlabhq/gitlabhq/issues/9904 --- doc/update/8.1-to-8.2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md index b08a79ca0a..46dfa2232b 100644 --- a/doc/update/8.1-to-8.2.md +++ b/doc/update/8.1-to-8.2.md @@ -142,7 +142,7 @@ git diff origin/8-1-stable:lib/support/nginx/gitlab origin/8-2-stable:lib/suppor If you are using Apache instead of NGINX please see the updated [Apache templates]. Also note that because Apache does not support upstreams behind Unix sockets you -will need to let gitlab-git-http-server listen on a TCP port. You can do this +will need to let gitlab-workhorse listen on a TCP port. You can do this via [/etc/default/gitlab]. [Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache From 00155b6c694d7d9a016b532694f5049b70e9a4db Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 8 Dec 2015 11:02:13 +0100 Subject: [PATCH 598/908] Suppress warning about missing `.gitlab-ci.yml` if builds are disabled When user disables GitLab Ci Service in project's settings then warning about missing `.gitlab-ci.yml` file should be supressed. This a matter of user experience as stated in #3761 (closes #3761). --- spec/features/commits_spec.rb | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 90739cd6a2..130a5016b5 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -60,15 +60,29 @@ describe "Commits" do end describe ".gitlab-ci.yml not found warning" do - it "does not show warning" do - visit ci_status_path(@commit) - expect(page).not_to have_content ".gitlab-ci.yml not found in this commit" + context 'ci service enabled' do + it "does not show warning" do + visit ci_status_path(@commit) + expect(page).not_to have_content ".gitlab-ci.yml not found in this commit" + end + + it "shows warning" do + stub_ci_commit_yaml_file(nil) + visit ci_status_path(@commit) + expect(page).to have_content ".gitlab-ci.yml not found in this commit" + end end - it "shows warning" do - stub_ci_commit_yaml_file(nil) - visit ci_status_path(@commit) - expect(page).to have_content ".gitlab-ci.yml not found in this commit" + context 'ci service disabled' do + before do + allow_any_instance_of(GitlabCiService).to receive(:active).and_return(false) + stub_ci_commit_yaml_file(nil) + visit ci_status_path(@commit) + end + + it 'does not show warning' do + expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + end end end end From 65fe4bb1ceed6bae05724f3494fb57e8b09fa616 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 8 Dec 2015 11:59:46 +0100 Subject: [PATCH 599/908] Improve gitlab ci commits specs (refactoring) This minimizes usage of instance variables in this spec, and changes double quotation marks to single when string interpolation is not being used. --- spec/features/commits_spec.rb | 78 +++++++++++++++---------------- spec/support/stub_gitlab_calls.rb | 4 ++ 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 130a5016b5..80ff4d3751 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -1,83 +1,83 @@ require 'spec_helper' -describe "Commits" do +describe 'Commits' do include CiStatusHelper let(:project) { create(:project) } - describe "CI" do + describe 'CI' do before do login_as :user project.team << [@user, :master] - @ci_project = project.ensure_gitlab_ci_project - @commit = FactoryGirl.create :ci_commit, gl_project: project, sha: project.commit.sha - @build = FactoryGirl.create :ci_build, commit: @commit - @generic_status = FactoryGirl.create :generic_commit_status, commit: @commit - end - - before do + project.ensure_gitlab_ci_project stub_ci_commit_to_return_yaml_file end - describe "GET /:project/commits/:sha/ci" do - before do - visit ci_status_path(@commit) - end - - it { expect(page).to have_content @commit.sha[0..7] } - it { expect(page).to have_content @commit.git_commit_message } - it { expect(page).to have_content @commit.git_author_name } + let!(:commit) do + FactoryGirl.create :ci_commit, gl_project: project, sha: project.commit.sha end - context "Download artifacts" do + let!(:build) { FactoryGirl.create :ci_build, commit: commit } + + describe 'GET /:project/commits/:sha/ci' do + before do + visit ci_status_path(commit) + end + + it { expect(page).to have_content commit.sha[0..7] } + it { expect(page).to have_content commit.git_commit_message } + it { expect(page).to have_content commit.git_author_name } + end + + context 'Download artifacts' do let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } before do - @build.update_attributes(artifacts_file: artifacts_file) + build.update_attributes(artifacts_file: artifacts_file) end it do - visit ci_status_path(@commit) - click_on "Download artifacts" + visit ci_status_path(commit) + click_on 'Download artifacts' expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) end end - describe "Cancel all builds" do - it "cancels commit" do - visit ci_status_path(@commit) - click_on "Cancel running" - expect(page).to have_content "canceled" + describe 'Cancel all builds' do + it 'cancels commit' do + visit ci_status_path(commit) + click_on 'Cancel running' + expect(page).to have_content 'canceled' end end - describe "Cancel build" do - it "cancels build" do - visit ci_status_path(@commit) - click_on "Cancel" - expect(page).to have_content "canceled" + describe 'Cancel build' do + it 'cancels build' do + visit ci_status_path(commit) + click_on 'Cancel' + expect(page).to have_content 'canceled' end end - describe ".gitlab-ci.yml not found warning" do + describe '.gitlab-ci.yml not found warning' do context 'ci service enabled' do it "does not show warning" do - visit ci_status_path(@commit) - expect(page).not_to have_content ".gitlab-ci.yml not found in this commit" + visit ci_status_path(commit) + expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' end - it "shows warning" do + it 'shows warning' do stub_ci_commit_yaml_file(nil) - visit ci_status_path(@commit) - expect(page).to have_content ".gitlab-ci.yml not found in this commit" + visit ci_status_path(commit) + expect(page).to have_content '.gitlab-ci.yml not found in this commit' end end context 'ci service disabled' do before do - allow_any_instance_of(GitlabCiService).to receive(:active).and_return(false) + stub_ci_service_disabled stub_ci_commit_yaml_file(nil) - visit ci_status_path(@commit) + visit ci_status_path(commit) end it 'does not show warning' do diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index 5b3eb1bfc5..9093618382 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -21,6 +21,10 @@ module StubGitlabCalls allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { ci_yaml } end + def stub_ci_service_disabled + allow_any_instance_of(GitlabCiService).to receive(:active).and_return(false) + end + private def gitlab_url From 2dafec91dd542ac641fea4750bf8fd68211a58af Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 14 Dec 2015 11:03:33 +0100 Subject: [PATCH 600/908] Add matcher class to ci status link --- app/helpers/ci_status_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index f8f2cbf131..c202c59206 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -57,7 +57,7 @@ module CiStatusHelper def render_ci_status(ci_commit) link_to ci_status_path(ci_commit), - class: "c#{ci_status_color(ci_commit)}", + class: "ci-status-link c#{ci_status_color(ci_commit)}", title: "Build #{ci_status_label(ci_commit)}", data: { toggle: 'tooltip', placement: 'left' } do ci_status_icon(ci_commit) From b8f67c5e4735eb25a3d03daeb95900dc87692123 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 14 Dec 2015 11:28:49 +0100 Subject: [PATCH 601/908] Do not display ci build status if builds enabled but no `.gitlab-ci.yml` Ref #3827 --- app/models/ci/commit.rb | 10 ++++++ .../projects/commit/_commit_box.html.haml | 2 +- app/views/projects/commits/_commit.html.haml | 4 +-- spec/features/commits_spec.rb | 31 ++++++++++++++++++- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 75465685e9..fca18ba79b 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -220,6 +220,16 @@ module Ci update!(committed_at: DateTime.now) end + ## + # This method checks if build status should be displayed. + # + # Build status should be available only if builds are enabled + # on project level and `.gitlab-ci.yml` file is present. + # + def show_build_status? + gl_project.builds_enabled? && ci_yaml_file + end + private def save_yaml_error(error) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 132cdc35c9..634924db24 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -40,7 +40,7 @@ - @commit.parents.each do |parent| = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace" -- if @ci_commit +- if @ci_commit && @ci_commit.show_build_status? .pull-right = link_to ci_status_path(@ci_commit), class: "ci-status ci-#{@ci_commit.status}" do = ci_status_icon(@ci_commit) diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 0d64486164..1303b27c4f 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -9,7 +9,7 @@ - cache_key.push(ci_commit.status) if ci_commit = cache(cache_key) do - %li.commit.js-toggle-container + %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } .commit-row-title %strong.str-truncated = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" @@ -17,7 +17,7 @@ %a.text-expander.js-toggle-button ... .pull-right - - if ci_commit + - if ci_commit && ci_commit.show_build_status? = render_ci_status(ci_commit)   = clipboard_button(clipboard_text: commit.id) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 80ff4d3751..f48d96c35a 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -19,7 +19,36 @@ describe 'Commits' do let!(:build) { FactoryGirl.create :ci_build, commit: commit } - describe 'GET /:project/commits/:sha/ci' do + describe 'Project commits' do + context 'builds enabled' do + context '.gitlab-ci.yml found' do + before do + visit namespace_project_commits_path(project.namespace, project, :master) + end + + it 'should show build status' do + page.within("//li[@id='commit-#{commit.short_sha}']") do + expect(page).to have_css(".ci-status-link") + end + end + end + + context 'no .gitlab-ci.yml found' do + before do + stub_ci_commit_yaml_file(nil) + visit namespace_project_commits_path(project.namespace, project, :master) + end + + it 'should not show build status' do + page.within("//li[@id='commit-#{commit.short_sha}']") do + expect(page).to have_no_css(".ci-status-link") + end + end + end + end + end + + describe 'Commit builds' do before do visit ci_status_path(commit) end From a0c2a7b0cbb4567a1f09c4cbc400bf75df47a072 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 14 Dec 2015 11:04:47 +0100 Subject: [PATCH 602/908] Fix migrations [ci skip] --- db/migrate/20151209144329_migrate_ci_web_hooks.rb | 5 +++-- db/migrate/20151209145909_migrate_ci_emails.rb | 4 ++-- db/migrate/20151210125232_migrate_ci_slack_service.rb | 10 +++++++--- .../20151210125927_migrate_ci_hip_chat_service.rb | 10 +++++++--- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/db/migrate/20151209144329_migrate_ci_web_hooks.rb b/db/migrate/20151209144329_migrate_ci_web_hooks.rb index 0293be3f6c..825ba1973f 100644 --- a/db/migrate/20151209144329_migrate_ci_web_hooks.rb +++ b/db/migrate/20151209144329_migrate_ci_web_hooks.rb @@ -3,8 +3,9 @@ class MigrateCiWebHooks < ActiveRecord::Migration def up execute( - 'INSERT INTO web_hooks (url, project_id, type, created_at, updated_at, push_events, build_events) ' \ - "SELECT ci_web_hooks.url, projects.id, 'ProjectHook', ci_web_hooks.created_at, ci_web_hooks.updated_at, #{false_value}, #{true_value} FROM ci_web_hooks " \ + 'INSERT INTO web_hooks (url, project_id, type, created_at, updated_at, push_events, issues_events, merge_requests_events, tag_push_events, note_events, build_events) ' \ + "SELECT ci_web_hooks.url, projects.id, 'ProjectHook', ci_web_hooks.created_at, ci_web_hooks.updated_at, " \ + "#{false_value}, #{false_value}, #{false_value}, #{false_value}, #{false_value}, #{true_value} FROM ci_web_hooks " \ 'JOIN ci_projects ON ci_web_hooks.project_id = ci_projects.id ' \ 'JOIN projects ON ci_projects.gitlab_id = projects.id' ) diff --git a/db/migrate/20151209145909_migrate_ci_emails.rb b/db/migrate/20151209145909_migrate_ci_emails.rb index 5ee1189358..202fac8e3f 100644 --- a/db/migrate/20151209145909_migrate_ci_emails.rb +++ b/db/migrate/20151209145909_migrate_ci_emails.rb @@ -30,9 +30,9 @@ class MigrateCiEmails < ActiveRecord::Migration # This function returns 0 or 1 for column def convert_bool(name) - if self.postgresql? + if Gitlab::Database.postgresql? # PostgreSQL uses BOOLEAN type - "CASE WHEN #{name} IS TRUE THEN '1' ELSE '0' END;" + "CASE WHEN #{name} IS TRUE THEN '1' ELSE '0' END" else # MySQL uses TINYINT "#{name}" diff --git a/db/migrate/20151210125232_migrate_ci_slack_service.rb b/db/migrate/20151210125232_migrate_ci_slack_service.rb index 4a5dfe866a..f14efa3e95 100644 --- a/db/migrate/20151210125232_migrate_ci_slack_service.rb +++ b/db/migrate/20151210125232_migrate_ci_slack_service.rb @@ -4,16 +4,20 @@ class MigrateCiSlackService < ActiveRecord::Migration def up properties_query = 'SELECT properties FROM ci_services ' \ 'JOIN ci_projects ON ci_services.project_id=ci_projects.id ' \ - 'WHERE ci_projects.gitlab_id=services.project_id' + "WHERE ci_projects.gitlab_id=services.project_id AND ci_services.type='Ci::SlackService' AND ci_services.active " \ + 'LIMIT 1' active_query = 'SELECT 1 FROM ci_services ' \ 'JOIN ci_projects ON ci_services.project_id=ci_projects.id ' \ - "WHERE ci_projects.gitlab_id=services.project_id AND ci_services.type='Ci::SlackService' AND ci_services.active" + "WHERE ci_projects.gitlab_id=services.project_id AND ci_services.type='Ci::SlackService' AND ci_services.active " \ + 'LIMIT 1' # We update the service since services are always generated for project, even if they are inactive # Activate service and migrate properties if currently the service is not active execute( - "UPDATE services SET properties=(#{properties_query}), build_events=#{true_value}, active=#{true_value} " \ + "UPDATE services SET properties=(#{properties_query}), active=#{true_value}, " \ + "push_events=#{false_value}, issues_events=#{false_value}, merge_requests_events=#{false_value}, " \ + "tag_push_events=#{false_value}, note_events=#{false_value}, build_events=#{true_value} " \ "WHERE NOT services.active AND services.type='SlackService' AND (#{active_query}) IS NOT NULL" ) diff --git a/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb index 2fdaf5fb91..b9e0432357 100644 --- a/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb +++ b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb @@ -5,16 +5,20 @@ class MigrateCiHipChatService < ActiveRecord::Migration # From properties strip `hipchat_` key properties_query = "SELECT REPLACE(properties, '\"hipchat_', '\"') FROM ci_services " \ 'JOIN ci_projects ON ci_services.project_id=ci_projects.id ' \ - 'WHERE ci_projects.gitlab_id=services.project_id' + "WHERE ci_projects.gitlab_id=services.project_id AND ci_services.type='Ci::HipChatService' AND ci_services.active " \ + 'LIMIT 1' active_query = 'SELECT 1 FROM ci_services ' \ 'JOIN ci_projects ON ci_services.project_id=ci_projects.id ' \ - "WHERE ci_projects.gitlab_id=services.project_id AND ci_services.type='Ci::HipchatService' AND ci_services.active" + "WHERE ci_projects.gitlab_id=services.project_id AND ci_services.type='Ci::HipChatService' AND ci_services.active " \ + 'LIMIT 1' # We update the service since services are always generated for project, even if they are inactive # Activate service and migrate properties if currently the service is not active execute( - "UPDATE services SET properties=(#{properties_query}), build_events=#{true_value}, active=#{true_value} " \ + "UPDATE services SET properties=(#{properties_query}), active=#{true_value}, " \ + "push_events=#{false_value}, issues_events=#{false_value}, merge_requests_events=#{false_value}, " \ + "tag_push_events=#{false_value}, note_events=#{false_value}, build_events=#{true_value} " \ "WHERE NOT services.active AND services.type='HipchatService' AND (#{active_query}) IS NOT NULL" ) From baa38f0dc1a0e1af84cd06a35450d772eee2d1c4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 14 Dec 2015 11:30:10 +0100 Subject: [PATCH 603/908] Fix runners admin view [ci skip] --- app/views/admin/runners/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index cd0cc2d177..32051852dc 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -82,7 +82,7 @@ = project.name_with_namespace %td .pull-right - = form_for [:admin, project.namespace, project, project.runner_projects.new] do |f| + = form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f| = f.hidden_field :runner_id, value: @runner.id = f.submit 'Enable', class: 'btn btn-xs' = paginate @projects From 208f99ff6c24a2dd5a69ed5666a1fd224d78c59b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 14 Dec 2015 12:03:28 +0100 Subject: [PATCH 604/908] Fix MySQL migration of CI emails [ci skip] --- db/migrate/20151209145909_migrate_ci_emails.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/db/migrate/20151209145909_migrate_ci_emails.rb b/db/migrate/20151209145909_migrate_ci_emails.rb index 202fac8e3f..7f330a2cf0 100644 --- a/db/migrate/20151209145909_migrate_ci_emails.rb +++ b/db/migrate/20151209145909_migrate_ci_emails.rb @@ -25,7 +25,11 @@ class MigrateCiEmails < ActiveRecord::Migration # This function escapes double-quotes and slash def escape_text(name) - "REPLACE(REPLACE(#{name}, '\\', '\\\\'), '\"', '\\\"')" + if Gitlab::Database.postgresql? + "REPLACE(REPLACE(#{name}, '\\', '\\\\'), '\"', '\\\"')" + else + "REPLACE(REPLACE(#{name}, '\\\\', '\\\\\\\\'), '\\\"', '\\\\\\\"')" + end end # This function returns 0 or 1 for column From 927c40ca496dcb1e7daa567ce173de044b1d3377 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 14 Dec 2015 12:31:23 +0100 Subject: [PATCH 605/908] Fix Ci::Project migration not migrating columns that cannot be NULL [ci skip] --- db/migrate/20151210125930_migrate_ci_to_project.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db/migrate/20151210125930_migrate_ci_to_project.rb b/db/migrate/20151210125930_migrate_ci_to_project.rb index d17b2a425f..7dfe05174e 100644 --- a/db/migrate/20151210125930_migrate_ci_to_project.rb +++ b/db/migrate/20151210125930_migrate_ci_to_project.rb @@ -27,11 +27,11 @@ class MigrateCiToProject < ActiveRecord::Migration def migrate_project_column(column, new_column = nil) new_column ||= column subquery = "SELECT ci_projects.#{column} FROM ci_projects WHERE projects.id = ci_projects.gitlab_id" - execute("UPDATE projects SET #{new_column}=(#{subquery}) WHERE #{new_column} IS NULL AND (#{subquery}) IS NOT NULL") + execute("UPDATE projects SET #{new_column}=(#{subquery}) WHERE (#{subquery}) IS NOT NULL") end def migrate_ci_service - subquery = "SELECT active FROM services WHERE projects.id = services.project_id AND type='GitlabCiService'" - execute("UPDATE projects SET builds_enabled=(#{subquery}) WHERE builds_enabled IS NULL AND (#{subquery}) IS NOT NULL") + subquery = "SELECT active FROM services WHERE projects.id = services.project_id AND type='GitlabCiService' LIMIT 1" + execute("UPDATE projects SET builds_enabled=(#{subquery}) WHERE (#{subquery}) IS NOT NULL") end end From cb4b4c57864867ba4aff04c38516042ef89f0bf7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 14 Dec 2015 12:45:03 +0100 Subject: [PATCH 606/908] Fix 500 when viewing specific runners on runners page [ci skip] --- app/views/admin/runners/show.html.haml | 2 +- app/views/projects/runners/_runner.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 32051852dc..8700b4820c 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -60,7 +60,7 @@ = project.name_with_namespace %td .pull-right - = link_to 'Disable', [:admin, project.namespace, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' + = link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' %table.table %thead diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 4d95afc28b..47ec420189 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -18,7 +18,7 @@ - runner_project = @project.runner_projects.find_by(runner_id: runner) = link_to 'Disable for this project', namespace_project_runner_project_path(@project.namespace, @project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' - elsif runner.specific? - = form_for [@project.namespace, @project, @project.runner_projects.new] do |f| + = form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f| = f.hidden_field :runner_id, value: runner.id = f.submit 'Enable for this project', class: 'btn btn-sm' .pull-right From 5ea5494af6e6bd87981f69cd97447f1ec2c0915a Mon Sep 17 00:00:00 2001 From: Job van der Voort Date: Mon, 14 Dec 2015 13:06:16 +0100 Subject: [PATCH 607/908] update guides for feature proposals on the issue tracker --- CONTRIBUTING.md | 49 ++++++++++++++++++++++++++++++++++++++++++------- PROCESS.md | 16 +++++++++------- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f5da063fd..284dc4606f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,15 +41,44 @@ This was inspired by [an article by Kent C. Dodds](https://medium.com/@kentcdodd To get support for your particular problem please use the [getting help channels](https://about.gitlab.com/getting-help/). -The [GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues) is only for obvious errors in the latest [stable or development release of GitLab](MAINTENANCE.md). If something is wrong but it is not a regression compared to older versions of GitLab please do not open an issue but a feature request. When submitting an issue please conform to the issue submission guidelines listed below. Not all issues will be addressed and your issue is more likely to be addressed if you submit a merge request which partially or fully addresses the issue. +The [GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues) is +for bugs in the latest GitLab release and feature proposals. -Do not use the issue tracker for feature requests. We have a specific [feature request forum](http://feedback.gitlab.com) for this purpose. Please keep feature requests as small and simple as possible, complex ones might be edited to make them small and simple. +When submitting an issue please conform to the issue submission guidelines listed below. +Not all issues will be addressed and your issue is more likely to be addressed +if you submit a merge request which partially or fully addresses the issue. -Please send a merge request with a tested solution or a merge request with a failing test instead of opening an issue if you can. If you're unsure where to post, post to the [mailing list](https://groups.google.com/forum/#!forum/gitlabhq) or [Stack Overflow](https://stackoverflow.com/questions/tagged/gitlab) first. There are a lot of helpful GitLab users there who may be able to help you quickly. If your particular issue turns out to be a bug, it will find its way from there. +Please send a merge request with a tested solution or a merge request with a failing test instead of opening an issue if you can. +If you're unsure where to post, post to the [mailing list](https://groups.google.com/forum/#!forum/gitlabhq) +or [Stack Overflow](https://stackoverflow.com/questions/tagged/gitlab) first. +There are a lot of helpful GitLab users there who may be able to help you quickly. +If your particular issue turns out to be a bug, it will find its way from there. + +### Feature proposals + +To create a feature proposal for CE and CI, open an issue on the +[issue tracker of CE](https://gitlab.com/gitlab-org/gitlab-ce/issues). + +For feature proposals for EE, open an issue on +[the issue tracker of EE](https://gitlab.com/gitlab-org/gitlab-ce/issues). + +Add the label `feature proposal` to the issue. + +Please keep feature proposals as small and simple as possible, +complex proposals might be edited to make them small and simple. + +For changes in the interface, it can be helpful to create a mockup first. +If you want to create something yourself, consider opening an issue first to +discuss whether it is interesting to include this in GitLab. ### Issue tracker guidelines -**[Search the issues](https://gitlab.com/gitlab-org/gitlab-ce/issues)** for similar entries before submitting your own, there's a good chance somebody else had the same issue. Show your support with `:+1:` and/or join the discussion. Please submit issues in the following format (as the first post): +**[Search the issues](https://gitlab.com/gitlab-org/gitlab-ce/issues)** +for similar entries before submitting your own, +there's a good chance somebody else had the same issue or feature proposal. +Show your support with `:+1:` and/or join the discussion. + +Please submit bugs in the following format (as the first post): 1. **Summary:** Summarize your issue in one sentence (what goes wrong, what did you expect to happen) 1. **Steps to reproduce:** How can we reproduce the issue @@ -65,7 +94,12 @@ Please send a merge request with a tested solution or a merge request with a fai ## Merge requests -We welcome merge requests with fixes and improvements to GitLab code, tests, and/or documentation. The features we would really like a merge request for are listed with the [status 'accepting merge requests' on our feature request forum](http://feedback.gitlab.com/forums/176466-general/status/796455) but other improvements are also welcome. If you want to add a new feature that is not marked it is best to first create a feedback issue (if there isn't one already) and leave a comment asking for it to be marked accepting merge requests. Please include screenshots or wireframes if the feature will also change the UI. +We welcome merge requests with fixes and improvements to GitLab code, tests, and/or documentation. The features we would really like a merge request for are listed with the +[label 'accepting merge requests' on our issue tracker for CE](https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests) +and [EE](https://gitlab.com/gitlab-org/gitlab-ee/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests) +but other improvements are also welcome. +If you want to add a new feature that is not labeled it is best to first create a feedback issue (if there isn't one already) and leave a comment asking for it to be marked accepting merge requests. +Please include screenshots or wireframes if the feature will also change the UI. Merge requests can be filed either at [gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests) or [github.com](https://github.com/gitlabhq/gitlabhq/pulls). @@ -89,7 +123,8 @@ If you can, please submit a merge request with the fix or improvements including 1. The MR description should give a motive for your change and the method you used to achieve it 1. If the MR changes the UI it should include before and after screenshots 1. If the MR changes CSS classes please include the list of affected pages `grep css-class ./app -R` -1. Link relevant [issues](https://gitlab.com/gitlab-org/gitlab-ce/issues) and/or [feature requests](http://feedback.gitlab.com/) from the merge request description and leave a comment on them with a link back to the MR +1. Link relevant [issues](https://gitlab.com/gitlab-org/gitlab-ce/issues) +from the merge request description and leave a comment on them with a link back to the MR 1. Be prepared to answer questions and incorporate feedback even if requests for this arrive weeks or months after your MR submission 1. If your MR touches code that executes shell commands, make sure it adheres to the [shell command guidelines]( doc/development/shell_commands.md). 1. Also have a look at the [shell command guidelines](doc/development/shell_commands.md) if your code reads or opens files, or handles paths to files on disk. @@ -136,7 +171,7 @@ If you add a dependency in GitLab (such as an operating system package) please c 1. What does this MR do? 1. Are there points in the code the reviewer needs to double check? 1. Why was this MR needed? -1. What are the relevant issue numbers / [Feature requests](http://feedback.gitlab.com/)? +1. What are the relevant issue numbers? 1. Screenshots (if relevant) ## Contribution acceptance criteria diff --git a/PROCESS.md b/PROCESS.md index 72fc348144..5f4d67bc10 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -8,7 +8,7 @@ Below we describe the contributing process to GitLab for two reasons. So that co ### Issue team - Looks for issues without [workflow labels](#how-we-handle-issues) and triages issue -- Closes invalid issues with a comment (duplicates, [feature requests](#feature-requests), [fixed in newer version](#issue-fixed-in-newer-version), [issue report for old version](#issue-report-for-old-version), not a problem in GitLab, etc.) +- Closes invalid issues with a comment (duplicates, [fixed in newer version](#issue-fixed-in-newer-version), [issue report for old version](#issue-report-for-old-version), not a problem in GitLab, etc.) - Asks for feedback from issue reporter ([invalid issue reports](#improperly-formatted-issue), [format code](#code-format), etc.) - Monitors all issues for feedback (but especially ones commented on since automatically watching them) - Closes issues with no feedback from the reporter for two weeks @@ -45,6 +45,8 @@ Workflow labels are purposely not very detailed since that would be hard to keep - *Frontend* needs help from a Front-end engineer - *Graphics* needs help from a Graphics designer - *up-for-grabs* is an issue suitable for first-time contributors, of reasonable difficulty and size. Not exclusive with other labels. +- *feature proposal* is a proposal for a new feature for GitLab. People are encouraged to vote +in support or comment for further detail. Do not use `feature request`. Example workflow: when a UX designer provided a design but it needs frontend work they remove the UX label and add the frontend label. @@ -62,7 +64,6 @@ If an issue is complex and needs the attention of a specific person, assignment - Bright orange `#eb6420`: workflow labels for core team members (attached MR, awaiting developer action/feedback) - Light blue `#82C5FF`: functional labels - Green labels `#009800`: issues that can generally be ignored. For example, issues given the following labels normally can be closed immediately: - - Feature request (see copy & paste response: [Feature requests](#feature-requests)) - Support (see copy & paste response: [Support requests and configuration questions](#support-requests-and-configuration-questions) ## Be kind @@ -75,10 +76,6 @@ Be kind to people trying to contribute. Be aware that people may be a non-native Thanks for the issue report. Please reformat your issue to conform to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). -### Feature requests - -Thank you for your interest in improving GitLab. We don't use the issue tracker for feature requests. Things that are wrong but are not a regression compared to older versions of GitLab are considered feature requests and not issues. Please use the \[feature request forum\]\(http://feedback.gitlab.com/) for this purpose or create a merge request implementing this feature. Have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) for more information. - ### Issue report for old version Thanks for the issue report but we only support issues for the latest stable version of GitLab. I'm closing this issue but if you still experience this problem in the latest stable version, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). @@ -113,7 +110,12 @@ This merge request has been closed because a request for more information has no ### Accepting merge requests -Is there a request on [the feature request forum](http://feedback.gitlab.com/forums/176466-general) that is similar to this? If so, can you make a comment with a link to it? Please be aware that new functionality that is not marked [accepting merge/pull requests](http://feedback.gitlab.com/forums/176466-general/status/796455) on the forum might not make it into GitLab. You might be asked to make changes and even after implementing them your feature might still be declined. If you want to reduce the chance of this happening please have a discussion in the forum first. +Is there an issue on the [issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues) +that is similar to this? +Could you please link it here? +Please be aware that new functionality that is not marked +[accepting merge requests](https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests) +might not make it into GitLab. ### Only accepting merge requests with green tests From c0ff4fdd99d44e154083242e32207f567374c8b1 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 14 Dec 2015 13:06:59 +0100 Subject: [PATCH 608/908] Move CI admin builds and runners specs to correct directory [ci skip] --- spec/features/{atom/builds_spec.rb => admin/admin_builds_spec.rb} | 0 .../{atom/runners_spec.rb => admin/admin_runners_spec.rb} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename spec/features/{atom/builds_spec.rb => admin/admin_builds_spec.rb} (100%) rename spec/features/{atom/runners_spec.rb => admin/admin_runners_spec.rb} (100%) diff --git a/spec/features/atom/builds_spec.rb b/spec/features/admin/admin_builds_spec.rb similarity index 100% rename from spec/features/atom/builds_spec.rb rename to spec/features/admin/admin_builds_spec.rb diff --git a/spec/features/atom/runners_spec.rb b/spec/features/admin/admin_runners_spec.rb similarity index 100% rename from spec/features/atom/runners_spec.rb rename to spec/features/admin/admin_runners_spec.rb From 6586856a1572535e0b9ca2f9021dfd88a158ffdd Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 14 Dec 2015 14:03:58 +0100 Subject: [PATCH 609/908] Use a new admin runners path when reseting runners token --- app/controllers/admin/application_settings_controller.rb | 2 +- spec/features/admin/admin_runners_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 4804035938..9dd16f8c73 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -16,7 +16,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController def reset_runners_token @application_setting.reset_runners_registration_token! flash[:notice] = 'New runners registration token has been generated!' - redirect_to ci_admin_runners_path + redirect_to admin_runners_path end private diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index d5dd11a01d..66a2cc0c15 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -64,7 +64,7 @@ describe "Admin Runners" do describe 'runners registration token' do let!(:token) { current_application_settings.ensure_runners_registration_token } - before { visit ci_admin_runners_path } + before { visit admin_runners_path } it 'has a registration token' do expect(page).to have_content("Registration token is #{token}") From 9d2b8326b2c106389024a2c3e921fbb6283db748 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 14 Dec 2015 14:25:18 +0100 Subject: [PATCH 610/908] Update commits spinach tests related to displaying build status --- features/project/commits/commits.feature | 3 ++- features/steps/project/commits/commits.rb | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index 21367fd76a..5bb2d0e976 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -19,7 +19,8 @@ Feature: Project Commits Scenario: I browse commit with ci from list Given commit has ci status - And I click on commit link + And repository contains ".gitlab-ci.yml" file + When I click on commit link Then I see commit ci info And I click status link Then I see builds list diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 101eb408ba..a3141fe3be 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -108,6 +108,10 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps create :ci_build, commit: ci_commit end + step 'repository contains ".gitlab-ci.yml" file' do + allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file).and_return(String.new) + end + step 'I see commit ci info' do expect(page).to have_content "build: pending" end From 51ef4a2fed382833833f15afae1da3f98bfa5031 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Sat, 12 Dec 2015 23:06:23 -0200 Subject: [PATCH 611/908] Sidekiq-cron configuration moved to gitlab.yml --- config/gitlab.yml.example | 9 +++++++++ config/initializers/1_settings.rb | 9 +++++++++ config/initializers/sidekiq.rb | 7 ++----- config/schedule.yml | 10 ---------- 4 files changed, 20 insertions(+), 15 deletions(-) delete mode 100644 config/schedule.yml diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index db378118f8..54dddb41ac 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -144,6 +144,15 @@ production: &base # plain_url: "http://..." # default: http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon # ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon + ## Auxiliary jobs + # Periodically executed jobs, to self-heal Gitlab, do external synchronizations, etc. + # Please read here for more information: https://github.com/ondrejbartas/sidekiq-cron#adding-cron-job + cron_jobs: + # Flag stuck CI builds as failed + stuck_ci_builds_worker: + cron: "0 0 * * *" + + # # 2. GitLab CI settings # ========================== diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 63d8ae1743..37b92d0794 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -224,6 +224,15 @@ Settings.gravatar['plain_url'] ||= 'http://www.gravatar.com/avatar/%{hash}?s=%{ Settings.gravatar['ssl_url'] ||= 'https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon' Settings.gravatar['host'] = Settings.get_host_without_www(Settings.gravatar['plain_url']) +# +# Cron Jobs +# +Settings['cron_jobs'] ||= Settingslogic.new({}) +Settings.cron_jobs['stuck_ci_builds_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['stuck_ci_builds_worker']['cron'] ||= '0 0 * * *' +Settings.cron_jobs['stuck_ci_builds_worker']['class'] = 'StuckCiBuildsWorker' + + # # GitLab Shell # diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 2e3a71912e..b42b718a7d 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -18,11 +18,8 @@ Sidekiq.configure_server do |config| chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] end - # Sidekiq-cron: load recurring jobs from schedule.yml - schedule_file = 'config/schedule.yml' - if File.exists?(schedule_file) - Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file) - end + # Sidekiq-cron: load recurring jobs from gitlab.yml + Sidekiq::Cron::Job.load_from_hash! Gitlab.config.cron_jobs # Database pool should be at least `sidekiq_concurrency` + 2 # For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md diff --git a/config/schedule.yml b/config/schedule.yml deleted file mode 100644 index 993a95fef5..0000000000 --- a/config/schedule.yml +++ /dev/null @@ -1,10 +0,0 @@ -# Here is a list of jobs that are scheduled to run periodically. -# We use a UNIX cron notation to specify execution schedule. -# -# Please read here for more information: -# https://github.com/ondrejbartas/sidekiq-cron#adding-cron-job - -stuck_ci_builds_worker: - cron: "0 0 * * *" - class: "StuckCiBuildsWorker" - queue: "default" From 0a81a681585bf699a7a41a449a2c0c21e2e335c6 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 14 Dec 2015 18:14:50 +0100 Subject: [PATCH 612/908] Update CHANGELOG [ci skip] --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 365b9e973f..6ff06091f6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -51,6 +51,9 @@ v 8.3.0 (unreleased) - Display referenced merge request statuses in the issue description (Greg Smethells) - Implement new sidebar for issue and merge request pages - Emoji picker improvements + - Suppress warning about missing `.gitlab-ci.yml` if builds are disabled + - Do not show build status unless builds are enabled and `.gitlab-ci.yml` is present + - Persist runners registration token in database v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) From 58bb985fe0e3aff148191d2a2a2c0246b60c2ebe Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 12 Dec 2015 13:59:53 -0800 Subject: [PATCH 613/908] Add upgrade guide for 8.2 to 8.3 [ci skip] --- doc/update/8.2-to-8.3.md | 200 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 doc/update/8.2-to-8.3.md diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md new file mode 100644 index 0000000000..071ed02494 --- /dev/null +++ b/doc/update/8.2-to-8.3.md @@ -0,0 +1,200 @@ +# From 8.2 to 8.3 + +**NOTE:** GitLab 8.0 introduced several significant changes related to +installation and configuration which *are not duplicated here*. Be sure you're +already running a working version of at least 8.0 before proceeding with this +guide. + +### 0. Double-check your Git version + +**This notice applies only to /usr/local/bin/git** + +If you compiled Git from source on your GitLab server then please double-check +that you are using a version that protects against CVE-2014-9390. For six +months after this vulnerability became known the GitLab installation guide +still contained instructions that would install the outdated, 'vulnerable' Git +version 2.1.2. + +Run the following command to get your current Git version: + +```sh +/usr/local/bin/git --version +``` + +If you see 'No such file or directory' then you did not install Git according +to the outdated instructions from the GitLab installation guide and you can go +to the next step 'Stop server' below. + +If you see a version string then it should be v1.8.5.6, v1.9.5, v2.0.5, v2.1.4, +v2.2.1 or newer. You can use the [instructions in the GitLab source +installation +guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) +to install a newer version of Git. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-3-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-3-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.8 +``` + +### 5. Replace gitlab-git-http-server with gitlab-workhorse + +Install and compile gitlab-workhorse. This requires [Go +1.5](https://golang.org/dl) which should already be on your system +from GitLab 8.1. + +```bash +cd /home/git +sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git +cd gitlab-workhorse +sudo -u git -H git checkout 0.4.2 +sudo -u git -H make +``` + +Update the GitLab init script and 'default' file. + +``` +cd /home/git/gitlab +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +test -e /etc/default/gitlab && \ + sudo sed -i.pre-8.2 's/^\([^=]*\)gitlab_git_http_server/\1gitlab_workhorse/' /etc/default/gitlab +``` + +Make sure that you also update your **NGINX configuration** to use +the new gitlab-workhorse.socket file. + +### 6. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 7. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +git diff origin/8-2-stable:config/gitlab.yml.example origin/8-3-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +View changes between the previous recommended Nginx configuration and the +current one: + +```sh +# For HTTPS configurations +git diff origin/8-2-stable:lib/support/nginx/gitlab-ssl origin/8-3-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-2-stable:lib/support/nginx/gitlab origin/8-3-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-git-http-server listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-3-stable/lib/support/init.d/gitlab.default.example#L34 + + +### 8. Use Redis v2.8.0+ + +Previous versions of GitLab allowed Redis versions >= 2.0 to be used, but +GitLab 8.3 uses Sidekiq 4.0, which requires Redis 2.8. You can check your Redis version +with the following command: + + redis-cli info | grep redis_version + +If you need to upgrade, see the [installation guide for Redis](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#6-redis). + +### 9. Start application + + sudo service gitlab start + sudo service nginx restart + +### 10. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.2) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.1 to 8.2](8.1-to-8.2.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +## Troubleshooting + +### "You appear to have cloned an empty repository." + +See the [7.14 to 8.0 update guide](7.14-to-8.0.md#troubleshooting). From 5bca9ec7508414ccba03f29ddca149ea93e62f45 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 14 Dec 2015 00:17:10 -0800 Subject: [PATCH 614/908] Rename gitlab-git-http-server mention with gitlab-workhorse [ci skip] --- doc/update/8.2-to-8.3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md index 071ed02494..563754ff4f 100644 --- a/doc/update/8.2-to-8.3.md +++ b/doc/update/8.2-to-8.3.md @@ -143,7 +143,7 @@ git diff origin/8-2-stable:lib/support/nginx/gitlab origin/8-3-stable:lib/suppor If you are using Apache instead of NGINX please see the updated [Apache templates]. Also note that because Apache does not support upstreams behind Unix sockets you -will need to let gitlab-git-http-server listen on a TCP port. You can do this +will need to let gitlab-workhorse listen on a TCP port. You can do this via [/etc/default/gitlab]. [Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache From b9b35012de171fe03d0be2c3adac6f219bb6927a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 14 Dec 2015 09:22:42 -0800 Subject: [PATCH 615/908] Update gitlab-workhorse and remove mention of gitlab-git-http-server [ci skip] --- doc/update/8.2-to-8.3.md | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md index 563754ff4f..f1000ef465 100644 --- a/doc/update/8.2-to-8.3.md +++ b/doc/update/8.2-to-8.3.md @@ -67,36 +67,23 @@ sudo -u git -H git checkout 8-3-stable-ee ```bash cd /home/git/gitlab-shell -sudo -u git -H git fetch +sudo -u git -H git fetch --all sudo -u git -H git checkout v2.6.8 ``` -### 5. Replace gitlab-git-http-server with gitlab-workhorse +### 5. Update gitlab-workhorse Install and compile gitlab-workhorse. This requires [Go 1.5](https://golang.org/dl) which should already be on your system from GitLab 8.1. ```bash -cd /home/git -sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git -cd gitlab-workhorse -sudo -u git -H git checkout 0.4.2 +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout 0.4.3 sudo -u git -H make ``` -Update the GitLab init script and 'default' file. - -``` -cd /home/git/gitlab -sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab -test -e /etc/default/gitlab && \ - sudo sed -i.pre-8.2 's/^\([^=]*\)gitlab_git_http_server/\1gitlab_workhorse/' /etc/default/gitlab -``` - -Make sure that you also update your **NGINX configuration** to use -the new gitlab-workhorse.socket file. - ### 6. Install libs, migrations, etc. ```bash From 739ce7883aaaf2652c7da985254bdec60cb15c51 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 14 Dec 2015 12:28:17 -0500 Subject: [PATCH 616/908] Prepare Installation doc for 8.3.0-rc1 [ci skip] --- doc/install/installation.md | 73 ++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 0a19a060a9..f8116a8a31 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -38,6 +38,7 @@ The GitLab installation consists of setting up the following components: 1. Packages / Dependencies 1. Ruby +1. Go 1. System Users 1. Database 1. Redis @@ -175,48 +176,52 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da ## 6. Redis As of this writing, most Debian/Ubuntu distributions ship with Redis 2.2 or -2.4. GitLab requires at least Redis 2.8. If your platform doesn't provide -this, the following instructions cover building and installing Redis from -scratch. +2.4. GitLab requires at least Redis 2.8. -Ubuntu users [can also use a PPA](https://launchpad.net/~chris-lea/+archive/ubuntu/redis-server) +Ubuntu users [can use a PPA](https://launchpad.net/~chris-lea/+archive/ubuntu/redis-server) to install a recent version of Redis. - # Build Redis - wget http://download.redis.io/releases/redis-2.8.23.tar.gz - tar xzf redis-2.8.23.tar.gz - cd redis-2.8.23 - make +The following instructions cover building and installing Redis from scratch: - # Install Redis - cd utils - sudo ./install_server.sh +```sh +# Build Redis +wget http://download.redis.io/releases/redis-2.8.23.tar.gz +tar xzf redis-2.8.23.tar.gz +cd redis-2.8.23 +make - # Configure redis to use sockets - sudo cp /etc/redis/redis.conf /etc/redis/redis.conf.orig +# Install Redis +cd utils +sudo ./install_server.sh - # Disable Redis listening on TCP by setting 'port' to 0 - sed 's/^port .*/port 0/' /etc/redis/redis.conf.orig | sudo tee /etc/redis/redis.conf +# Configure redis to use sockets +sudo cp /etc/redis/redis.conf /etc/redis/redis.conf.orig - # Enable Redis socket for default Debian / Ubuntu path - echo 'unixsocket /var/run/redis/redis.sock' | sudo tee -a /etc/redis/redis.conf - # Grant permission to the socket to all members of the redis group - echo 'unixsocketperm 770' | sudo tee -a /etc/redis/redis.conf +# Disable Redis listening on TCP by setting 'port' to 0 +sed 's/^port .*/port 0/' /etc/redis/redis.conf.orig | sudo tee /etc/redis/redis.conf - # Create the directory which contains the socket - mkdir /var/run/redis - chown redis:redis /var/run/redis - chmod 755 /var/run/redis - # Persist the directory which contains the socket, if applicable - if [ -d /etc/tmpfiles.d ]; then - echo 'd /var/run/redis 0755 redis redis 10d -' | sudo tee -a /etc/tmpfiles.d/redis.conf - fi +# Enable Redis socket for default Debian / Ubuntu path +echo 'unixsocket /var/run/redis/redis.sock' | sudo tee -a /etc/redis/redis.conf - # Activate the changes to redis.conf - sudo service redis_6379 start +# Grant permission to the socket to all members of the redis group +echo 'unixsocketperm 770' | sudo tee -a /etc/redis/redis.conf - # Add git to the redis group - sudo usermod -aG redis git +# Create the directory which contains the socket +mkdir /var/run/redis +chown redis:redis /var/run/redis +chmod 755 /var/run/redis + +# Persist the directory which contains the socket, if applicable +if [ -d /etc/tmpfiles.d ]; then + echo 'd /var/run/redis 0755 redis redis 10d -' | sudo tee -a /etc/tmpfiles.d/redis.conf +fi + +# Activate the changes to redis.conf +sudo service redis_6379 start + +# Add git to the redis group +sudo usermod -aG redis git +``` ## 7. GitLab @@ -226,9 +231,9 @@ to install a recent version of Redis. ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-2-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-3-stable gitlab -**Note:** You can change `8-2-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-3-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It From 381a9951e91275f41ce09a17380222f1af3d6a22 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 14 Dec 2015 13:17:23 -0500 Subject: [PATCH 617/908] Update 8.2-to-8.3 guide [ci skip] --- doc/update/8.2-to-8.3.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md index f1000ef465..e69c4f7ed3 100644 --- a/doc/update/8.2-to-8.3.md +++ b/doc/update/8.2-to-8.3.md @@ -26,8 +26,7 @@ to the outdated instructions from the GitLab installation guide and you can go to the next step 'Stop server' below. If you see a version string then it should be v1.8.5.6, v1.9.5, v2.0.5, v2.1.4, -v2.2.1 or newer. You can use the [instructions in the GitLab source -installation +v2.2.1 or newer. You can use the [instructions in the GitLab source installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) to install a newer version of Git. @@ -73,14 +72,13 @@ sudo -u git -H git checkout v2.6.8 ### 5. Update gitlab-workhorse -Install and compile gitlab-workhorse. This requires [Go -1.5](https://golang.org/dl) which should already be on your system -from GitLab 8.1. +Install and compile gitlab-workhorse. This requires [Go 1.5](https://golang.org/dl) +which should already be on your system from GitLab 8.1. ```bash cd /home/git/gitlab-workhorse sudo -u git -H git fetch --all -sudo -u git -H git checkout 0.4.3 +sudo -u git -H git checkout 0.4.2 sudo -u git -H make ``` @@ -136,7 +134,6 @@ via [/etc/default/gitlab]. [Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache [/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-3-stable/lib/support/init.d/gitlab.default.example#L34 - ### 8. Use Redis v2.8.0+ Previous versions of GitLab allowed Redis versions >= 2.0 to be used, but @@ -145,7 +142,7 @@ with the following command: redis-cli info | grep redis_version -If you need to upgrade, see the [installation guide for Redis](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#6-redis). +If you need to upgrade, see the [installation guide for Redis](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-3-stable/doc/install/installation.md#6-redis). ### 9. Start application From 0b2c5003e67c219cef564c9cdceadc290d9271d3 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Mon, 14 Dec 2015 17:18:32 -0200 Subject: [PATCH 618/908] Updated Rubocop to latest version --- Gemfile | 2 +- Gemfile.lock | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 7298e21ce6..9131797ed1 100644 --- a/Gemfile +++ b/Gemfile @@ -261,7 +261,7 @@ group :development, :test do gem 'spring-commands-spinach', '~> 1.0.0' gem 'spring-commands-teaspoon', '~> 0.0.2' - gem 'rubocop', '~> 0.28.0', require: false + gem 'rubocop', '~> 0.35.0', require: false gem 'coveralls', '~> 0.8.2', require: false gem 'simplecov', '~> 0.10.0', require: false gem 'flog', require: false diff --git a/Gemfile.lock b/Gemfile.lock index ff57460f5b..796c90573c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -513,7 +513,7 @@ GEM multi_json (~> 1.0) websocket-driver (>= 0.2.0) posix-spawn (0.3.11) - powerpack (0.0.9) + powerpack (0.1.1) pry (0.10.3) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -637,12 +637,13 @@ GEM rspec-mocks (~> 3.3.0) rspec-support (~> 3.3.0) rspec-support (3.3.0) - rubocop (0.28.0) + rubocop (0.35.1) astrolabe (~> 1.3) - parser (>= 2.2.0.pre.7, < 3.0) - powerpack (~> 0.0.6) + parser (>= 2.2.3.0, < 3.0) + powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) - ruby-progressbar (~> 1.4) + ruby-progressbar (~> 1.7) + tins (<= 1.6.0) ruby-fogbugz (0.2.1) crack (~> 0.4) ruby-progressbar (1.7.5) @@ -945,7 +946,7 @@ DEPENDENCIES rouge (~> 1.10.1) rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.3.0) - rubocop (~> 0.28.0) + rubocop (~> 0.35.0) ruby-fogbugz (~> 0.2.1) sanitize (~> 2.0) sass-rails (~> 4.0.5) From 5fb1fd27aa7f305e910fb00229d4b92e8722731b Mon Sep 17 00:00:00 2001 From: Job van der Voort Date: Mon, 14 Dec 2015 20:22:06 +0100 Subject: [PATCH 619/908] move up text regarding where to post issue --- CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 284dc4606f..105be7b25a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,12 +48,13 @@ When submitting an issue please conform to the issue submission guidelines liste Not all issues will be addressed and your issue is more likely to be addressed if you submit a merge request which partially or fully addresses the issue. -Please send a merge request with a tested solution or a merge request with a failing test instead of opening an issue if you can. If you're unsure where to post, post to the [mailing list](https://groups.google.com/forum/#!forum/gitlabhq) or [Stack Overflow](https://stackoverflow.com/questions/tagged/gitlab) first. There are a lot of helpful GitLab users there who may be able to help you quickly. If your particular issue turns out to be a bug, it will find its way from there. +Please send a merge request with a tested solution or a merge request with a failing test instead of opening an issue if you can. + ### Feature proposals To create a feature proposal for CE and CI, open an issue on the From 829dc1493cd82ff9c3ba7f517131ef5fe4c71e9c Mon Sep 17 00:00:00 2001 From: Mrinal Date: Mon, 14 Dec 2015 19:46:41 +0000 Subject: [PATCH 620/908] Fix Typo [mailinglist to mailing list] --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f5da063fd..306ffdaf68 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ Issues and merge requests should be in English and contain appropriate language Please help other GitLab users when you can. The channels people will reach out on can be found on the [getting help page](https://about.gitlab.com/getting-help/). -Sign up for the mailinglist, answer GitLab questions on StackOverflow or respond in the IRC channel. +Sign up for the mailing list, answer GitLab questions on StackOverflow or respond in the IRC channel. You can also sign up on [CodeTriage](http://www.codetriage.com/gitlabhq/gitlabhq) to help with one issue every day. ## I want to contribute! @@ -191,4 +191,4 @@ This code of conduct applies both within project spaces and in public spaces whe Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by emailing contact@gitlab.com -This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/) +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/) \ No newline at end of file From e7d7cd67bb3e43676ae1a42d65ee45d7a01fc75b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 14 Dec 2015 22:07:53 +0200 Subject: [PATCH 621/908] Clarify what CE and EE means for newcomers --- CONTRIBUTING.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 09edfc8c30..c10b9f9807 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,11 @@ Thank you for your interest in contributing to GitLab. This guide details how contribute to GitLab in a way that is efficient for everyone. If you have read this guide and want to know how the GitLab core-team operates please see [the GitLab contributing process](PROCESS.md). +GitLab comes into two flavors, GitLab Community Edition (CE) our free and open +source edition, and GitLab Enterprise Edition (EE) which is our commercial +edition. Throughout this guide you will see references to CE and EE for +abbreviation. + ## Contributor license agreement By submitting code as an individual you agree to the [individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md). By submitting code as an entity you agree to the [corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md). @@ -227,4 +232,4 @@ This code of conduct applies both within project spaces and in public spaces whe Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by emailing contact@gitlab.com -This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/) \ No newline at end of file +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/) From 5afe03315d12d1379c31a87a02fc9d9b6952f539 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 14 Dec 2015 18:14:07 -0200 Subject: [PATCH 622/908] Preserve trailing new lines at the end of file on the online editor Because Haml automatically indents the HTML source code, the contents of whitespace-sensitive tags like pre and textarea can get screwed up. --- CHANGELOG | 1 + app/views/projects/blob/_editor.html.haml | 3 +-- features/project/source/browse_files.feature | 11 +++++++++++ features/steps/project/source/browse_files.rb | 8 ++++++++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6ff06091f6..7f9dfd98cd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -54,6 +54,7 @@ v 8.3.0 (unreleased) - Suppress warning about missing `.gitlab-ci.yml` if builds are disabled - Do not show build status unless builds are enabled and `.gitlab-ci.yml` is present - Persist runners registration token in database + - Fix online editor should not remove newlines at the end of the file v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 333f5d470e..10b0281373 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -16,8 +16,7 @@ = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' .file-content.code - %pre.js-edit-mode-pane#editor - = params[:content] || local_assigns[:blob_data] + %pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]} - if local_assigns[:path] .js-edit-mode-pane#preview.hide .center diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 439787f264..02159ee377 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -34,6 +34,17 @@ Feature: Project Source Browse Files Then I am redirected to the new file And I should see its new content + @javascript + Scenario: I can create and commit file with new lines at the end of file + Given I click on "New file" link in repo + And I edit code with new lines at end of file + And I fill the new file name + And I fill the commit message + And I click on "Commit Changes" + Then I am redirected to the new file + And I click button "Edit" + And I should see its content with new lines preserved at end of file + @javascript Scenario: I can upload file and commit Given I click on "Upload file" link in repo diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index f2b9576426..b88709620a 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -37,6 +37,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(page).to have_content new_gitignore_content end + step 'I should see its content with new lines preserved at end of file' do + expect(evaluate_script('blob.editor.getValue()')).to eq "Sample\n\n\n" + end + step 'I click link "Raw"' do click_link 'Raw' end @@ -62,6 +66,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps set_new_content end + step 'I edit code with new lines at end of file' do + execute_script('blob.editor.setValue("Sample\n\n\n")') + end + step 'I fill the new file name' do fill_in :file_name, with: new_file_name end From f8bf6c4b2c5fb41ae92df09b9f968d5d5d61bb2b Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Fri, 11 Dec 2015 17:27:20 -0600 Subject: [PATCH 623/908] [ci skip] Add user repository integrity check rake task --- doc/raketasks/README.md | 3 +- doc/raketasks/check.md | 63 +++++++++++++++++++++++++++ doc/raketasks/check_repos_output.png | Bin 0 -> 73786 bytes lib/tasks/gitlab/check.rake | 56 ++++++++++++++++++++++-- 4 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 doc/raketasks/check.md create mode 100644 doc/raketasks/check_repos_output.png diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md index a8dc5c24df..cc8a22cd00 100644 --- a/doc/raketasks/README.md +++ b/doc/raketasks/README.md @@ -1,10 +1,11 @@ # Rake tasks - [Backup restore](backup_restore.md) +- [Check](check.md) - [Cleanup](cleanup.md) - [Features](features.md) - [Maintenance](maintenance.md) and self-checks - [User management](user_management.md) - [Web hooks](web_hooks.md) - [Import](import.md) of git repositories in bulk -- [Rebuild authorized_keys file](http://doc.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators \ No newline at end of file +- [Rebuild authorized_keys file](http://doc.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators diff --git a/doc/raketasks/check.md b/doc/raketasks/check.md new file mode 100644 index 0000000000..3ff3fee6a4 --- /dev/null +++ b/doc/raketasks/check.md @@ -0,0 +1,63 @@ +# Check Rake Tasks + +## Repository Integrity + +Even though Git is very resilient and tries to prevent data integrity issues, +there are times when things go wrong. The following Rake tasks intend to +help GitLab administrators diagnose problem repositories so they can be fixed. + +There are 3 things that are checked to determine integrity. + +1. Git repository file system check ([git fsck](https://git-scm.com/docs/git-fsck)). + This step verifies the connectivity and validity of objects in the repository. +1. Check for `config.lock` in the repository directory. +1. Check for any branch/references lock files in `refs/heads`. + +It's important to note that the existence of `config.lock` or reference locks +alone do not necessarily indicate a problem. Lock files are routinely created +and removed as Git and GitLab perform operations on the repository. They serve +to prevent data integrity issues. However, if a Git operation is interrupted these +locks may not be cleaned up properly. + +The following symptoms may indicate a problem with repository integrity. If users +experience these symptoms you may use the rake tasks described below to determine +exactly which repositories are causing the trouble. + +- Receiving an error when trying to push code - `remote: error: cannot lock ref` +- A 500 error when viewing the GitLab dashboard or when accessing a specific project. + +### Check all GitLab repositories + +This task loops through all repositories on the GitLab server and runs the +3 integrity checks described previously. + +``` +# omnibus-gitlab +sudo gitlab-rake gitlab:repo:check + +# installation from source +bundle exec rake gitlab:repo:check RAILS_ENV=production +``` + +### Check repositories for a specific user + +This task checks all repositories that a specific user has access to. This is important +because sometimes you know which user is experiencing trouble but you don't know +which project might be the cause. + +If the rake task is executed without brackets at the end, you will be prompted +to enter a username. + +```bash +# omnibus-gitlab +sudo gitlab-rake gitlab:user:check_repos +sudo gitlab-rake gitlab:user:check_repos[] + +# installation from source +bundle exec rake gitlab:user:check_repos RAILS_ENV=production +bundle exec rake gitlab:user:check_repos[] RAILS_ENV=production +``` + +Example output: + +![gitlab:user:check_repos output](check_repos_output.png) diff --git a/doc/raketasks/check_repos_output.png b/doc/raketasks/check_repos_output.png new file mode 100644 index 0000000000000000000000000000000000000000..916b16851018bd6de0258f465e6e125ea56d1e0c GIT binary patch literal 73786 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 DWM-fi literal 0 HcmV?d00001 diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index a25fac62cf..167b011266 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -822,10 +822,27 @@ namespace :gitlab do namespace_dirs.each do |namespace_dir| repo_dirs = Dir.glob(File.join(namespace_dir, '*')) - repo_dirs.each do |dir| - puts "\nChecking repo at #{dir}" - system(*%W(#{Gitlab.config.git.bin_path} fsck), chdir: dir) - end + repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) } + end + end + end + + namespace :user do + desc "GitLab | Check the integrity of a specific user's repositories" + task :check_repos, [:username] => :environment do |t, args| + username = args[:username] || prompt("Check repository integrity for which username? ".blue) + user = User.find_by(username: username) + if user + repo_dirs = user.authorized_projects.map do |p| + File.join( + Gitlab.config.gitlab_shell.repos_path, + "#{p.path_with_namespace}.git" + ) + end + + repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) } + else + puts "\nUser '#{username}' not found".red end end end @@ -952,4 +969,35 @@ namespace :gitlab do false end end + + def check_repo_integrity(repo_dir) + puts "\nChecking repo at #{repo_dir.yellow}" + + git_fsck(repo_dir) + check_config_lock(repo_dir) + check_ref_locks(repo_dir) + end + + def git_fsck(repo_dir) + puts "Running `git fsck`".yellow + system(*%W(#{Gitlab.config.git.bin_path} fsck), chdir: repo_dir) + end + + def check_config_lock(repo_dir) + config_exists = File.exist?(File.join(repo_dir,'config.lock')) + config_output = config_exists ? 'yes'.red : 'no'.green + puts "'config.lock' file exists?".yellow + " ... #{config_output}" + end + + def check_ref_locks(repo_dir) + lock_files = Dir.glob(File.join(repo_dir,'refs/heads/*.lock')) + if lock_files.present? + puts "Ref lock files exist:".red + lock_files.each do |lock_file| + puts " #{lock_file}" + end + else + puts "No ref lock files exist".green + end + end end From be41d84fb078667694ecbf5f2729175fbf8b0343 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Mon, 14 Dec 2015 15:34:46 -0600 Subject: [PATCH 624/908] Allow account unlock via email --- CHANGELOG | 1 + app/models/user.rb | 1 + .../devise/mailer/unlock_instructions.html.erb | 7 ------- .../devise/mailer/unlock_instructions.html.haml | 10 ++++++++++ app/views/devise/unlocks/new.html.erb | 12 ------------ app/views/devise/unlocks/new.html.haml | 14 ++++++++++++++ config/initializers/devise.rb | 4 ++-- .../20151210030143_add_unlock_token_to_user.rb | 5 +++++ db/schema.rb | 1 + spec/models/user_spec.rb | 1 + 10 files changed, 35 insertions(+), 21 deletions(-) delete mode 100644 app/views/devise/mailer/unlock_instructions.html.erb create mode 100644 app/views/devise/mailer/unlock_instructions.html.haml delete mode 100644 app/views/devise/unlocks/new.html.erb create mode 100644 app/views/devise/unlocks/new.html.haml create mode 100644 db/migrate/20151210030143_add_unlock_token_to_user.rb diff --git a/CHANGELOG b/CHANGELOG index 6ff06091f6..84e2684fb6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,7 @@ v 8.3.0 (unreleased) - Recognize issue/MR/snippet/commit links as references - Add ignore whitespace change option to commit view - Fire update hook from GitLab + - Allow account unlock via email - Style warning about mentioning many people in a comment - Fix: sort milestones by due date once again (Greg Smethells) - Migrate all CI::Services and CI::WebHooks to Services and WebHooks diff --git a/app/models/user.rb b/app/models/user.rb index fdd14f4571..829c37e88c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,6 +26,7 @@ # bio :string(255) # failed_attempts :integer default(0) # locked_at :datetime +# unlock_token :string(255) # username :string(255) # can_create_group :boolean default(TRUE), not null # can_create_team :boolean default(TRUE), not null diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb deleted file mode 100644 index 79d6c761d8..0000000000 --- a/app/views/devise/mailer/unlock_instructions.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -

Hello <%= @resource.email %>!

- -

Your account has been locked due to an excessive amount of unsuccessful sign in attempts.

- -

Click the link below to unlock your account:

- -

<%= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token) %>

diff --git a/app/views/devise/mailer/unlock_instructions.html.haml b/app/views/devise/mailer/unlock_instructions.html.haml new file mode 100644 index 0000000000..52b327e20c --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.haml @@ -0,0 +1,10 @@ +%p +Hello #{@resource.name}! + +%p + Your GitLab account has been locked due to an excessive amount of unsuccessful + sign in attempts. Your account will automatically unlock in + = time_ago_in_words(Devise.unlock_in.from_now) + or you may click the link below to unlock now. + +%p= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token) diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb deleted file mode 100644 index f9277d1673..0000000000 --- a/app/views/devise/unlocks/new.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -

Resend unlock instructions

- -<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> - <%= devise_error_messages! %> - -
<%= f.label :email %>
- <%= f.email_field :email %>
- -
<%= f.submit "Resend unlock instructions" %>
-<% end %> - -<%= render partial: "devise/shared/links" %> diff --git a/app/views/devise/unlocks/new.html.haml b/app/views/devise/unlocks/new.html.haml new file mode 100644 index 0000000000..49c087c064 --- /dev/null +++ b/app/views/devise/unlocks/new.html.haml @@ -0,0 +1,14 @@ +.login-box + .login-heading + %h3 Resend unlock email + .login-body + = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| + .devise-errors + = devise_error_messages! + .clearfix.append-bottom-20 + = f.email_field :email, class: 'form-control', placeholder: 'Email', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off' + .clearfix + = f.submit 'Resend unlock instructions', class: 'btn btn-success' + +.clearfix.prepend-top-20 + = render 'devise/shared/sign_in_link' diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 5fb43a86e1..2a09a4d373 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -121,14 +121,14 @@ Devise.setup do |config| config.lock_strategy = :failed_attempts # Defines which key will be used when locking and unlocking an account - # config.unlock_keys = [ :email ] + config.unlock_keys = [ :email ] # Defines which strategy will be used to unlock an account. # :email = Sends an unlock link to the user email # :time = Re-enables login after a certain amount of time (see :unlock_in below) # :both = Enables both strategies # :none = No unlock strategy. You should handle unlocking by yourself. - config.unlock_strategy = :time + config.unlock_strategy = :both # Number of authentication tries before locking an account if lock_strategy # is failed attempts. diff --git a/db/migrate/20151210030143_add_unlock_token_to_user.rb b/db/migrate/20151210030143_add_unlock_token_to_user.rb new file mode 100644 index 0000000000..0ea66ba65d --- /dev/null +++ b/db/migrate/20151210030143_add_unlock_token_to_user.rb @@ -0,0 +1,5 @@ +class AddUnlockTokenToUser < ActiveRecord::Migration + def change + add_column :users, :unlock_token, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 0167e30ff8..60b42f7a47 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -837,6 +837,7 @@ ActiveRecord::Schema.define(version: 20151210125932) do t.integer "consumed_timestep" t.integer "layout", default: 0 t.boolean "hide_project_limit", default: false + t.string "unlock_token" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index daa9d1087b..47889c04cf 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -26,6 +26,7 @@ # bio :string(255) # failed_attempts :integer default(0) # locked_at :datetime +# unlock_token :string(255) # username :string(255) # can_create_group :boolean default(TRUE), not null # can_create_team :boolean default(TRUE), not null From 2e5442817142651bc9ff16d8d431cb5c589ad9c2 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 15 Dec 2015 00:14:23 +0200 Subject: [PATCH 625/908] Clean up CONTRIBUTING.md [ci skip] * Use 80 chars width where possible * Remove easyfix label reference * Add new issue submission template * Add long links to the bottom of the page --- CONTRIBUTING.md | 345 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 243 insertions(+), 102 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c10b9f9807..ff86b90287 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,77 +1,99 @@ # Contribute to GitLab -Thank you for your interest in contributing to GitLab. -This guide details how contribute to GitLab in a way that is efficient for everyone. -If you have read this guide and want to know how the GitLab core-team operates please see [the GitLab contributing process](PROCESS.md). +Thank you for your interest in contributing to GitLab. This guide details how +to contribute to GitLab in a way that is efficient for everyone. GitLab comes into two flavors, GitLab Community Edition (CE) our free and open source edition, and GitLab Enterprise Edition (EE) which is our commercial edition. Throughout this guide you will see references to CE and EE for abbreviation. +If you have read this guide and want to know how the GitLab [core-team][] +operates please see [the GitLab contributing process](PROCESS.md). + ## Contributor license agreement -By submitting code as an individual you agree to the [individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md). By submitting code as an entity you agree to the [corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md). +By submitting code as an individual you agree to the +[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md). +By submitting code as an entity you agree to the +[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md). ## Security vulnerability disclosure -Please report suspected security vulnerabilities in private to support@gitlab.com, also see the [disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities. +Please report suspected security vulnerabilities in private to +`support@gitlab.com`, also see the +[disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/). +Please do **NOT** create publicly viewable issues for suspected security +vulnerabilities. ## Closing policy for issues and merge requests -GitLab is a popular open source project and the capacity to deal with issues and merge requests is limited. Out of respect for our volunteers, issues and merge requests not in line with the guidelines listed in this document may be closed without notice. +GitLab is a popular open source project and the capacity to deal with issues +and merge requests is limited. Out of respect for our volunteers, issues and +merge requests not in line with the guidelines listed in this document may be +closed without notice. -Please treat our volunteers with courtesy and respect, it will go a long way towards getting your issue resolved. +Please treat our volunteers with courtesy and respect, it will go a long way +towards getting your issue resolved. -Issues and merge requests should be in English and contain appropriate language for audiences of all ages. +Issues and merge requests should be in English and contain appropriate language +for audiences of all ages. ## Helping others -Please help other GitLab users when you can. -The channels people will reach out on can be found on the [getting help page](https://about.gitlab.com/getting-help/). -Sign up for the mailing list, answer GitLab questions on StackOverflow or respond in the IRC channel. -You can also sign up on [CodeTriage](http://www.codetriage.com/gitlabhq/gitlabhq) to help with one issue every day. +Please help other GitLab users when you can. The channels people will reach out +on can be found on the [getting help page][]. + +Sign up for the mailing list, answer GitLab questions on StackOverflow or +respond in the IRC channel. You can also sign up on [CodeTriage][] to help with +the remaining issues on the GitHub issue tracker. ## I want to contribute! If you want to contribute to GitLab, but are not sure where to start, -look for [issues](https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=up-for-grabs) -with the label `up-for-grabs`. -These issues will be of reasonable size and challenge, for anyone to start -contributing to GitLab. +look for [issues with the label `up-for-grabs`][up-for-grabs]. These issues +will be of reasonable size and challenge, for anyone to start contributing to +GitLab. -This was inspired by [an article by Kent C. Dodds](https://medium.com/@kentcdodds/first-timers-only-78281ea47455#.i2f363mx4). +This was inspired by [an article by Kent C. Dodds][medium-up-for-grabs]. ## Issue tracker -To get support for your particular problem please use the [getting help channels](https://about.gitlab.com/getting-help/). +To get support for your particular problem please use the +[getting help channels](https://about.gitlab.com/getting-help/). -The [GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues) is -for bugs in the latest GitLab release and feature proposals. +The [GitLab CE issue tracker on GitLab.com][ce-tracker] is for bugs concerning +the latest GitLab release and [feature proposals](#feature-proposals). -When submitting an issue please conform to the issue submission guidelines listed below. -Not all issues will be addressed and your issue is more likely to be addressed -if you submit a merge request which partially or fully addresses the issue. +When submitting an issue please conform to the issue submission guidelines +listed below. Not all issues will be addressed and your issue is more likely to +be addressed if you submit a merge request which partially or fully solves +the issue. -If you're unsure where to post, post to the [mailing list](https://groups.google.com/forum/#!forum/gitlabhq) -or [Stack Overflow](https://stackoverflow.com/questions/tagged/gitlab) first. -There are a lot of helpful GitLab users there who may be able to help you quickly. -If your particular issue turns out to be a bug, it will find its way from there. +If you're unsure where to post, post to the [mailing list][google-group] or +[Stack Overflow][stackoverflow] first. There are a lot of helpful GitLab users +there who may be able to help you quickly. If your particular issue turns out +to be a bug, it will find its way from there. -Please send a merge request with a tested solution or a merge request with a failing test instead of opening an issue if you can. +If it happens that you know the solution to an existing bug, please first +open the issue in order to keep track of it and then open the relevant merge +request that potentially fixes it. ### Feature proposals To create a feature proposal for CE and CI, open an issue on the -[issue tracker of CE](https://gitlab.com/gitlab-org/gitlab-ce/issues). +[issue tracker of CE][ce-tracker]. -For feature proposals for EE, open an issue on -[the issue tracker of EE](https://gitlab.com/gitlab-org/gitlab-ce/issues). +For feature proposals for EE, open an issue on the +[issue tracker of EE][ee-tracker]. -Add the label `feature proposal` to the issue. +In order to help track the feature proposals, we have created a +[`feature proposal`][fpl] label. For the time being, users that are not members +of the project cannot add labels. You can instead ask one of the [core team][] +members to add the label `feature proposal` to the issue. -Please keep feature proposals as small and simple as possible, -complex proposals might be edited to make them small and simple. +Please keep feature proposals as small and simple as possible, complex ones +might be edited to make them small and simple. For changes in the interface, it can be helpful to create a mockup first. If you want to create something yourself, consider opening an issue first to @@ -79,76 +101,143 @@ discuss whether it is interesting to include this in GitLab. ### Issue tracker guidelines -**[Search the issues](https://gitlab.com/gitlab-org/gitlab-ce/issues)** -for similar entries before submitting your own, -there's a good chance somebody else had the same issue or feature proposal. -Show your support with `:+1:` and/or join the discussion. +**[Search the issue tracker][ce-tracker]** for similar entries before +submitting your own, there's a good chance somebody else had the same issue or +feature proposal. Show your support with an award emoji and/or join the +discussion. -Please submit bugs in the following format (as the first post): +Please submit bugs using the following template in the issue description area. +The text in the parenthesis is there to help you with what to include. Omit it +when submitting the actual issue. You can copy-paste it and then edit as you +see fit. -1. **Summary:** Summarize your issue in one sentence (what goes wrong, what did you expect to happen) -1. **Steps to reproduce:** How can we reproduce the issue -1. **Expected behavior:** Describe your issue in detail -1. **Observed behavior** -1. **Relevant logs and/or screenshots:** Please use code blocks (\`\`\`) to format console output, logs, and code as it's very hard to read otherwise. -1. **Output of checks** - * Results of GitLab [Application Check](doc/install/installation.md#check-application-status) (For installations with omnibus-gitlab package: `sudo gitlab-rake gitlab:check SANITIZE=true`); For installations from source: `sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`); we will only investigate if the tests are passing - * Version of GitLab you are running; we will only investigate issues in the latest stable and development releases as per the [maintenance policy](MAINTENANCE.md) - * Add the last commit SHA-1 of the GitLab version you used to replicate the issue (obtainable from the help page) - * Describe your setup (use relevant parts from the env info: For installations with omnibus-gitlab package: `sudo gitlab-rake gitlab:env:info`; For installations from source: `sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`) -1. **Possible fixes**: If you can, link to the line of code that might be responsible for the problem +``` +## Summary + +(Summarize your issue in one sentence - what goes wrong, what did you expect to happen) + +## Steps to reproduce + +(How one can reproduce the issue - this is very important) + +## Expected behavior + +(What you should see instead) + +## Relevant logs and/or screenshots + +(Paste any relevant logs - please use code blocks (```) to format console output, +logs, and code as it's very hard to read otherwise.) + +## Output of checks + +### Results of GitLab Application Check + +(For installations with omnibus-gitlab package run and paste the output of: +sudo gitlab-rake gitlab:check SANITIZE=true) + +(For installations from source run and paste the output of: +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true) + +(we will only investigate if the tests are passing) + +### Results of GitLab Environment Info + +(For installations with omnibus-gitlab package run and paste the output of: +sudo gitlab-rake gitlab:env:info) + +(For installations from source run and paste the output of: +sudo -u git -H bundle exec rake gitlab:env:info) + +## Possible fixes + +(If you can, link to the line of code that might be responsible for the problem) + +``` ## Merge requests -We welcome merge requests with fixes and improvements to GitLab code, tests, and/or documentation. The features we would really like a merge request for are listed with the -[label 'accepting merge requests' on our issue tracker for CE](https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests) -and [EE](https://gitlab.com/gitlab-org/gitlab-ee/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests) -but other improvements are also welcome. -If you want to add a new feature that is not labeled it is best to first create a feedback issue (if there isn't one already) and leave a comment asking for it to be marked accepting merge requests. -Please include screenshots or wireframes if the feature will also change the UI. +We welcome merge requests with fixes and improvements to GitLab code, tests, +and/or documentation. The features we would really like a merge request for are +listed with the label [`Accepting Merge Requests` on our issue tracker for CE][accepting-mrs-ce] +and [EE][accepting-mrs-ee] but other improvements are also welcome. -Merge requests can be filed either at [gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests) or [github.com](https://github.com/gitlabhq/gitlabhq/pulls). +If you want to add a new feature that is not labeled it is best to first create +a feedback issue (if there isn't one already) and leave a comment asking for it +to be marked as `Accepting merge requests`. Please include screenshots or +wireframes if the feature will also change the UI. -If you are new to GitLab development (or web development in general), search for the label `easyfix` ([GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=easyfix), [GitHub](https://github.com/gitlabhq/gitlabhq/labels/easyfix)). Those are issues easy to fix, marked by the GitLab core-team. If you are unsure how to proceed but want to help, mention one of the core-team members to give you a hint. +Merge requests can be filed either at [GitLab.com][gitlab-mr-tracker] or at +[github.com][github-mr-tracker]. -To start with GitLab download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) and see [Development section](doc/development/README.md) in the help file. +If you are new to GitLab development (or web development in general), see the +[I want to contribute!](#i-want-to-contribute) section to get you started with +some potentially easy issues. + +To start with GitLab development download the [GitLab Development Kit][gdk] and +see the [Development section](doc/development/README.md) for some guidelines. ### Merge request guidelines -If you can, please submit a merge request with the fix or improvements including tests. If you don't know how to fix the issue but can write a test that exposes the issue we will accept that as well. In general bug fixes that include a regression test are merged quickly while new features without proper tests are least likely to receive timely feedback. The workflow to make a merge request is as follows: +If you can, please submit a merge request with the fix or improvements +including tests. If you don't know how to fix the issue but can write a test +that exposes the issue we will accept that as well. In general bug fixes that +include a regression test are merged quickly while new features without proper +tests are least likely to receive timely feedback. The workflow to make a merge +request is as follows: 1. Fork the project into your personal space on GitLab.com 1. Create a feature branch 1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code 1. Add your changes to the [CHANGELOG](CHANGELOG) -1. If you are changing the README, some documentation or other things which have no effect on the tests, add `[ci skip]` somewhere in the commit message -1. If you have multiple commits please combine them into one commit by [squashing them](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) -1. Push the commit to your fork +1. If you are changing the README, some documentation or other things which + have no effect on the tests, add `[ci skip]` somewhere in the commit message +1. If you have multiple commits please combine them into one commit by + [squashing them][git-squash] +1. Push the commit(s) to your fork 1. Submit a merge request (MR) to the master branch 1. The MR title should describe the change you want to make -1. The MR description should give a motive for your change and the method you used to achieve it +1. The MR description should give a motive for your change and the method you + used to achieve it 1. If the MR changes the UI it should include before and after screenshots -1. If the MR changes CSS classes please include the list of affected pages `grep css-class ./app -R` -1. Link relevant [issues](https://gitlab.com/gitlab-org/gitlab-ce/issues) -from the merge request description and leave a comment on them with a link back to the MR -1. Be prepared to answer questions and incorporate feedback even if requests for this arrive weeks or months after your MR submission -1. If your MR touches code that executes shell commands, make sure it adheres to the [shell command guidelines]( doc/development/shell_commands.md). -1. Also have a look at the [shell command guidelines](doc/development/shell_commands.md) if your code reads or opens files, or handles paths to files on disk. -1. If your code creates new files on disk please read the [shared files guidelines](doc/development/shared_files.md). +1. If the MR changes CSS classes please include the list of affected pages + `grep css-class ./app -R` +1. Link any relevant [issues][ce-tracker] in the merge request description and + leave a comment on them with a link back to the MR +1. Be prepared to answer questions and incorporate feedback even if requests + for this arrive weeks or months after your MR submission +1. If your MR touches code that executes shell commands, reads or opens files or + handles paths to files on disk, make sure it adheres to the + [shell command guidelines](doc/development/shell_commands.md) +1. If your code creates new files on disk please read the + [shared files guidelines](doc/development/shared_files.md). -The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. The best time to submit a MR and get feedback fast. -Before this time the GitLab B.V. team is still dealing with work that is created by the monthly release such as regressions requiring patch releases. -After the 7th it is already getting closer to the release date of the next version. This means there is less time to fix the issues created by merging large new features. +The **official merge window** is in the beginning of the month from the 1st to +the 7th day of the month. This is the best time to submit an MR and get +feedback fast. Before this time the GitLab Inc. team is still dealing with work +that is created by the monthly release such as regressions requiring patch +releases. After the 7th it is already getting closer to the release date of the +next version. This means there is less time to fix the issues created by +merging large new features. -Please keep the change in a single MR **as small as possible**. If you want to contribute a large feature think very hard what the minimum viable change is. Can you split functionality? Can you only submit the backend/API code? Can you start with a very simple UI? Can you do part of the refactor? The increased reviewability of small MR's that leads to higher code quality is more important to us than having a minimal commit log. The smaller a MR is the more likely it is it will be merged (quickly), after that you can send more MR's to enhance it. +Please keep the change in a single MR **as small as possible**. If you want to +contribute a large feature think very hard what the minimum viable change is. +Can you split the functionality? Can you only submit the backend/API code? Can +you start with a very simple UI? Can you do part of the refactor? The increased +reviewability of small MRs that leads to higher code quality is more important +to us than having a minimal commit log. The smaller an MR is the more likely it +is it will be merged (quickly). After that you can send more MRs to enhance it. -For examples of feedback on merge requests please look at already [closed merge requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed). If you would like quick feedback on your merge request feel free to mention one of the Merge Marshalls of [the core-team](https://about.gitlab.com/core-team/). Please ensure that your merge request meets the contribution acceptance criteria. +For examples of feedback on merge requests please look at already +[closed merge requests][]. If you would like quick feedback on your merge +request feel free to mention one of the Merge Marshalls of the [core team][]. +Please ensure that your merge request meets the contribution acceptance criteria. ## Definition of done -If you contribute to GitLab please know that changes involve more than just code. -We have the following [definition of done](http://guide.agilealliance.org/guide/definition-of-done.html). -Please ensure you support the feature you contribute through all of these steps. +If you contribute to GitLab please know that changes involve more than just +code. We have the following [definition of done][]. Please ensure you support +the feature you contribute through all of these steps. 1. Description explaining the relevancy (see following item) 1. Working and clean code that is commented where needed @@ -162,7 +251,9 @@ Please ensure you support the feature you contribute through all of these steps. 1. Community questions answered 1. Answers to questions radiated (in docs/wiki/etc.) -If you add a dependency in GitLab (such as an operating system package) please consider updating the following and note the applicability of each in your merge request: +If you add a dependency in GitLab (such as an operating system package) please +consider updating the following and note the applicability of each in your +merge request: 1. Note the addition in the release blog post (create one if it doesn't exist yet) https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/ 1. Upgrade guide, for example https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/7.5-to-7.6.md @@ -183,22 +274,31 @@ If you add a dependency in GitLab (such as an operating system package) please c ## Contribution acceptance criteria 1. The change is as small as possible (see the above paragraph for details) -1. Include proper tests and make all tests pass (unless it contains a test exposing a bug in existing code) -1. All tests have to pass, if you suspect a failing CI build is unrelated to your contribution ask for tests to be restarted. See [the CI setup document](http://doc.gitlab.com/ce/development/ci_setup.html) on who you can ask for test restart. -1. Initially contains a single commit (please use `git rebase -i` to squash commits) -1. Can merge without problems (if not please merge `master`, never rebase commits pushed to the remote server) +1. Include proper tests and make all tests pass (unless it contains a test + exposing a bug in existing code) +1. If you suspect a failing CI build is unrelated to your contribution, you may + try and restart the failing CI job or ask a developer to fix the + aforementioned failing test +1. Your MR initially contains a single commit (please use `git rebase -i` to + squash commits) +1. Your changes can merge without problems (if not please merge `master`, never + rebase commits pushed to the remote server) 1. Does not break any existing functionality -1. Fixes one specific issue or implements one specific feature (do not combine things, send separate merge requests if needed) -1. Migrations should do only one thing (eg: either create a table, move data to a new table or remove an old table) to aid retrying on failure +1. Fixes one specific issue or implements one specific feature (do not combine + things, send separate merge requests if needed) +1. Migrations should do only one thing (eg: either create a table, move data to + a new table or remove an old table) to aid retrying on failure 1. Keeps the GitLab code base clean and well structured 1. Contains functionality we think other users will benefit from too 1. Doesn't add configuration options since they complicate future changes -1. Changes after submitting the merge request should be in separate commits (no squashing). You will be asked to squash when the review is over, before merging. -1. It conforms to the following style guides. - If your change touches a line that does not follow the style, - modify the entire line to follow it. This prevents linting tools from generating warnings. - Don't touch neighbouring lines. As an exception, automatic mass refactoring modifications - may leave style non-compliant. +1. Changes after submitting the merge request should be in separate commits + (no squashing). If necessary, you will be asked to squash when the review is + over, before merging. +1. It conforms to the following style guides: + * If your change touches a line that does not follow the style, modify the + entire line to follow it. This prevents linting tools from generating warnings. + * Don't touch neighbouring lines. As an exception, automatic mass + refactoring modifications may leave style non-compliant. ## Style guides @@ -210,26 +310,67 @@ If you add a dependency in GitLab (such as an operating system package) please c 1. [Rails](https://github.com/bbatsov/rails-style-guide) 1. [Testing](https://github.com/thoughtbot/guides/tree/master/style#testing) 1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style#coffeescript) -1. [Shell commands](doc/development/shell_commands.md) created by GitLab contributors to enhance security +1. [Shell commands](doc/development/shell_commands.md) created by GitLab + contributors to enhance security 1. [Markdown](http://www.cirosantilli.com/markdown-styleguide) 1. [Database Migrations](doc/development/migration_style_guide.md) 1. [Documentation styleguide](doc_styleguide.md) -1. Interface text should be written subjectively instead of objectively. It should be the GitLab core team addressing a person. It should be written in present time and never use past tense (has been/was). For example instead of "prohibited this user from being saved due to the following errors:" the text should be "sorry, we could not create your account because:". Also these [excellent writing guidelines](https://github.com/NARKOZ/guides#writing). +1. Interface text should be written subjectively instead of objectively. It + should be the GitLab core team addressing a person. It should be written in + present time and never use past tense (has been/was). For example instead + of _prohibited this user from being saved due to the following errors:_ the + text should be _sorry, we could not create your account because:_ -This is also the style used by linting tools such as [RuboCop](https://github.com/bbatsov/rubocop), [PullReview](https://www.pullreview.com/) and [Hound CI](https://houndci.com). +This is also the style used by linting tools such as +[RuboCop](https://github.com/bbatsov/rubocop), +[PullReview](https://www.pullreview.com/) and [Hound CI](https://houndci.com). ## Code of conduct -As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. +As contributors and maintainers of this project, we pledge to respect all +people who contribute through reporting issues, posting feature requests, +updating documentation, submitting pull requests or patches, and other +activities. -We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. +We are committed to making participation in this project a harassment-free +experience for everyone, regardless of level of experience, gender, gender +identity and expression, sexual orientation, disability, personal appearance, +body size, race, ethnicity, age, or religion. -Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. +Examples of unacceptable behavior by participants include the use of sexual +language or imagery, derogatory comments or personal attacks, trolling, public +or private harassment, insults, or other unprofessional conduct. -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct. Project maintainers who do not +follow the Code of Conduct may be removed from the project team. -This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. +This code of conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. -Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by emailing contact@gitlab.com +Instances of abusive, harassing, or otherwise unacceptable behavior can be +reported by emailing contact@gitlab.com -This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/) +This Code of Conduct is adapted from the [Contributor Covenant][], version 1.1.0, +available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/). + +[core team]: https://about.gitlab.com/core-team/ +[getting help page]: https://about.gitlab.com/getting-help/ +[Codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq +[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up+for+grabs +[medium-up-for-grabs]: https://medium.com/@kentcdodds/first-timers-only-78281ea47455#.i2f363mx4 +[ce-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues +[ee-tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues +[google-group]: https://groups.google.com/forum/#!forum/gitlabhq +[stackoverflow]: https://stackoverflow.com/questions/tagged/gitlab +[fpl]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=feature+proposal +[accepting-mrs-ce]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests +[accepting-mrs-ee]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Accepting+Merge+Requests +[gitlab-mr-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests +[github-mr-tracker]: https://github.com/gitlabhq/gitlabhq/pulls +[gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit +[git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits +[closed merge requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed +[definition of done]: http://guide.agilealliance.org/guide/definition-of-done.html +[Contribution Covenant]: http://contributor-covenant.org From d1f1c5c60bfd58f966671d7895c1ef612e8f8897 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Mon, 14 Dec 2015 18:10:27 -0200 Subject: [PATCH 626/908] Updated .rubocop.yml to match 0.35.x changes --- .rubocop.yml | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index b4ca11c834..e92f10a791 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -76,7 +76,7 @@ Style/BlockEndNewline: Description: 'Put end statement of multiline block on its own line.' Enabled: true -Style/Blocks: +Style/BlockDelimiters: Description: >- Avoid using {...} for multi-line blocks (multiline chaining is always ugly). @@ -232,6 +232,10 @@ Style/EvenOdd: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' Enabled: false +Style/ExtraSpacing: + Description: 'Do not use unnecessary spacing.' + Enabled: false + Style/FileName: Description: 'Use snake_case for source file names.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files' @@ -431,6 +435,14 @@ Style/OpMethod: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg' Enabled: false +Style/ParallelAssignment: + Description: >- + Check for simple usages of parallel assignment. + It will only warn when the number of variables + matches on both sides of the assignment. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parallel-assignment' + Enabled: false + Style/ParenthesesAroundCondition: Description: >- Don't use parentheses around the condition of an @@ -669,6 +681,13 @@ Style/TrailingWhitespace: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace' Enabled: false +Style/TrailingUnderscoreVariable: + Description: >- + Checks for the usage of unneeded trailing underscores at the + end of parallel variable assignment. + AllowNamedUnderscoreVariables: true + Enabled: false + Style/TrivialAccessors: Description: 'Prefer attr_* methods to trivial readers/writers.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family' @@ -690,11 +709,6 @@ Style/UnneededPercentQ: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q' Enabled: false -Style/UnneededPercentX: - Description: 'Checks for %x when `` would do.' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-x' - Enabled: false - Style/VariableInterpolation: Description: >- Don't interpolate global, instance and class variables @@ -778,6 +792,10 @@ Metrics/MethodLength: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods' Enabled: false +Metrics/ModuleLength: + Description: 'Avoid modules longer than 100 lines of code.' + Enabled: false + #################### Lint ################################ ### Warnings @@ -987,6 +1005,12 @@ Rails/ScopeArgs: Description: 'Checks the arguments of ActiveRecord scopes.' Enabled: false +Rails/TimeZone: + Description: 'Checks the correct usage of time zone aware methods.' + StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time' + Reference: 'http://danilenko.org/2012/7/6/rails_timezones' + Enabled: false + Rails/Validation: Description: 'Use validates :attribute, hash of validations.' Enabled: false From 1c7128f34c10e558cac2080df7770b45863779d7 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 15 Dec 2015 00:34:22 +0200 Subject: [PATCH 627/908] Fix links [ci skip] --- CONTRIBUTING.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff86b90287..7ced7c5788 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -260,7 +260,7 @@ merge request: 1. Upgrader https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/upgrader.md#2-run-gitlab-upgrade-tool 1. Installation guide https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies 1. GitLab Development Kit https://gitlab.com/gitlab-org/gitlab-development-kit -1. Test suite https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/examples/configure_a_runner_to_run_the_gitlab_ce_test_suite.md +1. Test suite https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/prepare_build.sh 1. Omnibus package creator https://gitlab.com/gitlab-org/omnibus-gitlab ## Merge request description format @@ -303,13 +303,13 @@ merge request: ## Style guides 1. [Ruby](https://github.com/bbatsov/ruby-style-guide). - Important sections include [Source Code Layout](https://github.com/bbatsov/ruby-style-guide#source-code-layout) - and [Naming](https://github.com/bbatsov/ruby-style-guide#naming). Use: + Important sections include [Source Code Layout][rss-source] and + [Naming][rss-naming]. Use: - multi-line method chaining style **Option B**: dot `.` on previous line - string literal quoting style **Option A**: single quoted by default 1. [Rails](https://github.com/bbatsov/rails-style-guide) -1. [Testing](https://github.com/thoughtbot/guides/tree/master/style#testing) -1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style#coffeescript) +1. [Testing](https://github.com/thoughtbot/guides/tree/master/style/testing) +1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript) 1. [Shell commands](doc/development/shell_commands.md) created by GitLab contributors to enhance security 1. [Markdown](http://www.cirosantilli.com/markdown-styleguide) @@ -350,7 +350,7 @@ This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior can be -reported by emailing contact@gitlab.com +reported by emailing `contact@gitlab.com`. This Code of Conduct is adapted from the [Contributor Covenant][], version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/). @@ -359,7 +359,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [getting help page]: https://about.gitlab.com/getting-help/ [Codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq [up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up+for+grabs -[medium-up-for-grabs]: https://medium.com/@kentcdodds/first-timers-only-78281ea47455#.i2f363mx4 +[medium-up-for-grabs]: https://medium.com/@kentcdodds/first-timers-only-78281ea47455 [ce-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues [ee-tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues [google-group]: https://groups.google.com/forum/#!forum/gitlabhq @@ -373,4 +373,6 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits [closed merge requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed [definition of done]: http://guide.agilealliance.org/guide/definition-of-done.html -[Contribution Covenant]: http://contributor-covenant.org +[Contributor Covenant]: http://contributor-covenant.org +[rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout +[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming From b5291f95996743067bbec5a32f9c6cf0d34b36c7 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Tue, 15 Dec 2015 00:53:52 -0200 Subject: [PATCH 628/908] Fixed Rubocop offenses --- .../dashboard/snippets_controller.rb | 3 ++- app/controllers/projects/notes_controller.rb | 2 +- .../projects/protected_branches_controller.rb | 2 +- app/helpers/application_helper.rb | 2 +- app/helpers/external_wiki_helper.rb | 2 +- app/helpers/gitlab_markdown_helper.rb | 3 ++- app/helpers/projects_helper.rb | 7 +++--- app/helpers/tree_helper.rb | 2 +- app/mailers/notify.rb | 2 +- app/models/application_setting.rb | 12 +++++----- app/models/concerns/token_authenticatable.rb | 4 ++-- app/models/merge_request.rb | 4 +--- app/models/namespace.rb | 4 ++-- app/models/project.rb | 10 ++++----- app/models/project_services/bamboo_service.rb | 6 ++--- .../project_services/flowdock_service.rb | 2 +- .../project_services/gemnasium_service.rb | 2 +- .../project_services/teamcity_service.rb | 8 +++---- app/models/user.rb | 6 ++--- .../merge_requests/refresh_service.rb | 2 +- config/initializers/carrierwave.rb | 10 ++++----- config/routes.rb | 2 +- features/steps/explore/groups.rb | 8 +++---- features/steps/explore/projects.rb | 12 +++++----- features/steps/groups.rb | 2 +- features/steps/profile/profile.rb | 2 +- features/steps/project/project.rb | 2 +- features/steps/project/source/browse_files.rb | 12 +++++----- features/steps/shared/paths.rb | 8 +++---- lib/api/entities.rb | 2 +- lib/gitlab/backend/shell.rb | 2 +- .../bitbucket_import/project_creator.rb | 3 ++- lib/gitlab/diff/file.rb | 4 ++-- lib/gitlab/fogbugz_import/importer.rb | 2 +- lib/gitlab/fogbugz_import/project_creator.rb | 3 ++- lib/gitlab/gitlab_import/project_creator.rb | 3 ++- .../gitorious_import/project_creator.rb | 3 ++- lib/gitlab/google_code_import/importer.rb | 6 ++--- .../google_code_import/project_creator.rb | 3 ++- lib/gitlab/markdown/filter/markdown_filter.rb | 2 +- .../filter/table_of_contents_filter.rb | 2 +- lib/rouge/formatters/html_gitlab.rb | 2 +- spec/factories.rb | 3 ++- spec/features/security/group_access_spec.rb | 4 ++-- spec/helpers/groups_helper.rb | 2 +- spec/models/ci/commit_spec.rb | 4 ++-- spec/models/key_spec.rb | 2 +- .../project_services/hipchat_service_spec.rb | 22 +++++++++---------- .../slack_service/note_message_spec.rb | 8 +++---- spec/models/user_spec.rb | 4 ++-- spec/requests/api/merge_requests_spec.rb | 2 +- spec/requests/api/services_spec.rb | 2 +- .../create_commit_builds_service_spec.rb | 22 +++++++++---------- spec/services/git_tag_push_service_spec.rb | 16 +++++++------- spec/services/update_snippet_service_spec.rb | 2 +- spec/support/repo_helpers.rb | 12 +++++----- spec/workers/repository_fork_worker_spec.rb | 20 +++++++++-------- spec/workers/stuck_ci_builds_worker_spec.rb | 4 ++-- 58 files changed, 155 insertions(+), 154 deletions(-) diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb index f4354c6d8c..b3594d8253 100644 --- a/app/controllers/dashboard/snippets_controller.rb +++ b/app/controllers/dashboard/snippets_controller.rb @@ -1,6 +1,7 @@ class Dashboard::SnippetsController < Dashboard::ApplicationController def index - @snippets = SnippetsFinder.new.execute(current_user, + @snippets = SnippetsFinder.new.execute( + current_user, filter: :by_user, user: current_user, scope: params[:scope] diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 88b949a27a..4f1fddb458 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -68,7 +68,7 @@ class Projects::NotesController < Projects::ApplicationController data = { author: current_user, is_award: true, - note: note_params[:note].gsub(":", '') + note: note_params[:note].delete(":") } note = noteable.notes.find_by(data) diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index 6b52eccebf..e49259c34b 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -21,7 +21,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController if protected_branch && protected_branch.update_attributes( - developers_can_push: params[:developers_can_push] + developers_can_push: params[:developers_can_push] ) respond_to do |format| diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 21f962df20..e3bafce691 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -61,7 +61,7 @@ module ApplicationHelper options[:class] ||= '' options[:class] << ' identicon' bg_key = project.id % 7 - style = "background-color: ##{ allowed_colors.values[bg_key] }; color: #555" + style = "background-color: ##{allowed_colors.values[bg_key]}; color: #555" content_tag(:div, class: options[:class], style: style) do project.name[0, 1].upcase diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb index 838b85afdf..1f3401f290 100644 --- a/app/helpers/external_wiki_helper.rb +++ b/app/helpers/external_wiki_helper.rb @@ -1,7 +1,7 @@ module ExternalWikiHelper def get_project_wiki_path(project) external_wiki_service = project.services. - select { |service| service.to_param == 'external_wiki' }.first + find { |service| service.to_param == 'external_wiki' } if external_wiki_service.present? && external_wiki_service.active? external_wiki_service.properties['external_wiki_url'] else diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 5004e02ea0..5d15b3513c 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -65,7 +65,8 @@ module GitlabMarkdownHelper end def asciidoc(text) - Gitlab::Asciidoc.render(text, + Gitlab::Asciidoc.render( + text, project: @project, current_user: (current_user if defined?(current_user)), diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index d061136b7b..777817e24a 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -330,10 +330,9 @@ module ProjectsHelper def filename_path(project, filename) if project && blob = project.repository.send(filename) namespace_project_blob_path( - project.namespace, - project, - tree_join(project.default_branch, - blob.name) + project.namespace, + project, + tree_join(project.default_branch, blob.name) ) end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 886a1e734b..f448dd0ab6 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -79,7 +79,7 @@ module TreeHelper part_path = File.join(part_path, part) unless part_path.empty? part_path = part if part_path.empty? - next unless parts.last(2).include?(part) if parts.count > max_links + next if parts.count > max_links && !parts.last(2).include?(part) yield(part, tree_join(@ref, part_path)) end end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 9beb0de68f..3bbdd9cee7 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -17,7 +17,7 @@ class Notify < BaseMailer subject: subject, body: body.html_safe, content_type: 'text/html' - ) + ) end # Splits "gitlab.corp.company.com" up into "gitlab.corp.company.com", diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index faa0bdf840..1f4e8b3ef2 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -126,12 +126,12 @@ class ApplicationSetting < ActiveRecord::Base def restricted_signup_domains_raw=(values) self.restricted_signup_domains = [] self.restricted_signup_domains = values.split( - /\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace - | # or - \s # any whitespace character - | # or - [\r\n] # any number of newline characters - /x) + /\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace + | # or + \s # any whitespace character + | # or + [\r\n] # any number of newline characters + /x) self.restricted_signup_domains.reject! { |d| d.empty? } end end diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 56d38fe825..488ff8c31b 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -13,7 +13,7 @@ module TokenAuthenticatable @token_fields << token_field define_singleton_method("find_by_#{token_field}") do |token| - where(token_field => token).first if token + find_by(token_field => token) if token end define_method("ensure_#{token_field}") do @@ -37,7 +37,7 @@ module TokenAuthenticatable def generate_token_for(token_field) loop do token = Devise.friendly_token - break token unless self.class.unscoped.where(token_field => token).first + break token unless self.class.unscoped.find_by(token_field => token) end end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f6f77a1626..d7430d36c4 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -194,9 +194,7 @@ class MergeRequest < ActiveRecord::Base similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id if similar_mrs.any? errors.add :validate_branches, - "Cannot Create: This merge request already exists: #{ - similar_mrs.pluck(:title) - }" + "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}" end end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 1c4e101cc1..adafabbec0 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -45,7 +45,7 @@ class Namespace < ActiveRecord::Base class << self def by_path(path) - where('lower(path) = :value', value: path.downcase).first + find_by('lower(path) = :value', value: path.downcase) end # Case insensetive search for namespace by path or name @@ -148,6 +148,6 @@ class Namespace < ActiveRecord::Base end def find_fork_of(project) - projects.joins(:forked_project_link).where('forked_project_links.forked_from_project_id = ?', project.id).first + projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id) end end diff --git a/app/models/project.rb b/app/models/project.rb index e1f7bf971e..87116451ca 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -265,7 +265,7 @@ class Project < ActiveRecord::Base joins(:namespace). iwhere('namespaces.path' => namespace_path) - projects.where('projects.path' => project_path).take || + projects.find_by('projects.path' => project_path) || projects.iwhere('projects.path' => project_path).take end @@ -450,7 +450,7 @@ class Project < ActiveRecord::Base end def external_issue_tracker - @external_issues_tracker ||= external_issues_trackers.select(&:activated?).first + @external_issues_tracker ||= external_issues_trackers.find(&:activated?) end def can_have_issues_tracker_id? @@ -496,7 +496,7 @@ class Project < ActiveRecord::Base end def ci_service - @ci_service ||= ci_services.select(&:activated?).first + @ci_service ||= ci_services.find(&:activated?) end def avatar_type @@ -547,7 +547,7 @@ class Project < ActiveRecord::Base end def project_member_by_name_or_email(name = nil, email = nil) - user = users.where('name like ? or email like ?', name, email).first + user = users.find_by('name like ? or email like ?', name, email) project_members.where(user: user) if user end @@ -722,7 +722,7 @@ class Project < ActiveRecord::Base end def project_member(user) - project_members.where(user_id: user).first + project_members.find_by(user_id: user) end def default_branch diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 0a61ad96a0..73f99b229f 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -27,12 +27,10 @@ class BambooService < CiService validates :build_key, presence: true, if: :activated? validates :username, presence: true, - if: ->(service) { service.password? }, - if: :activated? + if: ->(service) { service.activated? && service.password? } validates :password, presence: true, - if: ->(service) { service.username? }, - if: :activated? + if: ->(service) { service.activated? && service.username? } attr_accessor :response diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 27fc19379f..15c7c907f7 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -58,6 +58,6 @@ class FlowdockService < Service repo_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}", commit_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/%s", diff_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/compare/%s...%s", - ) + ) end end diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 91ef267ad7..202fee042e 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -57,6 +57,6 @@ class GemnasiumService < Service token: token, api_key: api_key, repo: project.repository.path_to_repo - ) + ) end end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 29d4236745..a2d55190de 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -27,12 +27,10 @@ class TeamcityService < CiService validates :build_type, presence: true, if: :activated? validates :username, presence: true, - if: ->(service) { service.password? }, - if: :activated? + if: ->(service) { service.activated? && service.password? } validates :password, presence: true, - if: ->(service) { service.username? }, - if: :activated? + if: ->(service) { service.activated? && service.username? } attr_accessor :response @@ -147,6 +145,6 @@ class TeamcityService < CiService '', headers: { 'Content-type' => 'application/xml' }, basic_auth: auth - ) + ) end end diff --git a/app/models/user.rb b/app/models/user.rb index fdd14f4571..e0ce091c54 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -220,9 +220,9 @@ class User < ActiveRecord::Base def find_for_database_authentication(warden_conditions) conditions = warden_conditions.dup if login = conditions.delete(:login) - where(conditions).where(["lower(username) = :value OR lower(email) = :value", { value: login.downcase }]).first + where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase) else - where(conditions).first + find_by(conditions) end end @@ -285,7 +285,7 @@ class User < ActiveRecord::Base end def by_username_or_id(name_or_id) - where('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i).first + find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i) end def build_user(attrs = {}) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index b26c7513f5..8b3d56c2b4 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -112,7 +112,7 @@ module MergeRequests merge_requests_for_source_branch.each do |merge_request| SystemNoteService.change_branch_presence( - merge_request, merge_request.project, @current_user, + merge_request, merge_request.project, @current_user, :source, @branch_name, presence) end end diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index bfb8656df5..df28d30d75 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -31,11 +31,11 @@ if File.exists?(aws_file) if Rails.env.test? Fog.mock! connection = ::Fog::Storage.new( - aws_access_key_id: AWS_CONFIG['access_key_id'], - aws_secret_access_key: AWS_CONFIG['secret_access_key'], - provider: 'AWS', - region: AWS_CONFIG['region'] - ) + aws_access_key_id: AWS_CONFIG['access_key_id'], + aws_secret_access_key: AWS_CONFIG['secret_access_key'], + provider: 'AWS', + region: AWS_CONFIG['region'] + ) connection.directories.create(key: AWS_CONFIG['bucket']) end end diff --git a/config/routes.rb b/config/routes.rb index e2d4fcb65a..57be57e325 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -441,7 +441,7 @@ Rails.application.routes.draw do scope do post( - '/create_dir/*id', + '/create_dir/*id', to: 'tree#create_dir', constraints: { id: /.+/ }, as: 'create_dir' diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb index 87cd33c37e..87f32e70d5 100644 --- a/features/steps/explore/groups.rb +++ b/features/steps/explore/groups.rb @@ -75,18 +75,18 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps name: projectname, path: "#{groupname}-#{projectname}", visibility_level: visibility_level - ) + ) create(:issue, title: "#{projectname} feature", project: project - ) + ) create(:merge_request, title: "#{projectname} feature implemented", source_project: project, target_project: project - ) + ) create(:closed_issue_event, project: project - ) + ) end end diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb index f819dec219..742ba5d71f 100644 --- a/features/steps/explore/projects.rb +++ b/features/steps/explore/projects.rb @@ -61,11 +61,11 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps create(:issue, title: "Bug", project: public_project - ) + ) create(:issue, title: "New feature", project: public_project - ) + ) visit namespace_project_issues_path(public_project.namespace, public_project) end @@ -80,11 +80,11 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps create(:issue, title: "Internal Bug", project: internal_project - ) + ) create(:issue, title: "New internal feature", project: internal_project - ) + ) visit namespace_project_issues_path(internal_project.namespace, internal_project) end @@ -104,7 +104,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps title: "Bug fix for public project", source_project: public_project, target_project: public_project, - ) + ) end step 'I should see list of merge requests for "Community" project' do @@ -121,7 +121,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps title: "Feature implemented", source_project: internal_project, target_project: internal_project - ) + ) end step 'I should see list of merge requests for "Internal" project' do diff --git a/features/steps/groups.rb b/features/steps/groups.rb index f5e3fee61c..4c5122d1b7 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -85,7 +85,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps step 'I should see new group "Owned" avatar' do expect(owned_group.avatar).to be_instance_of AvatarUploader - expect(owned_group.avatar.url).to eq "/uploads/group/avatar/#{ Group.find_by(name:"Owned").id }/banana_sample.gif" + expect(owned_group.avatar.url).to eq "/uploads/group/avatar/#{Group.find_by(name:"Owned").id}/banana_sample.gif" end step 'I should see the "Remove avatar" button' do diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 40b2aa7c35..0305f7e6da 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -34,7 +34,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps step 'I should see new avatar' do expect(@user.avatar).to be_instance_of AvatarUploader - expect(@user.avatar.url).to eq "/uploads/user/avatar/#{ @user.id }/banana_sample.gif" + expect(@user.avatar.url).to eq "/uploads/user/avatar/#{@user.id}/banana_sample.gif" end step 'I should see the "Remove avatar" button' do diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 9ca7c8ebbc..37bf52b4a9 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -37,7 +37,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps step 'I should see new project avatar' do expect(@project.avatar).to be_instance_of AvatarUploader url = @project.avatar.url - expect(url).to eq "/uploads/project/avatar/#{ @project.id }/banana_sample.gif" + expect(url).to eq "/uploads/project/avatar/#{@project.id}/banana_sample.gif" end step 'I should see the "Remove avatar" button' do diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index f2b9576426..f94661c12a 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -230,13 +230,13 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I am redirected to the new file' do - expect(current_path).to eq(namespace_project_blob_path( - @project.namespace, @project, 'master/' + new_file_name)) + expect(current_path).to eq( + namespace_project_blob_path(@project.namespace, @project, 'master/' + new_file_name)) end step 'I am redirected to the new file with directory' do - expect(current_path).to eq(namespace_project_blob_path( - @project.namespace, @project, 'master/' + new_file_name_with_directory)) + expect(current_path).to eq( + namespace_project_blob_path(@project.namespace, @project, 'master/' + new_file_name_with_directory)) end step 'I am redirected to the new merge request page' do @@ -244,8 +244,8 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I am redirected to the root directory' do - expect(current_path).to eq(namespace_project_tree_path( - @project.namespace, @project, 'master/')) + expect(current_path).to eq( + namespace_project_tree_path(@project.namespace, @project, 'master/')) end step "I don't see the permalink link" do diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index c74a5fd3bc..b33bd33265 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -212,8 +212,8 @@ module SharedPaths end step 'I visit a binary file in the repo' do - visit namespace_project_blob_path(@project.namespace, @project, File.join( - root_ref, 'files/images/logo-black.png')) + visit namespace_project_blob_path(@project.namespace, @project, + File.join(root_ref, 'files/images/logo-black.png')) end step "I visit my project's commits page" do @@ -316,8 +316,8 @@ module SharedPaths end step 'I am on the ".gitignore" edit file page' do - expect(current_path).to eq(namespace_project_edit_blob_path( - @project.namespace, @project, File.join(root_ref, '.gitignore'))) + expect(current_path).to eq( + namespace_project_edit_blob_path(@project.namespace, @project, File.join(root_ref, '.gitignore'))) end step 'I visit project source page for "6d39438"' do diff --git a/lib/api/entities.rb b/lib/api/entities.rb index b1cd80bdf6..a5daa45faf 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -67,7 +67,7 @@ module API expose :shared_runners_enabled expose :creator_id expose :namespace - expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? } + expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ |project, options| project.forked? } expose :avatar_url expose :star_count, :forks_count end diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 87ac30b5ff..459e3d6bcd 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -2,7 +2,7 @@ module Gitlab class Shell class Error < StandardError; end - class KeyAdder < Struct.new(:io) + KeyAdder = Struct.new(:io) do def add_key(id, key) key.gsub!(/[[:space:]]+/, ' ').strip! io.puts("#{id}\t#{key}") diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb index 35e34d033e..03aac1a025 100644 --- a/lib/gitlab/bitbucket_import/project_creator.rb +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -11,7 +11,8 @@ module Gitlab end def execute - project = ::Projects::CreateService.new(current_user, + project = ::Projects::CreateService.new( + current_user, name: repo["name"], path: repo["slug"], description: repo["description"], diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 142058aa69..79061cd014 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -46,11 +46,11 @@ module Gitlab end def added_lines - diff_lines.select(&:added?).size + diff_lines.count(&:added?) end def removed_lines - diff_lines.select(&:removed?).size + diff_lines.count(&:removed?) end end end diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb index 496256700b..403ebeec47 100644 --- a/lib/gitlab/fogbugz_import/importer.rb +++ b/lib/gitlab/fogbugz_import/importer.rb @@ -199,7 +199,7 @@ module Gitlab s = s.gsub(/^#/, "\\#") s = s.gsub(/^-/, "\\-") s = s.gsub("`", "\\~") - s = s.gsub("\r", "") + s = s.delete("\r") s = s.gsub("\n", " \n") s end diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb index 8b1b6f48ed..e0163499e3 100644 --- a/lib/gitlab/fogbugz_import/project_creator.rb +++ b/lib/gitlab/fogbugz_import/project_creator.rb @@ -12,7 +12,8 @@ module Gitlab end def execute - project = ::Projects::CreateService.new(current_user, + project = ::Projects::CreateService.new( + current_user, name: repo.safe_name, path: repo.path, namespace: namespace, diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb index d9452de6a5..7baaadb813 100644 --- a/lib/gitlab/gitlab_import/project_creator.rb +++ b/lib/gitlab/gitlab_import/project_creator.rb @@ -11,7 +11,8 @@ module Gitlab end def execute - project = ::Projects::CreateService.new(current_user, + project = ::Projects::CreateService.new( + current_user, name: repo["name"], path: repo["path"], description: repo["description"], diff --git a/lib/gitlab/gitorious_import/project_creator.rb b/lib/gitlab/gitorious_import/project_creator.rb index cc9a91c91f..8e22aa9286 100644 --- a/lib/gitlab/gitorious_import/project_creator.rb +++ b/lib/gitlab/gitorious_import/project_creator.rb @@ -10,7 +10,8 @@ module Gitlab end def execute - ::Projects::CreateService.new(current_user, + ::Projects::CreateService.new( + current_user, name: repo.name, path: repo.path, description: repo.description, diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb index 87fee28dc0..62da327931 100644 --- a/lib/gitlab/google_code_import/importer.rb +++ b/lib/gitlab/google_code_import/importer.rb @@ -171,8 +171,6 @@ module Gitlab when /\AMilestone:/ "#fee3ff" - when *@closed_statuses.map { |s| nice_status_name(s) } - "#cfcfcf" when "Status: New" "#428bca" when "Status: Accepted" @@ -199,6 +197,8 @@ module Gitlab "#8e44ad" when "Type: Other" "#7f8c8d" + when *@closed_statuses.map { |s| nice_status_name(s) } + "#cfcfcf" else "#e2e2e2" end @@ -227,7 +227,7 @@ module Gitlab s = s.gsub("`", "\\`") # Carriage returns make me sad - s = s.gsub("\r", "") + s = s.delete("\r") # Markdown ignores single newlines, but we need them as
. s = s.gsub("\n", " \n") diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb index 1cb7d16aeb..87821c2346 100644 --- a/lib/gitlab/google_code_import/project_creator.rb +++ b/lib/gitlab/google_code_import/project_creator.rb @@ -11,7 +11,8 @@ module Gitlab end def execute - project = ::Projects::CreateService.new(current_user, + project = ::Projects::CreateService.new( + current_user, name: repo.name, path: repo.name, description: repo.summary, diff --git a/lib/gitlab/markdown/filter/markdown_filter.rb b/lib/gitlab/markdown/filter/markdown_filter.rb index 921e2a0794..b1b974fcc7 100644 --- a/lib/gitlab/markdown/filter/markdown_filter.rb +++ b/lib/gitlab/markdown/filter/markdown_filter.rb @@ -3,7 +3,7 @@ module Gitlab class MarkdownFilter < HTML::Pipeline::TextFilter def initialize(text, context = nil, result = nil) super text, context, result - @text = @text.gsub "\r", '' + @text = @text.delete "\r" end def call diff --git a/lib/gitlab/markdown/filter/table_of_contents_filter.rb b/lib/gitlab/markdown/filter/table_of_contents_filter.rb index bbb3bf7fc8..6be644b0f6 100644 --- a/lib/gitlab/markdown/filter/table_of_contents_filter.rb +++ b/lib/gitlab/markdown/filter/table_of_contents_filter.rb @@ -31,7 +31,7 @@ module Gitlab id = text.downcase id.gsub!(PUNCTUATION_REGEXP, '') # remove punctuation - id.gsub!(' ', '-') # replace spaces with dash + id.tr!(' ', '-') # replace spaces with dash id.squeeze!('-') # replace multiple dashes with one uniq = (headers[id] > 0) ? "-#{headers[id]}" : '' diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb index 6762ca47c3..8c309efc7b 100644 --- a/lib/rouge/formatters/html_gitlab.rb +++ b/lib/rouge/formatters/html_gitlab.rb @@ -39,7 +39,7 @@ module Rouge lineanchorsid: 'L', anchorlinenos: false, inline_theme: nil - ) + ) @nowrap = nowrap @cssclass = cssclass @linenos = linenos diff --git a/spec/factories.rb b/spec/factories.rb index 4bf93adabe..d6b4efa9a0 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -43,7 +43,8 @@ FactoryGirl.define do end after(:create) do |user, evaluator| - user.identities << create(:identity, + user.identities << create( + :identity, provider: evaluator.provider, extern_uid: evaluator.extern_uid ) diff --git a/spec/features/security/group_access_spec.rb b/spec/features/security/group_access_spec.rb index 4b78e3a61f..65f8073c69 100644 --- a/spec/features/security/group_access_spec.rb +++ b/spec/features/security/group_access_spec.rb @@ -16,11 +16,11 @@ describe 'Group access', feature: true do end end - def group_member(access_level, group = group) + def group_member(access_level, grp = group()) level = Object.const_get("Gitlab::Access::#{access_level.upcase}") create(:user).tap do |user| - group.add_user(user, level) + grp.add_user(user, level) end end diff --git a/spec/helpers/groups_helper.rb b/spec/helpers/groups_helper.rb index 5d17446068..4ea90a80a9 100644 --- a/spec/helpers/groups_helper.rb +++ b/spec/helpers/groups_helper.rb @@ -9,7 +9,7 @@ describe GroupsHelper do group.avatar = File.open(avatar_file_path) group.save! expect(group_icon(group.path).to_s). - to match("/uploads/group/avatar/#{ group.id }/banana_sample.gif") + to match("/uploads/group/avatar/#{group.id}/banana_sample.gif") end it 'should give default avatar_icon when no avatar is present' do diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index ac61c8fb52..b193e16e7f 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -37,14 +37,14 @@ describe Ci::Commit, models: true do it 'returns ordered list of commits' do commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project - commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: project + commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project expect(project.ci_commits.ordered).to eq([commit2, commit1]) end it 'returns commits ordered by committed_at and id, with nulls last' do commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project commit2 = FactoryGirl.create :ci_commit, committed_at: nil, project: project - commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: project + commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project commit4 = FactoryGirl.create :ci_commit, committed_at: nil, project: project expect(project.ci_commits.ordered).to eq([commit2, commit4, commit3, commit1]) end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index d7fe01976d..c962b83644 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -81,7 +81,7 @@ describe Key, models: true do it 'rejects the multiple line key' do key = build(:key) - key.key.gsub!(' ', "\n") + key.key.tr!(' ', "\n") expect(key).not_to be_valid end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index a5662b08bd..91dd92b7c6 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -57,23 +57,21 @@ describe HipchatService, models: true do it 'should use v1 if version is provided' do allow(hipchat).to receive(:api_version).and_return('v1') - expect(HipChat::Client).to receive(:new). - with(token, - api_version: 'v1', - server_url: server_url). - and_return( - double(:hipchat_service).as_null_object) + expect(HipChat::Client).to receive(:new).with( + token, + api_version: 'v1', + server_url: server_url + ).and_return(double(:hipchat_service).as_null_object) hipchat.execute(push_sample_data) end it 'should use v2 as the version when nothing is provided' do allow(hipchat).to receive(:api_version).and_return('') - expect(HipChat::Client).to receive(:new). - with(token, - api_version: 'v2', - server_url: server_url). - and_return( - double(:hipchat_service).as_null_object) + expect(HipChat::Client).to receive(:new).with( + token, + api_version: 'v2', + server_url: server_url + ).and_return(double(:hipchat_service).as_null_object) hipchat.execute(push_sample_data) end diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb index ebf8837570..06006b9a4f 100644 --- a/spec/models/project_services/slack_service/note_message_spec.rb +++ b/spec/models/project_services/slack_service/note_message_spec.rb @@ -89,10 +89,10 @@ describe SlackService::NoteMessage, models: true do it 'returns a message regarding notes on an issue' do message = SlackService::NoteMessage.new(@args) expect(message.pretext).to eq( - "Test User commented on " \ - " in : " \ - "*issue title*") - expected_attachments = [ + "Test User commented on " \ + " in : " \ + "*issue title*") + expected_attachments = [ { text: "comment on an issue", color: color, diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index daa9d1087b..376266c095 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -462,8 +462,8 @@ describe User, models: true do expect(User.search(user1.username.downcase).to_a).to eq([user1]) expect(User.search(user2.username.upcase).to_a).to eq([user2]) expect(User.search(user2.username.downcase).to_a).to eq([user2]) - expect(User.search(user1.username.downcase).to_a.count).to eq(2) - expect(User.search(user2.username.downcase).to_a.count).to eq(1) + expect(User.search(user1.username.downcase).to_a.size).to eq(2) + expect(User.search(user2.username.downcase).to_a.size).to eq(1) end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index a91fa73532..e194eb93cf 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -6,7 +6,7 @@ describe API::API, api: true do let(:user) { create(:user) } let!(:project) {create(:project, creator_id: user.id, namespace: user.namespace) } let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) } - let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.seconds) } + let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index b180d2fec7..fed9ae1949 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -29,7 +29,7 @@ describe API::API, api: true do if required_attributes.empty? expected_code = 200 else - attrs.delete(required_attributes.shuffle.first) + attrs.delete(required_attributes.sample) expected_code = 400 end diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb index 798c480b81..40ba6549e1 100644 --- a/spec/services/create_commit_builds_service_spec.rb +++ b/spec/services/create_commit_builds_service_spec.rb @@ -17,7 +17,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: [{ message: "Message" }] - ) + ) end it { expect(commit).to be_kind_of(Ci::Commit) } @@ -34,7 +34,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: [{ message: "Message" }] - ) + ) expect(result).to be_persisted end @@ -47,7 +47,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: [{ message: "Message" }] - ) + ) expect(result).to be_persisted end end @@ -59,7 +59,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: [{ message: 'Message' }] - ) + ) expect(result).to be_persisted expect(result.builds.any?).to be_falsey expect(result.status).to eq('skipped') @@ -76,7 +76,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) expect(commit.builds.any?).to be false expect(commit.status).to eq('failed') expect(commit.yaml_errors).to_not be_nil @@ -96,7 +96,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) expect(commit.builds.any?).to be false expect(commit.status).to eq("skipped") end @@ -110,7 +110,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) expect(commit.builds.first.name).to eq("staging") end @@ -123,7 +123,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) expect(commit.builds.any?).to be false expect(commit.status).to eq("skipped") expect(commit.yaml_errors).to be_nil @@ -139,7 +139,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) expect(commit.builds.count(:all)).to eq(2) commit = service.execute(project, user, @@ -147,7 +147,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) expect(commit.builds.count(:all)).to eq(2) end @@ -161,7 +161,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) expect(commit.status).to eq("failed") expect(commit.builds.any?).to be false diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index e2d15f1a83..b982274c52 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -58,14 +58,14 @@ describe GitTagPushService, services: true do it { is_expected.to include(timestamp: @commit.date.xmlschema) } it do is_expected.to include( - url: [ - Gitlab.config.gitlab.url, - project.namespace.to_param, - project.to_param, - 'commit', - @commit.id - ].join('/') - ) + url: [ + Gitlab.config.gitlab.url, + project.namespace.to_param, + project.to_param, + 'commit', + @commit.id + ].join('/') + ) end context "with a author" do diff --git a/spec/services/update_snippet_service_spec.rb b/spec/services/update_snippet_service_spec.rb index 124bb76e67..48d114896d 100644 --- a/spec/services/update_snippet_service_spec.rb +++ b/spec/services/update_snippet_service_spec.rb @@ -42,7 +42,7 @@ describe UpdateSnippetService, services: true do CreateSnippetService.new(project, user, opts).execute end - def update_snippet(project = nil, user, snippet, opts) + def update_snippet(project, user, snippet, opts) UpdateSnippetService.new(project, user, snippet, opts).execute end end diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb index aadf791bf3..aa8258d6da 100644 --- a/spec/support/repo_helpers.rb +++ b/spec/support/repo_helpers.rb @@ -45,12 +45,12 @@ eos def another_sample_commit OpenStruct.new( - id: "e56497bb5f03a90a51293fc6d516788730953899", - parent_id: '4cd80ccab63c82b4bad16faa5193fbd2aa06df40', - author_full_name: "Sytse Sijbrandij", - author_email: "sytse@gitlab.com", - files_changed_count: 1, - message: < Date: Tue, 15 Dec 2015 00:54:26 -0200 Subject: [PATCH 629/908] Disabled Rails/Date cop --- .rubocop.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index e92f10a791..89aa0591c3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -979,6 +979,12 @@ Rails/ActionFilter: Description: 'Enforces consistent use of action filter methods.' Enabled: true +Rails/Date: + Description: >- + Checks the correct usage of date aware methods, + such as Date.today, Date.current etc. + Enabled: false + Rails/DefaultScope: Description: 'Checks if the argument passed to default_scope is a block.' Enabled: false From 8e3f1fa629a61741282214b293c1bc9438aada59 Mon Sep 17 00:00:00 2001 From: tduehr Date: Wed, 11 Nov 2015 22:25:31 -0600 Subject: [PATCH 630/908] add CAS authentication support --- CHANGELOG | 1 + Gemfile | 1 + Gemfile.lock | 5 ++ app/controllers/application_controller.rb | 15 +++++ .../omniauth_callbacks_controller.rb | 16 ++++- config/gitlab.yml.example | 13 ++++ config/initializers/1_settings.rb | 4 ++ config/initializers/devise.rb | 10 +++ doc/integration/cas.md | 62 +++++++++++++++++++ lib/gitlab/o_auth/session.rb | 17 +++++ 10 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 doc/integration/cas.md create mode 100644 lib/gitlab/o_auth/session.rb diff --git a/CHANGELOG b/CHANGELOG index 7f9dfd98cd..0089b2d8b4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -66,6 +66,7 @@ v 8.2.3 v 8.2.3 - Webhook payload has an added, modified and removed properties for each commit - Fix 500 error when creating a merge request that removes a submodule + - Add CAS support (tduehr) v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) diff --git a/Gemfile b/Gemfile index 7298e21ce6..fc6638b751 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ gem 'devise-async', '~> 0.9.0' gem 'doorkeeper', '~> 2.2.0' gem 'omniauth', '~> 1.2.2' gem 'omniauth-bitbucket', '~> 0.0.2' +gem 'omniauth-cas3', '~> 1.1.2' gem 'omniauth-facebook', '~> 3.0.0' gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-gitlab', '~> 1.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index ff57460f5b..2f06713e5d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -458,6 +458,10 @@ GEM multi_json (~> 1.7) omniauth (~> 1.1) omniauth-oauth (~> 1.0) + omniauth-cas3 (1.1.3) + addressable (~> 2.3) + nokogiri (~> 1.6.6) + omniauth (~> 1.2) omniauth-facebook (3.0.0) omniauth-oauth2 (~> 1.2) omniauth-github (1.1.2) @@ -913,6 +917,7 @@ DEPENDENCIES octokit (~> 3.7.0) omniauth (~> 1.2.2) omniauth-bitbucket (~> 0.0.2) + omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 3.0.0) omniauth-github (~> 1.1.1) omniauth-gitlab (~> 1.0.0) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0d182e8eb0..01e2e7b2f9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,6 +10,7 @@ class ApplicationController < ActionController::Base before_action :authenticate_user_from_token! before_action :authenticate_user! + before_action :validate_user_service_ticket! before_action :reject_blocked! before_action :check_password_expiration before_action :ldap_security_check @@ -202,6 +203,20 @@ class ApplicationController < ActionController::Base end end + def validate_user_service_ticket! + return unless signed_in? && session[:service_tickets] + + valid = session[:service_tickets].all? do |provider, ticket| + Gitlab::OAuth::Session.valid?(provider, ticket) + end + + unless valid + session[:service_tickets] = nil + sign_out current_user + redirect_to new_user_session_path + end + end + def check_password_expiration if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? redirect_to new_profile_password_path and return diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index f809fa7500..4cad98b8e9 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -1,6 +1,6 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController - protect_from_forgery except: [:kerberos, :saml] + protect_from_forgery except: [:kerberos, :saml, :cas3] Gitlab.config.omniauth.providers.each do |provider| define_method provider['name'] do @@ -42,6 +42,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController render 'errors/omniauth_error', layout: "errors", status: 422 end + def cas3 + ticket = params['ticket'] + if ticket + handle_service_ticket oauth['provider'], ticket + end + handle_omniauth + end + private def handle_omniauth @@ -84,6 +92,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController redirect_to new_user_session_path end + def handle_service_ticket provider, ticket + Gitlab::OAuth::Session.create provider, ticket + session[:service_tickets] ||= {} + session[:service_tickets][provider] = ticket + end + def oauth @oauth ||= request.env['omniauth.auth'] end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index db378118f8..fcf034d791 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -287,6 +287,15 @@ production: &base # arguments, followed by optional 'args' which can be either a hash or an array. # Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html providers: + # See omniauth-cas3 for more configuration details + # - { name: 'cas3', + # label: 'cas3', + # args: { + # url: 'https://sso.example.com', + # disable_ssl_verification: false, + # login_url: '/cas/login', + # service_validate_url: '/cas/p3/serviceValidate', + # logout_url: '/cas/logout'} } # - { name: 'github', # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', @@ -324,6 +333,10 @@ production: &base # application_name: 'YOUR_APP_NAME', # application_password: 'YOUR_APP_PASSWORD' } } + # SSO maximum session duration in seconds. Defaults to CAS default of 8 hours. + # cas3: + # session_duration: 28800 + # Shared file storage settings shared: # path: /mnt/gitlab # Default: shared diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 63d8ae1743..fe0aaaedde 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -126,6 +126,10 @@ Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? Settings.omniauth['providers'] ||= [] +Settings.omniauth['cas3'] ||= Settingslogic.new({}) +Settings.omniauth.cas3['session_duration'] ||= 8.hours +Settings.omniauth['session_tickets'] ||= Settingslogic.new({}) +Settings.omniauth.session_tickets['cas3'] = 'ticket' Settings['shared'] ||= Settingslogic.new({}) Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 5fb43a86e1..92149826da 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -241,6 +241,16 @@ Devise.setup do |config| # An Array from the configuration will be expanded. provider_arguments.concat provider['args'] when Hash + # Add procs for handling SLO + if provider['name'] == 'cas3' + provider['args'][:on_single_sign_out] = lambda do |request| + ticket = request.params[:session_index] + raise "Service Ticket not found." unless Gitlab::OAuth::Session.valid?(:cas3, ticket) + Gitlab::OAuth::Session.destroy(:cas3, ticket) + true + end + end + # A Hash from the configuration will be passed as is. provider_arguments << provider['args'].symbolize_keys end diff --git a/doc/integration/cas.md b/doc/integration/cas.md new file mode 100644 index 0000000000..3490f6a38e --- /dev/null +++ b/doc/integration/cas.md @@ -0,0 +1,62 @@ +# CAS OmniAuth Provider + +To enable the CAS OmniAuth provider you must register your application with your CAS instance. This requires the service URL gitlab will supply to CAS. It should be something like: `https://gitlab.example.com:443/users/auth/cas3/callback?url`. By default handling for SLO is enabled, you only need to configure CAS for backchannel logout. + +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For instalations from source: + + ```sh + cd /home/git/gitlab + + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + name: "cas3", + label: "cas", + args: { + url: 'CAS_SERVER', + login_url: '/CAS_PATH/login', + service_validate_url: '/CAS_PATH/p3/serviceValidate', + logout_url: '/CAS_PATH/logout'} } + } + } + ] + ``` + + For installations from source: + + ``` + - { name: 'cas3', + label: 'cas', + args: { + url: 'CAS_SERVER', + login_url: '/CAS_PATH/login', + service_validate_url: '/CAS_PATH/p3/serviceValidate', + logout_url: '/CAS_PATH/logout'} } + ``` + +1. Change 'CAS_PATH' to the root of your CAS instance (ie. `cas`). + +1. If your CAS instance does not use default TGC lifetimes, update the `cas3.session_duration` to at least the current TGC maximum lifetime. To explicitly disable SLO, regardless of CAS settings, set this to 0. + +1. Save the configuration file. + +1. Restart GitLab for the changes to take effect. + +On the sign in page there should now be a CAS tab in the sign in form. \ No newline at end of file diff --git a/lib/gitlab/o_auth/session.rb b/lib/gitlab/o_auth/session.rb new file mode 100644 index 0000000000..f33bfd0bd0 --- /dev/null +++ b/lib/gitlab/o_auth/session.rb @@ -0,0 +1,17 @@ +module Gitlab + module OAuth + module Session + def self.create(provider, ticket) + Rails.cache.write("gitlab:#{provider}:#{ticket}", ticket, expires_in: Gitlab.config.omniauth.cas3.session_duration) + end + + def self.destroy(provider, ticket) + Rails.cache.delete("gitlab:#{provider}:#{ticket}") + end + + def self.valid?(provider, ticket) + Rails.cache.read("gitlab:#{provider}:#{ticket}").present? + end + end + end +end From 7c14541d2bb9735402086c951730344fa5203278 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 12 Dec 2015 23:42:52 +0100 Subject: [PATCH 631/908] Add feature specs for note polling Ref. #4032, !2084 --- spec/features/issues/note_polling_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 spec/features/issues/note_polling_spec.rb diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb new file mode 100644 index 0000000000..1698a0a3e5 --- /dev/null +++ b/spec/features/issues/note_polling_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +feature 'Issue notes polling' do + let!(:project) { create(:project, :public) } + let!(:issue) { create(:issue, project: project) } + + background do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + scenario 'Another user adds a comment to an issue', js: true do + note = create(:note_on_issue, noteable: issue, + project_id: project.project_id, + note: 'Looks good!') + sleep 16 # refresh interval in notes.js.coffee, 15 seconds + expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!') + end +end From 9470d05c70c15df75ae6be45812967abf89ae59a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 13 Dec 2015 00:39:47 +0100 Subject: [PATCH 632/908] Add spinach test for note polling This also increases capybara timeout to 15 seconds (note polling interval). Capybara will look for new note for this period of time. --- features/project/issues/issues.feature | 6 ++++++ features/steps/project/issues/issues.rb | 10 ++++++++++ features/support/capybara.rb | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index f08b30e0b8..ab234bc750 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -197,3 +197,9 @@ Feature: Project Issues And I should not see labels field And I submit new issue "500 error on profile" Then I should see issue "500 error on profile" + + @javascript + Scenario: Another user adds a comment to issue I'm currently viewing + Given I visit issue page "Release 0.4" + And another user adds a comment with text "Yay!" to issue "Release 0.4" + Then I should see a new comment with text "Yay!" diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index a13044c3ae..4a7ff21d38 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -284,6 +284,16 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end end + step 'another user adds a comment with text "Yay!" to issue "Release 0.4"' do + issue = Issue.find_by!(title: 'Release 0.4') + create(:note_on_issue, noteable: issue, note: 'Yay!') + end + + step 'I should see a new comment with text "Yay!"' do + page.within '#notes' do + expect(page).to have_content('Yay!') + end + end def filter_issue(text) fill_in 'issue_search', with: text end diff --git a/features/support/capybara.rb b/features/support/capybara.rb index 31dbf0feb2..4156c7ec48 100644 --- a/features/support/capybara.rb +++ b/features/support/capybara.rb @@ -2,7 +2,7 @@ require 'spinach/capybara' require 'capybara/poltergeist' # Give CI some extra time -timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10 +timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15 Capybara.javascript_driver = :poltergeist Capybara.register_driver :poltergeist do |app| From 3e789eab8be93c45685c417488da102d996209ba Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 13 Dec 2015 00:44:45 +0100 Subject: [PATCH 633/908] Add minor fixes in note polling specs --- spec/features/issues/note_polling_spec.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index 1698a0a3e5..5cf3dcf890 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -9,10 +9,8 @@ feature 'Issue notes polling' do end scenario 'Another user adds a comment to an issue', js: true do - note = create(:note_on_issue, noteable: issue, - project_id: project.project_id, - note: 'Looks good!') - sleep 16 # refresh interval in notes.js.coffee, 15 seconds + note = create(:note_on_issue, noteable: issue, note: 'Looks good!') + sleep 15 # refresh interval in notes.js.coffee is 15 seconds expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!') end end From 6fb120d1b0ad8adee7c5c431c7f8a399a2da5cea Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 14 Dec 2015 09:41:30 +0100 Subject: [PATCH 634/908] Assign notes object to a variable --- app/views/projects/notes/_notes_with_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 99c1b0fa43..eb378b4260 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -7,4 +7,4 @@ = render "projects/notes/form", view: diff_view :javascript - new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") + var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") From 8fe821cfa5acea2a4ab1fce7f35a76455d5a1cac Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 14 Dec 2015 09:41:59 +0100 Subject: [PATCH 635/908] Trigger notes refresh in specs instead of waiting for ajax --- spec/features/issues/note_polling_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index 5cf3dcf890..e4efdbe242 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -10,7 +10,7 @@ feature 'Issue notes polling' do scenario 'Another user adds a comment to an issue', js: true do note = create(:note_on_issue, noteable: issue, note: 'Looks good!') - sleep 15 # refresh interval in notes.js.coffee is 15 seconds + page.execute_script('notes.refresh();') expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!') end end From 3581927f5932746562581ff45052a62660f83a90 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 15 Dec 2015 14:30:00 +0100 Subject: [PATCH 636/908] Require gitlab-workhorse 0.5.0 --- GITLAB_WORKHORSE_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 2b7c5ae018..8f0916f768 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.4.2 +0.5.0 From 7781bda9bd82997f4a03de4cf911b1156ceb2cde Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 15 Dec 2015 15:51:16 +0100 Subject: [PATCH 637/908] Move Markdown/reference logic from Gitlab::Markdown to Banzai --- app/helpers/gitlab_markdown_helper.rb | 6 +- app/helpers/issues_helper.rb | 2 +- app/helpers/labels_helper.rb | 2 +- app/models/concerns/mentionable.rb | 16 +-- app/models/concerns/participable.rb | 29 ++--- app/models/issue.rb | 2 +- app/models/note.rb | 4 +- lib/banzai.rb | 13 ++ lib/banzai/cross_project_reference.rb | 22 ++++ lib/banzai/filter.rb | 10 ++ .../filter}/abstract_reference_filter.rb | 6 +- .../filter/autolink_filter.rb | 6 +- .../filter/commit_range_reference_filter.rb | 6 +- .../filter/commit_reference_filter.rb | 6 +- .../filter/emoji_filter.rb | 6 +- .../filter/external_issue_reference_filter.rb | 6 +- .../filter/external_link_filter.rb | 6 +- .../filter/issue_reference_filter.rb | 6 +- .../filter/label_reference_filter.rb | 6 +- .../filter/markdown_filter.rb | 11 +- .../filter/merge_request_reference_filter.rb | 6 +- .../filter/redactor_filter.rb | 8 +- .../filter}/reference_filter.rb | 27 +--- .../filter/reference_gatherer_filter.rb | 13 +- .../filter/relative_link_filter.rb | 6 +- .../filter/sanitization_filter.rb | 6 +- .../filter/snippet_reference_filter.rb | 6 +- .../filter/syntax_highlight_filter.rb | 6 +- .../filter/table_of_contents_filter.rb | 6 +- .../filter/task_list_filter.rb | 6 +- .../filter/upload_link_filter.rb | 6 +- .../filter/user_reference_filter.rb | 6 +- lib/banzai/lazy_reference.rb | 27 ++++ lib/banzai/pipeline.rb | 10 ++ lib/banzai/pipeline/asciidoc_pipeline.rb | 13 ++ lib/banzai/pipeline/atom_pipeline.rb | 14 +++ lib/banzai/pipeline/base_pipeline.rb | 30 +++++ .../pipeline}/combined_pipeline.rb | 8 +- .../pipeline/description_pipeline.rb | 8 +- .../pipeline/email_pipeline.rb | 8 +- .../pipeline/full_pipeline.rb | 6 +- lib/banzai/pipeline/gfm_pipeline.rb | 41 +++++++ .../pipeline/note_pipeline.rb | 8 +- .../pipeline/plain_markdown_pipeline.rb | 13 ++ lib/banzai/pipeline/post_process_pipeline.rb | 20 +++ .../pipeline/reference_extraction_pipeline.rb | 13 ++ lib/banzai/pipeline/single_line_pipeline.rb | 9 ++ lib/banzai/reference_extractor.rb | 55 +++++++++ lib/banzai/renderer.rb | 76 ++++++++++++ lib/gitlab/asciidoc.rb | 2 +- lib/gitlab/markdown.rb | 115 ------------------ .../markdown/cross_project_reference.rb | 24 ---- lib/gitlab/markdown/pipeline.rb | 4 +- .../markdown/pipeline/asciidoc_pipeline.rb | 13 -- lib/gitlab/markdown/pipeline/atom_pipeline.rb | 14 --- lib/gitlab/markdown/pipeline/gfm_pipeline.rb | 41 ------- .../pipeline/plain_markdown_pipeline.rb | 13 -- .../pipeline/post_process_pipeline.rb | 20 --- .../pipeline/reference_extraction_pipeline.rb | 13 -- .../markdown/pipeline/single_line_pipeline.rb | 9 -- lib/gitlab/reference_extractor.rb | 53 ++------ .../gitlab/markdown/reference_filter_spec.rb | 2 +- .../cross_project_reference_spec.rb | 2 +- .../filter/autolink_filter_spec.rb | 2 +- .../commit_range_reference_filter_spec.rb | 2 +- .../filter/commit_reference_filter_spec.rb | 2 +- .../filter/emoji_filter_spec.rb | 2 +- .../external_issue_reference_filter_spec.rb | 2 +- .../filter/external_link_filter_spec.rb | 2 +- .../filter/issue_reference_filter_spec.rb | 2 +- .../filter/label_reference_filter_spec.rb | 2 +- .../merge_request_reference_filter_spec.rb | 2 +- .../filter/redactor_filter_spec.rb | 2 +- .../filter/reference_gatherer_filter_spec.rb | 2 +- .../filter/relative_link_filter_spec.rb | 2 +- .../filter/sanitization_filter_spec.rb | 2 +- .../filter/snippet_reference_filter_spec.rb | 2 +- .../filter/syntax_highlight_filter_spec.rb | 2 +- .../filter/table_of_contents_filter_spec.rb | 2 +- .../filter/task_list_filter_spec.rb | 2 +- .../filter/upload_link_filter_spec.rb | 2 +- .../filter/user_reference_filter_spec.rb | 2 +- spec/lib/gitlab/asciidoc_spec.rb | 2 +- spec/support/filter_spec_helper.rb | 36 +++--- 84 files changed, 542 insertions(+), 491 deletions(-) create mode 100644 lib/banzai.rb create mode 100644 lib/banzai/cross_project_reference.rb create mode 100644 lib/banzai/filter.rb rename lib/{gitlab/markdown => banzai/filter}/abstract_reference_filter.rb (98%) rename lib/{gitlab/markdown => banzai}/filter/autolink_filter.rb (98%) rename lib/{gitlab/markdown => banzai}/filter/commit_range_reference_filter.rb (96%) rename lib/{gitlab/markdown => banzai}/filter/commit_reference_filter.rb (96%) rename lib/{gitlab/markdown => banzai}/filter/emoji_filter.rb (97%) rename lib/{gitlab/markdown => banzai}/filter/external_issue_reference_filter.rb (97%) rename lib/{gitlab/markdown => banzai}/filter/external_link_filter.rb (91%) rename lib/{gitlab/markdown => banzai}/filter/issue_reference_filter.rb (89%) rename lib/{gitlab/markdown => banzai}/filter/label_reference_filter.rb (98%) rename lib/{gitlab/markdown => banzai}/filter/markdown_filter.rb (91%) rename lib/{gitlab/markdown => banzai}/filter/merge_request_reference_filter.rb (94%) rename lib/{gitlab/markdown => banzai}/filter/redactor_filter.rb (88%) rename lib/{gitlab/markdown => banzai/filter}/reference_filter.rb (90%) rename lib/{gitlab/markdown => banzai}/filter/reference_gatherer_filter.rb (80%) rename lib/{gitlab/markdown => banzai}/filter/relative_link_filter.rb (98%) rename lib/{gitlab/markdown => banzai}/filter/sanitization_filter.rb (97%) rename lib/{gitlab/markdown => banzai}/filter/snippet_reference_filter.rb (91%) rename lib/{gitlab/markdown => banzai}/filter/syntax_highlight_filter.rb (94%) rename lib/{gitlab/markdown => banzai}/filter/table_of_contents_filter.rb (96%) rename lib/{gitlab/markdown => banzai}/filter/task_list_filter.rb (91%) rename lib/{gitlab/markdown => banzai}/filter/upload_link_filter.rb (94%) rename lib/{gitlab/markdown => banzai}/filter/user_reference_filter.rb (98%) create mode 100644 lib/banzai/lazy_reference.rb create mode 100644 lib/banzai/pipeline.rb create mode 100644 lib/banzai/pipeline/asciidoc_pipeline.rb create mode 100644 lib/banzai/pipeline/atom_pipeline.rb create mode 100644 lib/banzai/pipeline/base_pipeline.rb rename lib/{gitlab/markdown => banzai/pipeline}/combined_pipeline.rb (84%) rename lib/{gitlab/markdown => banzai}/pipeline/description_pipeline.rb (68%) rename lib/{gitlab/markdown => banzai}/pipeline/email_pipeline.rb (62%) rename lib/{gitlab/markdown => banzai}/pipeline/full_pipeline.rb (63%) create mode 100644 lib/banzai/pipeline/gfm_pipeline.rb rename lib/{gitlab/markdown => banzai}/pipeline/note_pipeline.rb (67%) create mode 100644 lib/banzai/pipeline/plain_markdown_pipeline.rb create mode 100644 lib/banzai/pipeline/post_process_pipeline.rb create mode 100644 lib/banzai/pipeline/reference_extraction_pipeline.rb create mode 100644 lib/banzai/pipeline/single_line_pipeline.rb create mode 100644 lib/banzai/reference_extractor.rb create mode 100644 lib/banzai/renderer.rb delete mode 100644 lib/gitlab/markdown.rb delete mode 100644 lib/gitlab/markdown/cross_project_reference.rb delete mode 100644 lib/gitlab/markdown/pipeline/asciidoc_pipeline.rb delete mode 100644 lib/gitlab/markdown/pipeline/atom_pipeline.rb delete mode 100644 lib/gitlab/markdown/pipeline/gfm_pipeline.rb delete mode 100644 lib/gitlab/markdown/pipeline/plain_markdown_pipeline.rb delete mode 100644 lib/gitlab/markdown/pipeline/post_process_pipeline.rb delete mode 100644 lib/gitlab/markdown/pipeline/reference_extraction_pipeline.rb delete mode 100644 lib/gitlab/markdown/pipeline/single_line_pipeline.rb rename spec/lib/{gitlab/markdown => banzai}/cross_project_reference_spec.rb (93%) rename spec/lib/{gitlab/markdown => banzai}/filter/autolink_filter_spec.rb (98%) rename spec/lib/{gitlab/markdown => banzai}/filter/commit_range_reference_filter_spec.rb (98%) rename spec/lib/{gitlab/markdown => banzai}/filter/commit_reference_filter_spec.rb (98%) rename spec/lib/{gitlab/markdown => banzai}/filter/emoji_filter_spec.rb (98%) rename spec/lib/{gitlab/markdown => banzai}/filter/external_issue_reference_filter_spec.rb (96%) rename spec/lib/{gitlab/markdown => banzai}/filter/external_link_filter_spec.rb (92%) rename spec/lib/{gitlab/markdown => banzai}/filter/issue_reference_filter_spec.rb (99%) rename spec/lib/{gitlab/markdown => banzai}/filter/label_reference_filter_spec.rb (98%) rename spec/lib/{gitlab/markdown => banzai}/filter/merge_request_reference_filter_spec.rb (98%) rename spec/lib/{gitlab/markdown => banzai}/filter/redactor_filter_spec.rb (97%) rename spec/lib/{gitlab/markdown => banzai}/filter/reference_gatherer_filter_spec.rb (97%) rename spec/lib/{gitlab/markdown => banzai}/filter/relative_link_filter_spec.rb (98%) rename spec/lib/{gitlab/markdown => banzai}/filter/sanitization_filter_spec.rb (99%) rename spec/lib/{gitlab/markdown => banzai}/filter/snippet_reference_filter_spec.rb (98%) rename spec/lib/{gitlab/markdown => banzai}/filter/syntax_highlight_filter_spec.rb (89%) rename spec/lib/{gitlab/markdown => banzai}/filter/table_of_contents_filter_spec.rb (97%) rename spec/lib/{gitlab/markdown => banzai}/filter/task_list_filter_spec.rb (78%) rename spec/lib/{gitlab/markdown => banzai}/filter/upload_link_filter_spec.rb (97%) rename spec/lib/{gitlab/markdown => banzai}/filter/user_reference_filter_spec.rb (98%) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 5004e02ea0..a0cf3dc084 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -20,7 +20,7 @@ module GitlabMarkdownHelper end user = current_user if defined?(current_user) - gfm_body = Gitlab::Markdown.render(escaped_body, project: @project, current_user: user, pipeline: :single_line) + gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line) fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body) if fragment.children.size == 1 && fragment.children[0].name == 'a' @@ -50,7 +50,7 @@ module GitlabMarkdownHelper context[:project] ||= @project - html = Gitlab::Markdown.render(text, context) + html = Banzai.render(text, context) context.merge!( current_user: (current_user if defined?(current_user)), @@ -61,7 +61,7 @@ module GitlabMarkdownHelper ref: @ref ) - Gitlab::Markdown.post_process(html, context) + Banzai.post_process(html, context) end def asciidoc(text) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index e66b9c628c..21149a15b6 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -121,6 +121,6 @@ module IssuesHelper end end - # Required for Gitlab::Markdown::IssueReferenceFilter + # Required for Banzai::IssueReferenceFilter module_function :url_for_issue end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 795fb439f2..97e8baa179 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -107,6 +107,6 @@ module LabelsHelper options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name]) end - # Required for Gitlab::Markdown::LabelReferenceFilter + # Required for Banzai::LabelReferenceFilter module_function :render_colored_label, :text_color_for_bg, :escape_once end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index d2ea9ab731..d4e3099453 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -23,7 +23,7 @@ module Mentionable included do if self < Participable - participant ->(current_user) { mentioned_users(current_user, load_lazy_references: false) } + participant ->(current_user) { mentioned_users(current_user) } end end @@ -43,9 +43,9 @@ module Mentionable self end - def all_references(current_user = self.author, text = nil, load_lazy_references: true) - ext = Gitlab::ReferenceExtractor.new(self.project, current_user, load_lazy_references: load_lazy_references) - + def all_references(current_user = self.author, text = nil) + ext = Gitlab::ReferenceExtractor.new(self.project, current_user) + if text ext.analyze(text) else @@ -59,13 +59,13 @@ module Mentionable ext end - def mentioned_users(current_user = nil, load_lazy_references: true) - all_references(current_user, load_lazy_references: load_lazy_references).users + def mentioned_users(current_user = nil) + all_references(current_user).users end # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. - def referenced_mentionables(current_user = self.author, text = nil, load_lazy_references: true) - refs = all_references(current_user, text, load_lazy_references: load_lazy_references) + def referenced_mentionables(current_user = self.author, text = nil) + refs = all_references(current_user, text) refs = (refs.issues + refs.merge_requests + refs.commits) # We're using this method instead of Array diffing because that requires diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 85367f89f4..808d80b053 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -38,20 +38,21 @@ module Participable # Be aware that this method makes a lot of sql queries. # Save result into variable if you are going to reuse it inside same request def participants(current_user = self.author, load_lazy_references: true) - participants = self.class.participant_attrs.flat_map do |attr| - value = - if attr.respond_to?(:call) - instance_exec(current_user, &attr) - else - send(attr) - end + participants = + Gitlab::ReferenceExtractor.lazily do + self.class.participant_attrs.flat_map do |attr| + value = + if attr.respond_to?(:call) + instance_exec(current_user, &attr) + else + send(attr) + end - participants_for(value, current_user) - end.compact.uniq - - if load_lazy_references - participants = Gitlab::Markdown::ReferenceFilter::LazyReference.load(participants).uniq + participants_for(value, current_user) + end.compact.uniq + end + unless Gitlab::ReferenceExtractor.lazy? participants.select! do |user| user.can?(:read_project, project) end @@ -64,12 +65,12 @@ module Participable def participants_for(value, current_user = nil) case value - when User, Gitlab::Markdown::ReferenceFilter::LazyReference + when User, Banzai::LazyReference [value] when Enumerable, ActiveRecord::Relation value.flat_map { |v| participants_for(v, current_user) } when Participable - value.participants(current_user, load_lazy_references: false) + value.participants(current_user) end end end diff --git a/app/models/issue.rb b/app/models/issue.rb index e04035b3af..9f4f4923e5 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -88,7 +88,7 @@ class Issue < ActiveRecord::Base note.all_references(load_lazy_references: false).merge_requests end.uniq - Gitlab::Markdown::ReferenceFilter::LazyReference.load(references).uniq.sort_by(&:iid) + Banzai::LazyReference.load(references).uniq.sort_by(&:iid) end # Reset issue events cache diff --git a/app/models/note.rb b/app/models/note.rb index 04053ccc61..ea69ef65fc 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -373,11 +373,11 @@ class Note < ActiveRecord::Base end def contains_emoji_only? - note =~ /\A#{Gitlab::Markdown::EmojiFilter.emoji_pattern}\s?\Z/ + note =~ /\A#{Banzai::EmojiFilter.emoji_pattern}\s?\Z/ end def award_emoji_name - original_name = note.match(Gitlab::Markdown::EmojiFilter.emoji_pattern)[1] + original_name = note.match(Banzai::EmojiFilter.emoji_pattern)[1] AwardEmoji.normilize_emoji_name(original_name) end end diff --git a/lib/banzai.rb b/lib/banzai.rb new file mode 100644 index 0000000000..093382261a --- /dev/null +++ b/lib/banzai.rb @@ -0,0 +1,13 @@ +module Banzai + def self.render(text, context = {}) + Renderer.render(text, context) + end + + def self.render_result(text, context = {}) + Renderer.render_result(text, context) + end + + def self.post_process(html, context) + Renderer.post_process(html, context) + end +end diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb new file mode 100644 index 0000000000..ba2866e1ef --- /dev/null +++ b/lib/banzai/cross_project_reference.rb @@ -0,0 +1,22 @@ +require 'banzai' + +module Banzai + # Common methods for ReferenceFilters that support an optional cross-project + # reference. + module CrossProjectReference + # Given a cross-project reference string, get the Project record + # + # Defaults to value of `context[:project]` if: + # * No reference is given OR + # * Reference given doesn't exist + # + # ref - String reference. + # + # Returns a Project, or nil if the reference can't be found + def project_from_ref(ref) + return context[:project] unless ref + + Project.find_with_namespace(ref) + end + end +end diff --git a/lib/banzai/filter.rb b/lib/banzai/filter.rb new file mode 100644 index 0000000000..fd4fe02425 --- /dev/null +++ b/lib/banzai/filter.rb @@ -0,0 +1,10 @@ +require 'active_support/core_ext/string/output_safety' +require 'banzai' + +module Banzai + module Filter + def self.[](name) + const_get("#{name.to_s.camelize}Filter") + end + end +end diff --git a/lib/gitlab/markdown/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb similarity index 98% rename from lib/gitlab/markdown/abstract_reference_filter.rb rename to lib/banzai/filter/abstract_reference_filter.rb index 9488e980c0..bdaa4721b4 100644 --- a/lib/gitlab/markdown/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -1,7 +1,7 @@ -require 'gitlab/markdown' +require 'banzai' -module Gitlab - module Markdown +module Banzai + module Filter # Issues, Merge Requests, Snippets, Commits and Commit Ranges share # similar functionality in reference filtering. class AbstractReferenceFilter < ReferenceFilter diff --git a/lib/gitlab/markdown/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb similarity index 98% rename from lib/gitlab/markdown/filter/autolink_filter.rb rename to lib/banzai/filter/autolink_filter.rb index c37c3bc55b..da4ee80c1b 100644 --- a/lib/gitlab/markdown/filter/autolink_filter.rb +++ b/lib/banzai/filter/autolink_filter.rb @@ -1,9 +1,9 @@ -require 'gitlab/markdown' +require 'banzai' require 'html/pipeline/filter' require 'uri' -module Gitlab - module Markdown +module Banzai + module Filter # HTML Filter for auto-linking URLs in HTML. # # Based on HTML::Pipeline::AutolinkFilter diff --git a/lib/gitlab/markdown/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb similarity index 96% rename from lib/gitlab/markdown/filter/commit_range_reference_filter.rb rename to lib/banzai/filter/commit_range_reference_filter.rb index 36b3258ef7..e67cd45ab9 100644 --- a/lib/gitlab/markdown/filter/commit_range_reference_filter.rb +++ b/lib/banzai/filter/commit_range_reference_filter.rb @@ -1,7 +1,7 @@ -require 'gitlab/markdown' +require 'banzai' -module Gitlab - module Markdown +module Banzai + module Filter # HTML filter that replaces commit range references with links. # # This filter supports cross-project references. diff --git a/lib/gitlab/markdown/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb similarity index 96% rename from lib/gitlab/markdown/filter/commit_reference_filter.rb rename to lib/banzai/filter/commit_reference_filter.rb index e3066a89b0..9e57608b48 100644 --- a/lib/gitlab/markdown/filter/commit_reference_filter.rb +++ b/lib/banzai/filter/commit_reference_filter.rb @@ -1,7 +1,7 @@ -require 'gitlab/markdown' +require 'banzai' -module Gitlab - module Markdown +module Banzai + module Filter # HTML filter that replaces commit references with links. # # This filter supports cross-project references. diff --git a/lib/gitlab/markdown/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb similarity index 97% rename from lib/gitlab/markdown/filter/emoji_filter.rb rename to lib/banzai/filter/emoji_filter.rb index da10e4d376..86838e1483 100644 --- a/lib/gitlab/markdown/filter/emoji_filter.rb +++ b/lib/banzai/filter/emoji_filter.rb @@ -1,10 +1,10 @@ require 'action_controller' -require 'gitlab/markdown' +require 'banzai' require 'gitlab_emoji' require 'html/pipeline/filter' -module Gitlab - module Markdown +module Banzai + module Filter # HTML filter that replaces :emoji: with images. # # Based on HTML::Pipeline::EmojiFilter diff --git a/lib/gitlab/markdown/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb similarity index 97% rename from lib/gitlab/markdown/filter/external_issue_reference_filter.rb rename to lib/banzai/filter/external_issue_reference_filter.rb index 14bdf5521f..f5737a7ac1 100644 --- a/lib/gitlab/markdown/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -1,7 +1,7 @@ -require 'gitlab/markdown' +require 'banzai' -module Gitlab - module Markdown +module Banzai + module Filter # HTML filter that replaces external issue tracker references with links. # References are ignored if the project doesn't use an external issue # tracker. diff --git a/lib/gitlab/markdown/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb similarity index 91% rename from lib/gitlab/markdown/filter/external_link_filter.rb rename to lib/banzai/filter/external_link_filter.rb index e09dfcb83c..ac87b9820a 100644 --- a/lib/gitlab/markdown/filter/external_link_filter.rb +++ b/lib/banzai/filter/external_link_filter.rb @@ -1,8 +1,8 @@ -require 'gitlab/markdown' +require 'banzai' require 'html/pipeline/filter' -module Gitlab - module Markdown +module Banzai + module Filter # HTML Filter to add a `rel="nofollow"` attribute to external links # class ExternalLinkFilter < HTML::Pipeline::Filter diff --git a/lib/gitlab/markdown/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb similarity index 89% rename from lib/gitlab/markdown/filter/issue_reference_filter.rb rename to lib/banzai/filter/issue_reference_filter.rb index 1ed69e2f43..51180cb901 100644 --- a/lib/gitlab/markdown/filter/issue_reference_filter.rb +++ b/lib/banzai/filter/issue_reference_filter.rb @@ -1,7 +1,7 @@ -require 'gitlab/markdown' +require 'banzai' -module Gitlab - module Markdown +module Banzai + module Filter # HTML filter that replaces issue references with links. References to # issues that do not exist are ignored. # diff --git a/lib/gitlab/markdown/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb similarity index 98% rename from lib/gitlab/markdown/filter/label_reference_filter.rb rename to lib/banzai/filter/label_reference_filter.rb index a2026eecae..07bac2dd7f 100644 --- a/lib/gitlab/markdown/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -1,7 +1,7 @@ -require 'gitlab/markdown' +require 'banzai' -module Gitlab - module Markdown +module Banzai + module Filter # HTML filter that replaces label references with links. class LabelReferenceFilter < ReferenceFilter # Public: Find label references in text diff --git a/lib/gitlab/markdown/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb similarity index 91% rename from lib/gitlab/markdown/filter/markdown_filter.rb rename to lib/banzai/filter/markdown_filter.rb index 921e2a0794..0072bab1f9 100644 --- a/lib/gitlab/markdown/filter/markdown_filter.rb +++ b/lib/banzai/filter/markdown_filter.rb @@ -1,5 +1,8 @@ -module Gitlab - module Markdown +require 'banzai' +require 'html/pipeline/filter' + +module Banzai + module Filter class MarkdownFilter < HTML::Pipeline::TextFilter def initialize(text, context = nil, result = nil) super text, context, result @@ -11,8 +14,8 @@ module Gitlab html.rstrip! html end - - private + + private def self.redcarpet_options # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use diff --git a/lib/gitlab/markdown/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb similarity index 94% rename from lib/gitlab/markdown/filter/merge_request_reference_filter.rb rename to lib/banzai/filter/merge_request_reference_filter.rb index 2eb77c46da..755b946a34 100644 --- a/lib/gitlab/markdown/filter/merge_request_reference_filter.rb +++ b/lib/banzai/filter/merge_request_reference_filter.rb @@ -1,7 +1,7 @@ -require 'gitlab/markdown' +require 'banzai' -module Gitlab - module Markdown +module Banzai + module Filter # HTML filter that replaces merge request references with links. References # to merge requests that do not exist are ignored. # diff --git a/lib/gitlab/markdown/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb similarity index 88% rename from lib/gitlab/markdown/filter/redactor_filter.rb rename to lib/banzai/filter/redactor_filter.rb index 33ef7ce18b..89e7a79789 100644 --- a/lib/gitlab/markdown/filter/redactor_filter.rb +++ b/lib/banzai/filter/redactor_filter.rb @@ -1,8 +1,8 @@ -require 'gitlab/markdown' +require 'banzai' require 'html/pipeline/filter' -module Gitlab - module Markdown +module Banzai + module Filter # HTML filter that removes references to records that the current user does # not have permission to view. # @@ -27,7 +27,7 @@ module Gitlab def user_can_reference?(node) if node.has_attribute?('data-reference-filter') reference_type = node.attr('data-reference-filter') - reference_filter = Gitlab::Markdown.const_get(reference_type) + reference_filter = Banzai::Filter.const_get(reference_type) reference_filter.user_can_reference?(current_user, node, context) else diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/banzai/filter/reference_filter.rb similarity index 90% rename from lib/gitlab/markdown/reference_filter.rb rename to lib/banzai/filter/reference_filter.rb index 3b83b8bd8f..33457a3f36 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -1,9 +1,9 @@ require 'active_support/core_ext/string/output_safety' -require 'gitlab/markdown' +require 'banzai' require 'html/pipeline/filter' -module Gitlab - module Markdown +module Banzai + module Filter # Base class for GitLab Flavored Markdown reference filters. # # References within
, , , and 

&I#>0_{^-L0SDa6;E-#{Wt}}|6@wPdcqQ{^ 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 Z1Sz8|r}y)&xA_A6i3v#yR`Bco`G1c#sD1zd diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 9ee26c41e6..3dbf1afc7a 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -20,22 +20,6 @@ Of course a command can execute code directly (`./configure;make;make install`) Jobs are used to create builds, which are then picked up by [runners](../runners/README.md) and executed within the environment of the runner. What is important, is that each job is run independently from each other. -## Why `.gitlab-ci.yml` - -By placing a single configuration file in the root of your repository, -it is version controlled and you get all the advantages of git. - -In addition, builds for older versions of the repository will work just fine, -as GitLab look at the `.gitlab-ci.yml` of the pushed commit. -This means that forks also build without any problem. - -You can even set up different builds for different branches. This allows you -to only deploy the `production` branch, for instance. - -By having a single source of truth, everyone can view and contribute to the -stability of your CI builds, eventually improving the quality of your development -cycle. - ## .gitlab-ci.yml The YAML syntax allows for using more complex job specifications than in the above example: @@ -201,7 +185,7 @@ This are two parameters that allow for setting a refs policy to limit when jobs There are a few rules that apply to usage of refs policy: -1. `only` and `except` are exclusive. If both `only` and `except` are defined in job specification only `only` is taken into account. +1. `only` and `except` are inclusive. If both `only` and `except` are defined in job specification the ref is filtered by `only` and `except`. 1. `only` and `except` allow for using the regexp expressions. 1. `only` and `except` allow for using special keywords: `branches` and `tags`. These names can be used for example to exclude all tags and all branches. @@ -214,6 +198,18 @@ job: - branches # use special keyword ``` +1. `only` and `except` allow for specify repository path to filter jobs for forks. +The repository path can be used to have jobs executed only for parent repository. + +```yaml +job: + only: + - branches@gitlab-org/gitlab-ce + except: + - master@gitlab-org/gitlab-ce +``` +The above will run `job` for all branches on `gitlab-org/gitlab-ce`, except master . + ### tags `tags` is used to select specific runners from the list of all runners that are allowed to run this project. From bd4ab21c07061e06166b08d86157e4004665ccbc Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 1 Dec 2015 12:49:22 +0100 Subject: [PATCH 142/908] Fix code docs --- app/models/commit_range.rb | 7 ++----- lib/gitlab/markdown/abstract_reference_filter.rb | 15 +++++++-------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 7b1164b024..449689faf6 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -13,8 +13,7 @@ # range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"} # range.to_s # => "f3f85602..e86e1013" # -# # Assuming `project` is a Project with a repository containing both commits: -# range.project = project +# # Assuming the specified project has a repository containing both commits: # range.valid_commits? # => true # class CommitRange @@ -68,7 +67,7 @@ class CommitRange range_string.strip! - unless range_string.match(/\A#{PATTERN}\z/) + unless range_string =~ /\A#{PATTERN}\z/ raise ArgumentError, "invalid CommitRange string format: #{range_string}" end @@ -123,8 +122,6 @@ class CommitRange # Check if both the starting and ending commit IDs exist in a project's # repository - # - # project - An optional Project to check (default: `project`) def valid_commits? commit_start.present? && commit_end.present? end diff --git a/lib/gitlab/markdown/abstract_reference_filter.rb b/lib/gitlab/markdown/abstract_reference_filter.rb index 4adc44361b..02a9e05a69 100644 --- a/lib/gitlab/markdown/abstract_reference_filter.rb +++ b/lib/gitlab/markdown/abstract_reference_filter.rb @@ -2,8 +2,8 @@ require 'gitlab/markdown' module Gitlab module Markdown - # Issues, Snippets and Merge Requests shares similar functionality in refernce filtering. - # All this functionality moved to this class + # Issues, Snippets, Merge Requests, Commits and Commit Ranges share + # similar functionality in refernce filtering. class AbstractReferenceFilter < ReferenceFilter include CrossProjectReference @@ -26,16 +26,15 @@ module Gitlab # Public: Find references in text (like `!123` for merge requests) # - # AnyReferenceFilter.references_in(text) do |match, object| - # "PREFIX#{object}" + # AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches| + # object = find_object(project_ref, id) + # "#{object.to_reference}" # end # - # PREFIX - symbol that detects reference (like ! for merge requests) - # object - reference object (snippet, merget request etc) # text - String text to search. # - # Yields the String match, the Integer referenced object ID, and an optional String - # of the external project reference. + # Yields the String match, the Integer referenced object ID, an optional String + # of the external project reference, and all of the matchdata. # # Returns a String replaced with the return of the block. def self.references_in(text) From 62c14ba2edf9ac4b4bb1e8c46c0c60f1b6574909 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 1 Dec 2015 12:58:45 +0100 Subject: [PATCH 143/908] Render commit reference using short sha, but include full sha in comment. --- app/models/commit.rb | 8 +++++++ app/models/commit_range.rb | 8 +++++++ app/models/concerns/referable.rb | 4 ++++ .../markdown/abstract_reference_filter.rb | 2 +- .../commit_range_reference_filter_spec.rb | 6 ++--- .../markdown/commit_reference_filter_spec.rb | 2 +- spec/models/commit_range_spec.rb | 22 ++++++++++++++++--- spec/models/commit_spec.rb | 17 +++++++++++--- 8 files changed, 58 insertions(+), 11 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index fc03d2580d..a5d041a49c 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -86,6 +86,14 @@ class Commit end def to_reference(from_project = nil) + if cross_project_reference?(from_project) + project.to_reference + self.class.reference_prefix + self.id + else + self.id + end + end + + def reference_link_text(from_project = nil) if cross_project_reference?(from_project) project.to_reference + self.class.reference_prefix + self.short_id else diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 449689faf6..f786a749b8 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -95,6 +95,14 @@ class CommitRange alias_method :id, :to_s def to_reference(from_project = nil) + if cross_project_reference?(from_project) + reference = project.to_reference + self.class.reference_prefix + self.id + else + self.id + end + end + + def reference_link_text(from_project = nil) reference = ref_from + notation + ref_to if cross_project_reference?(from_project) diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index 16e4d05486..ce064f675a 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -21,6 +21,10 @@ module Referable '' end + def reference_link_text(from_project = nil) + to_reference(from_project) + end + module ClassMethods # The character that prefixes the actual reference identifier # diff --git a/lib/gitlab/markdown/abstract_reference_filter.rb b/lib/gitlab/markdown/abstract_reference_filter.rb index 02a9e05a69..f6df9518fc 100644 --- a/lib/gitlab/markdown/abstract_reference_filter.rb +++ b/lib/gitlab/markdown/abstract_reference_filter.rb @@ -82,7 +82,7 @@ module Gitlab data = data_attribute(project: project.id, object_sym => object.id, original: match) url = matches[:url] || url_for_object(object, project) - text = object.to_reference(context[:project]) + text = object.reference_link_text(context[:project]) extras = object_link_text_extras(object, matches) text += " (#{extras.join(", ")})" if extras.any? diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb index 4b4c769a11..f65a3e8a0b 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -53,7 +53,7 @@ module Gitlab::Markdown it 'links with adjacent text' do doc = filter("See (#{reference}.)") - exp = Regexp.escape(range.to_reference) + exp = Regexp.escape(range.reference_link_text) expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) end @@ -125,7 +125,7 @@ module Gitlab::Markdown it 'links with adjacent text' do doc = filter("Fixed (#{reference}.)") - exp = Regexp.escape("#{project2.to_reference}@#{range.to_reference}") + exp = Regexp.escape("#{project2.to_reference}@#{range.reference_link_text}") expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) end @@ -163,7 +163,7 @@ module Gitlab::Markdown it 'links with adjacent text' do doc = filter("Fixed (#{reference}.)") - exp = Regexp.escape(range.to_reference(project)) + exp = Regexp.escape(range.reference_link_text(project)) expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) end diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index a8c9c7efd5..4cc6bbbfe9 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -148,7 +148,7 @@ module Gitlab::Markdown it 'links with adjacent text' do doc = filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(#{commit.to_reference(project)}<\/a>\.\)/) + expect(doc.to_html).to match(/\(#{commit.reference_link_text(project)}<\/a>\.\)/) end it 'ignores invalid commit IDs on the referenced project' do diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index 58283f0697..3c1009a2eb 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -38,15 +38,31 @@ describe CommitRange do let(:cross) { create(:project) } it 'returns a String reference to the object' do - expect(range.to_reference).to eq "#{sha_from}...#{sha_to}" + expect(range.to_reference).to eq "#{full_sha_from}...#{full_sha_to}" end it 'returns a String reference to the object' do - expect(range2.to_reference).to eq "#{sha_from}..#{sha_to}" + expect(range2.to_reference).to eq "#{full_sha_from}..#{full_sha_to}" end it 'supports a cross-project reference' do - expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{sha_from}...#{sha_to}" + expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{full_sha_from}...#{full_sha_to}" + end + end + + describe '#reference_link_text' do + let(:cross) { create(:project) } + + it 'returns a String reference to the object' do + expect(range.reference_link_text).to eq "#{sha_from}...#{sha_to}" + end + + it 'returns a String reference to the object' do + expect(range2.reference_link_text).to eq "#{sha_from}..#{sha_to}" + end + + it 'supports a cross-project reference' do + expect(range.reference_link_text(cross)).to eq "#{project.to_reference}@#{sha_from}...#{sha_to}" end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 1aaa927c21..974b52c183 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -15,12 +15,23 @@ describe Commit do describe '#to_reference' do it 'returns a String reference to the object' do - expect(commit.to_reference).to eq commit.short_id + expect(commit.to_reference).to eq commit.id end it 'supports a cross-project reference' do cross = double('project') - expect(commit.to_reference(cross)).to eq "#{project.to_reference}@#{commit.short_id}" + expect(commit.to_reference(cross)).to eq "#{project.to_reference}@#{commit.id}" + end + end + + describe '#reference_link_text' do + it 'returns a String reference to the object' do + expect(commit.reference_link_text).to eq commit.short_id + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(commit.reference_link_text(cross)).to eq "#{project.to_reference}@#{commit.short_id}" end end @@ -88,7 +99,7 @@ eos subject { create(:project).commit } let(:author) { create(:user, email: subject.author_email) } - let(:backref_text) { "commit #{subject.short_id}" } + let(:backref_text) { "commit #{subject.id}" } let(:set_mentionable_text) do ->(txt) { allow(subject).to receive(:safe_message).and_return(txt) } end From 55d0e04ffd3f178f41ae3ef13790eb6882b1ef8e Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 1 Dec 2015 14:26:57 +0100 Subject: [PATCH 144/908] Auto-detect the required gitlab-shell version --- doc/install/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 6164778060..618391e16d 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -312,7 +312,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da GitLab Shell is an SSH access and repository management software developed specially for GitLab. # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.8] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production + sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production # By default, the gitlab-shell config is generated from your main GitLab config. # You can review (and modify) the gitlab-shell config as follows: From f3ea06eb7f8bef748be0882cb0b4fb58deed8eef Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 1 Dec 2015 15:51:27 +0100 Subject: [PATCH 145/908] Autolink first so we don't pick up numeric anchors as issue references. --- app/models/commit.rb | 7 +--- app/models/commit_range.rb | 7 +--- app/models/issue.rb | 7 +--- app/models/merge_request.rb | 7 +--- app/models/snippet.rb | 7 +--- lib/gitlab/closing_issue_extractor.rb | 2 +- lib/gitlab/markdown.rb | 5 +-- .../markdown/abstract_reference_filter.rb | 20 ++++++---- .../markdown/commit_range_reference_filter.rb | 4 +- .../markdown/commit_reference_filter.rb | 4 +- lib/gitlab/markdown/external_link_filter.rb | 7 +--- .../merge_request_reference_filter.rb | 2 +- lib/gitlab/markdown/redactor_filter.rb | 2 +- lib/gitlab/markdown/reference_filter.rb | 23 +++++++++++ lib/gitlab/reference_extractor.rb | 14 +++++-- .../commit_range_reference_filter_spec.rb | 40 +++++++++---------- .../markdown/commit_reference_filter_spec.rb | 36 ++++++++--------- .../markdown/issue_reference_filter_spec.rb | 36 ++++++++--------- .../merge_request_reference_filter_spec.rb | 30 +++++++------- .../markdown/snippet_reference_filter_spec.rb | 32 +++++++-------- spec/support/filter_spec_helper.rb | 19 +++++++-- 21 files changed, 171 insertions(+), 140 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index a5d041a49c..c0998a4570 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -73,11 +73,8 @@ class Commit # This pattern supports cross-project references. def self.reference_pattern %r{ - #{link_reference_pattern} | - (?: - (?:#{Project.reference_pattern}#{reference_prefix})? - (?\h{6,40}) - ) + (?:#{Project.reference_pattern}#{reference_prefix})? + (?\h{6,40}) }x end diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index f786a749b8..b8bf36b32c 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -44,11 +44,8 @@ class CommitRange # This pattern supports cross-project references. def self.reference_pattern %r{ - #{link_reference_pattern} | - (?: - (?:#{Project.reference_pattern}#{reference_prefix})? - (?#{STRICT_PATTERN}) - ) + (?:#{Project.reference_pattern}#{reference_prefix})? + (?#{STRICT_PATTERN}) }x end diff --git a/app/models/issue.rb b/app/models/issue.rb index e62acfdfd9..187b6482b6 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -64,11 +64,8 @@ class Issue < ActiveRecord::Base # This pattern supports cross-project references. def self.reference_pattern %r{ - #{link_reference_pattern} | - (?: - (#{Project.reference_pattern})? - #{Regexp.escape(reference_prefix)}(?\d+) - ) + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?\d+) }x end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c1d3874ade..2a4aee7e5d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -146,11 +146,8 @@ class MergeRequest < ActiveRecord::Base # This pattern supports cross-project references. def self.reference_pattern %r{ - #{link_reference_pattern} | - (?: - (#{Project.reference_pattern})? - #{Regexp.escape(reference_prefix)}(?\d+) - ) + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?\d+) }x end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 8ec12ddf6e..f876be7a4c 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -60,11 +60,8 @@ class Snippet < ActiveRecord::Base # This pattern supports cross-project references. def self.reference_pattern %r{ - #{link_reference_pattern} | - (?: - (#{Project.reference_pattern})? - #{Regexp.escape(reference_prefix)}(?\d+) - ) + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?\d+) }x end diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb index 70b9943d7e..0cf4c91873 100644 --- a/lib/gitlab/closing_issue_extractor.rb +++ b/lib/gitlab/closing_issue_extractor.rb @@ -2,7 +2,7 @@ module Gitlab class ClosingIssueExtractor ISSUE_CLOSING_REGEX = begin pattern = Gitlab.config.gitlab.issue_closing_pattern - pattern = pattern.sub('%{issue_ref}', "(?:#{Issue.reference_pattern})") + pattern = pattern.sub('%{issue_ref}', "(?:(?:#{Issue.link_reference_pattern})|(?:#{Issue.reference_pattern}))") Regexp.new(pattern).freeze end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index ee458eda02..b082bfc434 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -181,6 +181,8 @@ module Gitlab Gitlab::Markdown::RelativeLinkFilter, Gitlab::Markdown::EmojiFilter, Gitlab::Markdown::TableOfContentsFilter, + Gitlab::Markdown::AutolinkFilter, + Gitlab::Markdown::ExternalLinkFilter, Gitlab::Markdown::UserReferenceFilter, Gitlab::Markdown::IssueReferenceFilter, @@ -191,9 +193,6 @@ module Gitlab Gitlab::Markdown::CommitReferenceFilter, Gitlab::Markdown::LabelReferenceFilter, - Gitlab::Markdown::AutolinkFilter, - Gitlab::Markdown::ExternalLinkFilter, - Gitlab::Markdown::TaskListFilter ] end diff --git a/lib/gitlab/markdown/abstract_reference_filter.rb b/lib/gitlab/markdown/abstract_reference_filter.rb index f6df9518fc..37ed423eed 100644 --- a/lib/gitlab/markdown/abstract_reference_filter.rb +++ b/lib/gitlab/markdown/abstract_reference_filter.rb @@ -37,8 +37,8 @@ module Gitlab # of the external project reference, and all of the matchdata. # # Returns a String replaced with the return of the block. - def self.references_in(text) - text.gsub(object_class.reference_pattern) do |match| + def self.references_in(text, pattern = object_class.reference_pattern) + text.gsub(pattern) do |match| yield match, $~[object_sym].to_i, $~[:project], $~ end end @@ -61,7 +61,11 @@ module Gitlab def call replace_text_nodes_matching(object_class.reference_pattern) do |content| - object_link_filter(content) + object_link_filter(content, object_class.reference_pattern) + end + + replace_link_nodes_matching(object_class.link_reference_pattern) do |content| + object_link_filter(content, object_class.link_reference_pattern) end end @@ -72,15 +76,17 @@ module Gitlab # # Returns a String with references replaced with links. All links # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. - def object_link_filter(text) - references_in(text) do |match, id, project_ref, matches| + def object_link_filter(text, pattern) + references_in(text, pattern) do |match, id, project_ref, matches| project = project_from_ref(project_ref) if project && object = find_object(project, id) title = escape_once(object_link_title(object)) klass = reference_class(object_sym) data = data_attribute(project: project.id, object_sym => object.id, original: match) - url = matches[:url] || url_for_object(object, project) + + url = matches[:url] if matches.names.include?("url") + url ||= url_for_object(object, project) text = object.reference_link_text(context[:project]) @@ -99,7 +105,7 @@ module Gitlab def object_link_text_extras(object, matches) extras = [] - if matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/ + if matches.names.include?("anchor") && matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/ extras << "comment #{$1}" end diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb index f24bed7619..36b3258ef7 100644 --- a/lib/gitlab/markdown/commit_range_reference_filter.rb +++ b/lib/gitlab/markdown/commit_range_reference_filter.rb @@ -10,8 +10,8 @@ module Gitlab CommitRange end - def self.references_in(text) - text.gsub(CommitRange.reference_pattern) do |match| + def self.references_in(text, pattern = CommitRange.reference_pattern) + text.gsub(pattern) do |match| yield match, $~[:commit_range], $~[:project], $~ end end diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb index cc7abc08c8..b4036578e6 100644 --- a/lib/gitlab/markdown/commit_reference_filter.rb +++ b/lib/gitlab/markdown/commit_reference_filter.rb @@ -10,8 +10,8 @@ module Gitlab Commit end - def self.references_in(text) - text.gsub(Commit.reference_pattern) do |match| + def self.references_in(text, pattern = Commit.reference_pattern) + text.gsub(pattern) do |match| yield match, $~[:commit], $~[:project], $~ end end diff --git a/lib/gitlab/markdown/external_link_filter.rb b/lib/gitlab/markdown/external_link_filter.rb index b679293201..e09dfcb83c 100644 --- a/lib/gitlab/markdown/external_link_filter.rb +++ b/lib/gitlab/markdown/external_link_filter.rb @@ -8,12 +8,9 @@ module Gitlab class ExternalLinkFilter < HTML::Pipeline::Filter def call doc.search('a').each do |node| - next unless node.has_attribute?('href') + link = node.attr('href') - klass = node.attribute('class') - next if klass && klass.include?('gfm') - - link = node.attribute('href').value + next unless link # Skip non-HTTP(S) links next unless link.start_with?('http') diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb index 3780a14a13..de71fc76a9 100644 --- a/lib/gitlab/markdown/merge_request_reference_filter.rb +++ b/lib/gitlab/markdown/merge_request_reference_filter.rb @@ -24,7 +24,7 @@ module Gitlab def object_link_text_extras(object, matches) extras = super - if matches[:path] && matches[:path] == '/diffs' + if matches.names.include?("path") && matches[:path] && matches[:path] == '/diffs' extras.unshift "diffs" end diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/gitlab/markdown/redactor_filter.rb index 2a58c798f9..bea714a01e 100644 --- a/lib/gitlab/markdown/redactor_filter.rb +++ b/lib/gitlab/markdown/redactor_filter.rb @@ -14,7 +14,7 @@ module Gitlab unless user_can_reference?(node) # The reference should be replaced by the original text, # which is not always the same as the rendered text. - text = node.attribute('data-original') || node.text + text = node.attr('data-original') || node.text node.replace(text) end end diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index a4c560f578..e52633ee74 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -122,6 +122,29 @@ module Gitlab doc end + def replace_link_nodes_matching(pattern) + return doc if project.nil? + + doc.search('a').each do |node| + klass = node.attr('class') + next if klass && klass.include?('gfm') + + link = node.attr('href') + text = node.text + + # Ignore ending punctionation like periods or commas + next unless link == text && text =~ /\A#{pattern}/ + + html = yield text + + next if html == text + + node.replace(html) + end + + doc + end + # Ensure that a :project key exists in context # # Note that while the key might exist, its value could be nil! diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index da8df8a302..3c3478a127 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -41,14 +41,14 @@ module Gitlab # Returns the results Array for the requested filter type def pipeline_result(filter_type) return [] if @text.blank? - + klass = "#{filter_type.to_s.camelize}ReferenceFilter" filter = Gitlab::Markdown.const_get(klass) context = { project: project, current_user: current_user, - + # We don't actually care about the links generated only_path: true, ignore_blockquotes: true, @@ -58,7 +58,15 @@ module Gitlab reference_filter: filter } - pipeline = HTML::Pipeline.new([filter, Gitlab::Markdown::ReferenceGathererFilter], context) + # We need to autolink first to finds links to referables, and to prevent + # numeric anchors to be parsed as issue references. + filters = [ + Gitlab::Markdown::AutolinkFilter, + filter, + Gitlab::Markdown::ReferenceGathererFilter + ] + + pipeline = HTML::Pipeline.new(filters, context) result = pipeline.call(@text) values = result[:references][filter_type].uniq diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb index f65a3e8a0b..9215838279 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -18,7 +18,7 @@ module Gitlab::Markdown %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do exp = act = "<#{elem}>Commit Range #{range.to_reference}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end @@ -27,14 +27,14 @@ module Gitlab::Markdown let(:reference2) { range2.to_reference } it 'links to a valid two-dot reference' do - doc = filter("See #{reference2}") + doc = reference_filter("See #{reference2}") expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param) end it 'links to a valid three-dot reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param) @@ -46,12 +46,12 @@ module Gitlab::Markdown exp = commit1.short_id + '...' + commit2.short_id - expect(filter("See #{reference}").css('a').first.text).to eq exp - expect(filter("See #{reference2}").css('a').first.text).to eq exp + expect(reference_filter("See #{reference}").css('a').first.text).to eq exp + expect(reference_filter("See #{reference2}").css('a').first.text).to eq exp end it 'links with adjacent text' do - doc = filter("See (#{reference}.)") + doc = reference_filter("See (#{reference}.)") exp = Regexp.escape(range.reference_link_text) expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) @@ -63,21 +63,21 @@ module Gitlab::Markdown expect(project).to receive(:valid_repo?).and_return(true) expect(project.repository).to receive(:commit).with(commit1.id.reverse) expect(project.repository).to receive(:commit).with(commit2.id) - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'includes a title attribute' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('title')).to eq range.reference_title end it 'includes default classes' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range' end it 'includes a data-project attribute' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-project') @@ -85,7 +85,7 @@ module Gitlab::Markdown end it 'includes a data-commit-range attribute' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-commit-range') @@ -93,7 +93,7 @@ module Gitlab::Markdown end it 'supports an :only_path option' do - doc = filter("See #{reference}", only_path: true) + doc = reference_filter("See #{reference}", only_path: true) link = doc.css('a').first.attr('href') expect(link).not_to match %r(https?://) @@ -116,14 +116,14 @@ module Gitlab::Markdown end it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) end it 'links with adjacent text' do - doc = filter("Fixed (#{reference}.)") + doc = reference_filter("Fixed (#{reference}.)") exp = Regexp.escape("#{project2.to_reference}@#{range.reference_link_text}") expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) @@ -131,10 +131,10 @@ module Gitlab::Markdown it 'ignores invalid commit IDs on the referenced project' do exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'adds to the results hash' do @@ -154,14 +154,14 @@ module Gitlab::Markdown end it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq reference end it 'links with adjacent text' do - doc = filter("Fixed (#{reference}.)") + doc = reference_filter("Fixed (#{reference}.)") exp = Regexp.escape(range.reference_link_text(project)) expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) @@ -169,10 +169,10 @@ module Gitlab::Markdown it 'ignores invalid commit IDs on the referenced project' do exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'adds to the results hash' do diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index 4cc6bbbfe9..6fe9b165ff 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -14,7 +14,7 @@ module Gitlab::Markdown %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do exp = act = "<#{elem}>Commit #{commit.id}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end @@ -24,7 +24,7 @@ module Gitlab::Markdown # Let's test a variety of commit SHA sizes just to be paranoid [6, 8, 12, 18, 20, 32, 40].each do |size| it "links to a valid reference of #{size} characters" do - doc = filter("See #{reference[0...size]}") + doc = reference_filter("See #{reference[0...size]}") expect(doc.css('a').first.text).to eq commit.short_id expect(doc.css('a').first.attr('href')). @@ -33,15 +33,15 @@ module Gitlab::Markdown end it 'always uses the short ID as the link text' do - doc = filter("See #{commit.id}") + doc = reference_filter("See #{commit.id}") expect(doc.text).to eq "See #{commit.short_id}" - doc = filter("See #{commit.id[0...6]}") + doc = reference_filter("See #{commit.id[0...6]}") expect(doc.text).to eq "See #{commit.short_id}" end it 'links with adjacent text' do - doc = filter("See (#{reference}.)") + doc = reference_filter("See (#{reference}.)") expect(doc.to_html).to match(/\(#{commit.short_id}<\/a>\.\)/) end @@ -51,28 +51,28 @@ module Gitlab::Markdown expect(project).to receive(:valid_repo?).and_return(true) expect(project.repository).to receive(:commit).with(invalid) - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'includes a title attribute' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('title')).to eq commit.link_title end it 'escapes the title attribute' do allow_any_instance_of(Commit).to receive(:title).and_return(%{">whatever#{exp}@#{commit.short_id}<\/a>\.\)/) @@ -123,7 +123,7 @@ module Gitlab::Markdown it 'ignores invalid commit IDs on the referenced project' do exp = act = "Committed #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'adds to the results hash' do @@ -139,21 +139,21 @@ module Gitlab::Markdown let(:reference) { urls.namespace_project_commit_url(project2.namespace, project2, commit.id) } it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) end it 'links with adjacent text' do - doc = filter("Fixed (#{reference}.)") + doc = reference_filter("Fixed (#{reference}.)") expect(doc.to_html).to match(/\(#{commit.reference_link_text(project)}<\/a>\.\)/) end it 'ignores invalid commit IDs on the referenced project' do exp = act = "Committed #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to match(/#{Regexp.escape(invalidate_reference(reference))}<\/a>/) end it 'adds to the results hash' do diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb index 296e8868f4..0a741688b8 100644 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -18,7 +18,7 @@ module Gitlab::Markdown %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do exp = act = "<#{elem}>Issue #{issue.to_reference}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end @@ -29,18 +29,18 @@ module Gitlab::Markdown expect(project).to receive(:get_issue).with(issue.iid).and_return(nil) exp = act = "Issue #{reference}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'links to a valid reference' do - doc = filter("Fixed #{reference}") + doc = reference_filter("Fixed #{reference}") expect(doc.css('a').first.attr('href')). to eq helper.url_for_issue(issue.iid, project) end it 'links with adjacent text' do - doc = filter("Fixed (#{reference}.)") + doc = reference_filter("Fixed (#{reference}.)") expect(doc.to_html).to match(/\(#{Regexp.escape(reference)}<\/a>\.\)/) end @@ -48,28 +48,28 @@ module Gitlab::Markdown invalid = invalidate_reference(reference) exp = act = "Fixed #{invalid}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'includes a title attribute' do - doc = filter("Issue #{reference}") + doc = reference_filter("Issue #{reference}") expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}" end it 'escapes the title attribute' do issue.update_attribute(:title, %{">whatever#{Regexp.escape(reference)}<\/a>\.\)/) end it 'ignores invalid issue IDs on the referenced project' do exp = act = "Fixed #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'adds to the results hash' do @@ -147,18 +147,18 @@ module Gitlab::Markdown with(issue.iid).and_return(nil) exp = act = "Issue #{reference}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to match(/#{Regexp.escape(reference)}<\/a>/) end it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq reference end it 'links with adjacent text' do - doc = filter("Fixed (#{reference}.)") + doc = reference_filter("Fixed (#{reference}.)") expect(doc.to_html).to match(/\(#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/) end diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb index ca3e7151e0..cdb3390e79 100644 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -14,7 +14,7 @@ module Gitlab::Markdown %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do exp = act = "<#{elem}>Merge #{merge.to_reference}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end @@ -22,42 +22,42 @@ module Gitlab::Markdown let(:reference) { merge.to_reference } it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. namespace_project_merge_request_url(project.namespace, project, merge) end it 'links with adjacent text' do - doc = filter("Merge (#{reference}.)") + doc = reference_filter("Merge (#{reference}.)") expect(doc.to_html).to match(/\(#{Regexp.escape(reference)}<\/a>\.\)/) end it 'ignores invalid merge IDs' do exp = act = "Merge #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'includes a title attribute' do - doc = filter("Merge #{reference}") + doc = reference_filter("Merge #{reference}") expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}" end it 'escapes the title attribute' do merge.update_attribute(:title, %{">whatever#{Regexp.escape(reference)}<\/a>\.\)/) end it 'ignores invalid merge IDs on the referenced project' do exp = act = "Merge #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'adds to the results hash' do @@ -124,14 +124,14 @@ module Gitlab::Markdown let(:reference) { urls.namespace_project_merge_request_url(project2.namespace, project2, merge) + '/diffs#note_123' } it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq reference end it 'links with adjacent text' do - doc = filter("Merge (#{reference}.)") + doc = reference_filter("Merge (#{reference}.)") expect(doc.to_html).to match(/\(#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/) end diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb index 3080a8a360..73d20957a5 100644 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -15,48 +15,48 @@ module Gitlab::Markdown %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do exp = act = "<#{elem}>Snippet #{reference}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end context 'internal reference' do it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. namespace_project_snippet_url(project.namespace, project, snippet) end it 'links with adjacent text' do - doc = filter("Snippet (#{reference}.)") + doc = reference_filter("Snippet (#{reference}.)") expect(doc.to_html).to match(/\(#{Regexp.escape(reference)}<\/a>\.\)/) end it 'ignores invalid snippet IDs' do exp = act = "Snippet #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'includes a title attribute' do - doc = filter("Snippet #{reference}") + doc = reference_filter("Snippet #{reference}") expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}" end it 'escapes the title attribute' do snippet.update_attribute(:title, %{">whatever#{Regexp.escape(reference)}<\/a>\.\)/) end it 'ignores invalid snippet IDs on the referenced project' do exp = act = "See #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'adds to the results hash' do @@ -122,21 +122,21 @@ module Gitlab::Markdown let(:reference) { urls.namespace_project_snippet_url(project2.namespace, project2, snippet) } it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) end it 'links with adjacent text' do - doc = filter("See (#{reference}.)") + doc = reference_filter("See (#{reference}.)") expect(doc.to_html).to match(/\(#{Regexp.escape(snippet.to_reference(project))}<\/a>\.\)/) end it 'ignores invalid snippet IDs on the referenced project' do exp = act = "See #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to match(/#{Regexp.escape(invalidate_reference(reference))}<\/a>/) end it 'adds to the results hash' do diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb index 97e5c270a5..91e3bee13c 100644 --- a/spec/support/filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -35,11 +35,24 @@ module FilterSpecHelper pipeline.call(body) end - def reference_pipeline_result(body, contexts = {}) + def reference_pipeline(contexts = {}) contexts.reverse_merge!(project: project) if defined?(project) - pipeline = HTML::Pipeline.new([described_class, Gitlab::Markdown::ReferenceGathererFilter], contexts) - pipeline.call(body) + filters = [ + Gitlab::Markdown::AutolinkFilter, + described_class, + Gitlab::Markdown::ReferenceGathererFilter + ] + + HTML::Pipeline.new(filters, contexts) + end + + def reference_pipeline_result(body, contexts = {}) + reference_pipeline(contexts).call(body) + end + + def reference_filter(html, contexts = {}) + reference_pipeline(contexts).to_document(html) end # Modify a String reference to make it invalid From 6dad2bc6e67f47149e8981730cf3f08938794ceb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 1 Dec 2015 16:15:53 +0100 Subject: [PATCH 146/908] Fix referenced_mentionables method. --- app/models/concerns/mentionable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 0d6cd86aad..634a8d0f27 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -67,7 +67,7 @@ module Mentionable # We're using this method instead of Array diffing because that requires # both of the object's `hash` values to be the same, which may not be the # case for otherwise identical Commit objects. - refs.reject! { |ref| ref == local_reference } + refs.reject { |ref| ref == local_reference } end # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. From b0ef603f71e3a55981f15944f16595470091c344 Mon Sep 17 00:00:00 2001 From: Job van der Voort Date: Tue, 1 Dec 2015 16:21:53 +0100 Subject: [PATCH 147/908] up for grabs label --- CONTRIBUTING.md | 10 ++++++++++ PROCESS.md | 1 + 2 files changed, 11 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 446eb1189a..7f5da063fd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,16 @@ The channels people will reach out on can be found on the [getting help page](ht Sign up for the mailinglist, answer GitLab questions on StackOverflow or respond in the IRC channel. You can also sign up on [CodeTriage](http://www.codetriage.com/gitlabhq/gitlabhq) to help with one issue every day. +## I want to contribute! + +If you want to contribute to GitLab, but are not sure where to start, +look for [issues](https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=up-for-grabs) +with the label `up-for-grabs`. +These issues will be of reasonable size and challenge, for anyone to start +contributing to GitLab. + +This was inspired by [an article by Kent C. Dodds](https://medium.com/@kentcdodds/first-timers-only-78281ea47455#.i2f363mx4). + ## Issue tracker To get support for your particular problem please use the [getting help channels](https://about.gitlab.com/getting-help/). diff --git a/PROCESS.md b/PROCESS.md index 482ad5fe9e..72fc348144 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -44,6 +44,7 @@ Workflow labels are purposely not very detailed since that would be hard to keep - *UX* needs needs help from a UX designer - *Frontend* needs help from a Front-end engineer - *Graphics* needs help from a Graphics designer +- *up-for-grabs* is an issue suitable for first-time contributors, of reasonable difficulty and size. Not exclusive with other labels. Example workflow: when a UX designer provided a design but it needs frontend work they remove the UX label and add the frontend label. From 1d6d757dbd563500671f57f45faa808510a612d1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 1 Dec 2015 16:25:56 +0100 Subject: [PATCH 148/908] Allow reference format as link href --- lib/gitlab/markdown.rb | 3 +- .../markdown/abstract_reference_filter.rb | 22 +++++++++----- .../external_issue_reference_filter.rb | 10 +++++-- lib/gitlab/markdown/label_reference_filter.rb | 10 +++++-- lib/gitlab/markdown/reference_filter.rb | 29 ++++++++++++++++++- lib/gitlab/markdown/relative_link_filter.rb | 3 ++ lib/gitlab/markdown/user_reference_filter.rb | 28 ++++++++++-------- 7 files changed, 80 insertions(+), 25 deletions(-) diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index b082bfc434..886a09f52a 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -178,7 +178,6 @@ module Gitlab Gitlab::Markdown::SanitizationFilter, Gitlab::Markdown::UploadLinkFilter, - Gitlab::Markdown::RelativeLinkFilter, Gitlab::Markdown::EmojiFilter, Gitlab::Markdown::TableOfContentsFilter, Gitlab::Markdown::AutolinkFilter, @@ -193,6 +192,8 @@ module Gitlab Gitlab::Markdown::CommitReferenceFilter, Gitlab::Markdown::LabelReferenceFilter, + Gitlab::Markdown::RelativeLinkFilter, + Gitlab::Markdown::TaskListFilter ] end diff --git a/lib/gitlab/markdown/abstract_reference_filter.rb b/lib/gitlab/markdown/abstract_reference_filter.rb index 37ed423eed..b044a73ed1 100644 --- a/lib/gitlab/markdown/abstract_reference_filter.rb +++ b/lib/gitlab/markdown/abstract_reference_filter.rb @@ -2,7 +2,7 @@ require 'gitlab/markdown' module Gitlab module Markdown - # Issues, Snippets, Merge Requests, Commits and Commit Ranges share + # Issues, Merge Requests, Snippets, Commits and Commit Ranges share # similar functionality in refernce filtering. class AbstractReferenceFilter < ReferenceFilter include CrossProjectReference @@ -64,8 +64,13 @@ module Gitlab object_link_filter(content, object_class.reference_pattern) end - replace_link_nodes_matching(object_class.link_reference_pattern) do |content| - object_link_filter(content, object_class.link_reference_pattern) + replace_link_nodes_with_href(object_class.reference_pattern) do |link, text| + object_link_filter(link, object_class.reference_pattern, link_text: text) + end + + replace_link_nodes_with_text(object_class.link_reference_pattern) do |text| + object_link_filter(text, object_class.link_reference_pattern) + end end end @@ -76,7 +81,7 @@ module Gitlab # # Returns a String with references replaced with links. All links # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. - def object_link_filter(text, pattern) + def object_link_filter(text, pattern, link_text: nil) references_in(text, pattern) do |match, id, project_ref, matches| project = project_from_ref(project_ref) @@ -88,10 +93,13 @@ module Gitlab url = matches[:url] if matches.names.include?("url") url ||= url_for_object(object, project) - text = object.reference_link_text(context[:project]) + text = link_text + unless text + text = object.reference_link_text(context[:project]) - extras = object_link_text_extras(object, matches) - text += " (#{extras.join(", ")})" if extras.any? + extras = object_link_text_extras(object, matches) + text += " (#{extras.join(", ")})" if extras.any? + end %(#{match}) + class="#{klass}">#{text}) end end diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb index 618acb7a57..4d0507b607 100644 --- a/lib/gitlab/markdown/label_reference_filter.rb +++ b/lib/gitlab/markdown/label_reference_filter.rb @@ -30,6 +30,10 @@ module Gitlab replace_text_nodes_matching(Label.reference_pattern) do |content| label_link_filter(content) end + + replace_link_nodes_with_href(Label.reference_pattern) do |link, text| + label_link_filter(link, link_text: text) + end end # Replace label references in text with links to the label specified. @@ -38,7 +42,7 @@ module Gitlab # # Returns a String with label references replaced with links. All links # have `gfm` and `gfm-label` class names attached for styling. - def label_link_filter(text) + def label_link_filter(text, link_text: nil) project = context[:project] self.class.references_in(text) do |match, id, name| @@ -49,8 +53,10 @@ module Gitlab klass = reference_class(:label) data = data_attribute(project: project.id, label: label.id) + text = link_text || render_colored_label(label) + %(#{render_colored_label(label)}) + class="#{klass}">#{text}) else match end diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index e52633ee74..2597784c7a 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -122,7 +122,7 @@ module Gitlab doc end - def replace_link_nodes_matching(pattern) + def replace_link_nodes_with_text(pattern) return doc if project.nil? doc.search('a').each do |node| @@ -132,6 +132,9 @@ module Gitlab link = node.attr('href') text = node.text + next unless link && text + + link = URI.decode(link) # Ignore ending punctionation like periods or commas next unless link == text && text =~ /\A#{pattern}/ @@ -145,6 +148,30 @@ module Gitlab doc end + def replace_link_nodes_with_href(pattern) + return doc if project.nil? + + doc.search('a').each do |node| + klass = node.attr('class') + next if klass && klass.include?('gfm') + + link = node.attr('href') + text = node.text + + next unless link && text + link = URI.decode(link) + next unless link && link =~ /\A#{pattern}\z/ + + html = yield link, text + + next if html == link + + node.replace(html) + end + + doc + end + # Ensure that a :project key exists in context # # Note that while the key might exist, its value could be nil! diff --git a/lib/gitlab/markdown/relative_link_filter.rb b/lib/gitlab/markdown/relative_link_filter.rb index 632be4d754..692c51fd32 100644 --- a/lib/gitlab/markdown/relative_link_filter.rb +++ b/lib/gitlab/markdown/relative_link_filter.rb @@ -17,6 +17,9 @@ module Gitlab return doc unless linkable_files? doc.search('a').each do |el| + klass = el.attr('class') + next if klass && klass.include?('gfm') + process_link_attr el.attribute('href') end diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb index ab5e1f6fe9..0a20d9c034 100644 --- a/lib/gitlab/markdown/user_reference_filter.rb +++ b/lib/gitlab/markdown/user_reference_filter.rb @@ -52,6 +52,10 @@ module Gitlab replace_text_nodes_matching(User.reference_pattern) do |content| user_link_filter(content) end + + replace_link_nodes_with_href(User.reference_pattern) do |link, text| + user_link_filter(link, link_text: text) + end end # Replace `@user` user references in text with links to the referenced @@ -61,12 +65,12 @@ module Gitlab # # Returns a String with `@user` references replaced with links. All links # have `gfm` and `gfm-project_member` class names attached for styling. - def user_link_filter(text) + def user_link_filter(text, link_text: nil) self.class.references_in(text) do |match, username| if username == 'all' - link_to_all + link_to_all(link_text: link_text) elsif namespace = Namespace.find_by(path: username) - link_to_namespace(namespace) || match + link_to_namespace(namespace, link_text: link_text) || match else match end @@ -83,36 +87,36 @@ module Gitlab reference_class(:project_member) end - def link_to_all + def link_to_all(link_text: nil) project = context[:project] url = urls.namespace_project_url(project.namespace, project, only_path: context[:only_path]) data = data_attribute(project: project.id) - text = User.reference_prefix + 'all' + text = link_text || User.reference_prefix + 'all' link_tag(url, data, text) end - def link_to_namespace(namespace) + def link_to_namespace(namespace, link_text: nil) if namespace.is_a?(Group) - link_to_group(namespace.path, namespace) + link_to_group(namespace.path, namespace, link_text: link_text) else - link_to_user(namespace.path, namespace) + link_to_user(namespace.path, namespace, link_text: link_text) end end - def link_to_group(group, namespace) + def link_to_group(group, namespace, link_text: nil) url = urls.group_url(group, only_path: context[:only_path]) data = data_attribute(group: namespace.id) - text = Group.reference_prefix + group + text = link_text || Group.reference_prefix + group link_tag(url, data, text) end - def link_to_user(user, namespace) + def link_to_user(user, namespace, link_text: nil) url = urls.user_url(user, only_path: context[:only_path]) data = data_attribute(user: namespace.owner_id) - text = User.reference_prefix + user + text = link_text || User.reference_prefix + user link_tag(url, data, text) end From d4030a845eebcb913a7aac1e8fd502706669d0cc Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 1 Dec 2015 16:26:05 +0100 Subject: [PATCH 149/908] Pick up direct links to issues/MRs as references. --- .../markdown/abstract_reference_filter.rb | 17 ++++- .../commit_range_reference_filter_spec.rb | 2 +- .../markdown/commit_reference_filter_spec.rb | 2 +- .../markdown/issue_reference_filter_spec.rb | 58 ++++++++++++--- .../markdown/label_reference_filter_spec.rb | 71 ++++++++++++++----- .../merge_request_reference_filter_spec.rb | 2 +- .../markdown/snippet_reference_filter_spec.rb | 2 +- .../markdown/user_reference_filter_spec.rb | 51 +++++++++---- 8 files changed, 162 insertions(+), 43 deletions(-) diff --git a/lib/gitlab/markdown/abstract_reference_filter.rb b/lib/gitlab/markdown/abstract_reference_filter.rb index b044a73ed1..0ec55c0207 100644 --- a/lib/gitlab/markdown/abstract_reference_filter.rb +++ b/lib/gitlab/markdown/abstract_reference_filter.rb @@ -60,17 +60,27 @@ module Gitlab end def call + # `#123` replace_text_nodes_matching(object_class.reference_pattern) do |content| object_link_filter(content, object_class.reference_pattern) end + # `[Issue](#123)`, which is turned into + # `Issue` replace_link_nodes_with_href(object_class.reference_pattern) do |link, text| object_link_filter(link, object_class.reference_pattern, link_text: text) end + # `http://gitlab.example.com/namespace/project/issues/123`, which is turned into + # `http://gitlab.example.com/namespace/project/issues/123` replace_link_nodes_with_text(object_class.link_reference_pattern) do |text| object_link_filter(text, object_class.link_reference_pattern) end + + # `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into + # `Issue` + replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text| + object_link_filter(link, object_class.link_reference_pattern, link_text: text) end end @@ -88,7 +98,12 @@ module Gitlab if project && object = find_object(project, id) title = escape_once(object_link_title(object)) klass = reference_class(object_sym) - data = data_attribute(project: project.id, object_sym => object.id, original: match) + + data = data_attribute( + original: link_text || match, + project: project.id, + object_sym => object.id + ) url = matches[:url] if matches.names.include?("url") url ||= url_for_object(object, project) diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb index 9215838279..9ce63f9af4 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -143,7 +143,7 @@ module Gitlab::Markdown end end - context 'URL cross-project reference' do + context 'cross-project URL reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:project, :public, namespace: namespace) } let(:range) { CommitRange.new("#{commit1.id}...master", project) } diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index 6fe9b165ff..78a3603269 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -132,7 +132,7 @@ module Gitlab::Markdown end end - context 'URL cross-project reference' do + context 'cross-project URL reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:project, :public, namespace: namespace) } let(:commit) { project2.commit } diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb index 0a741688b8..078ff3ed4b 100644 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -136,20 +136,12 @@ module Gitlab::Markdown end end - context 'URL cross-project reference' do + context 'cross-project URL reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:empty_project, :public, namespace: namespace) } let(:issue) { create(:issue, project: project2) } let(:reference) { helper.url_for_issue(issue.iid, project2) + "#note_123" } - it 'ignores valid references when cross-reference project uses external tracker' do - expect_any_instance_of(Project).to receive(:get_issue). - with(issue.iid).and_return(nil) - - exp = act = "Issue #{reference}" - expect(reference_filter(act).to_html).to match(/#{Regexp.escape(reference)}<\/a>/) - end - it 'links to a valid reference' do doc = reference_filter("See #{reference}") @@ -167,5 +159,53 @@ module Gitlab::Markdown expect(result[:references][:issue]).to eq [issue] end end + + context 'cross-project reference in link href' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { %Q{Reference} } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(Reference<\/a>\.\)/) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end + end + + context 'cross-project URL in link href' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { %Q{Reference} } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + "#note_123" + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(Reference<\/a>\.\)/) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end + end end end diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb index fc21b65a84..ef6dd524ab 100644 --- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb @@ -16,17 +16,17 @@ module Gitlab::Markdown %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do exp = act = "<#{elem}>Label #{reference}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end it 'includes default classes' do - doc = filter("Label #{reference}") + doc = reference_filter("Label #{reference}") expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label' end it 'includes a data-project attribute' do - doc = filter("Label #{reference}") + doc = reference_filter("Label #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-project') @@ -34,7 +34,7 @@ module Gitlab::Markdown end it 'includes a data-label attribute' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-label') @@ -42,7 +42,7 @@ module Gitlab::Markdown end it 'supports an :only_path context' do - doc = filter("Label #{reference}", only_path: true) + doc = reference_filter("Label #{reference}", only_path: true) link = doc.css('a').first.attr('href') expect(link).not_to match %r(https?://) @@ -56,33 +56,33 @@ module Gitlab::Markdown describe 'label span element' do it 'includes default classes' do - doc = filter("Label #{reference}") + doc = reference_filter("Label #{reference}") expect(doc.css('a span').first.attr('class')).to eq 'label color-label' end it 'includes a style attribute' do - doc = filter("Label #{reference}") + doc = reference_filter("Label #{reference}") expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/) end end context 'Integer-based references' do it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. namespace_project_issues_url(project.namespace, project, label_name: label.name) end it 'links with adjacent text' do - doc = filter("Label (#{reference}.)") + doc = reference_filter("Label (#{reference}.)") expect(doc.to_html).to match(%r(\(#{label.name}\.\))) end it 'ignores invalid label IDs' do exp = act = "Label #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end @@ -91,7 +91,7 @@ module Gitlab::Markdown let(:reference) { "#{Label.reference_prefix}#{label.name}" } it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. namespace_project_issues_url(project.namespace, project, label_name: label.name) @@ -99,14 +99,14 @@ module Gitlab::Markdown end it 'links with adjacent text' do - doc = filter("Label (#{reference}.)") + doc = reference_filter("Label (#{reference}.)") expect(doc.to_html).to match(%r(\(#{label.name}\.\))) end it 'ignores invalid label names' do exp = act = "Label #{Label.reference_prefix}#{label.name.reverse}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end @@ -115,7 +115,7 @@ module Gitlab::Markdown let(:reference) { label.to_reference(:name) } it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. namespace_project_issues_url(project.namespace, project, label_name: label.name) @@ -123,21 +123,58 @@ module Gitlab::Markdown end it 'links with adjacent text' do - doc = filter("Label (#{reference}.)") + doc = reference_filter("Label (#{reference}.)") expect(doc.to_html).to match(%r(\(#{label.name}\.\))) end it 'ignores invalid label names' do exp = act = %(Label #{Label.reference_prefix}"#{label.name.reverse}") - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end describe 'edge cases' do it 'gracefully handles non-references matching the pattern' do exp = act = '(format nil "~0f" 3.0) ; 3.0' - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'referencing a label in a link href' do + let(:reference) { %Q{Label} } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + end + + it 'links with adjacent text' do + doc = reference_filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(Label\.\))) + end + + it 'includes a data-project attribute' do + doc = reference_filter("Label #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-label attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-label') + expect(link.attr('data-label')).to eq label.id.to_s + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Label #{reference}") + expect(result[:references][:label]).to eq [label] end end end diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb index cdb3390e79..4a23205112 100644 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -117,7 +117,7 @@ module Gitlab::Markdown end end - context 'URL cross-project reference' do + context 'cross-project URL reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:project, :public, namespace: namespace) } let(:merge) { create(:merge_request, source_project: project2, target_project: project2) } diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb index 73d20957a5..b6f05710c3 100644 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -115,7 +115,7 @@ module Gitlab::Markdown end end - context 'URL cross-project reference' do + context 'cross-project URL reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:empty_project, :public, namespace: namespace) } let(:snippet) { create(:project_snippet, project: project2) } diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb index d9e0d7c42d..25379f0670 100644 --- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb @@ -14,13 +14,13 @@ module Gitlab::Markdown it 'ignores invalid users' do exp = act = "Hey #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq(exp) + expect(reference_filter(act).to_html).to eq(exp) end %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do exp = act = "<#{elem}>Hey #{reference}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end @@ -32,7 +32,7 @@ module Gitlab::Markdown end it 'supports a special @all mention' do - doc = filter("Hey #{reference}") + doc = reference_filter("Hey #{reference}") expect(doc.css('a').length).to eq 1 expect(doc.css('a').first.attr('href')) .to eq urls.namespace_project_url(project.namespace, project) @@ -46,26 +46,26 @@ module Gitlab::Markdown context 'mentioning a user' do it 'links to a User' do - doc = filter("Hey #{reference}") + doc = reference_filter("Hey #{reference}") expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) end it 'links to a User with a period' do user = create(:user, name: 'alphA.Beta') - doc = filter("Hey #{user.to_reference}") + doc = reference_filter("Hey #{user.to_reference}") expect(doc.css('a').length).to eq 1 end it 'links to a User with an underscore' do user = create(:user, name: 'ping_pong_king') - doc = filter("Hey #{user.to_reference}") + doc = reference_filter("Hey #{user.to_reference}") expect(doc.css('a').length).to eq 1 end it 'includes a data-user attribute' do - doc = filter("Hey #{reference}") + doc = reference_filter("Hey #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-user') @@ -83,12 +83,12 @@ module Gitlab::Markdown let(:reference) { group.to_reference } it 'links to the Group' do - doc = filter("Hey #{reference}") + doc = reference_filter("Hey #{reference}") expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) end it 'includes a data-group attribute' do - doc = filter("Hey #{reference}") + doc = reference_filter("Hey #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-group') @@ -102,21 +102,48 @@ module Gitlab::Markdown end it 'links with adjacent text' do - doc = filter("Mention me (#{reference}.)") + doc = reference_filter("Mention me (#{reference}.)") expect(doc.to_html).to match(/\(#{reference}<\/a>\.\)/) end it 'includes default classes' do - doc = filter("Hey #{reference}") + doc = reference_filter("Hey #{reference}") expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' end it 'supports an :only_path context' do - doc = filter("Hey #{reference}", only_path: true) + doc = reference_filter("Hey #{reference}", only_path: true) link = doc.css('a').first.attr('href') expect(link).not_to match %r(https?://) expect(link).to eq urls.user_path(user) end + + context 'referencing a user in a link href' do + let(:reference) { %Q{User} } + + it 'links to a User' do + doc = reference_filter("Hey #{reference}") + expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) + end + + it 'links with adjacent text' do + doc = reference_filter("Mention me (#{reference}.)") + expect(doc.to_html).to match(/\(User<\/a>\.\)/) + end + + it 'includes a data-user attribute' do + doc = reference_filter("Hey #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-user') + expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Hey #{reference}") + expect(result[:references][:user]).to eq [user] + end + end end end From c07f0fa735ba0d4cd926e601ebb3a40cfa197e21 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 1 Dec 2015 17:04:32 +0100 Subject: [PATCH 150/908] Add new references to markdown feature spec. --- spec/fixtures/markdown.md.erb | 17 +++++++++++++++++ spec/support/matchers/markdown_matchers.rb | 14 +++++++------- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 41d12afa9c..76e733165c 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -153,6 +153,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Ignores invalid: <%= User.reference_prefix %>fake_user - Ignored in code: `<%= user.to_reference %>` - Ignored in links: [Link to <%= user.to_reference %>](#user-link) +- Link to user by reference: [User](<%= user.to_reference %>) #### IssueReferenceFilter @@ -160,6 +161,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Issue in another project: <%= xissue.to_reference(project) %> - Ignored in code: `<%= issue.to_reference %>` - Ignored in links: [Link to <%= issue.to_reference %>](#issue-link) +- Issue by URL: <%= Gitlab.config.gitlab.url %>/<%= issue.project.to_reference %>/issues/<%= issue.iid %> +- Link to issue by reference: [Issue](<%= issue.to_reference %>) +- Link to issue by URL: [Issue](<%= Gitlab.config.gitlab.url %>/<%= issue.project.to_reference %>/issues/<%= issue.iid %>) #### MergeRequestReferenceFilter @@ -167,6 +171,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Merge request in another project: <%= xmerge_request.to_reference(project) %> - Ignored in code: `<%= merge_request.to_reference %>` - Ignored in links: [Link to <%= merge_request.to_reference %>](#merge-request-link) +- Merge request by URL: <%= Gitlab.config.gitlab.url %>/<%= merge_request.project.to_reference %>/merge_requests/<%= merge_request.iid %> +- Link to merge request by reference: [Merge request](<%= merge_request.to_reference %>) +- Link to merge request by URL: [Merge request](<%= Gitlab.config.gitlab.url %>/<%= merge_request.project.to_reference %>/merge_requests/<%= merge_request.iid %>) #### SnippetReferenceFilter @@ -174,6 +181,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Snippet in another project: <%= xsnippet.to_reference(project) %> - Ignored in code: `<%= snippet.to_reference %>` - Ignored in links: [Link to <%= snippet.to_reference %>](#snippet-link) +- Snippet by URL: <%= Gitlab.config.gitlab.url %>/<%= snippet.project.to_reference %>/snippets/<%= snippet.id %> +- Link to snippet by reference: [Snippet](<%= snippet.to_reference %>) +- Link to snippet by URL: [Snippet](<%= Gitlab.config.gitlab.url %>/<%= snippet.project.to_reference %>/snippets/<%= snippet.id %>) #### CommitRangeReferenceFilter @@ -181,6 +191,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Range in another project: <%= xcommit_range.to_reference(project) %> - Ignored in code: `<%= commit_range.to_reference %>` - Ignored in links: [Link to <%= commit_range.to_reference %>](#commit-range-link) +- Range by URL: <%= Gitlab.config.gitlab.url %>/<%= commit_range.project.to_reference %>/compare/<%= commit_range.id %> +- Link to range by reference: [Range](<%= commit_range.to_reference %>) +- Link to range by URL: [Range](<%= Gitlab.config.gitlab.url %>/<%= commit_range.project.to_reference %>/compare/<%= commit_range.id %>) #### CommitReferenceFilter @@ -188,6 +201,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Commit in another project: <%= xcommit.to_reference(project) %> - Ignored in code: `<%= commit.to_reference %>` - Ignored in links: [Link to <%= commit.to_reference %>](#commit-link) +- Commit by URL: <%= Gitlab.config.gitlab.url %>/<%= commit.project.to_reference %>/commit/<%= commit.id %> +- Link to commit by reference: [Commit](<%= commit.to_reference %>) +- Link to commit by URL: [Commit](<%= Gitlab.config.gitlab.url %>/<%= commit.project.to_reference %>/commit/<%= commit.id %>) #### LabelReferenceFilter @@ -196,6 +212,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Label by name in quotes: <%= label.to_reference(:name) %> - Ignored in code: `<%= simple_label.to_reference %>` - Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link) +- Link to label by reference: [Label](<%= label.to_reference %>) ### Task Lists diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 7500d0fdf8..7eadcd58c1 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -71,7 +71,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-project_member', count: 3) + expect(actual).to have_selector('a.gfm.gfm-project_member', count: 4) end end @@ -80,7 +80,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-issue', count: 3) + expect(actual).to have_selector('a.gfm.gfm-issue', count: 6) end end @@ -89,7 +89,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-merge_request', count: 3) + expect(actual).to have_selector('a.gfm.gfm-merge_request', count: 6) expect(actual).to have_selector('em a.gfm-merge_request') end end @@ -99,7 +99,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-snippet', count: 2) + expect(actual).to have_selector('a.gfm.gfm-snippet', count: 5) end end @@ -108,7 +108,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-commit_range', count: 2) + expect(actual).to have_selector('a.gfm.gfm-commit_range', count: 5) end end @@ -117,7 +117,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-commit', count: 2) + expect(actual).to have_selector('a.gfm.gfm-commit', count: 5) end end @@ -126,7 +126,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-label', count: 3) + expect(actual).to have_selector('a.gfm.gfm-label', count: 4) end end From 2f5074dc395c784f91abb3bacd23e75ce080a547 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 1 Dec 2015 17:13:47 +0100 Subject: [PATCH 151/908] Expand inline docs. --- .../markdown/abstract_reference_filter.rb | 6 +++-- lib/gitlab/markdown/reference_filter.rb | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/markdown/abstract_reference_filter.rb b/lib/gitlab/markdown/abstract_reference_filter.rb index 0ec55c0207..9488e980c0 100644 --- a/lib/gitlab/markdown/abstract_reference_filter.rb +++ b/lib/gitlab/markdown/abstract_reference_filter.rb @@ -3,7 +3,7 @@ require 'gitlab/markdown' module Gitlab module Markdown # Issues, Merge Requests, Snippets, Commits and Commit Ranges share - # similar functionality in refernce filtering. + # similar functionality in reference filtering. class AbstractReferenceFilter < ReferenceFilter include CrossProjectReference @@ -88,6 +88,8 @@ module Gitlab # to the referenced object's details page. # # text - String text to replace references in. + # pattern - Reference pattern to match against. + # link_text - Original content of the link being replaced. # # Returns a String with references replaced with links. All links # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. @@ -98,7 +100,7 @@ module Gitlab if project && object = find_object(project, id) title = escape_once(object_link_title(object)) klass = reference_class(object_sym) - + data = data_attribute( original: link_text || match, project: project.id, diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index 2597784c7a..b6d93e05ec 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -122,6 +122,18 @@ module Gitlab doc end + # Iterate through the document's link nodes, yielding the current node's + # content if: + # + # * The `project` context value is present AND + # * The node's content matches `pattern` + # + # pattern - Regex pattern against which to match the node's content + # + # Yields the current node's String contents. The result of the block will + # replace the node and update the current document. + # + # Returns the updated Nokogiri::HTML::DocumentFragment object. def replace_link_nodes_with_text(pattern) return doc if project.nil? @@ -148,6 +160,18 @@ module Gitlab doc end + # Iterate through the document's link nodes, yielding the current node's + # content if: + # + # * The `project` context value is present AND + # * The node's HREF matches `pattern` + # + # pattern - Regex pattern against which to match the node's HREF + # + # Yields the current node's String HREF and String content. + # The result of the block will replace the node and update the current document. + # + # Returns the updated Nokogiri::HTML::DocumentFragment object. def replace_link_nodes_with_href(pattern) return doc if project.nil? From 9ab7bdf7739935bf79c2e033212726a4be421a26 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 1 Dec 2015 19:45:58 -0200 Subject: [PATCH 152/908] Signed in admin should be able to add/remove himself to a group --- app/models/ability.rb | 6 ++---- features/admin/groups.feature | 16 ++++++++++++++++ features/steps/admin/groups.rb | 29 +++++++++++++++++++++++++++++ features/steps/shared/group.rb | 4 ++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 07f3a56ec7..10c41306c5 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -346,12 +346,10 @@ class Ability unless group.last_owner?(target_user) can_manage = group_abilities(user, group).include?(:admin_group_member) - if can_manage && user != target_user + if can_manage rules << :update_group_member rules << :destroy_group_member - end - - if user == target_user + elsif user == target_user rules << :destroy_group_member end end diff --git a/features/admin/groups.feature b/features/admin/groups.feature index 973918086a..2edb3964f7 100644 --- a/features/admin/groups.feature +++ b/features/admin/groups.feature @@ -33,3 +33,19 @@ Feature: Admin Groups When I visit admin group page When I select user "johndoe@gitlab.com" from user list as "Reporter" Then I should see "johndoe@gitlab.com" in team list in every project as "Reporter" + + @javascript + Scenario: Signed in admin should be able to add himself to a group + Given "John Doe" is owner of group "Owned" + When I visit group "Owned" members page + When I select current user as "Developer" + Then I should see current user as "Developer" + + @javascript + Scenario: Signed in admin should be able to remove himself from group + Given current user is developer of group "Owned" + When I visit group "Owned" members page + Then I should see current user as "Developer" + When I click on the "Remove User From Group" button for current user + When I visit group "Owned" members page + Then I should not see current user as "Developer" diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb index d27634858a..43fd91d0d4 100644 --- a/features/steps/admin/groups.rb +++ b/features/steps/admin/groups.rb @@ -1,5 +1,6 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps include SharedAuthentication + include SharedGroup include SharedPaths include SharedUser include SharedActiveTab @@ -88,6 +89,34 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps end end + step 'I select current user as "Developer"' do + page.within ".users-group-form" do + select2(current_user.id, from: "#user_ids", multiple: true) + select "Developer", from: "access_level" + end + + click_button "Add users to group" + end + + step 'I should see current user as "Developer"' do + page.within '.content-list' do + expect(page).to have_content(current_user.name) + expect(page).to have_content('Developer') + end + end + + step 'I click on the "Remove User From Group" button for current user' do + find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click + # poltergeist always confirms popups. + end + + step 'I should not see current user as "Developer"' do + page.within '.content-list' do + expect(page).not_to have_content(current_user.name) + expect(page).not_to have_content('Developer') + end + end + protected def current_group diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb index 58581653f2..fe6736dacd 100644 --- a/features/steps/shared/group.rb +++ b/features/steps/shared/group.rb @@ -1,6 +1,10 @@ module SharedGroup include Spinach::DSL + step 'current user is developer of group "Owned"' do + is_member_of(current_user.name, "Owned", Gitlab::Access::DEVELOPER) + end + step '"John Doe" is owner of group "Owned"' do is_member_of("John Doe", "Owned", Gitlab::Access::OWNER) end From 67cc6b0642573fe443126042dd36b15f05bc539c Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 1 Dec 2015 20:47:26 -0200 Subject: [PATCH 153/908] Signed in admin should be able to add/remove himself to a project --- app/models/ability.rb | 6 ++---- features/admin/projects.feature | 16 ++++++++++++++ features/steps/admin/projects.rb | 37 ++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 10c41306c5..cd5ae0fb0f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -365,12 +365,10 @@ class Ability unless target_user == project.owner can_manage = project_abilities(user, project).include?(:admin_project_member) - if can_manage && user != target_user + if can_manage rules << :update_project_member rules << :destroy_project_member - end - - if user == target_user + elsif user == target_user rules << :destroy_project_member end end diff --git a/features/admin/projects.feature b/features/admin/projects.feature index f7cec04eb7..c5ee80136c 100644 --- a/features/admin/projects.feature +++ b/features/admin/projects.feature @@ -27,3 +27,19 @@ Feature: Admin Projects And I visit admin project page When I transfer project to group 'Web' Then I should see project transfered + + @javascript + Scenario: Signed in admin should be able to add himself to a project + Given "John Doe" owns private project "Enterprise" + When I visit project "Enterprise" members page + When I select current user as "Developer" + Then I should see current user as "Developer" + + @javascript + Scenario: Signed in admin should be able to remove himself from a project + Given "John Doe" owns private project "Enterprise" + And current user is developer of project "Enterprise" + When I visit project "Enterprise" members page + Then I should see current user as "Developer" + When I click on the "Remove User From Project" button for current user + Then I should not see current user as "Developer" diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb index 5a1cc9aa15..a7a28755a6 100644 --- a/features/steps/admin/projects.rb +++ b/features/steps/admin/projects.rb @@ -3,6 +3,8 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps include SharedPaths include SharedAdmin include SharedProject + include SharedUser + include Select2Helper step 'I should see all non-archived projects' do Project.non_archived.each do |p| @@ -56,6 +58,41 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps expect(page).to have_content 'Namespace: Web' end + step 'I visit project "Enterprise" members page' do + project = Project.find_by!(name: "Enterprise") + visit namespace_project_project_members_path(project.namespace, project) + end + + step 'I select current user as "Developer"' do + page.within ".users-project-form" do + select2(current_user.id, from: "#user_ids", multiple: true) + select "Developer", from: "access_level" + end + + click_button "Add users to project" + end + + step 'I should see current user as "Developer"' do + page.within '.content-list' do + expect(page).to have_content(current_user.name) + expect(page).to have_content('Developer') + end + end + + step 'current user is developer of project "Enterprise"' do + project = Project.find_by!(name: "Enterprise") + project.team << [current_user, :developer] + end + + step 'I click on the "Remove User From Project" button for current user' do + find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click + # poltergeist always confirms popups. + end + + step 'I should not see current_user as "Developer"' do + expect(page).not_to have_selector(:css, '.content-list') + end + def project @project ||= Project.first end From 8034b61b52ed9e54366ee9d73d17a4ea1c00de44 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 1 Dec 2015 20:58:31 -0200 Subject: [PATCH 154/908] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index db812796b6..c088c89d86 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ v 8.2.2 - Fix Error 500 when viewing user's personal projects from admin page (Stan Hu) - Fix: Raw private snippets access workflow - Prevent "413 Request entity too large" errors when pushing large files with LFS + - Fix: As an admin, cannot add oneself as a member to a group/project v 8.2.1 - Forcefully update builds that didn't want to update with state machine From 9f30f5c63515709adce95eef166e96ea91811e36 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 1 Dec 2015 18:19:33 -0500 Subject: [PATCH 155/908] Make icon-link image transparent --- app/assets/images/icon-link.png | Bin 726 -> 1128 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/assets/images/icon-link.png b/app/assets/images/icon-link.png index 60021d5ac47686cce70570d22bad6670082caf56..7d89da97e11a0030fbab56b3d172f547ac9f896e 100644 GIT binary patch delta 1118 zcmV-k1fl!Z1?UKn8Gi%-0001Bxm5rF00d`2O+f$vv5yPND(xlykug~ z(jlQpiNYu_qFZ%}@X|F5QIH}KvO07LD$zq3Y#4a4E`1Po34aqMjXVU4xn>Gw5F!0L zxUTNJGdnZy&i2m9WgdUO_nn=2o@ZY{_`jw&iV4hN6{V;{6PmGAXeZP2h%rhbjdFHy zndiGiVO2)_+URAD1nlrCh5w8XDtK9Y7rd;H zHGY;g3Pc{;IDg1z>}0t?At(4k4x|8a)+-hI=s`e-f0VY$^Gbz$#+apm9bT_c&Ut1b zV2{@;w385FlhGR{@Dp)N;x)b|KP$ih&T7(5f-RVmKHG-4k8SvuW4y?S_UDvMv4^C;7hS&tPq;6z zjzzr8gz@pOjWH>9mkT_>q^L{w_?nFPwb7GN zVUiVrh#{Z&%nB-e8Grf3>lIXZ!7iIz;D%Q#sPVO2F$?I*jWZnLD6a=jHkE=JAK{oe z9jo#Ae1Dh%EU;0T6Q`INzlFo*f<%Y!PrJXxfWqdaq{GL|C7B&QsNjaz3fSY*60xo!+ zf-8QLfGb|5;EqoTnDO6~J#dtZsTmU?@JWFMWq-9JZ@f;$9KoHu$B!Im4~s+KlLD<4 zc)fytnDGRNrw+%Ci-HZF01>suyC~S<2~0ZSZ53?s1a1a`9~eWs4fsf^+^`V1AO*Ks zh4u$3{LTB~tu>y&Q7L@TGWZM6u`mEH;Z^={z!L?lQoC4e*=AOYe%xRTuP}}8$U_D8 zqJJ4D9U9f-by~WIPAC8DM_!#IbQf{suQsz>^7ooyB(RS4oHzD(rM$w=UY?cs%_=NO zQhV?MJ2Y!K#TFz000Ip-j0RMr1O;exUHkQm=um7W-BSpYa1Gm#SXTa1m4bhKG8>LC z-$Ef5_*6l1M|q~zb1utr!&-poRVq}nkALs^j@`sQ@w0c18m5GInvrbGpv(#$<`Z zc!_B&LZAZmID!+1giSfx>mM(EC1m0-Z@jK~#7Foz+1|R8bhk@$b%HFpDX2Hkw2Z zx|A?z5W+>c5W+>ERL~-&MTnbRrXdPa5Q0{;3pB}~G$s^gQ|*G72sFe9MHi9CXo`>) zE}|32#c%lDJMzY}z4QOA=H4fWyoKS8=BBou+#ZS-8OUawUw^81yL4cq#TBh0S1PcS z_Nia|QE9+pm9rZn0PU$DrS>XB1(r;&LNs9I%s~-rotWuM89}1=WzPX%wN8Z*0+9YC z_E%6Lo+SY7t&l1Zfc94CIvn`$dTj98DeZ$oVoL$GfB9Pp?X55>h_KanFk7qiK)c{* z8}UE`2j(7Jzkhh4rF&`zVVm_p4FjgmIltuXbp)69r?uh0`XyJi@>d9Kz@IgS0xRbI zh~0zPe9|9v3i Date: Wed, 2 Dec 2015 08:50:00 +0200 Subject: [PATCH 156/908] git rid of deprecated warnings --- app/models/user.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index e1144ca77b..15aab31564 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -794,4 +794,9 @@ class User < ActiveRecord::Base Gitlab::SQL::Union.new([personal_projects.select(:id), groups.select(:id), other.select(:id)]) end + + # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration + def send_devise_notification(notification, *args) + devise_mailer.send(notification, self, *args).deliver_later + end end From 9aac53bcee8f5fc99ec84036d688801d987959ea Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 10:54:24 +0100 Subject: [PATCH 157/908] Satisfy Rubocop --- app/models/commit_range.rb | 2 +- spec/lib/gitlab/markdown/commit_reference_filter_spec.rb | 2 +- spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index b8bf36b32c..14e7971fa0 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -93,7 +93,7 @@ class CommitRange def to_reference(from_project = nil) if cross_project_reference?(from_project) - reference = project.to_reference + self.class.reference_prefix + self.id + project.to_reference + self.class.reference_prefix + self.id else self.id end diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index 78a3603269..462a41b475 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -152,7 +152,7 @@ module Gitlab::Markdown end it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Committed #{invalidate_reference(reference)}" + act = "Committed #{invalidate_reference(reference)}" expect(reference_filter(act).to_html).to match(/#{Regexp.escape(invalidate_reference(reference))}<\/a>/) end diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb index b6f05710c3..3a9acc9d6d 100644 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -134,7 +134,7 @@ module Gitlab::Markdown end it 'ignores invalid snippet IDs on the referenced project' do - exp = act = "See #{invalidate_reference(reference)}" + act = "See #{invalidate_reference(reference)}" expect(reference_filter(act).to_html).to match(/#{Regexp.escape(invalidate_reference(reference))}<\/a>/) end From a8e463c8aca571ede3691c98f7f3990d3d880d0b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 10:56:05 +0100 Subject: [PATCH 158/908] Don't show project fork event as imported --- CHANGELOG | 1 + app/models/event.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index db812796b6..8fbe1e6ff1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ v 8.3.0 (unreleased) - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) - Add ignore whitespace change option to commit view - Fire update hook from GitLab + - Don't show project fork event as "imported" v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) diff --git a/app/models/event.rb b/app/models/event.rb index 9afd223bce..01d008035a 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -201,7 +201,7 @@ class Event < ActiveRecord::Base elsif commented? "commented on" elsif created_project? - if project.import? + if project.external_import? "imported" else "created" From edc37c25204d7a4446d15eac94e6e1d92d613ed9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 10:56:39 +0100 Subject: [PATCH 159/908] Add changelog item --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 090b54f41a..01e7e8d20b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ v 8.3.0 (unreleased) - Fix 500 error when update group member permission - Fix: Raw private snippets access workflow - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) + - Recognize issue/MR/snippet/commit links as references v 8.2.1 - Forcefully update builds that didn't want to update with state machine From 927a4576c6e0bff2c9a41e538efcd2c7691a6a74 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 2 Dec 2015 13:26:49 +0100 Subject: [PATCH 160/908] The Procfile is for development only --- Procfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Procfile b/Procfile index fd5f7ecb94..2e41485677 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,7 @@ +# For DEVELOPMENT only. Production uses Runit in +# https://gitlab.com/gitlab-org/omnibus-gitlab or the init scripts in +# lib/support/init.d, which call scripts in bin/ . +# web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"} worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q mailers -q default # mail_room: bundle exec mail_room -q -c config/mail_room.yml From a7682f8775a4609ac8c70151ffe8f3ccf3b767b6 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Tue, 24 Nov 2015 14:59:02 +0100 Subject: [PATCH 161/908] Specs for 'Merge When Build Succeeds' --- .gitignore | 1 + app/models/merge_request.rb | 13 ++- .../merge_when_build_succeeds_service.rb | 6 +- app/services/system_note_service.rb | 2 +- .../merge_requests/widget/_merged.html.haml | 4 +- .../widget/open/_accept.html.haml | 4 +- .../open/_merge_when_build_succeeds.html.haml | 21 ++--- lib/api/entities.rb | 1 + lib/api/merge_requests.rb | 22 ++--- spec/factories/ci/commits.rb | 23 +++--- spec/factories/commit_statuses.rb | 2 +- .../merge_when_build_succeeds_spec.rb | 80 +++++++++++++++++++ spec/models/merge_request_spec.rb | 24 ++++++ spec/requests/api/merge_requests_spec.rb | 17 +++- .../merge_requests/merge_service_spec.rb | 8 +- .../merge_when_build_succeeds_service_spec.rb | 78 ++++++++++++++++++ .../merge_requests/refresh_service_spec.rb | 5 +- spec/services/system_note_service_spec.rb | 22 +++-- 18 files changed, 268 insertions(+), 65 deletions(-) create mode 100644 spec/features/merge_requests/merge_when_build_succeeds_spec.rb create mode 100644 spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb diff --git a/.gitignore b/.gitignore index f5b6427ca0..881b3fb81a 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ public/assets/ public/uploads.* public/uploads/ shared/artifacts/ +TODO rails_best_practices_output.html /tags tmp/ diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 89f9e8fa6a..131dfda6b5 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -254,15 +254,14 @@ class MergeRequest < ActiveRecord::Base end end - def can_cancel_merge_when_build_succeeds?(user) - can_be_merged_by?(user) || self.author == user + def can_cancel_merge_when_build_succeeds?(current_user) + can_be_merged_by?(current_user) || self.author == current_user end - def can_remove_source_branch? - for_fork? && - !project.protected_branch(source_branch) && - !project.repository.root_ref(source_branch) && - can?(current_user, :push_code, project) + def can_remove_source_branch?(current_user) + !source_project.protected_branch?(source_branch) && + !source_project.root_ref?(source_branch) && + Ability.abilities.allowed?(current_user, :push_code, source_project) end def mr_and_commit_notes diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb index 15dcace5df..2f101e53a3 100644 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -11,13 +11,11 @@ module MergeRequests unless already_approved merge_request.merge_when_build_succeeds = true merge_request.merge_user = @current_user + + SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.ci_commit) end merge_request.save - - unless already_approved - SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.ci_commit) - end end # Triggers the automatic merge of merge_request once the build succeeds diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 7de4221c4c..ed557fef81 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -132,7 +132,7 @@ class SystemNoteService # Called when 'merge when build succeeds' is executed def self.merge_when_build_succeeds(noteable, project, author, ci_commit) - body = "Approved an automatic merge when the build for #{ci_commit.sha} succeeds" + body = "Enabled an automatic merge when the build for #{ci_commit.sha} succeeds" create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index a788fcea23..52e34ee617 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -13,7 +13,7 @@ %span.label-branch= @merge_request.target_branch The source branch has been removed. - - elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) + - elsif @merge_request.can_remove_source_branch?(current_user) .remove_source_branch_widget %p = succeed '.' do @@ -48,5 +48,3 @@ $('.remove_source_branch_in_progress').hide(); $('.remove_source_branch_widget.failed').show(); }); - - diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 5ec623b472..279e2ec91f 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -7,13 +7,13 @@ - ci_commit = @merge_request.ci_commit - if ci_commit && ci_commit.active? = f.button class: "btn btn-create btn-grouped merge_when_build_succeeds", name: "merge_when_build_succeeds" do - Merge when Build Succeeds + Merge When Build Succeeds = f.button class: "btn btn-create btn-grouped accept_merge_request #{status_class}" do Accept Merge Request Now - else = f.button class: "btn btn-create btn-grouped accept_merge_request #{status_class}" do Accept Merge Request - - if @merge_request.can_remove_source_branch? + - if @merge_request.can_remove_source_branch?(current_user) .accept-control.checkbox = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do = check_box_tag :should_remove_source_branch diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index 51e18f8442..43ba49c5a5 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -3,22 +3,25 @@ to be merged automatically when #{link_to "the build", ci_status_path(@merge_request.ci_commit)} succeeds. %div - - if @merge_request.merge_params["should_remove_source_branch"].present? + - source_branch_removed = @merge_request.merge_params["should_remove_source_branch"].present? + - if source_branch_removed = succeed '.' do The changes will be merged into %span.label-branch= @merge_request.target_branch The source branch will be removed. + - else %p = succeed '.' do The changes will be merged into %span.label-branch= @merge_request.target_branch The source branch will not be removed. - .clearfix.prepend-top-10 - - if @merge_request.can_remove_source_branch? && !@merge_request.merge_params["should_remove_source_branch"].present? - = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do - = icon('times') - Remove Source Branch When Merged - - if @merge_request.merge_when_build_succeeds - = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-warning btn-sm" do - Cancel Automatic Merge + - if (@merge_request.can_remove_source_branch?(current_user) && !source_branch_removed) || @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + .clearfix.prepend-top-10 + - if @merge_request.can_remove_source_branch?(current_user) && !source_branch_removed + = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do + = icon('times') + Remove Source Branch When Merged + - if @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-warning btn-sm" do + Cancel Automatic Merge diff --git a/lib/api/entities.rb b/lib/api/entities.rb index d6aec03d7f..6f9f71b094 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -169,6 +169,7 @@ module API expose :description expose :work_in_progress?, as: :work_in_progress expose :milestone, using: Entities::Milestone + expose :merge_when_build_succeeds end class MergeRequestChanges < MergeRequest diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index b72f816932..32cb1137ef 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -179,11 +179,11 @@ module API # Merge MR # # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - ID of MR - # merge_commit_message (optional) - Custom merge commit message - # should_remove_source_branch - When true, the source branch will be deleted if possible - # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR + # merge_commit_message (optional) - Custom merge commit message + # should_remove_source_branch (optional) - When true, the source branch will be deleted if possible + # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds # Example: # PUT /projects/:id/merge_request/:merge_request_id/merge # @@ -193,12 +193,11 @@ module API # Merge request can not be merged # because user dont have permissions to push into target branch unauthorized! unless merge_request.can_be_merged_by?(current_user) - - not_allowed! unless merge_request.open? && !merge_request.work_in_progress? + not_allowed! if !merge_request.open? || merge_request.work_in_progress? merge_request.check_if_can_be_merged if merge_request.unchecked? - render_api_error!('Branch cannot be merged', 406) if merge_request.can_be_merged? + render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged? merge_params = { commit_message: params[:merge_commit_message], @@ -206,7 +205,7 @@ module API } if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.ci_commit && merge_request.ci_commit.active? - ::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params). + ::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params). execute(merge_request) else ::MergeRequests::MergeService.new(merge_request.target_project, current_user, merge_params). @@ -224,11 +223,6 @@ module API post ":id/merge_request/:merge_request_id/cancel_merge_when_build_succeeds" do merge_request = user_project.merge_requests.find(params[:merge_request_id]) - allowed = ::Gitlab::GitAccess.new(current_user, user_project). - can_push_to_branch?(merge_request.target_branch) - - # Merge request can not be merged - # because user dont have permissions to push into target branch unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) ::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user).cancel(merge_request) diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb index 79e000b7cc..70e3fa319c 100644 --- a/spec/factories/ci/commits.rb +++ b/spec/factories/ci/commits.rb @@ -2,17 +2,18 @@ # # Table name: commits # -# id :integer not null, primary key -# project_id :integer -# ref :string(255) -# sha :string(255) -# before_sha :string(255) -# push_data :text -# created_at :datetime -# updated_at :datetime -# tag :boolean default(FALSE) -# yaml_errors :text -# committed_at :datetime +# id :integer not null, primary key +# project_id :integer +# ref :string(255) +# sha :string(255) +# before_sha :string(255) +# push_data :text +# created_at :datetime +# updated_at :datetime +# tag :boolean default(FALSE) +# yaml_errors :text +# committed_at :datetime +# gl_project_id :integer # # Read about factories at https://github.com/thoughtbot/factory_girl diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index 52de437052..8898b71e2a 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -5,7 +5,7 @@ FactoryGirl.define do name 'default' status 'success' description 'commit status' - commit factory: :ci_commit + commit factory: :ci_commit_with_one_job factory :generic_commit_status, class: GenericCommitStatus do name 'generic' diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb new file mode 100644 index 0000000000..b25a3f05e2 --- /dev/null +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +feature 'Merge When Build Succeeds', feature: true, js: true do + let(:user) { create(:user) } + + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") } + + before do + project.team << [user, :master] + project.enable_ci + end + + context "Active build for Merge Request" do + before do + ci_commit = create(:ci_commit, gl_project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) + ci_build = create(:ci_build, commit: ci_commit) + + login_as user + visit_merge_request(merge_request) + end + + it 'displays the Merge When Build Succeeds button' do + expect(page).to have_button "Merge When Build Succeeds" + end + + context "Merge When Build succeeds enabled" do + before do + click_button "Merge When Build Succeeds" + end + + it 'activates Merge When Build Succeeds feature' do + expect(page).to have_link "Cancel Automatic Merge" + + expect(page).to have_content "Approved by #{user.name} to be merged automatically when the build succeeds." + expect(page).to have_content "The source branch will not be removed." + end + end + end + + context 'When it is enabled' do + # No clue how, but push a new commit to the branch + let(:merge_request) { create(:merge_request_with_diffs, source_project: project, # source_branch: "mepmep", + author: user, title: "Bug NS-04", merge_when_build_succeeds: true) } + + before do + merge_request.source_project.team << [user, :master] + merge_request.source_branch = "feature" + merge_request.target_branch = "master" + merge_request.save! + + ci_commit = create(:ci_commit, gl_project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) + ci_build = create(:ci_build, commit: ci_commit) + + login_as user + visit_merge_request(merge_request) + end + + it 'cancels the automatic merge' do + click_link "Cancel Automatic Merge" + + expect(page).to have_button "Merge When Build Succeeds" + end + + it "allows the user to remove the source branch" do + expect(page).to have_link "Remove Source Branch When Merged" + end + end + + context 'Build is not active' do + it "should not allow for enabling" do + visit_merge_request(merge_request) + expect(page).not_to have_button "Merge When Build Succeeds" + end + end + + def visit_merge_request(merge_request) + visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 1bd09a1b0f..c7a9765825 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -174,6 +174,30 @@ describe MergeRequest do end end + describe '#can_remove_source_branch' do + let(:user) { build(:user)} + + before do + subject.source_project.team << [user, :master] + end + + it "cant be merged when its a a protected branch" do + subject.source_project.protected_branches = []; + + expect(subject.can_remove_source_branch?(user)).to be_falsey + end + + it "cant remove a root ref" do + subject.source_branch = "master"; + + expect(subject.can_remove_source_branch?(user)).to be_falsey + end + + it "is truthy in all other cases" do + expect(subject.can_remove_source_branch?(user)) + end + end + describe "#reset_merge_when_build_succeeds" do let(:merge_if_green) { create :merge_request, merge_when_build_succeeds: true } it "sets the item to false" do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index a68c7b1e46..91ae2059e9 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -303,19 +303,21 @@ describe API::API, api: true do end describe "PUT /projects/:id/merge_request/:merge_request_id/merge" do + let (:ci_commit) { create(:ci_commit_without_jobs) } + it "should return merge_request in case of success" do put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) expect(response.status).to eq(200) end - it "should return 405 if branch can't be merged" do + it "should return 406 if branch can't be merged" do allow_any_instance_of(MergeRequest). to receive(:can_be_merged?).and_return(false) put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) - expect(response.status).to eq(405) + expect(response.status).to eq(406) expect(json_response['message']).to eq('Branch cannot be merged') end @@ -340,6 +342,17 @@ describe API::API, api: true do expect(response.status).to eq(401) expect(json_response['message']).to eq('401 Unauthorized') end + + it "enables merge when build succeeds if the ci is active" do + allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) + allow(ci_commit).to receive(:active?).and_return(true) + + put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user), merge_when_build_succeeds: true + + expect(response.status).to eq(200) + expect(json_response['title']).to eq('Test') + expect(json_response['merge_when_build_succeeds']).to eq(true) + end end describe "PUT /projects/:id/merge_request/:merge_request_id" do diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 7483f51de0..242524286f 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -13,12 +13,12 @@ describe MergeRequests::MergeService do describe :execute do context 'valid params' do - let(:service) { MergeRequests::MergeService.new(project, user, {}) } + let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') } before do allow(service).to receive(:execute_hooks) - service.execute(merge_request, 'Awesome message') + service.execute(merge_request) end it { expect(merge_request).to be_valid } @@ -37,14 +37,14 @@ describe MergeRequests::MergeService do end context "error handling" do - let(:service) { MergeRequests::MergeService.new(project, user, {}) } + let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') } it 'saves error if there is an exception' do allow(service).to receive(:repository).and_raise("error") allow(service).to receive(:execute_hooks) - service.execute(merge_request, 'Awesome message') + service.execute(merge_request) expect(merge_request.merge_error).to eq("Something went wrong during merge") end diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb new file mode 100644 index 0000000000..8638539173 --- /dev/null +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +describe MergeRequests::MergeWhenBuildSucceedsService do + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request) } + let(:mr_merge_if_green_enabled) { create(:merge_request, merge_when_build_succeeds: true, + source_branch: "source_branch", target_branch: project.default_branch, + source_project: project, target_project: project, state: "opened") } + let(:ci_commit) { create(:ci_commit_with_one_job, ref: mr_merge_if_green_enabled.source_branch) } + let(:project) { create(:project) } + let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, commit_message: 'Awesome message') } + + before do + project.ci_commits = [ci_commit] + project.save! + end + describe "#execute" do + context 'first time enabling' do + before do + allow(merge_request).to receive(:ci_commit).and_return(ci_commit) + end + + it 'sets the params, merge_user, and flag' do + service.execute(merge_request) + + expect(merge_request).to be_valid + expect(merge_request.merge_when_build_succeeds).to be_truthy + expect(merge_request.merge_params).to eq commit_message: 'Awesome message' + expect(merge_request.merge_user).to be user + + note = merge_request.notes.last + expect(note.note).to include 'Enabled an automatic merge when the build for' + end + end + + context 'allready approved' do + let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, new_key: true) } + let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) } + + before do + allow(mr_merge_if_green_enabled).to receive(:ci_commit).and_return(ci_commit) + allow(mr_merge_if_green_enabled).to receive(:mergeable?).and_return(true) + allow(ci_commit).to receive(:success?).and_return(true) + end + + it 'updates the merge params' do + expect(SystemNoteService).not_to receive(:merge_when_build_succeeds) + + service.execute(mr_merge_if_green_enabled) + expect(mr_merge_if_green_enabled.merge_params).to have_key(:new_key) + end + end + end + + describe "#trigger" do + let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") } + + it "merges all merge requests with merge when build succeeds enabled" do + allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) + allow(ci_commit).to receive(:success?).and_return(true) + + expect(MergeWorker).to receive(:perform_async) + service.trigger(build) + end + end + + describe "#cancel" do + before do + service.cancel(mr_merge_if_green_enabled) + end + + it "resets all the merge_when_build_succeeds params" do + expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey + expect(mr_merge_if_green_enabled.merge_params).to eq({}) + expect(mr_merge_if_green_enabled.merge_user).to be nil + end + end +end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 7ee4488521..18b2659c1f 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -17,7 +17,8 @@ describe MergeRequests::RefreshService do source_project: @project, source_branch: 'master', target_branch: 'feature', - target_project: @project) + target_project: @project, + merge_when_build_succeeds: true) @fork_merge_request = create(:merge_request, source_project: @fork_project, @@ -46,6 +47,7 @@ describe MergeRequests::RefreshService do it { expect(@merge_request.notes).not_to be_empty } it { expect(@merge_request).to be_open } + it { expect(@merge_request.merge_when_build_succeeds).to be_falsey} it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request.notes).to be_empty } end @@ -146,6 +148,7 @@ describe MergeRequests::RefreshService do end end + def reload_mrs @merge_request.reload @fork_merge_request.reload diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 35912ece64..5d41a5cdc6 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -208,18 +208,28 @@ describe SystemNoteService do end describe '.merge_when_build_succeeds' do - let(:ci_commit) { create :ci_commit, gl_project: project } - let(:merge_request) { create :merge_request, project: project } + let(:ci_commit) { create :ci_commit_without_jobs } + let(:noteable) { create :merge_request } - subject { described_class.merge_when_build_succeeds(merge_request, project, author) } + subject { described_class.merge_when_build_succeeds(noteable, project, author, ci_commit) } it_behaves_like 'a system note' it "posts the Merge When Build Succeeds system note" do - allow(merge_request).to receive(:ci_commit).and_return(ci_commit) - allow(ci_commit).to receive(:short_sha).and_return('12345678') + expect(subject.note).to eq "Enabled an automatic merge when the build for 97de212e80737a608d939f648d959671fb0a0142 succeeds" + end + end - expect(subject.note).to eq "This merge request will be automatically merged when the build for 12345678 succeeds" + describe '.cancel_merge_when_build_succeeds' do + let(:ci_commit) { create :ci_commit_without_jobs } + let(:noteable) { create :merge_request } + + subject { described_class.cancel_merge_when_build_succeeds(noteable, project, author) } + + it_behaves_like 'a system note' + + it "posts the Merge When Build Succeeds system note" do + expect(subject.note).to eq "Canceled the automatic merge" end end From 25907ebe476a24bfdd2c451f18227d4fcf314b07 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 2 Dec 2015 13:28:21 +0100 Subject: [PATCH 162/908] Doc feature Merge When Build Succeeds --- .../disable_merge_when_build_succeeds.png | Bin 0 -> 20551 bytes .../enable_merge_when_build_succeeds.png | Bin 0 -> 13150 bytes doc/workflow/merge_when_build_succeeds.md | 20 ++++++++++++++++++ 3 files changed, 20 insertions(+) create mode 100644 doc/workflow/merge_requests/disable_merge_when_build_succeeds.png create mode 100644 doc/workflow/merge_requests/enable_merge_when_build_succeeds.png create mode 100644 doc/workflow/merge_when_build_succeeds.md diff --git a/doc/workflow/merge_requests/disable_merge_when_build_succeeds.png b/doc/workflow/merge_requests/disable_merge_when_build_succeeds.png new file mode 100644 index 0000000000000000000000000000000000000000..a45a4890b6249d6b8ad24408dece3f57873a24ff GIT binary patch literal 20551 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 literal 0 HcmV?d00001 diff --git a/doc/workflow/merge_requests/enable_merge_when_build_succeeds.png b/doc/workflow/merge_requests/enable_merge_when_build_succeeds.png new file mode 100644 index 0000000000000000000000000000000000000000..62a46c9508b582f82c1e8f3cb5c469f7216f5d01 GIT binary patch literal 13150 zcmc(`cT`i`*EZ@oDk>@*6;wd5AT=OLm3}PLNGHTlMF_oD>Bj;noj^c(F9{*R&`Ur; zDG9v_Aw-lSB}786`8J;4z3(6Qj(3bZzIWVjj}cgG_L_UGtU2d;o;l}wr?0DVo|T*R z*s){hwKN|a9y@j-6KJQNW&wWc7e6=${5s+NP|NrXeGB}}<)il0$H?8z$KTou ze$3v@-4!nGZQ})pyLmgf`_PZKs2n?Xq#}u_woBunl!7BOyLCY(pa!?koON^o}gX=VY=dVXXhm9}jQ5S=X|Y z+)|NX`SWI1;oZESTWjiGeT4bLzR43(s=e|yPgCG$fI$J*>A%SJcm8c2YY{u4`)~8K z@Sj5ewoY)oy!3DD&!~_8>GOx$w-dkb{qv7k@>oI!G)CiR%^*dyG z=fHKA2xY>kBUdAlNkz*g^kjnPt6O}*^@9hKFUZOb&pY7_0n6_%c3cl)1bmTo$z2Vr zGcvLd#_>@aMLsysOL0SW6s(*wW%h)`&6|uRRQ6sJ;pDBHj@0fSt0?ZXnN3}a#&_4G zxU4Tky;zqSx5>~(MAcvu3tXY$F^5^#XqS+Ci#A>%yTw2gWNvBCr;?j_g(+-LwT=p& z3*owH?ZwYLGTEdl z-*;&FfN)3gM{}7}9I#GZr_YpmsUGZYjtp6li}zu?x=;NaOutopgJ)dG|f0$wdoM;42h0I?DH*WP&L@jfSBKL6NW z7lvF)YduxF_pk7Cgu~>s=$)pr`)phIdWIlf--9^~St$u?v7D$9(2|$0|I#svp_V?g zPf+l8u`0s%C3{{DQ(1;=H)DwPJ6QCo-|pj|kLV9vx@wBD+7WrV*@yNS_k-9CK4JPo z=J%{Q>t@F59$wf|4O90`TW}->EopJO!ua_mP4mVmMY+*?U<97&RryeTQp`T*CgOHq z#4M5dO+byXLM91p(ZdY&Q&m{`ye#Bp=13b~$PA3>4SprV2NrusvyOMofI%4!q>M=3 zD&X*`=O{ZPEZTzLk@G!;BIE3DZ$_;|yljx30Xv zJno#K2bhJQ6n%fF8ek%xjLZ_zDB%NarHOSu*eLK?YzA^qsk_^!UWFOoN)LTFbv)>x znJ;dRrj4A!>VUosqsnJNV-CM=Sh<2z%MIz-6u2T^)#Td8f<AV}FWYl3=JLtZdA!-wf9*td(B-%l=?vJawZhl4%fZeo1Svbg5(E zod&b;vs+PWd{J@H!a$1(4p}ya^7BbvY25Q~@cG+HYw$gFDvwH$G--?64J}R;Geg;1 z1*=i@cAC1m^-@!FaB_!h&7qksPpfb7OMgR5!Bt7xnPyYlRsjdRIyo|C#c&k$TVjLD zH-W&pcN$erJ%J$#J3Ovmt02~SH@O35^(psOYQG#njQ47d>=(mwLW*qCV2hKhx0F4o z7EYtm*vZ`^hyQI1+y18z2=qwZw(6XH1%0W>r3b?nzFSig=ctitai5Cwy!YHArwv4W*g5G;J_Vi$*)kCIAvF?!zpWpF}dHI z2*oQSXW?(ugn#CLzSwb~Oy7?`6j|$h5Ki5Taj9M+61-lJWdTP9X^ z6nRxGncVUaEz7}QjmQJdllw8K^e;I9b&P!yZoxr4LM3<}qNc9CO-XLd_a^Z$bFMc*Ki$;j zRq(0%%%`j9w=yaNwL)FV2{2!hbsKC83s8SYX($DkMA;|sYAa&G4)!bvYwgq#F%{jl zD|Zs5giF(LzoEds!ths95Wl3hiCfK#I3msSF)WO`0CNhr)}ta1f0vks^xWUqUO^hE zJ3u%;9mgUHLh@r0?#l16AqiUE0i$9n1u6nmu^W920<4WYe~fcWCtl!!OsoI={j7x? z*QcA!rr%9FD_86TjJCQe-B7LV%3@|dF)jo5Sh@T{-YL936^rPNJDEQNc(pfL9;jdRI=$q=+xR~u^_23Ft61uKz~!P2mWTv z@Ylx+hyov$C3*nIo%e!kn&^jELS^e%Th3ufkmC+y};pX-J{G8NA^@Ue2QF zHWuiTdDst$zOU36la?uEj%&-tKyRmUr3Yq7HBj{CuZp|j5fKiQh#veCFqDl4qyFJM zsQIrWND}(R$t5C5Ij_8XBzgw6R{5%dU4K2OlBCo}a0*@BIsE#ZTQlOOyQ%g&0jNFk zGAIAii=V!N-35^XJ1RG6Pn_=!v1?7;f-8IIbZzd&lA8Oc*AK>ioYCYh*(|AeCO;uW zBzQzTY@ZaMuZT;=7ZUl#@>|`y*QLAe+Pe1_Zoj|4eaFnlB28mRmk-20lZf6-zf7Al zMLna5Fy=P^0bBN)+O zHK}&<=1q8ZMtXX2{w-WYEVtSB7fJ20wpSwpH|n02jbflKF2|3&yNdFqjjb>Os`X*5 zNx20so6?9JcZUHBH82okgH&lziX4p7L|jalDgV|T)wdyc$gsUe8ctrII`=fhYEPx+ z$JnGb6uS3mkMMDE<@r;$BUKL9!502&S&KtujVsIpItMQ&1hw?9A1!VFv*g=L{#ii7 zuoBcng0BXkh{*~a0GtVzQb}@=J-O8~=pLr-PLGRG47E5g;a_DjGxib zNh#0~qK?JgKy`ZK4~A8tA4_RwmUhJ@T_!F2a?ME87n71~+e*;l2+sCR1QeAag)KPO z8@*TdZ1Hh@YImYRBevBo=xL3@2B@jmq@>vvCK-QCjE#hO6`Yo)0rCxjcRc3I>t*#M5-1NH3lB+k`uq(f)1^5$j%^HT z+FgG#OskE9JPbBcKI{e~u@;PQE4>5iBVrF}?5so!8fqD)(WkuQfZRP&?0>A1xXldb z9fn}N;0RK;5_2b#W5BlVq71AgyDv zd2cpsAH5v=opbo+hwn=u`IJk0VdMGFwX2a^O*EZ!P-%#Ny>V`5L>GI?-67^niA0 zc6VI|uDWX$;O}bi`M3n}bbRvdaC2qdxexrr(4PtbOViAgQ~uV1!-hb%+icS!*)=2b zeg$PCnu@)$>vZaID(Ku^0%`Kn&lCI#>R2uJWEl7>ExyYHm*~aZc#FT@YPa6dEw}~PFNm3Cj||w-hfm2ohW*e0(JAw`oF?XaU+H{EB%^SPa)7YXoWiYOZG2>5XM6 zcdQ`OUJZc9T(EL)Dj2YFA8PN>3f!2rhkypR0x7;^9fjcg7SPyw*d>+BLRIF1WlP8g zeatK;cr|wO`hDRH>!0p@`)ZarsN7^=E#LN<)d)_H{Uma=ThtrdvdVrvFl})pcHk$E z+fX#~g~h6;j!&C!f%M_DkhI^VvH~L1_%%zi>g(n8i43`DXdby-kshMSALd;aXLoJr zI@2SP`X8^`TYEio@mmDy(X?G{GwlY{D;}e z8NMs&lPydwfai#AGw#hdaI|#uqlgDvKceWCi|> z>9mO1!?k#S_d6B}+ZRWb4d}G}8=d*J^yvSN8U1erTmIidQ2!(1W`B+S6cxU2=KJsH zzg=l{|Hi?6(W_?h-y0uK3e-+sP8t3`8*BQn>Ccv;;;2*=3noR@q8L6pEPgDr)$Xg0 zMn>6s<0upv6emx!;ndB!O&j`=~?+mG!ocTI!+RMm*nEsg7}`dhd>^)*#_Z>}Z`y+pzm? zi0FEtG~Urlnc|ws(t8yrlG>76e+s%3?wb^avyx@FhJ4r)A~Y37a(9_&yI6W)?h9Zu)`n@Z;aa?tI#DvDB1GMjQlkAfW+X>37Sb`%gIwqx>2u`S z$SF5Up6x9+jD>b=4T+0KmTJ9F=DpK7ETlO9bB&@3`D!^%FBu|>O7%vy`{mtbiAkUp zui{N4l6QJ51<1SSlUzI93j<>dyM5BUn-LpB{+u=ARtM@)rI37ywQ$wWiAI90+2%c% znF(p1VIi{j{&tt&*d@sZb}>jvwRci?>UCky?!!engmvfMx2p@=-uD2kzWEW1zubqA zUqvG3O^S>6&8Y;HFt7r`l(F&io@RT+68v*J7;6p#&6D;6U(MWECo>mB8?3@ZxFTY# z$4Ii@L`Be8%&;wNNF}9ub3SS7g?9tylkR5N|i<&}v`l!z}0LJj*V-1Y_*bRRdje*vvNr$ z+NIQgSEV#k!4lU2C0G@uqkk+B&aO`st&Me(Zxf|hrqN3lW2FAFQM`WD zVplWL(6&l1`5GIl^UCR~uW?A%?kq_mf}rykB`((nP3sGsHCfNZ-{0{bbcRn2nclUS z6T*?+x{8vO^+iIolkQ28Hvt`4_fUX%i`h}L;wNXPLx_B^|4UH1S3XW zQ3~(v3=8##JTS6cConzuwVN(LeB=+$8{!rP5$h%nnjZD@b;@eHUhTJlSPyqVfP09% zO`ajUeX@c(F|%mm!+h@H3-X8;OIGk+c3M`6ze-x(XWeq?I`^@Eh6!$@J2LUkPp`GR zP>}hr9W{LWbf8^!d+_cig^(DYTH45_@+lu$Me}?e-m3WQbM_Ktc@+2HeFKE8)VcF9 zG<3I9FN?fk0_uw+&xmXTs`XlP%duCq2MzT6f%FEt{gKDxd_kE=Ps420_LO=t$#_Gc zr%beiaAXgbt@=`OW=3(Zucyoi9vi-Y{9eu4L(TUwH5Ty0ZP(Qt0j*Cr z@v%*i(eE()acqa%!W#3BwO2rGh#P*{Ag7RGj8| zjZ574;g6RUOJ#W5hA`_f6f@hHx}EMN`J1=2(bT7-pKMV#tVUldOST;3zKAe{z!c@W z{AP6w^|@Lq!qtdZgUc}iM!_#(p>deBoUVzC+dme~^s-a&q^%g9b}hfim^ZS96QL-0 zkX4XN6Xu|?400VvJ1jgIV(|2HG$^Mlh2(LMmX)vDjHfHur(v6$YJrb`+B}&i>q@p@ zD9?;$?ag=?uWfzvnmo+NYpC*{|Bf4y!Vg!3@`i`AfNp=~>JQ2SmBbIwP*e2M(#*5B zJWRcP8d=)?kZpF*+V`WZ_;F-%JlhBbO1p`8Nx=1YRiWDtf?erMHK!W|zh?n+^O zgG{1si`>NLO{mL2UXGQkMaq_XXI(}FzZ{BN{>Y+xmc2Z+tzs!+?lsyc>*k|!CmDmW z_a*bHEOgCJ+JydELMaDc^Pgrj<#)-u#KUt9kN8XD0#ZCT6yzKGiyGW(q8&3 z{$A{t19^=z9-XF*PZj(=%aOGZiT%2)=aZ^E)fSxhs~}}88VNHS+8wDACdlfL*AljP zMcb;C+vB|WO&TWj!M*b#q%V`eapi*A%)Xe9^Asa3sK zOvB$GL#!dQc3P-CTb|GYmobX?2r5jS0+{wr_CRhKxSH#X>urq$jO&uNtcwMR9~;iG z$diP`ll+^kjkt8owB2ek=Sj9zzfS6^yBNJYO4DYZJQ>axaxWzB`O?_H9B&unMO1Q9 zDlYd%WWIBFYJko|^W(1L28;uGO4c3+b-O#X4-cuZGEN*bo0h+#P)qj4g16NgZ=?*< zdbJ?g9Yxt26DarBal5PExB+8)y4d8LalXI0Cs^vTU9s5Mr825Sr49lB8iL^F^OH`e zG_(SIIF=(W~eAV zX!^}i2aNI|OE2b5NMS)_A?6%lE~KB6X1g=zMI?#-5B=;GqW7z_Cbs@5v%T(tyxnDb zSkS#?r9?aNNIhvza`#+fwO6)r4;>7Wfkl9<)EnI-6*d#9?GlIYVu>!^bhFfc6(4VVT&luodwrYk~aE4JtIQV3! zx6XHoW5+Je9MuA(>}GFD?v;)2P1xWmFIr+q9i1xMrm(dV;{JEq_X9lovs>y;+ZDXU zJ(H>Mq3BdMZH4-4Qn(qz`ye^9h}+?6+SuTQuNOq*z<^@F419wtyZzjeQZULLsbYxCk3xJ)JJI~&BzDu?4L zx<`8gnXd%RGi~p1a$r6VkGK!Qcn;R8F65i&R4dLVY!(-aNZvB$zq5oIZsRg-uq$gS zM)75lOZ{v8MpT%eueMlKXfM^SB*I~V3$!k$F-h#OSS5OszRBTuh(ABkqzrrTfX&x| z7k8+vK(v#pvK!JBH_Iqgg1-Fzd})CZYQbsLXj0#?wW2q~@FiC<^&2wTA{)$y%~RKZ z>YJOX+r`;Qm=+vF4nmB57+cFc>gV(;1<&t~dexZ!^)l(^qe^y+Au}RtIjaMJ46zVGVE%rdT2F*J{C#&j};)|KQV`6AGd_E@>mFMl*9U4P+FuUZ+Tro+$O zS@pzKp+n&N8zBbXH8uE6Eh#SUD z*Zbcc=4V}KXgQDxWFj=ApKmfnhO$7lWBiexUV+6ucW3UpctLMYhf5gijfx1(Q`|5Ji*9$GiPi<^WokSR0$CjuL4MB$QWR3IbmMDZ|a>mk{>?`;F zOh4Z|8##*Y0g!w01?u7q53^J9FmeG$<2c-85oMG2l7yF=-C860qm|Eb$->GmcLG+X z9=Rf^y5AUc&)=k8(D{>3T2AxLy|dRz+NL1y#SS5!=#J&HwX<{^wk2A+veTwt4vAzFoE;UChbK9x5q@ntY-MwWAsi0eX)t!j1}3k&2=eSPT`hL zu}&Y%z71=v)xKcRJewQw2?0QM{Q~oo;k#aQ&*66Sq97qC_Rp=Z3Y2a#)mdlAFk?=q z%6DGHV)6sBX8CIgTlo(pGD)aIgbaSd^sHxPr8GDW-;lrN*8{6kx9Ty+m|^RmL|8ij z93VNvDwo`Dj!k%0VuGQXtSIko{S>Y((Uh;h6U#TD;kb~tORZH!_tOk2hH8>k)r!mL9KKJ|N2q4 zDO5@ixASJcOv7wCiNmNdrG_>o(Ma*9$6 zxlEIq3rs`ar=YX|f{9nBXa^I&w#O)o{%Y{mdae5h%Gb;!DqVdT+W7N@t5S_wq&b(i zMCV|?AZ2zl5CDaj6ZSHnX&w=3YqAfeKRAIAEoy1$eYDjNZHgPz@f(Xf$slgi`oPe( ziXa=}fAe5Jh|1wDWNWDlZzf zgrpsq4D)Hu##T0c$fC!LW(5b$uXBs-y{X4jJaF_Esm=IJmyM7`=-x^Zc5TcA2Iw7Q zO8)88sgBP5ZL8*Sko`x~dwS~hPt6~#JGjJf(mDV!X1FSa5=&}fK{eVd54o}evkkJ4 zgdEk;LyDpG%a~~0J)gwQclb>=P`12nQAvt93$)~=Z6$JdhM=uQ5n3x35^zs+XBI9p zIEa}lS~U*}w7XmJn}S-{eaZG=3&1Q5r$wfNh`OlIOI)kva2k*byOx5N3?^MvcB5t= zRcpxD5;Nh8sb4J%A~zGqoJ9c#E@v5ndIhSNGu$)LiB266plJ`Nm463g{?<}WDW?Uo zIsE+RvDz3b^zqZT5mplxkwa2P-dIx7yDFsb4}ge%SbVG@DklAumn>-9aLD3YFp}#= z`_9c)$cr4428T?T?S60(pfOGwY}n*;a(_3jWfO=uf6{2sVayP{i!`HBV5&=ZIc!< zTwhC1>a+{FS-?a}2C^X@w!wXV-XzCvflp!?kIrqMl<`mvV|c@R4ouqxpxY^bvz#Pd^^A@Y>;bzx zcN5RzFR)?fDp%0HX$|lw7*R#gd1;YI5h{M^62A{tZ^R<_JFl<+cp5PZ2`;}5@T_J6 zdNGUf$0=-Znv^PXAne7$y^ysBT}wuOT@D-)G7jG}D7U*Ns?`9Jg*QwRSZjq10%e+a zlmJBIN~zY%yQ!--7S$gMWjFRI_aHELwvY|Yo^1_8Z%(KAsn=1b&l(upc8x2*7{aZq4EnK-mJ+VS>`OfhZojlV78Xdg9E>tw0~T3Ano5nv|L59qT2bD#jv#lV9yM5hVNsj4(v=-`OQLWI@(IgxmddI7L+vZ^FR!xIF0?nT;E$=EulxQ&vVipRz4xz5v)yaI#W_s zwbcy|t(JGR_Pp4^2tk(&>=S>in~TcRu$B8J9h36ubw5t^DAyHhAAp+$d~f=d7&6#E zOWP$2uAN`YAH+5|2Fb{s!*vs)pWvpn(({2pQo-QCEGMUf*{BMoWqsp2A)3v?$OpJ*NgG8fmqj3HqRLQINj|C9l%QWT(313=o0z`&q5GB6IJQ}bd+V*w zZOB}HS~Ki%=!EWxyCrl3+vYV4X5_51i_%n>it+Rll=jHXon)CXbPUKXA#_()A^2~E ztxnEuDLHV>r!h?%I?@YodvJHW*4`Mht7;(+p!rn!S$?jmTjOpaowes^nrMJ9hggiXo z#ecu(v-MJ>ge?&Pgp~}9V}Q72n!7SAu-%lX+r_mUlocmYUFqkZI8c6_C*t_AfBwOb zpG&jr3Ny!Waupw-78EkGi^?SEMeir7XGDdQOG+8!lq&ZTW2%ICIk>@Jc*O5{{Zl)? zY^Uq&KCBeJ*_nD{kR;|s8pNcxYZL7>l>ct-u*qbB3pUYg--6{D#SO~~t$0gIl?*o< zbB?^Yb5*i;`}!-rQcga>jGq2&vO1@Pq7sCnV8AV3mkJ2fVLFBb__*BQ(Qb0Ue$-3K z2DPhlL{i1%prIB@P^ey~v4geO#{k!UxqPxPmb@t{sbmagTQu0`h8mN`pFf>lK0yG< zLt=SG%l$eC#Zv-i{U?$3e_T--GAe}gQ$e-L%s3JUCxf)DX_Wc$t?)QdYv;^#OJfG< z5Vf~-=GGY>D(r8SeJroo3R4<`137o^;qr1z3$iG`lm;_P$!sg02L;qS+VxnW0P@VTMHAK9bsBvhprpnf>c}pC zO%{N4CTZjK07m*Gd2XQqHzzEq^rVeNkhmmM?XjcoI+cjobLU0c}GsafphwdT9G@rZ;8V8n!rimf|r=kl?hJu@0nsO&_(rE8FA9qL>)9%Cru zLaEnN6^G%1+_?-UnPu05%PzbQLc@a#q*TIo8eB~q14`Ton_lBC>L`z5^g)=i^9M0- z1IGP|+MCG5k9j@-P!#Qhr>_~z0(W#XMg2A%jAMNK% ze9BL|PGw^Pz;dO~v6{s(O09TzP)^U_w`PmsCPxg>&{F(fsB9@N?s(+j$gSpOZp}A9 zHJ+dxcq+AnBs=Feo)c{56+ztZGs4T>Q5{19{L=i_(M~NjU*2!V%rJ2(9y%kNk=K5H zagq1{Nu^fl`V{gtFHLxnHOxni&Zf8zGn>;uI7d`~`J-5_P%Y1&GJy_OJ(g9b^biH3 zY3rNZ{gnj>zanEt7mGVid9^}4{r_b1l9cUdzFDTXxD=>lNb0Fs8lCYx?`u$2j6|=x zEDhJB`K>6I)~aL|Utawn&|1BC^S9%uM zD{f+69?iA)hDLpLrcSrVpP>)lPoKzC1X{2}K-jjFwCAflHoLzl zm;8w?=vS5J#MMq z!>;oUN5?Y0HCaD386QgU6DUCg`@VhS?|KYCLyxgt(}qTGf0ZPEZTo*7bNC<0G5^=8 ziT_^QEQ>CnVQ#haE~Eb*a`@-$KV(`W#qv-|P$S^{(YXp`ey}AD#vgw4RREJ}wXG!^ z<8+PdX%TSPC=By&I{sLT$MW?5bx`BKf93z52PXb2iM_!jX Date: Wed, 2 Dec 2015 13:40:05 +0100 Subject: [PATCH 163/908] Add cover-block to UI framework Signed-off-by: Dmitriy Zaporozhets --- app/views/help/ui.html.haml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 2169a821fb..8a60d6852b 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -34,8 +34,6 @@ %h3 %code .gray-content-block - - .gray-content-block.middle-block %h4 Normal block inside content = lorem @@ -45,6 +43,25 @@ = lorem + %h3 + %code .cover-block + + .cover-block + .avatar-holder + = image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: '' + .cover-title + John Smith + + .cover-desc + = lorem + + .cover-controls + = link_to '#', class: 'btn btn-gray' do + = icon('pencil') +   + = link_to '#', class: 'btn btn-gray' do + = icon('rss') + %h2#lists Lists %h3 @@ -258,7 +275,6 @@ .file-contenta.code = render 'shared/file_highlight', blob: blob - %h2#markdown Markdown %h3 %code .md or .wiki and others From 4d67a2909f46d807c3586a74938d6f7619429808 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 13:59:15 +0100 Subject: [PATCH 164/908] Use pointer cursor in award emoji selector --- app/assets/stylesheets/pages/issuable.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 3a08ee70bc..848ad7dac8 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -157,6 +157,7 @@ min-width: 214px; > li { + cursor: pointer; margin: 5px; } } From b7cac5a8dae03a1c870f58d22f51e156e62b0965 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 13:59:21 +0100 Subject: [PATCH 165/908] Use full names in emoji tooltip --- app/helpers/issues_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 493f370d9a..25befd654d 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -96,7 +96,7 @@ module IssuesHelper def emoji_author_list(notes, current_user) list = notes.map do |note| - note.author == current_user ? "me" : note.author.username + note.author == current_user ? "me" : note.author.name end list.join(", ") From 275c2a3161ac4d5638b8fa39a7355bbd9fc3cf46 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 13:59:42 +0100 Subject: [PATCH 166/908] Use new style for wiki --- app/assets/stylesheets/pages/wiki.scss | 5 ++++ app/views/projects/wikis/_form.html.haml | 16 ++++++------ .../projects/wikis/_main_links.html.haml | 11 +++----- app/views/projects/wikis/_nav.html.haml | 25 +++++++++++++------ app/views/projects/wikis/_new.html.haml | 4 +-- app/views/projects/wikis/edit.html.haml | 24 +++++++++--------- app/views/projects/wikis/git_access.html.haml | 2 +- app/views/projects/wikis/pages.html.haml | 7 +++--- app/views/projects/wikis/show.html.haml | 12 +++------ .../steps/project/source/markdown_render.rb | 2 +- features/steps/project/wiki.rb | 12 ++++----- 11 files changed, 62 insertions(+), 58 deletions(-) diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index dfaeba41cf..cdf514197c 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -4,3 +4,8 @@ margin-right: auto; padding-right: 7px; } + +.wiki-last-edit-by { + font-size: 80%; + font-weight: normal; +} diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 9c94c43e74..1d257818dc 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default' } do |f| -if @page.errors.any? #error_explanation .alert.alert-danger @@ -11,14 +11,7 @@ .col-sm-10 = f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: "form-control" - .row - .col-sm-offset-2.col-sm-10 - %p.cgray - To link to a (new) page you can just type - %code [Link Title](page-slug) - \. - - .form-group.wiki-content + .form-group = f.label :content, class: 'control-label' .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do @@ -27,6 +20,11 @@ .clearfix .error-alert + + .help-block + To link to a (new) page, simply type + %code [Link Title](page-slug) + \. .form-group = f.label :commit_message, class: 'control-label' .col-sm-10= f.text_field :message, class: 'form-control', rows: 18 diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 14f2582225..29bf5d62ab 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -1,9 +1,4 @@ %span.pull-right - - if can?(current_user, :create_wiki, @project) - = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new btn-grouped", "data-toggle" => "modal" do - %i.fa.fa-plus - New Page - - if (@page && @page.persisted?) = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do Page History @@ -11,5 +6,7 @@ = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do %i.fa.fa-pencil-square-o Edit - -= render 'projects/wikis/new' + - if can?(current_user, :admin_wiki, @project) + = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do + = icon('trash') + Delete diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index fffb4eb31a..e6e6ad5bc4 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -1,10 +1,19 @@ -%ul.center-top-menu - = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do - = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) +.project-issuable-filter + .controls + - if can?(current_user, :create_wiki, @project) + = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do + %i.fa.fa-plus + New Page - = nav_link(path: 'wikis#pages') do - = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project) + = render 'projects/wikis/new' - = nav_link(path: 'wikis#git_access') do - = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do - Git Access + %ul.center-top-menu + = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do + = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) + + = nav_link(path: 'wikis#pages') do + = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project) + + = nav_link(path: 'wikis#git_access') do + = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do + Git Access diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index dace172438..f0547e9c05 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -12,5 +12,5 @@ The page slug is invalid. Please don't use characters other then: a-z 0-9 _ - and / %p.hint Please don't use spaces. - .modal-footer - = link_to 'Build', '#', class: 'build-new-wiki btn btn-create' + .form-actions + = link_to 'Create Page', '#', class: 'build-new-wiki btn btn-create' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 0b709c3695..23f64fbbd1 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,16 +1,16 @@ -- page_title "Edit", @page.title, "Wiki" +- page_title "Edit", @page.title.capitalize, "Wiki" = render "header_title" = render 'nav' -.pull-right - = render 'main_links' -%h3.page-title - Editing - - %span.light #{@page.title} -%hr -= render 'form' +.gray-content-block + .pull-right + = render 'main_links' -.pull-right - - if @page.persisted? && can?(current_user, :admin_wiki, @project) - = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-sm btn-remove" do - Delete this page + %h3.page-title.oneline + %span.light Edit Page + - if @page.persisted? + = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page) + - else + = @page.title + += render 'form' diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index 6417ef4a38..11c8c4f0eb 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -5,7 +5,7 @@ .gray-content-block .row .col-sm-6 - %h3.page-title + %h3.page-title.oneline Git access for %strong= @project_wiki.path_with_namespace diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index d179a1abec..aae1ad69ad 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -1,11 +1,10 @@ -- page_title "All Pages", "Wiki" +- page_title "Pages", "Wiki" = render "header_title" = render 'nav' .gray-content-block - = render 'main_links' - %h3.page-title - All Pages + All pages in this wiki are listed below. + %ul.content-list - @wiki_pages.each do |wiki_page| %li diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 55fbf5a8b6..309d40f52b 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -5,11 +5,12 @@ .gray-content-block = render 'main_links' - %h3.page-title + %h3.page-title.oneline = @page.title.capitalize - .wiki-last-edit-by - Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} + %span.wiki-last-edit-by + · + last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} - if @page.historical? .warning_message @@ -21,8 +22,3 @@ .wiki = preserve do = render_wiki_content(@page) - -.gray-content-block.footer-block - .wiki-last-edit-by - Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} - diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb index c78e86fa1a..ec88c8c20c 100644 --- a/features/steps/project/source/markdown_render.rb +++ b/features/steps/project/source/markdown_render.rb @@ -238,7 +238,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps step 'I see new wiki page named test' do expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "test") - expect(page).to have_content "Editing" + expect(page).to have_content "Edit Page test" end When 'I go back to wiki page home' do diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index 02207dbffa..935c1ca8a2 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -5,7 +5,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps include SharedPaths step 'I click on the Cancel button' do - page.within(:css, ".form-actions") do + page.within(:css, ".wiki-form .form-actions") do click_on "Cancel" end end @@ -24,7 +24,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps expect(page).to have_content "link test" click_link "link test" - expect(page).to have_content "Editing" + expect(page).to have_content "Edit Page" end step 'I have an existing Wiki page' do @@ -68,7 +68,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I click on the "Delete this page" button' do - click_on "Delete this page" + click_on "Delete" end step 'The page should be deleted' do @@ -120,13 +120,13 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps step 'I should see the new wiki page form' do expect(current_path).to match('wikis/image.jpg') expect(page).to have_content('New Wiki Page') - expect(page).to have_content('Editing - image.jpg') + expect(page).to have_content('Edit Page image.jpg') end step 'I create a New page with paths' do click_on 'New Page' fill_in 'Page slug', with: 'one/two/three' - click_on 'Build' + click_on 'Create Page' fill_in "wiki_content", with: 'wiki content' click_on "Create page" expect(current_path).to include 'one/two/three' @@ -135,7 +135,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps step 'I create a New page with an invalid name' do click_on 'New Page' fill_in 'Page slug', with: 'invalid name' - click_on 'Build' + click_on 'Create Page' end step 'I should see an error message' do From b66694d236f71474054c63b1e8a683abacf7fdc3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:00:54 +0100 Subject: [PATCH 167/908] Add "New X" link to dashboard/group milestone project-specific issue/MR panels --- app/views/shared/_issues.html.haml | 7 ++++--- app/views/shared/_merge_requests.html.haml | 7 +++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml index 0dbb6a0439..4b4c9e9eab 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -3,8 +3,10 @@ .panel.panel-default.panel-small - project = group[0] .panel-heading - = link_to_project project - = link_to 'show all', namespace_project_issues_path(project.namespace, project), class: 'pull-right' + = link_to project.name_with_namespace, namespace_project_issues_path(project.namespace, project) + - if can?(current_user, :create_issue, project) + .pull-right + = link_to 'New issue', new_namespace_project_issue_path(project.namespace, project) %ul.well-list.issues-list - group[1].each do |issue| @@ -12,4 +14,3 @@ = paginate @issues, theme: "gitlab" - else .nothing-here-block No issues to show - diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml index c02c5af008..be17a511b2 100644 --- a/app/views/shared/_merge_requests.html.haml +++ b/app/views/shared/_merge_requests.html.haml @@ -3,8 +3,11 @@ .panel.panel-default.panel-small - project = group[0] .panel-heading - = link_to_project project - = link_to 'show all', namespace_project_merge_requests_path(project.namespace, project), class: 'pull-right' + = link_to project.name_with_namespace, namespace_project_merge_requests_path(project.namespace, project) + - if can?(current_user, :create_merge_request, project) + .pull-right + = link_to 'New merge request', new_namespace_project_merge_request_path(project.namespace, project) + %ul.well-list.mr-list - group[1].each do |merge_request| = render 'projects/merge_requests/merge_request', merge_request: merge_request From 0833057494dcf84c789fee8a1dd2c3f3f541d036 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:01:20 +0100 Subject: [PATCH 168/908] Use new style for milestone detail page --- app/views/dashboard/milestones/show.html.haml | 52 +++++--- app/views/groups/milestones/show.html.haml | 61 ++++++---- app/views/projects/milestones/show.html.haml | 113 ++++++++++-------- 3 files changed, 140 insertions(+), 86 deletions(-) diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 83077a398b..3536bbeaf4 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -1,19 +1,23 @@ - page_title @milestone.title, "Milestones" -%h4.page-title - .issue-box{ class: "issue-box-#{@milestone.closed? ? 'closed' : 'open'}" } - - if @milestone.closed? - Closed - - else - Open - Milestone #{@milestone.title} +- header_title "Milestones", dashboard_milestones_path + +.issuable-details + .page-title + .issue-box{ class: "issue-box-#{@milestone.closed? ? 'closed' : 'open'}" } + - if @milestone.closed? + Closed + - else + Open + Milestone #{@milestone.title} + + .gray-content-block.middle-block + %h2.issue-title + = gfm escape_once(@milestone.title) -%hr - if @milestone.complete? && @milestone.active? - .alert.alert-success + .alert.alert-success.prepend-top-default %span All issues for this milestone are closed. You may close the milestone now. -.description - .table-holder %table.table %thead @@ -44,7 +48,7 @@ #{@milestone.open_items_count} open = milestone_progress_bar(@milestone) -%ul.nav.nav-tabs +%ul.center-top-menu.no-top.no-bottom %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues @@ -58,25 +62,39 @@ Participants %span.badge= @milestone.participants.count - .pull-right - = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped" - .tab-content .tab-pane.active#tab-issues - .row + .gray-content-block.middle-block + .pull-right + = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped" + + .oneline + All issues in this milestone + + .row.prepend-top-default .col-md-6 = render 'issues', title: "Open", issues: @milestone.opened_issues .col-md-6 = render 'issues', title: "Closed", issues: @milestone.closed_issues .tab-pane#tab-merge-requests - .row + .gray-content-block.middle-block + .pull-right + = link_to 'Browse Merge Requests', merge_requests_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped" + + .oneline + All merge requests in this milestone + + .row.prepend-top-default .col-md-6 = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests .col-md-6 = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests .tab-pane#tab-participants + .gray-content-block.middle-block + .oneline + All participants to this milestone %ul.bordered-list - @milestone.participants.each do |user| %li diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index d161259e4a..3c1d881501 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -1,27 +1,29 @@ - page_title @milestone.title, "Milestones" = render "header_title" -%h4.page-title - .issue-box{ class: "issue-box-#{@milestone.closed? ? 'closed' : 'open'}" } - - if @milestone.closed? - Closed - - else - Open - Milestone #{@milestone.title} - .pull-right - - if can?(current_user, :admin_milestones, @group) - - if @milestone.active? - = link_to 'Close Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close" +.issuable-details + .page-title + .issue-box{ class: "issue-box-#{@milestone.closed? ? 'closed' : 'open'}" } + - if @milestone.closed? + Closed - else - = link_to 'Reopen Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" + Open + Milestone #{@milestone.title} + .pull-right + - if can?(current_user, :admin_milestones, @group) + - if @milestone.active? + = link_to 'Close Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close" + - else + = link_to 'Reopen Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" + + .gray-content-block.middle-block + %h2.issue-title + = gfm escape_once(@milestone.title) -%hr - if @milestone.complete? && @milestone.active? - .alert.alert-success + .alert.alert-success.prepend-top-default %span All issues for this milestone are closed. You may close the milestone now. -.description - .table-holder %table.table %thead @@ -52,7 +54,7 @@ #{@milestone.open_items_count} open = milestone_progress_bar(@milestone) -%ul.nav.nav-tabs +%ul.center-top-menu.no-top.no-bottom %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues @@ -66,25 +68,40 @@ Participants %span.badge= @milestone.participants.count - .pull-right - = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped" - .tab-content .tab-pane.active#tab-issues - .row + .gray-content-block.middle-block + .pull-right + = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped" + + .oneline + All issues in this milestone + + .row.prepend-top-default .col-md-6 = render 'issues', title: "Open", issues: @milestone.opened_issues .col-md-6 = render 'issues', title: "Closed", issues: @milestone.closed_issues .tab-pane#tab-merge-requests - .row + .gray-content-block.middle-block + .pull-right + = link_to 'Browse Merge Requests', merge_requests_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped" + + .oneline + All merge requests in this milestone + + .row.prepend-top-default .col-md-6 = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests .col-md-6 = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests .tab-pane#tab-participants + .gray-content-block.middle-block + .oneline + All participants to this milestone + %ul.bordered-list - @milestone.participants.each do |user| %li diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 3a898dfbcf..c3bda794c6 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -1,46 +1,50 @@ - page_title @milestone.title, "Milestones" = render "header_title" -%h4.page-title - .issue-box{ class: issue_box_class(@milestone) } - - if @milestone.closed? - Closed - - elsif @milestone.expired? - Expired - - else - Open - Milestone ##{@milestone.iid} - %small.creator - = @milestone.expires_at - .pull-right - - if can?(current_user, :admin_milestone, @project) - = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do - %i.fa.fa-pencil-square-o - Edit - - if @milestone.active? - = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped" +.issuable-details + .page-title + .issue-box{ class: issue_box_class(@milestone) } + - if @milestone.closed? + Closed + - elsif @milestone.expired? + Expired - else - = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped" - = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-remove" do - %i.fa.fa-trash-o - Remove + Open + Milestone ##{@milestone.iid} + - if @milestone.expires_at + %span.creator + · + = @milestone.expires_at + .pull-right + - if can?(current_user, :admin_milestone, @project) + = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do + %i.fa.fa-pencil-square-o + Edit + + - if @milestone.active? + = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped" + - else + = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped" + + = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-remove" do + %i.fa.fa-trash-o + Delete + + .gray-content-block.middle-block + %h2.issue-title + = gfm escape_once(@milestone.title) + %div + - if @milestone.description.present? + .description + .wiki + = preserve do + = markdown @milestone.description -%hr - if @milestone.issues.any? && @milestone.can_be_closed? - .alert.alert-success + .alert.alert-success.prepend-top-default %span All issues for this milestone are closed. You may close milestone now. -%h3.issue-title - = gfm escape_once(@milestone.title) -%div - - if @milestone.description.present? - .description - .wiki - = preserve do - = markdown @milestone.description - -%hr -.context +.context.prepend-top-default %p.lead Progress: #{@milestone.closed_items_count} closed @@ -51,8 +55,7 @@ %span.pull-right= @milestone.expires_at = milestone_progress_bar(@milestone) - -%ul.nav.nav-tabs +%ul.center-top-menu.no-top.no-bottom %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues @@ -66,17 +69,21 @@ Participants %span.badge= @users.count - .pull-right - - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do - %i.fa.fa-plus - New Issue - - if can?(current_user, :read_issue, @project) - = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped" - .tab-content .tab-pane.active#tab-issues - .row + .gray-content-block.middle-block + .pull-right + - if can?(current_user, :create_issue, @project) + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do + %i.fa.fa-plus + New Issue + - if can?(current_user, :read_issue, @project) + = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" + + .oneline + All issues in this milestone + + .row.prepend-top-default .col-md-4 = render('issues', title: 'Unstarted Issues (open and unassigned)', issues: @issues.opened.unassigned, id: 'unassigned') .col-md-4 @@ -85,7 +92,15 @@ = render('issues', title: 'Completed Issues (closed)', issues: @issues.closed, id: 'closed') .tab-pane#tab-merge-requests - .row + .gray-content-block.middle-block + .pull-right + - if can?(current_user, :read_merge_request, @project) + = link_to 'Browse Merge Requests', namespace_project_merge_requests_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" + + .oneline + All merge requests in this milestone + + .row.prepend-top-default .col-md-3 = render('merge_requests', title: 'Work in progress (open and unassigned)', merge_requests: @merge_requests.opened.unassigned, id: 'unassigned') .col-md-3 @@ -100,6 +115,10 @@ = render 'merge_request', merge_request: merge_request .tab-pane#tab-participants + .gray-content-block.middle-block + .oneline + All participants to this milestone + %ul.bordered-list - @users.each do |user| %li From 5a59712b8ac29450dfef0af0dd381529f0d9d7ae Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:02:12 +0100 Subject: [PATCH 169/908] Add "New X" buttons to dashboard and group issue, MR and milestone indexes --- app/views/dashboard/issues.html.haml | 31 +++++++++++---- app/views/dashboard/merge_requests.html.haml | 23 ++++++++++- .../dashboard/milestones/index.html.haml | 21 ++++++++-- app/views/groups/issues.html.haml | 38 +++++++++++++------ app/views/groups/merge_requests.html.haml | 30 +++++++++++---- app/views/groups/milestones/index.html.haml | 24 +++++++----- app/views/projects/milestones/index.html.haml | 15 +++++--- 7 files changed, 134 insertions(+), 48 deletions(-) diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index cd602e897b..829c3c8376 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -4,14 +4,31 @@ - if current_user = auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues") +.project-issuable-filter + .controls + .pull-left + - if current_user + .hidden-xs.pull-left + = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do + %i.fa.fa-rss -.append-bottom-20 - .pull-right - - if current_user - .hidden-xs.pull-left.prepend-top-20 - = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: '' do - %i.fa.fa-rss + - if @projects.any? { |project| can?(current_user, :create_issue, project) } + .dropdown.inline.prepend-left-10 + %button.dropdown-toggle.btn.btn-new{type: 'button', 'data-toggle' => 'dropdown'} + %i.fa.fa-plus + New Issue + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + - @projects.each do |project| + - if can?(current_user, :create_issue, project) + %li + = link_to new_namespace_project_issue_path(project.namespace, project) do + = project.name_with_namespace = render 'shared/issuable/filter', type: :issues -= render 'shared/issues' +.gray-content-block.second-block + List all issues from all projects you have access to. + +.prepend-top-default + = render 'shared/issues' diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index d1f332fa0d..2e91c8dec8 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -1,6 +1,25 @@ - page_title "Merge Requests" - header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id) -.append-bottom-20 +.project-issuable-filter + .controls + - if @projects.any? { |project| can?(current_user, :create_merge_request, project) } + .dropdown.inline + %button.dropdown-toggle.btn.btn-new{type: 'button', 'data-toggle' => 'dropdown'} + %i.fa.fa-plus + New Merge Request + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + - @projects.each do |project| + - if can?(current_user, :create_merge_request, project) + %li + = link_to new_namespace_project_merge_request_path(project.namespace, project) do + = project.name_with_namespace + = render 'shared/issuable/filter', type: :merge_requests -= render 'shared/merge_requests' + +.gray-content-block.second-block + List all merge requests from all projects you have access to. + +.prepend-top-default + = render 'shared/merge_requests' diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 635251e237..9aea75e50d 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -1,12 +1,25 @@ - page_title "Milestones" -- header_title "Milestones", dashboard_milestones_path +- header_title "Milestones", dashboard_milestones_path +.project-issuable-filter + .controls + - if @projects.any? { |project| can?(current_user, :admin_milestone, project) } + .dropdown.inline + %button.dropdown-toggle.btn.btn-new{type: 'button', 'data-toggle' => 'dropdown'} + %i.fa.fa-plus + New Milestone + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + - @projects.each do |project| + - if can?(current_user, :admin_milestone, project) + %li + = link_to new_namespace_project_milestone_path(project.namespace, project) do + = project.name_with_namespace -= render 'shared/milestones_filter' + = render 'shared/milestones_filter' .gray-content-block - .oneline - List all milestones from all projects you have access to. + List all milestones from all projects you have access to. .milestones %ul.content-list diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 08d97e418a..5a9739a0cd 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -4,21 +4,35 @@ - if current_user = auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues") +.project-issuable-filter + .controls + .pull-left + - if current_user + .hidden-xs.pull-left + = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do + %i.fa.fa-rss + - if @projects.any? { |project| can?(current_user, :create_issue, project) } + .dropdown.inline.prepend-left-10 + %button.dropdown-toggle.btn.btn-new{type: 'button', 'data-toggle' => 'dropdown'} + %i.fa.fa-plus + New Issue + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + - @projects.each do |project| + - if can?(current_user, :create_issue, project) + %li + = link_to new_namespace_project_issue_path(project.namespace, project) do + = project.name_with_namespace + + = render 'shared/issuable/filter', type: :issues -= render 'shared/issuable/filter', type: :issues .gray-content-block.second-block - .pull-right - - if current_user - .hidden-xs.pull-left - = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token) do - %i.fa.fa-rss - %div - Only issues from - %strong #{@group.name} - group are listed here. - - if current_user - To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page. + Only issues from + %strong #{@group.name} + group are listed here. + - if current_user + To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page. .prepend-top-default = render 'shared/issues' diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 425ad8331b..95c503a3af 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -1,13 +1,29 @@ - page_title "Merge Requests" - header_title group_title(@group, "Merge Requests", merge_requests_group_path(@group)) -= render 'shared/issuable/filter', type: :merge_requests +.project-issuable-filter + .controls + - if @projects.any? { |project| can?(current_user, :create_merge_request, project) } + .dropdown.inline + %button.dropdown-toggle.btn.btn-new{type: 'button', 'data-toggle' => 'dropdown'} + %i.fa.fa-plus + New Merge Request + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + - @projects.each do |project| + - if can?(current_user, :create_merge_request, project) + %li + = link_to new_namespace_project_merge_request_path(project.namespace, project) do + = project.name_with_namespace + + = render 'shared/issuable/filter', type: :merge_requests + .gray-content-block.second-block - %div - Only merge requests from - %strong #{@group.name} - group are listed here. - - if current_user - To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. + Only merge requests from + %strong #{@group.name} + group are listed here. + - if current_user + To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. + .prepend-top-default = render 'shared/merge_requests' diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 84ec77c618..b221d3a89a 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -1,18 +1,22 @@ - page_title "Milestones" - header_title group_title(@group, "Milestones", group_milestones_path(@group)) -= render 'shared/milestones_filter' +.project-issuable-filter + .controls + - if can?(current_user, :admin_milestones, @group) + .pull-right + %span.pull-right.hidden-xs + = link_to new_group_milestone_path(@group), class: "btn btn-new" do + = icon('plus') + New Milestone + + = render 'shared/milestones_filter' + .gray-content-block - - if can?(current_user, :admin_milestones, @group) - .pull-right - %span.pull-right.hidden-xs - = link_to new_group_milestone_path(@group), class: "btn btn-new" do - New Milestone + Only milestones from + %strong #{@group.name} + group are listed here. - .oneline - Only milestones from - %strong #{@group.name} - group are listed here. .milestones %ul.content-list - if @milestones.blank? diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index a207385bd4..114b06457a 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,15 +1,18 @@ - page_title "Milestones" = render "header_title" -= render 'shared/milestones_filter' -.gray-content-block - .pull-right - - if can? current_user, :admin_milestone, @project + +.project-issuable-filter + .controls + - if can?(current_user, :admin_milestone, @project) = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do %i.fa.fa-plus New Milestone - .oneline - Milestone allows you to group issues and set due date for it + + = render 'shared/milestones_filter' + +.gray-content-block + Milestone allows you to group issues and set due date for it .milestones %ul.content-list From 05224ae64ac769e3b9b06679d6e44fb120c879ef Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:05:10 +0100 Subject: [PATCH 170/908] Restore sidebar tooltips and fix logo tooltip location --- app/assets/javascripts/application.js.coffee | 26 ++- app/assets/javascripts/sidebar.js.coffee | 1 + app/assets/stylesheets/framework/sidebar.scss | 185 +++++++++--------- app/helpers/nav_helper.rb | 8 + app/views/layouts/_page.html.haml | 10 +- app/views/layouts/ci/_page.html.haml | 10 +- app/views/layouts/nav/_admin.html.haml | 28 +-- app/views/layouts/nav/_dashboard.html.haml | 18 +- app/views/layouts/nav/_explore.html.haml | 8 +- app/views/layouts/nav/_group.html.haml | 14 +- .../layouts/nav/_group_settings.html.haml | 6 +- app/views/layouts/nav/_profile.html.haml | 20 +- app/views/layouts/nav/_project.html.haml | 34 ++-- .../layouts/nav/_project_settings.html.haml | 24 +-- 14 files changed, 207 insertions(+), 185 deletions(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 945ffb660e..1539eba0fa 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -135,17 +135,25 @@ $ -> ), 1 # Initialize tooltips - $('body').tooltip({ - selector: '.has_tooltip, [data-toggle="tooltip"], .page-sidebar-collapsed .nav-sidebar a' + $('body').tooltip( + selector: '.has_tooltip, [data-toggle="tooltip"]' placement: (_, el) -> $el = $(el) - if $el.attr('id') == 'js-shortcuts-home' - # Place the logo tooltip on the right when collapsed, bottom when expanded - $el.parents('header').hasClass('header-collapsed') and 'right' or 'bottom' - else - # Otherwise use the data-placement attribute, or 'bottom' if undefined - $el.data('placement') or 'bottom' - }) + $el.data('placement') || 'bottom' + ) + + $('.header-logo .home').tooltip( + placement: (_, el) -> + $el = $(el) + if $('.page-with-sidebar').hasClass('page-sidebar-collapsed') then 'right' else 'bottom' + container: 'body' + ) + + $('.page-with-sidebar').tooltip( + selector: '.sidebar-collapsed .nav-sidebar a, .sidebar-collapsed a.sidebar-user' + placement: 'right' + container: 'body' + ) # Form submitter $('.trigger-submit').on 'change', -> diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index fb08016fba..ae59480af9 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -5,6 +5,7 @@ $(document).on("click", '.toggle-nav-collapse', (e) -> $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('header').toggleClass("header-collapsed header-expanded") + $('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded") $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) ) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index c1b0129c86..017f4d2657 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -1,4 +1,7 @@ .page-with-sidebar { + padding-top: $header-height; + transition-duration: .3s; + .sidebar-wrapper { position: fixed; top: 0; @@ -14,19 +17,15 @@ .sidebar-wrapper { z-index: 99; background: $background-color; - transition-duration: .3s; } .content-wrapper { - min-height: 100vh; width: 100%; padding: 20px; - background: #EAEBEC; .container-fluid { background: #FFF; padding: $gl-padding; - min-height: 90vh; &.container-blank { background: none; @@ -36,6 +35,83 @@ } } +.sidebar-wrapper { + .header-logo { + border-bottom: 1px solid transparent; + float: left; + height: $header-height; + width: $sidebar_width; + position: fixed; + z-index: 999; + overflow: hidden; + transition-duration: .3s; + + a { + float: left; + height: $header-height; + width: 100%; + padding: 11px 0 11px 22px; + overflow: hidden; + outline: none; + transition-duration: .3s; + + img { + width: 36px; + height: 36px; + } + + #tanuki-logo, img { + float: left; + } + + .gitlab-text-container { + width: 230px; + + h3 { + width: 158px; + float: left; + margin: 0; + margin-left: 14px; + font-size: 19px; + line-height: 41px; + font-weight: normal; + } + } + } + + &:hover { + background-color: #EEE; + } + } + + .sidebar-user { + padding: 9px 22px; + position: fixed; + bottom: 40px; + width: $sidebar_width; + overflow: hidden; + transition-duration: .3s; + + .username { + margin-left: 10px; + width: $sidebar_width - 2 * 10px; + font-size: 16px; + line-height: 34px; + } + } +} + + +.tanuki-shape { + transition: all 0.8s; + + &:hover { + fill: rgb(255, 255, 255); + transition: all 0.1s; + } +} + + .nav-sidebar { margin-top: 14 + $header-height; margin-bottom: 100px; @@ -62,7 +138,7 @@ color: $gray; display: block; text-decoration: none; - padding-left: 22px; + padding-left: 23px; font-weight: normal; outline: none; @@ -101,7 +177,6 @@ @mixin expanded-sidebar { padding-left: $sidebar_width; - transition-duration: .3s; .sidebar-wrapper { width: $sidebar_width; @@ -122,9 +197,8 @@ } } -@mixin folded-sidebar { - padding-left: 60px; - transition-duration: .3s; +@mixin collapsed-sidebar { + padding-left: $sidebar_collapsed_width; .sidebar-wrapper { width: $sidebar_collapsed_width; @@ -133,7 +207,7 @@ width: $sidebar_collapsed_width; a { - padding-left: 12px; + padding-left: ($sidebar_collapsed_width - 36) / 2; .gitlab-text-container { display: none; @@ -144,9 +218,13 @@ .nav-sidebar { width: $sidebar_collapsed_width; - li a { - span { - display: none; + li { + width: auto; + + a { + span { + display: none; + } } } } @@ -156,7 +234,7 @@ } .sidebar-user { - padding-left: 12px; + padding-left: ($sidebar_collapsed_width - 36) / 2; width: $sidebar_collapsed_width; .username { @@ -187,11 +265,11 @@ @media (max-width: $screen-md-max) { .page-sidebar-collapsed { - @include folded-sidebar; + @include collapsed-sidebar; } .page-sidebar-expanded { - @include folded-sidebar; + @include collapsed-sidebar; } .collapse-nav { @@ -201,83 +279,10 @@ @media(min-width: $screen-md-max) { .page-sidebar-collapsed { - @include folded-sidebar; + @include collapsed-sidebar; } .page-sidebar-expanded { @include expanded-sidebar; } } - -.sidebar-user { - padding: 9px 22px; - position: fixed; - bottom: 40px; - width: $sidebar_width; - overflow: hidden; - transition-duration: .3s; - - .username { - margin-left: 10px; - width: $sidebar_width - 2 * 10px; - font-size: 16px; - line-height: 34px; - } -} - -.sidebar-wrapper { - .header-logo { - border-bottom: 1px solid transparent; - float: left; - height: $header-height; - width: $sidebar_width; - overflow: hidden; - transition-duration: .3s; - - a { - float: left; - height: $header-height; - width: 100%; - padding: 10px 22px; - overflow: hidden; - outline: none; - - img { - width: 36px; - height: 36px; - } - - #tanuki-logo, img { - float: left; - } - - .gitlab-text-container { - width: 230px; - - h3 { - width: 158px; - float: left; - margin: 0; - margin-left: 14px; - font-size: 19px; - line-height: 41px; - font-weight: normal; - } - } - } - - &:hover { - background-color: #EEE; - } - } -} - - -.tanuki-shape { - transition: all 0.8s; - - &:hover { - fill: rgb(255, 255, 255); - transition: all 0.1s; - } -} diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 9b1dd8b8e5..e6fb8670e5 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -4,6 +4,14 @@ module NavHelper end def nav_sidebar_class + if nav_menu_collapsed? + "sidebar-collapsed" + else + "sidebar-expanded" + end + end + + def page_sidebar_class if nav_menu_collapsed? "page-sidebar-collapsed" else diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 352b8040cf..ec7cd79bc5 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,8 +1,8 @@ -.page-with-sidebar{ class: nav_sidebar_class } +.page-with-sidebar{ class: page_sidebar_class } = render "layouts/broadcast" - .sidebar-wrapper.nicescroll + .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .header-logo - = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do + = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do = brand_header_logo .gitlab-text-container %h3 GitLab @@ -17,8 +17,8 @@ .collapse-nav = render partial: 'layouts/collapse_button' - if current_user - = link_to current_user, class: 'sidebar-user' do - = image_tag avatar_icon(current_user, 60), alt: 'User activity', class: 'avatar avatar s36' + = link_to current_user, class: 'sidebar-user', title: "Profile" do + = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' .username = current_user.username .content-wrapper diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml index ab3e29c3f4..7e90af21b2 100644 --- a/app/views/layouts/ci/_page.html.haml +++ b/app/views/layouts/ci/_page.html.haml @@ -1,8 +1,8 @@ -.page-with-sidebar{ class: nav_sidebar_class } +.page-with-sidebar{ class: page_sidebar_class } = render "layouts/broadcast" - .sidebar-wrapper.nicescroll + .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .header-logo - = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do + = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do = brand_header_logo .gitlab-text-container %h3 GitLab @@ -14,8 +14,8 @@ .collapse-nav = render partial: 'layouts/collapse_button' - if current_user - = link_to current_user, class: 'sidebar-user' do - = image_tag avatar_icon(current_user, 60), alt: 'User activity', class: 'avatar avatar s36' + = link_to current_user, class: 'sidebar-user', title: "Profile" do + = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' .username = current_user.username .content-wrapper diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 2079feeeab..d04a3d1f22 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -5,78 +5,78 @@ %span Overview = nav_link(controller: [:admin, :projects]) do - = link_to admin_namespaces_projects_path, title: 'Projects', data: {placement: 'right'} do + = link_to admin_namespaces_projects_path, title: 'Projects' do = icon('cube fw') %span Projects = nav_link(controller: :users) do - = link_to admin_users_path, title: 'Users', data: {placement: 'right'} do + = link_to admin_users_path, title: 'Users' do = icon('user fw') %span Users = nav_link(controller: :groups) do - = link_to admin_groups_path, title: 'Groups', data: {placement: 'right'} do + = link_to admin_groups_path, title: 'Groups' do = icon('group fw') %span Groups = nav_link(controller: :deploy_keys) do - = link_to admin_deploy_keys_path, title: 'Deploy Keys', data: {placement: 'right'} do + = link_to admin_deploy_keys_path, title: 'Deploy Keys' do = icon('key fw') %span Deploy Keys = nav_link do - = link_to ci_admin_projects_path, title: 'Continuous Integration', data: {placement: 'right'} do + = link_to ci_admin_projects_path, title: 'Continuous Integration' do = icon('building fw') %span Continuous Integration = nav_link(controller: :logs) do - = link_to admin_logs_path, title: 'Logs', data: {placement: 'right'} do + = link_to admin_logs_path, title: 'Logs' do = icon('file-text fw') %span Logs = nav_link(controller: :broadcast_messages) do - = link_to admin_broadcast_messages_path, title: 'Broadcast Messages', data: {placement: 'right'} do + = link_to admin_broadcast_messages_path, title: 'Messages' do = icon('bullhorn fw') %span Messages = nav_link(controller: :hooks) do - = link_to admin_hooks_path, title: 'Hooks', data: {placement: 'right'} do + = link_to admin_hooks_path, title: 'Hooks' do = icon('external-link fw') %span Hooks = nav_link(controller: :background_jobs) do - = link_to admin_background_jobs_path, title: 'Background Jobs', data: {placement: 'right'} do + = link_to admin_background_jobs_path, title: 'Background Jobs' do = icon('cog fw') %span Background Jobs = nav_link(controller: :applications) do - = link_to admin_applications_path, title: 'Applications', data: {placement: 'right'} do + = link_to admin_applications_path, title: 'Applications' do = icon('cloud fw') %span Applications = nav_link(controller: :services) do - = link_to admin_application_settings_services_path, title: 'Service Templates', data: {placement: 'right'} do + = link_to admin_application_settings_services_path, title: 'Service Templates' do = icon('copy fw') %span Service Templates = nav_link(controller: :labels) do - = link_to admin_labels_path, title: 'Labels', data: {placement: 'right'} do + = link_to admin_labels_path, title: 'Labels' do = icon('tags fw') %span Labels = nav_link(controller: :abuse_reports) do - = link_to admin_abuse_reports_path, title: "Abuse reports" do + = link_to admin_abuse_reports_path, title: "Abuse Reports" do = icon('exclamation-circle fw') %span Abuse Reports %span.count= AbuseReport.count(:all) = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do - = link_to admin_application_settings_path, title: 'Settings', data: {placement: 'right'} do + = link_to admin_application_settings_path, title: 'Settings' do = icon('cogs fw') %span Settings diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index b1a1d53184..da69883130 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,50 +1,50 @@ %ul.nav.nav-sidebar = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: 'home'}) do - = link_to dashboard_projects_path, title: 'Projects', data: {placement: 'right'} do + = link_to dashboard_projects_path, title: 'Projects' do = icon('home fw') %span Projects = nav_link(path: 'dashboard#activity') do - = link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity', data: {placement: 'right'} do + = link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity' do = icon('dashboard fw') %span Activity = nav_link(controller: :groups) do - = link_to dashboard_groups_path, title: 'Groups', data: {placement: 'right'} do + = link_to dashboard_groups_path, title: 'Groups' do = icon('group fw') %span Groups = nav_link(controller: :milestones) do - = link_to dashboard_milestones_path, title: 'Milestones', data: {placement: 'right'} do + = link_to dashboard_milestones_path, title: 'Milestones' do = icon('clock-o fw') %span Milestones = nav_link(path: 'dashboard#issues') do - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues' do = icon('exclamation-circle fw') %span Issues %span.count= current_user.assigned_issues.opened.count = nav_link(path: 'dashboard#merge_requests') do - = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do + = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do = icon('tasks fw') %span Merge Requests %span.count= current_user.assigned_merge_requests.opened.count = nav_link(controller: :snippets) do - = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do + = link_to dashboard_snippets_path, title: 'Snippets' do = icon('clipboard fw') %span Snippets = nav_link(controller: :help) do - = link_to help_path, title: 'Help', data: {placement: 'right'} do + = link_to help_path, title: 'Help' do = icon('question-circle fw') %span Help %li.separate-item = nav_link(controller: :profile) do - = link_to profile_path, title: 'Profile settings', data: {placement: 'bottom'} do + = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do = icon('user fw') %span Profile Settings diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml index 21e565972a..48039ca291 100644 --- a/app/views/layouts/nav/_explore.html.haml +++ b/app/views/layouts/nav/_explore.html.haml @@ -1,21 +1,21 @@ %ul.nav.nav-sidebar = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do - = link_to explore_root_path, title: 'Projects', data: {placement: 'right'} do + = link_to explore_root_path, title: 'Projects' do = icon('home fw') %span Projects = nav_link(controller: :groups) do - = link_to explore_groups_path, title: 'Groups', data: {placement: 'right'} do + = link_to explore_groups_path, title: 'Groups' do = icon('group fw') %span Groups = nav_link(controller: :snippets) do - = link_to explore_snippets_path, title: 'Snippets', data: {placement: 'right'} do + = link_to explore_snippets_path, title: 'Snippets' do = icon('clipboard fw') %span Snippets = nav_link(controller: :help) do - = link_to help_path, title: 'Help', data: {placement: 'right'} do + = link_to help_path, title: 'Help' do = icon('question-circle fw') %span Help diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 319352876b..68da8d5de2 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,6 +1,6 @@ %ul.nav.nav-sidebar = nav_link do - = link_to root_path, title: 'Go to dashboard', data: {placement: 'right'}, class: 'back-link' do + = link_to root_path, title: 'Go to dashboard', class: 'back-link' do = icon('caret-square-o-left fw') %span Go to dashboard @@ -8,39 +8,39 @@ %li.separate-item = nav_link(path: 'groups#show', html_options: {class: 'home'}) do - = link_to group_path(@group), title: 'Home', data: {placement: 'right'} do + = link_to group_path(@group), title: 'Home' do = icon('dashboard fw') %span Group - if can?(current_user, :read_group, @group) - if current_user = nav_link(controller: [:group, :milestones]) do - = link_to group_milestones_path(@group), title: 'Milestones', data: {placement: 'right'} do + = link_to group_milestones_path(@group), title: 'Milestones' do = icon('clock-o fw') %span Milestones = nav_link(path: 'groups#issues') do - = link_to issues_group_path(@group), title: 'Issues', data: {placement: 'right'} do + = link_to issues_group_path(@group), title: 'Issues' do = icon('exclamation-circle fw') %span Issues - if current_user %span.count= Issue.opened.of_group(@group).count = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group), title: 'Merge Requests', data: {placement: 'right'} do + = link_to merge_requests_group_path(@group), title: 'Merge Requests' do = icon('tasks fw') %span Merge Requests - if current_user %span.count= MergeRequest.opened.of_group(@group).count = nav_link(controller: [:group_members]) do - = link_to group_group_members_path(@group), title: 'Members', data: {placement: 'right'} do + = link_to group_group_members_path(@group), title: 'Members' do = icon('users fw') %span Members - if can?(current_user, :admin_group, @group) = nav_link(html_options: { class: "separate-item" }) do - = link_to edit_group_path(@group), title: 'Settings', data: {placement: 'right'} do + = link_to edit_group_path(@group), title: 'Settings' do = icon ('cogs fw') %span Settings diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index c8411521f3..56a92fe910 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,6 +1,6 @@ %ul.nav.nav-sidebar = nav_link do - = link_to group_path(@group), title: 'Go to group', data: {placement: 'right'}, class: 'back-link' do + = link_to group_path(@group), title: 'Go to group', class: 'back-link' do = icon('caret-square-o-left fw') %span Go to group @@ -9,12 +9,12 @@ %ul.sidebar-subnav = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group), title: 'Group Settings', data: {placement: 'right'} do + = link_to edit_group_path(@group), title: 'Group Settings' do = icon ('pencil-square-o fw') %span Group Settings = nav_link(path: 'groups#projects') do - = link_to projects_group_path(@group), title: 'Projects', data: {placement: 'right'} do + = link_to projects_group_path(@group), title: 'Projects' do = icon('folder fw') %span Projects diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 0f3a793e30..64b30783c0 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,6 +1,6 @@ %ul.nav.nav-sidebar = nav_link do - = link_to root_path, title: 'Go to dashboard', data: {placement: 'right'}, class: 'back-link' do + = link_to root_path, title: 'Go to dashboard', class: 'back-link' do = icon('caret-square-o-left fw') %span Go to dashboard @@ -8,52 +8,52 @@ %li.separate-item = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do - = link_to profile_path, title: 'Profile', data: {placement: 'right'} do + = link_to profile_path, title: 'Profile Settings' do = icon('user fw') %span Profile Settings = nav_link(controller: [:accounts, :two_factor_auths]) do - = link_to profile_account_path, title: 'Account', data: {placement: 'right'} do + = link_to profile_account_path, title: 'Account' do = icon('gear fw') %span Account = nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new', 'applications#create']) do - = link_to applications_profile_path, title: 'Applications', data: {placement: 'right'} do + = link_to applications_profile_path, title: 'Applications' do = icon('cloud fw') %span Applications = nav_link(controller: :emails) do - = link_to profile_emails_path, title: 'Emails', data: {placement: 'right'} do + = link_to profile_emails_path, title: 'Emails' do = icon('envelope-o fw') %span Emails %span.count= current_user.emails.count + 1 - unless current_user.ldap_user? = nav_link(controller: :passwords) do - = link_to edit_profile_password_path, title: 'Password', data: {placement: 'right'} do + = link_to edit_profile_password_path, title: 'Password' do = icon('lock fw') %span Password = nav_link(controller: :notifications) do - = link_to profile_notifications_path, title: 'Notifications', data: {placement: 'right'} do + = link_to profile_notifications_path, title: 'Notifications' do = icon('inbox fw') %span Notifications = nav_link(controller: :keys) do - = link_to profile_keys_path, title: 'SSH Keys', data: {placement: 'right'} do + = link_to profile_keys_path, title: 'SSH Keys' do = icon('key fw') %span SSH Keys %span.count= current_user.keys.count = nav_link(controller: :preferences) do - = link_to profile_preferences_path, title: 'Preferences', data: {placement: 'right'} do + = link_to profile_preferences_path, title: 'Preferences' do -# TODO (rspeicher): Better icon? = icon('image fw') %span Preferences = nav_link(path: 'profiles#audit_log') do - = link_to audit_log_profile_path, title: 'Audit Log', data: {placement: 'right'} do + = link_to audit_log_profile_path, title: 'Audit Log' do = icon('history fw') %span Audit Log diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 2b91d7721f..87a7707b09 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,13 +1,13 @@ %ul.nav.nav-sidebar - if @project.group = nav_link do - = link_to group_path(@project.group), title: 'Go to group', data: {placement: 'right'}, class: 'back-link' do + = link_to group_path(@project.group), title: 'Go to group', class: 'back-link' do = icon('caret-square-o-left fw') %span Go to group - else = nav_link do - = link_to root_path, title: 'Go to dashboard', data: {placement: 'right'}, class: 'back-link' do + = link_to root_path, title: 'Go to dashboard', class: 'back-link' do = icon('caret-square-o-left fw') %span Go to dashboard @@ -15,32 +15,32 @@ %li.separate-item = nav_link(path: 'projects#show', html_options: {class: 'home'}) do - = link_to project_path(@project), title: 'Project', class: 'shortcuts-project', data: {placement: 'right'} do + = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do = icon('home fw') %span Project = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: 'Project Activity', class: 'shortcuts-project-activity', data: {placement: 'right'} do + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do = icon('dashboard fw') %span Activity - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do - = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree', data: {placement: 'right'} do + = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do = icon('files-o fw') %span Files - if project_nav_tab? :commits = nav_link(controller: %w(commit commits compare repositories tags branches releases)) do - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do = icon('history fw') %span Commits - if project_nav_tab? :builds = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds', data: {placement: 'right'} do + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do = icon('cubes fw') %span Builds @@ -48,28 +48,28 @@ - if project_nav_tab? :network = nav_link(controller: %w(network)) do - = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network', data: {placement: 'right'} do + = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do = icon('code-fork fw') %span Network - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do - = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs', data: {placement: 'right'} do + = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do = icon('area-chart fw') %span Graphs - if project_nav_tab? :milestones = nav_link(controller: :milestones) do - = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones', data: {placement: 'right'} do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do = icon('clock-o fw') %span Milestones - if project_nav_tab? :issues = nav_link(controller: :issues) do - = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do + = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do = icon('exclamation-circle fw') %span Issues @@ -78,7 +78,7 @@ - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do - = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do = icon('tasks fw') %span Merge Requests @@ -86,35 +86,35 @@ - if project_nav_tab? :settings = nav_link(controller: [:project_members, :teams]) do - = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab', data: {placement: 'right'} do + = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do = icon('users fw') %span Members - if project_nav_tab? :labels = nav_link(controller: :labels) do - = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels', data: {placement: 'right'} do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do = icon('tags fw') %span Labels - if project_nav_tab? :wiki = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki', data: {placement: 'right'} do + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do = icon('book fw') %span Wiki - if project_nav_tab? :snippets = nav_link(controller: :snippets) do - = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets', data: {placement: 'right'} do + = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do = icon('clipboard fw') %span Snippets - if project_nav_tab? :settings = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do - = link_to edit_project_path(@project), title: 'Settings', data: {placement: 'right'} do + = link_to edit_project_path(@project), title: 'Settings' do = icon('cogs fw') %span Settings diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 377a99e719..f0b3f27b62 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -1,6 +1,6 @@ %ul.nav.nav-sidebar = nav_link do - = link_to project_path(@project), title: 'Go to project', data: {placement: 'right'}, class: 'back-link' do + = link_to project_path(@project), title: 'Go to project', class: 'back-link' do = icon('caret-square-o-left fw') %span Go to project @@ -9,59 +9,59 @@ %ul.sidebar-subnav = nav_link(path: 'projects#edit') do - = link_to edit_project_path(@project), title: 'Project Settings', data: {placement: 'right'} do + = link_to edit_project_path(@project), title: 'Project Settings' do = icon('pencil-square-o fw') %span Project Settings = nav_link(controller: :deploy_keys) do - = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys', data: {placement: 'right'} do + = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do = icon('key fw') %span Deploy Keys = nav_link(controller: :hooks) do - = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks', data: {placement: 'right'} do + = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks' do = icon('link fw') %span Web Hooks = nav_link(controller: :services) do - = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services', data: {placement: 'right'} do + = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do = icon('cogs fw') %span Services = nav_link(controller: :protected_branches) do - = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches', data: {placement: 'right'} do + = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do = icon('lock fw') %span Protected Branches - if @project.builds_enabled? = nav_link(controller: :runners) do - = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners', data: {placement: 'right'} do + = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do = icon('cog fw') %span Runners = nav_link(controller: :variables) do - = link_to namespace_project_variables_path(@project.namespace, @project) do + = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do = icon('code fw') %span Variables = nav_link path: 'triggers#index' do - = link_to namespace_project_triggers_path(@project.namespace, @project) do + = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do = icon('retweet fw') %span Triggers = nav_link path: 'ci_web_hooks#index' do - = link_to namespace_project_ci_web_hooks_path(@project.namespace, @project) do + = link_to namespace_project_ci_web_hooks_path(@project.namespace, @project), title: 'CI Web Hooks' do = icon('link fw') %span CI Web Hooks = nav_link path: 'ci_settings#edit' do - = link_to edit_namespace_project_ci_settings_path(@project.namespace, @project) do + = link_to edit_namespace_project_ci_settings_path(@project.namespace, @project), title: 'CI Settings' do = icon('building fw') %span CI Settings = nav_link controller: 'ci_services' do - = link_to namespace_project_ci_services_path(@project.namespace, @project) do + = link_to namespace_project_ci_services_path(@project.namespace, @project), title: 'CI Services' do = icon('share fw') %span CI Services From 5c771cca3312dfd447ebe5e0b92ebe279ca1dd67 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:05:24 +0100 Subject: [PATCH 171/908] Fix logo height in signed-out header bar --- app/assets/stylesheets/framework/header.scss | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 02ea91602e..4dbbb56104 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -6,15 +6,17 @@ header { transition-duration: .3s; &.navbar-empty { + height: 58px; background: #FFF; border-bottom: 1px solid #EEE; .center-logo { - margin: 8px 0; + margin: 11px 0; text-align: center; - img { - height: 32px; + #tanuki-logo, img { + width: 36px; + height: 36px; } } } From 4f7380f94eb7d118b7be51c09e878dfcca84a942 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:05:58 +0100 Subject: [PATCH 172/908] Fix header tooltip alignment --- app/views/layouts/header/_default.html.haml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 3ca30d3baa..3892ef8eef 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -11,27 +11,27 @@ %li.hidden-sm.hidden-xs = render 'layouts/search' %li.visible-sm.visible-xs - = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom'} do + = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('search') - if session[:impersonator_id] %li.impersonation - = link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop impersonation', data: { toggle: 'tooltip', placement: 'bottom' } do + = link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('user-secret fw') - if current_user.is_admin? %li - = link_to admin_root_path, title: 'Admin area', data: {toggle: 'tooltip', placement: 'bottom'} do + = link_to admin_root_path, title: 'Admin Area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('wrench fw') - if current_user.can_create_project? %li - = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do + = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('plus fw') - if Gitlab::Sherlock.enabled? %li = link_to sherlock_transactions_path, title: 'Sherlock Transactions', - data: {toggle: 'tooltip', placement: 'bottom'} do + data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('tachometer fw') %li - = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom'} do + = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('sign-out') %h1.title= title From 1ef01e4f6a25743f2034da222407a41b63920df0 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:06:11 +0100 Subject: [PATCH 173/908] Fade in/out Back icon when sidebar is toggled --- app/assets/stylesheets/framework/sidebar.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 017f4d2657..458af76cb7 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -162,6 +162,10 @@ padding: 0px 8px; @include border-radius(6px); } + + &.back-link i { + transition-duration: .3s; + } } } } @@ -190,7 +194,7 @@ &.back-link { i { - visibility: hidden; + opacity: 0; } } } From 00a4be759e091bf3e07bde48650e1402da908896 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:06:25 +0100 Subject: [PATCH 174/908] Page titles are title case. --- app/views/layouts/admin.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index 1c738719bd..6591c52bdb 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -1,5 +1,5 @@ -- page_title "Admin area" -- header_title "Admin area", admin_root_path +- page_title "Admin Area" +- header_title "Admin Area", admin_root_path - sidebar "admin" = render template: "layouts/application" From 112cca872bd7982bd2452f2f0903d21342d5ea5f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:07:21 +0100 Subject: [PATCH 175/908] Move project visibility status to the left --- app/assets/stylesheets/framework/blocks.scss | 5 +++++ app/views/projects/_home_panel.html.haml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 1635df9c97..9e30d3b9c8 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -112,5 +112,10 @@ position: absolute; top: 10px; right: 10px; + + &.left { + left: 10px; + right: auto; + } } } diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index b30036966a..d94a279eaf 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -12,9 +12,9 @@ Forked from = link_to project_path(forked_from_project) do = forked_from_project.namespace.try(:name) - .cover-controls .visibility-level-label = visibility_level_icon(@project.visibility_level) + .cover-controls.left = visibility_level_label(@project.visibility_level) .project-repo-buttons From daca985a6e75d6f43c5cc5b487a0942d5bf93f68 Mon Sep 17 00:00:00 2001 From: Andrew Tomaka Date: Tue, 1 Dec 2015 23:40:24 -0500 Subject: [PATCH 176/908] Prevent impersonation if blocked --- .../admin/impersonation_controller.rb | 16 +++++++++++----- app/views/admin/users/_head.html.haml | 2 +- .../admin/impersonation_controller_spec.rb | 19 +++++++++++++++++++ spec/features/admin/admin_users_spec.rb | 10 ++++++++++ 4 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 spec/controllers/admin/impersonation_controller_spec.rb diff --git a/app/controllers/admin/impersonation_controller.rb b/app/controllers/admin/impersonation_controller.rb index 0382402afa..102dd43740 100644 --- a/app/controllers/admin/impersonation_controller.rb +++ b/app/controllers/admin/impersonation_controller.rb @@ -5,14 +5,20 @@ class Admin::ImpersonationController < Admin::ApplicationController before_action :authorize_impersonator! def create - session[:impersonator_id] = current_user.username - session[:impersonator_return_to] = request.env['HTTP_REFERER'] + if @user.blocked? + flash[:alert] = "You cannot impersonate a blocked user" - warden.set_user(user, scope: 'user') + redirect_to admin_user_path(@user) + else + session[:impersonator_id] = current_user.username + session[:impersonator_return_to] = request.env['HTTP_REFERER'] - flash[:alert] = "You are impersonating #{user.username}." + warden.set_user(user, scope: 'user') - redirect_to root_path + flash[:alert] = "You are impersonating #{user.username}." + + redirect_to root_path + end end def destroy diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index 8d1cab4137..5e17b01816 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -6,7 +6,7 @@ %span.cred (Admin) .pull-right - - unless @user == current_user + - unless @user == current_user || @user.blocked? = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-grouped btn-info" = link_to edit_admin_user_path(@user), class: "btn btn-grouped" do %i.fa.fa-pencil-square-o diff --git a/spec/controllers/admin/impersonation_controller_spec.rb b/spec/controllers/admin/impersonation_controller_spec.rb new file mode 100644 index 0000000000..d7a7ba1c5b --- /dev/null +++ b/spec/controllers/admin/impersonation_controller_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Admin::ImpersonationController do + let(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + describe 'CREATE #impersonation when blocked' do + let(:blocked_user) { create(:user, state: :blocked) } + + it 'does not allow impersonation' do + post :create, id: blocked_user.username + + expect(flash[:alert]).to eq 'You cannot impersonate a blocked user' + end + end +end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 86f01faffb..4570e40912 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -128,6 +128,16 @@ describe "Admin::Users", feature: true do expect(page).not_to have_content('Impersonate') end + + it 'should not show impersonate button for blocked user' do + another_user.block + + visit admin_user_path(another_user) + + expect(page).not_to have_content('Impersonate') + + another_user.activate + end end context 'when impersonating' do From 085e45a81cbdac73f41702764a1b53a56a298653 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:08:25 +0100 Subject: [PATCH 177/908] Add edit and RSS buttons to project home panel --- app/views/projects/_home_panel.html.haml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index d94a279eaf..695da7f07d 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -17,6 +17,15 @@ .cover-controls.left = visibility_level_label(@project.visibility_level) + .cover-controls + - if can?(current_user, :admin_project, @project) + = link_to edit_project_path(@project), class: 'btn btn-gray' do + = icon('pencil') + - if current_user +   + = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), class: 'btn btn-gray' do + = icon('rss') + .project-repo-buttons .split-one = render 'projects/buttons/star' From d65647e90c25a1cf28353b3d0476aec0a4c6ebb7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:08:59 +0100 Subject: [PATCH 178/908] Add visibility description tooltip to snippet and project visibility labels --- app/helpers/icons_helper.rb | 22 ++++++----- app/helpers/visibility_level_helper.rb | 44 +++++---------------- app/views/projects/_home_panel.html.haml | 4 +- app/views/shared/snippets/_header.html.haml | 4 +- 4 files changed, 27 insertions(+), 47 deletions(-) diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 1cf5b96481..5724d3aabe 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -27,16 +27,20 @@ module IconsHelper end end - def public_icon - icon('globe fw') - end + def visibility_level_icon(level, fw: true) + name = + case level + when Gitlab::VisibilityLevel::PRIVATE + 'lock' + when Gitlab::VisibilityLevel::INTERNAL + 'shield' + else # Gitlab::VisibilityLevel::PUBLIC + 'globe' + end + + name << " fw" if fw - def internal_icon - icon('shield fw') - end - - def private_icon - icon('lock fw') + icon(name) end def file_type_icon_class(type, mode, name) diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index b52cd23aba..72c65030f9 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -25,48 +25,24 @@ module VisibilityLevelHelper end def project_visibility_level_description(level) - capture_haml do - haml_tag :span do - case level - when Gitlab::VisibilityLevel::PRIVATE - haml_concat "Project access must be granted explicitly for each user." - when Gitlab::VisibilityLevel::INTERNAL - haml_concat "The project can be cloned by" - haml_concat "any logged in user." - when Gitlab::VisibilityLevel::PUBLIC - haml_concat "The project can be cloned" - haml_concat "without any" - haml_concat "authentication." - end - end + case level + when Gitlab::VisibilityLevel::PRIVATE + "Project access must be granted explicitly for each user." + when Gitlab::VisibilityLevel::INTERNAL + "The project can be cloned by any logged in user." + when Gitlab::VisibilityLevel::PUBLIC + "The project can be cloned without any authentication." end end def snippet_visibility_level_description(level) - capture_haml do - haml_tag :span do - case level - when Gitlab::VisibilityLevel::PRIVATE - haml_concat "The snippet is visible only for me." - when Gitlab::VisibilityLevel::INTERNAL - haml_concat "The snippet is visible for any logged in user." - when Gitlab::VisibilityLevel::PUBLIC - haml_concat "The snippet can be accessed" - haml_concat "without any" - haml_concat "authentication." - end - end - end - end - - def visibility_level_icon(level) case level when Gitlab::VisibilityLevel::PRIVATE - private_icon + "The snippet is visible only for me." when Gitlab::VisibilityLevel::INTERNAL - internal_icon + "The snippet is visible for any logged in user." when Gitlab::VisibilityLevel::PUBLIC - public_icon + "The snippet can be accessed without any authentication." end end diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 695da7f07d..c1669ac046 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -12,9 +12,9 @@ Forked from = link_to project_path(forked_from_project) do = forked_from_project.namespace.try(:name) - .visibility-level-label - = visibility_level_icon(@project.visibility_level) .cover-controls.left + .visibility-level-label.has_tooltip{title: project_visibility_level_description(@project.visibility_level), data: { container: 'body' } } + = visibility_level_icon(@project.visibility_level, fw: false) = visibility_level_label(@project.visibility_level) .cover-controls diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 0a4a790ec5..b6c9872f08 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -1,7 +1,7 @@ .snippet-details .page-title - .snippet-box{class: visibility_level_color(@snippet.visibility_level)} - = visibility_level_icon(@snippet.visibility_level) + .snippet-box.has_tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level), data: { container: 'body' }} + = visibility_level_icon(@snippet.visibility_level, fw: false) = visibility_level_label(@snippet.visibility_level) %span.snippet-id Snippet ##{@snippet.id} %span.creator From de5e28cdcfe8554f2f45f3247d8304aa94f5c3cb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:09:13 +0100 Subject: [PATCH 179/908] Use default cursor for project visibility label. --- app/assets/stylesheets/pages/projects.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 4a0fe54684..68a3617512 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -94,7 +94,7 @@ @extend .btn-gray; color: $gray; - cursor: auto; + cursor: default; i { color: inherit; From 762e759bb3546db7f058d8055ed7b62590d80217 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:09:18 +0100 Subject: [PATCH 180/908] Remove duplicate styles. --- app/assets/stylesheets/pages/projects.scss | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 68a3617512..25d4d0a2c4 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -30,12 +30,6 @@ } .project-home-panel { - text-align: center; - background: #f7f8fa; - margin: -$gl-padding; - padding: $gl-padding; - padding: 44px 0 17px 0; - .project-identicon-holder { margin-bottom: 16px; @@ -105,7 +99,6 @@ display: inline-table; position: relative; top: 17px; - margin-bottom: 44px; } .project-repo-buttons { From 6252fe4510376c917a690e655f8f4257132dde3e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:09:50 +0100 Subject: [PATCH 181/908] Unify new project namespace select and path input --- app/assets/stylesheets/framework/forms.scss | 8 ++++++++ app/helpers/namespaces_helper.rb | 6 +++--- app/views/projects/new.html.haml | 20 ++++++++++---------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 0edfe24f19..95674c5812 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -88,7 +88,15 @@ label { } .input-group { + .select2-container { + display: table-cell; + width: 200px !important; + } .input-group-addon { background-color: #f7f8fa; } + .input-group-addon:not(:first-child):not(:last-child) { + border-left: 0; + border-right: 0; + } } diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index e7f3cb2103..faba418c4d 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -1,10 +1,10 @@ module NamespacesHelper - def namespaces_options(selected = :current_user, scope = :default) + def namespaces_options(selected = :current_user, display_path: false) groups = current_user.owned_groups + current_user.masters_groups users = [current_user.namespace] - group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [g.human_name, g.id]} ] - users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [u.human_name, u.id]} ] + group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [display_path ? g.path : g.human_name, g.id]} ] + users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [display_path ? u.path : u.human_name, u.id]} ] options = [] options << group_opts diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index c9d1fc3da2..f6355ea670 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -11,16 +11,16 @@ Project path .col-sm-10 .input-group - = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 1, autofocus: true, required: true - .input-group-addon - \.git - - - if current_user.can_select_namespace? - .form-group - = f.label :namespace_id, class: 'control-label' do - %span Namespace - .col-sm-10 - = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'select2', tabindex: 2} + - if current_user.can_select_namespace? + .input-group-addon + = root_url + = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2', tabindex: 1} + .input-group-addon + \/ + - else + .input-group-addon + #{root_url}#{current_user.username}/ + = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true - if import_sources_enabled? .project-import.js-toggle-container From 37ab1120c8a252aa29702ef141ff4ea1163e0e75 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:10:03 +0100 Subject: [PATCH 182/908] Move "Add Group" button higher up in new project form --- app/views/projects/new.html.haml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index f6355ea670..a4bd8d54af 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -22,6 +22,11 @@ #{root_url}#{current_user.username}/ = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true + - if current_user.can_create_group? + .help-block + Want to house several dependent projects under the same namespace? + = link_to "Create a group", new_group_path + - if import_sources_enabled? .project-import.js-toggle-container .form-group @@ -96,14 +101,6 @@ .form-actions = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 - - if current_user.can_create_group? - .pull-right - .light.inline - .space-right - Need a group for several dependent projects? - = link_to new_group_path, class: "btn" do - Create a group - .save-project-loader.hide .center %h2 From a895d5d3b2252cd962b8ea19d2c1092cbee7fca6 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:10:32 +0100 Subject: [PATCH 183/908] Use select2 for bulk issue edit status --- app/assets/javascripts/issues.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index 40bb9e9cb0..ac9e022e72 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -29,7 +29,7 @@ $('#filter_issue_search').val($('#issue_search').val()) initSelects: -> - $("select#update_status").select2(width: 'resolve', dropdownAutoWidth: true) + $("select#update_state_event").select2(width: 'resolve', dropdownAutoWidth: true) $("select#update_assignee_id").select2(width: 'resolve', dropdownAutoWidth: true) $("select#update_milestone_id").select2(width: 'resolve', dropdownAutoWidth: true) $("select#label_name").select2(width: 'resolve', dropdownAutoWidth: true) From f385988d1648e54218d68e02a25eaad048b04d0b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:10:43 +0100 Subject: [PATCH 184/908] Use "Any Label" and "Any Milestone" in selects rather than the ambiguous "Any" option --- app/models/label.rb | 2 +- app/models/milestone.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/label.rb b/app/models/label.rb index b306aecbac..bef6063fe8 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -17,7 +17,7 @@ class Label < ActiveRecord::Base # Requests that have no label assigned. LabelStruct = Struct.new(:title, :name) None = LabelStruct.new('No Label', 'No Label') - Any = LabelStruct.new('Any', '') + Any = LabelStruct.new('Any Label', '') DEFAULT_COLOR = '#428BCA' diff --git a/app/models/milestone.rb b/app/models/milestone.rb index c2642b75b8..d8c7536cd3 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -16,9 +16,9 @@ class Milestone < ActiveRecord::Base # Represents a "No Milestone" state used for filtering Issues and Merge # Requests that have no milestone assigned. - MilestoneStruct = Struct.new(:title, :name) - None = MilestoneStruct.new('No Milestone', 'No Milestone') - Any = MilestoneStruct.new('Any', '') + MilestoneStruct = Struct.new(:title, :name, :id) + None = MilestoneStruct.new('No Milestone', 'No Milestone', 0) + Any = MilestoneStruct.new('Any Milestone', '', -1) include InternalId include Sortable From af541515fa49756d4d23719029648276ed6c2f5b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 2 Dec 2015 14:11:32 +0100 Subject: [PATCH 185/908] Remove "none" username for "Unassigned" and "Any User" select options --- app/assets/javascripts/users_select.js.coffee | 13 +++++-------- app/assets/stylesheets/framework/selects.scss | 8 +++++++- app/helpers/issues_helper.rb | 7 ++++--- app/helpers/selects_helper.rb | 14 ++++++++------ app/views/shared/issuable/_filter.html.haml | 4 ++-- 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index f5db74d84e..12abf806bf 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -32,17 +32,15 @@ class @UsersSelect if showNullUser nullUser = { name: 'Unassigned', - avatar: null, - username: 'none', id: 0 } data.results.unshift(nullUser) if showAnyUser + name = showAnyUser + name = 'Any User' if name == true anyUser = { - name: 'Any', - avatar: null, - username: 'none', + name: name, id: null } data.results.unshift(anyUser) @@ -50,7 +48,6 @@ class @UsersSelect if showEmailUser && data.results.length == 0 && query.term.match(/^[^@]+@[^@]+$/) emailUser = { name: "Invite \"#{query.term}\"", - avatar: null, username: query.term, id: query.term } @@ -82,10 +79,10 @@ class @UsersSelect else avatar = gon.default_avatar_url - "