From 4f0a38f1a833cab8c83e77a6c5d323057883188d Mon Sep 17 00:00:00 2001 From: Jeroen Nijhof Date: Wed, 21 Oct 2015 15:15:54 +0200 Subject: [PATCH 001/280] Added housekeeping for git repositories --- app/controllers/projects_controller.rb | 8 +++++++ app/services/projects/housekeeping_service.rb | 22 +++++++++++++++++++ app/views/projects/edit.html.haml | 11 ++++++++++ config/routes.rb | 1 + lib/gitlab/backend/shell.rb | 12 ++++++++++ 5 files changed, 54 insertions(+) create mode 100644 app/services/projects/housekeeping_service.rb diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 82119022cf9..c3efdffe563 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -171,6 +171,14 @@ class ProjectsController < ApplicationController end end + def housekeeping + ::Projects::HousekeepingService.new(@project).execute + + respond_to do |format| + format.html { redirect_to project_path(@project) } + end + end + def toggle_star current_user.toggle_star(@project) @project.reload diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb new file mode 100644 index 00000000000..48875ac3449 --- /dev/null +++ b/app/services/projects/housekeeping_service.rb @@ -0,0 +1,22 @@ +# Projects::HousekeepingService class +# +# Used for git housekeeping +# +# Ex. +# Projects::HousekeepingService.new(project, user).execute +# +module Projects + class HousekeepingService < BaseService + include Gitlab::ShellAdapter + + def initialize(project) + @project = project + end + + def execute + if gitlab_shell.exists?(@project.path_with_namespace + '.git') + gitlab_shell.gc(@project.path_with_namespace) + end + end + end +end diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index afbf88b5507..8e49299223c 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -141,6 +141,17 @@ - else .nothing-here-block Only the project owner can archive a project + .panel.panel-default.panel.panel-warning + .panel-heading Housekeeping + .errors-holder + .panel-body + %p + Runs a number of housekeeping tasks within the current repository, + such as compressing file revisions and removing unreachable objects. + %br + = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), + method: :post, class: "btn btn-warning" + .panel.panel-default.panel.panel-warning .panel-heading Rename repository .errors-holder diff --git a/config/routes.rb b/config/routes.rb index f6812c9280a..f6e17a21479 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -381,6 +381,7 @@ Gitlab::Application.routes.draw do delete :remove_fork post :archive post :unarchive + post :housekeeping post :toggle_star post :markdown_preview get :autocomplete_sources diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 01b8bda05c6..59f7a45b791 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -149,6 +149,18 @@ module Gitlab "#{path}.git", tag_name]) end + # Gc repository + # + # path - project path with namespace + # + # Ex. + # gc("gitlab/gitlab-ci") + # + def gc(path) + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'gc', + "#{path}.git"]) + end + # Add new key to gitlab-shell # # Ex. From c843722de2d778b6ec8fef0656797fd5a8074666 Mon Sep 17 00:00:00 2001 From: Jeff Stubler Date: Mon, 20 Jul 2015 20:34:19 -0500 Subject: [PATCH 002/280] Add graphs showing commits ahead and behind default to branches page --- CHANGELOG | 1 + app/assets/stylesheets/pages/commits.scss | 59 +++++++++++++++++++ .../projects/branches_controller.rb | 6 ++ app/models/project.rb | 2 + app/models/repository.rb | 42 ++++++++++++- app/views/projects/branches/_branch.html.haml | 14 +++++ 6 files changed, 122 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6bec4f606e7..98702088199 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,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. + - Add graphs of commits ahead and behind default branch (Jeff Stubler) v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 4e121b95d13..4a080db7464 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -113,3 +113,62 @@ li.commit { } } } + +.divergence-graph { + padding: 12px 12px 0 0; + float: right; + + .graph-side { + position: relative; + width: 80px; + height: 22px; + padding: 5px 0 13px; + float: left; + + .bar { + position: absolute; + height: 4px; + background-color: #ccc; + } + + .bar-behind { + right: 0; + border-radius: 3px 0 0 3px; + } + + .bar-ahead { + left: 0; + border-radius: 0 3px 3px 0; + } + + .count { + padding-top: 6px; + padding-bottom: 0px; + font-size: 12px; + color: #333; + display: block; + } + + .count-behind { + padding-right: 4px; + text-align: right; + } + + .count-ahead { + padding-left: 4px; + text-align: left; + } + } + + .graph-separator { + position: relative; + width: 1px; + height: 18px; + margin: 5px 0 0; + float: left; + background-color: #ccc; + } +} + + + diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 3ac0a75fa70..c3cd7642dd2 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -9,6 +9,12 @@ class Projects::BranchesController < Projects::ApplicationController @sort = params[:sort] || 'name' @branches = @repository.branches_sorted_by(@sort) @branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE) + + @max_commits = @branches.reduce(0) do + |memo, branch| + diverging_commit_counts = repository.diverging_commit_counts(branch) + [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max + end end def recent diff --git a/app/models/project.rb b/app/models/project.rb index 74b89aad499..e73a856c289 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -714,6 +714,8 @@ class Project < ActiveRecord::Base end def change_head(branch) + # Cached divergent commit counts are based on repository head + repository.expire_branch_cache gitlab_shell.update_repository_head(self.path_with_namespace, branch) reload_default_branch end diff --git a/app/models/repository.rb b/app/models/repository.rb index c9b36bd8170..9b270bc9d18 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -146,10 +146,27 @@ class Repository def size cache.fetch(:size) { raw_repository.size } end + + def diverging_commit_counts(branch) + branch_cache_key = ('diverging_commit_counts_' + branch.name).to_sym + cache.fetch(branch_cache_key) do + number_commits_behind = commits_between(branch.name, root_ref).size + number_commits_ahead = commits_between(root_ref, branch.name).size + + { behind: number_commits_behind, ahead: number_commits_ahead } + end + end def cache_keys - %i(size branch_names tag_names commit_count - readme version contribution_guide changelog license) + %i(size branch_names tag_names commit_count readme + contribution_guide changelog license) + end + + def branch_cache_keys + branches.map do + |branch| + ('diverging_commit_counts_' + branch.name).to_sym + end end def build_cache @@ -158,12 +175,28 @@ class Repository send(key) end end + + branches.each do |branch| + unless cache.exist?(('diverging_commit_counts_' + branch.name).to_sym) + send(:diverging_commit_counts, branch) + end + end end def expire_cache cache_keys.each do |key| cache.expire(key) end + + branches.each do |branch| + cache.expire(('diverging_commit_counts_' + branch.name).to_sym) + end + end + + def expire_branch_cache + branches.each do |branch| + cache.expire(('diverging_commit_counts_' + branch.name).to_sym) + end end def rebuild_cache @@ -171,6 +204,11 @@ class Repository cache.expire(key) send(key) end + + branches.each do |branch| + cache.expire(('diverging_commit_counts_' + branch.name).to_sym) + send(:diverging_commit_counts, branch) + end end def lookup_cache diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index cc0ec9483d2..9ddb10a1c74 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,4 +1,7 @@ - commit = @repository.commit(branch.target) +- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0 +- number_commits_behind = @repository.diverging_commit_counts(branch)[:behind] +- number_commits_ahead = @repository.diverging_commit_counts(branch)[:ahead] %li(class="js-branch-#{branch.name}") %div = link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do @@ -29,6 +32,17 @@ = 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: 'Removed branch cannot be restored. Are you sure?'}, remote: true do = icon("trash-o") + - if branch.name != @repository.root_ref + .divergence-graph{ :title => "#{number_commits_ahead} commits ahead, #{number_commits_behind} commits behind #{@repository.root_ref}" } + .graph-side + .bar.bar-behind{ :style => "width: #{number_commits_behind * bar_graph_width_factor}%" } + %span.count.count-behind= number_commits_behind + .graph-separator + .graph-side + .bar.bar-ahead{ :style => "width: #{number_commits_ahead * bar_graph_width_factor}%" } + %span.count.count-ahead= number_commits_ahead + + - if commit = render 'projects/branches/commit', commit: commit, project: @project - else From e0c64fac68b4b3acc48300956146b85e03b426ce Mon Sep 17 00:00:00 2001 From: Jeff Stubler Date: Wed, 11 Nov 2015 16:29:29 -0600 Subject: [PATCH 003/280] Refactor for style issues --- .../projects/branches_controller.rb | 3 +-- app/models/repository.rb | 24 ++++++++----------- app/views/projects/branches/_branch.html.haml | 11 +++++---- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index c3cd7642dd2..87884420952 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -10,8 +10,7 @@ class Projects::BranchesController < Projects::ApplicationController @branches = @repository.branches_sorted_by(@sort) @branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE) - @max_commits = @branches.reduce(0) do - |memo, branch| + @max_commits = @branches.reduce(0) do |memo, branch| diverging_commit_counts = repository.diverging_commit_counts(branch) [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max end diff --git a/app/models/repository.rb b/app/models/repository.rb index 9b270bc9d18..0e2d4ea1fb8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -148,8 +148,7 @@ class Repository end def diverging_commit_counts(branch) - branch_cache_key = ('diverging_commit_counts_' + branch.name).to_sym - cache.fetch(branch_cache_key) do + cache.fetch(:"diverging_commit_counts_#{branch.name}") do number_commits_behind = commits_between(branch.name, root_ref).size number_commits_ahead = commits_between(root_ref, branch.name).size @@ -158,14 +157,13 @@ class Repository end def cache_keys - %i(size branch_names tag_names commit_count readme - contribution_guide changelog license) + %i(size branch_names tag_names commit_count + readme version contribution_guide changelog license) end def branch_cache_keys - branches.map do - |branch| - ('diverging_commit_counts_' + branch.name).to_sym + branches.map do |branch| + :"diverging_commit_counts_#{branch.name}" end end @@ -177,7 +175,7 @@ class Repository end branches.each do |branch| - unless cache.exist?(('diverging_commit_counts_' + branch.name).to_sym) + unless cache.exist?(:"diverging_commit_counts_#{branch.name}") send(:diverging_commit_counts, branch) end end @@ -188,14 +186,12 @@ class Repository cache.expire(key) end - branches.each do |branch| - cache.expire(('diverging_commit_counts_' + branch.name).to_sym) - end + expire_branch_cache end def expire_branch_cache branches.each do |branch| - cache.expire(('diverging_commit_counts_' + branch.name).to_sym) + cache.expire(:"diverging_commit_counts_#{branch.name}") end end @@ -206,8 +202,8 @@ class Repository end branches.each do |branch| - cache.expire(('diverging_commit_counts_' + branch.name).to_sym) - send(:diverging_commit_counts, branch) + cache.expire(:"diverging_commit_counts_#{branch.name}") + diverging_commit_counts(branch) end end diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 9ddb10a1c74..a4202d1120d 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,7 +1,8 @@ - commit = @repository.commit(branch.target) - bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0 -- number_commits_behind = @repository.diverging_commit_counts(branch)[:behind] -- number_commits_ahead = @repository.diverging_commit_counts(branch)[:ahead] +- diverging_commit_counts = @repository.diverging_commit_counts(branch) +- number_commits_behind = diverging_commit_counts[:behind] +- number_commits_ahead = diverging_commit_counts[:ahead] %li(class="js-branch-#{branch.name}") %div = link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do @@ -33,13 +34,13 @@ = icon("trash-o") - if branch.name != @repository.root_ref - .divergence-graph{ :title => "#{number_commits_ahead} commits ahead, #{number_commits_behind} commits behind #{@repository.root_ref}" } + .divergence-graph{ title: "#{number_commits_ahead} commits ahead, #{number_commits_behind} commits behind #{@repository.root_ref}" } .graph-side - .bar.bar-behind{ :style => "width: #{number_commits_behind * bar_graph_width_factor}%" } + .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" } %span.count.count-behind= number_commits_behind .graph-separator .graph-side - .bar.bar-ahead{ :style => "width: #{number_commits_ahead * bar_graph_width_factor}%" } + .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" } %span.count.count-ahead= number_commits_ahead From 839aae0e473e85042f76391b44eaeb099235a813 Mon Sep 17 00:00:00 2001 From: Jeroen Nijhof Date: Thu, 19 Nov 2015 15:16:54 +0100 Subject: [PATCH 004/280] Added housekeeping status and moved path check to gitlab-shell --- app/controllers/projects_controller.rb | 9 +++++++-- app/services/projects/housekeeping_service.rb | 6 ++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index c3efdffe563..27b723fae6a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -8,7 +8,7 @@ class ProjectsController < ApplicationController before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists? # Authorize - before_action :authorize_admin_project!, only: [:edit, :update] + before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping] before_action :event_filter, only: [:show, :activity] layout :determine_layout @@ -172,9 +172,14 @@ class ProjectsController < ApplicationController end def housekeeping - ::Projects::HousekeepingService.new(@project).execute + status = ::Projects::HousekeepingService.new(@project).execute respond_to do |format| + if status + flash[:notice] = "Housekeeping finished successfully." + else + flash[:alert] = "Housekeeping failed." + end format.html { redirect_to project_path(@project) } end end diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index 48875ac3449..bea91b5f180 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -3,7 +3,7 @@ # Used for git housekeeping # # Ex. -# Projects::HousekeepingService.new(project, user).execute +# Projects::HousekeepingService.new(project).execute # module Projects class HousekeepingService < BaseService @@ -14,9 +14,7 @@ module Projects end def execute - if gitlab_shell.exists?(@project.path_with_namespace + '.git') - gitlab_shell.gc(@project.path_with_namespace) - end + gitlab_shell.gc(@project.path_with_namespace) end end end From d4690af8bc283c402e49cb8b3056c7de9d57e886 Mon Sep 17 00:00:00 2001 From: Jeroen Nijhof Date: Thu, 19 Nov 2015 16:04:07 +0100 Subject: [PATCH 005/280] Use GitlabShellWorker.perform_async for housekeeping --- app/controllers/projects_controller.rb | 8 ++------ app/services/projects/housekeeping_service.rb | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 27b723fae6a..ecaf4476246 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -172,14 +172,10 @@ class ProjectsController < ApplicationController end def housekeeping - status = ::Projects::HousekeepingService.new(@project).execute + ::Projects::HousekeepingService.new(@project).execute respond_to do |format| - if status - flash[:notice] = "Housekeeping finished successfully." - else - flash[:alert] = "Housekeeping failed." - end + flash[:notice] = "Housekeeping successfully started." format.html { redirect_to project_path(@project) } end end diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index bea91b5f180..0db85ac2142 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -14,7 +14,7 @@ module Projects end def execute - gitlab_shell.gc(@project.path_with_namespace) + GitlabShellWorker.perform_async(:gc, @project.path_with_namespace) end end end From c12514fc2d3996e7cfc3553d8e2bac04d0c5afec Mon Sep 17 00:00:00 2001 From: The rugged tests are fragile Date: Tue, 15 Dec 2015 15:05:57 +0800 Subject: [PATCH 006/280] Ignore config/sidekiq.yml [ci skip] --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f5b6427ca03..91ea81bfc4e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ config/initializers/smtp_settings.rb config/resque.yml config/unicorn.rb config/secrets.yml +config/sidekiq.yml coverage/* db/*.sqlite3 db/*.sqlite3-journal From 2e8ec7e7204b2876218db34439584204b1062265 Mon Sep 17 00:00:00 2001 From: Jeff Stubler Date: Tue, 15 Dec 2015 16:23:52 -0600 Subject: [PATCH 007/280] Fix diverging commit count calculation Rugged seemed to stop accepting branch names as valid refs, throwing `Rugged::ReferenceError` exceptions. Now the branch target rather than branch name is used, and the default branch's hash is also calculated, and these work properly. This used to work and might be something worth re-investigating in the future. --- app/models/repository.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 4186ef295c9..77e5bd975ec 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -157,9 +157,12 @@ class Repository end def diverging_commit_counts(branch) + root_ref_hash = raw_repository.rev_parse_target(root_ref).oid cache.fetch(:"diverging_commit_counts_#{branch.name}") do - number_commits_behind = commits_between(branch.name, root_ref).size - number_commits_ahead = commits_between(root_ref, branch.name).size + # Rugged seems to throw a `ReferenceError` when given branch_names rather + # than SHA-1 hashes + number_commits_behind = commits_between(branch.target, root_ref_hash).size + number_commits_ahead = commits_between(root_ref_hash, branch.target).size { behind: number_commits_behind, ahead: number_commits_ahead } end From b45ee2c314e2c26f4574f2e973dfa40204860c66 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Wed, 16 Dec 2015 10:08:05 -0400 Subject: [PATCH 008/280] better support for referencing and closing issues in asana_service.rb --- app/models/project_services/asana_service.rb | 28 +++++++++++++------ .../project_services/asana_service_spec.rb | 21 ++++++++++++++ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index e6e16058d41..bbc508e8f8e 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -98,17 +98,29 @@ automatically inspected. Leave blank to include all branches.' task_list = [] close_list = [] - message.split("\n").each do |line| - # look for a task ID or a full Asana url - task_list.concat(line.scan(/#(\d+)/)) - task_list.concat(line.scan(/https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)/)) - # look for a word starting with 'fix' followed by a task ID - close_list.concat(line.scan(/(fix\w*)\W*#(\d+)/i)) + # matches either: + # - #1234 + # - https://app.asana.com/0/0/1234 + # optionally preceded with: + # - fix/ed/es/ing + # - close/s/d + # - closing + issue_finder = /(fix\w*|clos[ei]\w*+)?\W*(?:https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)|#(\d+))/i + + message.scan(issue_finder).each do |tuple| + # tuple will be + # [ 'fix', 'id_from_url', 'id_from_pound' ] + taskid = tuple[2] || tuple[1] + task_list.push(taskid) + + if tuple[0] + close_list.push(taskid) + end end # post commit to every taskid found task_list.each do |taskid| - task = Asana::Task.find(taskid[0]) + task = Asana::Task.find(taskid) if task task.create_story(text: push_msg + ' ' + message) @@ -117,7 +129,7 @@ automatically inspected. Leave blank to include all branches.' # close all tasks that had 'fix(ed/es/ing) #:id' in them close_list.each do |taskid| - task = Asana::Task.find(taskid.last) + task = Asana::Task.find(taskid) if task task.modify(completed: true) diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb index 64bb92fba95..e368b03206e 100644 --- a/spec/models/project_services/asana_service_spec.rb +++ b/spec/models/project_services/asana_service_spec.rb @@ -62,5 +62,26 @@ describe AsanaService, models: true do @asana.check_commit('fix #456789', 'pushed') end + + it 'should be able to close via url' do + expect(Asana::Task).to receive(:find).with('42').twice + + @asana.check_commit('closes https://app.asana.com/19292/956299/42', 'pushed') + end + + it 'should allow multiple matches per line' do + expect(Asana::Task).to receive(:find).with('123').twice + expect(Asana::Task).to receive(:find).with('456').twice + expect(Asana::Task).to receive(:find).with('789').once + + expect(Asana::Task).to receive(:find).with('42').once + expect(Asana::Task).to receive(:find).with('12').twice + + message = <<-EOF + minor bigfix, refactoring, fixed #123 and Closes #456 work on #789 + ref https://app.asana.com/19292/956299/42 and closing https://app.asana.com/19292/956299/12 + EOF + @asana.check_commit(message, 'pushed') + end end end From 331154ffdf899a82c67487a70436ce49e494256b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 24 Dec 2015 14:38:23 +0100 Subject: [PATCH 009/280] Escape reference link text --- lib/banzai/filter/abstract_reference_filter.rb | 17 ++++++++++------- lib/banzai/filter/reference_filter.rb | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index bdaa4721b4b..6b200dc2017 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -110,13 +110,7 @@ module Banzai url = matches[:url] if matches.names.include?("url") url ||= url_for_object(object, 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? - end + text = link_text || escape_once(object_link_text(object, matches)) %( Date: Thu, 24 Dec 2015 14:43:07 +0100 Subject: [PATCH 010/280] Render milestone links as references --- app/models/milestone.rb | 18 +++++ .../filter/abstract_reference_filter.rb | 40 +++++----- .../filter/milestone_reference_filter.rb | 24 ++++++ lib/banzai/pipeline/gfm_pipeline.rb | 1 + lib/gitlab/reference_extractor.rb | 2 +- spec/features/markdown_spec.rb | 1 + spec/fixtures/markdown.md.erb | 8 ++ .../filter/milestone_reference_filter_spec.rb | 73 +++++++++++++++++++ spec/support/markdown_feature.rb | 8 ++ spec/support/matchers/markdown_matchers.rb | 9 +++ 10 files changed, 165 insertions(+), 19 deletions(-) create mode 100644 lib/banzai/filter/milestone_reference_filter.rb create mode 100644 spec/lib/banzai/filter/milestone_reference_filter_spec.rb diff --git a/app/models/milestone.rb b/app/models/milestone.rb index d8c7536cd31..e47b6440746 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -22,6 +22,7 @@ class Milestone < ActiveRecord::Base include InternalId include Sortable + include Referable include StripAttribute belongs_to :project @@ -61,6 +62,23 @@ class Milestone < ActiveRecord::Base end end + def self.reference_pattern + nil + end + + def self.link_reference_pattern + super("milestones", /(?\d+)/) + end + + def to_reference(from_project = nil) + h = Gitlab::Application.routes.url_helpers + h.namespace_project_milestone_url(self.project.namespace, self.project, self) + end + + def reference_link_text(from_project = nil) + %Q{ }.html_safe + self.title + end + def expired? if due_date due_date.past? diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 6b200dc2017..36d8c12e2b3 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -60,27 +60,31 @@ module Banzai end def call - # `#123` - replace_text_nodes_matching(object_class.reference_pattern) do |content| - object_link_filter(content, object_class.reference_pattern) + if object_class.reference_pattern + # `#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 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 + if object_class.link_reference_pattern + # `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 - # `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) + # `[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 end diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb new file mode 100644 index 00000000000..f99202af5e8 --- /dev/null +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -0,0 +1,24 @@ +require 'banzai' + +module Banzai + module Filter + # HTML filter that replaces milestone references with links. + # + # This filter supports cross-project references. + class MilestoneReferenceFilter < AbstractReferenceFilter + def self.object_class + Milestone + end + + def find_object(project, id) + project.milestones.find_by(iid: id) + end + + def url_for_object(issue, project) + h = Gitlab::Application.routes.url_helpers + h.namespace_project_milestone_url(project.namespace, project, milestone, + only_path: context[:only_path]) + end + end + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 38750b55ec7..838155e8831 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -22,6 +22,7 @@ module Banzai Filter::CommitRangeReferenceFilter, Filter::CommitReferenceFilter, Filter::LabelReferenceFilter, + Filter::MilestoneReferenceFilter, Filter::TaskListFilter ] diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 0a70d21b1ce..c87068051ab 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -18,7 +18,7 @@ module Gitlab super(text, context.merge(project: project)) end - %i(user label merge_request snippet commit commit_range).each do |type| + %i(user label milestone merge_request snippet commit commit_range).each do |type| define_method("#{type}s") do @references[type] ||= references(type, project: project, current_user: current_user) end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index fdd8cf07b12..e836d81c40b 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -212,6 +212,7 @@ describe 'GitLab Markdown', feature: true do expect(doc).to reference_commit_ranges expect(doc).to reference_commits expect(doc).to reference_labels + expect(doc).to reference_milestones end end diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index e8dfc5c0eb1..302b750aee5 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -214,6 +214,14 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link) - Link to label by reference: [Label](<%= label.to_reference %>) +#### MilestoneReferenceFilter + +- Milestone: <%= milestone.to_reference %> +- Milestone in another project: <%= xmilestone.to_reference(project) %> +- Ignored in code: `<%= milestone.to_reference %>` +- Ignored in links: [Link to <%= milestone.to_reference %>](#milestone-link) +- Link to milestone by URL: [Milestone](<%= urls.namespace_project_milestone_url(milestone.project.namespace, milestone.project, milestone) %>) + ### Task Lists - [ ] Incomplete task 1 diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb new file mode 100644 index 00000000000..c53e780d354 --- /dev/null +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe Banzai::Filter::MilestoneReferenceFilter, lib: true do + include FilterSpecHelper + + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, 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}>milestone #{milestone.to_reference}" + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { milestone.to_reference } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(project.namespace, project, milestone) + end + + it 'links with adjacent text' do + doc = reference_filter("milestone (#{reference}.)") + expect(doc.to_html).to match(/\(<\/i> #{Regexp.escape(milestone.title)}<\/a>\.\)/) + end + + it 'includes a title attribute' do + doc = reference_filter("milestone #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Milestone: #{milestone.title}" + end + + it 'escapes the title attribute' do + milestone.update_attribute(:title, %{">whatever Date: Thu, 24 Dec 2015 14:43:26 +0100 Subject: [PATCH 011/280] Link to milestone in "Milestone changed" system note --- app/services/system_note_service.rb | 4 ++-- app/views/shared/issuable/_sidebar.html.haml | 3 +-- spec/services/system_note_service_spec.rb | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 98a71cbf1ad..955a28b5a7e 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -41,7 +41,7 @@ class SystemNoteService # # Returns the created Note object def self.change_assignee(noteable, project, author, assignee) - body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}" + body = assignee.nil? ? 'Assignee removed' : "Reassigned to #{assignee.to_reference}" create_note(noteable: noteable, project: project, author: author, note: body) end @@ -103,7 +103,7 @@ class SystemNoteService # Returns the created Note object def self.change_milestone(noteable, project, author, milestone) body = 'Milestone ' - body += milestone.nil? ? 'removed' : "changed to #{milestone.title}" + body += milestone.nil? ? 'removed' : "changed to #{milestone.to_reference(project)}" create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 79c5cc7f40a..9d65a621e53 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -28,8 +28,7 @@ %span.back-to-milestone = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do %strong - = icon('clock-o') - = issuable.milestone.title + = issuable.milestone.reference_link_text - else .light None .selectbox diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index c9f828ae2f7..d3364a71022 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -171,7 +171,7 @@ describe SystemNoteService, services: true do context 'when milestone added' do it 'sets the note text' do - expect(subject.note).to eq "Milestone changed to #{milestone.title}" + expect(subject.note).to eq "Milestone changed to #{milestone.to_reference}" end end From 88c35095a8dafd722de63422fdfefd3db65532fe Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 24 Dec 2015 15:34:47 +0100 Subject: [PATCH 012/280] Add changelog item --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 0f9ae1e3b52..30cd589fddd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ v 8.4.0 (unreleased) - Fix Error 500 when doing a search in dashboard before visiting any project (Stan Hu) - Implement new UI for group page - Add project permissions to all project API endpoints (Stan Hu) + - Link to milestone in "Milestone changed" system note v 8.3.0 - Add CAS support (tduehr) From 0ca74f7aad0d5b63955c86397fffdd9e670c56f3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 24 Dec 2015 15:40:59 +0100 Subject: [PATCH 013/280] Use `to_reference` in system notes. --- 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 955a28b5a7e..1083bcec054 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -66,7 +66,7 @@ class SystemNoteService def self.change_label(noteable, project, author, added_labels, removed_labels) labels_count = added_labels.count + removed_labels.count - references = ->(label) { "~#{label.id}" } + references = ->(label) { label.to_reference(:id) } added_labels = added_labels.map(&references).join(' ') removed_labels = removed_labels.map(&references).join(' ') From 32543f3bd94b1b66dd949b0fb1f57bff3732eb45 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 24 Dec 2015 21:19:03 +0100 Subject: [PATCH 014/280] More escaping! --- lib/banzai/filter/abstract_reference_filter.rb | 8 ++++---- lib/banzai/filter/reference_filter.rb | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 36d8c12e2b3..b99ccd98624 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -102,7 +102,7 @@ module Banzai project = project_from_ref(project_ref) if project && object = find_object(project, id) - title = escape_once(object_link_title(object)) + title = object_link_title(object) klass = reference_class(object_sym) data = data_attribute( @@ -114,11 +114,11 @@ module Banzai url = matches[:url] if matches.names.include?("url") url ||= url_for_object(object, project) - text = link_text || escape_once(object_link_text(object, matches)) + text = link_text || object_link_text(object, matches) %(#{text}) + title="#{escape_once(title)}" + class="#{klass}">#{escape_once(text)}) else match end diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index c183702516a..a22a7a7afd3 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -44,7 +44,7 @@ module Banzai # Returns a String def data_attribute(attributes = {}) attributes[:reference_filter] = self.class.name.demodulize - attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{value}") }.join(" ") + attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ") end def escape_once(html) From bd21e3d7319dce4600881f7f8677b28f3f55cc5e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 25 Dec 2015 16:41:02 +0100 Subject: [PATCH 015/280] Add Open Graph data for group, project and commit. --- app/helpers/page_layout_helper.rb | 2 ++ app/views/groups/show.html.haml | 2 ++ app/views/layouts/_head.html.haml | 10 +++++----- app/views/projects/commit/show.html.haml | 4 +++- app/views/projects/show.html.haml | 2 ++ 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 791cb9e50bd..b84644d6996 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -53,6 +53,8 @@ module PageLayoutHelper @project.avatar_url || default elsif @user avatar_icon(@user) + elsif @group + @group.avatar_url || default else default end diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index c2c7c581b3e..8179cdfac80 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,3 +1,5 @@ +- page_description @group.description + - unless can?(current_user, :read_group, @group) - @disable_search_panel = true diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 2e0bd2007a3..2e9a34a8807 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -1,13 +1,11 @@ +- site_name = "GitLab" %head{prefix: "og: http://ogp.me/ns#"} %meta{charset: "utf-8"} %meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'} - %meta{name: 'referrer', content: 'origin-when-cross-origin'} - - %meta{name: "description", content: page_description} -# Open Graph - http://ogp.me/ %meta{property: 'og:type', content: "object"} - %meta{property: 'og:site_name', content: "GitLab"} + %meta{property: 'og:site_name', content: site_name} %meta{property: 'og:title', content: page_title} %meta{property: 'og:description', content: page_description} %meta{property: 'og:image', content: page_image} @@ -20,8 +18,9 @@ %meta{property: 'twitter:image', content: page_image} = page_card_meta_tags - - page_title "GitLab" + - page_title site_name %title= page_title + %meta{name: "description", content: page_description} = favicon_link_tag 'favicon.ico' @@ -34,6 +33,7 @@ = include_gon + %meta{name: 'referrer', content: 'origin-when-cross-origin'} %meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'} %meta{name: 'theme-color', content: '#474D57'} diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 069b8b1f169..58aa45e8d2c 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,4 +1,6 @@ -- page_title "#{@commit.title} (#{@commit.short_id})", "Commits" +- page_title "#{@commit.title} (#{@commit.short_id})", "Commits" +- page_description @commit.description + = render "projects/commits/header_title" = render "commit_box" - if @ci_commit diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 7466a098e24..74ce005eaa2 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,3 +1,5 @@ +- page_description @project.description + = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity") From a7756a4b51b0127caa19d1fb20953cb27c4c62a8 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 27 Dec 2015 19:49:48 -0500 Subject: [PATCH 016/280] Add specs for page_image using a Group's avatar --- spec/helpers/page_layout_helper_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index fd7107779f6..60c4eb21814 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -96,6 +96,22 @@ describe PageLayoutHelper do helper.page_image end end + + context 'with @group' do + it 'uses Group avatar if available' do + group = double(avatar_url: 'http://example.com/uploads/avatar.png') + helper.instance_variable_set(:@group, group) + + expect(helper.page_image).to eq group.avatar_url + end + + it 'falls back to the default' do + group = double(avatar_url: nil) + helper.instance_variable_set(:@group, group) + + expect(helper.page_image).to end_with 'assets/gitlab_logo.png' + end + end end describe 'page_card_attributes' do From dcca64a5230bbfd53ef5db8403d132deac4667f2 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 27 Dec 2015 19:58:44 -0500 Subject: [PATCH 017/280] Use `assign` instead of `instance_variable_set` --- spec/helpers/page_layout_helper_spec.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index 60c4eb21814..300dccf50ec 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -45,14 +45,14 @@ describe PageLayoutHelper do describe 'page_description_default' do it 'uses Project description when available' do project = double(description: 'Project Description') - helper.instance_variable_set(:@project, project) + assign(:project, project) expect(helper.page_description_default).to eq 'Project Description' end it 'uses brand_title when Project description is nil' do project = double(description: nil) - helper.instance_variable_set(:@project, project) + assign(:project, project) expect(helper).to receive(:brand_title).and_return('Brand Title') expect(helper.page_description_default).to eq 'Brand Title' @@ -73,14 +73,14 @@ describe PageLayoutHelper do context 'with @project' do it 'uses Project avatar if available' do project = double(avatar_url: 'http://example.com/uploads/avatar.png') - helper.instance_variable_set(:@project, project) + assign(:project, project) expect(helper.page_image).to eq project.avatar_url end it 'falls back to the default' do project = double(avatar_url: nil) - helper.instance_variable_set(:@project, project) + assign(:project, project) expect(helper.page_image).to end_with 'assets/gitlab_logo.png' end @@ -89,7 +89,7 @@ describe PageLayoutHelper do context 'with @user' do it 'delegates to avatar_icon helper' do user = double('User') - helper.instance_variable_set(:@user, user) + assign(:user, user) expect(helper).to receive(:avatar_icon).with(user) @@ -100,14 +100,14 @@ describe PageLayoutHelper do context 'with @group' do it 'uses Group avatar if available' do group = double(avatar_url: 'http://example.com/uploads/avatar.png') - helper.instance_variable_set(:@group, group) + assign(:group, group) expect(helper.page_image).to eq group.avatar_url end it 'falls back to the default' do group = double(avatar_url: nil) - helper.instance_variable_set(:@group, group) + assign(:group, group) expect(helper.page_image).to end_with 'assets/gitlab_logo.png' end From 141b8b67ff4cbe67778ff6815a51f49834e290b9 Mon Sep 17 00:00:00 2001 From: Michi302 Date: Mon, 28 Dec 2015 15:50:44 +0100 Subject: [PATCH 018/280] Make single user API endpoint return Entities::User instead of Entities::UserBasic --- doc/api/users.md | 7 +++++++ lib/api/users.rb | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/api/users.md b/doc/api/users.md index 66d2fd52526..773fe36d277 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -123,6 +123,13 @@ Parameters: "name": "John Smith", "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", + "created_at": "2012-05-23T08:00:58Z", + "is_admin": false, + "bio": null, + "skype": "", + "linkedin": "", + "twitter": "", + "website_url": "" } ``` diff --git a/lib/api/users.rb b/lib/api/users.rb index 3400f0713ef..0d7813428e2 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -39,7 +39,7 @@ module API if current_user.is_admin? present @user, with: Entities::UserFull else - present @user, with: Entities::UserBasic + present @user, with: Entities::User end end From eb162c2b2741bfd1ea8dd1b49e12027f7ed03594 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 23 Dec 2015 22:01:38 -0500 Subject: [PATCH 019/280] adds settings menu to the top for editting and leaving projects --- app/assets/stylesheets/pages/projects.scss | 7 +++++++ app/views/projects/_home_panel.html.haml | 21 +++++++++++++++++---- app/views/projects/show.html.haml | 6 +----- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index cff3edb7ed2..be6ef43e49c 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -26,6 +26,13 @@ } .project-home-panel { + + .cover-controls { + .project-settings-dropdown { + margin-left: 10px; + } + } + .project-identicon-holder { margin-bottom: 16px; diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index e92115b9b98..c3f710fa3fb 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -18,13 +18,26 @@ = 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') + - access = user_max_access_in_project(current_user.id, @project) + - can_edit = can?(current_user, :admin_project, @project) + - if access || can_edit + %span.dropdown.project-settings-dropdown + %a.dropdown-new.btn.btn-gray.notifications-btn#notifications-button{href: '#', 'data-toggle' => 'dropdown'} + = icon('cog') + = icon('angle-down') + %ul.dropdown-menu.dropdown-menu-right + - if can_edit + %li + = link_to edit_project_path(@project) do + Edit Project + - if access + %li + = link_to leave_namespace_project_project_members_path(@project.namespace, @project), + data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do + Leave project .project-repo-buttons .split-one.count-buttons diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 7466a098e24..5d04776f7f4 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -75,8 +75,4 @@ - if access .prepend-top-20.project-footer .gray-content-block.footer-block.center - You have #{access} access to this project. - - if @project.project_member_by_id(current_user) - = link_to leave_namespace_project_project_members_path(@project.namespace, @project), - data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project', class: 'cred' do - Leave this project + You have #{access} access to this project. \ No newline at end of file From 8ee6abaaf472d7d4562d3c718ad1a68ceb71631f Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 28 Dec 2015 10:36:35 -0500 Subject: [PATCH 020/280] fixes tests failing --- app/views/projects/_home_panel.html.haml | 4 ++-- spec/features/projects_spec.rb | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index c3f710fa3fb..0f61e623396 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -25,7 +25,7 @@ - can_edit = can?(current_user, :admin_project, @project) - if access || can_edit %span.dropdown.project-settings-dropdown - %a.dropdown-new.btn.btn-gray.notifications-btn#notifications-button{href: '#', 'data-toggle' => 'dropdown'} + %a.dropdown-new.btn.btn-gray#project-settings-button{href: '#', 'data-toggle' => 'dropdown'} = icon('cog') = icon('angle-down') %ul.dropdown-menu.dropdown-menu-right @@ -37,7 +37,7 @@ %li = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do - Leave project + Leave Project .project-repo-buttons .split-one.count-buttons diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 74b148f5d17..d4a92072a94 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -81,7 +81,10 @@ feature 'Project', feature: true do end it { expect(page).to have_content('You have Master access to this project.') } - it { expect(page).to have_link('Leave this project') } + it { + find('#project-settings').click + expect(page).to have_link('Leave Project') + } end def remove_with_confirm(button_text, confirm_with) From d985d08a19d945ad4615b319c9346ec88abf2f97 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 28 Dec 2015 17:37:03 -0500 Subject: [PATCH 021/280] adds proper `it` for multi line. adds `find` with correct id for button. --- spec/features/projects_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index d4a92072a94..4836b3b9b14 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -81,10 +81,10 @@ feature 'Project', feature: true do end it { expect(page).to have_content('You have Master access to this project.') } - it { - find('#project-settings').click + it 'click project-settings and find leave project' do + find('#project-settings-button').click expect(page).to have_link('Leave Project') - } + end end def remove_with_confirm(button_text, confirm_with) From dddb54f0b47f35ff6e42c1823c0f1385bb6ae30a Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Tue, 29 Dec 2015 12:58:32 +0100 Subject: [PATCH 022/280] Fix link, filename and crosslink to omnibus docs. --- doc/README.md | 2 +- .../{enviroment_variables.md => environment_variables.md} | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) rename doc/administration/{enviroment_variables.md => environment_variables.md} (79%) diff --git a/doc/README.md b/doc/README.md index d82ff8b908b..4162cde6dc1 100644 --- a/doc/README.md +++ b/doc/README.md @@ -56,7 +56,7 @@ - [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. - [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. - [Log system](logs/logs.md) Log system. -- [Environmental Variables](administration/environmental_variables.md) to configure GitLab. +- [Environment Variables](administration/environment_variables.md) to configure GitLab. - [Operations](operations/README.md) Keeping GitLab up and running - [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects. - [Security](security/README.md) Learn what you can do to further secure your GitLab instance. diff --git a/doc/administration/enviroment_variables.md b/doc/administration/environment_variables.md similarity index 79% rename from doc/administration/enviroment_variables.md rename to doc/administration/environment_variables.md index d7f5cb7c21f..5043e69db8d 100644 --- a/doc/administration/enviroment_variables.md +++ b/doc/administration/environment_variables.md @@ -39,7 +39,12 @@ GITLAB_DATABASE_PASSWORD | GITLAB_DATABASE_HOST | localhost GITLAB_DATABASE_PORT | 5432 -## Other variables +## Adding more variables We welcome merge requests to make more settings configurable via variables. Please stick to the naming scheme "GITLAB_#{name 1_settings.rb in upper case}". + +## Omnibus configuration + +It's possible to preconfigure the GitLab image by adding the environment variable: `GITLAB_OMNIBUS_CONFIG` to docker run command. +For more information see the ['preconfigure-docker-container' section in the Omnibus documentation](http://doc.gitlab.com/omnibus/docker/#preconfigure-docker-container). From 8e74460534cdb0081ca1ac3e52892a4ebad05fe3 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 29 Dec 2015 12:53:21 -0500 Subject: [PATCH 023/280] adds ajax to bottom discussion buttons. Now submits issue and closes via ajax. --- app/assets/javascripts/issue.js.coffee | 12 +++++++++++- app/assets/javascripts/notes.js.coffee | 16 ++-------------- app/views/projects/issues/_discussion.html.haml | 6 ++---- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index c256ec8f41b..0d26c58a81d 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -16,12 +16,16 @@ class @Issue $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList initIssueBtnEventListeners: -> + _this = @ issueFailMessage = 'Unable to update this issue at this time.' $('a.btn-close, a.btn-reopen').on 'click', (e) -> e.preventDefault() e.stopImmediatePropagation() $this = $(this) isClose = $this.hasClass('btn-close') + shouldSubmit = $this.hasClass('btn-comment') + if shouldSubmit + _this.submitNoteForm($this.closest('form')) $this.prop('disabled', true) url = $this.attr('href') $.ajax @@ -32,12 +36,13 @@ class @Issue new Flash(issueFailMessage, 'alert') success: (data, textStatus, jqXHR) -> if data.saved - $this.addClass('hidden') if isClose + $('a.btn-close').addClass('hidden') $('a.btn-reopen').removeClass('hidden') $('div.status-box-closed').removeClass('hidden') $('div.status-box-open').addClass('hidden') else + $('a.btn-reopen').addClass('hidden') $('a.btn-close').removeClass('hidden') $('div.status-box-closed').addClass('hidden') $('div.status-box-open').removeClass('hidden') @@ -45,6 +50,11 @@ class @Issue new Flash(issueFailMessage, 'alert') $this.prop('disabled', false) + submitNoteForm: (form) => + noteText = form.find("textarea.js-note-text").val() + if noteText.trim().length > 0 + form.submit() + disableTaskList: -> $('.detail-page-description .js-task-list-container').taskList('disable') $(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container' diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 9e5204bfeeb..ba0de896201 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -19,6 +19,8 @@ class @Notes @cleanBinding() @addBinding() @initTaskList() + # for updating the comment disscussion buttons once that issue #5534 is approved. + # @updateTargetButtons({target:$("#note_note")}) addBinding: -> # add note to UI after creation @@ -33,8 +35,6 @@ class @Notes $(document).on "click", ".note-edit-cancel", @cancelEdit # Reopen and close actions for Issue/MR combined with note form submit - $(document).on "click", ".js-note-target-reopen", @targetReopen - $(document).on "click", ".js-note-target-close", @targetClose $(document).on "click", ".js-comment-button", @updateCloseButton $(document).on "keyup", ".js-note-text", @updateTargetButtons @@ -512,17 +512,6 @@ class @Notes visibilityChange: => @refresh() - targetReopen: (e) => - @submitNoteForm($(e.target).parents('form')) - - targetClose: (e) => - @submitNoteForm($(e.target).parents('form')) - - submitNoteForm: (form) => - noteText = form.find(".js-note-text").val() - if noteText.trim().length > 0 - form.submit() - updateCloseButton: (e) => textarea = $(e.target) form = textarea.parents('form') @@ -531,7 +520,6 @@ class @Notes updateTargetButtons: (e) => textarea = $(e.target) form = textarea.parents('form') - if textarea.val().trim().length > 0 form.find('.js-note-target-reopen').text('Comment & reopen') form.find('.js-note-target-close').text('Comment & close') diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index dc434cf38c4..673020a4e30 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -1,9 +1,7 @@ - content_for :note_actions do - if can?(current_user, :update_issue, @issue) - - if @issue.closed? - = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue' - - else - = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-close js-note-target-close', title: 'Close Issue' + = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue' + = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue' #notes = render 'projects/notes/notes_with_form' From 2cd2c54bd1c83f8545b5f903d7717c4848453f0c Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Tue, 29 Dec 2015 15:37:48 -0400 Subject: [PATCH 024/280] Update Asana field descriptions --- app/models/project_services/asana_service.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index bbc508e8f8e..a16adf10432 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -53,14 +53,12 @@ http://developer.asana.com/documentation/#api_keys' { type: 'text', name: 'api_key', - placeholder: 'User API token. User must have access to task, -all comments will be attributed to this user.' + placeholder: 'User Personal Access Token. User must have access to task, all comments will be attributed to this user.' }, { type: 'text', name: 'restrict_to_branch', - placeholder: 'Comma-separated list of branches which will be -automatically inspected. Leave blank to include all branches.' + placeholder: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' } ] end From 12ec1e3c407a4b72f670483a558a9cb95449c838 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Tue, 29 Dec 2015 15:39:58 -0400 Subject: [PATCH 025/280] Update Asana service to work with Personal Access Token, lessen number of requests to Asana API --- app/models/project_services/asana_service.rb | 58 ++++++++------------ 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index a16adf10432..111a60431b1 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -67,35 +67,34 @@ http://developer.asana.com/documentation/#api_keys' %w(push) end + def client + @_client ||= begin + Asana::Client.new do |c| + c.authentication :access_token, api_key + end + end + end + def execute(data) return unless supported_events.include?(data[:object_kind]) - Asana.configure do |client| - client.api_key = api_key - end - - user = data[:user_name] - branch = Gitlab::Git.ref_name(data[:ref]) - - branch_restriction = restrict_to_branch.to_s - # check the branch restriction is poplulated and branch is not included + branch = Gitlab::Git.ref_name(data[:ref]) + branch_restriction = restrict_to_branch.to_s if branch_restriction.length > 0 && branch_restriction.index(branch).nil? return end + user = data[:user_name] project_name = project.name_with_namespace - push_msg = user + ' pushed to branch ' + branch + ' of ' + project_name + push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} )" data[:commits].each do |commit| - check_commit(' ( ' + commit[:url] + ' ): ' + commit[:message], push_msg) + check_commit(commit[:message], push_msg) end end def check_commit(message, push_msg) - task_list = [] - close_list = [] - # matches either: # - #1234 # - https://app.asana.com/0/0/1234 @@ -109,28 +108,19 @@ http://developer.asana.com/documentation/#api_keys' # tuple will be # [ 'fix', 'id_from_url', 'id_from_pound' ] taskid = tuple[2] || tuple[1] - task_list.push(taskid) + + begin + task = Asana::Task.find_by_id(client, taskid) + rescue Exception => e + puts e.message + puts e.backtrace.inspect + next + end + + task.add_comment(text: "#{push_msg} #{message}") if tuple[0] - close_list.push(taskid) - end - end - - # post commit to every taskid found - task_list.each do |taskid| - task = Asana::Task.find(taskid) - - if task - task.create_story(text: push_msg + ' ' + message) - end - end - - # close all tasks that had 'fix(ed/es/ing) #:id' in them - close_list.each do |taskid| - task = Asana::Task.find(taskid) - - if task - task.modify(completed: true) + task.update(completed: true) end end end From 4c1f1c2c91a1a43f0ee38fc0f5e8c92dea996f04 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Tue, 29 Dec 2015 15:40:50 -0400 Subject: [PATCH 026/280] Update Asana specs --- .../project_services/asana_service_spec.rb | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb index e368b03206e..306d18171be 100644 --- a/spec/models/project_services/asana_service_spec.rb +++ b/spec/models/project_services/asana_service_spec.rb @@ -52,30 +52,54 @@ describe AsanaService, models: true do end it 'should call Asana service to created a story' do - expect(Asana::Task).to receive(:find).with('123456').once + d1 = double('Asana::Task', add_comment: true) + expect(d1).to receive(:add_comment) + expect(Asana::Task).to receive(:find_by_id).with(anything, '123456').once.and_return(d1) @asana.check_commit('related to #123456', 'pushed') end it 'should call Asana service to created a story and close a task' do - expect(Asana::Task).to receive(:find).with('456789').twice + d1 = double('Asana::Task', add_comment: true) + expect(d1).to receive(:add_comment) + expect(d1).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1) @asana.check_commit('fix #456789', 'pushed') end it 'should be able to close via url' do - expect(Asana::Task).to receive(:find).with('42').twice + d1 = double('Asana::Task', add_comment: true) + expect(d1).to receive(:add_comment) + expect(d1).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1) @asana.check_commit('closes https://app.asana.com/19292/956299/42', 'pushed') end it 'should allow multiple matches per line' do - expect(Asana::Task).to receive(:find).with('123').twice - expect(Asana::Task).to receive(:find).with('456').twice - expect(Asana::Task).to receive(:find).with('789').once + d1 = double('Asana::Task', add_comment: true) + expect(d1).to receive(:add_comment) + expect(d1).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1) - expect(Asana::Task).to receive(:find).with('42').once - expect(Asana::Task).to receive(:find).with('12').twice + d2 = double('Asana::Task', add_comment: true) + expect(d2).to receive(:add_comment) + expect(d2).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2) + + d3 = double('Asana::Task', add_comment: true) + expect(d3).to receive(:add_comment) + expect(Asana::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3) + + d4 = double('Asana::Task', add_comment: true) + expect(d4).to receive(:add_comment) + expect(Asana::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4) + + d5 = double('Asana::Task', add_comment: true) + expect(d5).to receive(:add_comment) + expect(d5).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5) message = <<-EOF minor bigfix, refactoring, fixed #123 and Closes #456 work on #789 From e8080fc8fb216fb6da7d3f056e8b4417f807d09e Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Tue, 29 Dec 2015 16:00:36 -0400 Subject: [PATCH 027/280] Fix error in Asana service --- app/models/project_services/asana_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 111a60431b1..80c56b9097b 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -87,9 +87,9 @@ http://developer.asana.com/documentation/#api_keys' user = data[:user_name] project_name = project.name_with_namespace - push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} )" data[:commits].each do |commit| + push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} )" check_commit(commit[:message], push_msg) end end From 8662f4737b8d5eeebbe8e5e527f6e2148fea2ddf Mon Sep 17 00:00:00 2001 From: Nathan Lowe Date: Tue, 29 Dec 2015 20:58:18 -0500 Subject: [PATCH 028/280] docs: raketasks: Add documentation on uploading to mounted shares --- doc/raketasks/backup_restore.md | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 093450a6de3..cdd6652b7b0 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -153,6 +153,49 @@ with the name of your bucket: } ``` +### Uploading to locally mounted shares + +You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by +using the [`Local`](https://github.com/fog/fog-local#usage) storage provider. +The directory pointed to by the `local_root` key **must** be owned by the `git` +user **when mounted** (mounting with the `uid=` of the `git` user for `CIFS` and +`SMB`) or the user that you are executing the backup tasks under (for omnibus +packages, this is the `git` user). + +The `backup_upload_remote_directory` **must** be set in addition to the +`local_root` key. This is the sub directory inside the mounted directory that +backups will be copied to, and will be created if it does not exist. If the +directory that you want to copy the tarballs to is the root of your mounted +directory, just use `.` instead. + +For omnibus packages: + +```ruby +gitlab_rails['backup_upload_connection'] = { + :provider => 'Local', + :local_root => '/mnt/backups' +} + +# The directory inside the mounted folder to copy backups to +# Use '.' to store them in the root directory +gitlab_rails['backup_upload_remote_directory'] = 'gitlab_backups' +``` + +For installations from source: + +```yaml + backup: + # snip + upload: + # Fog storage connection settings, see http://fog.io/storage/ . + connection: + provider: Local + local_root: '/mnt/backups' + # The directory inside the mounted folder to copy backups to + # Use '.' to store them in the root directory + remote_directory: 'gitlab_backups' +``` + ## Backup archive permissions The backup archives created by GitLab (123456_gitlab_backup.tar) will have owner/group git:git and 0600 permissions by default. From 6eb273a11cf7521448f4bcb5f9376df24c2f9f3f Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Wed, 30 Dec 2015 00:50:44 -0400 Subject: [PATCH 029/280] Restore colon in Asana comment --- app/models/project_services/asana_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 80c56b9097b..183ce2df787 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -89,7 +89,7 @@ http://developer.asana.com/documentation/#api_keys' project_name = project.name_with_namespace data[:commits].each do |commit| - push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} )" + push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} ):" check_commit(commit[:message], push_msg) end end From 7b98d0e1a24645df802ad65911c290ad057d1422 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Wed, 30 Dec 2015 00:51:05 -0400 Subject: [PATCH 030/280] Update Asana service documentation --- app/models/project_services/asana_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 183ce2df787..ab5772356f1 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -40,8 +40,8 @@ get the commit comment added to it. You can also close a task with a message containing: `fix #123456`. -You can find your Api Keys here: -http://developer.asana.com/documentation/#api_keys' +You can create a Personal Access Token here: +http://app.asana.com/-/account_api' end def to_param From 9e7a88f089323964088945829523b798ea6b78b5 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Wed, 30 Dec 2015 00:52:56 -0400 Subject: [PATCH 031/280] Better handling of errors in Asana service [ci skip] --- app/models/project_services/asana_service.rb | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index ab5772356f1..cb4f6ddb3a5 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -111,17 +111,16 @@ http://app.asana.com/-/account_api' begin task = Asana::Task.find_by_id(client, taskid) - rescue Exception => e - puts e.message - puts e.backtrace.inspect + task.add_comment(text: "#{push_msg} #{message}") + + if tuple[0] + task.update(completed: true) + end + rescue => e + Rails.logger.error(e.message) + Rails.logger.error(e.backtrace.join("\n")) next end - - task.add_comment(text: "#{push_msg} #{message}") - - if tuple[0] - task.update(completed: true) - end end end end From 5a997a02a2cd71877d2af7b72fb67e86722882b2 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 30 Dec 2015 06:29:56 -0500 Subject: [PATCH 032/280] removes commented out code. --- app/assets/javascripts/notes.js.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index ba0de896201..8ba00ecbbab 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -19,8 +19,6 @@ class @Notes @cleanBinding() @addBinding() @initTaskList() - # for updating the comment disscussion buttons once that issue #5534 is approved. - # @updateTargetButtons({target:$("#note_note")}) addBinding: -> # add note to UI after creation From d9d2e8a3e8de62beff685457e4e305f93122b206 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 29 Dec 2015 21:58:16 -0500 Subject: [PATCH 033/280] Support a single directory traversal in RelativeLinkFilter Prior, if we were viewing a blob at `https://example.com/namespace/project/blob/master/doc/some-file.md` and it contained a relative link such as `[README](../README.md)`, the resulting link when viewing the blob would be: `https://example.com/namespace/project/blob/README.md` which omits the `master` ref, resulting in a 404. --- lib/banzai/filter/relative_link_filter.rb | 2 +- spec/lib/banzai/filter/relative_link_filter_spec.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 5a081125f21..66f166939e4 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -91,7 +91,7 @@ module Banzai parts = request_path.split('/') parts.pop if path_type(request_path) != 'tree' - while parts.length > 1 && path.start_with?('../') + while path.start_with?('../') parts.pop path.sub!('../', '') end diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index 0b3e5ecbc9f..0e6685f0ffb 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -92,6 +92,14 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" end + it 'rebuilds relative URL for a file in the repository root' do + relative_link = link('../README.md') + doc = filter(relative_link, requested_path: 'doc/some-file.md') + + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/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']). From c479dbae225728234848467c5451f48f27462a05 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 29 Dec 2015 20:42:26 -0500 Subject: [PATCH 034/280] Add js-requires-input and js-quick-submit to abuse report form --- app/views/abuse_reports/new.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml index cffd7684008..3e5cdd2ce4a 100644 --- a/app/views/abuse_reports/new.html.haml +++ b/app/views/abuse_reports/new.html.haml @@ -2,7 +2,7 @@ %h3.page-title Report abuse %p Please use this form to report users who create spam issues, comments or behave inappropriately. %hr -= form_for @abuse_report, html: { class: 'form-horizontal'} do |f| += form_for @abuse_report, html: { class: 'form-horizontal js-requires-input'} do |f| = f.hidden_field :user_id - if @abuse_report.errors.any? .alert.alert-danger @@ -16,7 +16,7 @@ .form-group = f.label :message, class: 'control-label' .col-sm-10 - = f.text_area :message, class: "form-control", rows: 2, required: true + = f.text_area :message, class: "form-control js-quick-submit", rows: 2, required: true .help-block Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment. From 3077cb52d904154b98ee3e9aced5b3aadae86941 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 30 Dec 2015 17:16:02 +0100 Subject: [PATCH 035/280] Use XPath for searching link nodes This is a tad faster than letting Nokogiri figure out whether it should evaluate the query as CSS or XPath and then actually evaluating it. --- lib/banzai/filter/reference_filter.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 8ca05ace88c..7198a8b03e2 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -124,7 +124,7 @@ module Banzai def replace_link_nodes_with_text(pattern) return doc if project.nil? - doc.search('a').each do |node| + doc.xpath('descendant-or-self::a').each do |node| klass = node.attr('class') next if klass && klass.include?('gfm') @@ -162,7 +162,7 @@ module Banzai def replace_link_nodes_with_href(pattern) return doc if project.nil? - doc.search('a').each do |node| + doc.xpath('descendant-or-self::a').each do |node| klass = node.attr('class') next if klass && klass.include?('gfm') From d3951dfaa13b9aaf11695ef10fa63456ac75cc48 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 30 Dec 2015 17:16:59 +0100 Subject: [PATCH 036/280] Don't use delegate to delegate trivial methods Around 300 ms (in total) would be spent in these delegated methods due to the extra stuff ActiveSupport adds to the compiled methods. Because these delegations are so simple we can just manually define the methods, saving around 275 milliseconds. --- lib/banzai/filter/abstract_reference_filter.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 63ad8910c0f..230387c8383 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -47,7 +47,17 @@ module Banzai { object_sym => LazyReference.new(object_class, node.attr(data_reference)) } end - delegate :object_class, :object_sym, :references_in, to: :class + def object_class + self.class.object_class + end + + def object_sym + self.class.object_sym + end + + def references_in(*args, &block) + self.class.references_in(*args, &block) + end def find_object(project, id) # Implement in child class From 054df415f94abe1e517a729e53cdb325d592d31b Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 30 Dec 2015 18:16:53 +0100 Subject: [PATCH 037/280] Optimize CSS expressions produced by Nokogiri Nokogiri produces inefficient XPath expressions when given CSS expressions such as "a.gfm". Luckily these expressions can be optimized quite easily while still achieving the same results. In the two cases where this optimization is applied the run time has been reduced from around 170 ms to around 15 ms. --- lib/banzai/filter/redactor_filter.rb | 2 +- lib/banzai/filter/reference_gatherer_filter.rb | 2 +- lib/banzai/querying.rb | 18 ++++++++++++++++++ spec/lib/banzai/querying_spec.rb | 13 +++++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 lib/banzai/querying.rb create mode 100644 spec/lib/banzai/querying_spec.rb diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb index f01a32b5ae5..66f77902319 100644 --- a/lib/banzai/filter/redactor_filter.rb +++ b/lib/banzai/filter/redactor_filter.rb @@ -10,7 +10,7 @@ module Banzai # class RedactorFilter < HTML::Pipeline::Filter def call - doc.css('a.gfm').each do |node| + Querying.css(doc, 'a.gfm').each do |node| unless user_can_see_reference?(node) # The reference should be replaced by the original text, # which is not always the same as the rendered text. diff --git a/lib/banzai/filter/reference_gatherer_filter.rb b/lib/banzai/filter/reference_gatherer_filter.rb index 12412ff7ea9..bef04112919 100644 --- a/lib/banzai/filter/reference_gatherer_filter.rb +++ b/lib/banzai/filter/reference_gatherer_filter.rb @@ -16,7 +16,7 @@ module Banzai end def call - doc.css('a.gfm').each do |node| + Querying.css(doc, 'a.gfm').each do |node| gather_references(node) end diff --git a/lib/banzai/querying.rb b/lib/banzai/querying.rb new file mode 100644 index 00000000000..1e1b51e683e --- /dev/null +++ b/lib/banzai/querying.rb @@ -0,0 +1,18 @@ +module Banzai + module Querying + # Searches a Nokogiri document using a CSS query, optionally optimizing it + # whenever possible. + # + # document - A document/element to search. + # query - The CSS query to use. + # + # Returns a Nokogiri::XML::NodeSet. + def self.css(document, query) + # When using "a.foo" Nokogiri compiles this to "//a[...]" but + # "descendant::a[...]" is quite a bit faster and achieves the same result. + xpath = Nokogiri::CSS.xpath_for(query)[0].gsub(%r{^//}, 'descendant::') + + document.xpath(xpath) + end + end +end diff --git a/spec/lib/banzai/querying_spec.rb b/spec/lib/banzai/querying_spec.rb new file mode 100644 index 00000000000..27da2a7439c --- /dev/null +++ b/spec/lib/banzai/querying_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe Banzai::Querying do + describe '.css' do + it 'optimizes queries for elements with classes' do + document = double(:document) + + expect(document).to receive(:xpath).with(/^descendant::a/) + + described_class.css(document, 'a.gfm') + end + end +end From a6c60127e3e2966b2f29fa6e6e79503b130c2b02 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 31 Dec 2015 17:14:02 +0100 Subject: [PATCH 038/280] Removed tracking of raw SQL queries This particular setup had 3 problems: 1. Storing SQL queries as tags is very inefficient as InfluxDB ends up indexing every query (and they can get pretty large). Storing these as values instead means we can't always display the SQL as easily. 2. We already instrument ActiveRecord query methods, thus we already have timing information about database queries. 3. SQL obfuscation is difficult to get right and I'd rather not expose sensitive data by accident. --- config/initializers/metrics.rb | 1 - lib/gitlab/metrics/obfuscated_sql.rb | 47 ---------- .../metrics/subscribers/active_record.rb | 48 ---------- .../lib/gitlab/metrics/obfuscated_sql_spec.rb | 93 ------------------- .../metrics/subscribers/active_record_spec.rb | 32 ------- 5 files changed, 221 deletions(-) delete mode 100644 lib/gitlab/metrics/obfuscated_sql.rb delete mode 100644 lib/gitlab/metrics/subscribers/active_record.rb delete mode 100644 spec/lib/gitlab/metrics/obfuscated_sql_spec.rb delete mode 100644 spec/lib/gitlab/metrics/subscribers/active_record_spec.rb diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index 2e4908192a1..94c535dc562 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -7,7 +7,6 @@ if Gitlab::Metrics.enabled? # These are manually require'd so the classes are registered properly with # ActiveSupport. require 'gitlab/metrics/subscribers/action_view' - require 'gitlab/metrics/subscribers/active_record' Gitlab::Application.configure do |config| config.middleware.use(Gitlab::Metrics::RackMiddleware) diff --git a/lib/gitlab/metrics/obfuscated_sql.rb b/lib/gitlab/metrics/obfuscated_sql.rb deleted file mode 100644 index fe97d7a0534..00000000000 --- a/lib/gitlab/metrics/obfuscated_sql.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Gitlab - module Metrics - # Class for producing SQL queries with sensitive data stripped out. - class ObfuscatedSQL - REPLACEMENT = / - \d+(\.\d+)? # integers, floats - | '.+?' # single quoted strings - | \/.+?(? Date: Thu, 31 Dec 2015 17:47:07 +0100 Subject: [PATCH 039/280] Cache InfluxDB settings after the first use This ensures we don't need to load anything from either PostgreSQL or the Rails cache whenever creating new InfluxDB connections. --- lib/gitlab/metrics.rb | 32 ++++++++++++++++++-------------- spec/lib/gitlab/metrics_spec.rb | 12 +++--------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 2d266ccfe9e..c5b98a0115e 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -6,16 +6,21 @@ module Gitlab METRICS_ROOT = Rails.root.join('lib', 'gitlab', 'metrics').to_s PATH_REGEX = /^#{RAILS_ROOT}\/?/ - def self.pool_size - current_application_settings[:metrics_pool_size] || 16 - end - - def self.timeout - current_application_settings[:metrics_timeout] || 10 + def self.settings + @settings ||= { + enabled: current_application_settings[:metrics_enabled], + pool_size: current_application_settings[:metrics_pool_size], + timeout: current_application_settings[:metrics_timeout], + method_call_threshold: current_application_settings[:metrics_method_call_threshold], + host: current_application_settings[:metrics_host], + username: current_application_settings[:metrics_username], + password: current_application_settings[:metrics_password], + port: current_application_settings[:metrics_port] + } end def self.enabled? - current_application_settings[:metrics_enabled] || false + settings[:enabled] || false end def self.mri? @@ -26,8 +31,7 @@ module Gitlab # This is memoized since this method is called for every instrumented # method. Loading data from an external cache on every method call slows # things down too much. - @method_call_threshold ||= - (current_application_settings[:metrics_method_call_threshold] || 10) + @method_call_threshold ||= settings[:method_call_threshold] end def self.pool @@ -90,11 +94,11 @@ module Gitlab # When enabled this should be set before being used as the usual pattern # "@foo ||= bar" is _not_ thread-safe. if enabled? - @pool = ConnectionPool.new(size: pool_size, timeout: timeout) do - host = current_application_settings[:metrics_host] - user = current_application_settings[:metrics_username] - pw = current_application_settings[:metrics_password] - port = current_application_settings[:metrics_port] + @pool = ConnectionPool.new(size: settings[:pool_size], timeout: settings[:timeout]) do + host = settings[:host] + user = settings[:username] + pw = settings[:password] + port = settings[:port] InfluxDB::Client. new(udp: { host: host, port: port }, username: user, password: pw) diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 6c0682cac4d..c2924708f44 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -1,15 +1,9 @@ require 'spec_helper' describe Gitlab::Metrics do - describe '.pool_size' do - it 'returns a Fixnum' do - expect(described_class.pool_size).to be_an_instance_of(Fixnum) - end - end - - describe '.timeout' do - it 'returns a Fixnum' do - expect(described_class.timeout).to be_an_instance_of(Fixnum) + describe '.settings' do + it 'returns a Hash' do + expect(described_class.settings).to be_an_instance_of(Hash) end end From bd9f86bb8abb4759a0c72f94fb0492b1ff8619b5 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 31 Dec 2015 17:51:12 +0100 Subject: [PATCH 040/280] Use separate series for Rails/Sidekiq transactions This removes the need for tagging all metrics with a "process_type" tag. --- lib/gitlab/metrics/metric.rb | 3 +-- lib/gitlab/metrics/rack_middleware.rb | 4 +++- lib/gitlab/metrics/sidekiq_middleware.rb | 4 +++- lib/gitlab/metrics/transaction.rb | 7 +++---- spec/lib/gitlab/metrics/instrumentation_spec.rb | 2 +- spec/lib/gitlab/metrics/metric_spec.rb | 1 - spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb | 2 +- spec/lib/gitlab/metrics/subscribers/action_view_spec.rb | 2 +- spec/lib/gitlab/metrics/transaction_spec.rb | 4 ++-- 9 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb index 753008df99a..8319e628a40 100644 --- a/lib/gitlab/metrics/metric.rb +++ b/lib/gitlab/metrics/metric.rb @@ -19,8 +19,7 @@ module Gitlab { series: @series, tags: @tags.merge( - hostname: Metrics.hostname, - process_type: Sidekiq.server? ? 'sidekiq' : 'rails' + hostname: Metrics.hostname ), values: @values, timestamp: @created_at.to_i * 1_000_000_000 diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb index 5c0587c4c51..bb9e4fcb918 100644 --- a/lib/gitlab/metrics/rack_middleware.rb +++ b/lib/gitlab/metrics/rack_middleware.rb @@ -4,6 +4,8 @@ module Gitlab class RackMiddleware CONTROLLER_KEY = 'action_controller.instance' + SERIES = 'rails_transactions' + def initialize(app) @app = app end @@ -30,7 +32,7 @@ module Gitlab end def transaction_from_env(env) - trans = Transaction.new + trans = Transaction.new(SERIES) trans.add_tag(:request_method, env['REQUEST_METHOD']) trans.add_tag(:request_uri, env['REQUEST_URI']) diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb index ad441decfa2..6e804dd2562 100644 --- a/lib/gitlab/metrics/sidekiq_middleware.rb +++ b/lib/gitlab/metrics/sidekiq_middleware.rb @@ -4,8 +4,10 @@ module Gitlab # # This middleware is intended to be used as a server-side middleware. class SidekiqMiddleware + SERIES = 'sidekiq_transactions' + def call(worker, message, queue) - trans = Transaction.new + trans = Transaction.new(SERIES) begin trans.run { yield } diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb index a61dbd989e7..43a7dab5323 100644 --- a/lib/gitlab/metrics/transaction.rb +++ b/lib/gitlab/metrics/transaction.rb @@ -4,8 +4,6 @@ module Gitlab class Transaction THREAD_KEY = :_gitlab_metrics_transaction - SERIES = 'transactions' - attr_reader :uuid, :tags def self.current @@ -13,7 +11,8 @@ module Gitlab end # name - The name of this transaction as a String. - def initialize + def initialize(series) + @series = series @metrics = [] @uuid = SecureRandom.uuid @@ -55,7 +54,7 @@ module Gitlab end def track_self - add_metric(SERIES, { duration: duration }, @tags) + add_metric(@series, { duration: duration }, @tags) end def submit diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index a7eab9d11cc..a9003d8796b 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Metrics::Instrumentation do - let(:transaction) { Gitlab::Metrics::Transaction.new } + let(:transaction) { Gitlab::Metrics::Transaction.new('rspec') } before do @dummy = Class.new do diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb index aa76315c79c..9b942855140 100644 --- a/spec/lib/gitlab/metrics/metric_spec.rb +++ b/spec/lib/gitlab/metrics/metric_spec.rb @@ -39,7 +39,6 @@ describe Gitlab::Metrics::Metric do expect(hash[:tags]).to be_an_instance_of(Hash) expect(hash[:tags][:hostname]).to be_an_instance_of(String) - expect(hash[:tags][:process_type]).to be_an_instance_of(String) end it 'includes the values' do diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb index 5882e7d81c7..5fda6de52f4 100644 --- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::Metrics::SidekiqMiddleware do describe '#tag_worker' do it 'adds the worker class and action to the transaction' do - trans = Gitlab::Metrics::Transaction.new + trans = Gitlab::Metrics::Transaction.new('rspec') worker = double(:worker, class: double(:class, name: 'TestWorker')) expect(trans).to receive(:add_tag).with(:action, 'TestWorker#perform') diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb index c6cd584663f..bca76ca5a69 100644 --- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Metrics::Subscribers::ActionView do - let(:transaction) { Gitlab::Metrics::Transaction.new } + let(:transaction) { Gitlab::Metrics::Transaction.new('rspec') } let(:subscriber) { described_class.new } diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index 6862fc9e2d1..345163bfbea 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Metrics::Transaction do - let(:transaction) { described_class.new } + let(:transaction) { described_class.new('rspec') } describe '#duration' do it 'returns the duration of a transaction in seconds' do @@ -58,7 +58,7 @@ describe Gitlab::Metrics::Transaction do describe '#track_self' do it 'adds a metric for the transaction itself' do expect(transaction).to receive(:add_metric). - with(described_class::SERIES, { duration: transaction.duration }, {}) + with('rspec', { duration: transaction.duration }, {}) transaction.track_self end From cafc784ee1d5d0a0279077272af8ee435bb110e4 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 31 Dec 2015 17:55:10 +0100 Subject: [PATCH 041/280] Removed tracking of hostnames for metrics This isn't hugely useful and mostly wastes InfluxDB space. We can re-add this whenever needed (but only once we really need it). --- config/initializers/metrics.rb | 1 - lib/gitlab/metrics.rb | 6 ------ lib/gitlab/metrics/metric.rb | 6 ++---- spec/lib/gitlab/metrics/metric_spec.rb | 2 -- spec/lib/gitlab/metrics_spec.rb | 6 ------ 5 files changed, 2 insertions(+), 19 deletions(-) diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index 94c535dc562..ebb20be7283 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -1,6 +1,5 @@ if Gitlab::Metrics.enabled? require 'influxdb' - require 'socket' require 'connection_pool' require 'method_source' diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index c5b98a0115e..ee88ab34d6c 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -38,10 +38,6 @@ module Gitlab @pool end - def self.hostname - @hostname - end - # Returns a relative path and line number based on the last application call # frame. def self.last_relative_application_frame @@ -89,8 +85,6 @@ module Gitlab value.to_s.gsub('=', '\\=') end - @hostname = Socket.gethostname - # When enabled this should be set before being used as the usual pattern # "@foo ||= bar" is _not_ thread-safe. if enabled? diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb index 8319e628a40..7ea9555cc8c 100644 --- a/lib/gitlab/metrics/metric.rb +++ b/lib/gitlab/metrics/metric.rb @@ -17,10 +17,8 @@ module Gitlab # Returns a Hash in a format that can be directly written to InfluxDB. def to_hash { - series: @series, - tags: @tags.merge( - hostname: Metrics.hostname - ), + series: @series, + tags: @tags, values: @values, timestamp: @created_at.to_i * 1_000_000_000 } diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb index 9b942855140..f718d536130 100644 --- a/spec/lib/gitlab/metrics/metric_spec.rb +++ b/spec/lib/gitlab/metrics/metric_spec.rb @@ -37,8 +37,6 @@ describe Gitlab::Metrics::Metric do it 'includes the tags' do expect(hash[:tags]).to be_an_instance_of(Hash) - - expect(hash[:tags][:hostname]).to be_an_instance_of(String) end it 'includes the values' do diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index c2924708f44..c2782f95c8e 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -13,12 +13,6 @@ describe Gitlab::Metrics do end end - describe '.hostname' do - it 'returns a String containing the hostname' do - expect(described_class.hostname).to eq(Socket.gethostname) - end - end - describe '.last_relative_application_frame' do it 'returns an Array containing a file path and line number' do file, line = described_class.last_relative_application_frame From 90029a5caaef1fd9d41a8ac02a7e9840ce3ac7b5 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Thu, 31 Dec 2015 18:27:34 -0400 Subject: [PATCH 042/280] Actually test the posted comment in Asana service --- .../project_services/asana_service_spec.rb | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb index 306d18171be..a7b32ac07a9 100644 --- a/spec/models/project_services/asana_service_spec.rb +++ b/spec/models/project_services/asana_service_spec.rb @@ -40,6 +40,20 @@ describe AsanaService, models: true do let(:user) { create(:user) } let(:project) { create(:project) } + def create_data_for_commits(*messages) + data = { + object_kind: 'push', + ref: 'master', + user_name: user.name, + commits: messages.map do |m| + { + message: m, + url: 'https://gitlab.com/', + } + end + } + end + before do @asana = AsanaService.new allow(@asana).to receive_messages( @@ -51,12 +65,15 @@ describe AsanaService, models: true do ) end - it 'should call Asana service to created a story' do - d1 = double('Asana::Task', add_comment: true) - expect(d1).to receive(:add_comment) + it 'should call Asana service to create a story' do + data = create_data_for_commits('Message from commit. related to #123456') + expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.name_with_namespace} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}" + + d1 = double('Asana::Task') + expect(d1).to receive(:add_comment).with(text: expected_message) expect(Asana::Task).to receive(:find_by_id).with(anything, '123456').once.and_return(d1) - @asana.check_commit('related to #123456', 'pushed') + @asana.execute(data) end it 'should call Asana service to created a story and close a task' do From 571df5f44bfec89b21bdce0f91f9acfdda6d7660 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Thu, 31 Dec 2015 18:29:00 -0400 Subject: [PATCH 043/280] Use `execute` in Asana specs --- .../project_services/asana_service_spec.rb | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb index a7b32ac07a9..0db48c75d1d 100644 --- a/spec/models/project_services/asana_service_spec.rb +++ b/spec/models/project_services/asana_service_spec.rb @@ -76,53 +76,56 @@ describe AsanaService, models: true do @asana.execute(data) end - it 'should call Asana service to created a story and close a task' do - d1 = double('Asana::Task', add_comment: true) + it 'should call Asana service to create a story and close a task' do + data = create_data_for_commits('fix #456789') + d1 = double('Asana::Task') expect(d1).to receive(:add_comment) expect(d1).to receive(:update).with(completed: true) expect(Asana::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1) - @asana.check_commit('fix #456789', 'pushed') + @asana.execute(data) end it 'should be able to close via url' do - d1 = double('Asana::Task', add_comment: true) + data = create_data_for_commits('closes https://app.asana.com/19292/956299/42') + d1 = double('Asana::Task') expect(d1).to receive(:add_comment) expect(d1).to receive(:update).with(completed: true) expect(Asana::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1) - @asana.check_commit('closes https://app.asana.com/19292/956299/42', 'pushed') + @asana.execute(data) end it 'should allow multiple matches per line' do - d1 = double('Asana::Task', add_comment: true) - expect(d1).to receive(:add_comment) - expect(d1).to receive(:update).with(completed: true) - expect(Asana::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1) - - d2 = double('Asana::Task', add_comment: true) - expect(d2).to receive(:add_comment) - expect(d2).to receive(:update).with(completed: true) - expect(Asana::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2) - - d3 = double('Asana::Task', add_comment: true) - expect(d3).to receive(:add_comment) - expect(Asana::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3) - - d4 = double('Asana::Task', add_comment: true) - expect(d4).to receive(:add_comment) - expect(Asana::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4) - - d5 = double('Asana::Task', add_comment: true) - expect(d5).to receive(:add_comment) - expect(d5).to receive(:update).with(completed: true) - expect(Asana::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5) - message = <<-EOF minor bigfix, refactoring, fixed #123 and Closes #456 work on #789 ref https://app.asana.com/19292/956299/42 and closing https://app.asana.com/19292/956299/12 EOF - @asana.check_commit(message, 'pushed') + data = create_data_for_commits(message) + d1 = double('Asana::Task') + expect(d1).to receive(:add_comment) + expect(d1).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1) + + d2 = double('Asana::Task') + expect(d2).to receive(:add_comment) + expect(d2).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2) + + d3 = double('Asana::Task') + expect(d3).to receive(:add_comment) + expect(Asana::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3) + + d4 = double('Asana::Task') + expect(d4).to receive(:add_comment) + expect(Asana::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4) + + d5 = double('Asana::Task') + expect(d5).to receive(:add_comment) + expect(d5).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5) + + @asana.execute(data) end end end From bf249550d03a21d4aca3847b6d32b6d71de3956b Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 30 Dec 2015 22:03:43 -0500 Subject: [PATCH 044/280] number_with_delimiter most of the things --- app/views/admin/builds/index.html.haml | 6 ++--- app/views/admin/dashboard/index.html.haml | 22 +++++++++---------- app/views/admin/groups/index.html.haml | 2 +- app/views/admin/users/index.html.haml | 14 ++++++------ app/views/layouts/nav/_admin.html.haml | 6 ++--- app/views/layouts/nav/_dashboard.html.haml | 4 ++-- app/views/layouts/nav/_group.html.haml | 4 ++-- app/views/layouts/nav/_profile.html.haml | 4 ++-- app/views/layouts/nav/_project.html.haml | 6 ++--- app/views/projects/issues/_issues.html.haml | 2 +- .../merge_requests/_merge_requests.html.haml | 2 +- 11 files changed, 36 insertions(+), 36 deletions(-) diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index 55da06a7fe9..52c36af6225 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -8,17 +8,17 @@ %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) + %span.badge.js-running-count= number_with_delimiter(@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) + %span.badge.js-running-count= number_with_delimiter(@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) + %span.badge.js-totalbuilds-count= number_with_delimiter(@all_builds.count(:id)) .gray-content-block #{(@scope || 'running').capitalize} builds diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 531247e9148..cc389c3ae08 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -6,35 +6,35 @@ %p Forks %span.light.pull-right - = ForkedProjectLink.count + = number_with_delimiter(ForkedProjectLink.count) %p Issues %span.light.pull-right - = Issue.count + = number_with_delimiter(Issue.count) %p Merge Requests %span.light.pull-right - = MergeRequest.count + = number_with_delimiter(MergeRequest.count) %p Notes %span.light.pull-right - = Note.count + = number_with_delimiter(Note.count) %p Snippets %span.light.pull-right - = Snippet.count + = number_with_delimiter(Snippet.count) %p SSH Keys %span.light.pull-right - = Key.count + = number_with_delimiter(Key.count) %p Milestones %span.light.pull-right - = Milestone.count + = number_with_delimiter(Milestone.count) %p Active Users %span.light.pull-right - = User.active.count + = number_with_delimiter(User.active.count) .col-md-4 %h4 Features @@ -99,7 +99,7 @@ %h4 Projects .data = link_to admin_namespaces_projects_path do - %h1= Project.count + %h1= number_with_delimiter(Project.count) %hr = link_to('New Project', new_project_path, class: "btn btn-new") .col-sm-4 @@ -107,7 +107,7 @@ %h4 Users .data = link_to admin_users_path do - %h1= User.count + %h1= number_with_delimiter(User.count) %hr = link_to 'New User', new_admin_user_path, class: "btn btn-new" .col-sm-4 @@ -115,7 +115,7 @@ %h4 Groups .data = link_to admin_groups_path do - %h1= Group.count + %h1= number_with_delimiter(Group.count) %hr = link_to 'New Group', new_admin_group_path, class: "btn btn-new" diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 5ce7cdf2f8d..3940210e19b 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -1,6 +1,6 @@ - page_title "Groups" %h3.page-title - Groups (#{@groups.total_count}) + Groups (#{number_with_delimiter(@groups.total_count)}) = link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right" %p.light diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index bc08458312c..a92c9c152b9 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -8,27 +8,27 @@ %li{class: "#{'active' unless params[:filter]}"} = link_to admin_users_path do Active - %small.pull-right= User.active.count + %small.pull-right= number_with_delimiter(User.active.count) %li{class: "#{'active' if params[:filter] == "admins"}"} = link_to admin_users_path(filter: "admins") do Admins - %small.pull-right= User.admins.count + %small.pull-right= number_with_delimiter(User.admins.count) %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"} = link_to admin_users_path(filter: 'two_factor_enabled') do 2FA Enabled - %small.pull-right= User.with_two_factor.count + %small.pull-right= number_with_delimiter(User.with_two_factor.count) %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"} = link_to admin_users_path(filter: 'two_factor_disabled') do 2FA Disabled - %small.pull-right= User.without_two_factor.count + %small.pull-right= number_with_delimiter(User.without_two_factor.count) %li{class: "#{'active' if params[:filter] == "blocked"}"} = link_to admin_users_path(filter: "blocked") do Blocked - %small.pull-right= User.blocked.count + %small.pull-right= number_with_delimiter(User.blocked.count) %li{class: "#{'active' if params[:filter] == "wop"}"} = link_to admin_users_path(filter: "wop") do Without projects - %small.pull-right= User.without_projects.count + %small.pull-right= number_with_delimiter(User.without_projects.count) %hr = form_tag admin_users_path, method: :get, class: 'form-inline' do .form-group @@ -42,7 +42,7 @@ %section.col-md-9 .panel.panel-default .panel-heading - Users (#{@users.total_count}) + Users (#{number_with_delimiter(@users.total_count)}) .panel-head-actions .dropdown.inline %a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"} diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index c60ac5eefac..cffdb52cc23 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -29,13 +29,13 @@ = icon('cog fw') %span Runners - %span.count= Ci::Runner.count(:all) + %span.count= number_with_delimiter(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) + %span.count= number_with_delimiter(Ci::Build.count(:all)) = nav_link(controller: :logs) do = link_to admin_logs_path, title: 'Logs' do = icon('file-text fw') @@ -80,7 +80,7 @@ = icon('exclamation-circle fw') %span Abuse Reports - %span.count= AbuseReport.count(:all) + %span.count= number_with_delimiter(AbuseReport.count(:all)) = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do = link_to admin_application_settings_path, title: 'Settings' do diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index da698831300..106abd24a56 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -24,13 +24,13 @@ = icon('exclamation-circle fw') %span Issues - %span.count= current_user.assigned_issues.opened.count + %span.count= number_with_delimiter(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' do = icon('tasks fw') %span Merge Requests - %span.count= current_user.assigned_merge_requests.opened.count + %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) = nav_link(controller: :snippets) do = link_to dashboard_snippets_path, title: 'Snippets' do = icon('clipboard fw') diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 68da8d5de2a..e5e2a59eaed 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -25,14 +25,14 @@ %span Issues - if current_user - %span.count= Issue.opened.of_group(@group).count + %span.count= number_with_delimiter(Issue.opened.of_group(@group).count) = nav_link(path: 'groups#merge_requests') 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 + %span.count= number_with_delimiter(MergeRequest.opened.of_group(@group).count) = nav_link(controller: [:group_members]) do = link_to group_group_members_path(@group), title: 'Members' do = icon('users fw') diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 64b30783c05..f3ded04419b 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -27,7 +27,7 @@ = icon('envelope-o fw') %span Emails - %span.count= current_user.emails.count + 1 + %span.count= number_with_delimiter(current_user.emails.count + 1) - unless current_user.ldap_user? = nav_link(controller: :passwords) do = link_to edit_profile_password_path, title: 'Password' do @@ -45,7 +45,7 @@ = icon('key fw') %span SSH Keys - %span.count= current_user.keys.count + %span.count= number_with_delimiter(current_user.keys.count) = nav_link(controller: :preferences) do = link_to profile_preferences_path, title: 'Preferences' do -# TODO (rspeicher): Better icon? diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index c0d62028639..d3eaf0f3209 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.builds.running_or_pending.count(:all) + %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do @@ -67,7 +67,7 @@ %span Issues - if @project.default_issues_tracker? - %span.count.issue_counter= @project.issues.opened.count + %span.count.issue_counter= number_with_delimiter(@project.issues.opened.count) - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do @@ -75,7 +75,7 @@ = icon('tasks fw') %span Merge Requests - %span.count.merge_counter= @project.merge_requests.opened.count + %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) - if project_nav_tab? :settings = nav_link(controller: [:project_members, :teams]) do diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index ca5b1a8386d..e0e89b764d5 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -7,7 +7,7 @@ - if @issues.present? .issuable-filter-count %span.pull-right - = @issues.total_count + = number_with_delimiter(@issues.total_count) issues for this filter = paginate @issues, theme: "gitlab" diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index 0af970e4b92..29d09d0a652 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -7,7 +7,7 @@ - if @merge_requests.present? .issuable-filter-count %span.pull-right - = @merge_requests.total_count + = number_with_delimiter(@merge_requests.total_count) merge requests for this filter = paginate @merge_requests, theme: "gitlab" From f3ea2f29a5b18f176ed0bbfde55e9cc7d429ebe6 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 1 Jan 2016 17:40:10 -0500 Subject: [PATCH 045/280] Remove jquery.history.js plugin We're not concerning ourselves with non-HTML5 browser compatibility, and this removes 21 KB from our compiled JavaScript. Bonus fix: There was an extra space after the query string in the URLs that has now been removed. --- app/assets/javascripts/application.js.coffee | 1 - app/assets/javascripts/issues.js.coffee | 4 ++-- app/assets/javascripts/merge_requests.js.coffee | 4 ++-- vendor/assets/javascripts/jquery.history.js | 1 - 4 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 vendor/assets/javascripts/jquery.history.js diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index affab5bb030..fe76d11ab58 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -10,7 +10,6 @@ #= require jquery.cookie #= require jquery.endless-scroll #= require jquery.highlight -#= require jquery.history #= require jquery.waitforimages #= require jquery.atwho #= require jquery.scrollTo diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index ac9e022e727..35d34b20fae 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -54,7 +54,7 @@ form = $("#issue_search_form") search = $("#issue_search").val() $('.issues-holder').css("opacity", '0.5') - issues_url = form.attr('action') + '? '+ form.serialize() + issues_url = form.attr('action') + '?' + form.serialize() $.ajax type: "GET" @@ -65,7 +65,7 @@ success: (data) -> $('.issues-holder').html(data.html) # Change url so if user reload a page - search results are saved - History.replaceState {page: issues_url}, document.title, issues_url + history.replaceState {page: issues_url}, document.title, issues_url Issues.reload() dataType: "json" diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee index 83434c1b9ba..b3c73ffce5d 100644 --- a/app/assets/javascripts/merge_requests.js.coffee +++ b/app/assets/javascripts/merge_requests.js.coffee @@ -16,7 +16,7 @@ form = $("#issue_search_form") search = $("#issue_search").val() $('.merge-requests-holder').css("opacity", '0.5') - issues_url = form.attr('action') + '? '+ form.serialize() + issues_url = form.attr('action') + '?' + form.serialize() $.ajax type: "GET" @@ -27,7 +27,7 @@ success: (data) -> $('.merge-requests-holder').html(data.html) # Change url so if user reload a page - search results are saved - History.replaceState {page: issues_url}, document.title, issues_url + history.replaceState {page: issues_url}, document.title, issues_url MergeRequests.reload() dataType: "json" diff --git a/vendor/assets/javascripts/jquery.history.js b/vendor/assets/javascripts/jquery.history.js deleted file mode 100644 index 8d4edcd210e..00000000000 --- a/vendor/assets/javascripts/jquery.history.js +++ /dev/null @@ -1 +0,0 @@ -window.JSON||(window.JSON={}),function(){function f(a){return a<10?"0"+a:a}function quote(a){return escapable.lastIndex=0,escapable.test(a)?'"'+a.replace(escapable,function(a){var b=meta[a];return typeof b=="string"?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;c")&&c[0]);return a>4?a:!1}();return a},m.isInternetExplorer=function(){var a=m.isInternetExplorer.cached=typeof m.isInternetExplorer.cached!="undefined"?m.isInternetExplorer.cached:Boolean(m.getInternetExplorerMajorVersion());return a},m.emulated={pushState:!Boolean(a.history&&a.history.pushState&&a.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(e.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(e.userAgent)),hashChange:Boolean(!("onhashchange"in a||"onhashchange"in d)||m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8)},m.enabled=!m.emulated.pushState,m.bugs={setHash:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),safariPoll:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),ieDoubleCheck:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<7)},m.isEmptyObject=function(a){for(var b in a)return!1;return!0},m.cloneObject=function(a){var b,c;return a?(b=k.stringify(a),c=k.parse(b)):c={},c},m.getRootUrl=function(){var a=d.location.protocol+"//"+(d.location.hostname||d.location.host);if(d.location.port||!1)a+=":"+d.location.port;return a+="/",a},m.getBaseHref=function(){var a=d.getElementsByTagName("base"),b=null,c="";return a.length===1&&(b=a[0],c=b.href.replace(/[^\/]+$/,"")),c=c.replace(/\/+$/,""),c&&(c+="/"),c},m.getBaseUrl=function(){var a=m.getBaseHref()||m.getBasePageUrl()||m.getRootUrl();return a},m.getPageUrl=function(){var a=m.getState(!1,!1),b=(a||{}).url||d.location.href,c;return c=b.replace(/\/+$/,"").replace(/[^\/]+$/,function(a,b,c){return/\./.test(a)?a:a+"/"}),c},m.getBasePageUrl=function(){var a=d.location.href.replace(/[#\?].*/,"").replace(/[^\/]+$/,function(a,b,c){return/[^\/]$/.test(a)?"":a}).replace(/\/+$/,"")+"/";return a},m.getFullUrl=function(a,b){var c=a,d=a.substring(0,1);return b=typeof b=="undefined"?!0:b,/[a-z]+\:\/\//.test(a)||(d==="/"?c=m.getRootUrl()+a.replace(/^\/+/,""):d==="#"?c=m.getPageUrl().replace(/#.*/,"")+a:d==="?"?c=m.getPageUrl().replace(/[\?#].*/,"")+a:b?c=m.getBaseUrl()+a.replace(/^(\.\/)+/,""):c=m.getBasePageUrl()+a.replace(/^(\.\/)+/,"")),c.replace(/\#$/,"")},m.getShortUrl=function(a){var b=a,c=m.getBaseUrl(),d=m.getRootUrl();return m.emulated.pushState&&(b=b.replace(c,"")),b=b.replace(d,"/"),m.isTraditionalAnchor(b)&&(b="./"+b),b=b.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),b},m.store={},m.idToState=m.idToState||{},m.stateToId=m.stateToId||{},m.urlToId=m.urlToId||{},m.storedStates=m.storedStates||[],m.savedStates=m.savedStates||[],m.normalizeStore=function(){m.store.idToState=m.store.idToState||{},m.store.urlToId=m.store.urlToId||{},m.store.stateToId=m.store.stateToId||{}},m.getState=function(a,b){typeof a=="undefined"&&(a=!0),typeof b=="undefined"&&(b=!0);var c=m.getLastSavedState();return!c&&b&&(c=m.createStateObject()),a&&(c=m.cloneObject(c),c.url=c.cleanUrl||c.url),c},m.getIdByState=function(a){var b=m.extractId(a.url),c;if(!b){c=m.getStateString(a);if(typeof m.stateToId[c]!="undefined")b=m.stateToId[c];else if(typeof m.store.stateToId[c]!="undefined")b=m.store.stateToId[c];else{for(;;){b=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof m.idToState[b]=="undefined"&&typeof m.store.idToState[b]=="undefined")break}m.stateToId[c]=b,m.idToState[b]=a}}return b},m.normalizeState=function(a){var b,c;if(!a||typeof a!="object")a={};if(typeof a.normalized!="undefined")return a;if(!a.data||typeof a.data!="object")a.data={};b={},b.normalized=!0,b.title=a.title||"",b.url=m.getFullUrl(m.unescapeString(a.url||d.location.href)),b.hash=m.getShortUrl(b.url),b.data=m.cloneObject(a.data),b.id=m.getIdByState(b),b.cleanUrl=b.url.replace(/\??\&_suid.*/,""),b.url=b.cleanUrl,c=!m.isEmptyObject(b.data);if(b.title||c)b.hash=m.getShortUrl(b.url).replace(/\??\&_suid.*/,""),/\?/.test(b.hash)||(b.hash+="?"),b.hash+="&_suid="+b.id;return b.hashedUrl=m.getFullUrl(b.hash),(m.emulated.pushState||m.bugs.safariPoll)&&m.hasUrlDuplicate(b)&&(b.url=b.hashedUrl),b},m.createStateObject=function(a,b,c){var d={data:a,title:b,url:c};return d=m.normalizeState(d),d},m.getStateById=function(a){a=String(a);var c=m.idToState[a]||m.store.idToState[a]||b;return c},m.getStateString=function(a){var b,c,d;return b=m.normalizeState(a),c={data:b.data,title:a.title,url:a.url},d=k.stringify(c),d},m.getStateId=function(a){var b,c;return b=m.normalizeState(a),c=b.id,c},m.getHashByState=function(a){var b,c;return b=m.normalizeState(a),c=b.hash,c},m.extractId=function(a){var b,c,d;return c=/(.*)\&_suid=([0-9]+)$/.exec(a),d=c?c[1]||a:a,b=c?String(c[2]||""):"",b||!1},m.isTraditionalAnchor=function(a){var b=!/[\/\?\.]/.test(a);return b},m.extractState=function(a,b){var c=null,d,e;return b=b||!1,d=m.extractId(a),d&&(c=m.getStateById(d)),c||(e=m.getFullUrl(a),d=m.getIdByUrl(e)||!1,d&&(c=m.getStateById(d)),!c&&b&&!m.isTraditionalAnchor(a)&&(c=m.createStateObject(null,null,e))),c},m.getIdByUrl=function(a){var c=m.urlToId[a]||m.store.urlToId[a]||b;return c},m.getLastSavedState=function(){return m.savedStates[m.savedStates.length-1]||b},m.getLastStoredState=function(){return m.storedStates[m.storedStates.length-1]||b},m.hasUrlDuplicate=function(a){var b=!1,c;return c=m.extractState(a.url),b=c&&c.id!==a.id,b},m.storeState=function(a){return m.urlToId[a.url]=a.id,m.storedStates.push(m.cloneObject(a)),a},m.isLastSavedState=function(a){var b=!1,c,d,e;return m.savedStates.length&&(c=a.id,d=m.getLastSavedState(),e=d.id,b=c===e),b},m.saveState=function(a){return m.isLastSavedState(a)?!1:(m.savedStates.push(m.cloneObject(a)),!0)},m.getStateByIndex=function(a){var b=null;return typeof a=="undefined"?b=m.savedStates[m.savedStates.length-1]:a<0?b=m.savedStates[m.savedStates.length+a]:b=m.savedStates[a],b},m.getHash=function(){var a=m.unescapeHash(d.location.hash);return a},m.unescapeString=function(b){var c=b,d;for(;;){d=a.unescape(c);if(d===c)break;c=d}return c},m.unescapeHash=function(a){var b=m.normalizeHash(a);return b=m.unescapeString(b),b},m.normalizeHash=function(a){var b=a.replace(/[^#]*#/,"").replace(/#.*/,"");return b},m.setHash=function(a,b){var c,e,f;return b!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.setHash,args:arguments,queue:b}),!1):(c=m.escapeHash(a),m.busy(!0),e=m.extractState(a,!0),e&&!m.emulated.pushState?m.pushState(e.data,e.title,e.url,!1):d.location.hash!==c&&(m.bugs.setHash?(f=m.getPageUrl(),m.pushState(null,null,f+"#"+c,!1)):d.location.hash=c),m)},m.escapeHash=function(b){var c=m.normalizeHash(b);return c=a.escape(c),m.bugs.hashEscape||(c=c.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),c},m.getHashByUrl=function(a){var b=String(a).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return b=m.unescapeHash(b),b},m.setTitle=function(a){var b=a.title,c;b||(c=m.getStateByIndex(0),c&&c.url===a.url&&(b=c.title||m.options.initialTitle));try{d.getElementsByTagName("title")[0].innerHTML=b.replace("<","<").replace(">",">").replace(" & "," & ")}catch(e){}return d.title=b,m},m.queues=[],m.busy=function(a){typeof a!="undefined"?m.busy.flag=a:typeof m.busy.flag=="undefined"&&(m.busy.flag=!1);if(!m.busy.flag){h(m.busy.timeout);var b=function(){var a,c,d;if(m.busy.flag)return;for(a=m.queues.length-1;a>=0;--a){c=m.queues[a];if(c.length===0)continue;d=c.shift(),m.fireQueueItem(d),m.busy.timeout=g(b,m.options.busyDelay)}};m.busy.timeout=g(b,m.options.busyDelay)}return m.busy.flag},m.busy.flag=!1,m.fireQueueItem=function(a){return a.callback.apply(a.scope||m,a.args||[])},m.pushQueue=function(a){return m.queues[a.queue||0]=m.queues[a.queue||0]||[],m.queues[a.queue||0].push(a),m},m.queue=function(a,b){return typeof a=="function"&&(a={callback:a}),typeof b!="undefined"&&(a.queue=b),m.busy()?m.pushQueue(a):m.fireQueueItem(a),m},m.clearQueue=function(){return m.busy.flag=!1,m.queues=[],m},m.stateChanged=!1,m.doubleChecker=!1,m.doubleCheckComplete=function(){return m.stateChanged=!0,m.doubleCheckClear(),m},m.doubleCheckClear=function(){return m.doubleChecker&&(h(m.doubleChecker),m.doubleChecker=!1),m},m.doubleCheck=function(a){return m.stateChanged=!1,m.doubleCheckClear(),m.bugs.ieDoubleCheck&&(m.doubleChecker=g(function(){return m.doubleCheckClear(),m.stateChanged||a(),!0},m.options.doubleCheckInterval)),m},m.safariStatePoll=function(){var b=m.extractState(d.location.href),c;if(!m.isLastSavedState(b))c=b;else return;return c||(c=m.createStateObject()),m.Adapter.trigger(a,"popstate"),m},m.back=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.back,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.back(!1)}),n.go(-1),!0)},m.forward=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.forward,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.forward(!1)}),n.go(1),!0)},m.go=function(a,b){var c;if(a>0)for(c=1;c<=a;++c)m.forward(b);else{if(!(a<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(c=-1;c>=a;--c)m.back(b)}return m};if(m.emulated.pushState){var o=function(){};m.pushState=m.pushState||o,m.replaceState=m.replaceState||o}else m.onPopState=function(b,c){var e=!1,f=!1,g,h;return m.doubleCheckComplete(),g=m.getHash(),g?(h=m.extractState(g||d.location.href,!0),h?m.replaceState(h.data,h.title,h.url,!1):(m.Adapter.trigger(a,"anchorchange"),m.busy(!1)),m.expectedStateId=!1,!1):(e=m.Adapter.extractEventData("state",b,c)||!1,e?f=m.getStateById(e):m.expectedStateId?f=m.getStateById(m.expectedStateId):f=m.extractState(d.location.href),f||(f=m.createStateObject(null,null,d.location.href)),m.expectedStateId=!1,m.isLastSavedState(f)?(m.busy(!1),!1):(m.storeState(f),m.saveState(f),m.setTitle(f),m.Adapter.trigger(a,"statechange"),m.busy(!1),!0))},m.Adapter.bind(a,"popstate",m.onPopState),m.pushState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.pushState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.pushState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0},m.replaceState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.replaceState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.replaceState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0};if(f){try{m.store=k.parse(f.getItem("History.store"))||{}}catch(p){m.store={}}m.normalizeStore()}else m.store={},m.normalizeStore();m.Adapter.bind(a,"beforeunload",m.clearAllIntervals),m.Adapter.bind(a,"unload",m.clearAllIntervals),m.saveState(m.storeState(m.extractState(d.location.href,!0))),f&&(m.onUnload=function(){var a,b;try{a=k.parse(f.getItem("History.store"))||{}}catch(c){a={}}a.idToState=a.idToState||{},a.urlToId=a.urlToId||{},a.stateToId=a.stateToId||{};for(b in m.idToState){if(!m.idToState.hasOwnProperty(b))continue;a.idToState[b]=m.idToState[b]}for(b in m.urlToId){if(!m.urlToId.hasOwnProperty(b))continue;a.urlToId[b]=m.urlToId[b]}for(b in m.stateToId){if(!m.stateToId.hasOwnProperty(b))continue;a.stateToId[b]=m.stateToId[b]}m.store=a,m.normalizeStore(),f.setItem("History.store",k.stringify(a))},m.intervalList.push(i(m.onUnload,m.options.storeInterval)),m.Adapter.bind(a,"beforeunload",m.onUnload),m.Adapter.bind(a,"unload",m.onUnload));if(!m.emulated.pushState){m.bugs.safariPoll&&m.intervalList.push(i(m.safariStatePoll,m.options.safariPollInterval));if(e.vendor==="Apple Computer, Inc."||(e.appCodeName||"")==="Mozilla")m.Adapter.bind(a,"hashchange",function(){m.Adapter.trigger(a,"popstate")}),m.getHash()&&m.Adapter.onDomLoad(function(){m.Adapter.trigger(a,"hashchange")})}},m.init()}(window) \ No newline at end of file From 3c4d24b71ed08310c3704547601ff3b9ebaceb03 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 1 Jan 2016 17:52:40 -0500 Subject: [PATCH 046/280] Remove jquery.blockUI.js plugin It was required but never used. --- app/assets/javascripts/application.js.coffee | 1 - vendor/assets/javascripts/jquery.blockUI.js | 590 ------------------- 2 files changed, 591 deletions(-) delete mode 100644 vendor/assets/javascripts/jquery.blockUI.js diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index affab5bb030..65ca1367724 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -14,7 +14,6 @@ #= require jquery.waitforimages #= require jquery.atwho #= require jquery.scrollTo -#= require jquery.blockUI #= require jquery.turbolinks #= require turbolinks #= require autosave diff --git a/vendor/assets/javascripts/jquery.blockUI.js b/vendor/assets/javascripts/jquery.blockUI.js deleted file mode 100644 index c8702d79b65..00000000000 --- a/vendor/assets/javascripts/jquery.blockUI.js +++ /dev/null @@ -1,590 +0,0 @@ -/*! - * jQuery blockUI plugin - * Version 2.60.0-2013.04.05 - * @requires jQuery v1.7 or later - * - * Examples at: http://malsup.com/jquery/block/ - * Copyright (c) 2007-2013 M. Alsup - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - * Thanks to Amir-Hossein Sobhi for some excellent contributions! - */ - -;(function() { -/*jshint eqeqeq:false curly:false latedef:false */ -"use strict"; - - function setup($) { - $.fn._fadeIn = $.fn.fadeIn; - - var noOp = $.noop || function() {}; - - // this bit is to ensure we don't call setExpression when we shouldn't (with extra muscle to handle - // retarded userAgent strings on Vista) - var msie = /MSIE/.test(navigator.userAgent); - var ie6 = /MSIE 6.0/.test(navigator.userAgent) && ! /MSIE 8.0/.test(navigator.userAgent); - var mode = document.documentMode || 0; - var setExpr = $.isFunction( document.createElement('div').style.setExpression ); - - // global $ methods for blocking/unblocking the entire page - $.blockUI = function(opts) { install(window, opts); }; - $.unblockUI = function(opts) { remove(window, opts); }; - - // convenience method for quick growl-like notifications (http://www.google.com/search?q=growl) - $.growlUI = function(title, message, timeout, onClose) { - var $m = $('
'); - if (title) $m.append('

'+title+'

'); - if (message) $m.append('

'+message+'

'); - if (timeout === undefined) timeout = 3000; - $.blockUI({ - message: $m, fadeIn: 700, fadeOut: 1000, centerY: false, - timeout: timeout, showOverlay: false, - onUnblock: onClose, - css: $.blockUI.defaults.growlCSS - }); - }; - - // plugin method for blocking element content - $.fn.block = function(opts) { - if ( this[0] === window ) { - $.blockUI( opts ); - return this; - } - var fullOpts = $.extend({}, $.blockUI.defaults, opts || {}); - this.each(function() { - var $el = $(this); - if (fullOpts.ignoreIfBlocked && $el.data('blockUI.isBlocked')) - return; - $el.unblock({ fadeOut: 0 }); - }); - - return this.each(function() { - if ($.css(this,'position') == 'static') { - this.style.position = 'relative'; - $(this).data('blockUI.static', true); - } - this.style.zoom = 1; // force 'hasLayout' in ie - install(this, opts); - }); - }; - - // plugin method for unblocking element content - $.fn.unblock = function(opts) { - if ( this[0] === window ) { - $.unblockUI( opts ); - return this; - } - return this.each(function() { - remove(this, opts); - }); - }; - - $.blockUI.version = 2.60; // 2nd generation blocking at no extra cost! - - // override these in your code to change the default behavior and style - $.blockUI.defaults = { - // message displayed when blocking (use null for no message) - message: '

Please wait...

', - - title: null, // title string; only used when theme == true - draggable: true, // only used when theme == true (requires jquery-ui.js to be loaded) - - theme: false, // set to true to use with jQuery UI themes - - // styles for the message when blocking; if you wish to disable - // these and use an external stylesheet then do this in your code: - // $.blockUI.defaults.css = {}; - css: { - padding: 0, - margin: 0, - width: '30%', - top: '40%', - left: '35%', - textAlign: 'center', - color: '#000', - border: '3px solid #aaa', - backgroundColor:'#fff', - cursor: 'wait' - }, - - // minimal style set used when themes are used - themedCSS: { - width: '30%', - top: '40%', - left: '35%' - }, - - // styles for the overlay - overlayCSS: { - backgroundColor: '#000', - opacity: 0.6, - cursor: 'wait' - }, - - // style to replace wait cursor before unblocking to correct issue - // of lingering wait cursor - cursorReset: 'default', - - // styles applied when using $.growlUI - growlCSS: { - width: '350px', - top: '10px', - left: '', - right: '10px', - border: 'none', - padding: '5px', - opacity: 0.6, - cursor: 'default', - color: '#fff', - backgroundColor: '#000', - '-webkit-border-radius':'10px', - '-moz-border-radius': '10px', - 'border-radius': '10px' - }, - - // IE issues: 'about:blank' fails on HTTPS and javascript:false is s-l-o-w - // (hat tip to Jorge H. N. de Vasconcelos) - /*jshint scripturl:true */ - iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank', - - // force usage of iframe in non-IE browsers (handy for blocking applets) - forceIframe: false, - - // z-index for the blocking overlay - baseZ: 1000, - - // set these to true to have the message automatically centered - centerX: true, // <-- only effects element blocking (page block controlled via css above) - centerY: true, - - // allow body element to be stetched in ie6; this makes blocking look better - // on "short" pages. disable if you wish to prevent changes to the body height - allowBodyStretch: true, - - // enable if you want key and mouse events to be disabled for content that is blocked - bindEvents: true, - - // be default blockUI will supress tab navigation from leaving blocking content - // (if bindEvents is true) - constrainTabKey: true, - - // fadeIn time in millis; set to 0 to disable fadeIn on block - fadeIn: 200, - - // fadeOut time in millis; set to 0 to disable fadeOut on unblock - fadeOut: 400, - - // time in millis to wait before auto-unblocking; set to 0 to disable auto-unblock - timeout: 0, - - // disable if you don't want to show the overlay - showOverlay: true, - - // if true, focus will be placed in the first available input field when - // page blocking - focusInput: true, - - // elements that can receive focus - focusableElements: ':input:enabled:visible', - - // suppresses the use of overlay styles on FF/Linux (due to performance issues with opacity) - // no longer needed in 2012 - // applyPlatformOpacityRules: true, - - // callback method invoked when fadeIn has completed and blocking message is visible - onBlock: null, - - // callback method invoked when unblocking has completed; the callback is - // passed the element that has been unblocked (which is the window object for page - // blocks) and the options that were passed to the unblock call: - // onUnblock(element, options) - onUnblock: null, - - // callback method invoked when the overlay area is clicked. - // setting this will turn the cursor to a pointer, otherwise cursor defined in overlayCss will be used. - onOverlayClick: null, - - // don't ask; if you really must know: http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493 - quirksmodeOffsetHack: 4, - - // class name of the message block - blockMsgClass: 'blockMsg', - - // if it is already blocked, then ignore it (don't unblock and reblock) - ignoreIfBlocked: false - }; - - // private data and functions follow... - - var pageBlock = null; - var pageBlockEls = []; - - function install(el, opts) { - var css, themedCSS; - var full = (el == window); - var msg = (opts && opts.message !== undefined ? opts.message : undefined); - opts = $.extend({}, $.blockUI.defaults, opts || {}); - - if (opts.ignoreIfBlocked && $(el).data('blockUI.isBlocked')) - return; - - opts.overlayCSS = $.extend({}, $.blockUI.defaults.overlayCSS, opts.overlayCSS || {}); - css = $.extend({}, $.blockUI.defaults.css, opts.css || {}); - if (opts.onOverlayClick) - opts.overlayCSS.cursor = 'pointer'; - - themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, opts.themedCSS || {}); - msg = msg === undefined ? opts.message : msg; - - // remove the current block (if there is one) - if (full && pageBlock) - remove(window, {fadeOut:0}); - - // if an existing element is being used as the blocking content then we capture - // its current place in the DOM (and current display style) so we can restore - // it when we unblock - if (msg && typeof msg != 'string' && (msg.parentNode || msg.jquery)) { - var node = msg.jquery ? msg[0] : msg; - var data = {}; - $(el).data('blockUI.history', data); - data.el = node; - data.parent = node.parentNode; - data.display = node.style.display; - data.position = node.style.position; - if (data.parent) - data.parent.removeChild(node); - } - - $(el).data('blockUI.onUnblock', opts.onUnblock); - var z = opts.baseZ; - - // blockUI uses 3 layers for blocking, for simplicity they are all used on every platform; - // layer1 is the iframe layer which is used to supress bleed through of underlying content - // layer2 is the overlay layer which has opacity and a wait cursor (by default) - // layer3 is the message content that is displayed while blocking - var lyr1, lyr2, lyr3, s; - if (msie || opts.forceIframe) - lyr1 = $(''); - else - lyr1 = $(''); - - if (opts.theme) - lyr2 = $(''); - else - lyr2 = $(''); - - if (opts.theme && full) { - s = ''; - } - else if (opts.theme) { - s = ''; - } - else if (full) { - s = ''; - } - else { - s = ''; - } - lyr3 = $(s); - - // if we have a message, style it - if (msg) { - if (opts.theme) { - lyr3.css(themedCSS); - lyr3.addClass('ui-widget-content'); - } - else - lyr3.css(css); - } - - // style the overlay - if (!opts.theme /*&& (!opts.applyPlatformOpacityRules)*/) - lyr2.css(opts.overlayCSS); - lyr2.css('position', full ? 'fixed' : 'absolute'); - - // make iframe layer transparent in IE - if (msie || opts.forceIframe) - lyr1.css('opacity',0.0); - - //$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el); - var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') : $(el); - $.each(layers, function() { - this.appendTo($par); - }); - - if (opts.theme && opts.draggable && $.fn.draggable) { - lyr3.draggable({ - handle: '.ui-dialog-titlebar', - cancel: 'li' - }); - } - - // ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling) - var expr = setExpr && (!$.support.boxModel || $('object,embed', full ? null : el).length > 0); - if (ie6 || expr) { - // give body 100% height - if (full && opts.allowBodyStretch && $.support.boxModel) - $('html,body').css('height','100%'); - - // fix ie6 issue when blocked element has a border width - if ((ie6 || !$.support.boxModel) && !full) { - var t = sz(el,'borderTopWidth'), l = sz(el,'borderLeftWidth'); - var fixT = t ? '(0 - '+t+')' : 0; - var fixL = l ? '(0 - '+l+')' : 0; - } - - // simulate fixed position - $.each(layers, function(i,o) { - var s = o[0].style; - s.position = 'absolute'; - if (i < 2) { - if (full) - s.setExpression('height','Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.support.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"'); - else - s.setExpression('height','this.parentNode.offsetHeight + "px"'); - if (full) - s.setExpression('width','jQuery.support.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"'); - else - s.setExpression('width','this.parentNode.offsetWidth + "px"'); - if (fixL) s.setExpression('left', fixL); - if (fixT) s.setExpression('top', fixT); - } - else if (opts.centerY) { - if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"'); - s.marginTop = 0; - } - else if (!opts.centerY && full) { - var top = (opts.css && opts.css.top) ? parseInt(opts.css.top, 10) : 0; - var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + '+top+') + "px"'; - s.setExpression('top',expression); - } - }); - } - - // show the message - if (msg) { - if (opts.theme) - lyr3.find('.ui-widget-content').append(msg); - else - lyr3.append(msg); - if (msg.jquery || msg.nodeType) - $(msg).show(); - } - - if ((msie || opts.forceIframe) && opts.showOverlay) - lyr1.show(); // opacity is zero - if (opts.fadeIn) { - var cb = opts.onBlock ? opts.onBlock : noOp; - var cb1 = (opts.showOverlay && !msg) ? cb : noOp; - var cb2 = msg ? cb : noOp; - if (opts.showOverlay) - lyr2._fadeIn(opts.fadeIn, cb1); - if (msg) - lyr3._fadeIn(opts.fadeIn, cb2); - } - else { - if (opts.showOverlay) - lyr2.show(); - if (msg) - lyr3.show(); - if (opts.onBlock) - opts.onBlock(); - } - - // bind key and mouse events - bind(1, el, opts); - - if (full) { - pageBlock = lyr3[0]; - pageBlockEls = $(opts.focusableElements,pageBlock); - if (opts.focusInput) - setTimeout(focus, 20); - } - else - center(lyr3[0], opts.centerX, opts.centerY); - - if (opts.timeout) { - // auto-unblock - var to = setTimeout(function() { - if (full) - $.unblockUI(opts); - else - $(el).unblock(opts); - }, opts.timeout); - $(el).data('blockUI.timeout', to); - } - } - - // remove the block - function remove(el, opts) { - var count; - var full = (el == window); - var $el = $(el); - var data = $el.data('blockUI.history'); - var to = $el.data('blockUI.timeout'); - if (to) { - clearTimeout(to); - $el.removeData('blockUI.timeout'); - } - opts = $.extend({}, $.blockUI.defaults, opts || {}); - bind(0, el, opts); // unbind events - - if (opts.onUnblock === null) { - opts.onUnblock = $el.data('blockUI.onUnblock'); - $el.removeData('blockUI.onUnblock'); - } - - var els; - if (full) // crazy selector to handle odd field errors in ie6/7 - els = $('body').children().filter('.blockUI').add('body > .blockUI'); - else - els = $el.find('>.blockUI'); - - // fix cursor issue - if ( opts.cursorReset ) { - if ( els.length > 1 ) - els[1].style.cursor = opts.cursorReset; - if ( els.length > 2 ) - els[2].style.cursor = opts.cursorReset; - } - - if (full) - pageBlock = pageBlockEls = null; - - if (opts.fadeOut) { - count = els.length; - els.fadeOut(opts.fadeOut, function() { - if ( --count === 0) - reset(els,data,opts,el); - }); - } - else - reset(els, data, opts, el); - } - - // move blocking element back into the DOM where it started - function reset(els,data,opts,el) { - var $el = $(el); - els.each(function(i,o) { - // remove via DOM calls so we don't lose event handlers - if (this.parentNode) - this.parentNode.removeChild(this); - }); - - if (data && data.el) { - data.el.style.display = data.display; - data.el.style.position = data.position; - if (data.parent) - data.parent.appendChild(data.el); - $el.removeData('blockUI.history'); - } - - if ($el.data('blockUI.static')) { - $el.css('position', 'static'); // #22 - } - - if (typeof opts.onUnblock == 'function') - opts.onUnblock(el,opts); - - // fix issue in Safari 6 where block artifacts remain until reflow - var body = $(document.body), w = body.width(), cssW = body[0].style.width; - body.width(w-1).width(w); - body[0].style.width = cssW; - } - - // bind/unbind the handler - function bind(b, el, opts) { - var full = el == window, $el = $(el); - - // don't bother unbinding if there is nothing to unbind - if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked'))) - return; - - $el.data('blockUI.isBlocked', b); - - // don't bind events when overlay is not in use or if bindEvents is false - if (!full || !opts.bindEvents || (b && !opts.showOverlay)) - return; - - // bind anchors and inputs for mouse and key events - var events = 'mousedown mouseup keydown keypress keyup touchstart touchend touchmove'; - if (b) - $(document).bind(events, opts, handler); - else - $(document).unbind(events, handler); - - // former impl... - // var $e = $('a,:input'); - // b ? $e.bind(events, opts, handler) : $e.unbind(events, handler); - } - - // event handler to suppress keyboard/mouse events when blocking - function handler(e) { - // allow tab navigation (conditionally) - if (e.keyCode && e.keyCode == 9) { - if (pageBlock && e.data.constrainTabKey) { - var els = pageBlockEls; - var fwd = !e.shiftKey && e.target === els[els.length-1]; - var back = e.shiftKey && e.target === els[0]; - if (fwd || back) { - setTimeout(function(){focus(back);},10); - return false; - } - } - } - var opts = e.data; - var target = $(e.target); - if (target.hasClass('blockOverlay') && opts.onOverlayClick) - opts.onOverlayClick(); - - // allow events within the message content - if (target.parents('div.' + opts.blockMsgClass).length > 0) - return true; - - // allow events for content that is not being blocked - return target.parents().children().filter('div.blockUI').length === 0; - } - - function focus(back) { - if (!pageBlockEls) - return; - var e = pageBlockEls[back===true ? pageBlockEls.length-1 : 0]; - if (e) - e.focus(); - } - - function center(el, x, y) { - var p = el.parentNode, s = el.style; - var l = ((p.offsetWidth - el.offsetWidth)/2) - sz(p,'borderLeftWidth'); - var t = ((p.offsetHeight - el.offsetHeight)/2) - sz(p,'borderTopWidth'); - if (x) s.left = l > 0 ? (l+'px') : '0'; - if (y) s.top = t > 0 ? (t+'px') : '0'; - } - - function sz(el, p) { - return parseInt($.css(el,p),10)||0; - } - - } - - - /*global define:true */ - if (typeof define === 'function' && define.amd && define.amd.jQuery) { - define(['jquery'], setup); - } else { - setup(jQuery); - } - -})(); From 93ef00850001962955a2f9c8f34a530a9cf8972f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 1 Jan 2016 21:11:39 -0500 Subject: [PATCH 047/280] Bump bootstrap-sass to ~> 3.3.0 --- Gemfile | 2 +- Gemfile.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 2a1c4f7d73a..f77b7d25bb2 100644 --- a/Gemfile +++ b/Gemfile @@ -200,7 +200,7 @@ gem 'turbolinks', '~> 2.5.0' gem 'jquery-turbolinks', '~> 2.1.0' gem 'addressable', '~> 2.3.8' -gem 'bootstrap-sass', '~> 3.0' +gem 'bootstrap-sass', '~> 3.3.0' gem 'font-awesome-rails', '~> 4.2' gem 'gitlab_emoji', '~> 0.2.0' gem 'gon', '~> 6.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index 9769ae80a7d..3b977944eec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -66,7 +66,7 @@ GEM attr_encrypted (1.3.4) encryptor (>= 1.3.0) attr_required (1.0.0) - autoprefixer-rails (6.1.2) + autoprefixer-rails (6.2.3) execjs json awesome_print (1.2.0) @@ -82,9 +82,9 @@ GEM erubis (>= 2.6.6) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - bootstrap-sass (3.3.5) - autoprefixer-rails (>= 5.0.0.1) - sass (>= 3.2.19) + bootstrap-sass (3.3.6) + autoprefixer-rails (>= 5.2.1) + sass (>= 3.3.4) brakeman (3.1.4) erubis (~> 2.6) fastercsv (~> 1.5) @@ -843,7 +843,7 @@ DEPENDENCIES benchmark-ips better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) - bootstrap-sass (~> 3.0) + bootstrap-sass (~> 3.3.0) brakeman (~> 3.1.0) browser (~> 1.0.0) bullet From 762210db6d3a16307c032d84454b55474221e50c Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 1 Jan 2016 21:29:21 -0500 Subject: [PATCH 048/280] Bump cal-heatmap-rails to ~> 3.5.0 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 2a1c4f7d73a..2418a02a451 100644 --- a/Gemfile +++ b/Gemfile @@ -172,7 +172,7 @@ gem 'ruby-fogbugz', '~> 0.2.1' gem 'd3_rails', '~> 3.5.5' #cal-heatmap -gem "cal-heatmap-rails", "~> 0.0.1" +gem 'cal-heatmap-rails', '~> 3.5.0' # underscore-rails gem "underscore-rails", "~> 1.8.0" diff --git a/Gemfile.lock b/Gemfile.lock index 9769ae80a7d..ddd31e4802d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -106,7 +106,7 @@ GEM bundler (~> 1.2) thor (~> 0.18) byebug (8.2.1) - cal-heatmap-rails (0.0.1) + cal-heatmap-rails (3.5.1) capybara (2.4.4) mime-types (>= 1.16) nokogiri (>= 1.3.3) @@ -849,7 +849,7 @@ DEPENDENCIES bullet bundler-audit byebug - cal-heatmap-rails (~> 0.0.1) + cal-heatmap-rails (~> 3.5.0) capybara (~> 2.4.0) capybara-screenshot (~> 1.0.0) carrierwave (~> 0.9.0) From 2417f55467247471d2b39de0387b30617ecf59de Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 1 Jan 2016 21:34:49 -0500 Subject: [PATCH 049/280] Bump d3_rails to ~> 3.5.0 --- Gemfile | 2 +- Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 2418a02a451..1a628a68310 100644 --- a/Gemfile +++ b/Gemfile @@ -169,7 +169,7 @@ gem 'asana', '~> 0.4.0' gem 'ruby-fogbugz', '~> 0.2.1' # d3 -gem 'd3_rails', '~> 3.5.5' +gem 'd3_rails', '~> 3.5.0' #cal-heatmap gem 'cal-heatmap-rails', '~> 3.5.0' diff --git a/Gemfile.lock b/Gemfile.lock index ddd31e4802d..7b70c05bc48 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -859,7 +859,7 @@ DEPENDENCIES connection_pool (~> 2.0) coveralls (~> 0.8.2) creole (~> 0.5.0) - d3_rails (~> 3.5.5) + d3_rails (~> 3.5.0) database_cleaner (~> 1.4.0) default_value_for (~> 3.0.0) devise (~> 3.5.3) From f8c3b69fb675f33607315acfacb4fdac170a3635 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 1 Jan 2016 22:10:16 -0500 Subject: [PATCH 050/280] Remove unused "options" object from Calendar JS --- app/assets/javascripts/calendar.js.coffee | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index 97621236924..d80e0e716ce 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -1,9 +1,4 @@ class @Calendar - options = - month: "short" - day: "numeric" - year: "numeric" - constructor: (timestamps, starting_year, starting_month, calendar_activities_path) -> cal = new CalHeatMap() cal.init From 4caf0433b7a06427c82eb1b2830d2c2dd830f144 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 1 Jan 2016 22:10:48 -0500 Subject: [PATCH 051/280] Reorder JS requires The old way broke d3, for some reason. --- app/assets/javascripts/application.js.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index affab5bb030..e7e997640d5 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -16,6 +16,8 @@ #= require jquery.scrollTo #= require jquery.blockUI #= require jquery.turbolinks +#= require d3 +#= require cal-heatmap #= require turbolinks #= require autosave #= require bootstrap @@ -27,7 +29,6 @@ #= require branch-graph #= require ace/ace #= require ace/ext-searchbox -#= require d3 #= require underscore #= require nprogress #= require nprogress-turbolinks @@ -39,7 +40,6 @@ #= require shortcuts_dashboard_navigation #= require shortcuts_issuable #= require shortcuts_network -#= require cal-heatmap #= require jquery.nicescroll.min #= require_tree . From 1a1113f7c4ad6e5bceb898593cba6caecbdf0097 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 1 Jan 2016 22:11:34 -0500 Subject: [PATCH 052/280] Simplify `ContributionsCalendar#starting_year` and `#starting_month` --- lib/gitlab/contributions_calendar.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index 8a7f8dc5003..85583dce9ee 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -45,11 +45,11 @@ module Gitlab end def starting_year - (Time.now - 1.year).strftime("%Y") + 1.year.ago.year end def starting_month - Date.today.strftime("%m").to_i + Date.today.month end end end From d1873f16b4cc954b1417fa84517d44b22a05821f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 1 Jan 2016 22:12:50 -0500 Subject: [PATCH 053/280] Update cal-heatmap style overrides Some were no longer needed, others needed `!important`. --- .../stylesheets/framework/calendar.scss | 42 +++---------------- 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index a36fefe22c5..580012abd77 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -19,38 +19,33 @@ } } } + /** * This overwrites the default values of the cal-heatmap gem */ .calendar { .qi { - background-color: #999; fill: #fff; } .q1 { - background-color: #dae289; - fill: #ededed; + fill: #ededed !important; } .q2 { - background-color: #cedb9c; - fill: #ACD5F2; + fill: #ACD5F2 !important; } .q3 { - background-color: #b5cf6b; - fill: #7FA8D1; + fill: #7FA8D1 !important; } .q4 { - background-color: #637939; - fill: #49729B; + fill: #49729B !important; } .q5 { - background-color: #3b6427; - fill: #254E77; + fill: #254E77 !important; } .domain-background { @@ -59,32 +54,7 @@ } .ch-tooltip { - position: absolute; - display: none; - margin-top: 22px; - margin-left: 1px; - font-size: 13px; padding: 3px; font-weight: 550; - background-color: #222; - span { - position: absolute; - width: 200px; - text-align: center; - visibility: hidden; - border-radius: 10px; - &:after { - content: ''; - position: absolute; - top: 100%; - left: 50%; - margin-left: -8px; - width: 0; - height: 0; - border-top: 8px solid #000000; - border-right: 8px solid transparent; - border-left: 8px solid transparent; - } - } } } From c80f75ea1fc1dda4001ab32d52136445abb09b53 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 2 Jan 2016 15:29:55 -0500 Subject: [PATCH 054/280] Give the logo shapes meaningful IDs --- app/views/shared/_logo.svg | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg index da49c48acd3..90f5f4e672b 100644 --- a/app/views/shared/_logo.svg +++ b/app/views/shared/_logo.svg @@ -5,13 +5,13 @@ - - - - - - - + + + + + + + From e5800d65de66aded6178252c4ae5025633eccc5e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 2 Jan 2016 15:32:40 -0500 Subject: [PATCH 055/280] Use the logo as a loading indicator Closes #5616 --- app/assets/javascripts/logo.js.coffee | 45 +++++++++++++++++++ app/assets/stylesheets/framework/sidebar.scss | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/logo.js.coffee diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee new file mode 100644 index 00000000000..8cd9f865e37 --- /dev/null +++ b/app/assets/javascripts/logo.js.coffee @@ -0,0 +1,45 @@ +NProgress.configure(showSpinner: false) + +defaultClass = 'tanuki-shape' +highlightClass = 'highlight' +pieces = [ + 'path#tanuki-right-cheek', + 'path#tanuki-right-eye, path#tanuki-right-ear', + 'path#tanuki-nose', + 'path#tanuki-left-eye, path#tanuki-left-ear', + 'path#tanuki-left-cheek', +] +timeout = null + +clearHighlights = -> + $(".#{defaultClass}").attr('class', defaultClass) + +start = -> + clearHighlights() + work(0) + +stop = -> + window.clearTimeout(timeout) + clearHighlights() + +work = (pieceIndex) => + # jQuery's addClass won't work on an SVG. Who knew! + $piece = $(pieces[pieceIndex]) + $piece.attr('class', "#{defaultClass} #{highlightClass}") + + timeout = setTimeout(=> + $piece.attr('class', defaultClass) + + # If we hit the last piece, reset the index and then reverse the array to + # get a nice back-and-forth sweeping look + if pieceIndex + 1 >= pieces.length + nextIndex = 0 + pieces.reverse() + else + nextIndex = pieceIndex + 1 + + work(nextIndex) + , 200) + +$(document).on 'page:fetch', start +$(document).on 'page:change', stop diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 458af76cb75..83243dd2457 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -105,7 +105,7 @@ .tanuki-shape { transition: all 0.8s; - &:hover { + &:hover, &.highlight { fill: rgb(255, 255, 255); transition: all 0.1s; } From 71c31ecf731ff9f6f1d7107ae03397ec98c9f61f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 2 Jan 2016 19:57:21 -0500 Subject: [PATCH 056/280] Ensure the sweep always starts from the left --- app/assets/javascripts/logo.js.coffee | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee index 8cd9f865e37..47135a6c5eb 100644 --- a/app/assets/javascripts/logo.js.coffee +++ b/app/assets/javascripts/logo.js.coffee @@ -1,14 +1,14 @@ NProgress.configure(showSpinner: false) defaultClass = 'tanuki-shape' -highlightClass = 'highlight' pieces = [ - 'path#tanuki-right-cheek', - 'path#tanuki-right-eye, path#tanuki-right-ear', - 'path#tanuki-nose', - 'path#tanuki-left-eye, path#tanuki-left-ear', 'path#tanuki-left-cheek', + 'path#tanuki-left-eye, path#tanuki-left-ear', + 'path#tanuki-nose', + 'path#tanuki-right-eye, path#tanuki-right-ear', + 'path#tanuki-right-cheek', ] +firstPiece = pieces[0] timeout = null clearHighlights = -> @@ -16,18 +16,19 @@ clearHighlights = -> start = -> clearHighlights() + pieces.reverse() unless pieces[0] == firstPiece work(0) stop = -> window.clearTimeout(timeout) clearHighlights() -work = (pieceIndex) => +work = (pieceIndex) -> # jQuery's addClass won't work on an SVG. Who knew! $piece = $(pieces[pieceIndex]) - $piece.attr('class', "#{defaultClass} #{highlightClass}") + $piece.attr('class', "#{defaultClass} highlight") - timeout = setTimeout(=> + timeout = setTimeout(-> $piece.attr('class', defaultClass) # If we hit the last piece, reset the index and then reverse the array to From fd178c1e7d23b0bf96565ae5177485e847c9271d Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 2 Jan 2016 19:53:45 -0500 Subject: [PATCH 057/280] Prevent duplicate "username has already been taken" validation message Closes #201 - two-year-old bug, woo! :boom: :tada: --- app/models/user.rb | 5 ++++- spec/models/user_spec.rb | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index df87f3b79bd..20f907e4347 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -352,10 +352,13 @@ class User < ActiveRecord::Base end def namespace_uniq + # Return early if username already failed the first uniqueness validation + return if self.errors[:username].include?('has already been taken') + namespace_name = self.username existing_namespace = Namespace.by_path(namespace_name) if existing_namespace && existing_namespace != self.namespace - self.errors.add :username, "already exists" + self.errors.add(:username, 'has already been taken') end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 2f184bbaf92..a16161e673e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -106,7 +106,7 @@ describe User, models: true do end it 'validates uniqueness' do - expect(subject).to validate_uniqueness_of(:username) + expect(subject).to validate_uniqueness_of(:username).case_insensitive end end From 2802d4992d064e63cf2015c7bb002318eaa84c41 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 3 Jan 2016 11:20:14 -0800 Subject: [PATCH 058/280] Expire view caches when application settings change Closes #5728 --- CHANGELOG | 1 + app/views/events/_event.html.haml | 2 +- app/views/projects/commits/_commit.html.haml | 2 +- app/views/shared/projects/_project.html.haml | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ea403642e32..4c1148bc4dc 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.4.0 (unreleased) + - Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu) - Implement new UI for group page - Implement search inside emoji picker - Add API support for looking up a user by username (Stan Hu) diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 9aacc79d686..46432a92348 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -3,7 +3,7 @@ .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} - = cache [event, "v2.1"] do + = cache [event, current_application_settings, "v2.1"] do = image_tag avatar_icon(event.author_email, 46), class: "avatar s46", alt:'' - if event.created_project? = render "events/event/created_project", event: event diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 28b82dd31f3..012825f0fdb 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -5,7 +5,7 @@ - note_count = notes.user.count - ci_commit = project.ci_commit(commit.sha) -- cache_key = [project.path_with_namespace, commit.id, note_count] +- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count] - cache_key.push(ci_commit.status) if ci_commit = cache(cache_key) do diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index c36995b94d7..86249851a82 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -5,7 +5,7 @@ - css_class = '' unless local_assigns[:css_class] - css_class += " no-description" unless project.description.present? %li.project-row{ class: css_class } - = cache [project.namespace, project, controller.controller_name, controller.action_name, 'v2.2'] do + = cache [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.2'] do = link_to project_path(project), class: dom_class(project) do - if avatar .dash-project-avatar From 086cfc8685a6489ca032899307c77f828f515fbb Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 26 Aug 2015 23:36:17 -0700 Subject: [PATCH 059/280] Fix API project lookups when querying with a namespace with dots Attempting to use the /projects/:id API by specifying :id in "namespace/project" format would always result in a 404 if the namespace contained a dot. The reason? From http://guides.rubyonrails.org/routing.html#specifying-constraints: "By default the :id parameter doesn't accept dots - this is because the dot is used as a separator for formatted routes. If you need to use a dot within an :id add a constraint which overrides this - for example id: /[^\/]+/ allows anything except a slash." Closes https://github.com/gitlabhq/gitlabhq/issues/9573 --- CHANGELOG | 1 + lib/api/projects.rb | 2 +- spec/requests/api/projects_spec.rb | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 2b7d5808e7e..e651aece696 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ v 8.4.0 (unreleased) - Revert back upvote and downvote button to the issue and MR pages - Swap position of Assignee and Author selector on Issuables (Zeger-Jan van de Weg) - Fix version check image in Safari + - Fix API project lookups when querying with a namespace with dots (Stan Hu) v 8.3.3 (unreleased) - Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu) diff --git a/lib/api/projects.rb b/lib/api/projects.rb index a9e0960872a..0781236cf6d 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -3,7 +3,7 @@ module API class Projects < Grape::API before { authenticate! } - resource :projects do + resource :projects, requirements: { id: /[^\/]+/ } do helpers do def map_public_to_visibility_level(attrs) publik = attrs.delete(:public) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 7f0f9454b10..ab2530859ea 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -382,6 +382,15 @@ describe API::API, api: true do expect(response.status).to eq(404) end + it 'should handle users with dots' do + dot_user = create(:user, username: 'dot.user') + project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace) + + get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user) + expect(response.status).to eq(200) + expect(json_response['name']).to eq(project.name) + end + describe 'permissions' do context 'all projects' do it 'Contains permission information' do From 8bfc46451eae5bd92380b9fbdf91b9636d55066c Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Wed, 23 Dec 2015 10:20:07 -0500 Subject: [PATCH 060/280] Show 'New Merge Request' buttons on canonical repo. --- CHANGELOG | 2 ++ app/views/projects/buttons/_dropdown.html.haml | 7 ++++--- app/views/projects/merge_requests/index.html.haml | 5 +++-- features/project/fork.feature | 10 ++++++++++ features/steps/project/fork.rb | 10 ++++++++++ 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2b7d5808e7e..e4245060489 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -91,6 +91,8 @@ v 8.3.0 - 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 + - Expose Git's version in the admin area + - Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye) v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 1f639fecc30..459e6da2fe2 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -8,11 +8,12 @@ = link_to url_for_new_issue(@project, only_path: true) do = icon('exclamation-circle fw') New issue - - if can?(current_user, :create_merge_request, @project) + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : current_user.fork_of(@project) + - if merge_project %li - = link_to new_namespace_project_merge_request_path(@project.namespace, @project) do + = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do = icon('tasks fw') - New merge request + New Merge Request - if can?(current_user, :create_snippet, @project) %li = link_to new_namespace_project_snippet_path(@project.namespace, @project) do diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 086298e5af1..972fce9ad3d 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -6,9 +6,10 @@ .controls = render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project) - - if can? current_user, :create_merge_request, @project + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : current_user.fork_of(@project) + - if merge_project .pull-left.hidden-xs - = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new", title: "New Merge Request" do + = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do %i.fa.fa-plus New Merge Request = render 'shared/issuable/filter', type: :merge_requests diff --git a/features/project/fork.feature b/features/project/fork.feature index 22f68e5b340..40849352370 100644 --- a/features/project/fork.feature +++ b/features/project/fork.feature @@ -1,3 +1,4 @@ +@forks Feature: Project Fork Background: Given I sign in as a user @@ -14,3 +15,12 @@ Feature: Project Fork And I click link "Fork" When I fork to my namespace Then I should see a "Name has already been taken" warning + + Scenario: Merge request on canonical repo goes to fork merge request page + Given I click link "Fork" + And I fork to my namespace + Then I should see the forked project page + When I visit project "Shop" page + Then I should see "New merge request" + And I goto the Merge Requests page + Then I should see "New merge request" diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index b0230add34f..878ddea46ff 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -30,4 +30,14 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps click_link current_user.name end end + + step 'I should see "New Merge Request"' do + expect(page).to have_content "New Merge Request" + end + + step 'I goto the Merge Requests page' do + page.within '.page-sidebar-expanded' do + click_link "Merge Requests" + end + end end From 95e6327b85b8230dc834c781203c72b318551b5d Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Wed, 23 Dec 2015 12:24:25 -0500 Subject: [PATCH 061/280] Fix tests --- app/views/projects/buttons/_dropdown.html.haml | 2 +- app/views/projects/merge_requests/index.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 459e6da2fe2..35a9d3223a6 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -8,7 +8,7 @@ = link_to url_for_new_issue(@project, only_path: true) do = icon('exclamation-circle fw') New issue - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : current_user.fork_of(@project) + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - if merge_project %li = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 972fce9ad3d..8d5d0394a82 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -6,7 +6,7 @@ .controls = render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project) - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : current_user.fork_of(@project) + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - if merge_project .pull-left.hidden-xs = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do From 796c11e9a7d955ac40c3d1f8427f3f65063ac8c4 Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Wed, 23 Dec 2015 12:36:49 -0500 Subject: [PATCH 062/280] Remove feature tag from testing locally --- features/project/fork.feature | 1 - 1 file changed, 1 deletion(-) diff --git a/features/project/fork.feature b/features/project/fork.feature index 40849352370..1182f493e34 100644 --- a/features/project/fork.feature +++ b/features/project/fork.feature @@ -1,4 +1,3 @@ -@forks Feature: Project Fork Background: Given I sign in as a user From edd2ce38369e5a332b1b9932647d670862ffddbf Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Fri, 25 Dec 2015 11:30:48 -0500 Subject: [PATCH 063/280] Change text back. Add additional tests. --- app/views/projects/buttons/_dropdown.html.haml | 2 +- features/project/fork.feature | 2 ++ features/steps/project/fork.rb | 13 +++++++++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 35a9d3223a6..f9ab78e7874 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -13,7 +13,7 @@ %li = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do = icon('tasks fw') - New Merge Request + New merge request - if can?(current_user, :create_snippet, @project) %li = link_to new_namespace_project_snippet_path(@project.namespace, @project) do diff --git a/features/project/fork.feature b/features/project/fork.feature index 1182f493e34..37cd53ee977 100644 --- a/features/project/fork.feature +++ b/features/project/fork.feature @@ -23,3 +23,5 @@ Feature: Project Fork Then I should see "New merge request" And I goto the Merge Requests page Then I should see "New merge request" + And I click link "New merge request" + Then I should see the new merge request page for my namespace diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 878ddea46ff..e98bd51ca89 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -31,8 +31,8 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end end - step 'I should see "New Merge Request"' do - expect(page).to have_content "New Merge Request" + step 'I should see "New merge request"' do + expect(page).to have_content(/new merge request/i) end step 'I goto the Merge Requests page' do @@ -40,4 +40,13 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps click_link "Merge Requests" end end + + step 'I click link "New merge request"' do + expect(page).to have_content(/new merge request/i) + click_link "New Merge Request" + end + + step 'I should see the new merge request page for my namespace' do + current_path.should have_content(/#{current_user.namespace.name}/i) + end end From 0134a85bee1e1f932d7c9d8752d8eb8e5148baf2 Mon Sep 17 00:00:00 2001 From: Christian Mehlmauer Date: Mon, 4 Jan 2016 00:29:30 +0100 Subject: [PATCH 064/280] added make command to docs --- doc/update/patch_versions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index c19ee49f9e0..a10e62877ba 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -48,6 +48,7 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`ca cd /home/git/gitlab-workhorse sudo -u git -H git fetch sudo -u git -H git checkout `cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` -b `cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` +sudo -u git -H make ``` ### 5. Install libs, migrations, etc. From 96075be6f4688a59335130dc796132ad4f232442 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 4 Jan 2016 11:37:46 +0100 Subject: [PATCH 065/280] Ability to increment custom transaction values This will be used to store/increment the total query/view rendering timings on a per transaction basis. This in turn can greatly reduce the amount of metrics stored. --- lib/gitlab/metrics/transaction.rb | 15 +++++++++++++-- spec/lib/gitlab/metrics/transaction_spec.rb | 12 ++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb index 43a7dab5323..0aaebf262d4 100644 --- a/lib/gitlab/metrics/transaction.rb +++ b/lib/gitlab/metrics/transaction.rb @@ -19,7 +19,8 @@ module Gitlab @started_at = nil @finished_at = nil - @tags = {} + @values = Hash.new(0) + @tags = {} end def duration @@ -44,6 +45,10 @@ module Gitlab @metrics << Metric.new(series, values, tags) end + def increment(name, value) + @values[name] += value + end + def add_tag(key, value) @tags[key] = value end @@ -54,7 +59,13 @@ module Gitlab end def track_self - add_metric(@series, { duration: duration }, @tags) + values = { duration: duration } + + @values.each do |name, value| + values[name] = value + end + + add_metric(@series, values, @tags) end def submit diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index 345163bfbea..18d63bdbdb9 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -38,6 +38,18 @@ describe Gitlab::Metrics::Transaction do end end + describe '#increment' do + it 'increments a counter' do + transaction.increment(:time, 1) + transaction.increment(:time, 2) + + expect(transaction).to receive(:add_metric). + with('rspec', { duration: 0.0, time: 3 }, {}) + + transaction.track_self + end + end + describe '#add_tag' do it 'adds a tag' do transaction.add_tag(:foo, 'bar') From 66a997a91403eef62ffd9fb789e899619d021a26 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 4 Jan 2016 12:14:36 +0100 Subject: [PATCH 066/280] Track total query/view timings in transactions --- config/initializers/metrics.rb | 1 + lib/gitlab/metrics/subscribers/action_view.rb | 1 + .../metrics/subscribers/active_record.rb | 22 ++++++++++++ .../metrics/subscribers/action_view_spec.rb | 3 ++ .../metrics/subscribers/active_record_spec.rb | 35 +++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 lib/gitlab/metrics/subscribers/active_record.rb create mode 100644 spec/lib/gitlab/metrics/subscribers/active_record_spec.rb diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index ebb20be7283..52ace27b7ae 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -6,6 +6,7 @@ if Gitlab::Metrics.enabled? # These are manually require'd so the classes are registered properly with # ActiveSupport. require 'gitlab/metrics/subscribers/action_view' + require 'gitlab/metrics/subscribers/active_record' Gitlab::Application.configure do |config| config.middleware.use(Gitlab::Metrics::RackMiddleware) diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb index 7e0dcf99d92..7c0105d543a 100644 --- a/lib/gitlab/metrics/subscribers/action_view.rb +++ b/lib/gitlab/metrics/subscribers/action_view.rb @@ -19,6 +19,7 @@ module Gitlab values = values_for(event) tags = tags_for(event) + current_transaction.increment(:view_duration, event.duration) current_transaction.add_metric(SERIES, values, tags) end diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb new file mode 100644 index 00000000000..8008b3bc895 --- /dev/null +++ b/lib/gitlab/metrics/subscribers/active_record.rb @@ -0,0 +1,22 @@ +module Gitlab + module Metrics + module Subscribers + # Class for tracking the total query duration of a transaction. + class ActiveRecord < ActiveSupport::Subscriber + attach_to :active_record + + def sql(event) + return unless current_transaction + + current_transaction.increment(:sql_duration, event.duration) + end + + private + + def current_transaction + Transaction.current + end + end + end + end +end diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb index bca76ca5a69..699d50f770a 100644 --- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb @@ -28,6 +28,9 @@ describe Gitlab::Metrics::Subscribers::ActionView do line: 4 } + expect(transaction).to receive(:increment). + with(:view_duration, 2.1) + expect(transaction).to receive(:add_metric). with(described_class::SERIES, values, tags) diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb new file mode 100644 index 00000000000..9ecedd934c6 --- /dev/null +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Gitlab::Metrics::Subscribers::ActiveRecord do + let(:transaction) { Gitlab::Metrics::Transaction.new('rspec') } + let(:subscriber) { described_class.new } + + let(:event) do + double(:event, duration: 0.2, + payload: { sql: 'SELECT * FROM users WHERE id = 10' }) + end + + describe '#sql' do + describe 'without a current transaction' do + it 'simply returns' do + expect_any_instance_of(Gitlab::Metrics::Transaction). + to_not receive(:increment) + + subscriber.sql(event) + end + end + + describe 'with a current transaction' do + it 'increments the :sql_duration value' do + expect(subscriber).to receive(:current_transaction). + at_least(:once). + and_return(transaction) + + expect(transaction).to receive(:increment). + with(:sql_duration, 0.2) + + subscriber.sql(event) + end + end + end +end From 825b46f8a3eb620f99192217d414b72dffe597d7 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 4 Jan 2016 12:19:45 +0100 Subject: [PATCH 067/280] Track total method call times per transaction This makes it easier to see where time is spent without having to aggregate all the individual points in the method_calls series. --- lib/gitlab/metrics/instrumentation.rb | 2 ++ spec/lib/gitlab/metrics/instrumentation_spec.rb | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb index 06fc2f25948..d9fce2e6758 100644 --- a/lib/gitlab/metrics/instrumentation.rb +++ b/lib/gitlab/metrics/instrumentation.rb @@ -123,6 +123,8 @@ module Gitlab duration = (Time.now - start) * 1000.0 if duration >= Gitlab::Metrics.method_call_threshold + trans.increment(:method_duration, duration) + trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES, { duration: duration }, method: #{label.inspect}) diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index a9003d8796b..af020f652be 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -48,6 +48,9 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) + expect(transaction).to receive(:increment). + with(:method_duration, a_kind_of(Numeric)) + expect(transaction).to receive(:add_metric). with(described_class::SERIES, an_instance_of(Hash), method: 'Dummy.foo') @@ -102,6 +105,9 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) + expect(transaction).to receive(:increment). + with(:method_duration, a_kind_of(Numeric)) + expect(transaction).to receive(:add_metric). with(described_class::SERIES, an_instance_of(Hash), method: 'Dummy#bar') From 2ea464bb272fa52ff34a188a921f0bc90811ca45 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 4 Jan 2016 12:45:31 +0100 Subject: [PATCH 068/280] Use separate series for Rails/Sidekiq sample stats This removes the need for any tags to differentiate between Sidekiq and Rails statistics while still being able to separate the two. --- lib/gitlab/metrics/sampler.rb | 19 +++++++++---- spec/lib/gitlab/metrics/sampler_spec.rb | 38 +++++++++++++++++++------ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/lib/gitlab/metrics/sampler.rb b/lib/gitlab/metrics/sampler.rb index 998578e1c0a..1ea425bc904 100644 --- a/lib/gitlab/metrics/sampler.rb +++ b/lib/gitlab/metrics/sampler.rb @@ -50,12 +50,11 @@ module Gitlab end def sample_memory_usage - @metrics << Metric.new('memory_usage', value: System.memory_usage) + add_metric('memory_usage', value: System.memory_usage) end def sample_file_descriptors - @metrics << Metric. - new('file_descriptors', value: System.file_descriptor_count) + add_metric('file_descriptors', value: System.file_descriptor_count) end if Metrics.mri? @@ -69,7 +68,7 @@ module Gitlab counts['Symbol'] = Symbol.all_symbols.length counts.each do |name, count| - @metrics << Metric.new('object_counts', { count: count }, type: name) + add_metric('object_counts', { count: count }, type: name) end end else @@ -91,7 +90,17 @@ module Gitlab stats[:count] = stats[:minor_gc_count] + stats[:major_gc_count] - @metrics << Metric.new('gc_statistics', stats) + add_metric('gc_statistics', stats) + end + + def add_metric(series, values, tags = {}) + prefix = sidekiq? ? 'sidekiq_' : 'rails_' + + @metrics << Metric.new("#{prefix}#{series}", values, tags) + end + + def sidekiq? + Sidekiq.server? end end end diff --git a/spec/lib/gitlab/metrics/sampler_spec.rb b/spec/lib/gitlab/metrics/sampler_spec.rb index 51a941c48cd..27211350fbe 100644 --- a/spec/lib/gitlab/metrics/sampler_spec.rb +++ b/spec/lib/gitlab/metrics/sampler_spec.rb @@ -51,8 +51,8 @@ describe Gitlab::Metrics::Sampler do expect(Gitlab::Metrics::System).to receive(:memory_usage). and_return(9000) - expect(Gitlab::Metrics::Metric).to receive(:new). - with('memory_usage', value: 9000). + expect(sampler).to receive(:add_metric). + with(/memory_usage/, value: 9000). and_call_original sampler.sample_memory_usage @@ -64,8 +64,8 @@ describe Gitlab::Metrics::Sampler do expect(Gitlab::Metrics::System).to receive(:file_descriptor_count). and_return(4) - expect(Gitlab::Metrics::Metric).to receive(:new). - with('file_descriptors', value: 4). + expect(sampler).to receive(:add_metric). + with(/file_descriptors/, value: 4). and_call_original sampler.sample_file_descriptors @@ -74,8 +74,8 @@ describe Gitlab::Metrics::Sampler do describe '#sample_objects' do it 'adds a metric containing the amount of allocated objects' do - expect(Gitlab::Metrics::Metric).to receive(:new). - with('object_counts', an_instance_of(Hash), an_instance_of(Hash)). + expect(sampler).to receive(:add_metric). + with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)). at_least(:once). and_call_original @@ -87,11 +87,33 @@ describe Gitlab::Metrics::Sampler do it 'adds a metric containing garbage collection statistics' do expect(GC::Profiler).to receive(:total_time).and_return(0.24) - expect(Gitlab::Metrics::Metric).to receive(:new). - with('gc_statistics', an_instance_of(Hash)). + expect(sampler).to receive(:add_metric). + with(/gc_statistics/, an_instance_of(Hash)). and_call_original sampler.sample_gc end end + + describe '#add_metric' do + it 'prefixes the series name for a Rails process' do + expect(sampler).to receive(:sidekiq?).and_return(false) + + expect(Gitlab::Metrics::Metric).to receive(:new). + with('rails_cats', { value: 10 }, {}). + and_call_original + + sampler.add_metric('cats', value: 10) + end + + it 'prefixes the series name for a Sidekiq process' do + expect(sampler).to receive(:sidekiq?).and_return(true) + + expect(Gitlab::Metrics::Metric).to receive(:new). + with('sidekiq_cats', { value: 10 }, {}). + and_call_original + + sampler.add_metric('cats', value: 10) + end + end end From 4c5be69583a61b9cce40b43dc93f00e1ae909680 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 4 Jan 2016 14:06:48 +0200 Subject: [PATCH 069/280] catch Emoji encode error --- 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 80e2741b09a..c12456a187f 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -99,7 +99,7 @@ module IssuesHelper end def emoji_icon(name, unicode = nil, aliases = []) - unicode ||= Emoji.emoji_filename(name) + unicode ||= Emoji.emoji_filename(name) rescue "" content_tag :div, "", class: "icon emoji-icon emoji-#{unicode}", From 2ee8f555996baca6b470d223ffad65419b730398 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 4 Jan 2016 13:17:02 +0100 Subject: [PATCH 070/280] Automatically prefix transaction series names This ensures Rails and Sidekiq transactions are split into the series "rails_transactions" and "sidekiq_transactions" respectively. --- lib/gitlab/metrics/rack_middleware.rb | 4 +--- lib/gitlab/metrics/sidekiq_middleware.rb | 4 +--- lib/gitlab/metrics/transaction.rb | 15 +++++++++------ spec/lib/gitlab/metrics/instrumentation_spec.rb | 2 +- .../lib/gitlab/metrics/sidekiq_middleware_spec.rb | 2 +- .../metrics/subscribers/action_view_spec.rb | 2 +- .../metrics/subscribers/active_record_spec.rb | 2 +- spec/lib/gitlab/metrics/transaction_spec.rb | 8 ++++---- 8 files changed, 19 insertions(+), 20 deletions(-) diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb index bb9e4fcb918..5c0587c4c51 100644 --- a/lib/gitlab/metrics/rack_middleware.rb +++ b/lib/gitlab/metrics/rack_middleware.rb @@ -4,8 +4,6 @@ module Gitlab class RackMiddleware CONTROLLER_KEY = 'action_controller.instance' - SERIES = 'rails_transactions' - def initialize(app) @app = app end @@ -32,7 +30,7 @@ module Gitlab end def transaction_from_env(env) - trans = Transaction.new(SERIES) + trans = Transaction.new trans.add_tag(:request_method, env['REQUEST_METHOD']) trans.add_tag(:request_uri, env['REQUEST_URI']) diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb index 6e804dd2562..ad441decfa2 100644 --- a/lib/gitlab/metrics/sidekiq_middleware.rb +++ b/lib/gitlab/metrics/sidekiq_middleware.rb @@ -4,10 +4,8 @@ module Gitlab # # This middleware is intended to be used as a server-side middleware. class SidekiqMiddleware - SERIES = 'sidekiq_transactions' - def call(worker, message, queue) - trans = Transaction.new(SERIES) + trans = Transaction.new begin trans.run { yield } diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb index 0aaebf262d4..68b86de0655 100644 --- a/lib/gitlab/metrics/transaction.rb +++ b/lib/gitlab/metrics/transaction.rb @@ -10,9 +10,7 @@ module Gitlab Thread.current[THREAD_KEY] end - # name - The name of this transaction as a String. - def initialize(series) - @series = series + def initialize @metrics = [] @uuid = SecureRandom.uuid @@ -40,9 +38,10 @@ module Gitlab end def add_metric(series, values, tags = {}) - tags = tags.merge(transaction_id: @uuid) + tags = tags.merge(transaction_id: @uuid) + prefix = sidekiq? ? 'sidekiq_' : 'rails_' - @metrics << Metric.new(series, values, tags) + @metrics << Metric.new("#{prefix}#{series}", values, tags) end def increment(name, value) @@ -65,12 +64,16 @@ module Gitlab values[name] = value end - add_metric(@series, values, @tags) + add_metric('transactions', values, @tags) end def submit Metrics.submit_metrics(@metrics.map(&:to_hash)) end + + def sidekiq? + Sidekiq.server? + end end end end diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index af020f652be..2a37cd40dde 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Metrics::Instrumentation do - let(:transaction) { Gitlab::Metrics::Transaction.new('rspec') } + let(:transaction) { Gitlab::Metrics::Transaction.new } before do @dummy = Class.new do diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb index 5fda6de52f4..5882e7d81c7 100644 --- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::Metrics::SidekiqMiddleware do describe '#tag_worker' do it 'adds the worker class and action to the transaction' do - trans = Gitlab::Metrics::Transaction.new('rspec') + trans = Gitlab::Metrics::Transaction.new worker = double(:worker, class: double(:class, name: 'TestWorker')) expect(trans).to receive(:add_tag).with(:action, 'TestWorker#perform') diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb index 699d50f770a..05e4fbbeb51 100644 --- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Metrics::Subscribers::ActionView do - let(:transaction) { Gitlab::Metrics::Transaction.new('rspec') } + let(:transaction) { Gitlab::Metrics::Transaction.new } let(:subscriber) { described_class.new } diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index 9ecedd934c6..81f35ba5d40 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Metrics::Subscribers::ActiveRecord do - let(:transaction) { Gitlab::Metrics::Transaction.new('rspec') } + let(:transaction) { Gitlab::Metrics::Transaction.new } let(:subscriber) { described_class.new } let(:event) do diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index 18d63bdbdb9..b9b94947afa 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Metrics::Transaction do - let(:transaction) { described_class.new('rspec') } + let(:transaction) { described_class.new } describe '#duration' do it 'returns the duration of a transaction in seconds' do @@ -32,7 +32,7 @@ describe Gitlab::Metrics::Transaction do describe '#add_metric' do it 'adds a metric tagged with the transaction UUID' do expect(Gitlab::Metrics::Metric).to receive(:new). - with('foo', { number: 10 }, { transaction_id: transaction.uuid }) + with('rails_foo', { number: 10 }, { transaction_id: transaction.uuid }) transaction.add_metric('foo', number: 10) end @@ -44,7 +44,7 @@ describe Gitlab::Metrics::Transaction do transaction.increment(:time, 2) expect(transaction).to receive(:add_metric). - with('rspec', { duration: 0.0, time: 3 }, {}) + with('transactions', { duration: 0.0, time: 3 }, {}) transaction.track_self end @@ -70,7 +70,7 @@ describe Gitlab::Metrics::Transaction do describe '#track_self' do it 'adds a metric for the transaction itself' do expect(transaction).to receive(:add_metric). - with('rspec', { duration: transaction.duration }, {}) + with('transactions', { duration: transaction.duration }, {}) transaction.track_self end From 567dc62b6dd114ac129eb2f45baa8155f5f11a51 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 30 Dec 2015 12:25:42 -0200 Subject: [PATCH 071/280] Show 'All' tab by default in the builds page --- CHANGELOG | 2 + app/controllers/admin/builds_controller.rb | 6 +- app/controllers/projects/builds_controller.rb | 6 +- app/views/admin/builds/index.html.haml | 10 +- app/views/projects/builds/index.html.haml | 12 +- spec/features/admin/admin_builds_spec.rb | 115 +++++++++++------- spec/features/builds_spec.rb | 23 ++-- 7 files changed, 105 insertions(+), 69 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b25e0eabd7d..a0d5039905b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,8 @@ v 8.4.0 (unreleased) - Revert back upvote and downvote button to the issue and MR pages - Swap position of Assignee and Author selector on Issuables (Zeger-Jan van de Weg) - Fix version check image in Safari + - Enable "Add key" button when user fills in a proper key (Stan Hu) + - Show 'All' tab by default in the builds page v 8.3.3 (unreleased) - Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu) diff --git a/app/controllers/admin/builds_controller.rb b/app/controllers/admin/builds_controller.rb index 83d9684c706..0db91eaaf2e 100644 --- a/app/controllers/admin/builds_controller.rb +++ b/app/controllers/admin/builds_controller.rb @@ -5,12 +5,12 @@ class Admin::BuildsController < Admin::ApplicationController @builds = @all_builds.order('created_at DESC') @builds = case @scope - when 'all' - @builds + when 'running' + @builds.running_or_pending.reverse_order when 'finished' @builds.finished else - @builds.running_or_pending.reverse_order + @builds end @builds = @builds.page(params[:page]).per(30) end diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 26ba12520c7..39d3ba26ba2 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -12,12 +12,12 @@ class Projects::BuildsController < Projects::ApplicationController @builds = @all_builds.order('created_at DESC') @builds = case @scope - when 'all' - @builds + when 'running' + @builds.running_or_pending.reverse_order when 'finished' @builds.finished else - @builds.running_or_pending.reverse_order + @builds end @builds = @builds.page(params[:page]).per(30) end diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index 52c36af6225..ddd4e1481eb 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -7,6 +7,11 @@ %ul.center-top-menu %li{class: ('active' if @scope.nil?)} = link_to admin_builds_path do + All + %span.badge.js-totalbuilds-count= @all_builds.count(:id) + + %li{class: ('active' if @scope == 'running')} + = link_to admin_builds_path(scope: :running) do Running %span.badge.js-running-count= number_with_delimiter(@all_builds.running_or_pending.count(:id)) @@ -15,11 +20,6 @@ Finished %span.badge.js-running-count= number_with_delimiter(@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= number_with_delimiter(@all_builds.count(:id)) - .gray-content-block #{(@scope || 'running').capitalize} builds diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 1a26908ab11..2fa5ad80fda 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -11,6 +11,12 @@ %ul.center-top-menu %li{class: ('active' if @scope.nil?)} = link_to project_builds_path(@project) do + All + %span.badge.js-totalbuilds-count + = number_with_delimiter(@all_builds.count(:id)) + + %li{class: ('active' if @scope == 'running')} + = link_to project_builds_path(@project, scope: :running) do Running %span.badge.js-running-count = number_with_delimiter(@all_builds.running_or_pending.count(:id)) @@ -21,12 +27,6 @@ %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 - = number_with_delimiter(@all_builds.count(:id)) - .gray-content-block #{(@scope || 'running').capitalize} builds from this project diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index 72764b1629d..b955d0b0c46 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -1,69 +1,98 @@ require 'spec_helper' -describe "Admin Builds" do - let(:commit) { FactoryGirl.create :ci_commit } - let(:build) { FactoryGirl.create :ci_build, commit: commit } - +describe 'Admin Builds' do before do login_as :admin end - describe "GET /admin/builds" do - before do - build - visit admin_builds_path - end + describe 'GET /admin/builds' do + let(:commit) { create(:ci_commit) } - it { expect(page).to have_content "Running" } - it { expect(page).to have_content build.short_sha } - end + context 'All tab' do + context 'when have builds' do + it 'shows all builds' do + create(:ci_build, commit: commit, status: :pending) + create(:ci_build, commit: commit, status: :running) + create(:ci_build, commit: commit, status: :success) + create(:ci_build, commit: commit, status: :failed) - describe "Tabs" do - it "shows all builds" do - FactoryGirl.create :ci_build, commit: commit, status: "pending" - FactoryGirl.create :ci_build, commit: commit, status: "running" - FactoryGirl.create :ci_build, commit: commit, status: "success" - FactoryGirl.create :ci_build, commit: commit, status: "failed" + visit admin_builds_path - visit admin_builds_path - - within ".center-top-menu" do - click_on "All" + expect(page).to have_selector('.project-issuable-filter li.active', text: 'All') + expect(page.all('.build-link').size).to eq(4) + expect(page).to have_link 'Cancel all' + end end - expect(page.all(".build-link").size).to eq(4) + context 'when have no builds' do + it 'shows a message' do + visit admin_builds_path + + expect(page).to have_selector('.project-issuable-filter li.active', text: 'All') + expect(page).to have_content 'No builds to show' + expect(page).not_to have_link 'Cancel all' + end + end end - 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" + context 'Running tab' do + context 'when have running builds' do + it 'shows running builds' do + build1 = create(:ci_build, commit: commit, status: :pending) + build2 = create(:ci_build, commit: commit, status: :success) + build3 = create(:ci_build, commit: commit, status: :failed) - visit admin_builds_path + visit admin_builds_path(scope: :running) - within ".center-top-menu" do - click_on "Finished" + expect(page).to have_selector('.project-issuable-filter li.active', text: 'Running') + expect(page.find('.build-link')).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).to have_link 'Cancel all' + end end - 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")).to have_content(build2.id) + context 'when have no builds running' do + it 'shows a message' do + create(:ci_build, commit: commit, status: :success) + + visit admin_builds_path(scope: :running) + + expect(page).to have_selector('.project-issuable-filter li.active', text: 'Running') + expect(page).to have_content 'No builds to show' + expect(page).not_to have_link 'Cancel all' + end + end end - it "shows running builds" do - build = FactoryGirl.create :ci_build, commit: commit, status: "pending" - build2 = FactoryGirl.create :ci_build, commit: commit, status: "success" - build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed" + context 'Finished tab' do + context 'when have finished builds' do + it 'shows finished builds' do + build1 = create(:ci_build, commit: commit, status: :pending) + build2 = create(:ci_build, commit: commit, status: :running) + build3 = create(:ci_build, commit: commit, status: :success) - visit admin_builds_path + visit admin_builds_path(scope: :finished) - within ".center-top-menu" do - click_on "Running" + expect(page).to have_selector('.project-issuable-filter li.active', text: 'Finished') + 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')).to have_content(build3.id) + expect(page).to have_link 'Cancel all' + end end - 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) + context 'when have no builds finished' do + it 'shows a message' do + create(:ci_build, commit: commit, status: :running) + + visit admin_builds_path(scope: :finished) + + expect(page).to have_selector('.project-issuable-filter li.active', text: 'Finished') + expect(page).to have_content 'No builds to show' + expect(page).to have_link 'Cancel all' + end + end end end end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index f0031a0a247..240e56839df 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -15,11 +15,11 @@ describe "Builds" do context "Running scope" do before do @build.run! - visit namespace_project_builds_path(@project.namespace, @project) + visit namespace_project_builds_path(@project.namespace, @project, scope: :running) end - it { expect(page).to have_content 'Running' } - it { expect(page).to have_content 'Cancel running' } + it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'Running') } + it { expect(page).to have_link '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 } @@ -31,21 +31,22 @@ describe "Builds" do visit namespace_project_builds_path(@project.namespace, @project, scope: :finished) end + it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'Finished') } it { expect(page).to have_content 'No builds to show' } - it { expect(page).to have_content 'Cancel running' } + it { expect(page).to have_link 'Cancel running' } end context "All builds" do before do @project.builds.running_or_pending.each(&:success) - visit namespace_project_builds_path(@project.namespace, @project, scope: :all) + visit namespace_project_builds_path(@project.namespace, @project) end - it { expect(page).to have_content 'All' } + it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'All') } 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 running' } + it { expect(page).to_not have_link 'Cancel running' } end end @@ -56,8 +57,12 @@ describe "Builds" do click_link "Cancel running" end - it { expect(page).to have_content 'No builds to show' } - it { expect(page).to_not have_content 'Cancel running' } + it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'All') } + it { expect(page).to have_content 'canceled' } + 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_link 'Cancel running' } end describe "GET /:project/builds/:id" do From 8de491a68fcb130d436d2c85c0fda900381875cf Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 4 Jan 2016 14:21:39 +0100 Subject: [PATCH 072/280] Fix Rubocop styling in AR subscriber specs --- spec/lib/gitlab/metrics/subscribers/active_record_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index 81f35ba5d40..7bc070a4d09 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do let(:event) do double(:event, duration: 0.2, - payload: { sql: 'SELECT * FROM users WHERE id = 10' }) + payload: { sql: 'SELECT * FROM users WHERE id = 10' }) end describe '#sql' do From 4b027bc93a7875c3937f6b90ac1049b4a4d72da5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 4 Jan 2016 14:29:06 +0100 Subject: [PATCH 073/280] Add DEBUG_BANZAI_CACHE env var to debug Banzai cache issue. --- lib/banzai/renderer.rb | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index 115ae914524..910e1c6994e 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -1,7 +1,5 @@ module Banzai module Renderer - CACHE_ENABLED = false - # Convert a Markdown String into an HTML-safe String of HTML # # Note that while the returned HTML will have been sanitized of dangerous @@ -20,13 +18,22 @@ module Banzai cache_key = context.delete(:cache_key) cache_key = full_cache_key(cache_key, context[:pipeline]) - if cache_key && CACHE_ENABLED - Rails.cache.fetch(cache_key) do - cacheless_render(text, context) + cacheless = cacheless_render(text, context) + + if cache_key && ENV["DEBUG_BANZAI_CACHE"] + cached = Rails.cache.fetch(cache_key) { cacheless } + + if cached != cacheless + Rails.logger.warn "Banzai cache mismatch" + Rails.logger.warn "Text: #{text.inspect}" + Rails.logger.warn "Context: #{context.inspect}" + Rails.logger.warn "Cache key: #{cache_key.inspect}" + Rails.logger.warn "Cacheless: #{cacheless.inspect}" + Rails.logger.warn "With cache: #{cached.inspect}" end - else - cacheless_render(text, context) end + + cacheless end def self.render_result(text, context = {}) From 79ec7f289748ca5812d51c3a61e3a2f9c2464fda Mon Sep 17 00:00:00 2001 From: Steve Norman Date: Tue, 5 May 2015 15:13:11 +0000 Subject: [PATCH 074/280] Added system hooks messages for renaming and transferring a project --- CHANGELOG | 1 + app/models/project.rb | 6 +++ app/services/projects/transfer_service.rb | 3 ++ app/services/system_hooks_service.rb | 11 ++++- doc/system_hooks/system_hooks.md | 50 +++++++++++++++++++++- spec/services/system_hooks_service_spec.rb | 47 ++++++++++++++------ 6 files changed, 102 insertions(+), 16 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b25e0eabd7d..d0e065ac6c2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ v 8.4.0 (unreleased) - Add link to merge request on build detail page - Revert back upvote and downvote button to the issue and MR pages - Swap position of Assignee and Author selector on Issuables (Zeger-Jan van de Weg) + - Add system hook messages for project rename and transfer (Steve Norman) - Fix version check image in Safari v 8.3.3 (unreleased) diff --git a/app/models/project.rb b/app/models/project.rb index 017471995ec..eadc42d1da5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -81,6 +81,7 @@ class Project < ActiveRecord::Base acts_as_taggable_on :tags attr_accessor :new_default_branch + attr_accessor :old_path_with_namespace # Relations belongs_to :creator, foreign_key: 'creator_id', class_name: 'User' @@ -701,6 +702,11 @@ 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 + + @old_path_with_namespace = old_path_with_namespace + + SystemHooksService.new.execute_hooks_for(self, :rename) + @repository = nil rescue # Returning false does not rollback after_* transaction but gives diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 64ea6dd42eb..2e734654466 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -55,6 +55,9 @@ module Projects # Move uploads Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path) + project.old_path_with_namespace = old_path + + SystemHooksService.new.execute_hooks_for(project, :transfer) true end end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 8b5143e1eb7..6dc854ec33d 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -18,7 +18,8 @@ class SystemHooksService def build_event_data(model, event) data = { event_name: build_event_name(model, event), - created_at: model.created_at.xmlschema + created_at: model.created_at.xmlschema, + updated_at: model.updated_at.xmlschema } case model @@ -34,6 +35,14 @@ class SystemHooksService end when Project data.merge!(project_data(model)) + + if event == :rename || event == :transfer + data.merge!({ + old_path_with_namespace: model.old_path_with_namespace + }) + end + + data when User data.merge!({ name: model.name, diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index 5cb05b13b3e..49f98ded046 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -1,6 +1,6 @@ # System hooks -Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`. +Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `project_rename`, `project_transfer`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`. System hooks can be used, e.g. for logging or changing information in a LDAP server. @@ -17,6 +17,7 @@ X-Gitlab-Event: System Hook ```json { "created_at": "2012-07-21T07:30:54Z", + "updated_at": "2012-07-21T07:38:22Z", "event_name": "project_create", "name": "StoreCloud", "owner_email": "johnsmith@gmail.com", @@ -33,6 +34,7 @@ X-Gitlab-Event: System Hook ```json { "created_at": "2012-07-21T07:30:58Z", + "updated_at": "2012-07-21T07:38:22Z", "event_name": "project_destroy", "name": "Underscore", "owner_email": "johnsmith@gmail.com", @@ -44,11 +46,48 @@ X-Gitlab-Event: System Hook } ``` +**Project renamed:** + +```json +{ + "created_at": "2012-07-21T07:30:58Z", + "updated_at": "2012-07-21T07:38:22Z", + "event_name": "project_rename", + "name": "Underscore", + "path": "underscore", + "path_with_namespace": "jsmith/underscore", + "project_id": 73, + "owner_name": "John Smith", + "owner_email": "johnsmith@gmail.com", + "project_visibility": "internal", + "old_path_with_namespace": "jsmith/overscore", +} +``` + +**Project transferred:** + +```json +{ + "created_at": "2012-07-21T07:30:58Z", + "updated_at": "2012-07-21T07:38:22Z", + "event_name": "project_transfer", + "name": "Underscore", + "path": "underscore", + "path_with_namespace": "scores/underscore", + "project_id": 73, + "owner_name": "John Smith", + "owner_email": "johnsmith@gmail.com", + "project_visibility": "internal", + "old_path_with_namespace": "jsmith/overscore", +} +``` + **New Team Member:** ```json { "created_at": "2012-07-21T07:30:56Z", + "updated_at": "2012-07-21T07:38:22Z", "event_name": "user_add_to_team", "project_access": "Master", "project_id": 74, @@ -67,6 +106,7 @@ X-Gitlab-Event: System Hook ```json { "created_at": "2012-07-21T07:30:56Z", + "updated_at": "2012-07-21T07:38:22Z", "event_name": "user_remove_from_team", "project_access": "Master", "project_id": 74, @@ -85,6 +125,7 @@ X-Gitlab-Event: System Hook ```json { "created_at": "2012-07-21T07:44:07Z", + "updated_at": "2012-07-21T07:38:22Z", "email": "js@gitlabhq.com", "event_name": "user_create", "name": "John Smith", @@ -97,6 +138,7 @@ X-Gitlab-Event: System Hook ```json { "created_at": "2012-07-21T07:44:07Z", + "updated_at": "2012-07-21T07:38:22Z", "email": "js@gitlabhq.com", "event_name": "user_destroy", "name": "John Smith", @@ -110,6 +152,7 @@ X-Gitlab-Event: System Hook { "event_name": "key_create", "created_at": "2014-08-18 18:45:16 UTC", + "updated_at": "2012-07-21T07:38:22Z", "username": "root", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC58FwqHUbebw2SdT7SP4FxZ0w+lAO/erhy2ylhlcW/tZ3GY3mBu9VeeiSGoGz8hCx80Zrz+aQv28xfFfKlC8XQFpCWwsnWnQqO2Lv9bS8V1fIHgMxOHIt5Vs+9CAWGCCvUOAurjsUDoE2ALIXLDMKnJxcxD13XjWdK54j6ZXDB4syLF0C2PnAQSVY9X7MfCYwtuFmhQhKaBussAXpaVMRHltie3UYSBUUuZaB3J4cg/7TxlmxcNd+ppPRIpSZAB0NI6aOnqoBCpimscO/VpQRJMVLr3XiSYeT6HBiDXWHnIVPfQc03OGcaFqOit6p8lYKMaP/iUQLm+pgpZqrXZ9vB john@localhost", "id": 4 @@ -122,6 +165,7 @@ X-Gitlab-Event: System Hook { "event_name": "key_destroy", "created_at": "2014-08-18 18:45:16 UTC", + "updated_at": "2012-07-21T07:38:22Z", "username": "root", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC58FwqHUbebw2SdT7SP4FxZ0w+lAO/erhy2ylhlcW/tZ3GY3mBu9VeeiSGoGz8hCx80Zrz+aQv28xfFfKlC8XQFpCWwsnWnQqO2Lv9bS8V1fIHgMxOHIt5Vs+9CAWGCCvUOAurjsUDoE2ALIXLDMKnJxcxD13XjWdK54j6ZXDB4syLF0C2PnAQSVY9X7MfCYwtuFmhQhKaBussAXpaVMRHltie3UYSBUUuZaB3J4cg/7TxlmxcNd+ppPRIpSZAB0NI6aOnqoBCpimscO/VpQRJMVLr3XiSYeT6HBiDXWHnIVPfQc03OGcaFqOit6p8lYKMaP/iUQLm+pgpZqrXZ9vB john@localhost", "id": 4 @@ -133,6 +177,7 @@ X-Gitlab-Event: System Hook ```json { "created_at": "2012-07-21T07:30:54Z", + "updated_at": "2012-07-21T07:38:22Z", "event_name": "group_create", "name": "StoreCloud", "owner_email": "johnsmith@gmail.com", @@ -147,6 +192,7 @@ X-Gitlab-Event: System Hook ```json { "created_at": "2012-07-21T07:30:54Z", + "updated_at": "2012-07-21T07:38:22Z", "event_name": "group_destroy", "name": "StoreCloud", "owner_email": "johnsmith@gmail.com", @@ -161,6 +207,7 @@ X-Gitlab-Event: System Hook ```json { "created_at": "2012-07-21T07:30:56Z", + "updated_at": "2012-07-21T07:38:22Z", "event_name": "user_add_to_group", "group_access": "Master", "group_id": 78, @@ -176,6 +223,7 @@ X-Gitlab-Event: System Hook ```json { "created_at": "2012-07-21T07:30:56Z", + "updated_at": "2012-07-21T07:38:22Z", "event_name": "user_remove_from_group", "group_access": "Master", "group_id": 78, diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index febc78d2784..4455ae7b321 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -9,37 +9,54 @@ describe SystemHooksService, services: true do let(:group_member) { create(:group_member) } context 'event data' do - it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :email, :user_id) } - it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :email, :user_id) } - it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) } - it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) } + it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id) } + it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id) } + it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } + it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } + it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) } + it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) } it { expect(event_data(key, :create)).to include(:username, :key, :id) } it { expect(event_data(key, :destroy)).to include(:username, :key, :id) } + it do + project.old_path_with_namespace = 'renamed_from_path' + expect(event_data(project, :rename)).to include( + :event_name, :name, :created_at, :updated_at, :path, :project_id, + :owner_name, :owner_email, :project_visibility, + :old_path_with_namespace + ) + end + it do + project.old_path_with_namespace = 'transfered_from_path' + expect(event_data(project, :transfer)).to include( + :event_name, :name, :created_at, :updated_at, :path, :project_id, + :owner_name, :owner_email, :project_visibility, + :old_path_with_namespace + ) + end + it do expect(event_data(group, :create)).to include( - :event_name, :name, :created_at, :path, :group_id, :owner_name, - :owner_email + :event_name, :name, :created_at, :updated_at, :path, :group_id, + :owner_name, :owner_email ) end it do expect(event_data(group, :destroy)).to include( - :event_name, :name, :created_at, :path, :group_id, :owner_name, - :owner_email + :event_name, :name, :created_at, :updated_at, :path, :group_id, + :owner_name, :owner_email ) end it do expect(event_data(group_member, :create)).to include( - :event_name, :created_at, :group_name, :group_path, :group_id, :user_id, - :user_name, :user_email, :group_access + :event_name, :created_at, :updated_at, :group_name, :group_path, + :group_id, :user_id, :user_name, :user_email, :group_access ) end it do expect(event_data(group_member, :destroy)).to include( - :event_name, :created_at, :group_name, :group_path, :group_id, :user_id, - :user_name, :user_email, :group_access + :event_name, :created_at, :updated_at, :group_name, :group_path, + :group_id, :user_id, :user_name, :user_email, :group_access ) end end @@ -49,6 +66,8 @@ describe SystemHooksService, services: true do it { expect(event_name(user, :destroy)).to eq "user_destroy" } it { expect(event_name(project, :create)).to eq "project_create" } it { expect(event_name(project, :destroy)).to eq "project_destroy" } + it { expect(event_name(project, :rename)).to eq "project_rename" } + it { expect(event_name(project, :transfer)).to eq "project_transfer" } it { expect(event_name(project_member, :create)).to eq "user_add_to_team" } it { expect(event_name(project_member, :destroy)).to eq "user_remove_from_team" } it { expect(event_name(key, :create)).to eq 'key_create' } From 8b1844912561a7e6dd0cc361ea1514f2a340e275 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 28 Dec 2015 13:32:18 +0200 Subject: [PATCH 075/280] remove public field from namespace and refactoring --- app/controllers/explore/groups_controller.rb | 2 +- app/controllers/users_controller.rb | 2 +- app/finders/groups_finder.rb | 44 ----------------- app/finders/joined_groups_finder.rb | 49 ------------------- app/helpers/search_helper.rb | 2 +- app/models/ability.rb | 2 +- app/models/group.rb | 8 --- app/views/groups/edit.html.haml | 9 ---- app/views/groups/show.html.haml | 4 +- ...1228111122_remove_public_from_namespace.rb | 6 +++ db/schema.rb | 8 ++- features/explore/groups.feature | 15 ------ spec/finders/groups_finder_spec.rb | 48 ------------------ spec/finders/joined_groups_finder_spec.rb | 49 ------------------- spec/helpers/search_helper_spec.rb | 2 +- spec/models/group_spec.rb | 27 ---------- 16 files changed, 16 insertions(+), 261 deletions(-) delete mode 100644 app/finders/groups_finder.rb delete mode 100644 app/finders/joined_groups_finder.rb create mode 100644 db/migrate/20151228111122_remove_public_from_namespace.rb delete mode 100644 spec/finders/groups_finder_spec.rb delete mode 100644 spec/finders/joined_groups_finder_spec.rb diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb index 9575a87ee41..a9bf4321f73 100644 --- a/app/controllers/explore/groups_controller.rb +++ b/app/controllers/explore/groups_controller.rb @@ -1,6 +1,6 @@ class Explore::GroupsController < Explore::ApplicationController def index - @groups = GroupsFinder.new.execute(current_user) + @groups = Group.order_id_desc @groups = @groups.search(params[:search]) if params[:search].present? @groups = @groups.sort(@sort = params[:sort]) @groups = @groups.page(params[:page]).per(PER_PAGE) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 30cb869eb2a..280228dbcc0 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -7,7 +7,7 @@ class UsersController < ApplicationController @projects = PersonalProjectsFinder.new(@user).execute(current_user) - @groups = JoinedGroupsFinder.new(@user).execute(current_user) + @groups = @user.groups.order_id_desc respond_to do |format| format.html diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb deleted file mode 100644 index 91cb0f228f0..00000000000 --- a/app/finders/groups_finder.rb +++ /dev/null @@ -1,44 +0,0 @@ -class GroupsFinder - # Finds the groups available to the given user. - # - # current_user - The user to find the groups for. - # - # Returns an ActiveRecord::Relation. - def execute(current_user = nil) - if current_user - relation = groups_visible_to_user(current_user) - else - relation = public_groups - end - - relation.order_id_desc - end - - private - - # This method returns the groups "current_user" can see. - def groups_visible_to_user(current_user) - base = groups_for_projects(public_and_internal_projects) - - union = Gitlab::SQL::Union. - new([base.select(:id), current_user.authorized_groups.select(:id)]) - - Group.where("namespaces.id IN (#{union.to_sql})") - end - - def public_groups - groups_for_projects(public_projects) - end - - def groups_for_projects(projects) - Group.public_and_given_groups(projects.select(:namespace_id)) - end - - def public_projects - Project.unscoped.public_only - end - - def public_and_internal_projects - Project.unscoped.public_and_internal_only - end -end diff --git a/app/finders/joined_groups_finder.rb b/app/finders/joined_groups_finder.rb deleted file mode 100644 index e7523136fea..00000000000 --- a/app/finders/joined_groups_finder.rb +++ /dev/null @@ -1,49 +0,0 @@ -# Class for finding the groups a user is a member of. -class JoinedGroupsFinder - def initialize(user = nil) - @user = user - end - - # Finds the groups of the source user, optionally limited to those visible to - # the current user. - # - # current_user - If given the groups of "@user" will only include the groups - # "current_user" can also see. - # - # Returns an ActiveRecord::Relation. - def execute(current_user = nil) - if current_user - relation = groups_visible_to_user(current_user) - else - relation = public_groups - end - - relation.order_id_desc - end - - private - - # Returns the groups the user in "current_user" can see. - # - # This list includes all public/internal projects as well as the projects of - # "@user" that "current_user" also has access to. - def groups_visible_to_user(current_user) - base = @user.authorized_groups.visible_to_user(current_user) - extra = public_and_internal_groups - union = Gitlab::SQL::Union.new([base.select(:id), extra.select(:id)]) - - Group.where("namespaces.id IN (#{union.to_sql})") - end - - def public_groups - groups_for_projects(@user.authorized_projects.public_only) - end - - def public_and_internal_groups - groups_for_projects(@user.authorized_projects.public_and_internal_only) - end - - def groups_for_projects(projects) - @user.groups.public_and_given_groups(projects.select(:namespace_id)) - end -end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index a6ee6880247..d4f78258626 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -70,7 +70,7 @@ module SearchHelper # Autocomplete results for the current user's groups def groups_autocomplete(term, limit = 5) - GroupsFinder.new.execute(current_user).search(term).limit(limit).map do |group| + Group.search(term).limit(limit).map do |group| { label: "group: #{search_result_sanitize(group.name)}", url: group_path(group) diff --git a/app/models/ability.rb b/app/models/ability.rb index 1b3ee757040..5a1a67db8e1 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -69,7 +69,7 @@ class Ability subject.group end - if group && group.public_profile? + if group && group.projects.public_only.any? [:read_group] else [] diff --git a/app/models/group.rb b/app/models/group.rb index 1b5b875a19e..b8f2ab6ae5d 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -50,10 +50,6 @@ class Group < Namespace User.reference_pattern end - def public_and_given_groups(ids) - where('public IS TRUE OR namespaces.id IN (?)', ids) - end - def visible_to_user(user) where(id: user.authorized_groups.select(:id).reorder(nil)) end @@ -125,10 +121,6 @@ class Group < Namespace end end - def public_profile? - self.public || projects.public_only.any? - end - def post_create_hook Gitlab::AppLogger.info("Group \"#{name}\" was created") diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 1dea77c2e96..7e3e2e28bc9 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -24,15 +24,6 @@ %hr = link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" - .form-group - %hr - = f.label :public, class: 'control-label' do - Public - .col-sm-10 - .checkbox - = f.check_box :public - %span.descr Make this group public (even if there are no public projects inside this group) - .form-actions = f.submit 'Save group', class: "btn btn-save" diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index c2c7c581b3e..a607d860d7d 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -47,5 +47,5 @@ = render "projects", projects: @projects - else - %p - This group does not have public projects + %p.center-top-menu.no-top + No projects to show diff --git a/db/migrate/20151228111122_remove_public_from_namespace.rb b/db/migrate/20151228111122_remove_public_from_namespace.rb new file mode 100644 index 00000000000..f4c848bbf47 --- /dev/null +++ b/db/migrate/20151228111122_remove_public_from_namespace.rb @@ -0,0 +1,6 @@ +# Migration type: online +class RemovePublicFromNamespace < ActiveRecord::Migration + def change + remove_column :namespaces, :public, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index df7f72d5ad4..7a6d34b8153 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -547,22 +547,20 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree create_table "namespaces", force: :cascade do |t| - t.string "name", null: false - t.string "path", null: false + t.string "name", null: false + t.string "path", null: false t.integer "owner_id" t.datetime "created_at" t.datetime "updated_at" t.string "type" - t.string "description", default: "", null: false + t.string "description", default: "", null: false t.string "avatar" - t.boolean "public", default: false end add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree - 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: :cascade do |t| diff --git a/features/explore/groups.feature b/features/explore/groups.feature index a42e59c98f2..5fc9b135601 100644 --- a/features/explore/groups.feature +++ b/features/explore/groups.feature @@ -105,15 +105,6 @@ Feature: Explore Groups When I visit the public groups area Then I should see group "TestGroup" - Scenario: I should not see group with internal project in public groups area - Given group "TestGroup" has internal project "Internal" - When I visit the public groups area - Then I should not see group "TestGroup" - - Scenario: I should not see group with private project in public groups area - When I visit the public groups area - Then I should not see group "TestGroup" - Scenario: I should see group with public project in public groups area as user Given group "TestGroup" has public project "Community" When I sign in as a user @@ -125,9 +116,3 @@ Feature: Explore Groups When I sign in as a user And I visit the public groups area Then I should see group "TestGroup" - - Scenario: I should not see group with private project in public groups area as user - When I sign in as a user - And I visit the public groups area - Then I should not see group "TestGroup" - diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb deleted file mode 100644 index 4f6a000822e..00000000000 --- a/spec/finders/groups_finder_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'spec_helper' - -describe GroupsFinder do - describe '#execute' do - let(:user) { create(:user) } - - let(:group1) { create(:group) } - let(:group2) { create(:group) } - let(:group3) { create(:group) } - let(:group4) { create(:group, public: true) } - - let!(:public_project) { create(:project, :public, group: group1) } - let!(:internal_project) { create(:project, :internal, group: group2) } - let!(:private_project) { create(:project, :private, group: group3) } - - let(:finder) { described_class.new } - - describe 'with a user' do - subject { finder.execute(user) } - - describe 'when the user is not a member of any groups' do - it { is_expected.to eq([group4, group2, group1]) } - end - - describe 'when the user is a member of a group' do - before do - group3.add_user(user, Gitlab::Access::DEVELOPER) - end - - it { is_expected.to eq([group4, group3, group2, group1]) } - end - - describe 'when the user is a member of a private project' do - before do - private_project.team.add_user(user, Gitlab::Access::DEVELOPER) - end - - it { is_expected.to eq([group4, group3, group2, group1]) } - end - end - - describe 'without a user' do - subject { finder.execute } - - it { is_expected.to eq([group4, group1]) } - end - end -end diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb deleted file mode 100644 index 2d9068cc720..00000000000 --- a/spec/finders/joined_groups_finder_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'spec_helper' - -describe JoinedGroupsFinder do - describe '#execute' do - let(:source_user) { create(:user) } - let(:current_user) { create(:user) } - - let(:group1) { create(:group) } - let(:group2) { create(:group) } - let(:group3) { create(:group) } - let(:group4) { create(:group, public: true) } - - let!(:public_project) { create(:project, :public, group: group1) } - let!(:internal_project) { create(:project, :internal, group: group2) } - let!(:private_project) { create(:project, :private, group: group3) } - - let(:finder) { described_class.new(source_user) } - - before do - [group1, group2, group3, group4].each do |group| - group.add_user(source_user, Gitlab::Access::MASTER) - end - end - - describe 'with a current user' do - describe 'when the current user has access to the projects of the source user' do - before do - private_project.team.add_user(current_user, Gitlab::Access::DEVELOPER) - end - - subject { finder.execute(current_user) } - - it { is_expected.to eq([group4, group3, group2, group1]) } - end - - describe 'when the current user does not have access to the projects of the source user' do - subject { finder.execute(current_user) } - - it { is_expected.to eq([group4, group2, group1]) } - end - end - - describe 'without a current user' do - subject { finder.execute } - - it { is_expected.to eq([group4, group1]) } - end - end -end diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index ebe9c29d91c..f0d553f5f1d 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -43,7 +43,7 @@ describe SearchHelper do end it "includes the public group" do - group = create(:group, public: true) + group = create(:group) expect(search_autocomplete_opts(group.name).size).to eq(1) end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 646f767e6fe..ba5acceadff 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -38,14 +38,6 @@ describe Group, models: true do it { is_expected.not_to validate_presence_of :owner } end - describe '.public_and_given_groups' do - let!(:public_group) { create(:group, public: true) } - - subject { described_class.public_and_given_groups([group.id]) } - - it { is_expected.to eq([public_group, group]) } - end - describe '.visible_to_user' do let!(:group) { create(:group) } let!(:user) { create(:user) } @@ -112,23 +104,4 @@ describe Group, models: true do expect(group.avatar_type).to eq(["only images allowed"]) end end - - describe "public_profile?" do - it "returns true for public group" do - group = create(:group, public: true) - expect(group.public_profile?).to be_truthy - end - - it "returns true for non-public group with public project" do - group = create(:group) - create(:project, :public, group: group) - expect(group.public_profile?).to be_truthy - end - - it "returns false for non-public group with no public projects" do - group = create(:group) - create(:project, group: group) - expect(group.public_profile?).to be_falsy - end - end end From 712af98e9c11b28a11fb31fc86af5ef83a5485cb Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 4 Jan 2016 09:07:50 -0500 Subject: [PATCH 076/280] removes footer message about access to project --- app/views/projects/show.html.haml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 5d04776f7f4..ffbe445b447 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -68,11 +68,4 @@ = render 'projects/last_commit', commit: @repository.commit, project: @project %div{class: "project-show-#{default_project_view}"} - = render default_project_view - -- if current_user - - access = user_max_access_in_project(current_user.id, @project) - - if access - .prepend-top-20.project-footer - .gray-content-block.footer-block.center - You have #{access} access to this project. \ No newline at end of file + = render default_project_view \ No newline at end of file From 770517d3e7f9926957b384084ab726651bfecaea Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 4 Jan 2016 09:42:10 -0500 Subject: [PATCH 077/280] Revert "Merge branch 'rs-remove-jquery-blockui' into 'master' " This reverts commit bc12750fcc7a8637771e1449493cdd3cee9ebd64, reversing changes made to 8a04b84e09c4318ac46808d0debc4997b52a2314. --- app/assets/javascripts/application.js.coffee | 1 + vendor/assets/javascripts/jquery.blockUI.js | 590 +++++++++++++++++++ 2 files changed, 591 insertions(+) create mode 100644 vendor/assets/javascripts/jquery.blockUI.js diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index b9b095e004a..7d3f18fcdbe 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -13,6 +13,7 @@ #= require jquery.waitforimages #= require jquery.atwho #= require jquery.scrollTo +#= require jquery.blockUI #= require jquery.turbolinks #= require d3 #= require cal-heatmap diff --git a/vendor/assets/javascripts/jquery.blockUI.js b/vendor/assets/javascripts/jquery.blockUI.js new file mode 100644 index 00000000000..c8702d79b65 --- /dev/null +++ b/vendor/assets/javascripts/jquery.blockUI.js @@ -0,0 +1,590 @@ +/*! + * jQuery blockUI plugin + * Version 2.60.0-2013.04.05 + * @requires jQuery v1.7 or later + * + * Examples at: http://malsup.com/jquery/block/ + * Copyright (c) 2007-2013 M. Alsup + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * Thanks to Amir-Hossein Sobhi for some excellent contributions! + */ + +;(function() { +/*jshint eqeqeq:false curly:false latedef:false */ +"use strict"; + + function setup($) { + $.fn._fadeIn = $.fn.fadeIn; + + var noOp = $.noop || function() {}; + + // this bit is to ensure we don't call setExpression when we shouldn't (with extra muscle to handle + // retarded userAgent strings on Vista) + var msie = /MSIE/.test(navigator.userAgent); + var ie6 = /MSIE 6.0/.test(navigator.userAgent) && ! /MSIE 8.0/.test(navigator.userAgent); + var mode = document.documentMode || 0; + var setExpr = $.isFunction( document.createElement('div').style.setExpression ); + + // global $ methods for blocking/unblocking the entire page + $.blockUI = function(opts) { install(window, opts); }; + $.unblockUI = function(opts) { remove(window, opts); }; + + // convenience method for quick growl-like notifications (http://www.google.com/search?q=growl) + $.growlUI = function(title, message, timeout, onClose) { + var $m = $('
'); + if (title) $m.append('

'+title+'

'); + if (message) $m.append('

'+message+'

'); + if (timeout === undefined) timeout = 3000; + $.blockUI({ + message: $m, fadeIn: 700, fadeOut: 1000, centerY: false, + timeout: timeout, showOverlay: false, + onUnblock: onClose, + css: $.blockUI.defaults.growlCSS + }); + }; + + // plugin method for blocking element content + $.fn.block = function(opts) { + if ( this[0] === window ) { + $.blockUI( opts ); + return this; + } + var fullOpts = $.extend({}, $.blockUI.defaults, opts || {}); + this.each(function() { + var $el = $(this); + if (fullOpts.ignoreIfBlocked && $el.data('blockUI.isBlocked')) + return; + $el.unblock({ fadeOut: 0 }); + }); + + return this.each(function() { + if ($.css(this,'position') == 'static') { + this.style.position = 'relative'; + $(this).data('blockUI.static', true); + } + this.style.zoom = 1; // force 'hasLayout' in ie + install(this, opts); + }); + }; + + // plugin method for unblocking element content + $.fn.unblock = function(opts) { + if ( this[0] === window ) { + $.unblockUI( opts ); + return this; + } + return this.each(function() { + remove(this, opts); + }); + }; + + $.blockUI.version = 2.60; // 2nd generation blocking at no extra cost! + + // override these in your code to change the default behavior and style + $.blockUI.defaults = { + // message displayed when blocking (use null for no message) + message: '

Please wait...

', + + title: null, // title string; only used when theme == true + draggable: true, // only used when theme == true (requires jquery-ui.js to be loaded) + + theme: false, // set to true to use with jQuery UI themes + + // styles for the message when blocking; if you wish to disable + // these and use an external stylesheet then do this in your code: + // $.blockUI.defaults.css = {}; + css: { + padding: 0, + margin: 0, + width: '30%', + top: '40%', + left: '35%', + textAlign: 'center', + color: '#000', + border: '3px solid #aaa', + backgroundColor:'#fff', + cursor: 'wait' + }, + + // minimal style set used when themes are used + themedCSS: { + width: '30%', + top: '40%', + left: '35%' + }, + + // styles for the overlay + overlayCSS: { + backgroundColor: '#000', + opacity: 0.6, + cursor: 'wait' + }, + + // style to replace wait cursor before unblocking to correct issue + // of lingering wait cursor + cursorReset: 'default', + + // styles applied when using $.growlUI + growlCSS: { + width: '350px', + top: '10px', + left: '', + right: '10px', + border: 'none', + padding: '5px', + opacity: 0.6, + cursor: 'default', + color: '#fff', + backgroundColor: '#000', + '-webkit-border-radius':'10px', + '-moz-border-radius': '10px', + 'border-radius': '10px' + }, + + // IE issues: 'about:blank' fails on HTTPS and javascript:false is s-l-o-w + // (hat tip to Jorge H. N. de Vasconcelos) + /*jshint scripturl:true */ + iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank', + + // force usage of iframe in non-IE browsers (handy for blocking applets) + forceIframe: false, + + // z-index for the blocking overlay + baseZ: 1000, + + // set these to true to have the message automatically centered + centerX: true, // <-- only effects element blocking (page block controlled via css above) + centerY: true, + + // allow body element to be stetched in ie6; this makes blocking look better + // on "short" pages. disable if you wish to prevent changes to the body height + allowBodyStretch: true, + + // enable if you want key and mouse events to be disabled for content that is blocked + bindEvents: true, + + // be default blockUI will supress tab navigation from leaving blocking content + // (if bindEvents is true) + constrainTabKey: true, + + // fadeIn time in millis; set to 0 to disable fadeIn on block + fadeIn: 200, + + // fadeOut time in millis; set to 0 to disable fadeOut on unblock + fadeOut: 400, + + // time in millis to wait before auto-unblocking; set to 0 to disable auto-unblock + timeout: 0, + + // disable if you don't want to show the overlay + showOverlay: true, + + // if true, focus will be placed in the first available input field when + // page blocking + focusInput: true, + + // elements that can receive focus + focusableElements: ':input:enabled:visible', + + // suppresses the use of overlay styles on FF/Linux (due to performance issues with opacity) + // no longer needed in 2012 + // applyPlatformOpacityRules: true, + + // callback method invoked when fadeIn has completed and blocking message is visible + onBlock: null, + + // callback method invoked when unblocking has completed; the callback is + // passed the element that has been unblocked (which is the window object for page + // blocks) and the options that were passed to the unblock call: + // onUnblock(element, options) + onUnblock: null, + + // callback method invoked when the overlay area is clicked. + // setting this will turn the cursor to a pointer, otherwise cursor defined in overlayCss will be used. + onOverlayClick: null, + + // don't ask; if you really must know: http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493 + quirksmodeOffsetHack: 4, + + // class name of the message block + blockMsgClass: 'blockMsg', + + // if it is already blocked, then ignore it (don't unblock and reblock) + ignoreIfBlocked: false + }; + + // private data and functions follow... + + var pageBlock = null; + var pageBlockEls = []; + + function install(el, opts) { + var css, themedCSS; + var full = (el == window); + var msg = (opts && opts.message !== undefined ? opts.message : undefined); + opts = $.extend({}, $.blockUI.defaults, opts || {}); + + if (opts.ignoreIfBlocked && $(el).data('blockUI.isBlocked')) + return; + + opts.overlayCSS = $.extend({}, $.blockUI.defaults.overlayCSS, opts.overlayCSS || {}); + css = $.extend({}, $.blockUI.defaults.css, opts.css || {}); + if (opts.onOverlayClick) + opts.overlayCSS.cursor = 'pointer'; + + themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, opts.themedCSS || {}); + msg = msg === undefined ? opts.message : msg; + + // remove the current block (if there is one) + if (full && pageBlock) + remove(window, {fadeOut:0}); + + // if an existing element is being used as the blocking content then we capture + // its current place in the DOM (and current display style) so we can restore + // it when we unblock + if (msg && typeof msg != 'string' && (msg.parentNode || msg.jquery)) { + var node = msg.jquery ? msg[0] : msg; + var data = {}; + $(el).data('blockUI.history', data); + data.el = node; + data.parent = node.parentNode; + data.display = node.style.display; + data.position = node.style.position; + if (data.parent) + data.parent.removeChild(node); + } + + $(el).data('blockUI.onUnblock', opts.onUnblock); + var z = opts.baseZ; + + // blockUI uses 3 layers for blocking, for simplicity they are all used on every platform; + // layer1 is the iframe layer which is used to supress bleed through of underlying content + // layer2 is the overlay layer which has opacity and a wait cursor (by default) + // layer3 is the message content that is displayed while blocking + var lyr1, lyr2, lyr3, s; + if (msie || opts.forceIframe) + lyr1 = $(''); + else + lyr1 = $(''); + + if (opts.theme) + lyr2 = $(''); + else + lyr2 = $(''); + + if (opts.theme && full) { + s = ''; + } + else if (opts.theme) { + s = ''; + } + else if (full) { + s = ''; + } + else { + s = ''; + } + lyr3 = $(s); + + // if we have a message, style it + if (msg) { + if (opts.theme) { + lyr3.css(themedCSS); + lyr3.addClass('ui-widget-content'); + } + else + lyr3.css(css); + } + + // style the overlay + if (!opts.theme /*&& (!opts.applyPlatformOpacityRules)*/) + lyr2.css(opts.overlayCSS); + lyr2.css('position', full ? 'fixed' : 'absolute'); + + // make iframe layer transparent in IE + if (msie || opts.forceIframe) + lyr1.css('opacity',0.0); + + //$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el); + var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') : $(el); + $.each(layers, function() { + this.appendTo($par); + }); + + if (opts.theme && opts.draggable && $.fn.draggable) { + lyr3.draggable({ + handle: '.ui-dialog-titlebar', + cancel: 'li' + }); + } + + // ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling) + var expr = setExpr && (!$.support.boxModel || $('object,embed', full ? null : el).length > 0); + if (ie6 || expr) { + // give body 100% height + if (full && opts.allowBodyStretch && $.support.boxModel) + $('html,body').css('height','100%'); + + // fix ie6 issue when blocked element has a border width + if ((ie6 || !$.support.boxModel) && !full) { + var t = sz(el,'borderTopWidth'), l = sz(el,'borderLeftWidth'); + var fixT = t ? '(0 - '+t+')' : 0; + var fixL = l ? '(0 - '+l+')' : 0; + } + + // simulate fixed position + $.each(layers, function(i,o) { + var s = o[0].style; + s.position = 'absolute'; + if (i < 2) { + if (full) + s.setExpression('height','Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.support.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"'); + else + s.setExpression('height','this.parentNode.offsetHeight + "px"'); + if (full) + s.setExpression('width','jQuery.support.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"'); + else + s.setExpression('width','this.parentNode.offsetWidth + "px"'); + if (fixL) s.setExpression('left', fixL); + if (fixT) s.setExpression('top', fixT); + } + else if (opts.centerY) { + if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"'); + s.marginTop = 0; + } + else if (!opts.centerY && full) { + var top = (opts.css && opts.css.top) ? parseInt(opts.css.top, 10) : 0; + var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + '+top+') + "px"'; + s.setExpression('top',expression); + } + }); + } + + // show the message + if (msg) { + if (opts.theme) + lyr3.find('.ui-widget-content').append(msg); + else + lyr3.append(msg); + if (msg.jquery || msg.nodeType) + $(msg).show(); + } + + if ((msie || opts.forceIframe) && opts.showOverlay) + lyr1.show(); // opacity is zero + if (opts.fadeIn) { + var cb = opts.onBlock ? opts.onBlock : noOp; + var cb1 = (opts.showOverlay && !msg) ? cb : noOp; + var cb2 = msg ? cb : noOp; + if (opts.showOverlay) + lyr2._fadeIn(opts.fadeIn, cb1); + if (msg) + lyr3._fadeIn(opts.fadeIn, cb2); + } + else { + if (opts.showOverlay) + lyr2.show(); + if (msg) + lyr3.show(); + if (opts.onBlock) + opts.onBlock(); + } + + // bind key and mouse events + bind(1, el, opts); + + if (full) { + pageBlock = lyr3[0]; + pageBlockEls = $(opts.focusableElements,pageBlock); + if (opts.focusInput) + setTimeout(focus, 20); + } + else + center(lyr3[0], opts.centerX, opts.centerY); + + if (opts.timeout) { + // auto-unblock + var to = setTimeout(function() { + if (full) + $.unblockUI(opts); + else + $(el).unblock(opts); + }, opts.timeout); + $(el).data('blockUI.timeout', to); + } + } + + // remove the block + function remove(el, opts) { + var count; + var full = (el == window); + var $el = $(el); + var data = $el.data('blockUI.history'); + var to = $el.data('blockUI.timeout'); + if (to) { + clearTimeout(to); + $el.removeData('blockUI.timeout'); + } + opts = $.extend({}, $.blockUI.defaults, opts || {}); + bind(0, el, opts); // unbind events + + if (opts.onUnblock === null) { + opts.onUnblock = $el.data('blockUI.onUnblock'); + $el.removeData('blockUI.onUnblock'); + } + + var els; + if (full) // crazy selector to handle odd field errors in ie6/7 + els = $('body').children().filter('.blockUI').add('body > .blockUI'); + else + els = $el.find('>.blockUI'); + + // fix cursor issue + if ( opts.cursorReset ) { + if ( els.length > 1 ) + els[1].style.cursor = opts.cursorReset; + if ( els.length > 2 ) + els[2].style.cursor = opts.cursorReset; + } + + if (full) + pageBlock = pageBlockEls = null; + + if (opts.fadeOut) { + count = els.length; + els.fadeOut(opts.fadeOut, function() { + if ( --count === 0) + reset(els,data,opts,el); + }); + } + else + reset(els, data, opts, el); + } + + // move blocking element back into the DOM where it started + function reset(els,data,opts,el) { + var $el = $(el); + els.each(function(i,o) { + // remove via DOM calls so we don't lose event handlers + if (this.parentNode) + this.parentNode.removeChild(this); + }); + + if (data && data.el) { + data.el.style.display = data.display; + data.el.style.position = data.position; + if (data.parent) + data.parent.appendChild(data.el); + $el.removeData('blockUI.history'); + } + + if ($el.data('blockUI.static')) { + $el.css('position', 'static'); // #22 + } + + if (typeof opts.onUnblock == 'function') + opts.onUnblock(el,opts); + + // fix issue in Safari 6 where block artifacts remain until reflow + var body = $(document.body), w = body.width(), cssW = body[0].style.width; + body.width(w-1).width(w); + body[0].style.width = cssW; + } + + // bind/unbind the handler + function bind(b, el, opts) { + var full = el == window, $el = $(el); + + // don't bother unbinding if there is nothing to unbind + if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked'))) + return; + + $el.data('blockUI.isBlocked', b); + + // don't bind events when overlay is not in use or if bindEvents is false + if (!full || !opts.bindEvents || (b && !opts.showOverlay)) + return; + + // bind anchors and inputs for mouse and key events + var events = 'mousedown mouseup keydown keypress keyup touchstart touchend touchmove'; + if (b) + $(document).bind(events, opts, handler); + else + $(document).unbind(events, handler); + + // former impl... + // var $e = $('a,:input'); + // b ? $e.bind(events, opts, handler) : $e.unbind(events, handler); + } + + // event handler to suppress keyboard/mouse events when blocking + function handler(e) { + // allow tab navigation (conditionally) + if (e.keyCode && e.keyCode == 9) { + if (pageBlock && e.data.constrainTabKey) { + var els = pageBlockEls; + var fwd = !e.shiftKey && e.target === els[els.length-1]; + var back = e.shiftKey && e.target === els[0]; + if (fwd || back) { + setTimeout(function(){focus(back);},10); + return false; + } + } + } + var opts = e.data; + var target = $(e.target); + if (target.hasClass('blockOverlay') && opts.onOverlayClick) + opts.onOverlayClick(); + + // allow events within the message content + if (target.parents('div.' + opts.blockMsgClass).length > 0) + return true; + + // allow events for content that is not being blocked + return target.parents().children().filter('div.blockUI').length === 0; + } + + function focus(back) { + if (!pageBlockEls) + return; + var e = pageBlockEls[back===true ? pageBlockEls.length-1 : 0]; + if (e) + e.focus(); + } + + function center(el, x, y) { + var p = el.parentNode, s = el.style; + var l = ((p.offsetWidth - el.offsetWidth)/2) - sz(p,'borderLeftWidth'); + var t = ((p.offsetHeight - el.offsetHeight)/2) - sz(p,'borderTopWidth'); + if (x) s.left = l > 0 ? (l+'px') : '0'; + if (y) s.top = t > 0 ? (t+'px') : '0'; + } + + function sz(el, p) { + return parseInt($.css(el,p),10)||0; + } + + } + + + /*global define:true */ + if (typeof define === 'function' && define.amd && define.amd.jQuery) { + define(['jquery'], setup); + } else { + setup(jQuery); + } + +})(); From bb6b793c55150ecbe072456bc4b151191764b642 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Mon, 4 Jan 2016 11:19:39 -0400 Subject: [PATCH 078/280] Don't log backtrace in Asana service --- app/models/project_services/asana_service.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index cb4f6ddb3a5..7d367e40037 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -118,7 +118,6 @@ http://app.asana.com/-/account_api' end rescue => e Rails.logger.error(e.message) - Rails.logger.error(e.backtrace.join("\n")) next end end From 618056031827727ff1928b125569dae2e05f9bd1 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 4 Jan 2016 10:36:53 -0500 Subject: [PATCH 079/280] rempves tests for "you have master access" text --- spec/features/projects_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 4836b3b9b14..9a01c89ae2a 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -80,7 +80,6 @@ feature 'Project', feature: true do visit namespace_project_path(project.namespace, project) end - it { expect(page).to have_content('You have Master access to this project.') } it 'click project-settings and find leave project' do find('#project-settings-button').click expect(page).to have_link('Leave Project') From ad42441d2dc98dbf385e54c352a6a5a38155b223 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 1 Jan 2016 17:52:40 -0500 Subject: [PATCH 080/280] Remove jquery.blockUI.js plugin It was only used to block the issue (but not merge request) list when the sort was changed. --- app/assets/javascripts/application.js.coffee | 1 - app/assets/javascripts/issues.js.coffee | 7 - vendor/assets/javascripts/jquery.blockUI.js | 590 ------------------- 3 files changed, 598 deletions(-) delete mode 100644 vendor/assets/javascripts/jquery.blockUI.js diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 7d3f18fcdbe..b9b095e004a 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -13,7 +13,6 @@ #= require jquery.waitforimages #= require jquery.atwho #= require jquery.scrollTo -#= require jquery.blockUI #= require jquery.turbolinks #= require d3 #= require cal-heatmap diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index 35d34b20fae..a0acf3028bf 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -15,13 +15,6 @@ $(this).html totalIssues + 1 else $(this).html totalIssues - 1 - $("body").on "click", ".issues-other-filters .dropdown-menu a", -> - $('.issues-list').block( - message: null, - overlayCSS: - backgroundColor: '#DDD' - opacity: .4 - ) reload: -> Issues.initSelects() diff --git a/vendor/assets/javascripts/jquery.blockUI.js b/vendor/assets/javascripts/jquery.blockUI.js deleted file mode 100644 index c8702d79b65..00000000000 --- a/vendor/assets/javascripts/jquery.blockUI.js +++ /dev/null @@ -1,590 +0,0 @@ -/*! - * jQuery blockUI plugin - * Version 2.60.0-2013.04.05 - * @requires jQuery v1.7 or later - * - * Examples at: http://malsup.com/jquery/block/ - * Copyright (c) 2007-2013 M. Alsup - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - * Thanks to Amir-Hossein Sobhi for some excellent contributions! - */ - -;(function() { -/*jshint eqeqeq:false curly:false latedef:false */ -"use strict"; - - function setup($) { - $.fn._fadeIn = $.fn.fadeIn; - - var noOp = $.noop || function() {}; - - // this bit is to ensure we don't call setExpression when we shouldn't (with extra muscle to handle - // retarded userAgent strings on Vista) - var msie = /MSIE/.test(navigator.userAgent); - var ie6 = /MSIE 6.0/.test(navigator.userAgent) && ! /MSIE 8.0/.test(navigator.userAgent); - var mode = document.documentMode || 0; - var setExpr = $.isFunction( document.createElement('div').style.setExpression ); - - // global $ methods for blocking/unblocking the entire page - $.blockUI = function(opts) { install(window, opts); }; - $.unblockUI = function(opts) { remove(window, opts); }; - - // convenience method for quick growl-like notifications (http://www.google.com/search?q=growl) - $.growlUI = function(title, message, timeout, onClose) { - var $m = $('
'); - if (title) $m.append('

'+title+'

'); - if (message) $m.append('

'+message+'

'); - if (timeout === undefined) timeout = 3000; - $.blockUI({ - message: $m, fadeIn: 700, fadeOut: 1000, centerY: false, - timeout: timeout, showOverlay: false, - onUnblock: onClose, - css: $.blockUI.defaults.growlCSS - }); - }; - - // plugin method for blocking element content - $.fn.block = function(opts) { - if ( this[0] === window ) { - $.blockUI( opts ); - return this; - } - var fullOpts = $.extend({}, $.blockUI.defaults, opts || {}); - this.each(function() { - var $el = $(this); - if (fullOpts.ignoreIfBlocked && $el.data('blockUI.isBlocked')) - return; - $el.unblock({ fadeOut: 0 }); - }); - - return this.each(function() { - if ($.css(this,'position') == 'static') { - this.style.position = 'relative'; - $(this).data('blockUI.static', true); - } - this.style.zoom = 1; // force 'hasLayout' in ie - install(this, opts); - }); - }; - - // plugin method for unblocking element content - $.fn.unblock = function(opts) { - if ( this[0] === window ) { - $.unblockUI( opts ); - return this; - } - return this.each(function() { - remove(this, opts); - }); - }; - - $.blockUI.version = 2.60; // 2nd generation blocking at no extra cost! - - // override these in your code to change the default behavior and style - $.blockUI.defaults = { - // message displayed when blocking (use null for no message) - message: '

Please wait...

', - - title: null, // title string; only used when theme == true - draggable: true, // only used when theme == true (requires jquery-ui.js to be loaded) - - theme: false, // set to true to use with jQuery UI themes - - // styles for the message when blocking; if you wish to disable - // these and use an external stylesheet then do this in your code: - // $.blockUI.defaults.css = {}; - css: { - padding: 0, - margin: 0, - width: '30%', - top: '40%', - left: '35%', - textAlign: 'center', - color: '#000', - border: '3px solid #aaa', - backgroundColor:'#fff', - cursor: 'wait' - }, - - // minimal style set used when themes are used - themedCSS: { - width: '30%', - top: '40%', - left: '35%' - }, - - // styles for the overlay - overlayCSS: { - backgroundColor: '#000', - opacity: 0.6, - cursor: 'wait' - }, - - // style to replace wait cursor before unblocking to correct issue - // of lingering wait cursor - cursorReset: 'default', - - // styles applied when using $.growlUI - growlCSS: { - width: '350px', - top: '10px', - left: '', - right: '10px', - border: 'none', - padding: '5px', - opacity: 0.6, - cursor: 'default', - color: '#fff', - backgroundColor: '#000', - '-webkit-border-radius':'10px', - '-moz-border-radius': '10px', - 'border-radius': '10px' - }, - - // IE issues: 'about:blank' fails on HTTPS and javascript:false is s-l-o-w - // (hat tip to Jorge H. N. de Vasconcelos) - /*jshint scripturl:true */ - iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank', - - // force usage of iframe in non-IE browsers (handy for blocking applets) - forceIframe: false, - - // z-index for the blocking overlay - baseZ: 1000, - - // set these to true to have the message automatically centered - centerX: true, // <-- only effects element blocking (page block controlled via css above) - centerY: true, - - // allow body element to be stetched in ie6; this makes blocking look better - // on "short" pages. disable if you wish to prevent changes to the body height - allowBodyStretch: true, - - // enable if you want key and mouse events to be disabled for content that is blocked - bindEvents: true, - - // be default blockUI will supress tab navigation from leaving blocking content - // (if bindEvents is true) - constrainTabKey: true, - - // fadeIn time in millis; set to 0 to disable fadeIn on block - fadeIn: 200, - - // fadeOut time in millis; set to 0 to disable fadeOut on unblock - fadeOut: 400, - - // time in millis to wait before auto-unblocking; set to 0 to disable auto-unblock - timeout: 0, - - // disable if you don't want to show the overlay - showOverlay: true, - - // if true, focus will be placed in the first available input field when - // page blocking - focusInput: true, - - // elements that can receive focus - focusableElements: ':input:enabled:visible', - - // suppresses the use of overlay styles on FF/Linux (due to performance issues with opacity) - // no longer needed in 2012 - // applyPlatformOpacityRules: true, - - // callback method invoked when fadeIn has completed and blocking message is visible - onBlock: null, - - // callback method invoked when unblocking has completed; the callback is - // passed the element that has been unblocked (which is the window object for page - // blocks) and the options that were passed to the unblock call: - // onUnblock(element, options) - onUnblock: null, - - // callback method invoked when the overlay area is clicked. - // setting this will turn the cursor to a pointer, otherwise cursor defined in overlayCss will be used. - onOverlayClick: null, - - // don't ask; if you really must know: http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493 - quirksmodeOffsetHack: 4, - - // class name of the message block - blockMsgClass: 'blockMsg', - - // if it is already blocked, then ignore it (don't unblock and reblock) - ignoreIfBlocked: false - }; - - // private data and functions follow... - - var pageBlock = null; - var pageBlockEls = []; - - function install(el, opts) { - var css, themedCSS; - var full = (el == window); - var msg = (opts && opts.message !== undefined ? opts.message : undefined); - opts = $.extend({}, $.blockUI.defaults, opts || {}); - - if (opts.ignoreIfBlocked && $(el).data('blockUI.isBlocked')) - return; - - opts.overlayCSS = $.extend({}, $.blockUI.defaults.overlayCSS, opts.overlayCSS || {}); - css = $.extend({}, $.blockUI.defaults.css, opts.css || {}); - if (opts.onOverlayClick) - opts.overlayCSS.cursor = 'pointer'; - - themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, opts.themedCSS || {}); - msg = msg === undefined ? opts.message : msg; - - // remove the current block (if there is one) - if (full && pageBlock) - remove(window, {fadeOut:0}); - - // if an existing element is being used as the blocking content then we capture - // its current place in the DOM (and current display style) so we can restore - // it when we unblock - if (msg && typeof msg != 'string' && (msg.parentNode || msg.jquery)) { - var node = msg.jquery ? msg[0] : msg; - var data = {}; - $(el).data('blockUI.history', data); - data.el = node; - data.parent = node.parentNode; - data.display = node.style.display; - data.position = node.style.position; - if (data.parent) - data.parent.removeChild(node); - } - - $(el).data('blockUI.onUnblock', opts.onUnblock); - var z = opts.baseZ; - - // blockUI uses 3 layers for blocking, for simplicity they are all used on every platform; - // layer1 is the iframe layer which is used to supress bleed through of underlying content - // layer2 is the overlay layer which has opacity and a wait cursor (by default) - // layer3 is the message content that is displayed while blocking - var lyr1, lyr2, lyr3, s; - if (msie || opts.forceIframe) - lyr1 = $(''); - else - lyr1 = $(''); - - if (opts.theme) - lyr2 = $(''); - else - lyr2 = $(''); - - if (opts.theme && full) { - s = ''; - } - else if (opts.theme) { - s = ''; - } - else if (full) { - s = ''; - } - else { - s = ''; - } - lyr3 = $(s); - - // if we have a message, style it - if (msg) { - if (opts.theme) { - lyr3.css(themedCSS); - lyr3.addClass('ui-widget-content'); - } - else - lyr3.css(css); - } - - // style the overlay - if (!opts.theme /*&& (!opts.applyPlatformOpacityRules)*/) - lyr2.css(opts.overlayCSS); - lyr2.css('position', full ? 'fixed' : 'absolute'); - - // make iframe layer transparent in IE - if (msie || opts.forceIframe) - lyr1.css('opacity',0.0); - - //$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el); - var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') : $(el); - $.each(layers, function() { - this.appendTo($par); - }); - - if (opts.theme && opts.draggable && $.fn.draggable) { - lyr3.draggable({ - handle: '.ui-dialog-titlebar', - cancel: 'li' - }); - } - - // ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling) - var expr = setExpr && (!$.support.boxModel || $('object,embed', full ? null : el).length > 0); - if (ie6 || expr) { - // give body 100% height - if (full && opts.allowBodyStretch && $.support.boxModel) - $('html,body').css('height','100%'); - - // fix ie6 issue when blocked element has a border width - if ((ie6 || !$.support.boxModel) && !full) { - var t = sz(el,'borderTopWidth'), l = sz(el,'borderLeftWidth'); - var fixT = t ? '(0 - '+t+')' : 0; - var fixL = l ? '(0 - '+l+')' : 0; - } - - // simulate fixed position - $.each(layers, function(i,o) { - var s = o[0].style; - s.position = 'absolute'; - if (i < 2) { - if (full) - s.setExpression('height','Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.support.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"'); - else - s.setExpression('height','this.parentNode.offsetHeight + "px"'); - if (full) - s.setExpression('width','jQuery.support.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"'); - else - s.setExpression('width','this.parentNode.offsetWidth + "px"'); - if (fixL) s.setExpression('left', fixL); - if (fixT) s.setExpression('top', fixT); - } - else if (opts.centerY) { - if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"'); - s.marginTop = 0; - } - else if (!opts.centerY && full) { - var top = (opts.css && opts.css.top) ? parseInt(opts.css.top, 10) : 0; - var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + '+top+') + "px"'; - s.setExpression('top',expression); - } - }); - } - - // show the message - if (msg) { - if (opts.theme) - lyr3.find('.ui-widget-content').append(msg); - else - lyr3.append(msg); - if (msg.jquery || msg.nodeType) - $(msg).show(); - } - - if ((msie || opts.forceIframe) && opts.showOverlay) - lyr1.show(); // opacity is zero - if (opts.fadeIn) { - var cb = opts.onBlock ? opts.onBlock : noOp; - var cb1 = (opts.showOverlay && !msg) ? cb : noOp; - var cb2 = msg ? cb : noOp; - if (opts.showOverlay) - lyr2._fadeIn(opts.fadeIn, cb1); - if (msg) - lyr3._fadeIn(opts.fadeIn, cb2); - } - else { - if (opts.showOverlay) - lyr2.show(); - if (msg) - lyr3.show(); - if (opts.onBlock) - opts.onBlock(); - } - - // bind key and mouse events - bind(1, el, opts); - - if (full) { - pageBlock = lyr3[0]; - pageBlockEls = $(opts.focusableElements,pageBlock); - if (opts.focusInput) - setTimeout(focus, 20); - } - else - center(lyr3[0], opts.centerX, opts.centerY); - - if (opts.timeout) { - // auto-unblock - var to = setTimeout(function() { - if (full) - $.unblockUI(opts); - else - $(el).unblock(opts); - }, opts.timeout); - $(el).data('blockUI.timeout', to); - } - } - - // remove the block - function remove(el, opts) { - var count; - var full = (el == window); - var $el = $(el); - var data = $el.data('blockUI.history'); - var to = $el.data('blockUI.timeout'); - if (to) { - clearTimeout(to); - $el.removeData('blockUI.timeout'); - } - opts = $.extend({}, $.blockUI.defaults, opts || {}); - bind(0, el, opts); // unbind events - - if (opts.onUnblock === null) { - opts.onUnblock = $el.data('blockUI.onUnblock'); - $el.removeData('blockUI.onUnblock'); - } - - var els; - if (full) // crazy selector to handle odd field errors in ie6/7 - els = $('body').children().filter('.blockUI').add('body > .blockUI'); - else - els = $el.find('>.blockUI'); - - // fix cursor issue - if ( opts.cursorReset ) { - if ( els.length > 1 ) - els[1].style.cursor = opts.cursorReset; - if ( els.length > 2 ) - els[2].style.cursor = opts.cursorReset; - } - - if (full) - pageBlock = pageBlockEls = null; - - if (opts.fadeOut) { - count = els.length; - els.fadeOut(opts.fadeOut, function() { - if ( --count === 0) - reset(els,data,opts,el); - }); - } - else - reset(els, data, opts, el); - } - - // move blocking element back into the DOM where it started - function reset(els,data,opts,el) { - var $el = $(el); - els.each(function(i,o) { - // remove via DOM calls so we don't lose event handlers - if (this.parentNode) - this.parentNode.removeChild(this); - }); - - if (data && data.el) { - data.el.style.display = data.display; - data.el.style.position = data.position; - if (data.parent) - data.parent.appendChild(data.el); - $el.removeData('blockUI.history'); - } - - if ($el.data('blockUI.static')) { - $el.css('position', 'static'); // #22 - } - - if (typeof opts.onUnblock == 'function') - opts.onUnblock(el,opts); - - // fix issue in Safari 6 where block artifacts remain until reflow - var body = $(document.body), w = body.width(), cssW = body[0].style.width; - body.width(w-1).width(w); - body[0].style.width = cssW; - } - - // bind/unbind the handler - function bind(b, el, opts) { - var full = el == window, $el = $(el); - - // don't bother unbinding if there is nothing to unbind - if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked'))) - return; - - $el.data('blockUI.isBlocked', b); - - // don't bind events when overlay is not in use or if bindEvents is false - if (!full || !opts.bindEvents || (b && !opts.showOverlay)) - return; - - // bind anchors and inputs for mouse and key events - var events = 'mousedown mouseup keydown keypress keyup touchstart touchend touchmove'; - if (b) - $(document).bind(events, opts, handler); - else - $(document).unbind(events, handler); - - // former impl... - // var $e = $('a,:input'); - // b ? $e.bind(events, opts, handler) : $e.unbind(events, handler); - } - - // event handler to suppress keyboard/mouse events when blocking - function handler(e) { - // allow tab navigation (conditionally) - if (e.keyCode && e.keyCode == 9) { - if (pageBlock && e.data.constrainTabKey) { - var els = pageBlockEls; - var fwd = !e.shiftKey && e.target === els[els.length-1]; - var back = e.shiftKey && e.target === els[0]; - if (fwd || back) { - setTimeout(function(){focus(back);},10); - return false; - } - } - } - var opts = e.data; - var target = $(e.target); - if (target.hasClass('blockOverlay') && opts.onOverlayClick) - opts.onOverlayClick(); - - // allow events within the message content - if (target.parents('div.' + opts.blockMsgClass).length > 0) - return true; - - // allow events for content that is not being blocked - return target.parents().children().filter('div.blockUI').length === 0; - } - - function focus(back) { - if (!pageBlockEls) - return; - var e = pageBlockEls[back===true ? pageBlockEls.length-1 : 0]; - if (e) - e.focus(); - } - - function center(el, x, y) { - var p = el.parentNode, s = el.style; - var l = ((p.offsetWidth - el.offsetWidth)/2) - sz(p,'borderLeftWidth'); - var t = ((p.offsetHeight - el.offsetHeight)/2) - sz(p,'borderTopWidth'); - if (x) s.left = l > 0 ? (l+'px') : '0'; - if (y) s.top = t > 0 ? (t+'px') : '0'; - } - - function sz(el, p) { - return parseInt($.css(el,p),10)||0; - } - - } - - - /*global define:true */ - if (typeof define === 'function' && define.amd && define.amd.jQuery) { - define(['jquery'], setup); - } else { - setup(jQuery); - } - -})(); From 12ce1cbfcfff3e28cae82d327b056644457f65f6 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 4 Jan 2016 12:11:13 +0100 Subject: [PATCH 081/280] Merge pull request GH-9938 from huacnlee/hotfix/note_mail_with_notification Hotfix note mail with notification --- app/mailers/emails/notes.rb | 2 +- spec/services/notification_service_spec.rb | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index 65f37e92677..e1382d2da12 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -48,7 +48,7 @@ module Emails yield - SentNotification.record(@note, recipient_id, reply_key) + SentNotification.record_note(@note, recipient_id, reply_key) end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index c103752198d..fc8cf425c3a 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -52,6 +52,9 @@ describe NotificationService, services: true do it do add_users_with_subscription(note.project, issue) + # Ensure create SentNotification by noteable = issue 6 times, not noteable = note + expect(SentNotification).to receive(:record).with(issue, any_args).exactly(6).times + ActionMailer::Base.deliveries.clear notification.new_note(note) From 10a9751ace57a368ebf39a1f8a3b172783cbeab1 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 4 Jan 2016 13:41:43 -0500 Subject: [PATCH 082/280] Update CHANGELOG [ci skip] --- CHANGELOG | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index a0d5039905b..ff82ba8e86f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,7 +14,6 @@ v 8.4.0 (unreleased) - Revert back upvote and downvote button to the issue and MR pages - Swap position of Assignee and Author selector on Issuables (Zeger-Jan van de Weg) - Fix version check image in Safari - - Enable "Add key" button when user fills in a proper key (Stan Hu) - Show 'All' tab by default in the builds page v 8.3.3 (unreleased) From 7df3c1e8eafcb071f23ac1f4142e81ce8c1d9def Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 4 Jan 2016 14:54:56 -0500 Subject: [PATCH 083/280] Correct the logo ID names Her left, not ours! --- app/assets/javascripts/logo.js.coffee | 8 ++++---- app/views/shared/_logo.svg | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee index 47135a6c5eb..a8f97bdf3d7 100644 --- a/app/assets/javascripts/logo.js.coffee +++ b/app/assets/javascripts/logo.js.coffee @@ -2,11 +2,11 @@ NProgress.configure(showSpinner: false) defaultClass = 'tanuki-shape' pieces = [ - 'path#tanuki-left-cheek', - 'path#tanuki-left-eye, path#tanuki-left-ear', - 'path#tanuki-nose', - 'path#tanuki-right-eye, path#tanuki-right-ear', 'path#tanuki-right-cheek', + 'path#tanuki-right-eye, path#tanuki-right-ear', + 'path#tanuki-nose', + 'path#tanuki-left-eye, path#tanuki-left-ear', + 'path#tanuki-left-cheek', ] firstPiece = pieces[0] timeout = null diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg index 90f5f4e672b..3d279ec228c 100644 --- a/app/views/shared/_logo.svg +++ b/app/views/shared/_logo.svg @@ -5,13 +5,13 @@ - - - - - - - + + + + + + + From 9f46ca444354d4c6b52de3f23ce17c11f705d006 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 4 Jan 2016 14:57:11 -0500 Subject: [PATCH 084/280] Decrease the logo sweep delay --- app/assets/javascripts/logo.js.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee index a8f97bdf3d7..b4dc993dead 100644 --- a/app/assets/javascripts/logo.js.coffee +++ b/app/assets/javascripts/logo.js.coffee @@ -1,5 +1,6 @@ NProgress.configure(showSpinner: false) +delay = 150 defaultClass = 'tanuki-shape' pieces = [ 'path#tanuki-right-cheek', @@ -12,7 +13,7 @@ firstPiece = pieces[0] timeout = null clearHighlights = -> - $(".#{defaultClass}").attr('class', defaultClass) + $(".#{defaultClass}.highlight").attr('class', defaultClass) start = -> clearHighlights() @@ -40,7 +41,7 @@ work = (pieceIndex) -> nextIndex = pieceIndex + 1 work(nextIndex) - , 200) + , delay) $(document).on 'page:fetch', start $(document).on 'page:change', stop From 567d87d90f2ba7195901ea24d240686c6030a4a7 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 4 Jan 2016 15:18:04 -0500 Subject: [PATCH 085/280] Restructure logo JS to use `setInterval` --- app/assets/javascripts/logo.js.coffee | 42 ++++++++++++--------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee index b4dc993dead..e864a674cdd 100644 --- a/app/assets/javascripts/logo.js.coffee +++ b/app/assets/javascripts/logo.js.coffee @@ -1,6 +1,5 @@ NProgress.configure(showSpinner: false) -delay = 150 defaultClass = 'tanuki-shape' pieces = [ 'path#tanuki-right-cheek', @@ -9,39 +8,36 @@ pieces = [ 'path#tanuki-left-eye, path#tanuki-left-ear', 'path#tanuki-left-cheek', ] +pieceIndex = 0 firstPiece = pieces[0] -timeout = null + +currentTimer = null +delay = 150 clearHighlights = -> $(".#{defaultClass}.highlight").attr('class', defaultClass) start = -> clearHighlights() + pieceIndex = 0 pieces.reverse() unless pieces[0] == firstPiece - work(0) + currentTimer = setInterval(work, delay) stop = -> - window.clearTimeout(timeout) + clearInterval(currentTimer) clearHighlights() -work = (pieceIndex) -> - # jQuery's addClass won't work on an SVG. Who knew! - $piece = $(pieces[pieceIndex]) - $piece.attr('class', "#{defaultClass} highlight") +work = -> + clearHighlights() + $(pieces[pieceIndex]).attr('class', "#{defaultClass} highlight") - timeout = setTimeout(-> - $piece.attr('class', defaultClass) + # If we hit the last piece, reset the index and then reverse the array to + # get a nice back-and-forth sweeping look + if pieceIndex == pieces.length - 1 + pieceIndex = 0 + pieces.reverse() + else + pieceIndex++ - # If we hit the last piece, reset the index and then reverse the array to - # get a nice back-and-forth sweeping look - if pieceIndex + 1 >= pieces.length - nextIndex = 0 - pieces.reverse() - else - nextIndex = pieceIndex + 1 - - work(nextIndex) - , delay) - -$(document).on 'page:fetch', start -$(document).on 'page:change', stop +$(document).on('page:fetch', start) +$(document).on('page:change', stop) From 93096247d86a88e84a0bff7c8dd8496179638a9d Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 4 Jan 2016 15:00:21 -0800 Subject: [PATCH 086/280] Don't notify users twice if they are both project watchers and subscribers Closes #4708 --- CHANGELOG | 1 + app/services/notification_service.rb | 1 + spec/services/notification_service_spec.rb | 12 ++++++++++++ 3 files changed, 14 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index e4b35b281bb..e8eb5d568f5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.4.0 (unreleased) - Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu) + - Don't notify users twice if they are both project watchers and subscribers (Stan Hu) - Implement new UI for group page - Implement search inside emoji picker - Add API support for looking up a user by username (Stan Hu) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index bdf7b3ad2bb..e4edc55bf69 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -413,6 +413,7 @@ class NotificationService recipients = reject_unsubscribed_users(recipients, target) recipients.delete(current_user) + recipients = recipients.uniq recipients end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index c103752198d..588ecc51382 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -61,6 +61,7 @@ describe NotificationService, services: true do should_email(note.noteable.assignee) should_email(@u_mentioned) should_email(@subscriber) + should_email(@watcher_and_subscriber) should_email(@subscribed_participant) should_not_email(note.author) should_not_email(@u_participating) @@ -245,6 +246,7 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@watcher_and_subscriber) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -260,6 +262,7 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@watcher_and_subscriber) should_not_email(@unsubscriber) should_not_email(@u_participating) end @@ -282,6 +285,7 @@ describe NotificationService, services: true do should_email(merge_request.assignee) should_email(@u_watcher) + should_email(@watcher_and_subscriber) should_email(@u_participant_mentioned) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -296,6 +300,7 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@watcher_and_subscriber) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -310,6 +315,7 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@watcher_and_subscriber) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -324,6 +330,7 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@watcher_and_subscriber) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -338,6 +345,7 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@watcher_and_subscriber) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -387,14 +395,18 @@ describe NotificationService, services: true do @subscriber = create :user @unsubscriber = create :user @subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: Notification::N_PARTICIPATING) + @watcher_and_subscriber = create(:user, notification_level: Notification::N_WATCH) project.team << [@subscribed_participant, :master] project.team << [@subscriber, :master] project.team << [@unsubscriber, :master] + project.team << [@watcher_and_subscriber, :master] issuable.subscriptions.create(user: @subscriber, subscribed: true) issuable.subscriptions.create(user: @subscribed_participant, subscribed: true) issuable.subscriptions.create(user: @unsubscriber, subscribed: false) + # Make the watcher a subscriber to detect dupes + issuable.subscriptions.create(user: @watcher_and_subscriber, subscribed: true) end def sent_to_user?(user) From b807a23b4b8fdf1deff120bc9c0bb762991cef3d Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 4 Jan 2016 18:35:05 -0500 Subject: [PATCH 087/280] Redesign the AbuseReports index - Shows when the reported user joined - Show relative timestamp for when the report was created - Parse message with restricted Markdown pipeline to autolink URLs --- .../abuse_reports/_abuse_report.html.haml | 22 ++++++++++--------- app/views/admin/abuse_reports/index.html.haml | 5 ++--- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml index d3afc658cd6..cf50a376e11 100644 --- a/app/views/admin/abuse_reports/_abuse_report.html.haml +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -1,20 +1,22 @@ - reporter = abuse_report.reporter - user = abuse_report.user %tr - %td - - if reporter - = link_to reporter.name, reporter - - else - (removed) - %td - = abuse_report.created_at.to_s(:short) - %td - = abuse_report.message %td - if user - = link_to user.name, user + = link_to user.name, [:admin, user] + .light.small + Joined #{time_ago_with_tooltip(user.created_at)} - else (removed) + %td + - if reporter + = link_to reporter.name, [:admin, reporter] + - else + (removed) + .light.small + = time_ago_with_tooltip(abuse_report.created_at) + %td + = markdown(abuse_report.message.squish!, pipeline: :single_line) %td - if user = link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true), diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml index 40a5fe4628b..bc4a9cedb2c 100644 --- a/app/views/admin/abuse_reports/index.html.haml +++ b/app/views/admin/abuse_reports/index.html.haml @@ -6,10 +6,9 @@ %table.table %thead %tr - %th Reported by - %th Reported at - %th Message %th User + %th Reported by + %th Message %th Primary action %th = render @abuse_reports From 0e60282e36faab8b0f4faee0b71716987df28416 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 4 Jan 2016 18:46:43 -0500 Subject: [PATCH 088/280] Redirect back to user profile page after abuse report Now the reporter will see the fruits of their labor, namely, the red icon! --- app/controllers/abuse_reports_controller.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb index 20bc5173f1d..5718fd22de9 100644 --- a/app/controllers/abuse_reports_controller.rb +++ b/app/controllers/abuse_reports_controller.rb @@ -14,7 +14,7 @@ class AbuseReportsController < ApplicationController end message = "Thank you for your report. A GitLab administrator will look into it shortly." - redirect_to root_path, notice: message + redirect_to @abuse_report.user, notice: message else render :new end @@ -23,6 +23,9 @@ class AbuseReportsController < ApplicationController private def report_params - params.require(:abuse_report).permit(:user_id, :message) + params.require(:abuse_report).permit(%i( + message + user_id + )) end end From 01248d205103fe6c408e914e8943873ceb7acb2a Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 4 Jan 2016 18:56:48 -0500 Subject: [PATCH 089/280] Make AbuseReportMailer responsible for knowing if it should deliver --- app/mailers/abuse_report_mailer.rb | 10 ++++++- spec/mailers/abuse_report_mailer_spec.rb | 38 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 spec/mailers/abuse_report_mailer_spec.rb diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb index f0c41f69a5c..d0ce827a595 100644 --- a/app/mailers/abuse_report_mailer.rb +++ b/app/mailers/abuse_report_mailer.rb @@ -2,11 +2,19 @@ class AbuseReportMailer < BaseMailer include Gitlab::CurrentSettings def notify(abuse_report_id) + return unless deliverable? + @abuse_report = AbuseReport.find(abuse_report_id) mail( - to: current_application_settings.admin_notification_email, + to: current_application_settings.admin_notification_email, subject: "#{@abuse_report.user.name} (#{@abuse_report.user.username}) was reported for abuse" ) end + + private + + def deliverable? + current_application_settings.admin_notification_email.present? + end end diff --git a/spec/mailers/abuse_report_mailer_spec.rb b/spec/mailers/abuse_report_mailer_spec.rb new file mode 100644 index 00000000000..eb433c38873 --- /dev/null +++ b/spec/mailers/abuse_report_mailer_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +describe AbuseReportMailer do + include EmailSpec::Matchers + + describe '.notify' do + context 'with admin_notification_email set' do + before do + stub_application_setting(admin_notification_email: 'admin@example.com') + end + + it 'sends to the admin_notification_email' do + report = create(:abuse_report) + + mail = described_class.notify(report.id) + + expect(mail).to deliver_to 'admin@example.com' + end + + it 'includes the user in the subject' do + report = create(:abuse_report) + + mail = described_class.notify(report.id) + + expect(mail).to have_subject "#{report.user.name} (#{report.user.username}) was reported for abuse" + end + end + + context 'with no admin_notification_email set' do + it 'returns early' do + stub_application_setting(admin_notification_email: nil) + + expect { described_class.notify(spy).deliver_now }. + not_to change { ActionMailer::Base.deliveries.count } + end + end + end +end From 46a220ae3c0e646aac63a3230399fcc8979df6ec Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 4 Jan 2016 18:59:42 -0500 Subject: [PATCH 090/280] Add `AbuseReport#notify` Tell, Don't Ask. --- app/controllers/abuse_reports_controller.rb | 4 +- app/models/abuse_report.rb | 6 ++ .../abuse_reports_controller_spec.rb | 80 ++++++------------- spec/models/abuse_report_spec.rb | 17 ++++ 4 files changed, 49 insertions(+), 58 deletions(-) diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb index 5718fd22de9..38814459f66 100644 --- a/app/controllers/abuse_reports_controller.rb +++ b/app/controllers/abuse_reports_controller.rb @@ -9,9 +9,7 @@ class AbuseReportsController < ApplicationController @abuse_report.reporter = current_user if @abuse_report.save - if current_application_settings.admin_notification_email.present? - AbuseReportMailer.notify(@abuse_report.id).deliver_later - end + @abuse_report.notify message = "Thank you for your report. A GitLab administrator will look into it shortly." redirect_to @abuse_report.user, notice: message diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index 89b3116b9f2..55864236b2f 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -18,4 +18,10 @@ class AbuseReport < ActiveRecord::Base validates :user, presence: true validates :message, presence: true validates :user_id, uniqueness: true + + def notify + return unless self.persisted? + + AbuseReportMailer.notify(self.id).deliver_later + end end diff --git a/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb index 15824a1c67f..80a418feb3e 100644 --- a/spec/controllers/abuse_reports_controller_spec.rb +++ b/spec/controllers/abuse_reports_controller_spec.rb @@ -1,76 +1,46 @@ require 'spec_helper' describe AbuseReportsController do - let(:reporter) { create(:user) } - let(:user) { create(:user) } - let(:message) { "This user is a spammer" } + let(:reporter) { create(:user) } + let(:user) { create(:user) } + let(:attrs) do + attributes_for(:abuse_report) do |hash| + hash[:user_id] = user.id + end + end before do sign_in(reporter) end - describe "POST create" do - context "with admin notification email set" do - let(:admin_email) { "admin@example.com"} - - before(:each) do - stub_application_setting(admin_notification_email: admin_email) + describe 'POST create' do + context 'with valid attributes' do + it 'saves the abuse report' do + expect do + post :create, abuse_report: attrs + end.to change { AbuseReport.count }.by(1) end - it "sends a notification email" do - perform_enqueued_jobs do - post :create, - abuse_report: { - user_id: user.id, - message: message - } + it 'calls notify' do + expect_any_instance_of(AbuseReport).to receive(:notify) - 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 + post :create, abuse_report: attrs 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 + it 'redirects back to the reported user' do + post :create, abuse_report: attrs + + expect(response).to redirect_to user end end - context "without admin notification email set" do - before(:each) do - stub_application_setting(admin_notification_email: nil) - end + context 'with invalid attributes' do + it 'renders new' do + attrs.delete(:user_id) + post :create, abuse_report: attrs - it "does not send a notification email" do - expect do - post :create, - abuse_report: { - user_id: user.id, - message: message - } - end.not_to change { ActionMailer::Base.deliveries.count } - end - - it "saves the abuse report" do - expect do - post :create, - abuse_report: { - user_id: user.id, - message: message - } - end.to change { AbuseReport.count }.by(1) + expect(response).to render_template(:new) end end end - end diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index d45319b25d4..46cab1644c7 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -28,4 +28,21 @@ RSpec.describe AbuseReport, type: :model do it { is_expected.to validate_presence_of(:message) } it { is_expected.to validate_uniqueness_of(:user_id) } end + + describe '#notify' do + it 'delivers' do + expect(AbuseReportMailer).to receive(:notify).with(subject.id). + and_return(spy) + + subject.notify + end + + it 'returns early when not persisted' do + report = build(:abuse_report) + + expect(AbuseReportMailer).not_to receive(:notify) + + report.notify + end + end end From f7ba38c073387eedb13375ad6286ba08ce6badb9 Mon Sep 17 00:00:00 2001 From: Andriy Dyadyura Date: Tue, 5 Jan 2016 10:45:18 +0100 Subject: [PATCH 091/280] markdown fixes --- .../stylesheets/framework/typography.scss | 6 +- db/schema.rb | 516 +++++++++--------- 2 files changed, 261 insertions(+), 261 deletions(-) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index c3e4ad0ad00..05d0c865164 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -54,17 +54,17 @@ h3 { margin: 24px 0 12px 0; - font-size: 1.25em; + font-size: 1.1em; } h4 { margin: 24px 0 12px 0; - font-size: 1.1em; + font-size: 1em; } h5 { margin: 24px 0 12px 0; - font-size: 1em; + font-size: 0.95em; } h6 { diff --git a/db/schema.rb b/db/schema.rb index 7a6d34b8153..48e6983684a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -32,44 +32,44 @@ ActiveRecord::Schema.define(version: 20151229112614) do t.text "sign_in_text" t.datetime "created_at" t.datetime "updated_at" - t.string "home_page_url" - t.integer "default_branch_protection", default: 2 - t.boolean "twitter_sharing_enabled", default: true + t.string "home_page_url", limit: 255 + t.integer "default_branch_protection", default: 2 + t.boolean "twitter_sharing_enabled", default: true t.text "restricted_visibility_levels" - t.boolean "version_check_enabled", default: true - t.integer "max_attachment_size", default: 10, null: false + t.boolean "version_check_enabled", default: true + t.integer "max_attachment_size", default: 10, null: false t.integer "default_project_visibility" t.integer "default_snippet_visibility" t.text "restricted_signup_domains" - t.boolean "user_oauth_applications", default: true - t.string "after_sign_out_path" - t.integer "session_expire_delay", default: 10080, null: false + t.boolean "user_oauth_applications", default: true + t.string "after_sign_out_path", limit: 255 + t.integer "session_expire_delay", default: 10080, null: false t.text "import_sources" t.text "help_page_text" - 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 "admin_notification_email", limit: 255 + t.boolean "shared_runners_enabled", default: true, null: false + t.integer "max_artifacts_size", default: 100, null: false t.string "runners_registration_token" - t.boolean "require_two_factor_authentication", default: false - t.integer "two_factor_grace_period", default: 48 - t.boolean "metrics_enabled", default: false - t.string "metrics_host", default: "localhost" + t.boolean "require_two_factor_authentication", default: false + t.integer "two_factor_grace_period", default: 48 + t.boolean "metrics_enabled", default: false + t.string "metrics_host", default: "localhost" t.string "metrics_username" t.string "metrics_password" - t.integer "metrics_pool_size", default: 16 - t.integer "metrics_timeout", default: 10 - t.integer "metrics_method_call_threshold", default: 10 - t.boolean "recaptcha_enabled", default: false + t.integer "metrics_pool_size", default: 16 + t.integer "metrics_timeout", default: 10 + t.integer "metrics_method_call_threshold", default: 10 + t.boolean "recaptcha_enabled", default: false t.string "recaptcha_site_key" t.string "recaptcha_private_key" - t.integer "metrics_port", default: 8089 + t.integer "metrics_port", default: 8089 end 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 - t.string "entity_type", null: false + t.integer "author_id", null: false + t.string "type", limit: 255, null: false + t.integer "entity_id", null: false + t.string "entity_type", limit: 255, null: false t.text "details" t.datetime "created_at" t.datetime "updated_at" @@ -80,14 +80,14 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree create_table "broadcast_messages", force: :cascade do |t| - t.text "message", null: false + t.text "message", null: false t.datetime "starts_at" t.datetime "ends_at" t.integer "alert_type" t.datetime "created_at" t.datetime "updated_at" - t.string "color" - t.string "font" + t.string "color", limit: 255 + t.string "font", limit: 255 end create_table "ci_application_settings", force: :cascade do |t| @@ -99,7 +99,7 @@ ActiveRecord::Schema.define(version: 20151229112614) do create_table "ci_builds", force: :cascade do |t| t.integer "project_id" - t.string "status" + t.string "status", limit: 255 t.datetime "finished_at" t.text "trace" t.datetime "created_at" @@ -110,19 +110,19 @@ ActiveRecord::Schema.define(version: 20151229112614) do t.integer "commit_id" t.text "commands" t.integer "job_id" - t.string "name" - t.boolean "deploy", default: false + t.string "name", limit: 255 + t.boolean "deploy", default: false t.text "options" - t.boolean "allow_failure", default: false, null: false - t.string "stage" + t.boolean "allow_failure", default: false, null: false + t.string "stage", limit: 255 t.integer "trigger_request_id" t.integer "stage_idx" t.boolean "tag" - t.string "ref" + t.string "ref", limit: 255 t.integer "user_id" - t.string "type" - t.string "target_url" - t.string "description" + t.string "type", limit: 255 + t.string "target_url", limit: 255 + t.string "description", limit: 255 t.text "artifacts_file" t.integer "gl_project_id" end @@ -141,13 +141,13 @@ ActiveRecord::Schema.define(version: 20151229112614) do create_table "ci_commits", force: :cascade do |t| t.integer "project_id" - t.string "ref" - t.string "sha" - t.string "before_sha" + t.string "ref", limit: 255 + t.string "sha", limit: 255 + t.string "before_sha", limit: 255 t.text "push_data" t.datetime "created_at" t.datetime "updated_at" - t.boolean "tag", default: false + t.boolean "tag", default: false t.text "yaml_errors" t.datetime "committed_at" t.integer "gl_project_id" @@ -174,16 +174,16 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree create_table "ci_jobs", force: :cascade do |t| - t.integer "project_id", null: false + t.integer "project_id", null: false t.text "commands" - t.boolean "active", default: true, null: false + t.boolean "active", default: true, null: false t.datetime "created_at" t.datetime "updated_at" - t.string "name" - t.boolean "build_branches", default: true, null: false - t.boolean "build_tags", default: false, null: false - t.string "job_type", default: "parallel" - t.string "refs" + t.string "name", limit: 255 + t.boolean "build_branches", default: true, null: false + t.boolean "build_tags", default: false, null: false + t.string "job_type", limit: 255, default: "parallel" + t.string "refs", limit: 255 t.datetime "deleted_at" end @@ -191,25 +191,25 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree create_table "ci_projects", force: :cascade do |t| - t.string "name" - t.integer "timeout", default: 3600, null: false + t.string "name", limit: 255 + t.integer "timeout", default: 3600, null: false t.datetime "created_at" t.datetime "updated_at" - t.string "token" - t.string "default_ref" - t.string "path" - t.boolean "always_build", default: false, null: false + t.string "token", limit: 255 + t.string "default_ref", limit: 255 + t.string "path", limit: 255 + t.boolean "always_build", default: false, null: false t.integer "polling_interval" - t.boolean "public", default: false, null: false - t.string "ssh_url_to_repo" + t.boolean "public", default: false, null: false + t.string "ssh_url_to_repo", limit: 255 t.integer "gitlab_id" - t.boolean "allow_git_fetch", default: true, null: false - t.string "email_recipients", default: "", null: false - t.boolean "email_add_pusher", default: true, null: false - t.boolean "email_only_broken_builds", default: true, null: false - t.string "skip_refs" - t.string "coverage_regex" - t.boolean "shared_runners_enabled", default: false + t.boolean "allow_git_fetch", default: true, null: false + t.string "email_recipients", limit: 255, default: "", null: false + t.boolean "email_add_pusher", default: true, null: false + t.boolean "email_only_broken_builds", default: true, null: false + t.string "skip_refs", limit: 255 + t.string "coverage_regex", limit: 255 + t.boolean "shared_runners_enabled", default: false t.text "generated_yaml_config" end @@ -228,34 +228,34 @@ ActiveRecord::Schema.define(version: 20151229112614) do 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| - t.string "token" + t.string "token", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.string "description" + t.string "description", limit: 255 t.datetime "contacted_at" - t.boolean "active", default: true, null: false - t.boolean "is_shared", default: false - t.string "name" - t.string "version" - t.string "revision" - t.string "platform" - t.string "architecture" + t.boolean "active", default: true, null: false + t.boolean "is_shared", default: false + t.string "name", limit: 255 + t.string "version", limit: 255 + t.string "revision", limit: 255 + t.string "platform", limit: 255 + t.string "architecture", limit: 255 end create_table "ci_services", force: :cascade do |t| - t.string "type" - t.string "title" - t.integer "project_id", null: false + t.string "type", limit: 255 + t.string "title", limit: 255 + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" - t.boolean "active", default: false, null: false + t.boolean "active", default: false, null: false t.text "properties" end add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree create_table "ci_sessions", force: :cascade do |t| - t.string "session_id", null: false + t.string "session_id", limit: 255, null: false t.text "data" t.datetime "created_at" t.datetime "updated_at" @@ -267,9 +267,9 @@ ActiveRecord::Schema.define(version: 20151229112614) do create_table "ci_taggings", force: :cascade do |t| t.integer "tag_id" t.integer "taggable_id" - t.string "taggable_type" + t.string "taggable_type", limit: 255 t.integer "tagger_id" - t.string "tagger_type" + t.string "tagger_type", limit: 255 t.string "context", limit: 128 t.datetime "created_at" end @@ -278,8 +278,8 @@ ActiveRecord::Schema.define(version: 20151229112614) do 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: :cascade do |t| - t.string "name" - t.integer "taggings_count", default: 0 + t.string "name", limit: 255 + t.integer "taggings_count", default: 0 end add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree @@ -293,7 +293,7 @@ ActiveRecord::Schema.define(version: 20151229112614) do end create_table "ci_triggers", force: :cascade do |t| - t.string "token" + t.string "token", limit: 255 t.integer "project_id" t.datetime "deleted_at" t.datetime "created_at" @@ -306,19 +306,19 @@ ActiveRecord::Schema.define(version: 20151229112614) do create_table "ci_variables", force: :cascade do |t| t.integer "project_id" - t.string "key" + t.string "key", limit: 255 t.text "value" t.text "encrypted_value" - t.string "encrypted_value_salt" - t.string "encrypted_value_iv" + t.string "encrypted_value_salt", limit: 255 + t.string "encrypted_value_iv", limit: 255 t.integer "gl_project_id" end 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 - t.integer "project_id", null: false + t.string "url", limit: 255, null: false + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" end @@ -333,8 +333,8 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree create_table "emails", force: :cascade do |t| - t.integer "user_id", null: false - t.string "email", null: false + t.integer "user_id", null: false + t.string "email", limit: 255, null: false t.datetime "created_at" t.datetime "updated_at" end @@ -343,9 +343,9 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree create_table "events", force: :cascade do |t| - t.string "target_type" + t.string "target_type", limit: 255 t.integer "target_id" - t.string "title" + t.string "title", limit: 255 t.text "data" t.integer "project_id" t.datetime "created_at" @@ -371,8 +371,8 @@ ActiveRecord::Schema.define(version: 20151229112614) 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: :cascade do |t| - t.string "extern_uid" - t.string "provider" + t.string "extern_uid", limit: 255 + t.string "provider", limit: 255 t.integer "user_id" t.datetime "created_at" t.datetime "updated_at" @@ -382,17 +382,17 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree create_table "issues", force: :cascade do |t| - t.string "title" + t.string "title", limit: 255 t.integer "assignee_id" t.integer "author_id" t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.integer "position", default: 0 - t.string "branch_name" + t.integer "position", default: 0 + t.string "branch_name", limit: 255 t.text "description" t.integer "milestone_id" - t.string "state" + t.string "state", limit: 255 t.integer "iid" t.integer "updated_by_id" end @@ -412,10 +412,10 @@ ActiveRecord::Schema.define(version: 20151229112614) do t.datetime "created_at" t.datetime "updated_at" t.text "key" - t.string "title" - t.string "type" - t.string "fingerprint" - t.boolean "public", default: false, null: false + t.string "title", limit: 255 + t.string "type", limit: 255 + t.string "fingerprint", limit: 255 + t.boolean "public", default: false, null: false end add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree @@ -424,7 +424,7 @@ ActiveRecord::Schema.define(version: 20151229112614) do create_table "label_links", force: :cascade do |t| t.integer "label_id" t.integer "target_id" - t.string "target_type" + t.string "target_type", limit: 255 t.datetime "created_at" t.datetime "updated_at" end @@ -433,22 +433,22 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree create_table "labels", force: :cascade do |t| - t.string "title" - t.string "color" + t.string "title", limit: 255 + t.string "color", limit: 255 t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.boolean "template", default: false + t.boolean "template", default: false end add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree create_table "lfs_objects", force: :cascade do |t| - t.string "oid", null: false - t.integer "size", null: false + t.string "oid", limit: 255, null: false + t.integer "size", null: false t.datetime "created_at" t.datetime "updated_at" - t.string "file" + t.string "file", limit: 255 end add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree @@ -463,17 +463,17 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree 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 + t.integer "access_level", null: false + t.integer "source_id", null: false + t.string "source_type", limit: 255, null: false t.integer "user_id" - t.integer "notification_level", null: false - t.string "type" + t.integer "notification_level", null: false + t.string "type", limit: 255 t.datetime "created_at" t.datetime "updated_at" t.integer "created_by_id" - t.string "invite_email" - t.string "invite_token" + t.string "invite_email", limit: 255 + t.string "invite_token", limit: 255 t.datetime "invite_accepted_at" end @@ -485,10 +485,10 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree create_table "merge_request_diffs", force: :cascade do |t| - t.string "state" + t.string "state", limit: 255 t.text "st_commits" t.text "st_diffs" - t.integer "merge_request_id", null: false + t.integer "merge_request_id", null: false t.datetime "created_at" t.datetime "updated_at" end @@ -496,26 +496,26 @@ ActiveRecord::Schema.define(version: 20151229112614) 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", limit: 255, null: false + t.string "source_branch", limit: 255, null: false + t.integer "source_project_id", null: false t.integer "author_id" t.integer "assignee_id" - t.string "title" + t.string "title", limit: 255 t.datetime "created_at" t.datetime "updated_at" t.integer "milestone_id" - t.string "state" - t.string "merge_status" - t.integer "target_project_id", null: false + t.string "state", limit: 255 + t.string "merge_status", limit: 255 + 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.string "merge_error", limit: 255 t.text "merge_params" - t.boolean "merge_when_build_succeeds", default: false, null: false + t.boolean "merge_when_build_succeeds", default: false, null: false t.integer "merge_user_id" end @@ -531,13 +531,13 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree create_table "milestones", force: :cascade do |t| - t.string "title", null: false - t.integer "project_id", null: false + t.string "title", limit: 255, null: false + t.integer "project_id", null: false t.text "description" t.date "due_date" t.datetime "created_at" t.datetime "updated_at" - t.string "state" + t.string "state", limit: 255 t.integer "iid" end @@ -547,14 +547,14 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree create_table "namespaces", force: :cascade do |t| - t.string "name", null: false - t.string "path", null: false + t.string "name", limit: 255, null: false + t.string "path", limit: 255, null: false t.integer "owner_id" t.datetime "created_at" t.datetime "updated_at" - t.string "type" - t.string "description", default: "", null: false - t.string "avatar" + t.string "type", limit: 255 + t.string "description", limit: 255, default: "", null: false + t.string "avatar", limit: 255 end add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree @@ -565,19 +565,19 @@ ActiveRecord::Schema.define(version: 20151229112614) do create_table "notes", force: :cascade do |t| t.text "note" - t.string "noteable_type" + t.string "noteable_type", limit: 255 t.integer "author_id" t.datetime "created_at" t.datetime "updated_at" t.integer "project_id" - t.string "attachment" - t.string "line_code" - t.string "commit_id" + t.string "attachment", limit: 255 + t.string "line_code", limit: 255 + t.string "commit_id", limit: 255 t.integer "noteable_id" - t.boolean "system", default: false, null: false + t.boolean "system", default: false, null: false t.text "st_diff" t.integer "updated_by_id" - t.boolean "is_award", default: false, null: false + t.boolean "is_award", default: false, null: false end add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree @@ -593,14 +593,14 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree 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 - t.integer "expires_in", null: false - t.text "redirect_uri", null: false - t.datetime "created_at", null: false + t.integer "resource_owner_id", null: false + t.integer "application_id", null: false + t.string "token", limit: 255, null: false + t.integer "expires_in", null: false + t.text "redirect_uri", null: false + t.datetime "created_at", null: false t.datetime "revoked_at" - t.string "scopes" + t.string "scopes", limit: 255 end add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree @@ -608,12 +608,12 @@ ActiveRecord::Schema.define(version: 20151229112614) do create_table "oauth_access_tokens", force: :cascade do |t| t.integer "resource_owner_id" t.integer "application_id" - t.string "token", null: false - t.string "refresh_token" + t.string "token", limit: 255, null: false + t.string "refresh_token", limit: 255 t.integer "expires_in" t.datetime "revoked_at" - t.datetime "created_at", null: false - t.string "scopes" + t.datetime "created_at", null: false + t.string "scopes", limit: 255 end add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree @@ -621,15 +621,15 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree create_table "oauth_applications", force: :cascade do |t| - t.string "name", null: false - t.string "uid", null: false - t.string "secret", null: false - t.text "redirect_uri", null: false - t.string "scopes", default: "", null: false + t.string "name", limit: 255, null: false + t.string "uid", limit: 255, null: false + t.string "secret", limit: 255, null: false + t.text "redirect_uri", null: false + t.string "scopes", limit: 255, default: "", null: false t.datetime "created_at" t.datetime "updated_at" t.integer "owner_id" - t.string "owner_type" + t.string "owner_type", limit: 255 end add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree @@ -641,39 +641,39 @@ ActiveRecord::Schema.define(version: 20151229112614) do end create_table "projects", force: :cascade do |t| - t.string "name" - t.string "path" + t.string "name", limit: 255 + t.string "path", limit: 255 t.text "description" t.datetime "created_at" t.datetime "updated_at" t.integer "creator_id" - t.boolean "issues_enabled", default: true, null: false - t.boolean "wall_enabled", default: true, null: false - t.boolean "merge_requests_enabled", default: true, null: false - t.boolean "wiki_enabled", default: true, null: false + t.boolean "issues_enabled", default: true, null: false + t.boolean "wall_enabled", default: true, null: false + t.boolean "merge_requests_enabled", default: true, null: false + t.boolean "wiki_enabled", default: true, null: false t.integer "namespace_id" - t.string "issues_tracker", default: "gitlab", null: false - t.string "issues_tracker_id" - t.boolean "snippets_enabled", default: true, null: false + t.string "issues_tracker", limit: 255, default: "gitlab", null: false + t.string "issues_tracker_id", limit: 255 + t.boolean "snippets_enabled", default: true, null: false t.datetime "last_activity_at" - t.string "import_url" - t.integer "visibility_level", default: 0, null: false - t.boolean "archived", default: false, null: false - t.string "avatar" - t.string "import_status" - t.float "repository_size", default: 0.0 - t.integer "star_count", default: 0, null: false - t.string "import_type" - t.string "import_source" - t.integer "commit_count", default: 0 + t.string "import_url", limit: 255 + t.integer "visibility_level", default: 0, null: false + t.boolean "archived", default: false, null: false + t.string "avatar", limit: 255 + t.string "import_status", limit: 255 + t.float "repository_size", default: 0.0 + t.integer "star_count", default: 0, null: false + t.string "import_type", limit: 255 + t.string "import_source", limit: 255 + 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.boolean "builds_enabled", default: true, null: false + t.boolean "shared_runners_enabled", default: true, null: false 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 + 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 @@ -689,17 +689,17 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree create_table "protected_branches", force: :cascade do |t| - t.integer "project_id", null: false - t.string "name", null: false + t.integer "project_id", null: false + t.string "name", limit: 255, null: false t.datetime "created_at" t.datetime "updated_at" - t.boolean "developers_can_push", default: false, null: false + t.boolean "developers_can_push", default: false, null: false end add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree create_table "releases", force: :cascade do |t| - t.string "tag" + t.string "tag", limit: 255 t.text "description" t.integer "project_id" t.datetime "created_at" @@ -712,30 +712,30 @@ ActiveRecord::Schema.define(version: 20151229112614) do create_table "sent_notifications", force: :cascade do |t| t.integer "project_id" t.integer "noteable_id" - t.string "noteable_type" + t.string "noteable_type", limit: 255 t.integer "recipient_id" - t.string "commit_id" - t.string "reply_key", null: false - t.string "line_code" + t.string "commit_id", limit: 255 + t.string "reply_key", limit: 255, null: false + t.string "line_code", limit: 255 end add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree create_table "services", force: :cascade do |t| - t.string "type" - t.string "title" + t.string "type", limit: 255 + t.string "title", limit: 255 t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.boolean "active", default: false, null: false + t.boolean "active", default: false, null: false t.text "properties" - t.boolean "template", default: false - t.boolean "push_events", default: true - t.boolean "issues_events", default: true - 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 + t.boolean "template", default: false + t.boolean "push_events", default: true + t.boolean "issues_events", default: true + 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 @@ -743,16 +743,16 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "services", ["template"], name: "index_services_on_template", using: :btree create_table "snippets", force: :cascade do |t| - t.string "title" + t.string "title", limit: 255 t.text "content" - t.integer "author_id", null: false + t.integer "author_id", null: false t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.string "file_name" + t.string "file_name", limit: 255 t.datetime "expires_at" - t.string "type" - t.integer "visibility_level", default: 0, null: false + t.string "type", limit: 255 + t.integer "visibility_level", default: 0, null: false end add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree @@ -765,7 +765,7 @@ ActiveRecord::Schema.define(version: 20151229112614) do create_table "subscriptions", force: :cascade do |t| t.integer "user_id" t.integer "subscribable_id" - t.string "subscribable_type" + t.string "subscribable_type", limit: 255 t.boolean "subscribed" t.datetime "created_at" t.datetime "updated_at" @@ -776,10 +776,10 @@ ActiveRecord::Schema.define(version: 20151229112614) do create_table "taggings", force: :cascade do |t| t.integer "tag_id" t.integer "taggable_id" - t.string "taggable_type" + t.string "taggable_type", limit: 255 t.integer "tagger_id" - t.string "tagger_type" - t.string "context" + t.string "tagger_type", limit: 255 + t.string "context", limit: 255 t.datetime "created_at" end @@ -787,67 +787,67 @@ ActiveRecord::Schema.define(version: 20151229112614) do 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: :cascade do |t| - t.string "name" - t.integer "taggings_count", default: 0 + t.string "name", limit: 255 + t.integer "taggings_count", default: 0 end add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree 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" + t.string "email", limit: 255, default: "", null: false + t.string "encrypted_password", limit: 255, default: "", null: false + t.string "reset_password_token", limit: 255 t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0 + t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" - t.string "current_sign_in_ip" - t.string "last_sign_in_ip" + t.string "current_sign_in_ip", limit: 255 + t.string "last_sign_in_ip", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.string "name" - t.boolean "admin", default: false, null: false - t.integer "projects_limit", default: 10 - t.string "skype", default: "", null: false - t.string "linkedin", default: "", null: false - t.string "twitter", default: "", null: false - t.string "authentication_token" - t.integer "theme_id", default: 1, null: false - t.string "bio" - t.integer "failed_attempts", default: 0 + t.string "name", limit: 255 + t.boolean "admin", default: false, null: false + t.integer "projects_limit", default: 10 + t.string "skype", limit: 255, default: "", null: false + t.string "linkedin", limit: 255, default: "", null: false + t.string "twitter", limit: 255, default: "", null: false + t.string "authentication_token", limit: 255 + t.integer "theme_id", default: 1, null: false + t.string "bio", limit: 255 + t.integer "failed_attempts", default: 0 t.datetime "locked_at" - t.string "username" - t.boolean "can_create_group", default: true, null: false - t.boolean "can_create_team", default: true, null: false - t.string "state" - t.integer "color_scheme_id", default: 1, null: false - t.integer "notification_level", default: 1, null: false + t.string "username", limit: 255 + t.boolean "can_create_group", default: true, null: false + t.boolean "can_create_team", default: true, null: false + t.string "state", limit: 255 + t.integer "color_scheme_id", default: 1, null: false + t.integer "notification_level", default: 1, null: false t.datetime "password_expires_at" t.integer "created_by_id" t.datetime "last_credential_check_at" - t.string "avatar" - t.string "confirmation_token" + t.string "avatar", limit: 255 + t.string "confirmation_token", limit: 255 t.datetime "confirmed_at" t.datetime "confirmation_sent_at" - t.string "unconfirmed_email" - t.boolean "hide_no_ssh_key", default: false - t.string "website_url", default: "", null: false - t.string "notification_email" - t.boolean "hide_no_password", default: false - t.boolean "password_automatically_set", default: false - t.string "location" - t.string "encrypted_otp_secret" - t.string "encrypted_otp_secret_iv" - t.string "encrypted_otp_secret_salt" - t.boolean "otp_required_for_login", default: false, null: false + t.string "unconfirmed_email", limit: 255 + t.boolean "hide_no_ssh_key", default: false + t.string "website_url", limit: 255, default: "", null: false + t.string "notification_email", limit: 255 + t.boolean "hide_no_password", default: false + t.boolean "password_automatically_set", default: false + t.string "location", limit: 255 + t.string "encrypted_otp_secret", limit: 255 + t.string "encrypted_otp_secret_iv", limit: 255 + t.string "encrypted_otp_secret_salt", limit: 255 + t.boolean "otp_required_for_login", default: false, null: false t.text "otp_backup_codes" - t.string "public_email", default: "", null: false - t.integer "dashboard", default: 0 - t.integer "project_view", default: 0 + t.string "public_email", limit: 255, default: "", null: false + t.integer "dashboard", default: 0 + t.integer "project_view", default: 0 t.integer "consumed_timestep" - t.integer "layout", default: 0 - t.boolean "hide_project_limit", default: false + t.integer "layout", default: 0 + t.boolean "hide_project_limit", default: false t.string "unlock_token" t.datetime "otp_grace_period_started_at" end @@ -874,19 +874,19 @@ ActiveRecord::Schema.define(version: 20151229112614) do add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree create_table "web_hooks", force: :cascade do |t| - t.string "url" + t.string "url", limit: 255 t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.string "type", default: "ProjectHook" + t.string "type", limit: 255, default: "ProjectHook" t.integer "service_id" - t.boolean "push_events", default: true, null: false - t.boolean "issues_events", default: false, null: false - t.boolean "merge_requests_events", default: false, null: false - 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 + t.boolean "push_events", default: true, null: false + t.boolean "issues_events", default: false, null: false + t.boolean "merge_requests_events", default: false, null: false + 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 From 00e8532d89fb04b39e9811c5ec5953dd2d098f6f Mon Sep 17 00:00:00 2001 From: Andriy Dyadyura Date: Tue, 5 Jan 2016 10:51:59 +0100 Subject: [PATCH 092/280] markdown fixes --- app/assets/stylesheets/framework/typography.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 05d0c865164..714369d9f15 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -59,7 +59,7 @@ h4 { margin: 24px 0 12px 0; - font-size: 1em; + font-size: 0.98em; } h5 { From d17945bb4ef0750f546a9dea79df1abf4ef140a5 Mon Sep 17 00:00:00 2001 From: Felix Eckhofer Date: Tue, 5 Jan 2016 13:01:23 +0100 Subject: [PATCH 093/280] Remove misleading `ssh-dsa` The keytype field is actually `ssh-dss` for DSA keys and they will not be stored as `id_rsa.pub`. Note that newer version of ssh actually also support `ecdsa-sha2-nistp256` and others so it is also misleading to assume the field always starts with `ssh-`. --- doc/ssh/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ssh/README.md b/doc/ssh/README.md index fe5b45dd432..64795233e41 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -9,7 +9,7 @@ already has one by running the following command: cat ~/.ssh/id_rsa.pub ``` -If you see a long string starting with `ssh-rsa` or `ssh-dsa`, you can skip the `ssh-keygen` step. +If you see a long string starting with `ssh-rsa`, you can skip the `ssh-keygen` step. 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 @@ -29,7 +29,7 @@ cat ~/.ssh/id_rsa.pub ``` Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your -user profile. Please copy the complete key starting with `ssh-` and ending +user profile. Please copy the complete key starting with `ssh-rsa` and ending with your username and host. To copy your public key to the clipboard, use code below. Depending on your From ca8639a4662c59b15ef4e0ed02b6587ce4049b0d Mon Sep 17 00:00:00 2001 From: Felix Eckhofer Date: Tue, 5 Jan 2016 13:02:45 +0100 Subject: [PATCH 094/280] Clarify the key generation step Users must not change the default filename or they will have to create a suitable .ssh/config which is out-of-scope here. --- 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 64795233e41..c4b1fc3b626 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -20,8 +20,9 @@ To generate a new SSH key, use the following command: ssh-keygen -t rsa -C "$your_email" ``` This command will prompt you for a location and filename to store the key -pair and for a password. When prompted for the location and filename, you -can press enter to use the default. +pair and for a password. When prompted for the location and filename, just +press enter to use the default. If you use a different name, the key will not +be used automatically. Use the command below to show your public key: ```bash From 766a59630f8e004fbd99549dc6d27f7b29fff62f Mon Sep 17 00:00:00 2001 From: Felix Eckhofer Date: Tue, 5 Jan 2016 13:06:23 +0100 Subject: [PATCH 095/280] Fix grammar --- doc/ssh/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ssh/README.md b/doc/ssh/README.md index c4b1fc3b626..77eb53427e2 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -33,7 +33,7 @@ Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your user profile. Please copy the complete key starting with `ssh-rsa` and ending with your username and host. -To copy your public key to the clipboard, use code below. Depending on your +To copy your public key to the clipboard, use the code below. Depending on your OS you'll need to use a different command: **Windows:** From f60bceb988bd629f9adecc070ef5579d264f27c6 Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Tue, 5 Jan 2016 13:20:06 +0100 Subject: [PATCH 096/280] Add CI data to projcet entity --- lib/api/entities.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 26e7c956e8f..e8154e0f383 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -71,6 +71,9 @@ module API expose :avatar_url expose :star_count, :forks_count expose :open_issues_count, if: lambda { |project, options| project.issues_enabled? && project.default_issues_tracker? } + + expose :build_allow_git_fetch, :build_timeout, :build_coverage_regex + expose :runners_token end class ProjectMember < UserBasic From 57a68c722c180be04dfe1682b1fbced11c8d735b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 5 Jan 2016 13:59:32 +0100 Subject: [PATCH 097/280] Update project services to include all supported ones [ci skip] --- doc/project_services/project_services.md | 42 +++++++++++++++--------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 03937d20728..2d3e899383f 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -1,20 +1,30 @@ # Project Services - -__Project integrations with external services for continuous integration and more.__ + +Project services allow you to integrate GitLab with other applications. Below +is list of the currently supported ones. Click on the service links to see +further configuration instructions and details. Contributions are welcome. ## Services -- Assembla -- [Atlassian Bamboo CI](bamboo.md) An Atlassian product for continuous integration. -- Build box -- Campfire -- Emails on push -- Flowdock -- Gemnasium -- GitLab CI -- [HipChat](hipchat.md) An Atlassian product for private group chat and instant messaging. -- [Irker](irker.md) An IRC gateway to receive messages on repository updates. -- Pivotal Tracker -- Pushover -- Slack -- TeamCity +| Service | Description | +| ------- | ----------- | +| Asana | Asana - Teamwork without email | +| Assembla | Project Management Software (Source Commits Endpoint) | +| [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server | +| Buildkite | Continuous integration and deployments | +| Builds emails | Email the builds status to a list of recipients | +| Campfire | Simple web-based real-time group chat | +| Custom Issue Tracker | Custom issue tracker | +| Drone CI | Continuous Integration platform built on Docker, written in Go | +| Emails on push | Email the commits and diff of each push to a list of recipients | +| External Wiki | Replaces the link to the internal wiki with a link to an external wiki | +| Flowdock | Flowdock is a collaboration web app for technical teams | +| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | +| [HipChat](hipchat.md) | Private group chat and IM | +| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | +| JIRA | Jira issue tracker | +| JetBrains TeamCity CI | A continuous integration and build server | +| PivotalTracker | Project Management Software (Source Commits Endpoint) | +| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | +| Redmine | Redmine issue tracker | +| Slack | A team communication tool for the 21st century | From 6ce01ca3ece007f135c6b5a9bc258fdd9e8d71de Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 5 Jan 2016 11:00:10 -0200 Subject: [PATCH 098/280] Validate README format before displaying MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Take the first previewable README file as project’s README, otherwise if none file is available, or we can’t preview any of them, we assume that project doesn’t have a README file. --- app/models/tree.rb | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/models/tree.rb b/app/models/tree.rb index 93b3246a668..e0e04d8859f 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -17,17 +17,15 @@ class Tree def readme return @readme if defined?(@readme) - available_readmes = blobs.select(&:readme?) - - if available_readmes.count == 0 - return @readme = nil + # Take the first previewable readme, or return nil if none is available or + # we can't preview any of them + readme_tree = blobs.find do |blob| + blob.readme? && (previewable?(blob.name) || plain?(blob.name)) end - # Take the first previewable readme, or the first available readme, if we - # can't preview any of them - readme_tree = available_readmes.find do |readme| - previewable?(readme.name) - end || available_readmes.first + if readme_tree.nil? + return @readme = nil + end readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name) From e57b506222f535774059cbb0f986621384c5a8f7 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 5 Jan 2016 05:30:01 -0800 Subject: [PATCH 099/280] Suggest prefacing find command with sudo when base permissions are wrong Closes #5872 --- lib/tasks/gitlab/check.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 0469c5a61c3..2dc2953e328 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -431,7 +431,7 @@ namespace :gitlab do try_fixing_it( "sudo chmod -R ug+rwX,o-rwx #{repo_base_path}", "sudo chmod -R ug-s #{repo_base_path}", - "find #{repo_base_path} -type d -print0 | sudo xargs -0 chmod g+s" + "sudo find #{repo_base_path} -type d -print0 | sudo xargs -0 chmod g+s" ) for_more_information( see_installation_guide_section "GitLab Shell" From 8e33ec1deea60a176cd43092c913200d28ea04fe Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 5 Jan 2016 11:30:30 -0200 Subject: [PATCH 100/280] [ci skip] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 47ef06bee54..72698e4fe3f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,7 @@ v 8.4.0 (unreleased) - Fix version check image in Safari - Show 'All' tab by default in the builds page - Fix API project lookups when querying with a namespace with dots (Stan Hu) + - Validate README format before displaying v 8.3.3 (unreleased) - Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu) From 0a21731e3bc400ceb9898c9efbc2a186f5348e09 Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Tue, 5 Jan 2016 15:00:25 +0100 Subject: [PATCH 101/280] Add ci fields in project create/update feature API --- lib/api/helpers.rb | 2 +- lib/api/projects.rb | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index a4df810e755..563c12e4f74 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -157,7 +157,7 @@ module API def attributes_for_keys(keys, custom_params = nil) attrs = {} keys.each do |key| - if params[key].present? or (params.has_key?(key) and params[key] == false) + if params[key].present? or (params.has_key?(key) and (params[key].empty? or params[key] == false)) attrs[key] = params[key] end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 0781236cf6d..7dd6b133f9b 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -114,7 +114,10 @@ module API :namespace_id, :public, :visibility_level, - :import_url] + :import_url, + :build_allow_git_fetch, + :build_timeout, + :build_coverage_regex] attrs = map_public_to_visibility_level(attrs) @project = ::Projects::CreateService.new(current_user, attrs).execute if @project.saved? @@ -159,7 +162,10 @@ module API :shared_runners_enabled, :public, :visibility_level, - :import_url] + :import_url, + :build_allow_git_fetch, + :build_timeout, + :build_coverage_regex] attrs = map_public_to_visibility_level(attrs) @project = ::Projects::CreateService.new(user, attrs).execute if @project.saved? @@ -215,7 +221,10 @@ module API :snippets_enabled, :shared_runners_enabled, :public, - :visibility_level] + :visibility_level, + :build_allow_git_fetch, + :build_timeout, + :build_coverage_regex] attrs = map_public_to_visibility_level(attrs) authorize_admin_project authorize! :rename_project, user_project if attrs[:name].present? From 097faeb481db2a4956b41049c041d55f5da4e2c1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 Jan 2016 16:24:42 +0100 Subject: [PATCH 102/280] Get "Merge when build succeeds" to work when commits were pushed to MR target branch while builds were running --- CHANGELOG | 1 + .../projects/merge_requests_controller.rb | 2 +- app/models/merge_request.rb | 16 +++++++++++----- .../projects/merge_requests/_show.html.haml | 2 +- lib/api/merge_requests.rb | 2 +- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d841b149615..240fa43d6cb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,7 @@ v 8.4.0 (unreleased) - Fix API project lookups when querying with a namespace with dots (Stan Hu) v 8.3.3 (unreleased) + - Get "Merge when build succeeds" to work when commits were pushed to MR target branch while builds were running - Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu) - Enable "Add key" button when user fills in a proper key (Stan Hu) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index ab5c953189c..de948d271c8 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -153,7 +153,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def merge_check - @merge_request.check_if_can_be_merged if @merge_request.unchecked? + @merge_request.check_if_can_be_merged render partial: "projects/merge_requests/widget/show.html.haml", layout: false end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ac25d38eb63..30d0c2b5961 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -229,6 +229,8 @@ class MergeRequest < ActiveRecord::Base end def check_if_can_be_merged + return unless unchecked? + can_be_merged = project.repository.can_be_merged?(source_sha, target_branch) @@ -252,7 +254,11 @@ class MergeRequest < ActiveRecord::Base end def mergeable? - open? && !work_in_progress? && can_be_merged? + return false unless open? && !work_in_progress? + + check_if_can_be_merged + + can_be_merged? end def gitlab_merge_status @@ -452,6 +458,10 @@ class MergeRequest < ActiveRecord::Base !source_branch_exists? || !target_branch_exists? end + def broken? + self.commits.blank? || branch_missing? || cannot_be_merged? + end + def can_be_merged_by?(user) ::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch) end @@ -507,8 +517,4 @@ class MergeRequest < ActiveRecord::Base def ci_commit @ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project end - - def broken? - self.commits.blank? || branch_missing? || cannot_be_merged? - end end diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index ba7c2c01e93..095876450a0 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -38,7 +38,7 @@ = 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) + - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user) .light.prepend-top-default You can also accept this merge request manually using the = succeed '.' do diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 3c1c6bda260..5c97fe1c88c 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -211,7 +211,7 @@ module API unauthorized! unless merge_request.can_be_merged_by?(current_user) not_allowed! if !merge_request.open? || merge_request.work_in_progress? - merge_request.check_if_can_be_merged if merge_request.unchecked? + merge_request.check_if_can_be_merged render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged? From 0bab4788ef870945feccbb102834fd89433dfef2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 Jan 2016 16:31:05 +0100 Subject: [PATCH 103/280] Satisfy Rubocop --- spec/models/project_services/asana_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb index 0db48c75d1d..f3d15f3c1ea 100644 --- a/spec/models/project_services/asana_service_spec.rb +++ b/spec/models/project_services/asana_service_spec.rb @@ -41,7 +41,7 @@ describe AsanaService, models: true do let(:project) { create(:project) } def create_data_for_commits(*messages) - data = { + { object_kind: 'push', ref: 'master', user_name: user.name, From 0b661516324862506d5ec30c44cac704346a90ad Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 Jan 2016 16:45:53 +0100 Subject: [PATCH 104/280] Remove icon from milestone reference. --- app/models/milestone.rb | 2 +- app/views/shared/issuable/_sidebar.html.haml | 3 ++- lib/banzai/filter/milestone_reference_filter.rb | 2 -- spec/lib/banzai/filter/milestone_reference_filter_spec.rb | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index e47b6440746..eaa2db2e247 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -76,7 +76,7 @@ class Milestone < ActiveRecord::Base end def reference_link_text(from_project = nil) - %Q{ }.html_safe + self.title + self.title end def expired? diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 9d65a621e53..79c5cc7f40a 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -28,7 +28,8 @@ %span.back-to-milestone = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do %strong - = issuable.milestone.reference_link_text + = icon('clock-o') + = issuable.milestone.title - else .light None .selectbox diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index f99202af5e8..e88b27c1fae 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -3,8 +3,6 @@ require 'banzai' module Banzai module Filter # HTML filter that replaces milestone references with links. - # - # This filter supports cross-project references. class MilestoneReferenceFilter < AbstractReferenceFilter def self.object_class Milestone diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index c53e780d354..86b71210100 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -29,7 +29,7 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do it 'links with adjacent text' do doc = reference_filter("milestone (#{reference}.)") - expect(doc.to_html).to match(/\(<\/i> #{Regexp.escape(milestone.title)}<\/a>\.\)/) + expect(doc.to_html).to match(/\(#{Regexp.escape(milestone.title)}<\/a>\.\)/) end it 'includes a title attribute' do @@ -41,7 +41,7 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do milestone.update_attribute(:title, %{">whatever Date: Tue, 5 Jan 2016 18:17:55 +0100 Subject: [PATCH 105/280] Fix notification spec --- spec/services/notification_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index b5c7b01357a..6d219f35895 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -53,7 +53,7 @@ describe NotificationService, services: true do add_users_with_subscription(note.project, issue) # Ensure create SentNotification by noteable = issue 6 times, not noteable = note - expect(SentNotification).to receive(:record).with(issue, any_args).exactly(6).times + expect(SentNotification).to receive(:record).with(issue, any_args).exactly(7).times ActionMailer::Base.deliveries.clear From 95f1fe724a47b12b1d66a722be13d15c5fbd2688 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 21 Dec 2015 16:52:45 -0200 Subject: [PATCH 106/280] Import GitHub Pull Requests into GitLab --- lib/gitlab/github_import/importer.rb | 64 +++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index b5720f6e2cb..656fac9ac7e 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -12,7 +12,16 @@ module Gitlab end def execute - #Issues && Comments + import_issues + import_pull_requests + + true + end + + private + + def import_issues + # Issues && Comments client.list_issues(project.import_source, state: :all, sort: :created, direction: :asc).each do |issue| @@ -33,18 +42,59 @@ module Gitlab description: body, title: issue.title, state: issue.state == 'closed' ? 'closed' : 'opened', - author_id: gl_user_id(project, issue.user.id) + author_id: gl_author_id(project, issue.user.id) ) end end end - private + def import_pull_requests + client.pull_requests(project.import_source, state: :all, + sort: :created, + direction: :asc).each do |pull_request| + body = @formatter.author_line(pull_request.user.login) + body += pull_request.body || "" - def gl_user_id(project, github_id) - user = User.joins(:identities). - find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s) - (user && user.id) || project.creator_id + source_branch = pull_request.head.ref + target_branch = pull_request.base.ref + + merge_request = MergeRequest.create!( + title: pull_request.title, + description: body, + source_project: project, + source_branch: source_branch, + target_project: project, + target_branch: target_branch, + state: merge_request_state(pull_request), + author_id: gl_author_id(project, pull_request.user.id), + assignee_id: gl_user_id(pull_request.assignee.try(:id)), + created_at: pull_request.created_at, + updated_at: pull_request.updated_at + ) + end + end + + def merge_request_state(pull_request) + case true + when pull_request.state == 'closed' && pull_request.merged_at.present? + 'merged' + when pull_request.state == 'closed' + 'closed' + else + 'opened' + end + end + + def gl_author_id(project, github_id) + gl_user_id(github_id) || project.creator_id + end + + def gl_user_id(github_id) + if github_id + User.joins(:identities). + find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s). + try(:id) + end end end end From 223f7f53453d544a8c46c75ca9c54b8b60620961 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 21 Dec 2015 19:08:54 -0200 Subject: [PATCH 107/280] Import comments on GitHub Pull Request into GitLab --- lib/gitlab/github_import/importer.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 656fac9ac7e..35a2930d4a1 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -71,9 +71,23 @@ module Gitlab created_at: pull_request.created_at, updated_at: pull_request.updated_at ) + + client.issue_comments(project.import_source, pull_request.number).each do |c| + merge_request.notes.create!( + project: project, + note: format_body(c.user.login, c.body), + author_id: gl_author_id(project, c.user.id), + created_at: c.created_at, + updated_at: c.updated_at + ) + end end end + def format_body(author, body) + @formatter.author_line(author) + (body || "") + end + def merge_request_state(pull_request) case true when pull_request.state == 'closed' && pull_request.merged_at.present? From f19bf0eaa73007081f2ab10f6a0fb176d5356e36 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 21 Dec 2015 19:09:09 -0200 Subject: [PATCH 108/280] Import comments on the diff of a GitHub Pull Request into GitLab --- lib/gitlab/github_import/importer.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 35a2930d4a1..4b753d24601 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -81,6 +81,18 @@ module Gitlab updated_at: c.updated_at ) end + + client.pull_request_comments(project.import_source, pull_request.number).each do |c| + merge_request.notes.create!( + project: project, + note: format_body(c.user.login, c.body), + commit_id: c.commit_id, + line_code: generate_line_code(c.path, c.position), + author_id: gl_author_id(project, c.user.id), + created_at: c.created_at, + updated_at: c.updated_at + ) + end end end @@ -99,6 +111,10 @@ module Gitlab end end + def generate_line_code(file_path, position) + Gitlab::Diff::LineCode.generate(file_path, position, 0) + end + def gl_author_id(project, github_id) gl_user_id(github_id) || project.creator_id end From d72b25811e3f0b722ae1c0906e2fe7dffd312403 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 21 Dec 2015 19:42:56 -0200 Subject: [PATCH 109/280] Doesn't import GitHub PR where branches were no longer available --- lib/gitlab/github_import/importer.rb | 80 +++++++++++++++------------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 4b753d24601..f8a9e0d55ab 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -52,50 +52,56 @@ module Gitlab client.pull_requests(project.import_source, state: :all, sort: :created, direction: :asc).each do |pull_request| - body = @formatter.author_line(pull_request.user.login) - body += pull_request.body || "" + source_branch = find_branch(pull_request.head.ref) + target_branch = find_branch(pull_request.base.ref) - source_branch = pull_request.head.ref - target_branch = pull_request.base.ref - - merge_request = MergeRequest.create!( - title: pull_request.title, - description: body, - source_project: project, - source_branch: source_branch, - target_project: project, - target_branch: target_branch, - state: merge_request_state(pull_request), - author_id: gl_author_id(project, pull_request.user.id), - assignee_id: gl_user_id(pull_request.assignee.try(:id)), - created_at: pull_request.created_at, - updated_at: pull_request.updated_at - ) - - client.issue_comments(project.import_source, pull_request.number).each do |c| - merge_request.notes.create!( - project: project, - note: format_body(c.user.login, c.body), - author_id: gl_author_id(project, c.user.id), - created_at: c.created_at, - updated_at: c.updated_at + if source_branch && target_branch + # Pull Request + merge_request = MergeRequest.create!( + title: pull_request.title, + description: format_body(pull_request.user.login, pull_request.body), + source_project: project, + source_branch: source_branch.name, + target_project: project, + target_branch: target_branch.name, + state: merge_request_state(pull_request), + author_id: gl_author_id(project, pull_request.user.id), + assignee_id: gl_user_id(pull_request.assignee.try(:id)), + created_at: pull_request.created_at, + updated_at: pull_request.updated_at ) - end - client.pull_request_comments(project.import_source, pull_request.number).each do |c| - merge_request.notes.create!( - project: project, - note: format_body(c.user.login, c.body), - commit_id: c.commit_id, - line_code: generate_line_code(c.path, c.position), - author_id: gl_author_id(project, c.user.id), - created_at: c.created_at, - updated_at: c.updated_at - ) + # Comments on Pull Request + client.issue_comments(project.import_source, pull_request.number).each do |c| + merge_request.notes.create!( + project: project, + note: format_body(c.user.login, c.body), + author_id: gl_author_id(project, c.user.id), + created_at: c.created_at, + updated_at: c.updated_at + ) + end + + # Comments on Pull Request diff + client.pull_request_comments(project.import_source, pull_request.number).each do |c| + merge_request.notes.create!( + project: project, + note: format_body(c.user.login, c.body), + commit_id: c.commit_id, + line_code: generate_line_code(c.path, c.position), + author_id: gl_author_id(project, c.user.id), + created_at: c.created_at, + updated_at: c.updated_at + ) + end end end end + def find_branch(name) + project.repository.find_branch(name) + end + def format_body(author, body) @formatter.author_line(author) + (body || "") end From 6c846ef83d51a176002027e89245a4ea62b4f2bf Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 21 Dec 2015 21:17:57 -0200 Subject: [PATCH 110/280] Extract methods to import comments on a GitHub Pull Request --- lib/gitlab/github_import/importer.rb | 52 +++++++++++++++------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index f8a9e0d55ab..2c64f5cebc7 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -56,7 +56,6 @@ module Gitlab target_branch = find_branch(pull_request.base.ref) if source_branch && target_branch - # Pull Request merge_request = MergeRequest.create!( title: pull_request.title, description: format_body(pull_request.user.login, pull_request.body), @@ -71,33 +70,38 @@ module Gitlab updated_at: pull_request.updated_at ) - # Comments on Pull Request - client.issue_comments(project.import_source, pull_request.number).each do |c| - merge_request.notes.create!( - project: project, - note: format_body(c.user.login, c.body), - author_id: gl_author_id(project, c.user.id), - created_at: c.created_at, - updated_at: c.updated_at - ) - end - - # Comments on Pull Request diff - client.pull_request_comments(project.import_source, pull_request.number).each do |c| - merge_request.notes.create!( - project: project, - note: format_body(c.user.login, c.body), - commit_id: c.commit_id, - line_code: generate_line_code(c.path, c.position), - author_id: gl_author_id(project, c.user.id), - created_at: c.created_at, - updated_at: c.updated_at - ) - end + import_comments_on_pull_request(merge_request, pull_request) + import_comments_on_pull_request_diff(merge_request, pull_request) end end end + def import_comments_on_pull_request(merge_request, pull_request) + client.issue_comments(project.import_source, pull_request.number).each do |c| + merge_request.notes.create!( + project: project, + note: format_body(c.user.login, c.body), + author_id: gl_author_id(project, c.user.id), + created_at: c.created_at, + updated_at: c.updated_at + ) + end + end + + def import_comments_on_pull_request_diff(merge_request, pull_request) + client.pull_request_comments(project.import_source, pull_request.number).each do |c| + merge_request.notes.create!( + project: project, + note: format_body(c.user.login, c.body), + commit_id: c.commit_id, + line_code: generate_line_code(c.path, c.position), + author_id: gl_author_id(project, c.user.id), + created_at: c.created_at, + updated_at: c.updated_at + ) + end + end + def find_branch(name) project.repository.find_branch(name) end From dc72a8b30502dd28bf850c2dfdbf31b687fde5d3 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 23 Dec 2015 15:04:46 -0200 Subject: [PATCH 111/280] Refactoring GithubImport::Importer --- lib/gitlab/github_import/comment.rb | 59 +++++++ lib/gitlab/github_import/importer.rb | 73 ++------- lib/gitlab/github_import/pull_request.rb | 103 ++++++++++++ spec/lib/gitlab/github_import/comment_spec.rb | 80 +++++++++ .../gitlab/github_import/pull_request_spec.rb | 153 ++++++++++++++++++ 5 files changed, 407 insertions(+), 61 deletions(-) create mode 100644 lib/gitlab/github_import/comment.rb create mode 100644 lib/gitlab/github_import/pull_request.rb create mode 100644 spec/lib/gitlab/github_import/comment_spec.rb create mode 100644 spec/lib/gitlab/github_import/pull_request_spec.rb diff --git a/lib/gitlab/github_import/comment.rb b/lib/gitlab/github_import/comment.rb new file mode 100644 index 00000000000..55de78f889d --- /dev/null +++ b/lib/gitlab/github_import/comment.rb @@ -0,0 +1,59 @@ +module Gitlab + module GithubImport + class Comment + attr_reader :project, :raw_data + + def initialize(project, raw_data) + @project = project + @raw_data = raw_data + @formatter = Gitlab::ImportFormatter.new + end + + def attributes + { + project: project, + note: note, + commit_id: raw_data.commit_id, + line_code: line_code, + author_id: author_id, + created_at: raw_data.created_at, + updated_at: raw_data.updated_at + } + end + + private + + def author + raw_data.user.login + end + + def author_id + gl_user_id(raw_data.user.id) || project.creator_id + end + + def body + raw_data.body || "" + end + + def line_code + if on_diff? + Gitlab::Diff::LineCode.generate(raw_data.path, raw_data.position, 0) + end + end + + def on_diff? + raw_data.path && raw_data.position + end + + def note + @formatter.author_line(author) + body + end + + def gl_user_id(github_id) + User.joins(:identities). + find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s). + try(:id) + end + end + end +end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 2c64f5cebc7..7c495655012 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -51,80 +51,31 @@ module Gitlab def import_pull_requests client.pull_requests(project.import_source, state: :all, sort: :created, - direction: :asc).each do |pull_request| - source_branch = find_branch(pull_request.head.ref) - target_branch = find_branch(pull_request.base.ref) + direction: :asc).each do |raw_data| + pull_request = PullRequest.new(project, raw_data) - if source_branch && target_branch - merge_request = MergeRequest.create!( - title: pull_request.title, - description: format_body(pull_request.user.login, pull_request.body), - source_project: project, - source_branch: source_branch.name, - target_project: project, - target_branch: target_branch.name, - state: merge_request_state(pull_request), - author_id: gl_author_id(project, pull_request.user.id), - assignee_id: gl_user_id(pull_request.assignee.try(:id)), - created_at: pull_request.created_at, - updated_at: pull_request.updated_at - ) - - import_comments_on_pull_request(merge_request, pull_request) - import_comments_on_pull_request_diff(merge_request, pull_request) + if pull_request.valid? + merge_request = MergeRequest.create!(pull_request.attributes) + import_comments_on_pull_request(merge_request, raw_data) + import_comments_on_pull_request_diff(merge_request, raw_data) end end end def import_comments_on_pull_request(merge_request, pull_request) - client.issue_comments(project.import_source, pull_request.number).each do |c| - merge_request.notes.create!( - project: project, - note: format_body(c.user.login, c.body), - author_id: gl_author_id(project, c.user.id), - created_at: c.created_at, - updated_at: c.updated_at - ) + client.issue_comments(project.import_source, pull_request.number).each do |raw_data| + comment = Comment.new(project, raw_data) + merge_request.notes.create!(comment.attributes) end end def import_comments_on_pull_request_diff(merge_request, pull_request) - client.pull_request_comments(project.import_source, pull_request.number).each do |c| - merge_request.notes.create!( - project: project, - note: format_body(c.user.login, c.body), - commit_id: c.commit_id, - line_code: generate_line_code(c.path, c.position), - author_id: gl_author_id(project, c.user.id), - created_at: c.created_at, - updated_at: c.updated_at - ) + client.pull_request_comments(project.import_source, pull_request.number).each do |raw_data| + comment = Comment.new(project, raw_data) + merge_request.notes.create!(comment.attributes) end end - def find_branch(name) - project.repository.find_branch(name) - end - - def format_body(author, body) - @formatter.author_line(author) + (body || "") - end - - def merge_request_state(pull_request) - case true - when pull_request.state == 'closed' && pull_request.merged_at.present? - 'merged' - when pull_request.state == 'closed' - 'closed' - else - 'opened' - end - end - - def generate_line_code(file_path, position) - Gitlab::Diff::LineCode.generate(file_path, position, 0) - end - def gl_author_id(project, github_id) gl_user_id(github_id) || project.creator_id end diff --git a/lib/gitlab/github_import/pull_request.rb b/lib/gitlab/github_import/pull_request.rb new file mode 100644 index 00000000000..61e846472f2 --- /dev/null +++ b/lib/gitlab/github_import/pull_request.rb @@ -0,0 +1,103 @@ +module Gitlab + module GithubImport + class PullRequest + attr_reader :project, :raw_data + + def initialize(project, raw_data) + @project = project + @raw_data = raw_data + @formatter = Gitlab::ImportFormatter.new + end + + def attributes + { + title: raw_data.title, + description: description, + source_project: source_project, + source_branch: source_branch.name, + target_project: target_project, + target_branch: target_branch.name, + state: state, + author_id: author_id, + assignee_id: assignee_id, + created_at: raw_data.created_at, + updated_at: updated_at + } + end + + def valid? + source_branch.present? && target_branch.present? + end + + private + + def assigned? + raw_data.assignee.present? + end + + def assignee_id + if assigned? + gl_user_id(raw_data.assignee.id) + end + end + + def author + raw_data.user.login + end + + def author_id + gl_user_id(raw_data.user.id) || project.creator_id + end + + def body + raw_data.body || "" + end + + def description + @formatter.author_line(author) + body + end + + def source_project + project + end + + def source_branch + source_project.repository.find_branch(raw_data.head.ref) + end + + def target_project + project + end + + def target_branch + target_project.repository.find_branch(raw_data.base.ref) + end + + def state + @state ||= case true + when raw_data.state == 'closed' && raw_data.merged_at.present? + 'merged' + when raw_data.state == 'closed' + 'closed' + else + 'opened' + end + end + + def updated_at + case state + when 'merged' then raw_data.merged_at + when 'closed' then raw_data.closed_at + else + raw_data.updated_at + end + end + + def gl_user_id(github_id) + User.joins(:identities). + find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s). + try(:id) + end + end + end +end diff --git a/spec/lib/gitlab/github_import/comment_spec.rb b/spec/lib/gitlab/github_import/comment_spec.rb new file mode 100644 index 00000000000..ff6b115574b --- /dev/null +++ b/spec/lib/gitlab/github_import/comment_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::Comment, lib: true do + let(:project) { create(:project) } + let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') } + let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') } + let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') } + let(:base_data) do + { + body: "I'm having a problem with this.", + user: octocat, + created_at: created_at, + updated_at: updated_at + } + end + + subject(:comment) { described_class.new(project, raw_data)} + + describe '#attributes' do + context 'when do not reference a portion of the diff' do + let(:raw_data) { OpenStruct.new(base_data) } + + it 'returns formatted attributes' do + expected = { + project: project, + note: "*Created by: octocat*\n\nI'm having a problem with this.", + commit_id: nil, + line_code: nil, + author_id: project.creator_id, + created_at: created_at, + updated_at: updated_at + } + + expect(comment.attributes).to eq(expected) + end + end + + context 'when on a portion of the diff' do + let(:diff_data) do + { + body: 'Great stuff', + commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e', + diff_hunk: '@@ -16,33 +16,40 @@ public class Connection : IConnection...', + path: 'file1.txt', + position: 1 + } + end + + let(:raw_data) { OpenStruct.new(base_data.merge(diff_data)) } + + it 'returns formatted attributes' do + expected = { + project: project, + note: "*Created by: octocat*\n\nGreat stuff", + commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e', + line_code: 'ce1be0ff4065a6e9415095c95f25f47a633cef2b_0_1', + author_id: project.creator_id, + created_at: created_at, + updated_at: updated_at + } + + expect(comment.attributes).to eq(expected) + end + end + + context 'when author is a GitLab user' do + let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) } + + it 'returns project#creator_id as author_id when is not a GitLab user' do + expect(comment.attributes.fetch(:author_id)).to eq project.creator_id + end + + it 'returns GitLab user id as author_id when is a GitLab user' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(comment.attributes.fetch(:author_id)).to eq gl_user.id + end + end + end +end diff --git a/spec/lib/gitlab/github_import/pull_request_spec.rb b/spec/lib/gitlab/github_import/pull_request_spec.rb new file mode 100644 index 00000000000..6ac32a78955 --- /dev/null +++ b/spec/lib/gitlab/github_import/pull_request_spec.rb @@ -0,0 +1,153 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::PullRequest, lib: true do + let(:project) { create(:project) } + let(:source_branch) { OpenStruct.new(ref: 'feature') } + let(:target_branch) { OpenStruct.new(ref: 'master') } + let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') } + let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } + let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } + let(:base_data) do + { + state: 'open', + title: 'New feature', + body: 'Please pull these awesome changes', + head: source_branch, + base: target_branch, + assignee: nil, + user: octocat, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + merged_at: nil + } + end + + subject(:pull_request) { described_class.new(project, raw_data)} + + describe '#attributes' do + context 'when pull request is open' do + let(:raw_data) { OpenStruct.new(base_data.merge(state: 'open')) } + + it 'returns formatted attributes' do + expected = { + title: 'New feature', + description: "*Created by: octocat*\n\nPlease pull these awesome changes", + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master', + state: 'opened', + author_id: project.creator_id, + assignee_id: nil, + created_at: created_at, + updated_at: updated_at + } + + expect(pull_request.attributes).to eq(expected) + end + end + + context 'when pull request is closed' do + let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } + let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', closed_at: closed_at)) } + + it 'returns formatted attributes' do + expected = { + title: 'New feature', + description: "*Created by: octocat*\n\nPlease pull these awesome changes", + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master', + state: 'closed', + author_id: project.creator_id, + assignee_id: nil, + created_at: created_at, + updated_at: closed_at + } + + expect(pull_request.attributes).to eq(expected) + end + end + + context 'when pull request is merged' do + let(:merged_at) { DateTime.strptime('2011-01-28T13:01:12Z') } + let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', merged_at: merged_at)) } + + it 'returns formatted attributes' do + expected = { + title: 'New feature', + description: "*Created by: octocat*\n\nPlease pull these awesome changes", + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master', + state: 'merged', + author_id: project.creator_id, + assignee_id: nil, + created_at: created_at, + updated_at: merged_at + } + + expect(pull_request.attributes).to eq(expected) + end + end + + context 'when it is assigned to someone' do + let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) } + + it 'returns nil as assigned_id when is not a GitLab user' do + expect(pull_request.attributes.fetch(:assignee_id)).to be_nil + end + + it 'returns GitLab user id as assigned_id when is a GitLab user' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id + end + end + + context 'when author is a GitLab user' do + let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) } + + it 'returns project#creator_id as author_id when is not a GitLab user' do + expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id + end + + it 'returns GitLab user id as author_id when is a GitLab user' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id + end + end + end + + describe '#valid?' do + let(:invalid_branch) { OpenStruct.new(ref: 'invalid-branch') } + + context 'when source and target branches exists' do + let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: target_branch)) } + + it 'returns true' do + expect(pull_request.valid?).to eq true + end + end + + context 'when source branch doesn not exists' do + let(:raw_data) { OpenStruct.new(base_data.merge(head: invalid_branch, base: target_branch)) } + + it 'returns false' do + expect(pull_request.valid?).to eq false + end + end + + context 'when target branch doesn not exists' do + let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: invalid_branch)) } + + it 'returns false' do + expect(pull_request.valid?).to eq false + end + end + end +end From 98909dd12cd27b85921962326bcaf651c092dcd5 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 23 Dec 2015 20:02:59 -0200 Subject: [PATCH 112/280] Generate separate comments when importing GitHub Issues into GitLab --- lib/gitlab/github_import/base_formatter.rb | 21 +++ .../{comment.rb => comment_formatter.rb} | 18 +-- lib/gitlab/github_import/importer.rb | 59 +++----- lib/gitlab/github_import/issue_formatter.rb | 66 +++++++++ ...l_request.rb => pull_request_formatter.rb} | 22 +-- ...ment_spec.rb => comment_formatter_spec.rb} | 2 +- .../github_import/issue_formatter_spec.rb | 139 ++++++++++++++++++ ...spec.rb => pull_request_formatter_spec.rb} | 15 +- 8 files changed, 266 insertions(+), 76 deletions(-) create mode 100644 lib/gitlab/github_import/base_formatter.rb rename lib/gitlab/github_import/{comment.rb => comment_formatter.rb} (63%) create mode 100644 lib/gitlab/github_import/issue_formatter.rb rename lib/gitlab/github_import/{pull_request.rb => pull_request_formatter.rb} (80%) rename spec/lib/gitlab/github_import/{comment_spec.rb => comment_formatter_spec.rb} (97%) create mode 100644 spec/lib/gitlab/github_import/issue_formatter_spec.rb rename spec/lib/gitlab/github_import/{pull_request_spec.rb => pull_request_formatter_spec.rb} (92%) diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb new file mode 100644 index 00000000000..202263c6742 --- /dev/null +++ b/lib/gitlab/github_import/base_formatter.rb @@ -0,0 +1,21 @@ +module Gitlab + module GithubImport + class BaseFormatter + attr_reader :formatter, :project, :raw_data + + def initialize(project, raw_data) + @project = project + @raw_data = raw_data + @formatter = Gitlab::ImportFormatter.new + end + + private + + def gl_user_id(github_id) + User.joins(:identities). + find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s). + try(:id) + end + end + end +end diff --git a/lib/gitlab/github_import/comment.rb b/lib/gitlab/github_import/comment_formatter.rb similarity index 63% rename from lib/gitlab/github_import/comment.rb rename to lib/gitlab/github_import/comment_formatter.rb index 55de78f889d..7d58e53991a 100644 --- a/lib/gitlab/github_import/comment.rb +++ b/lib/gitlab/github_import/comment_formatter.rb @@ -1,14 +1,6 @@ module Gitlab module GithubImport - class Comment - attr_reader :project, :raw_data - - def initialize(project, raw_data) - @project = project - @raw_data = raw_data - @formatter = Gitlab::ImportFormatter.new - end - + class CommentFormatter < BaseFormatter def attributes { project: project, @@ -46,13 +38,7 @@ module Gitlab end def note - @formatter.author_line(author) + body - end - - def gl_user_id(github_id) - User.joins(:identities). - find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s). - try(:id) + formatter.author_line(author) + body end end end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 7c495655012..38ca7372202 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -21,29 +21,17 @@ module Gitlab private def import_issues - # Issues && Comments client.list_issues(project.import_source, state: :all, sort: :created, - direction: :asc).each do |issue| - if issue.pull_request.nil? + direction: :asc).each do |raw_data| + gh_issue = IssueFormatter.new(project, raw_data) - body = @formatter.author_line(issue.user.login) - body += issue.body || "" + if gh_issue.valid? + issue = Issue.create!(gh_issue.attributes) - if issue.comments > 0 - body += @formatter.comments_header - - client.issue_comments(project.import_source, issue.number).each do |c| - body += @formatter.comment(c.user.login, c.created_at, c.body) - end + if gh_issue.has_comments? + import_comments(gh_issue.number, issue) end - - project.issues.create!( - description: body, - title: issue.title, - state: issue.state == 'closed' ? 'closed' : 'opened', - author_id: gl_author_id(project, issue.user.id) - ) end end end @@ -52,39 +40,30 @@ module Gitlab client.pull_requests(project.import_source, state: :all, sort: :created, direction: :asc).each do |raw_data| - pull_request = PullRequest.new(project, raw_data) + pull_request = PullRequestFormatter.new(project, raw_data) if pull_request.valid? merge_request = MergeRequest.create!(pull_request.attributes) - import_comments_on_pull_request(merge_request, raw_data) - import_comments_on_pull_request_diff(merge_request, raw_data) + import_comments(pull_request.number, merge_request) + import_comments_on_diff(pull_request.number, merge_request) end end end - def import_comments_on_pull_request(merge_request, pull_request) - client.issue_comments(project.import_source, pull_request.number).each do |raw_data| - comment = Comment.new(project, raw_data) - merge_request.notes.create!(comment.attributes) - end + def import_comments(issue_number, noteable) + comments = client.issue_comments(project.import_source, issue_number) + create_comments(comments, noteable) end - def import_comments_on_pull_request_diff(merge_request, pull_request) - client.pull_request_comments(project.import_source, pull_request.number).each do |raw_data| - comment = Comment.new(project, raw_data) - merge_request.notes.create!(comment.attributes) - end + def import_comments_on_diff(pull_request_number, merge_request) + comments = client.pull_request_comments(project.import_source, pull_request_number) + create_comments(comments, merge_request) end - def gl_author_id(project, github_id) - gl_user_id(github_id) || project.creator_id - end - - def gl_user_id(github_id) - if github_id - User.joins(:identities). - find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s). - try(:id) + def create_comments(comments, noteable) + comments.each do |raw_data| + comment = CommentFormatter.new(project, raw_data) + noteable.notes.create!(comment.attributes) end end end diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb new file mode 100644 index 00000000000..1e3ba44f27c --- /dev/null +++ b/lib/gitlab/github_import/issue_formatter.rb @@ -0,0 +1,66 @@ +module Gitlab + module GithubImport + class IssueFormatter < BaseFormatter + def attributes + { + project: project, + title: raw_data.title, + description: description, + state: state, + author_id: author_id, + assignee_id: assignee_id, + created_at: raw_data.created_at, + updated_at: updated_at + } + end + + def has_comments? + raw_data.comments > 0 + end + + def number + raw_data.number + end + + def valid? + raw_data.pull_request.nil? + end + + private + + def assigned? + raw_data.assignee.present? + end + + def assignee_id + if assigned? + gl_user_id(raw_data.assignee.id) + end + end + + def author + raw_data.user.login + end + + def author_id + gl_user_id(raw_data.user.id) || project.creator_id + end + + def body + raw_data.body || "" + end + + def description + @formatter.author_line(author) + body + end + + def state + raw_data.state == 'closed' ? 'closed' : 'opened' + end + + def updated_at + state == 'closed' ? raw_data.closed_at : raw_data.updated_at + end + end + end +end diff --git a/lib/gitlab/github_import/pull_request.rb b/lib/gitlab/github_import/pull_request_formatter.rb similarity index 80% rename from lib/gitlab/github_import/pull_request.rb rename to lib/gitlab/github_import/pull_request_formatter.rb index 61e846472f2..42dc09c2ac5 100644 --- a/lib/gitlab/github_import/pull_request.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -1,14 +1,6 @@ module Gitlab module GithubImport - class PullRequest - attr_reader :project, :raw_data - - def initialize(project, raw_data) - @project = project - @raw_data = raw_data - @formatter = Gitlab::ImportFormatter.new - end - + class PullRequestFormatter < BaseFormatter def attributes { title: raw_data.title, @@ -25,6 +17,10 @@ module Gitlab } end + def number + raw_data.number + end + def valid? source_branch.present? && target_branch.present? end @@ -54,7 +50,7 @@ module Gitlab end def description - @formatter.author_line(author) + body + formatter.author_line(author) + body end def source_project @@ -92,12 +88,6 @@ module Gitlab raw_data.updated_at end end - - def gl_user_id(github_id) - User.joins(:identities). - find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s). - try(:id) - end end end end diff --git a/spec/lib/gitlab/github_import/comment_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb similarity index 97% rename from spec/lib/gitlab/github_import/comment_spec.rb rename to spec/lib/gitlab/github_import/comment_formatter_spec.rb index ff6b115574b..a324a82e69f 100644 --- a/spec/lib/gitlab/github_import/comment_spec.rb +++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::GithubImport::Comment, lib: true do +describe Gitlab::GithubImport::CommentFormatter, lib: true do let(:project) { create(:project) } let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') } let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') } diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb new file mode 100644 index 00000000000..fd05428b322 --- /dev/null +++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb @@ -0,0 +1,139 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::IssueFormatter, lib: true do + let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) } + let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') } + let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } + let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } + + let(:base_data) do + { + number: 1347, + state: 'open', + title: 'Found a bug', + body: "I'm having a problem with this.", + assignee: nil, + user: octocat, + comments: 0, + pull_request: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil + } + end + + subject(:issue) { described_class.new(project, raw_data)} + + describe '#attributes' do + context 'when issue is open' do + let(:raw_data) { OpenStruct.new(base_data.merge(state: 'open')) } + + it 'returns formatted attributes' do + expected = { + project: project, + title: 'Found a bug', + description: "*Created by: octocat*\n\nI'm having a problem with this.", + state: 'opened', + author_id: project.creator_id, + assignee_id: nil, + created_at: created_at, + updated_at: updated_at + } + + expect(issue.attributes).to eq(expected) + end + end + + context 'when issue is closed' do + let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } + let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', closed_at: closed_at)) } + + it 'returns formatted attributes' do + expected = { + project: project, + title: 'Found a bug', + description: "*Created by: octocat*\n\nI'm having a problem with this.", + state: 'closed', + author_id: project.creator_id, + assignee_id: nil, + created_at: created_at, + updated_at: closed_at + } + + expect(issue.attributes).to eq(expected) + end + end + + context 'when it is assigned to someone' do + let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) } + + it 'returns nil as assignee_id when is not a GitLab user' do + expect(issue.attributes.fetch(:assignee_id)).to be_nil + end + + it 'returns GitLab user id as assignee_id when is a GitLab user' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id + end + end + + context 'when author is a GitLab user' do + let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) } + + it 'returns project#creator_id as author_id when is not a GitLab user' do + expect(issue.attributes.fetch(:author_id)).to eq project.creator_id + end + + it 'returns GitLab user id as author_id when is a GitLab user' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(issue.attributes.fetch(:author_id)).to eq gl_user.id + end + end + end + + describe '#has_comments?' do + context 'when number of comments is greater than zero' do + let(:raw_data) { OpenStruct.new(base_data.merge(comments: 1)) } + + it 'returns true' do + expect(issue.has_comments?).to eq true + end + end + + context 'when number of comments is equal to zero' do + let(:raw_data) { OpenStruct.new(base_data.merge(comments: 0)) } + + it 'returns false' do + expect(issue.has_comments?).to eq false + end + end + end + + describe '#number' do + let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) } + + it 'returns pull request number' do + expect(issue.number).to eq 1347 + end + end + + describe '#valid?' do + context 'when mention a pull request' do + let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: OpenStruct.new)) } + + it 'returns false' do + expect(issue.valid?).to eq false + end + end + + context 'when does not mention a pull request' do + let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: nil)) } + + it 'returns true' do + expect(issue.valid?).to eq true + end + end + end +end diff --git a/spec/lib/gitlab/github_import/pull_request_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb similarity index 92% rename from spec/lib/gitlab/github_import/pull_request_spec.rb rename to spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 6ac32a78955..b4465ef3743 100644 --- a/spec/lib/gitlab/github_import/pull_request_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::GithubImport::PullRequest, lib: true do +describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:project) { create(:project) } let(:source_branch) { OpenStruct.new(ref: 'feature') } let(:target_branch) { OpenStruct.new(ref: 'master') } @@ -9,6 +9,7 @@ describe Gitlab::GithubImport::PullRequest, lib: true do let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:base_data) do { + number: 1347, state: 'open', title: 'New feature', body: 'Please pull these awesome changes', @@ -97,11 +98,11 @@ describe Gitlab::GithubImport::PullRequest, lib: true do context 'when it is assigned to someone' do let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) } - it 'returns nil as assigned_id when is not a GitLab user' do + it 'returns nil as assignee_id when is not a GitLab user' do expect(pull_request.attributes.fetch(:assignee_id)).to be_nil end - it 'returns GitLab user id as assigned_id when is a GitLab user' do + it 'returns GitLab user id as assignee_id when is a GitLab user' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id @@ -123,6 +124,14 @@ describe Gitlab::GithubImport::PullRequest, lib: true do end end + describe '#number' do + let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) } + + it 'returns pull request number' do + expect(pull_request.number).to eq 1347 + end + end + describe '#valid?' do let(:invalid_branch) { OpenStruct.new(ref: 'invalid-branch') } From 2dc74b48a32c64b18ca684e3adfb51ab9d87cf2b Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 23 Dec 2015 20:04:44 -0200 Subject: [PATCH 113/280] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index b0972ceab68..6c04f3967b0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -33,6 +33,7 @@ v 8.3.1 - Fix LDAP identity and user retrieval when special characters are used - Move Sidekiq-cron configuration to gitlab.yml - Enable forcing Two-Factor authentication sitewide, with optional grace period + - Import GitHub Pull Requests into GitLab v 8.3.0 - Bump rack-attack to 4.3.1 for security fix (Stan Hu) From 837a9065f0ff192d2efd55edcc2658a92c127b21 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 5 Jan 2016 15:15:36 -0200 Subject: [PATCH 114/280] Ensure that we're only importing local pull requests --- lib/gitlab/github_import/importer.rb | 2 +- .../github_import/pull_request_formatter.rb | 8 +++++++ .../pull_request_formatter_spec.rb | 22 +++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 38ca7372202..2b0afbc7b39 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -42,7 +42,7 @@ module Gitlab direction: :asc).each do |raw_data| pull_request = PullRequestFormatter.new(project, raw_data) - if pull_request.valid? + if !pull_request.cross_project? && pull_request.valid? merge_request = MergeRequest.create!(pull_request.attributes) import_comments(pull_request.number, merge_request) import_comments_on_diff(pull_request.number, merge_request) diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 42dc09c2ac5..b7c47958cc7 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -17,6 +17,10 @@ module Gitlab } end + def cross_project? + source_repo.fork == true + end + def number raw_data.number end @@ -57,6 +61,10 @@ module Gitlab project end + def source_repo + raw_data.head.repo + end + def source_branch source_project.repository.find_branch(raw_data.head.ref) end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index b4465ef3743..9aefec77f6d 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -124,6 +124,28 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end + describe '#cross_project?' do + context 'when source repo is not a fork' do + let(:local_repo) { OpenStruct.new(fork: false) } + let(:source_branch) { OpenStruct.new(ref: 'feature', repo: local_repo) } + let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch)) } + + it 'returns false' do + expect(pull_request.cross_project?).to eq false + end + end + + context 'when source repo is a fork' do + let(:forked_repo) { OpenStruct.new(fork: true) } + let(:source_branch) { OpenStruct.new(ref: 'feature', repo: forked_repo) } + let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch)) } + + it 'returns true' do + expect(pull_request.cross_project?).to eq true + end + end + end + describe '#number' do let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) } From e194b962d3cd638e72d3ea7144e20fe8a9093574 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 5 Jan 2016 15:23:48 -0200 Subject: [PATCH 115/280] Update doc for importing existing GitHub projects to GitLab --- doc/workflow/importing/import_projects_from_github.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index 2d77c6d1172..2027a055c37 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -14,7 +14,7 @@ If you want to import from a GitHub Enterprise instance, you need to use GitLab ![Importer page](github_importer/importer.png) -* To import a project, you can simple click "Add". The importer will import your repository and issues. Once the importer is done, a new GitLab project will be created with your imported data. +* To import a project, you can simple click "Add". The importer will import your repository, issues, and pull requests. Once the importer is done, a new GitLab project will be created with your imported data. ### Note -When you import your projects from GitHub, it is not possible to keep your labels and milestones. We are working on improving this in the near future. +When you import your projects from GitHub, it is not possible to keep your labels, milestones, and cross-repository pull requests. We are working on improving this in the near future. From 8393e3e04b8b336ec80cb0da49d273f709043120 Mon Sep 17 00:00:00 2001 From: Michi302 Date: Tue, 5 Jan 2016 19:05:41 +0100 Subject: [PATCH 116/280] Add missing changelog entry --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 57f0b9f30d5..698f4e5f1cf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.4.0 (unreleased) - Add CAS support (tduehr) - Add link to merge request on build detail page. - Revert back upvote and downvote button to the issue and MR pages + - Change single user API endpoint to return more detailed data (Michael Potthoff) v 8.3.2 (unreleased) - Disable --follow in `git log` to avoid loading duplicate commit data in infinite scroll (Stan Hu) From 045e8cc38c608b46924ce1ef4de3e8c2adefb1ba Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 29 Dec 2015 20:23:07 -0500 Subject: [PATCH 117/280] Update version check images to use SVG --- CHANGELOG | 1 + lib/version_check.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b0972ceab68..ed45042eb13 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,7 @@ v 8.4.0 (unreleased) - Fix version check image in Safari - Show 'All' tab by default in the builds page - Fix API project lookups when querying with a namespace with dots (Stan Hu) + - Update version check images to use SVG v 8.3.3 (unreleased) - Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu) diff --git a/lib/version_check.rb b/lib/version_check.rb index ea23344948c..91ad07feee5 100644 --- a/lib/version_check.rb +++ b/lib/version_check.rb @@ -13,6 +13,6 @@ class VersionCheck end def host - 'https://version.gitlab.com/check.png' + 'https://version.gitlab.com/check.svg' end end From db2d067eecc5d40e5f5b4e50a9d8ab505b207e54 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 5 Jan 2016 20:38:35 +0100 Subject: [PATCH 118/280] Fix project destroy callback See gitlab-org/gitlab-ee!107. --- app/models/ci/build.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 3e67b2771c1..d7fccb2197d 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -54,6 +54,8 @@ module Ci # To prevent db load megabytes of data from trace default_scope -> { select(Ci::Build.columns_without_lazy) } + before_destroy { project } + class << self def columns_without_lazy (column_names - LAZY_ATTRIBUTES).map do |column_name| @@ -145,10 +147,6 @@ module Ci end end - def project - commit.project - end - def project_id commit.project.id end From a298f694327b1241fc0d06618228e3750c20c5a1 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 5 Jan 2016 14:50:52 -0500 Subject: [PATCH 119/280] Use `User#avatar_url` instead of `avatar_icon` helper --- app/helpers/page_layout_helper.rb | 10 ++--- spec/helpers/page_layout_helper_spec.rb | 55 ++++++++----------------- 2 files changed, 22 insertions(+), 43 deletions(-) diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index b84644d6996..f2a4afebbd1 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -49,12 +49,10 @@ module PageLayoutHelper def page_image default = image_url('gitlab_logo.png') - if @project - @project.avatar_url || default - elsif @user - avatar_icon(@user) - elsif @group - @group.avatar_url || default + subject = @project || @user || @group + + if subject.present? + subject.avatar_url || default else default end diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index 300dccf50ec..83aeafcf31a 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -70,46 +70,27 @@ describe PageLayoutHelper do expect(helper.page_image).to end_with 'assets/gitlab_logo.png' end - context 'with @project' do - it 'uses Project avatar if available' do - project = double(avatar_url: 'http://example.com/uploads/avatar.png') - assign(:project, project) + %w(project user group).each do |type| + context "with @#{type} assigned" do + it "uses #{type.titlecase} avatar if available" do + object = double(avatar_url: 'http://example.com/uploads/avatar.png') + assign(type, object) - expect(helper.page_image).to eq project.avatar_url + expect(helper.page_image).to eq object.avatar_url + end + + it 'falls back to the default when avatar_url is nil' do + object = double(avatar_url: nil) + assign(type, object) + + expect(helper.page_image).to end_with 'assets/gitlab_logo.png' + end end - it 'falls back to the default' do - project = double(avatar_url: nil) - assign(:project, project) - - expect(helper.page_image).to end_with 'assets/gitlab_logo.png' - end - end - - context 'with @user' do - it 'delegates to avatar_icon helper' do - user = double('User') - assign(:user, user) - - expect(helper).to receive(:avatar_icon).with(user) - - helper.page_image - end - end - - context 'with @group' do - it 'uses Group avatar if available' do - group = double(avatar_url: 'http://example.com/uploads/avatar.png') - assign(:group, group) - - expect(helper.page_image).to eq group.avatar_url - end - - it 'falls back to the default' do - group = double(avatar_url: nil) - assign(:group, group) - - expect(helper.page_image).to end_with 'assets/gitlab_logo.png' + context "with no assignments" do + it 'falls back to the default' do + expect(helper.page_image).to end_with 'assets/gitlab_logo.png' + end end end end From 43053c2e6f03ad60f85728f36c46588979f68024 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 5 Jan 2016 14:54:59 -0500 Subject: [PATCH 120/280] Make `page_description` less magical :sparkles: --- app/helpers/page_layout_helper.rb | 12 +---------- app/views/layouts/group.html.haml | 7 ++++--- app/views/layouts/project.html.haml | 7 ++++--- spec/helpers/page_layout_helper_spec.rb | 27 ++----------------------- 4 files changed, 11 insertions(+), 42 deletions(-) diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index f2a4afebbd1..5c0dd36252e 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -27,7 +27,7 @@ module PageLayoutHelper # # Returns an HTML-safe String. def page_description(description = nil) - @page_description ||= page_description_default + @page_description ||= brand_title if description.present? @page_description = description.squish @@ -36,16 +36,6 @@ module PageLayoutHelper end end - # Default value for page_description when one hasn't been defined manually by - # a view - def page_description_default - if @project - @project.description || brand_title - else - brand_title - end - end - def page_image default = image_url('gitlab_logo.png') diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 31888c5580e..1ce8d0ef7b5 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -1,5 +1,6 @@ -- page_title @group.name -- header_title group_title(@group) unless header_title -- sidebar "group" unless sidebar +- page_title @group.name +- page_description @group.description +- header_title group_title(@group) unless header_title +- sidebar "group" unless sidebar = render template: "layouts/application" diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index abf73bcc709..f81283a5ddb 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -1,6 +1,7 @@ -- page_title @project.name_with_namespace -- header_title project_title(@project) unless header_title -- sidebar "project" unless sidebar +- page_title @project.name_with_namespace +- page_description @project.description +- header_title project_title(@project) unless header_title +- sidebar "project" unless sidebar - content_for :scripts_body_top do - project = @target_project || @project diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index 83aeafcf31a..a097786ba6d 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -2,8 +2,8 @@ require 'rails_helper' describe PageLayoutHelper do describe 'page_description' do - it 'defaults to value returned by page_description_default helper' do - allow(helper).to receive(:page_description_default).and_return('Foo') + it 'defaults to value returned by brand_title helper' do + allow(helper).to receive(:brand_title).and_return('Foo') expect(helper.page_description).to eq 'Foo' end @@ -42,29 +42,6 @@ describe PageLayoutHelper do end end - describe 'page_description_default' do - it 'uses Project description when available' do - project = double(description: 'Project Description') - assign(:project, project) - - expect(helper.page_description_default).to eq 'Project Description' - end - - it 'uses brand_title when Project description is nil' do - project = double(description: nil) - assign(:project, project) - - expect(helper).to receive(:brand_title).and_return('Brand Title') - expect(helper.page_description_default).to eq 'Brand Title' - end - - it 'falls back to brand_title' do - allow(helper).to receive(:brand_title).and_return('Brand Title') - - expect(helper.page_description_default).to eq 'Brand Title' - end - end - describe 'page_image' do it 'defaults to the GitLab logo' do expect(helper.page_image).to end_with 'assets/gitlab_logo.png' From a0793d69c538cbb6a2b9ff4389192862f6d16962 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 5 Jan 2016 15:02:42 -0500 Subject: [PATCH 121/280] Remove now-redundant `page_description` call from Projects#show --- app/views/projects/show.html.haml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index a8f924bbb7c..8436be433b1 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,5 +1,3 @@ -- page_description @project.description - = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity") @@ -70,4 +68,4 @@ = render 'projects/last_commit', commit: @repository.commit, project: @project %div{class: "project-show-#{default_project_view}"} - = render default_project_view \ No newline at end of file + = render default_project_view From 6d3b5ea2a9611dc7d87bd48043f34f9e0930e052 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 5 Jan 2016 16:47:09 -0500 Subject: [PATCH 122/280] Remove now-redundant `page_description` call from Groups#show --- app/views/groups/show.html.haml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index e7f619d2d6b..a607d860d7d 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,5 +1,3 @@ -- page_description @group.description - - unless can?(current_user, :read_group, @group) - @disable_search_panel = true From 1e6fc0c6a440ad707d990282ab7a93c178e35cfa Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 30 Dec 2015 16:44:22 -0500 Subject: [PATCH 123/280] Define a limited set of filters for SingleLinePipeline Removes the following filters from its parent GfmPipeline: - SyntaxHighlightFilter - UploadLinkFilter - TableOfContentsFilter - LabelReferenceFilter - TaskListFilter Closes #1697 --- lib/banzai/pipeline/single_line_pipeline.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb index 6725c9039a9..a3c9d4f43aa 100644 --- a/lib/banzai/pipeline/single_line_pipeline.rb +++ b/lib/banzai/pipeline/single_line_pipeline.rb @@ -3,7 +3,23 @@ require 'banzai' module Banzai module Pipeline class SingleLinePipeline < GfmPipeline + def self.filters + @filters ||= [ + Filter::SanitizationFilter, + Filter::EmojiFilter, + Filter::AutolinkFilter, + Filter::ExternalLinkFilter, + + Filter::UserReferenceFilter, + Filter::IssueReferenceFilter, + Filter::ExternalIssueReferenceFilter, + Filter::MergeRequestReferenceFilter, + Filter::SnippetReferenceFilter, + Filter::CommitRangeReferenceFilter, + Filter::CommitReferenceFilter, + ] + end end end end From 52e41dcac4d0f76de77029aae07fce60b61d86ef Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 5 Jan 2016 18:02:12 -0500 Subject: [PATCH 124/280] Fix the abuse report detail URL in the HTML email template --- app/views/abuse_report_mailer/notify.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml index 619533e09a7..2741eb44357 100644 --- a/app/views/abuse_report_mailer/notify.html.haml +++ b/app/views/abuse_report_mailer/notify.html.haml @@ -8,4 +8,4 @@ = @abuse_report.message %p - = link_to "View details", abuse_reports_url + = link_to "View details", admin_abuse_reports_url From cbdc8dd46ff2b74e5817053f774cc8f17db55f77 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 5 Jan 2016 20:42:25 -0500 Subject: [PATCH 125/280] adds ajax open and close merges, with discussion/comments working as well --- .../javascripts/merge_request.js.coffee | 43 +++++++++++++++++++ app/helpers/merge_requests_helper.rb | 4 ++ app/models/merge_request.rb | 10 ----- .../merge_requests/_discussion.html.haml | 9 ++-- .../merge_requests/show/_mr_title.html.haml | 26 ++++++----- 5 files changed, 67 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 9047587db81..8c321319b30 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -22,6 +22,7 @@ class @MergeRequest if $("a.btn-close").length @initTaskList() + @initMergeRequestBtnEventListeners() # Local jQuery finder $: (selector) -> @@ -35,6 +36,48 @@ class @MergeRequest # Show the first tab (Commits) $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show') + initMergeRequestBtnEventListeners: -> + _this = @ + mergeRequestFailMessage = 'Unable to update this merge request at this time.' + $('a.btn-close, a.btn-reopen').on 'click', (e) -> + e.preventDefault() + e.stopImmediatePropagation() + $this = $(this) + isClose = $this.hasClass('btn-close') + shouldSubmit = $this.hasClass('btn-comment') + if shouldSubmit + _this.submitNoteForm($this.closest('form')) + $this.prop('disabled', true) + url = $this.attr('href') + $.ajax + type: 'PUT', + url: url, + error: (jqXHR, textStatus, errorThrown) -> + mergeRequestStatus = if isClose then 'close' else 'open' + new Flash(mergeRequestFailMessage, 'alert') + success: (data, textStatus, jqXHR) -> + if data.saved + if isClose + $('a.btn-close').addClass('hidden') + $('a.issuable-edit').addClass('hidden') + $('a.btn-reopen').removeClass('hidden') + $('div.status-box-closed').removeClass('hidden') + $('div.status-box-open').addClass('hidden') + else + $('a.btn-reopen').addClass('hidden') + $('a.issuable-edit').removeClass('hidden') + $('a.btn-close').removeClass('hidden') + $('div.status-box-closed').addClass('hidden') + $('div.status-box-open').removeClass('hidden') + else + new Flash(mergeRequestFailMessage, 'alert') + $this.prop('disabled', false) + + submitNoteForm: (form) => + noteText = form.find("textarea.js-note-text").val() + if noteText.trim().length > 0 + form.submit() + showAllCommits: -> this.$('.first-commits').remove() this.$('.all-commits').removeClass 'hide' diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 1dd07a2a220..fafe2acd538 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -19,6 +19,10 @@ module MergeRequestsHelper } end + def merge_request_button_visibility(mr, closed) + return 'hidden' if mr.closed? == closed + end + def mr_css_classes(mr) classes = "merge-request" classes << " closed" if mr.closed? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ac25d38eb63..f013018ec08 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -456,16 +456,6 @@ class MergeRequest < ActiveRecord::Base ::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch) end - def state_human_name - if merged? - "Merged" - elsif closed? - "Closed" - else - "Open" - end - end - def target_sha @target_sha ||= target_project. repository.commit(target_branch).sha diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index bff3c3b283d..ed3628a36a3 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -1,8 +1,7 @@ - content_for :note_actions do - if can?(current_user, :update_merge_request, @merge_request) - - if @merge_request.open? - = link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" - - if @merge_request.closed? - = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" + = link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close}, format: 'json'), data: {no_turbolinks: true}, method: :put, class: "btn btn-nr btn-grouped btn-close close-mr-link btn-comment js-note-target-close #{merge_request_button_visibility(@merge_request, true)}", title: "Close merge request" + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen}, format: 'json'), data: {no_turbolinks: true}, method: :put, class: "btn btn-nr btn-grouped btn-reopen reopen-mr-link btn-comment js-note-target-reopen #{merge_request_button_visibility(@merge_request, false)}", title: "Reopen merge request" -#notes= render "projects/notes/notes_with_form" +#notes + = render "projects/notes/notes_with_form" 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 fc6fb2a0d42..7996d35c462 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,6 +1,12 @@ .detail-page-header - .status-box{ class: status_box_class(@merge_request) } - = @merge_request.state_human_name + - if @merge_request.merged? + .status-box{ class: "status-box-merged" } + Merged + - else + .status-box{ class: "status-box-closed #{merge_request_button_visibility(@merge_request, false)}"} + Closed + .status-box{ class: "status-box-open #{merge_request_button_visibility(@merge_request, true)}"} + Open %span.identifier Merge Request ##{@merge_request.iid} %span.creator @@ -15,11 +21,11 @@ = time_ago_with_tooltip(@merge_request.updated_at, placement: 'bottom') .issue-btn-group.pull-right - - if can?(current_user, :update_merge_request, @merge_request) - - if @merge_request.open? - = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: 'btn btn-nr btn-grouped btn-close', title: 'Close merge request' - = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-nr btn-grouped issuable-edit', id: 'edit_merge_request' do - %i.fa.fa-pencil-square-o - Edit - - if @merge_request.closed? - = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'btn btn-nr btn-grouped btn-reopen reopen-mr-link', title: 'Reopen merge request' + - if can?(current_user, :update_merge_request, @merge_request) && !@merge_request.merged? + + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, method: :put, class: "btn btn-nr btn-grouped btn-reopen reopen-mr-link #{merge_request_button_visibility(@merge_request, false)}", title: 'Reopen merge request' + = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, method: :put, class: "btn btn-nr btn-grouped btn-close #{merge_request_button_visibility(@merge_request, true)}", title: 'Close merge request' + + = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn btn-nr btn-grouped issuable-edit #{merge_request_button_visibility(@merge_request, true)}", id: 'edit_merge_request' do + %i.fa.fa-pencil-square-o + Edit \ No newline at end of file From 987989b632d4f610f0ec17b65f1c7d24530c99ff Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 6 Jan 2016 14:41:26 +0300 Subject: [PATCH 126/280] Remove block button on abuse reports if user is already blocked --- app/views/admin/abuse_reports/_abuse_report.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml index cf50a376e11..8aa34c0ffac 100644 --- a/app/views/admin/abuse_reports/_abuse_report.html.haml +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -23,6 +23,6 @@ data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-xs btn-remove js-remove-tr" %td - - if user + - if user && !user.blocked? = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs" = link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr" From e16d9edf9995735da7a900c35bc7be73cc481473 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 6 Jan 2016 14:48:52 +0300 Subject: [PATCH 127/280] Show that user is already blocked on Abuse Reports --- app/views/admin/abuse_reports/_abuse_report.html.haml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml index 8aa34c0ffac..853a780c576 100644 --- a/app/views/admin/abuse_reports/_abuse_report.html.haml +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -25,4 +25,7 @@ %td - if user && !user.blocked? = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs" + - else + .btn.btn-xs + Already Blocked = link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr" From 384445eca6249363c0da6d8b96e7ee030dc6fab3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 6 Jan 2016 13:02:51 +0100 Subject: [PATCH 128/280] Don't override issue page description in project layout. --- app/helpers/page_layout_helper.rb | 11 +++-------- app/views/layouts/_head.html.haml | 2 ++ app/views/layouts/group.html.haml | 2 +- app/views/layouts/project.html.haml | 2 +- spec/helpers/page_layout_helper_spec.rb | 6 ++---- 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 5c0dd36252e..82f805fa444 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -27,11 +27,9 @@ module PageLayoutHelper # # Returns an HTML-safe String. def page_description(description = nil) - @page_description ||= brand_title - if description.present? @page_description = description.squish - else + elsif @page_description.present? sanitize(@page_description, tags: []).truncate_words(30) end end @@ -41,11 +39,8 @@ module PageLayoutHelper subject = @project || @user || @group - if subject.present? - subject.avatar_url || default - else - default - end + image = subject.avatar_url if subject.present? + image || default end # Define or get attributes to be used as Twitter card metadata diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 1a2187e551b..38ca4f91c4d 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -1,3 +1,5 @@ +- page_description brand_title unless page_description + - site_name = "GitLab" %head{prefix: "og: http://ogp.me/ns#"} %meta{charset: "utf-8"} diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 1ce8d0ef7b5..2e483b7148d 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -1,5 +1,5 @@ - page_title @group.name -- page_description @group.description +- page_description @group.description unless page_description - header_title group_title(@group) unless header_title - sidebar "group" unless sidebar diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index f81283a5ddb..ab527e8e438 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -1,5 +1,5 @@ - page_title @project.name_with_namespace -- page_description @project.description +- page_description @project.description unless page_description - header_title project_title(@project) unless header_title - sidebar "project" unless sidebar diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index a097786ba6d..cf632f594c7 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -2,10 +2,8 @@ require 'rails_helper' describe PageLayoutHelper do describe 'page_description' do - it 'defaults to value returned by brand_title helper' do - allow(helper).to receive(:brand_title).and_return('Foo') - - expect(helper.page_description).to eq 'Foo' + it 'defaults to nil' do + expect(helper.page_description).to eq nil end it 'returns the last-pushed description' do From b9ed3961b55cf3dbc1a6d4c841d295dd23161c90 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 6 Jan 2016 13:25:13 +0100 Subject: [PATCH 129/280] Revert "Add DEBUG_BANZAI_CACHE env var to debug Banzai cache issue." This reverts commit 4b027bc93a7875c3937f6b90ac1049b4a4d72da5. --- lib/banzai/renderer.rb | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index 910e1c6994e..115ae914524 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -1,5 +1,7 @@ module Banzai module Renderer + CACHE_ENABLED = false + # Convert a Markdown String into an HTML-safe String of HTML # # Note that while the returned HTML will have been sanitized of dangerous @@ -18,22 +20,13 @@ module Banzai cache_key = context.delete(:cache_key) cache_key = full_cache_key(cache_key, context[:pipeline]) - cacheless = cacheless_render(text, context) - - if cache_key && ENV["DEBUG_BANZAI_CACHE"] - cached = Rails.cache.fetch(cache_key) { cacheless } - - if cached != cacheless - Rails.logger.warn "Banzai cache mismatch" - Rails.logger.warn "Text: #{text.inspect}" - Rails.logger.warn "Context: #{context.inspect}" - Rails.logger.warn "Cache key: #{cache_key.inspect}" - Rails.logger.warn "Cacheless: #{cacheless.inspect}" - Rails.logger.warn "With cache: #{cached.inspect}" + if cache_key && CACHE_ENABLED + Rails.cache.fetch(cache_key) do + cacheless_render(text, context) end + else + cacheless_render(text, context) end - - cacheless end def self.render_result(text, context = {}) From cf19efec3ac0ab4510359dd71df3d511762230c3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 6 Jan 2016 13:26:02 +0100 Subject: [PATCH 130/280] Revert "Temporarily disable Markdown caching" This reverts commit d337d5e7137d9b3fd0f9b8890a3ba9296323acc7. --- lib/banzai/renderer.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index 115ae914524..891c0fd7749 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -1,7 +1,5 @@ module Banzai module Renderer - CACHE_ENABLED = false - # Convert a Markdown String into an HTML-safe String of HTML # # Note that while the returned HTML will have been sanitized of dangerous @@ -20,7 +18,7 @@ module Banzai cache_key = context.delete(:cache_key) cache_key = full_cache_key(cache_key, context[:pipeline]) - if cache_key && CACHE_ENABLED + if cache_key Rails.cache.fetch(cache_key) do cacheless_render(text, context) end From 37ce5f312eabf95deff7aac68f6bce6ba6e106b9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 6 Jan 2016 13:33:11 +0100 Subject: [PATCH 131/280] Fix mentionable reference extraction caching. --- app/models/concerns/mentionable.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 6316ee208b5..98f71ae8cb0 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -51,8 +51,11 @@ module Mentionable else self.class.mentionable_attrs.each do |attr, options| text = send(attr) - options[:cache_key] = [self, attr] if options.delete(:cache) && self.persisted? - ext.analyze(text, options) + + context = options.dup + context[:cache_key] = [self, attr] if context.delete(:cache) && self.persisted? + + ext.analyze(text, context) end end From 18b17072c6cc7eb199d1da34a3ea481dcd53a8cf Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 6 Jan 2016 13:33:47 +0100 Subject: [PATCH 132/280] Add regression test. --- spec/models/note_spec.rb | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 593d8f76215..151a29e974b 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -125,6 +125,19 @@ describe Note, models: true do let(:set_mentionable_text) { ->(txt) { subject.note = txt } } end + describe "#all_references" do + let!(:note1) { create(:note) } + let!(:note2) { create(:note) } + + it "reads the rendered note body from the cache" do + expect(Banzai::Renderer).to receive(:render).with(note1.note, pipeline: :note, cache_key: [note1, "note"], project: note1.project) + expect(Banzai::Renderer).to receive(:render).with(note2.note, pipeline: :note, cache_key: [note2, "note"], project: note2.project) + + note1.all_references + note2.all_references + end + end + describe :search do let!(:note) { create(:note, note: "WoW") } @@ -164,7 +177,7 @@ describe Note, models: true do expect(note.editable?).to be_falsy end end - + describe "set_award!" do let(:issue) { create :issue } From f71642017ebfd409e20735b621dd3a9fe09add12 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 6 Jan 2016 07:33:50 -0500 Subject: [PATCH 133/280] adds tests (and passes them) for ajax open and close merge requests. --- .../fixtures/merge_requests_show.html.haml | 12 ++- spec/javascripts/issue_spec.js.coffee | 2 +- spec/javascripts/merge_request_spec.js.coffee | 88 +++++++++++++++++++ 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/spec/javascripts/fixtures/merge_requests_show.html.haml b/spec/javascripts/fixtures/merge_requests_show.html.haml index 8447dfdda32..fdfa8a273e2 100644 --- a/spec/javascripts/fixtures/merge_requests_show.html.haml +++ b/spec/javascripts/fixtures/merge_requests_show.html.haml @@ -1,4 +1,14 @@ -%a.btn-close +:css + .hidden { display: none !important } + +.flash-container + .flash-alert + .flash-notice + +.status-box.status-box-open Open +.status-box.status-box-closed.hidden Closed +%a.btn-close{"href" => "http://gitlab.com/merge_requests/6/close"} Close +%a.btn-reopen.hidden{"href" => "http://gitlab.com/merge_requests/6/reopen"} Reopen .detail-page-description .description.js-task-list-container diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee index 7e67c778861..e1c22860da7 100644 --- a/spec/javascripts/issue_spec.js.coffee +++ b/spec/javascripts/issue_spec.js.coffee @@ -44,7 +44,7 @@ describe 'reopen/close issue', -> expect($('div.status-box-closed')).toBeVisible() expect($('div.status-box-open')).toBeHidden() - it 'fails to closes an issue with success:false', -> + it 'fails to close an issue with success:false', -> $.ajax = (obj) -> expect(obj.type).toBe('PUT') diff --git a/spec/javascripts/merge_request_spec.js.coffee b/spec/javascripts/merge_request_spec.js.coffee index 22ebc7039d1..e21bfde38ad 100644 --- a/spec/javascripts/merge_request_spec.js.coffee +++ b/spec/javascripts/merge_request_spec.js.coffee @@ -21,3 +21,91 @@ describe 'MergeRequest', -> expect(req.data.merge_request.description).not.toBe(null) $('.js-task-list-field').trigger('tasklist:changed') + + describe 'reopen/close merge request', -> + fixture.preload('merge_requests_show.html') + beforeEach -> + fixture.load('merge_requests_show.html') + @merge_request = new MergeRequest({}) + it 'closes a merge request', -> + $.ajax = (obj) -> + expect(obj.type).toBe('PUT') + expect(obj.url).toBe('http://gitlab.com/merge_requests/6/close') + obj.success saved:true + + $btnClose = $('a.btn-close') + $btnReopen = $('a.btn-reopen') + expect($btnReopen).toBeHidden() + expect($btnClose.text()).toBe('Close') + expect(typeof $btnClose.prop('disabled')).toBe('undefined') + + $btnClose.trigger('click') + + expect($btnReopen).toBeVisible() + + expect($btnClose).toBeHidden() + expect($('div.status-box-closed')).toBeVisible() + expect($('div.status-box-open')).toBeHidden() + + it 'fails to close a merge request with success:false', -> + + $.ajax = (obj) -> + expect(obj.type).toBe('PUT') + expect(obj.url).toBe('http://goesnowhere.nothing/whereami') + obj.success saved:false + + $btnClose = $('a.btn-close') + $btnReopen = $('a.btn-reopen') + $btnClose.attr('href','http://goesnowhere.nothing/whereami') + expect($btnReopen).toBeHidden() + expect($btnClose.text()).toBe('Close') + expect(typeof $btnClose.prop('disabled')).toBe('undefined') + + $btnClose.trigger('click') + + expect($btnReopen).toBeHidden() + expect($btnClose).toBeVisible() + expect($('div.status-box-closed')).toBeHidden() + expect($('div.status-box-open')).toBeVisible() + expect($('div.flash-alert')).toBeVisible() + expect($('div.flash-alert').text()).toBe('Unable to update this merge request at this time.') + + it 'fails to closes an issue with HTTP error', -> + + $.ajax = (obj) -> + expect(obj.type).toBe('PUT') + expect(obj.url).toBe('http://goesnowhere.nothing/whereami') + obj.error() + + $btnClose = $('a.btn-close') + $btnReopen = $('a.btn-reopen') + $btnClose.attr('href','http://goesnowhere.nothing/whereami') + expect($btnReopen).toBeHidden() + expect($btnClose.text()).toBe('Close') + expect(typeof $btnClose.prop('disabled')).toBe('undefined') + + $btnClose.trigger('click') + + expect($btnReopen).toBeHidden() + expect($btnClose).toBeVisible() + expect($('div.status-box-closed')).toBeHidden() + expect($('div.status-box-open')).toBeVisible() + expect($('div.flash-alert')).toBeVisible() + expect($('div.flash-alert').text()).toBe('Unable to update this merge request at this time.') + + it 'reopens a merge request', -> + $.ajax = (obj) -> + expect(obj.type).toBe('PUT') + expect(obj.url).toBe('http://gitlab.com/merge_requests/6/reopen') + obj.success saved: true + + $btnClose = $('a.btn-close') + $btnReopen = $('a.btn-reopen') + expect($btnReopen.text()).toBe('Reopen') + + $btnReopen.trigger('click') + + expect($btnReopen).toBeHidden() + expect($btnClose).toBeVisible() + expect($('div.status-box-open')).toBeVisible() + expect($('div.status-box-closed')).toBeHidden() \ No newline at end of file From 79c0e7212af0a6b0243bc0512a75eb936fb8d862 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 6 Jan 2016 02:30:59 +0000 Subject: [PATCH 134/280] Annotate models --- app/models/application_setting.rb | 15 ++- app/models/ci/build.rb | 1 + app/models/ci/runner_project.rb | 11 +- app/models/ci/trigger.rb | 13 +- app/models/ci/variable.rb | 3 +- app/models/commit_status.rb | 55 +++++---- app/models/generic_commit_status.rb | 1 + app/models/group.rb | 1 - app/models/hooks/project_hook.rb | 1 + app/models/hooks/service_hook.rb | 1 + app/models/hooks/system_hook.rb | 1 + app/models/hooks/web_hook.rb | 1 + app/models/merge_request.rb | 44 +++---- app/models/namespace.rb | 1 - app/models/project.rb | 7 ++ app/models/project_services/asana_service.rb | 2 + .../project_services/assembla_service.rb | 1 + app/models/project_services/bamboo_service.rb | 1 + .../project_services/buildkite_service.rb | 1 + .../project_services/builds_email_service.rb | 1 + .../project_services/campfire_service.rb | 1 + app/models/project_services/ci_service.rb | 1 + .../custom_issue_tracker_service.rb | 1 + .../project_services/drone_ci_service.rb | 1 + .../emails_on_push_service.rb | 1 + .../project_services/external_wiki_service.rb | 1 + .../project_services/flowdock_service.rb | 1 + .../project_services/gemnasium_service.rb | 1 + .../project_services/gitlab_ci_service.rb | 1 + .../gitlab_issue_tracker_service.rb | 1 + .../project_services/hipchat_service.rb | 1 + app/models/project_services/irker_service.rb | 1 + .../project_services/issue_tracker_service.rb | 1 + app/models/project_services/jira_service.rb | 1 + .../pivotaltracker_service.rb | 1 + .../project_services/pushover_service.rb | 1 + .../project_services/redmine_service.rb | 1 + app/models/project_services/slack_service.rb | 1 + .../project_services/teamcity_service.rb | 1 + app/models/service.rb | 1 + app/models/user.rb | 113 +++++++++--------- spec/factories/merge_requests.rb | 41 ++++--- spec/factories/projects.rb | 7 ++ spec/models/application_setting_spec.rb | 65 ++++++---- spec/models/ci/commit_spec.rb | 2 +- spec/models/ci/runner_project_spec.rb | 11 +- spec/models/ci/trigger_spec.rb | 13 +- spec/models/ci/variable_spec.rb | 3 +- spec/models/commit_status_spec.rb | 1 + spec/models/external_wiki_service_spec.rb | 1 + spec/models/generic_commit_status_spec.rb | 1 + spec/models/group_spec.rb | 1 - spec/models/merge_request_spec.rb | 41 ++++--- spec/models/namespace_spec.rb | 1 - spec/models/project_spec.rb | 7 ++ spec/models/service_spec.rb | 1 + spec/models/user_spec.rb | 113 +++++++++--------- 57 files changed, 350 insertions(+), 254 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index be69d317d73..6c6c2468374 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -27,9 +27,20 @@ # 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) -# require_two_factor_authentication :boolean default(TRUE) +# runners_registration_token :string +# require_two_factor_authentication :boolean default(FALSE) # two_factor_grace_period :integer default(48) +# metrics_enabled :boolean default(FALSE) +# metrics_host :string default("localhost") +# metrics_username :string +# metrics_password :string +# metrics_pool_size :integer default(16) +# metrics_timeout :integer default(10) +# metrics_method_call_threshold :integer default(10) +# recaptcha_enabled :boolean default(FALSE) +# recaptcha_site_key :string +# recaptcha_private_key :string +# metrics_port :integer default(8089) # class ApplicationSetting < ActiveRecord::Base diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index d7fccb2197d..30f79fd3bfa 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -29,6 +29,7 @@ # target_url :string(255) # description :string(255) # artifacts_file :text +# gl_project_id :integer # module Ci diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb index 93d9be144e8..7b16f207a26 100644 --- a/app/models/ci/runner_project.rb +++ b/app/models/ci/runner_project.rb @@ -2,11 +2,12 @@ # # Table name: ci_runner_projects # -# id :integer not null, primary key -# runner_id :integer not null -# project_id :integer not null -# created_at :datetime -# updated_at :datetime +# id :integer not null, primary key +# runner_id :integer not null +# project_id :integer +# created_at :datetime +# updated_at :datetime +# gl_project_id :integer # module Ci diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index 23516709a41..bb98cd5c7da 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -2,12 +2,13 @@ # # Table name: ci_triggers # -# id :integer not null, primary key -# token :string(255) -# project_id :integer not null -# deleted_at :datetime -# created_at :datetime -# updated_at :datetime +# id :integer not null, primary key +# token :string(255) +# project_id :integer +# deleted_at :datetime +# created_at :datetime +# updated_at :datetime +# gl_project_id :integer # module Ci diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 56759d3e50f..7f6f497f325 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -3,12 +3,13 @@ # Table name: ci_variables # # id :integer not null, primary key -# project_id :integer not null +# project_id :integer # key :string(255) # value :text # encrypted_value :text # encrypted_value_salt :string(255) # encrypted_value_iv :string(255) +# gl_project_id :integer # module Ci diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 21c5c87bc3d..ff479493474 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -1,30 +1,35 @@ # == 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 +# Table name: ci_builds +# +# id :integer not null, primary key +# project_id :integer +# status :string(255) +# 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(255) +# deploy :boolean default(FALSE) +# options :text +# allow_failure :boolean default(FALSE), not null +# stage :string(255) +# trigger_request_id :integer +# stage_idx :integer +# tag :boolean +# ref :string(255) +# user_id :integer +# type :string(255) +# target_url :string(255) +# description :string(255) +# artifacts_file :text +# gl_project_id :integer # class CommitStatus < ActiveRecord::Base diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb index 12c934e2494..97f4f03a9a5 100644 --- a/app/models/generic_commit_status.rb +++ b/app/models/generic_commit_status.rb @@ -29,6 +29,7 @@ # target_url :string(255) # description :string(255) # artifacts_file :text +# gl_project_id :integer # class GenericCommitStatus < CommitStatus diff --git a/app/models/group.rb b/app/models/group.rb index b8f2ab6ae5d..5a31b46920c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -11,7 +11,6 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) -# public :boolean default(FALSE) # require 'carrierwave/orm/activerecord' diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index 22638057773..fa18ba5dbbe 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -15,6 +15,7 @@ # tag_push_events :boolean default(FALSE) # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) +# build_events :boolean default(FALSE), not null # class ProjectHook < WebHook diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb index 09bb3ee52a2..b333a337347 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -15,6 +15,7 @@ # tag_push_events :boolean default(FALSE) # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) +# build_events :boolean default(FALSE), not null # class ServiceHook < WebHook diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index 2f63c59b07e..d81512fae5d 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -15,6 +15,7 @@ # tag_push_events :boolean default(FALSE) # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) +# build_events :boolean default(FALSE), not null # class SystemHook < WebHook diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 40eb0e20b4b..7164c0e1e90 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -15,6 +15,7 @@ # tag_push_events :boolean default(FALSE) # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) +# build_events :boolean default(FALSE), not null # class WebHook < ActiveRecord::Base diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ac25d38eb63..1b98474fc55 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -2,28 +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) -# merge_params :text (serialized to hash) -# merge_when_build_succeeds :boolean default(false), not null -# merge_user_id :integer +# 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 +# merge_when_build_succeeds :boolean default(FALSE), not null +# merge_user_id :integer # require Rails.root.join("app/models/commit") diff --git a/app/models/namespace.rb b/app/models/namespace.rb index adafabbec07..bdb33f37495 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -11,7 +11,6 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) -# public :boolean default(FALSE) # class Namespace < ActiveRecord::Base diff --git a/app/models/project.rb b/app/models/project.rb index b1a6cfa86af..7626c698816 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -29,6 +29,13 @@ # import_source :string(255) # commit_count :integer default(0) # import_error :text +# ci_id :integer +# builds_enabled :boolean default(TRUE), not null +# shared_runners_enabled :boolean default(TRUE), not null +# runners_token :string +# build_coverage_regex :string +# build_allow_git_fetch :boolean default(TRUE), not null +# build_timeout :integer default(3600), not null # require 'carrierwave/orm/activerecord' diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 7d367e40037..792ad804575 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -16,7 +16,9 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # + require 'asana' class AsanaService < Service diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index fb7e0c0fb0d..29d841faed8 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class AssemblaService < Service diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index aa8746beb80..9e7f642180e 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class BambooService < CiService diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 199ee3a9d0d..3efbfd2eec3 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # require "addressable/uri" diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 8247c79fc33..92c9b13c9b9 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class BuildsEmailService < Service diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index e591afdda64..6e8f0842524 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class CampfireService < Service diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 88186113c68..c3f70d1f972 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # # Base class for CI services diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index 7c2027c18e6..88a3e9218cb 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class CustomIssueTrackerService < IssueTrackerService diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index 08e5ccb3855..b4724bb647e 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class DroneCiService < CiService diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 8f5d8b086eb..b831577cd97 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class EmailsOnPushService < Service diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index 74c57949b4d..b402b68665a 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class ExternalWikiService < Service diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 15c7c907f7e..8605ce66e48 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # require "flowdock-git-hook" diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 202fee042e3..61babe9cfe5 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # require "gemnasium/gitlab_service" diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index b64d97ce75d..33f0d7ea01a 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # # TODO(ayufan): The GitLabCiService is deprecated and the type should be removed when the database entries are removed diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 9558292fea3..7aa04309f54 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class GitlabIssueTrackerService < IssueTrackerService diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 1e1686a11c6..32a81808930 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class HipchatService < Service diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index d24aa317cf3..bd9b580038f 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # require 'uri' diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 936e574cccd..ed201979d39 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class IssueTrackerService < Service diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index e216f406e1c..a1b77c61576 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class JiraService < IssueTrackerService diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index ade9ee97873..c9a890c7e3f 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class PivotaltrackerService < Service diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 53edf522e9a..3d7e8bbee61 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class PushoverService < Service diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index dd9ba97ee1f..de974354c77 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class RedmineService < IssueTrackerService diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 375b4534d07..d89cf6d17b2 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class SlackService < Service diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index a63700693d7..b8e9416131a 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class TeamcityService < CiService diff --git a/app/models/service.rb b/app/models/service.rb index d3bf7f0ebd1..24f4bf7646e 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # # To add new service you should build a class inherited from Service diff --git a/app/models/user.rb b/app/models/user.rb index 20f907e4347..46b36c605b0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,62 +2,63 @@ # # Table name: users # -# id :integer not null, primary key -# email :string(255) default(""), not null -# encrypted_password :string(255) default(""), not null -# reset_password_token :string(255) -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string(255) -# last_sign_in_ip :string(255) -# created_at :datetime -# updated_at :datetime -# name :string(255) -# admin :boolean default(FALSE), not null -# projects_limit :integer default(10) -# skype :string(255) default(""), not null -# linkedin :string(255) default(""), not null -# twitter :string(255) default(""), not null -# authentication_token :string(255) -# theme_id :integer default(1), not null -# 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 -# state :string(255) -# color_scheme_id :integer default(1), not null -# notification_level :integer default(1), not null -# password_expires_at :datetime -# created_by_id :integer -# last_credential_check_at :datetime -# avatar :string(255) -# confirmation_token :string(255) -# confirmed_at :datetime -# confirmation_sent_at :datetime -# unconfirmed_email :string(255) -# hide_no_ssh_key :boolean default(FALSE) -# website_url :string(255) default(""), not null -# notification_email :string(255) -# hide_no_password :boolean default(FALSE) -# password_automatically_set :boolean default(FALSE) -# location :string(255) -# encrypted_otp_secret :string(255) -# encrypted_otp_secret_iv :string(255) -# encrypted_otp_secret_salt :string(255) -# otp_required_for_login :boolean default(FALSE), not null -# otp_backup_codes :text -# public_email :string(255) default(""), not null -# dashboard :integer default(0) -# project_view :integer default(0) -# consumed_timestep :integer -# layout :integer default(0) -# hide_project_limit :boolean default(FALSE) +# id :integer not null, primary key +# email :string(255) default(""), not null +# encrypted_password :string(255) default(""), not null +# reset_password_token :string(255) +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default(0) +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string(255) +# last_sign_in_ip :string(255) +# created_at :datetime +# updated_at :datetime +# name :string(255) +# admin :boolean default(FALSE), not null +# projects_limit :integer default(10) +# skype :string(255) default(""), not null +# linkedin :string(255) default(""), not null +# twitter :string(255) default(""), not null +# authentication_token :string(255) +# theme_id :integer default(1), not null +# bio :string(255) +# failed_attempts :integer default(0) +# locked_at :datetime +# username :string(255) +# can_create_group :boolean default(TRUE), not null +# can_create_team :boolean default(TRUE), not null +# state :string(255) +# color_scheme_id :integer default(1), not null +# notification_level :integer default(1), not null +# password_expires_at :datetime +# created_by_id :integer +# last_credential_check_at :datetime +# avatar :string(255) +# confirmation_token :string(255) +# confirmed_at :datetime +# confirmation_sent_at :datetime +# unconfirmed_email :string(255) +# hide_no_ssh_key :boolean default(FALSE) +# website_url :string(255) default(""), not null +# notification_email :string(255) +# hide_no_password :boolean default(FALSE) +# password_automatically_set :boolean default(FALSE) +# location :string(255) +# encrypted_otp_secret :string(255) +# encrypted_otp_secret_iv :string(255) +# encrypted_otp_secret_salt :string(255) +# otp_required_for_login :boolean default(FALSE), not null +# otp_backup_codes :text +# public_email :string(255) default(""), not null +# dashboard :integer default(0) +# project_view :integer default(0) +# consumed_timestep :integer +# layout :integer default(0) +# hide_project_limit :boolean default(FALSE) +# unlock_token :string +# otp_grace_period_started_at :datetime # require 'carrierwave/orm/activerecord' diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 5b4d7f41bc4..0c6a881f868 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.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 +# merge_when_build_succeeds :boolean default(FALSE), not null +# merge_user_id :integer # FactoryGirl.define do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 112213377ff..c14b99606ba 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -29,6 +29,13 @@ # import_source :string(255) # commit_count :integer default(0) # import_error :text +# ci_id :integer +# builds_enabled :boolean default(TRUE), not null +# shared_runners_enabled :boolean default(TRUE), not null +# runners_token :string +# build_coverage_regex :string +# build_allow_git_fetch :boolean default(TRUE), not null +# build_timeout :integer default(3600), not null # FactoryGirl.define do diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 35d8220ae54..91b250265e6 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -2,32 +2,45 @@ # # Table name: application_settings # -# id :integer not null, primary key -# default_projects_limit :integer -# signup_enabled :boolean -# signin_enabled :boolean -# gravatar_enabled :boolean -# sign_in_text :text -# created_at :datetime -# updated_at :datetime -# home_page_url :string(255) -# default_branch_protection :integer default(2) -# twitter_sharing_enabled :boolean default(TRUE) -# restricted_visibility_levels :text -# version_check_enabled :boolean default(TRUE) -# max_attachment_size :integer default(10), not null -# default_project_visibility :integer -# default_snippet_visibility :integer -# restricted_signup_domains :text -# user_oauth_applications :boolean default(TRUE) -# after_sign_out_path :string(255) -# session_expire_delay :integer default(10080), not null -# import_sources :text -# help_page_text :text -# 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) +# id :integer not null, primary key +# default_projects_limit :integer +# signup_enabled :boolean +# signin_enabled :boolean +# gravatar_enabled :boolean +# sign_in_text :text +# created_at :datetime +# updated_at :datetime +# home_page_url :string(255) +# default_branch_protection :integer default(2) +# twitter_sharing_enabled :boolean default(TRUE) +# restricted_visibility_levels :text +# version_check_enabled :boolean default(TRUE) +# max_attachment_size :integer default(10), not null +# default_project_visibility :integer +# default_snippet_visibility :integer +# restricted_signup_domains :text +# user_oauth_applications :boolean default(TRUE) +# after_sign_out_path :string(255) +# session_expire_delay :integer default(10080), not null +# import_sources :text +# help_page_text :text +# 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 +# require_two_factor_authentication :boolean default(FALSE) +# two_factor_grace_period :integer default(48) +# metrics_enabled :boolean default(FALSE) +# metrics_host :string default("localhost") +# metrics_username :string +# metrics_password :string +# metrics_pool_size :integer default(16) +# metrics_timeout :integer default(10) +# metrics_method_call_threshold :integer default(10) +# recaptcha_enabled :boolean default(FALSE) +# recaptcha_site_key :string +# recaptcha_private_key :string +# metrics_port :integer default(8089) # require 'spec_helper' diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index b193e16e7f8..dfc0cc3be1c 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -13,7 +13,7 @@ # tag :boolean default(FALSE) # yaml_errors :text # committed_at :datetime -# project_id :integer +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb index da8491357a5..000a732db77 100644 --- a/spec/models/ci/runner_project_spec.rb +++ b/spec/models/ci/runner_project_spec.rb @@ -2,11 +2,12 @@ # # Table name: ci_runner_projects # -# id :integer not null, primary key -# runner_id :integer not null -# project_id :integer not null -# created_at :datetime -# updated_at :datetime +# id :integer not null, primary key +# runner_id :integer not null +# project_id :integer +# created_at :datetime +# updated_at :datetime +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb index cb2f51e2011..159be939300 100644 --- a/spec/models/ci/trigger_spec.rb +++ b/spec/models/ci/trigger_spec.rb @@ -2,12 +2,13 @@ # # Table name: ci_triggers # -# id :integer not null, primary key -# token :string(255) -# project_id :integer not null -# deleted_at :datetime -# created_at :datetime -# updated_at :datetime +# id :integer not null, primary key +# token :string(255) +# project_id :integer +# deleted_at :datetime +# created_at :datetime +# updated_at :datetime +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 31b56953a13..71e84091cb7 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -3,12 +3,13 @@ # Table name: ci_variables # # id :integer not null, primary key -# project_id :integer not null +# project_id :integer # key :string(255) # value :text # encrypted_value :text # encrypted_value_salt :string(255) # encrypted_value_iv :string(255) +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index b8f901b3433..82c68ff6cb1 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -29,6 +29,7 @@ # target_url :string(255) # description :string(255) # artifacts_file :text +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/external_wiki_service_spec.rb b/spec/models/external_wiki_service_spec.rb index b198aa77526..d37978720bf 100644 --- a/spec/models/external_wiki_service_spec.rb +++ b/spec/models/external_wiki_service_spec.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # require 'spec_helper' diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index d61c1c96bde..5b0883d8702 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -29,6 +29,7 @@ # target_url :string(255) # description :string(255) # artifacts_file :text +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index ba5acceadff..3c995053eec 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -11,7 +11,6 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) -# public :boolean default(FALSE) # require 'spec_helper' diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index e0653a8327d..291e6200a5b 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.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 +# merge_when_build_succeeds :boolean default(FALSE), not null +# merge_user_id :integer # require 'spec_helper' diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 4fa2d2bc4d2..e0b3290e416 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -11,7 +11,6 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) -# public :boolean default(FALSE) # require 'spec_helper' diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 400bdf2d962..a3de23369e1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -29,6 +29,13 @@ # import_source :string(255) # commit_count :integer default(0) # import_error :text +# ci_id :integer +# builds_enabled :boolean default(TRUE), not null +# shared_runners_enabled :boolean default(TRUE), not null +# runners_token :string +# build_coverage_regex :string +# build_allow_git_fetch :boolean default(TRUE), not null +# build_timeout :integer default(3600), not null # require 'spec_helper' diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 0ca82365b98..173628c08d0 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # require 'spec_helper' diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a16161e673e..3cd63b2b0e8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,62 +2,63 @@ # # Table name: users # -# id :integer not null, primary key -# email :string(255) default(""), not null -# encrypted_password :string(255) default(""), not null -# reset_password_token :string(255) -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string(255) -# last_sign_in_ip :string(255) -# created_at :datetime -# updated_at :datetime -# name :string(255) -# admin :boolean default(FALSE), not null -# projects_limit :integer default(10) -# skype :string(255) default(""), not null -# linkedin :string(255) default(""), not null -# twitter :string(255) default(""), not null -# authentication_token :string(255) -# theme_id :integer default(1), not null -# 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 -# state :string(255) -# color_scheme_id :integer default(1), not null -# notification_level :integer default(1), not null -# password_expires_at :datetime -# created_by_id :integer -# last_credential_check_at :datetime -# avatar :string(255) -# confirmation_token :string(255) -# confirmed_at :datetime -# confirmation_sent_at :datetime -# unconfirmed_email :string(255) -# hide_no_ssh_key :boolean default(FALSE) -# website_url :string(255) default(""), not null -# notification_email :string(255) -# hide_no_password :boolean default(FALSE) -# password_automatically_set :boolean default(FALSE) -# location :string(255) -# encrypted_otp_secret :string(255) -# encrypted_otp_secret_iv :string(255) -# encrypted_otp_secret_salt :string(255) -# otp_required_for_login :boolean default(FALSE), not null -# otp_backup_codes :text -# public_email :string(255) default(""), not null -# dashboard :integer default(0) -# project_view :integer default(0) -# consumed_timestep :integer -# layout :integer default(0) -# hide_project_limit :boolean default(FALSE) +# id :integer not null, primary key +# email :string(255) default(""), not null +# encrypted_password :string(255) default(""), not null +# reset_password_token :string(255) +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default(0) +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string(255) +# last_sign_in_ip :string(255) +# created_at :datetime +# updated_at :datetime +# name :string(255) +# admin :boolean default(FALSE), not null +# projects_limit :integer default(10) +# skype :string(255) default(""), not null +# linkedin :string(255) default(""), not null +# twitter :string(255) default(""), not null +# authentication_token :string(255) +# theme_id :integer default(1), not null +# bio :string(255) +# failed_attempts :integer default(0) +# locked_at :datetime +# username :string(255) +# can_create_group :boolean default(TRUE), not null +# can_create_team :boolean default(TRUE), not null +# state :string(255) +# color_scheme_id :integer default(1), not null +# notification_level :integer default(1), not null +# password_expires_at :datetime +# created_by_id :integer +# last_credential_check_at :datetime +# avatar :string(255) +# confirmation_token :string(255) +# confirmed_at :datetime +# confirmation_sent_at :datetime +# unconfirmed_email :string(255) +# hide_no_ssh_key :boolean default(FALSE) +# website_url :string(255) default(""), not null +# notification_email :string(255) +# hide_no_password :boolean default(FALSE) +# password_automatically_set :boolean default(FALSE) +# location :string(255) +# encrypted_otp_secret :string(255) +# encrypted_otp_secret_iv :string(255) +# encrypted_otp_secret_salt :string(255) +# otp_required_for_login :boolean default(FALSE), not null +# otp_backup_codes :text +# public_email :string(255) default(""), not null +# dashboard :integer default(0) +# project_view :integer default(0) +# consumed_timestep :integer +# layout :integer default(0) +# hide_project_limit :boolean default(FALSE) +# unlock_token :string +# otp_grace_period_started_at :datetime # require 'spec_helper' From da53fcba2d0e46e47a8fd6a79591a6367e863d57 Mon Sep 17 00:00:00 2001 From: Janis Meybohm Date: Wed, 23 Dec 2015 11:17:25 +0100 Subject: [PATCH 135/280] Enable Microsoft Azure OAuth2 support --- CHANGELOG | 1 + Gemfile | 1 + Gemfile.lock | 5 ++ app/assets/images/auth_buttons/azure_64.png | Bin 0 -> 986 bytes app/helpers/auth_helper.rb | 2 +- doc/integration/azure.md | 83 ++++++++++++++++++++ doc/integration/omniauth.md | 1 + 7 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 app/assets/images/auth_buttons/azure_64.png create mode 100644 doc/integration/azure.md diff --git a/CHANGELOG b/CHANGELOG index cd745d3746a..8ee32013772 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,7 @@ v 8.4.0 (unreleased) - Fix API project lookups when querying with a namespace with dots (Stan Hu) - Update version check images to use SVG - Validate README format before displaying + - Enable Microsoft Azure OAuth2 support (Janis Meybohm) v 8.3.3 (unreleased) - Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu) diff --git a/Gemfile b/Gemfile index 3ce4ba4a2a5..6145745b6f3 100644 --- a/Gemfile +++ b/Gemfile @@ -33,6 +33,7 @@ gem 'omniauth-saml', '~> 1.4.0' gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd' +gem 'omniauth-azure-oauth2' gem 'rack-oauth2', '~> 1.2.1' # reCAPTCHA protection diff --git a/Gemfile.lock b/Gemfile.lock index ffb7cef0aba..2b42f325503 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -488,6 +488,10 @@ GEM activesupport nokogiri (>= 1.4.4) omniauth (~> 1.0) + omniauth-azure-oauth2 (0.0.6) + jwt (~> 1.0) + omniauth (~> 1.0) + omniauth-oauth2 (~> 1.1) opennebula (4.14.2) json nokogiri @@ -927,6 +931,7 @@ DEPENDENCIES omniauth-shibboleth (~> 1.2.0) omniauth-twitter (~> 1.2.0) omniauth_crowd + omniauth-azure-oauth2 org-ruby (~> 0.9.12) paranoia (~> 2.0) pg (~> 0.18.2) diff --git a/app/assets/images/auth_buttons/azure_64.png b/app/assets/images/auth_buttons/azure_64.png new file mode 100644 index 0000000000000000000000000000000000000000..a82c751e001e4bcbbe3b9872513650aedf0cdb76 GIT binary patch literal 986 zcmV<0110>4P)RN2ar2+ z&INuqZR^bdAz%`w30&~E$ z9AD7Y@<2ua+Bupy=qWPCfiZ0b?EzNh$a!EHm<1ZZ6^(W`wi6$C0~Y2bIaLPMG-^$q zi$`-D_^toQo!IwmUMk^xa-_p(6}Y94U6OON%F}{Ew5U{9ZyW_7+BI|PobV+IF+u{Gef9w#?}U%=HKD(jgv8^9gl z5wNY1C#$^73P4%=$|$`(=dI(TQT+OlANB7~4uAvT05|{+fCJzFH~nFVE_4JY&C$_D#k10eBBYeduru*vCwR1AOXZU!BaCua@bT(o`(XzPh-Nk*@(c zi^v1uexFm|E`k4@FuDhhofYrqO{J*DEam_lEt*-ERYz+Rs_J;eOj0&tLV z(HA$CH4xqh?y=b zY442^fY-nVX5KDVY|LO5MbBdt871J7b-p1fbdiK#fEY5o>k60Y3!K z!z|O!JOz$hC#8rjvIzVwU<>#Pyld0GVnx_n;EC$YBxahtN+OV&&~cGOLQ_Z=Kqu=~ zBa}qaw)OXh>W$s5j8UB|2uvCC+d)(5wnl5Q)ISNQ2B*pg{tjXE5%>(e;ka}zN1|R? z)YGbgk=-m~miVvm7fb!U6ddauIZ2cp=^f1;2uJk!3eni8wkW)wk}b^bYl|6;h_Xu_!1_BouvAOCoq$VA{5$N&HU07*qo IM6N<$g0cd-UjP6A literal 0 HcmV?d00001 diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 0cfc0565e84..de669e529a7 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,5 +1,5 @@ module AuthHelper - PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook).freeze + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze def ldap_enabled? diff --git a/doc/integration/azure.md b/doc/integration/azure.md new file mode 100644 index 00000000000..48dddf7df44 --- /dev/null +++ b/doc/integration/azure.md @@ -0,0 +1,83 @@ +# Microsoft Azure OAuth2 OmniAuth Provider + +To enable the Microsoft Azure OAuth2 OmniAuth provider you must register your application with Azure. Azure will generate a client ID and secret key for you to use. + +1. Sign in to the [Azure Management Portal](https://manage.windowsazure.com>). + +1. Select "Active Directory" on the left and choose the directory you want to use to register GitLab. + +1. Select "Applications" at the top bar and click the "Add" button the bottom. + +1. Select "Add an application my organization is developing". + +1. Provide the project information and click the "Next" button. + - Name: 'GitLab' works just fine here. + - Type: 'WEB APPLICATION AND/OR WEB API' + +1. On the "App properties" page enter the needed URI's and click the "Complete" button. + - SIGN-IN URL: Enter the URL of your GitLab installation (e.g 'https://gitlab.mycompany.com/') + - APP ID URI: Enter the endpoint URL for Microsoft to use, just has to be unique (e.g 'https://mycompany.onmicrosoft.com/gitlab') + +1. Select "Configure" in the top menu. + +1. Add a "Reply URL" pointing to the Azure OAuth callback of your GitLab installation (e.g. https://gitlab.mycompany.com/users/auth/azure_oauth2/callback). + +1. Create a "Client secret" by selecting a duration, the secret will be generated as soon as you click the "Save" button in the bottom menu.. + +1. Note the "CLIENT ID" and the "CLIENT SECRET". + +1. Select "View endpoints" from the bottom menu. + +1. You will see lots of endpoint URLs in the form 'https://login.microsoftonline.com/TENANT ID/...', note down the TENANT ID part of one of those endpoints. + +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For installations 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" => "azure_oauth2", + "args" => { + "client_id" => "CLIENT ID", + "client_secret" => "CLIENT SECRET", + "tenant_id" => "TENANT ID", + } + } + ] + ``` + + For installations from source: + + ``` + - { name: 'azure_oauth2', + args: { client_id: "CLIENT ID", + client_secret: "CLIENT SECRET", + tenant_id: "TENANT ID" } } + ``` + +1. Replace 'CLIENT ID', 'CLIENT SECRET' and 'TENANT ID' with the values you got above. + +1. Save the configuration file. + +1. Restart GitLab for the changes to take effect. + +On the sign in page there should now be a Microsoft icon below the regular sign in form. Click the icon to begin the authentication process. Microsoft will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index f2b1721fc03..e9e17eb4165 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -78,6 +78,7 @@ Now we can choose one or more of the Supported Providers below to continue confi - [Shibboleth](shibboleth.md) - [SAML](saml.md) - [Crowd](crowd.md) +- [Azure](azure.md) ## Enable OmniAuth for an Existing User From fe19169836e1d7855d0ad5351b3f666dce8fcda1 Mon Sep 17 00:00:00 2001 From: Jeroen Nijhof Date: Wed, 6 Jan 2016 15:51:10 +0100 Subject: [PATCH 136/280] Use panel-default and .form-actions --- app/views/projects/edit.html.haml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index ef758b5fb7a..31e752c6649 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -174,6 +174,19 @@ .danger-settings + .panel.panel-default + .panel-heading Housekeeping + .errors-holder + .panel-body + %p + Runs a number of housekeeping tasks within the current repository, + such as compressing file revisions and removing unreachable objects. + %br + + .form-actions + = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), + method: :post, class: "btn btn-default" + - if can? current_user, :archive_project, @project - if @project.archived? .panel.panel-success @@ -210,17 +223,6 @@ - else .nothing-here-block Only the project owner can archive a project - .panel.panel-default.panel.panel-warning - .panel-heading Housekeeping - .errors-holder - .panel-body - %p - Runs a number of housekeeping tasks within the current repository, - such as compressing file revisions and removing unreachable objects. - %br - = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), - method: :post, class: "btn btn-warning" - .panel.panel-default.panel.panel-warning .panel-heading Rename repository .errors-holder From 0b2fa3bfa4bea00973ad6b8c3dfb8302d84fe4de Mon Sep 17 00:00:00 2001 From: Jose Corcuera Date: Wed, 6 Jan 2016 09:54:43 -0500 Subject: [PATCH 137/280] Fix problem with projects ending with .keys #3076 --- CHANGELOG | 1 + config/routes.rb | 6 +++--- spec/routing/project_routing_spec.rb | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cd745d3746a..c0b03a3ed31 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.4.0 (unreleased) - Add "Frequently used" category to emoji picker - Add CAS support (tduehr) - Add link to merge request on build detail page + - Fix: Problem with projects ending with .keys (Jose Corcuera) - Revert back upvote and downvote button to the issue and MR pages - Swap position of Assignee and Author selector on Issuables (Zeger-Jan van de Weg) - Add system hook messages for project rename and transfer (Steve Norman) diff --git a/config/routes.rb b/config/routes.rb index 3e7d9f78710..d7a9df10eba 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -52,9 +52,6 @@ Rails.application.routes.draw do API::API.logger Rails.logger mount API::API => '/api' - # Get all keys of user - get ':username.keys' => 'profiles/keys#get_keys' , constraints: { username: /.*/ } - constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? } constraints constraint do mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq @@ -668,5 +665,8 @@ Rails.application.routes.draw do end end + # Get all keys of user + get ':username.keys' => 'profiles/keys#get_keys' , constraints: { username: /.*/ } + get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 82f62a8709c..f0f0fbe619c 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -80,6 +80,7 @@ describe ProjectsController, 'routing' do it 'to #show' do expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq') + expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') end it 'to #update' do From bde76f62fc2bd259dcc37ca649a01a84035ddcd9 Mon Sep 17 00:00:00 2001 From: Jeroen Nijhof Date: Wed, 6 Jan 2016 15:57:49 +0100 Subject: [PATCH 138/280] Added CHANGELOG for housekeeping and changed GITLAB_SHELL_VERSION to 2.6.10 --- CHANGELOG | 1 + GITLAB_SHELL_VERSION | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index cd745d3746a..394a25cfa06 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.4.0 (unreleased) + - Add housekeeping function to project settings page - Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu) - Don't notify users twice if they are both project watchers and subscribers (Stan Hu) - Implement new UI for group page diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index d48d3702aed..a04abec9149 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.6.9 +2.6.10 From 7549102bb727daecc51da84af39956b32fc41537 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 6 Jan 2016 15:30:02 +0100 Subject: [PATCH 139/280] Store SQL/view timings in milliseconds Transaction timings are also already stored in milliseconds, this keeps things consistent. --- lib/gitlab/metrics/subscribers/action_view.rb | 8 ++++++-- lib/gitlab/metrics/subscribers/active_record.rb | 6 +++++- spec/lib/gitlab/metrics/subscribers/action_view_spec.rb | 4 ++-- spec/lib/gitlab/metrics/subscribers/active_record_spec.rb | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb index 7c0105d543a..84d9e383625 100644 --- a/lib/gitlab/metrics/subscribers/action_view.rb +++ b/lib/gitlab/metrics/subscribers/action_view.rb @@ -19,7 +19,7 @@ module Gitlab values = values_for(event) tags = tags_for(event) - current_transaction.increment(:view_duration, event.duration) + current_transaction.increment(:view_duration, duration(event)) current_transaction.add_metric(SERIES, values, tags) end @@ -28,7 +28,7 @@ module Gitlab end def values_for(event) - { duration: event.duration } + { duration: duration(event) } end def tags_for(event) @@ -48,6 +48,10 @@ module Gitlab def current_transaction Transaction.current end + + def duration(event) + event.duration * 1000.0 + end end end end diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb index 8008b3bc895..6fa73e7a3be 100644 --- a/lib/gitlab/metrics/subscribers/active_record.rb +++ b/lib/gitlab/metrics/subscribers/active_record.rb @@ -8,7 +8,7 @@ module Gitlab def sql(event) return unless current_transaction - current_transaction.increment(:sql_duration, event.duration) + current_transaction.increment(:sql_duration, duration(event)) end private @@ -16,6 +16,10 @@ module Gitlab def current_transaction Transaction.current end + + def duration(event) + event.duration * 1000.0 + end end end end diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb index 05e4fbbeb51..0a4cc5e929b 100644 --- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb @@ -21,7 +21,7 @@ describe Gitlab::Metrics::Subscribers::ActionView do describe '#render_template' do it 'tracks rendering of a template' do - values = { duration: 2.1 } + values = { duration: 2100 } tags = { view: 'app/views/x.html.haml', file: 'app/views/x.html.haml', @@ -29,7 +29,7 @@ describe Gitlab::Metrics::Subscribers::ActionView do } expect(transaction).to receive(:increment). - with(:view_duration, 2.1) + with(:view_duration, 2100) expect(transaction).to receive(:add_metric). with(described_class::SERIES, values, tags) diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index 7bc070a4d09..ca86142a2f4 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -26,7 +26,7 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do and_return(transaction) expect(transaction).to receive(:increment). - with(:sql_duration, 0.2) + with(:sql_duration, 200) subscriber.sql(event) end From 8fdc00bd4c59183a20a60a6b93228268230bbd2e Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 6 Jan 2016 17:49:02 +0100 Subject: [PATCH 140/280] Remove InfluxDB username/password InfluxDB over UDP doesn't use authentication, thus there's no need for these settings. --- .../admin/application_settings_controller.rb | 2 -- .../application_settings/_form.html.haml | 8 -------- ...60106164438_remove_influxdb_credentials.rb | 6 ++++++ db/schema.rb | 20 +++++++++---------- lib/gitlab/metrics.rb | 6 +----- 5 files changed, 16 insertions(+), 26 deletions(-) create mode 100644 db/migrate/20160106164438_remove_influxdb_credentials.rb diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 10e736fd362..44d06b6a647 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -70,8 +70,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :metrics_enabled, :metrics_host, :metrics_port, - :metrics_username, - :metrics_password, :metrics_pool_size, :metrics_timeout, :metrics_method_call_threshold, diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 89b38a0dad0..81337432ab7 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -179,14 +179,6 @@ your server configuration specifies a database to store data in when sending messages to this port, without it metrics data will not be saved. - .form-group - = f.label :metrics_username, 'InfluxDB username', class: 'control-label col-sm-2' - .col-sm-10 - = f.text_field :metrics_username, class: 'form-control' - .form-group - = f.label :metrics_password, 'InfluxDB password', class: 'control-label col-sm-2' - .col-sm-10 - = f.text_field :metrics_password, class: 'form-control' .form-group = f.label :metrics_pool_size, 'Connection pool size', class: 'control-label col-sm-2' .col-sm-10 diff --git a/db/migrate/20160106164438_remove_influxdb_credentials.rb b/db/migrate/20160106164438_remove_influxdb_credentials.rb new file mode 100644 index 00000000000..47e74400b97 --- /dev/null +++ b/db/migrate/20160106164438_remove_influxdb_credentials.rb @@ -0,0 +1,6 @@ +class RemoveInfluxdbCredentials < ActiveRecord::Migration + def change + remove_column :application_settings, :metrics_username, :string + remove_column :application_settings, :metrics_password, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 48e6983684a..2ded8a45e18 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: 20151229112614) do +ActiveRecord::Schema.define(version: 20160106164438) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -50,16 +50,14 @@ ActiveRecord::Schema.define(version: 20151229112614) do t.boolean "shared_runners_enabled", default: true, null: false t.integer "max_artifacts_size", default: 100, null: false t.string "runners_registration_token" - t.boolean "require_two_factor_authentication", default: false - t.integer "two_factor_grace_period", default: 48 - t.boolean "metrics_enabled", default: false - t.string "metrics_host", default: "localhost" - t.string "metrics_username" - t.string "metrics_password" - t.integer "metrics_pool_size", default: 16 - t.integer "metrics_timeout", default: 10 - t.integer "metrics_method_call_threshold", default: 10 - t.boolean "recaptcha_enabled", default: false + t.boolean "require_two_factor_authentication", default: false + t.integer "two_factor_grace_period", default: 48 + t.boolean "metrics_enabled", default: false + t.string "metrics_host", default: "localhost" + t.integer "metrics_pool_size", default: 16 + t.integer "metrics_timeout", default: 10 + t.integer "metrics_method_call_threshold", default: 10 + t.boolean "recaptcha_enabled", default: false t.string "recaptcha_site_key" t.string "recaptcha_private_key" t.integer "metrics_port", default: 8089 diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index ee88ab34d6c..44356a0e42c 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -13,8 +13,6 @@ module Gitlab timeout: current_application_settings[:metrics_timeout], method_call_threshold: current_application_settings[:metrics_method_call_threshold], host: current_application_settings[:metrics_host], - username: current_application_settings[:metrics_username], - password: current_application_settings[:metrics_password], port: current_application_settings[:metrics_port] } end @@ -90,12 +88,10 @@ module Gitlab if enabled? @pool = ConnectionPool.new(size: settings[:pool_size], timeout: settings[:timeout]) do host = settings[:host] - user = settings[:username] - pw = settings[:password] port = settings[:port] InfluxDB::Client. - new(udp: { host: host, port: port }, username: user, password: pw) + new(udp: { host: host, port: port }) end end end From 4d41294d71f2a8910a3aa5d475f8eb3923ca3531 Mon Sep 17 00:00:00 2001 From: Tommy Beadle Date: Wed, 6 Jan 2016 11:42:31 -0500 Subject: [PATCH 141/280] Include the user_id in user_*_team system hooks. This fixes an issue where the user_id is not included in the data for user_add_to_team and user_remove_from_team system hooks. The documentation already states that the user_id should be included. --- app/services/system_hooks_service.rb | 1 + spec/services/system_hooks_service_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 6dc854ec33d..9f677444f1f 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -101,6 +101,7 @@ class SystemHooksService project_id: model.project.id, user_name: model.user.name, user_email: model.user.email, + user_id: model.user.id, access_level: model.human_access, project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase } diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index 4455ae7b321..41df4951d16 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -13,8 +13,8 @@ describe SystemHooksService, services: true do it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id) } it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) } - it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) } + it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :user_id, :access_level, :project_visibility) } + it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :user_id, :access_level, :project_visibility) } it { expect(event_data(key, :create)).to include(:username, :key, :id) } it { expect(event_data(key, :destroy)).to include(:username, :key, :id) } From 12e32224c17253f9616cd05d0bce806881fa8db9 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 6 Jan 2016 20:05:22 +0100 Subject: [PATCH 142/280] Add documentation on enabling/disabling GitLab CI [ci skip] --- doc/README.md | 1 + doc/ci/README.md | 1 + doc/ci/enable_or_disable_ci.md | 70 +++++++++++++++++++++++++++++++ doc/ci/img/features_settings.png | Bin 0 -> 18691 bytes 4 files changed, 72 insertions(+) create mode 100644 doc/ci/enable_or_disable_ci.md create mode 100644 doc/ci/img/features_settings.png diff --git a/doc/README.md b/doc/README.md index f4553a899d3..25fe3abcb9a 100644 --- a/doc/README.md +++ b/doc/README.md @@ -19,6 +19,7 @@ ## CI Documentation - [Quick Start](ci/quick_start/README.md) +- [Enable or disable GitLab CI](ci/enable_or_disable_ci.md) - [Configuring project (.gitlab-ci.yml)](ci/yaml/README.md) - [Configuring runner](ci/runners/README.md) - [Configuring deployment](ci/deployment/README.md) diff --git a/doc/ci/README.md b/doc/ci/README.md index a1f5513d88e..4cdd2e1ad33 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -3,6 +3,7 @@ ### User documentation * [Quick Start](quick_start/README.md) +* [Enable or disable GitLab CI](enable_or_disable_ci.md) * [Configuring project (.gitlab-ci.yml)](yaml/README.md) * [Configuring runner](runners/README.md) * [Configuring deployment](deployment/README.md) diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md new file mode 100644 index 00000000000..2803bb5f34a --- /dev/null +++ b/doc/ci/enable_or_disable_ci.md @@ -0,0 +1,70 @@ +## Enable or disable GitLab CI + +_To effectively use GitLab CI, you need a valid [`.gitlab-ci.yml`](yaml/README.md) +file present at the root directory of your project and a +[runner](runners/README.md) properly set up. You can read our +[quick start guide](quick_start/README.md) to get you started._ + +If you are using an external CI server like Jenkins or Drone CI, it is advised +to disable GitLab CI in order to not have any conflicts with the commits status +API. + +--- + +As of GitLab 8.2, GitLab CI is mainly exposed via the `/builds` page of a +project. Disabling GitLab CI in a project does not delete any previous builds. +In fact, the `/builds` page can still be accessed, although it's hidden from +the left sidebar menu. + +GitLab CI is enabled by default on new installations and can be disabled either +individually under each project's settings, or site wide by modifying the +settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations +respectively. + +### Per project user setting + +The setting to enable or disable GitLab CI can be found with the name **Builds** +under the **Features** area of a project's settings along with **Issues**, +**Merge Requests**, **Wiki** and **Snippets**. Select or deselect the checkbox +and hit **Save** for the settings to take effect. + +![Features settings](img/features_settings.png) + +--- + +### Site wide administrator setting + +You can disable GitLab CI site wide, by modifying the settings in `gitlab.yml` +and `gitlab.rb` for source and Omnibus installations respectively. + +Two things to note. + +1. Disabling GitLab CI, will affect only newly created projects. Projects that + had it enabled prior this modification, will work as before. +1. Even if you disable GitLab CI, users will still be able to enable it in the + project's settings. + +--- + +For installations from source, open `gitlab.yml` with your editor and set +`builds` to `false`: + +```yaml +## Default project features settings +default_projects_features: + issues: true + merge_requests: true + wiki: true + snippets: false + builds: false +``` + +Save the file and restart GitLab: `sudo service gitlab restart`. + +For Omnibus installations, edit `/etc/gitlab/gitlab.rb` and add the line: + +``` +gitlab-rails['gitlab_default_projects_features_builds'] = false +``` + +Save the file and reconfigure GitLab: `sudo gitlab-ctl reconfigure`. diff --git a/doc/ci/img/features_settings.png b/doc/ci/img/features_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..17aba5d14d89e3dbd5c3765859853d3646847c1b GIT binary patch literal 18691 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 Uo+7|EWkBxS)Voo5?f#Sh3(Cmb%7 literal 0 HcmV?d00001 From 7d013f7848ea46e68665a8efcd9d4453584945e1 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 6 Jan 2016 21:05:46 -0800 Subject: [PATCH 143/280] Fix missing date of month in network graph when commits span a month Closes #3635, #1383 --- CHANGELOG | 1 + app/assets/javascripts/branch-graph.js.coffee | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index cd745d3746a..35577e21415 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.4.0 (unreleased) + - Fix missing date of month in network graph when commits span a month (Stan Hu) - Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu) - Don't notify users twice if they are both project watchers and subscribers (Stan Hu) - Implement new UI for group page diff --git a/app/assets/javascripts/branch-graph.js.coffee b/app/assets/javascripts/branch-graph.js.coffee index 917228bd276..f2fd2a775a4 100644 --- a/app/assets/javascripts/branch-graph.js.coffee +++ b/app/assets/javascripts/branch-graph.js.coffee @@ -66,7 +66,7 @@ class @BranchGraph r.rect(40, 0, 30, @barHeight).attr fill: "#444" for day, mm in @days - if cuday isnt day[0] + if cuday isnt day[0] || cumonth isnt day[1] # Dates r.text(55, @offsetY + @unitTime * mm, day[0]) .attr( From a8e23bd1b5029fb04ecb22f9c60fe7a8f9d45091 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 7 Jan 2016 10:10:46 +0100 Subject: [PATCH 144/280] Fix hyphenation typos [ci skip] --- doc/ci/enable_or_disable_ci.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md index 2803bb5f34a..9bd2f5aff22 100644 --- a/doc/ci/enable_or_disable_ci.md +++ b/doc/ci/enable_or_disable_ci.md @@ -17,11 +17,11 @@ In fact, the `/builds` page can still be accessed, although it's hidden from the left sidebar menu. GitLab CI is enabled by default on new installations and can be disabled either -individually under each project's settings, or site wide by modifying the +individually under each project's settings, or site-wide by modifying the settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations respectively. -### Per project user setting +### Per-project user setting The setting to enable or disable GitLab CI can be found with the name **Builds** under the **Features** area of a project's settings along with **Issues**, @@ -32,15 +32,15 @@ and hit **Save** for the settings to take effect. --- -### Site wide administrator setting +### Site-wide administrator setting -You can disable GitLab CI site wide, by modifying the settings in `gitlab.yml` +You can disable GitLab CI site-wide, by modifying the settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations respectively. -Two things to note. +Two things to note: -1. Disabling GitLab CI, will affect only newly created projects. Projects that - had it enabled prior this modification, will work as before. +1. Disabling GitLab CI, will affect only newly-created projects. Projects that + had it enabled prior to this modification, will work as before. 1. Even if you disable GitLab CI, users will still be able to enable it in the project's settings. From 8884505350c8594e663c5af6feb8eb1b06f3a0c8 Mon Sep 17 00:00:00 2001 From: koreamic Date: Sun, 13 Dec 2015 02:27:48 +0900 Subject: [PATCH 145/280] Add new feature to find file Using the fuzzy filter, develop "file finder" feature. - feedback(http://feedback.gitlab.com/forums/176466-general/suggestions/4987909-add-file-finder-fuzzy-input-in-files-tab-to-ju ) - fuzzy filter(https://github.com/jeancroy/fuzzaldrin-plus) - shortcuts(when "t" was hitted at tree view, go to 'file find' page and 'esc' is to go back) - depends on gitlab_git 7.2.22 --- CHANGELOG | 1 + Gemfile | 2 +- Gemfile.lock | 2 +- app/assets/javascripts/application.js.coffee | 1 + app/assets/javascripts/dispatcher.js.coffee | 4 +- .../javascripts/project_find_file.js.coffee | 125 ++++++++++++++++++ .../javascripts/shortcuts_find_file.js.coffee | 19 +++ app/assets/javascripts/shortcuts_tree.coffee | 4 + app/assets/stylesheets/pages/tree.scss | 8 ++ .../projects/find_file_controller.rb | 26 ++++ app/controllers/projects/refs_controller.rb | 2 + app/models/repository.rb | 5 + app/views/help/_shortcuts.html.haml | 26 ++++ app/views/layouts/nav/_project.html.haml | 3 +- app/views/projects/_find_file_link.html.haml | 3 + app/views/projects/find_file/show.html.haml | 27 ++++ app/views/projects/tree/show.html.haml | 8 +- config/routes.rb | 18 +++ doc/workflow/shortcuts.png | Bin 78736 -> 48782 bytes features/project/find_file.feature | 42 ++++++ features/steps/project/project_find_file.rb | 73 ++++++++++ features/steps/shared/paths.rb | 4 + .../projects/find_file_controller_spec.rb | 66 +++++++++ spec/routing/project_routing_spec.rb | 12 ++ .../assets/javascripts/fuzzaldrin-plus.min.js | 1 + 25 files changed, 473 insertions(+), 9 deletions(-) create mode 100644 app/assets/javascripts/project_find_file.js.coffee create mode 100644 app/assets/javascripts/shortcuts_find_file.js.coffee create mode 100644 app/assets/javascripts/shortcuts_tree.coffee create mode 100644 app/controllers/projects/find_file_controller.rb create mode 100644 app/views/projects/_find_file_link.html.haml create mode 100644 app/views/projects/find_file/show.html.haml create mode 100644 features/project/find_file.feature create mode 100644 features/steps/project/project_find_file.rb create mode 100644 spec/controllers/projects/find_file_controller_spec.rb create mode 100644 vendor/assets/javascripts/fuzzaldrin-plus.min.js diff --git a/CHANGELOG b/CHANGELOG index e7f1d2b67da..22fb91baaf0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -106,6 +106,7 @@ v 8.3.0 - Fix online editor should not remove newlines at the end of the file - Expose Git's version in the admin area - Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye) + - Add file finder feature in tree view v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) diff --git a/Gemfile b/Gemfile index 6145745b6f3..6b0bc241494 100644 --- a/Gemfile +++ b/Gemfile @@ -49,7 +49,7 @@ gem "browser", '~> 1.0.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.2.20' +gem "gitlab_git", '~> 7.2.22' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes diff --git a/Gemfile.lock b/Gemfile.lock index 2b42f325503..a1168ed3b7a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -887,7 +887,7 @@ DEPENDENCIES github-markup (~> 1.3.1) gitlab-flowdock-git-hook (~> 1.0.1) gitlab_emoji (~> 0.2.0) - gitlab_git (~> 7.2.20) + gitlab_git (~> 7.2.22) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.1.0) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index b9b095e004a..c095e5ae2b1 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -40,6 +40,7 @@ #= require shortcuts_network #= require jquery.nicescroll.min #= require_tree . +#= require fuzzaldrin-plus.min window.slugify = (text) -> text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 69e061ce6e9..58d6b9d4060 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -87,7 +87,9 @@ class Dispatcher new GroupAvatar() when 'projects:tree:show' new TreeView() - shortcut_handler = new ShortcutsNavigation() + shortcut_handler = new ShortcutsTree() + when 'projects:find_file:show' + shortcut_handler = true when 'projects:blob:show' new LineHighlighter() shortcut_handler = new ShortcutsNavigation() diff --git a/app/assets/javascripts/project_find_file.js.coffee b/app/assets/javascripts/project_find_file.js.coffee new file mode 100644 index 00000000000..0dd32352c34 --- /dev/null +++ b/app/assets/javascripts/project_find_file.js.coffee @@ -0,0 +1,125 @@ +class @ProjectFindFile + constructor: (@element, @options)-> + @filePaths = {} + @inputElement = @element.find(".file-finder-input") + + # init event + @initEvent() + + # focus text input box + @inputElement.focus() + + # load file list + @load(@options.url) + + # init event + initEvent: -> + @inputElement.off "keyup" + @inputElement.on "keyup", (event) => + target = $(event.target) + value = target.val() + oldValue = target.data("oldValue") ? "" + + if value != oldValue + target.data("oldValue", value) + @findFile() + @element.find("tr.tree-item").eq(0).addClass("selected").focus() + + @element.find(".tree-content-holder .tree-table").on "click", (event) -> + if (event.target.nodeName != "A") + path = @element.find(".tree-item-file-name a", this).attr("href") + location.href = path if path + + # find file + findFile: -> + searchText = @inputElement.val() + result = if searchText.length > 0 then fuzzaldrinPlus.filter(@filePaths, searchText) else @filePaths + @renderList result, searchText + + # files pathes load + load: (url) -> + $.ajax + url: url + method: "get" + dataType: "json" + success: (data) => + @element.find(".loading").hide() + @filePaths = data + @findFile() + @element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus() + + # render result + renderList: (filePaths, searchText) -> + @element.find(".tree-table > tbody").empty() + + for filePath, i in filePaths + break if i == 20 + + if searchText + matches = fuzzaldrinPlus.match(filePath, searchText) + + blobItemUrl = "#{@options.blobUrlTemplate}/#{filePath}" + + html = @makeHtml filePath, matches, blobItemUrl + @element.find(".tree-table > tbody").append(html) + + # highlight text(awefwbwgtc -> awefwbwgtc ) + highlighter = (element, text, matches) -> + lastIndex = 0 + highlightText = "" + matchedChars = [] + + for matchIndex in matches + unmatched = text.substring(lastIndex, matchIndex) + + if unmatched + element.append(matchedChars.join("").bold()) if matchedChars.length + matchedChars = [] + element.append(document.createTextNode(unmatched)) + + matchedChars.push(text[matchIndex]) + lastIndex = matchIndex + 1 + + element.append(matchedChars.join("").bold()) if matchedChars.length + element.append(document.createTextNode(text.substring(lastIndex))) + + # make tbody row html + makeHtml: (filePath, matches, blobItemUrl) -> + $tr = $("") + if matches + $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl)) + else + $tr.find("a").attr("href", blobItemUrl).text(filePath) + + return $tr + + selectRow: (type) -> + rows = @element.find(".files-slider tr.tree-item") + selectedRow = @element.find(".files-slider tr.tree-item.selected") + + if rows && rows.length > 0 + if selectedRow && selectedRow.length > 0 + if type == "UP" + next = selectedRow.prev() + else if type == "DOWN" + next = selectedRow.next() + + if next.length > 0 + selectedRow.removeClass "selected" + selectedRow = next + else + selectedRow = rows.eq(0) + selectedRow.addClass("selected").focus() + + selectRowUp: => + @selectRow "UP" + + selectRowDown: => + @selectRow "DOWN" + + goToTree: => + location.href = @options.treeUrl + + goToBlob: => + path = @element.find(".tree-item.selected .tree-item-file-name a").attr("href") + location.href = path if path diff --git a/app/assets/javascripts/shortcuts_find_file.js.coffee b/app/assets/javascripts/shortcuts_find_file.js.coffee new file mode 100644 index 00000000000..311e80bae19 --- /dev/null +++ b/app/assets/javascripts/shortcuts_find_file.js.coffee @@ -0,0 +1,19 @@ +#= require shortcuts_navigation + +class @ShortcutsFindFile extends ShortcutsNavigation + constructor: (@projectFindFile) -> + super() + _oldStopCallback = Mousetrap.stopCallback + # override to fire shortcuts action when focus in textbox + Mousetrap.stopCallback = (event, element, combo) => + if element == @projectFindFile.inputElement[0] and (combo == 'up' or combo == 'down' or combo == 'esc' or combo == 'enter') + # when press up/down key in textbox, cusor prevent to move to home/end + event.preventDefault() + return false + + return _oldStopCallback(event, element, combo) + + Mousetrap.bind('up', @projectFindFile.selectRowUp) + Mousetrap.bind('down', @projectFindFile.selectRowDown) + Mousetrap.bind('esc', @projectFindFile.goToTree) + Mousetrap.bind('enter', @projectFindFile.goToBlob) diff --git a/app/assets/javascripts/shortcuts_tree.coffee b/app/assets/javascripts/shortcuts_tree.coffee new file mode 100644 index 00000000000..ba0839c9fc0 --- /dev/null +++ b/app/assets/javascripts/shortcuts_tree.coffee @@ -0,0 +1,4 @@ +class @ShortcutsTree extends ShortcutsNavigation + constructor: -> + super() + Mousetrap.bind('t', -> ShortcutsTree.findAndFollowLink('.shortcuts-find-file')) diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index d4ab6967ccd..97505edeabf 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,5 +1,13 @@ .tree-holder { + .file-finder { + width: 50%; + .file-finder-input { + width: 95%; + display: inline-block; + } + } + .tree-table { margin-bottom: 0; diff --git a/app/controllers/projects/find_file_controller.rb b/app/controllers/projects/find_file_controller.rb new file mode 100644 index 00000000000..54a0c447aee --- /dev/null +++ b/app/controllers/projects/find_file_controller.rb @@ -0,0 +1,26 @@ +# Controller for viewing a repository's file structure +class Projects::FindFileController < Projects::ApplicationController + include ExtractsPath + include ActionView::Helpers::SanitizeHelper + include TreeHelper + + before_action :require_non_empty_project + before_action :assign_ref_vars + before_action :authorize_download_code! + + def show + return render_404 unless @repository.commit(@ref) + + respond_to do |format| + format.html + end + end + + def list + file_paths = @repo.ls_files(@ref) + + respond_to do |format| + format.json { render json: file_paths } + end + end +end diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index c4e18c17077..a8f091819ca 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -20,6 +20,8 @@ class Projects::RefsController < Projects::ApplicationController namespace_project_network_path(@project.namespace, @project, @id, @options) when "graphs" namespace_project_graph_path(@project.namespace, @project, @id) + when "find_file" + namespace_project_find_file_path(@project.namespace, @project, @id) when "graphs_commits" commits_namespace_project_graph_path(@project.namespace, @project, @id) else diff --git a/app/models/repository.rb b/app/models/repository.rb index 6ecd2d2f27e..9deb08d93b8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -681,6 +681,11 @@ class Repository end end + def ls_files(ref) + actual_ref = ref || root_ref + raw_repository.ls_files(actual_ref) + end + private def cache diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index e8e331dd109..9ee6f07b26b 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -40,6 +40,32 @@ %td.shortcut .key enter %td Open Selection + %tr + %td.shortcut + .key t + %td Go to finding file + %tbody + %tr + %th + %th Finding Project File + %tr + %td.shortcut + .key + %i.fa.fa-arrow-up + %td Move selection up + %tr + %td.shortcut + .key + %i.fa.fa-arrow-down + %td Move selection down + %tr + %td.shortcut + .key enter + %td Open Selection + %tr + %td.shortcut + .key esc + %td Go back .col-lg-4 %table.shortcut-mappings diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index d3eaf0f3209..270ccfd387f 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -25,7 +25,7 @@ %span Activity - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do = icon('files-o fw') %span @@ -117,4 +117,3 @@ %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/_find_file_link.html.haml b/app/views/projects/_find_file_link.html.haml new file mode 100644 index 00000000000..08e2fc48be7 --- /dev/null +++ b/app/views/projects/_find_file_link.html.haml @@ -0,0 +1,3 @@ += link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do + = icon('search') + %span Find File diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml new file mode 100644 index 00000000000..2930209fb56 --- /dev/null +++ b/app/views/projects/find_file/show.html.haml @@ -0,0 +1,27 @@ +- page_title "Find File", @ref +- header_title project_title(@project, "Files", project_files_path(@project)) + +.file-finder-holder.tree-holder.clearfix + .gray-content-block.top-block + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'find_file', path: @path + %ul.breadcrumb.repo-breadcrumb + %li + = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do + = @project.path + %li.file-finder + %input#file_find.form-control.file-finder-input{type: "text", placeholder: 'Find by path'} + + %div.tree-content-holder + .table-holder + %table.table.files-slider{class: "table_#{@hex_path} tree-table table-striped" } + %tbody + = spinner nil, true + +:coffeescript + projectFindFile = new ProjectFindFile($(".file-finder-holder"), { + url: "#{escape_javascript(namespace_project_files_path(@project.namespace, @project, @ref, @options.merge(format: :json)))}" + treeUrl: "#{escape_javascript(namespace_project_tree_path(@project.namespace, @project, @ref))}" + blobUrlTemplate: "#{escape_javascript(namespace_project_blob_path(@project.namespace, @project, @id || @commit.id))}" + }) + new ShortcutsFindFile(projectFindFile) diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index ec14bd7f65a..c57570afa09 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -3,12 +3,12 @@ = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") - = render 'projects/last_push' -- if can? current_user, :download_code, @project - .tree-download-holder - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true +.pull-right + = render 'projects/find_file_link' + - if can? current_user, :download_code, @project + = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true #tree-holder.tree-holder.clearfix .gray-content-block.top-block diff --git a/config/routes.rb b/config/routes.rb index 3e7d9f78710..5b69d06eb76 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -440,6 +440,24 @@ Rails.application.routes.draw do ) end + scope do + get( + '/find_file/*id', + to: 'find_file#show', + constraints: { id: /.+/, format: /html/ }, + as: :find_file + ) + end + + scope do + get( + '/files/*id', + to: 'find_file#list', + constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ }, + as: :files + ) + end + scope do post( '/create_dir/*id', diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png index 68756ed1f98dd474ada0b8da8d98840a25c3a123..e5914aa8e67556518de69841e4c06472a29d4d3e 100644 GIT binary patch literal 48782 zcmdSBcT`hb8}1uHMUbMP6lsD2(nWe#ibxApX`v~I^iV@DA|O(g-bH#Z3WVNOh!CXr zgbs-iS}399M%;UU`#a~{GwwL|uahwtj7%nLz4KjJbN=3EJ}VL0KottITVwzLfI{_& zk}d#13<3a%c&}V0v>ex_kP?1faMM*$08|VxtrOl{w0WfY2mq*#y?SDKiSV8j{KUu& z0HEyp{d3`)bCDGQ(0WHz>5;yd*{{>tNM6${R-duJ`(USytQRSgZUqe4slr-Qi(IQ2 z6&nH&jMMRZ#D2n$q*wB<-9|`r)vnp(Myi=6I3X(!uQPa*tY7K*6cH-6VsDm z4CcADto-Qg5TK1V1p5_!5cqI7y(?@uT>vj_ACpy}5>31@07HA>53=U(@YISA+lj#p3EU z=%2>Ai+?rEW#SK6@pn#}0uL~2^7vJJSs0&Yp9rN6!YA6RSe_DuN3ggFA ztt$Ne_g==Tu&x|Iht#xNa+m%ZO|!aRA&jvRdXgTv>nuiFNB>WIM?q+MYg5l(kt0oa z!peWXdczs|XCUd<@`s}|-v}^3&wjzrG+KVoD*(WR^9=t<8_oc4$6@H5y*{O1zm-w@ z&k>HO@bmKD!8e;D^Rv-YBUG#ycO(kGMd82OPc>qIK~zZF;WZr4@TrAS;n~`C8-$;t zyuG}y>iMCotgnCsiT-2{DLbW&B%`_!c}BIjp02gm)Y}KvjfV~2K==OHxNx7zYf}PS*=I=2?2~6c9Dpo;wvuZ0+}lj55WG%i2=R`Cb;Ef zt-Tl3WzGR75v?aX^4Rv`y1I@GN<6{TtKDh>gZe9PZV;K2oYB z?WnbjH4rCxiaOvmr$0U0A%Ub-VIGIIwA~0ah(y3RqfcZ5@TK@z@<*<6THB$YCgTJG z4;KOq;u>+>1i53(1j(R4pSC_{Y1GG@b*ptlDX7A;>3m1Q9-wR!L4a&J7 zntdCean?uALOX(Aw!!TW3$E#sylLWcjG1u_QL=18X&AJNrxVIfLobI3w zHmfF`Gi?R~B=w0?T48&i%?GIrXy88GuE*xq!hM*)NduVS;1Y@i_XBQSP6~2|aAU3n z+-XfVKVq38(?*}|8NG=$UvIxxcf7i=>wlnb>{M`=)fPaFfWy}e6tT8S4{?cyEUh=^ zw+;rb^^(fU=VqTSkQ^NUT!J`EIaRlqD-Ghh5iL!JVxqI)Tl`>El<*}Cxc>~bx>r7N zR(A&JghNH~JR;r1zq8XU%18;)w8OV^9WCT#WzP;>=(FG9#hs;Nx_fK!-2pBhC{pqg z(z#@HEqdcYXD#KT!z1&CuFkX@0|ISt=Mlq|BdHBEBi+8veHLBjLy+gvS_V$}-ltgs zSfh*3%BZOwbWTs37u^tUS2aV>eSA%kn#Ni*W}ra76ryGgPJX zOv8$S(ySEHuq6Bl5n$@Ssg~uAkJ>x(-f~KE0RW<6G~=9f3%#q(<&Oa>Z! z6Us9Wpu3{i`TRj1%>4ABJRSyja%*sIwtaZ%P2&BWFf>*``6_7Y*MY&vNyG3LOuDoD zuwb+1t*$ZVD-U*j&sGv%d#m$h5)#|YpS)SOCJpLPe&B|5&1%36K&A!Pj)GwgCWjRM zxS_mK1WB9T@1TR~ox6fz!>3JGp2)wwzdK9S#(|M+qxsPJ?jR3As)z$dky`|>HYyM& z3nr9ZmA0FqW0r$5%-LMu3(yEWQcMpxK9)}7heqV3aH=66gpzWbAzk}%doavVO!BI4 z1axx{^EhrX;6xn)`qh53jzSJj1sa-lp50>b8>9Li0J41A0ZEpT01_Jn3tlf=H1jy3!%`-@8Ei-kWkdK;pOr~L!P55XTI3*& zK-w1E+h8=uCfMK1Hg~-?r+qo?=azxF(iU!P>-2oBXx1c5dz)ZqE($g{n4xzy`mo_H z<>OMgapZ)UczCr9p$dg?3e_ffj zae8?5)XuB$b?@uVfI!uaQY2~puw>V2_Gwh>v8Sk^4Gm#FCd0j7fGEf5Xgx^Wj40Z?PR8A4`)}P^W)OZ-+A~Q zr~2Ll?fXDePLf&!@cjPh2F9C`;E4P^YZA^dGctK`|3|i_$c3hanF|q{+Ang%f%t(b zL4n>4rMRXpI!tCyk@o&N$xyilrXt{Dw*L`>|Gp!a9^{rs?0Z*^1?ZSJXc=GM3u)qLz2A8^)!D=%>l(8zAh7^)1gmsm8=L}@UK zd~e5gIgi%tVRZa+%+r$Y)cx8<%JTnl|>Td6WG9E5tUPw+0&uf2P zp-g_l>y9ne!|fOaa^WE5MAP(=;A;A|uA9`*L*FLcqc=dxs0HbxIp1$;vgXTC$4O^< zZWg{r2S5GVRKCK^Qv;X0*j^vOXA#yk9fo{XV$FG#n>!(GyXPzT*1qFCu;Y7j`YcY5 z^J@o?Ug#$gaQ?>d=Uvm1CW*jmD)X7Cpa}c$4wZ@hg92Oo zz3o`8z>PNDf5Fu08vJZ4F#lh;^%tA~CWtNyoS!0y|Ir^_0ZdT;-`M0rJZ|;i(Z64l z7Vm@6aQr4~>B*ne0Mgls$031uOz}T#`T%?yB7f%mAAIEkpVDW({F8KtTbq9qCu!FF zA1I!nW`GW=vz>QnU+Hb{!Jl6ZKv4fb8U4R0eFThLsysFLrwAakDQ7*M%sx&1hpjvR zcQkFG{yzcs?7WFp`6S`FqWO&d$}+;G;-&klaP1K5}on zT$;XZFL=s`6~zGX%Af?1km{)LF9p3_)%2HIJTj97^DAW@^d6CCeK?$=dm*;^*9nEL zHcEO_+-5Q-AC2f`#vZd7l+cOQYEeM4_S}nUc$fq86sw!{d)X!TH^W*3RDY^?yY2@+ zD71rGoRP@q`6!U%_eP8PXycdni!wz$aEU-+)}@`8G{ssPf$uB4?5uk}0;g!n*4w4t zck5&o@v->Ak~saJ()cA*R1EfJqM&099m;LpaZjJtDSyTE>+>g?M@K#Pw@(uUHlm+h z12}h?l!+5{*@(OEM<8t0UfX~^H%H{7DOx|^5}mpqOT%yqHrN-EaLW>-Fx0<)Z~&KxQT_dC!zfHZ++Yi3opXCDnDr!`QHsI6nI&}KK zt3>#FFq#)FTKDa8R0At=)2RrwV2 z<85@6pt}2B1f29~ODV6FWcfq^u10Qjacp$E>P>sC5YJ3e=BvbmZq-aH=69J7Pkfh| zW3=LcN539wB}?Qox&%~1CEYyr8$#gUH>U?wjqs&R0na75*etn#G&+{w2RpeqxilMV zu&R-tKeN2sNPWA}z_Tr4=j$}GFTto%*i4)`_q~0wOeL9SVH6$DSx-qVT`=>~+fytz z6_k$yg|+v*hS87FEVO)<6=4wIy%N;2BZiJfg8g;68!Q-Wnp2gz7DQRUc&E(|N*aNa zu&O_J>xP|l)imNi46nH?1i|A=Xx=I|d>}OiOY}(~BK_ZAA1-dE!+ESKKpcGcL>}oA5 zwXPSqP^LGQ+$;e9GT@zgCAwMGwaG))sbbNRG}!(H5f?Bk@cY2Px~z+?!Na|$=q7de zzFlB07T`JAIz0H)#f_B{B<4bHC3Wi5XUCXA+lOWHZfvaLmBeh{17< zvr#b3b-d3Aq$8WntCRip&2f^(*87$6Tg+Q=?VcoP20+k#;3=EJNO!|lMgug?qe_qJ z62{6!HO|eEOFJ+^m>#Xvyy}yMu36NR(sa4j=oSWBnG!-l3dUT#?sX;|?&@1L+zvzz zty+NeGfQK{H(%Be3O0DFw%Y9S0#0`><*Ei6bOVAK^6-%1V*@+>cwGTJVl{mYbl(-n zQelh;$#EITifuS%mY7g?9=`%|{9z3B#dH>T{NvKtcC-w$oT+ ze8_?667=($lIaCA+FF)cy%Zw&q9z%Z^n>CvrAA@O4q3Jj^;tcha4(>)w6ki!%h?f~ zN_lA1vZLd4F(B)(U9LU-U<^gk!LTV9j_PLj^S)YhJmB`K*%4<~SvhcpDaxbePAORv zcRJo$KR&UFGjtv;o{{j@dIysJ{$`}zmTt(}U68hCiptn9OR8$m z7x3xbpd3lIGhS^TC@3h&KALTmVo4$_dJdN6L#dl{M_ajmPVZQmY#_gATXqth#j`+)np5 z@8c(h48iR^R1Ra--xyJwsm^DLxFXg!*`Ogh?`3Iaj#B<<5j6HVS?J?gPXy}gUXYyC zp|Z<9OSq1>ZO3Y@O3L=zD=AOf!;U)x%MP z@D~!?^NmkNICdUw#&YS)?`Ax_04STk3flC=ct`57!V3C74XFDYW$|hXWcq{)K7g$@ zDpB0aUZl=ZUt#w@P?awoU@1mu2CIh?aKUb2_1VWM=2;QhRlV%i!Bi*BYHDb=rwNC* z1~Nua0A-$Gy~E><#4Y#)WbO}={kS%8n}t9%XW96MzZmA@ZLln(4gWt-`|$si zb0?46t&M2&b-9m@FSw|)zmMA`Zg=uRow3Y}i`Oy%g8cVK8i!04M$(JYEe$sOk+{t! zOq8_wNT;+I;8)n{FU*|3zaH!YV8RA2`)!EBt|g+8!84H@6u75CkU}?EhkTh^Oo$B$S1tBMDXX9I@W% z!3cGzZL7p3WbJLR@;wW&4cg*~%n}Ho68;7Ml{?KAlu_eWV9G<=w8!JS+m*pGqW+mA z4L+__J@CckllueJhNSW^nb9D--n2t`z^ekBbABFh9lgE?H%+A|o<$8Sv_$MdpJx2X2YK+-xm&|$jD@cXTwK)nm^_D+?V|O+ziYv`7QJP>NVMB*fou$X z)^h#ebDGx6te5dHq6w-39Gu^2;<<36q_D;!H)mUuLp+x=!|Gy9bbHUL#8G2Kr1BwZ zPfTCM2TkqPLM;mBu=g48MbTb|&ea?+LSs|9h&Na!b<;g(Li>rN~gJBf;15gC3%qqjBmD!)A#)wykRMIWP2aTlsCYOE<&+BV-kg>=R8Z|DYs>99HWSL-ueRF?ffBm}8HS0oZx>-XpSBdBo5ImN z`N=o4Wm3P7sSMQh-lw10M7)367jIbBxv2i-gr{Y=l;(XEd}nP-WSQqC4?Fn6VG2s# z=vn6&uE_-S>@-Her0u$!VCu?Z=hNNUDlMRu}1PDj$TfCUqIAiLdo4@jROoW4DgDklPtpoxpRwSAxEJ+sVq z>VDA&b-e;FU{b5`z+;c=QBKyOw@XN?7g43R#b)P#nQ5 z_qcM6Cnwlmz-)C_-)ZmrKA+%PfYnb)18NTS&4gw-=W9!!k^|+}hP=5|j9R72#}}nL z=1-@aGtc0WnapN{&-$ztQZ2*yj3$DDL}0Ek!+(UKt_Bkk;%G#2D;Xcuf86Be@$`BJ zm!4VOF61KRPO$&nY`wRCaAQbi!DWb>o@`AGvszWw-WcT0^?R8z+;v*vm7>AwKOT8p z7sv!X%R|-5I~^4e@@Cn^ddU6(V%p(B98`Q}Pw8(9d*$g?_rN^PfAZ7AIpb6nI`eo1 zxwiD#L0gMk0$SVmTk;=Es;q-d=Z<1MH)#VlS1Z+jVEbyamXC``1S2(*$3}iI-kh0o=Aw%LlbtwB;B*)Ip*I2uCT2N4DsL<&lrLJmpElVzBHaR{3hkjvEKu&S5i7eiJ#5KnJv&xo! zHKFK7a3rLLKwGqlOOCcIY>z1(@8rEsj@R1=`}Qof#?B@qmaj%%y3@OI0Wr_o4K%9w zxuKuwGGjj6FsJIT>Dyhjt?v{G@jWuS+QNb-jj4Jmot#Z4E!`|Pde7aC=j z8LK8{RT|0+%XbV;RRsC=Lvo!?~poW+e8sxo}(mX%KHPuL@giMhD}T z6Y=Dkv<%JUDE@Qd=HEAw}>8X2i-{v0slXvf|uy;8WdqL{m%susvrP)s%Zew{+O0SfW zR-C^+zo8hP=0VHiZHR+JgCo^%yZ5Z;gP+%~jR&{Jq}a*{m^5^W-dqLcmzB}Ip6yH@ zh@U88xl}(v~VM6{B`@yoIjuz@tHTx%juQiCeGcZS&EsiUqk>Hob2K| zU=Ra?v0T1@R3_Xci65j2;uo=*@h%aaBIX%HTgC)j`V)sZkTBCP+yDd}wCm$#Yn3e1 z1c4X4_^*JjlL3BR{u@Nd0PVgOCHO4kPxlE3253%dp*r6HcPWfCmJ2*9OS%Yfo^{)Y zU|L&?mjQrZpI5V9tW(#{Db12M*89p=~!oNxghy9~`R$lsh2mOC7@crra&!+mnWI!tnF&%zWfhY07nLyVIj`vTH z5JrIKcfErH)Yj^*{zJz*``WoJ4u3}=2ye2VWU#H zxQ=*N#T0?Or`8u}&#&G8SKeJA!h=BHwun{M$ZO&ZkN~GT0 zWsIcp$Zi)&ntZ^5a?kT*@CsI}WwG7kvAD2Mhl7H6|BJ3e>j)>c7Q3vW-n$&b-X)%n zX|VZeDo}{=`=ldIKmS~xv>FPNmdFko=qEnn;HzdK|BcCVchE8J1y8WkzM`D(xIH4L z%gv)U`FZkrN8ETCybik`nzvsF%Cs)QCn;yzL_kHVfvgm!kGsTbaZ*m@M&>5j@jup9 ze%v=p^y(Z~&q5Ov7yUx_x<~f#=CMlxE|lSJHRIR;3^>2Y{U=@1Dzj;##$qP*&G0Bc z|Bs$es&hSkP^@Ql#N#az^Y3V(SwDQkc?nwvUW9w-&vi&JJiMJMplhF}Fh+oAgYi2$ zZ%<`U$%4_6O8y@nBZ(%w38jQlOtB-nYR_x!!O^UBx{NnW0#O5Qp9Uex5zW3XGI6lf$`*kN;q=m4%ar(M`gpRKW4 z^c?Cc%qm)xqAI<}t1J#a>KxfiLMeM#%bDkAS;{(0m6mu|7fsMNpk2pv>gCrdVgTEO zGAktHV&?2jG53uo$CSbkG{kcw6Yb|$VHii+qPOy5;iO43x!AQpVQZi z*cnprylpI-SZ32f0o{_l3sp?uY4Wt8T(vfT-r=&9rNU>!qS_*&rRNqypK$YpS?l~ z+UK7IW@<@co|2=lD_Q*5iNW|isp+%5&Y9U)Wbz|kFS~zzzxksXu?(Ru{BHttHp!DN3vQ*wXyt{C?|4`(@9uUgso8% z1ElRxy*ziU@t9FFrXZ5}Rw)_ZmzbRj0|%S@FB#vQZajHL=OnujMISp#f}Ir#%o89A z66ZBMa9OM76gK9g&HBWd)T#In-6m6ja#r2hu(!-l2F9GM51+qB0TJkY3t2w212gpS zq-x47NqSSe`jgqN$U4!e^|E{+nrHmfH42n>%-sDP{14Fd~MM4X(bJ`(LKQ7(sw}|E5%z|E|EK%5$|4-feNrk zkVe~jrO8f750W|XnisSJmwxhwFGMTu@~j)ra?QXfZI{SNYDTFn8KRok?Gl@uiE=m} zC5WNKjHLeJZK>h2ruBB~dhBtX;Riz}0(Aa7IxwAc_JaiYSmOgE=tz7Hoa$E)h^_;1 zD#WO6$L?HPU}bTx40?G3WcJ8|Oz?&Ev8a}R=h&z+V9WCoTCUK{FRFo(vPRJ4#&;9m z-9uOyaWb_`;nmV{2{1p2N~v@Rwtl5D{E~B~qxij53eXNn=gJ6r&bo|`%53JC5=5vr z6w+wDVS1?$0X8+{!hK!Z#jorH=ZMsWG9OL7>Rjo?7J{6Z7tT!~b+a#7hReKDE%Z6) zvbeK`UbWCip@S@%nk)~lpxtR6>tW0Oe87UxMjJ`Q)mx1QhI9+J%^qlR5hRpvJ#D1a2GL)!fvvomOM9QXVv@9RQ9yItzmf&2)Qq|@oB$+ABUGp@wy$*QbClf9Dee?` zOpLk4;5zWAlUVzZ-^PZdc+5^eQTkr?o6#cAO^Ro?VqxBCi=may4a1u2jlQLx8tJzr z1AE#HG^M$a~bBk%-c_ZdRjWAO6GkaGc#N z1x~C`;sQ|Sa<#G83lU5$tv zimtUll2u9{J+DpC%9wVErK1uLYeN~&{9|p@~7X@6j!b3%}lRW<_R4Hp!XFxZ3m)iYKDv#rgf2 z2H`Pdf2XT}IV!!=Ro;j@IM*NjRTPUv=i+3aS;~G<+G>-%AB0!&$Szqw{PR{A8$pHy zR5AX8Qvd)axcJQ1?*A(U6ZYnRvYx*8X$ovT-jrSh($y0Ds7CyxfVsy1cP;g#&EMK6 zX{J|08u20&;*iA(@a+2Ez~>*)*#7^I^&zOGqwVttwfv~Iu9}Lw)T9XKFAnDbiz>!i zRdpAi11!NDKq(_>S=^IAIUp!a0Sq>bL5=n)I15EduW}K)$FSVebSf zPJ?SprRMz`K!vC#a;5S=Q`8V_!9h?BkL)%ky=l)+L%&vd^P`6OB7atCmbb>3%%f$R zuGjALr@Ca+cMIp6i3*~Zt)#R@D>U6iSG=!5=k5>``3c4XVuwGpU9HiiuR&KVs>^2Y zq*pKf*2RL3d*a8&C$FyRd}C!S zhX!3FW;+1yOU+YJM-Mvz5{z$n4gVx#^Py zM06wt?8O;hP^~JWXSh|q%v4$ ziNv*!3zsXbvh1GwPMqv#JUF6^UY7QP{r!n-x&f6~lYo?FSniso4rRsNxvVXz#}&A= z!&;?+jcRNxc?(G5J~)9iG((`>Xx+MptFsq0u?(TMhK9%mM_tIo40|#^(Q|&SBi9fg z&)ZuU+6RP3{YmIx(h5vL0$7vjor^+lrwmr+b@$@xpRN2|An-|X25o2?Qys_2{lQLrG{x*`Z@t{xkLQ77R?r);GO5s zXBLSjSP35uA5_j%1Rwb?is1`M*-ggef-D3%PIojZk_ zKFUtIhHTzUHZJKm%pj_@K(?m$@s|A}8UyiqvWzkOC01C{4q0rs)Vs{;;iVNzH9fOg zKc%F6A1rlF+3Oft!2Y}9&8xJ5$Ol@vk?`(M`_4qzB#X3I^(s}%3Db(T&EAq{Md&fu z3)BtayQK)itL4p zR6lrZgHK-1;hOxo<1NmYpUgN`%L-?5%yOYJ4O^aR?@P5+2EbbNs1B#ed{<`=*aZv7 zCA6%bw;eLgkRRvqvGUH#wAe^W5RgmtQx>In3~$mp|ERiAz1hC!Re4rE^s>UNw-YZt z)GE{Vmq>_-&|8B3#N}h|tKqBP8?hWI)jYEvfg5Fo^0+q4;TYzm&S$xf5Z>f?UY8MC zlH)r^a>{gFm-un!1_g`fIs-xM7|=hs1T(lEKpQzZRWQ@Yr}^FPG>x24C!1%~9RGlH zBR2?78=S8IoC6iF78-cmLB7iMXYL|VF4JC#DC?P@%c}2UfaL}=m%UUwLhNV$6_#w_e?%!p z_i?%XUlZYbLT>Eof<0ok!$0TQ^3xi(-DDz5^{UTa*fHup4qSc|nC0}?AfL*8Yd*?p zp9efSqQN{guOq!hPW6iy-?nl1*RJJ$X7Q#k@IN>p_MH`~FDc`p@Z+j$-5nrO_fBmK zDQIiX?_d@Z9EkV{TZLzj>WbC>-fI2trPDD^ehpmzGh*47;d#t@r`^!caN<6Y#@oXK zcA)THFmDJIgH((Df#v4ZjyO+@2Q?Ych95JoQ8BP~xR_(O&*pzJR1mn0-wBt4FubFI z4%|!0_v;D?MiUHZ-&ohR$)$grmpsOugd9pdSO>#3A_V-~1S*LPP;b;+=M>cv4^?cu zZv?g~O4*y}t3@1XIlt;?iye{6k2-Xsq?FTM(Z}+Me_netempu!U&y5@>)Z zJP8Y&c;UAbg>TXAbc^zu^k!R@t= zO5xy4T@wVBbWU_3-Y*L%-q6(Ku5d*Qc@<) z(s)DA_dOK)c}>E)kYa~&xo4mq#fr-Ul)-<3A-Dh^p|dh~Lg~3*B6T4jnmb=VodB;~ zWoBue4{KkZAtg(D??0MH<3#JV717#f;%$5z92y`O9+-Yg*gaX@x~<$_Fj29nj;fDc zd-$fAQ_GFOqECd6-=%SJGc?OZY*{(zz9I)TdrR>}e)KEzvH$aM2Is{Jig$ESeZ7t% zO){z;*JVXb_QyXmM35p~G{e1|p|E)xr~Pjidg9(cza)@qoxvGOlATcy%v7^E(qE?9 zrbT0x@BfHF=#;eH&AqrPm+)AEf_(O!X4RaJn=u6Q!s>t?o|l;=;FA_9T#CydCC#|# zfQjfaxg`Xa?7tJ6AxQsw^cF7&UX*)gNb2W=s#51oTu4`J0`8Mk!ZjDG$)DY9{xZ6d zm$d#RHgeyw{$zO-Pl6*?lOg3wh6@KXwqKLA?4;#?{v2ma>nG^qpXyXSU2~+m!IG9- zF1W-2FOzOhR`GCP(1VKtc$;naG`3>o-4z#N2It5ti38E~3|K+ny+u!?iyE+vU59vF z!3wJ7YGx|uhg+kTz0Kw>J zm(36JlAn@A+0+q_ZahM7y$A!QiSJaLkFVYBx5a4=i7=YCIc@G&%uP^YpFfUH(&Q%r zSeVP!W|1dJU^!7QCkRt1fwB(2@oSPy-wUN$m^zouE3@>pE~}56%jP_PI5y-jj{P&6 z|9jdb{J#dt#NS?x|8ds)4}1T1eEgEuo2BE+_sn*fE=XT_@6udub>&;;o zMn8yd5prqTeQAOzu)FwriQMRULJ0v_RP z{bOv4pGe8N0Uw zI?V42TX>d-{ho>`_I!OM+U)YZ)bqQpf%9;_z@%q*Uvrjwj9thw)0}l8_ImcJJVv|n zL+%%H-z?sa#-gp~8Hg8uakz%wubqP`&@(ALvn;D0DkJxHOxp?G1ksX}5mhp0^=fp0 zpx=kQw(u(JeiDybP05n2r}k!HO&bFd{e2<>Wq^+t(Xxei*YKjw zByzg@{0e+_A?}knivAI4+6#Ps@?uzm0opCmG<#RB z4qR&HqRiIP zm%B=OsJDYj0-oKLMrF$cj%RQGF4oRI%rs z53^VsK{wAUlcs>{eoS-HmBi>g4J-C_dGNmAd3e1xd0h`_$k`8jqGWHQIm}7cBcnzkdlRO#B^{N%ys3aNAeYi zM!AJXZ4wS|%X=nVay)$KR+7ewXGOX$3beD4Qo5mn4biyHS*Y?+l(3)!V(yZ_Fy;OX z5umB#3*Jc#yxr{gFniGYHGakcxjd(DyvM<4A#vGi7k(h-rtcM$-RY|OXjyN{0M?S)YVagrN?Gs!fOz&l^atNs#c z%aSNA3oKjus7JfT3^gpgohGt2d zus>Wb8UNHuji$wJ7nvk=u~w9BD&ND4rFF45kPsQX`7>yiy8^F@V)y(;u{Bm%K!SiV zzzW^VhEst20OVraAQlip0qF|7M)W#ZJ>$0fJ6pBFQ%xI%)dNFWRqo+FK3*;c9pYq5 zqp7Zh%b8@$>UMJvksn?U1A=%BF%3O*jVGO*v~=<&VqAM;Q9neo+Z^P;wvBSr0Z6g| zMj`OFpOFyoVx^jj2HzQ9?bqim-u1<7MB`}h!eCzIZ{{&kj&C)soy#)r%64@z8Till z|5l0}(`mb|s-=FtM36k{%iB2))SU|%sP5RU?c?63-*}eP!EP7dc_mm~K%J@GqqO;W zdxX@fqftr2#&3+xA%F@`3V<(&9a!0e3&4z~bcLbYKo^rphYGBPqYrS3F+G;`qhpm@Nu@lVUO z$IFwCK7Ak)JD^RpidV21alik;BeYn+*O*W;QLl;Qtn8MNIEmgKTh+Ip*Cd<#m{e$7 zUP6>)=vn+&R7*V2s`GZq%A?9nNx~(FT8pPQ zx}G?sPuk*6z9mJlhIDLmXpVS&i^8$Bk>;j;b>t^(qe5*J8K zV?+N-Vm*Qt5G50wGCtgd!jcf&YKV0Us7RX5V{Fv=3@CgUlb8~VQ8+U5En5BNb=07i zeCsCMJFj|}oJ5>egA=|!=FRmweZq6{$6#+$)l)q$_NFVY`h~18&;ysTv~JdGoEuVV)sm z!KGgZq%xOhV;#6OBCuhe%Vop{6C$lP0-Lrno&#ykXiM0~F9otfm9g2H?`_Q7COIV! z0jIe3#JuK-#|=wI{Fyd8PU*G)ntz!fS_uh;bL;#DA%Gd|_zH*)7T#bZZsWdwCh%sN z^!bX75L78&vA5-E0SXJUqC3m!f7=4vQcU4czkJ&)hMZHT#n(nP(IWogbvw05qD%$qF(?KOPf+M*lZ&}6LL-e!*HCT0Vza;^{#;JJzUCQ-7bUzO7G z)f-e)JR;u2$yEi?THf_f$SeCu+HYX?8pu3moRpb?kRf0pL+bO8y;m;`eozsYSjW3> z?$Oz0J*GEl6pkjCg~W>*)nA@z-o8HM2gx4uNZWpbG!{sf?pcdizDQ)yGf5kmT=JXm z0ZsF!mn$m82|Kngt<0067DFzXsZ^<+KbO~D;hh}J>l8dr@Eo6ar3~g(`155FUStFp z$e)J+k=z6(k}ZU3>pc?xbbiP!eP24L*r+Bqo%@~NU-AL~z^ftWJ{V-f;cI(>rG;RI zTK@3lF1xwLTr%eNfX(HJ%jd50Ab^~zcxE$3asJxP9yxy!9eLq)u|HoXaWMrrbx=%@ zP@I#mw*R%yaOHkx6-JMsdmsepew&GY2XxIs$}(`5!$ZS7=94I~$gR5KQS&14E1+qi zkH$5eXRLk{#{_ROq2uj#_f8h$DY5snVzJT^x#M`` zrmbM1{c9`eMP}0!@@f8^CH_Uf`B3>n!-G`YnG4p3((LI|sqN4=DVPDHY$n0*?4i+F z)3ut{%Nl+i1sHL|a}RYP!4=-?o>gL+zGC-dSLq69Sm36;(2EJZm0-G%f1;-1w1f<%wZq>+vLlf`VD~L4Y#lL}gHCsF00c$oX z_l=YASVe4&u1DaV72dRq7U@VidHM9oY&}26)vJ2v*I`R8b>J!`WlT|PGxKNi$4nnK z!$u5pU!HO@_pU2nbG^(!bRl*a;trL7be^!kiAf|(Q>B0T9)4#G!b*NH$--I$R1X zCdJmfS=!_oNor&_EqP6{IU6E5a4`E_+VVo|#lhtV)cP2~CFaf-UG6SX(%yaIb@RVes+ySN zU-sB8c0tRn^-N+zv(@A@M2*mu@3@@9WST?eqF?0JaK(lINXY0{MNJ0r`DC$r{>HU zS5QCZ-}?lA8m+bLj~#m=d-Ha_C*clC;I)v-RRhxa^>y3DS9h;@bZCex+U2PAsEO)i z!M^G4JR2Ey3dJ09*nGbn%h*n1*a5;n4xzIuObNc6lxlD%M`YAp8y@CTDdC2+uPq*Fd_vIn3L?(z0vg(?_e~#*&g{uU% zv-+#H3n{(+hueEct%^DNWaQ0{1+RnGYY9A@iE!bsiy?jkbNgVFzAMRPep(8(>SDf* zK#$Fanyn&k0c>C=hD^X8A0(iN3xuc;A>Wo1n1-!?82sXMifAm{;I8y2$|YY88Hm}w zo2kqzy-OTQa!X&3&FQfc4wKunc_IT^NYY4pVV?$_%NPA<**Q1Y#79=cJ(07@VSP!5 z#$pp@Q!}FX16{AMv9JGNiOnJ12!-1RIjm!TC|Cu>;p(?X`_k_Nvm-O*H2Uw2!JL?{VG z1dUZkmHafUQ>x8>Uux*t^RK2CX>{LYd|i-tpsi)ia|aA zyyp8r6rC*$ArCU!B|`G%QnJO2oz^?|CflqqoSSPVu=@||$GD9iP3e6%G4D5P=btb# zAPst2C)kp2ctZG-()bp1&^T}6$&@_6j0p^LT)NKcTV{`(9ErWhL@&xTqTb;1GHn%? zczi{PchdDVy=Z*kA(@rLwwUYaZ`}?)9u^Wy; zxekjZvRdBXG`Ou!q4U2qhjPE z*G>M2_#HbT*8+Ce^`HM$2v+CKu{E&37zZMYu83|BzRY!?+LxJ)mnEYL5l25E`t=jz zWTTf;PxoW1FR9e-@-$U2G1t;Sor&u>yt(1-Tr)r4FAvfOS1RA@@n6ZU^88)-U6XhM zUR73(^HuujlSoQX3Ov^2Q_0fe=12AJsV0&ioN(oqqtKk-YV4bCy_kDn|5gQV46vXe z9;ZVvR5eI-P}zQ@oBgL`yusKUsT~kcyq&ykdrgI5^@2l`npN^h8%^_DAs2$ zFjC1oUPy}UO_s`2shId%v{Wb8x3pZZq(?uJr%7{;8s+I$jEfQ@M*Szsh;n&N+tO^# zjfro!9F7>JWLxYQKuG?)z8nbkjX-`AdEP$6smf_4PHz8b6Z>+~=Qu^G?+Va8MMT7P?4!3Q6<4GFB=atCwSjR~@~NiLNmuDt}j0>{Y%e~*Ki z$zrvQEN{}w-d7CKjm8@CG=nP!L zM7sVqY2K4fCNIk|e%yCpqJy#V$xLtiHnPfY6;x;~$y|Y6hlDep=5K6Vjle6<>huT~ zJl*ZYIVth%?_|OwL@UN`w+!ictJxXk)T{eZ>;t&JkRs((wr$rZ;1WneUelP2A!WxV z9Ad?yC*Tx#FZgbmlLfU%?xL+~mPGCxf+@IYwTsCii|Q3-O#P07P-bp_=ws$lXiGOv zM7yJdWFRRd!d8_44m@QO+%Kj`b~TeXAno;~UxF@XH~!m%!%;VSyD^A}2$v%5{0}b= zHO8(VJZ$EWK;r5Z`|75XE(%xuuwHPf8jmp)nI!~Rx)JyMjzR5V%O=yQm~W8e zAg~l1N#1e>^|^J|#1*&_^m(AI%q3&iVI}zh(Ulugp4TuiHZxa2lh~9p6SOZeTwsnd zt7u)r5?fs?_)Cnj;{U4BpH|$cS^>dxnc7d)Jd+O+dao4{fMzuKy5t#dE9y4s-*}5X z(CdgW)f;>QE?`hlk(d0RD)PxJG*fBFjqxWCk1oE?$`k@dz2dEp{o-UZx6VxXtu^xM zR^8e_H6K&{{C=sa(uvf;ky|E`k-SP{mvDmm&k>;?*W;z-m<&J^*QY+%n#&Z^prE_e z^SWJGv%z9#CUQaF&GI>H`szMglq=1iwwMmvhM_4!e4Y~#3nxc&YUq8cvyf(rvU@Ol zT;Am4R}?1IwBGVMP-sdg9v7OQ`t93IiEi49XGA=a&)m^m8|Bw|k;M9ND$XTGH;A$c zq^{9 zx2NgOO3Wm*XQnVB3Wzknkxb|Hlm4E3cNO+ZHSJdr7Wdb)66k5eejytON=`V$q@nxG z<>IB1xa%wNPYE$rx%(x(ArP6}`L$1GcV|9238mLk3#iVu+!rYG)u=XLb14kgAY1r$`&4x(imOZ4CBzrR}*VXP*?kk2NsWfVDkPTAWwxgkGWEbE@{W{;;Bo3>NQ~3?`d*nB`r5isSb(6}| z>ht3!{KAoLvC7<3WSAw?(fHkW_^|6W`lfE-gubcUE)Lwa`-d)zSx}u*Q1H~Ags|S_ zW}!&TM4xRg-@14GuGx+h`ixN1)Usm(y5b+z<$~U*qt6EP1ICpb0C-IA8K&Z+7yDtN zs-Rxxvf`HjBH-21f~C#kX3Oni;6)M4R_k;0=kU~1phfV_6OK5f@;V1v=04jIoX{vp z$Sj(-MuDuoXPGk7AvfZz{#Y%s=ung(fjpw6Q;j1!xqNwiH-0ingC&+cC5Sbl!?~^c zLHRyVqQttqFW3GrrrCHucF#sgbDW?(H|9=;|MA0cJp3$o`X)nKy|++=W&pVTPkb-Z zMRZ@HoGEpGM7qL%{X@cxMZejK5XYrE!lB%_fREM$)w-TrgNu)7IiBkI?; zigb+Z37ERI-C3<8D7Zh1QA8`A>YgdL{Wyj%*Fr{j`aX#eBwzohdf}6lWAC0zKdM41 zt{PM2b~cwvLblu!s+5Dl6-T8*DlKBOgb+-DK1&%5^*6kkoKZ`c;7_B14MNX7j8Y0t*)NrJAE zthCkku@W}iJ(;3d(u+V)-#|lKdg!RIrR9}L>3LuBP(fbsRrPHC$gm@I zdPG1H9@{>nX(x+sPxg}W?Uf$mZ>}y8je(@rx^pbPxd9B_b~(}A;X7AHUnt>mlyup` z_CX^_W-8D&^zsmC)%9T2nNLK&d?HVbO=6$+)b-`h_zTTT2etE?1v*y>XvRJ3#Wgrl z%@)i1;#4ihUcbWk_eovMG4pWZ075QRMtUrsK{Ht|UO3kG%v)VF2yrJ2)~Zxz^kR~ zBv|9egmHvO;2FT^ynLAXYDqqVd@_By=M8A~owFb116m)~uCo#>@#WxCZa3#2%YE~3 zpQ@xJ_hE%xs^2r?dnI?2i9L% ziQQW(e@2p}RB0^<0^A4|ic)WzK>ezHx=q72T&R`FjW*y2s7BU1dp?l%vV0+!j=#U$ z{>~Q8a=YMkky}=WJMrp~mE6n+_^&8N-qXR@9SEzDS_RDn0vXqHwEG>8Jr~3%qw1MQ zJ@V#~u6@fR0uG=x19IGrB8|yfa#k!~Y-gHB9qEU;t$ex_^zfWGxSs*l-ZNshst4^1 zh_3#{Y4@6O8X}{w-;uteHbJMXoD*U;ZeIHOQ@dJXKeru+a5^WvF$BM%gWb2A&*+6? zuU0(G1@SM-v3t}L*9Nt8YSZYyR?Q0_Chq7>U%F*o=YUnU>)h&R80K_iVY*%mfG<5d zf?9OxQ1utcxrQBW8HtRQuoDr(ywIHz+i=WeJ)tzUCND$YoywM?gx*wc>;R0+ofvCT zraFIGEp6|T$RWLy$K?+zqK=%qUEEjZ41#UjhMeHbWqn1LcTedeC7J}*)hmAHjrUbcAWh2#$jsxG^?qOo)P zL0b%ISVh9oZDB(I%J`L;*FD|#k8-wIkGZ__t;+gJZfER*li|GEfwM09-PnFam1UA< zFT1)i_=<}DfKh4HHoV0G9w}O4kdd9{DkG=UMO#>nWaV+I8}pzZY2X>jG2)2s$7;~8 zgHI|Ovi2_RiA%SCxoRk$lD!gi1W!2-yMsvf;BFjSp5%QkeB9ggph`+V?-`sp#x2T><;H)&|b^>tSEiCSGZPN<= zoK)X)n}=2!Yk6F2r@U+I(An}T129Mdy3bg%%7Jw{vHoU*DY$>?`>c`T*`Th`Uy*;Wx@AJWL*X0BZR~3@R%xO|=FfZx0 zvEM|IZw44E+uYl+o@7$OBSuUygWn1`D@9Y#eM1xOI7?bexC2X!vIeRkm2i4KF6s1x zFJ@4A{|A3!0S7CFfbsOZPlaj~V1P5%d5QjYFn21@VI&)nNlaxAFwTBIn*M#vY4f?L z^K#Zl_$RZP#U)8MOZg$gLDNucl-RC~hwCY|rI>GFJ@aouL{7@n$y3+LBCiu9STG>n}e5qTyaS_OT z^=A@y*ET4;52v8v$-M}dxIyvv^QM5%sWPXw$G#u!{nEf%MMoleTAYW)FN(`vb>D6h zv`Vg@zh09*vkGz9TOm}t@v^O!0^lmsE_V4Yve7dkY<`VY!^Zy;QMhmHY&r|*_TY!1i5;Ch6BmXMz}(aQ4B%P}hO?fV<| zZC6S?)D$CNc{7T$`fVk$Q3*S4fxCo*c0yF-E;gWG^(W+Ljot_O*?B%907UX_i& z2e{CNR(87G1}>3H(DsZ+8-UNN+ASs+u+|${9?Vd#ZZ9*e!!*wSxtgN?Dzl0j)Om38^j#a-*6xU zlZDj~)Bx)r(GYlAyY;)#RkV-5h^uNoXZH!jXmS9|6EQGA&3!QvTje-Ep8^l>|62iQ zIKahE;)QcGI}YMSLZf6DVUq)whHw1`F2}$q+=m6dem~%3LRPx@1O(InB|!omO8|9? z-U|F5DkBUG{_`1~%Rd-2dOUU?-F=uqGmuNB;SOB!i_evbtXwrOpzWQ~xSw>u5aVvpe^UQp}rMy@{`C3Y3R7Pv{#E=QRL)Dp3I>hLVaoG>R5X?D+ zJ!Nt(;xEq`qiaTpwofnN!Z@g)^mX|OpWoL<{YpqcfU9!QKZEM8)!H@Fi54;Y3blx0 z62uJF&-a&C0{k&G6TTO>e|!ffc$=dW7E_{CDY4WWao|>G`BW{HdZ16UsFCQ`j+##( zm92S9re`UoaK*Hb2}srj65c;I4e@d&rT_(yFz!yzEUhX|>Fp)Qw-55~2g9FceX6tO z8bTP@!r14>9lVk@TMF{%f^ywTyLVw$aN#VqG&!myBOALukD^vqyV~g_)f>WJcf43l z4pl1Z2|lB5DF0%uxgP>!%a1|RX~mLH-v{*?f$FC~-!9wDwT|=LI|LoscBWqZ3E3eQ z2wYyLAtG8&7q`oJfV6t1P$+9up5ePN_4fayVqv@{ALqEXC`55D6+09srx&MllW_N za=?n&sn?Egwwi>(;EI`J(*rJr-7t41*DR=H(7p$UN2A8OXGMV}zSznWy_xbAjt1F_}qt$KK?J%*iX8_)JnXKDrHx%e{GAW|{Y5R)m)J z&_}F(OQ{|-yME(5{r{=gb|rS%nU^`C>g-rh^(38yRjBEshC}HA8c?`BGo{n4B3H17 ze1~8T41_0lkMQ^Iy&J^euDVk>#z&vW!?kK#xlvV<6-IXw-cmRgHYx~USFO572E9e_ zx!p)P3v>E)Pw2#k@aBHkEhR5mhf2?&*6q@TWbfN=Axc8`68E!nm%ndL>mAmJO%~ta zZnRmf;{_KDQ7kyu6>iBz_X58j53|>fWcCow6^yyqeX7D+oI-X)X@l^^;!IVEQeO-NfG7HbPc4PSWe%JF} z_MDF_(fR;Ch<@#UHgPg3tIjP1#4%4jWjjxkd{(gE+*u!OOO=$$bomU!s@J#)vDu`8 z4`x2-#6&723IpqS!@Qp_=fd^Nvj$(&WdN6N1H~#qLlHdIC zcJ*&@#tK3H7!0sXsB7M$SSHx;UaH>3f=BIM`GR!;MJw(;9JpCB^IYCX*>{_{SCFr^ zGcrHiEa)z4ChloZDexV+d_0e^>{so1x8;7Hh4CkpH(Qe zA`<=4-K72ITk2EWiJI@;Uq`%Eg3@PdlrIt2YoI+0WlBNQEosLh=sR)pm8u&g-F`}< zPd>HsC~y{N+qo{_;v1NRt_B!f7fekpJSyNnBvOwQ(M+}^sSwpKzv~aWV$^7a%JsK5>hmWVzqA=iZPVrP zl|D7=E!Iowy&YXRy7Kwz<5Xp?Xv_zy+y1waGT%o|Jg8L?^Y0S9{;2eDcXGt%YVwO3C#O&-*wufI`4y7&se!4|}ho?}) zG*MkKXgeIF?3Q@=LQk7Ex;#`-j&DJ#HJL1!dU^W5z_&73zPi4JmC_9Pu4Sam%%56b zcgp>GK`|I%L@{|zJ^{7VK9&0$R^T7RX^WXrT@pCgtL2+hvvzJ>A*kubV_5QJ%8e-y zy1erao4aAm(l47wv>`DP)g1+rt6O$2?n3M(NXq-K+11=-;Pj_lW`Ftq%_#f(=QG7e z#VQjbdzugWsC=DB-7p#^})B+{VVgFs%*wn_nc_t7W5x4aYVRhg^dWekh|$ zQ%al2Y3iR#q(d9PWBdgPUDy8c;R|VPm9tz<1dHKF&mdy6eo)9@SVc8aU-|w0HfTAa z2J1j|k&Z{7U!p(g>D_;W3KTMW&>g)@wgP>6sjRu=_c`Px;?4!MLJc_gn@Z63#%;Lx zJ&443k{Go0Y3-PXhs`M4^~rYep;HuQef%uGfy4deQ4853k*Vpe44TT-JeI_CVOOuz zf>UQDJS~8)G`}_9BNtFn=X3Qs8kLP`l<@h-V)R!xS#OKxgp|BSk~|;iWThsl3|QWo zP&)EL{UAGj%a^(?HN6KDve*_@o!Ns$2+}bGF4dq36+W{F8R8yuc5S7j0VstGC16hi ze5(vRu>V*2`UY8c%>X~nz$=3%S@PS_>E;AYy>zzJ8>D}u3_%w*L1CeN#5WcQ}xz{#<1K+4URzAb@wmLjH zEPhG?@Y~*z$(jqj(Rl^h3BqfaO+|U#zknLFmk311a*jRzDTMbV{}jU26#rifVYKjl znC65=7hMX|=&i^zhUp&C!Ie010EmJE#7Y;0$VpRErjJczBaZ!ffbLEA1E6QZyWmD| zUKl4q?94hTr#2YG8}a{uoPvK0Q5eJG8eTL&{)B#*|H7uLj6o*=5ZVTvEbgEBeW)d~ znR5hH?SK>XI{N#qJ8B0ah#U=fn#%qV=EmsGv@P1m!!r;GoR#{CEay5-NhVCyzraXK zWMvC=i`X7fb~IeouBTw5ojoIGrW#G516RsWE0?{9vtwMife(#L2}mD^qo~?0MyTSe}i4Pk{n0(09sf zAJF9=X5S(_^7I91WUJ7GrkYl0dHD7+M~ul3mDrjVTQWsc9~HacW7y_GVwRkE(EeNl zl#!P~EiHS!dVYofJBkj;XEwKu&%4oj2}3+Fr#8xW#SCr2EPiKSDU7iF2|e>cBz@tp z1cYa$-axUL!cZ4lv#E9ZDJvK6r3qm!IrV^z@N8c!AD6{a1@woL{qhvsZm!$lb_(Z; zRb8QTfN1TP<+X>23GAc%T^`dm%1os;>n>zfCUAi|1T;WLc&p# zo;R{@$7Y33gj3oU0nS|_S>J7`+N)6}8J-q>8JhRlxubIWrM$L?()_c#WQQo;3x<1u zDo^kX#5Ww?eG*Dr@Ve12XLNc6W0((oJUDYY+kdW@!&!^TgG}+8fN&;OR79Xysk~eR z6+7$}%DxVPI~LpCp)TK!RC5-=smjH3nN|Kl;FjhNvpRYkt*fnteY4=BGl5m48u?L^ zt4vX#Ai)kj&VlW-+G*NZP@Zx>jCQQXpIk3o^YE;iWc~BmU0t{w!%kDvG%uB{QuWB$ z`3IHYMig6q#C1}ff+7!$p_6G#&0f{7E4aFhZ_GMs>J7Pni?`Odtn}%KI)~vX6Q5YQ zb;QsN6#G0N#4z?y+~taC&Uv(+R=RrS@=%f0-cZ)Q4|igWLu3>*=ojQSKkb`vt4l7R zd0|jN(S{%%yaziP600*Y7&7?6k(}j_CTmM>L%o1xA+)|I4VRv>R1~fej@7;#JEi~U z14u&2??lv$U&N!Za4zT^FWLn#9_DEt4m`@)ZG3DXkMa%=%N`F6hOroUNR;qsi_?P0 znf+JGi^4wz_6hL~{ysE#+DJBZ+rHOno~VGOEj@gEpN-*4O9< z@!h^AcKROpyP;zHZ3GmSq80+4kI|KRh1%_V`2P8<%=>JZxK7i)X8?ZR1bsnTgtq7f z;tl`u*j9aU$>^NOO-a zD%G!$gon0iybi?GAM!K}%ZPLD^&ofq(ZkZ;*L7;f$F=gi{mgUC$5(OE)`73A9%eEG>TH#tHmY?k6*W1~7%3(3F1w`5#32x(DUPMWu= zqHCdkB#+CWA^R}a-^({zw9=EIVrf5=6*N0|ZgsK$=lVavSpMkpq*upW&Zw4BnNfI_ z0A_jSw`ixKtwJ`ikInc;LBHBEzCeu1x@@1+EZ)qgQK*Vhs|(l5;gg3sX`;JEFN;qm zo9Idpn-9Sfj!V|-#@6RT>H3A(<*^G#w9R+JV^=ejjVe6lNk)TmmBDO!XLQ zBf4ijJKc6I(cs!6&%jp?ge({qr=$bm;?V1i#fc%;4#e;R_Y4^!Fr&>$#lNt4ArQR@ zUdRh+$S6pXI%p7r?AH+q4su!XR~_wt_0Cn!VsT4hac>^4S8ZsYA!WG!6L2?nR0Wzp zAPBw5di%!Za3S4qg-ne~HBZ_Yh*~y@oCXXg4C8dOA`fq}`xD7JMNZ2<&*JDom z2QKFp0cOHLC(de8MqFa^fESh^TxNpbt@IOd)p$J<04GjLOzv`JA!>{6!605@H4X6_O z$o(_gub4F>RCaHv$i*1B8US5}Fxjk3rmlgA)SP<FY=EexmHM465 zxL>V#>!`Cha^4L`w{+*|W?l`bK4~FO47>}y$L`vzYzUOUY z)^~5*_f*1@_GxuUFj$bElK2ZDVtzO6wrT z`1snPINZHOijx@|9*h04P-gK` z9A8%#NKOIlz>;Fva{0cMA{juHUzOZz^M0oUMza`6rf%2VVbSjhi1N7Zhf)8LeF*jPxAj zWhVE6tsT*qA6;6&CY@P8T?b`I8}r-gzvn@ESb+s@8L@DaVnM87TEZ};oM#( zrWw71Y7X$1ycHmbEYWu=i^|d&OH!nGVteL!-`VYvh*Cx1Cn@)BtTz%CxpqW^qo%#q zMHO*%RVf_$1K)#g36SRXw>CICW>~=A2t%}}4Rhx->8vWEn2|q&BExp=l5%aC*)J=k zJV5&0<4$DjcZg2%pj4t{?E-z!6?NX4+=-Z(flg6VH=`Z>-mj@&bPCmR+QfRUp;JqP z_MQ7ktPqP`VM+=ppj+W6Cn_uU|00Kd?Ec#|B+MNUbc^d;1O8#wIUt0{l>OzW@%`+C zpEwKWEzh5_4g=xBA6X@I=w+Qd7FOnHUuFH_hv`%z^27Qawvt+3BpoEVi|309_ux2p zSghP0YQ?dv#b@pBjTD`kisyEDuRqiUU<4+@DF-#;EJsXAnt?k@6`HGbP^ls$DcHlM z;l+U&bHk!M-sv7`?8@@P6JS6p9<)r;HxhE3>d@xYEB}Fpc5^veqQ@tc2d{jg*BYNT zP_aN_2-?oGr7b%Ux@eKFwkD?1Y(L(;Jm|FJ@b!z;W{+lqyI${#7gq?T?`ZzLVBy+d zxjiU>p@tu!_qMBkP~y}~1`8)?$(tMx%$mALd)7joI;}S@>p)PWG8b7OkmHY&PHmqD z*&8tOeUM1x)T1S1F+3uP>b??& zcqiZ<+a9=57$kepy5jsZZrZ^=HGv;ik0@kT1#N+E5h+Y5?L_qxkl}fSmv-`|_RA&R&mKC$u5*0Nb zcZr2Z3E;r_d`n(BiDx|9t6rGBFXD8iK~(t2-rR7i54t;%2Bk)*JDtqGCuz0MGqb~T z>9KH|>cE<^K@+`<%v^qyk$l1U;B*_(`D6Ya!+7e^kIXnSylNx&KjNeFs#sT#{?icu zE@Z0#kiH$3Ae?+a1(%H@Yn3R(55v?kqq>?kWLT+{cF?gQe;4Zwv{Q4^mIJ6UraJ}( zZKg6MM5W42NvjK0r}M`SsIsg9w?wjAxn45(O0!q}r_DB0Wi|)S9F;ZDIWgq<1ri?} zB=8}th+gpg< ziG1qH>U*eyvT38sy}|k0S4t6Y^C@Ug0aiEQ(;V;L-W)3OMdgj20nPX&ejm3gUldM$ zXa0OsGkSIk4Bg}jd1(OIUE+UXy=3X2xis3tzarZG-ou!Z@d`KbUHvPlX{(77Goh}D zyc!G)9==yGi$VPARK3+fy#PR96jJEwMxlps04_qn)T5&hw9?N|vhL5|?sVPIynw z$L{JpM7;X(G^=*?sV3Br;CkalXRfH4?a_acv_O}#gT6&>Q*dqY1?;3NCgoPPTw0BS zk{}wJqRY6Q9?kPdP=#s1`vh2h!mi>czEV*Un(md`tX^-5rPZ8k@&}MbyUm6?nw#yU zpU4!k-A^>B$%ISJJ2+3_#rxUW6gW|?84>AYXJi{b>0PSqu<<+{{Pu5JHYiZel>3e9 z-?$hV7?u2K7@<)K?r0UJ?R%6lNBK5xZ$DMk@Wf<0-KhXwmzt!Y)kY~z)Y4NYLmp!Q zoy+$$eX6FVQgn6S?+Ir=CnT_{Hs&T?c<#Jsln!Qv_$s|vtx>#6V8H7xh|ldKL4k;# z)t?mdiDPUEf~~j_p*!hQ|=Zk5NX6=7k&JCtZY<#|0~(xU=|OCuM(4AfeYoD zXIVnlWS`j$_w*;zOt8+X@vmB2PASV@((bgjD}~i*OfQ9#{hwIazpcE^yOO%8+gAep zameGUVG38#908IM%@JT|{NV^vsDAoxZ{O-N(@9pV#58MNPesC&JB#iSO(L_L6V1g* z5Ye6U%DnC-BCvWb`!eQ4Hg(KXY4ncC`VfG%uB>!_pa`NGiD8~SFoq2gTdSjE_85h) z0M-?#KjnM@oz}xI!usgHO1g~h!lM5R!!-~>?jmlz|-!6kI4uvd5uTftA$E`I~nND`)pBEeakenVfs%w-!BzP$n`g= z3}?+S@zcw&PQZ6yJcFV8xieGsrPo-97%|D&PUkAQo=s7g%a`O=J8v8tY^tL{gbP#kU@&%u(D)en9G$`fkz}b`+BpnO5FA&qVB8 z2buvq&6NQyuTLos7GL64xK?^E=Hg7Xf9B#yjq*7H3ytNa%1YI&r#W&x=JuxrpyqsC zV=Yd&d4<=xS}u2O>*rzhX)qk#+p?#>-ZyRoLACjo>i!$gu4*bFVn{Iffi#ip9=OD; z%o+Dl$)^8YuV^3)31AWdittD<$-YqV{A}Usopxt|KO4oiBE^RfCN*;dBE9kuNPjGDHzjLXovvEJts?MS+mQH*^a=(#6N7Y`dMe0f9mFiL|9M@?m^+yY;@1rZ;iSleZYeKCht7rKPO>MY%#Z3cXrYdO>rE6 zI46p<4YOQ+pZ;DW=1}Eq{R`Nuiqj+;iWENJ`mQjRd)J2EL3S?xMX#Ol%Z{@xIM=4_j=~h0^OSj&mP-1+YRoRx_*d(1 z@Ij(Ry^3t&ZR2EHw;GCs-m>cTevPt~!HmMWuLvR%_tVo`Hs7je+QZNl?R33B#hitV z1{l7+()^MJ@(;&>gN)8%n&V3wX=8-8KQZ6JIU>4EtCo|=Xis2uHn!)An-RatB;NZ zNI&LW2w+}qk|5tRLK=3^6ZOAAJZbJSbSN$El&V6g(_FezSWtx$MDY%Qv3LGwQB33R zMXbFUrv0He`z}RJ{|9+PiC=WO>*!dW_f!>YCNCBoXG=6beMev9VV8PJf`JiiIWf*X z>AAAYe%KlY#`^}%Nrw^lDUyY8&r0{Tffp0X_rS84Y%xhbh*pAo?`zigy>@+?i<~hQ zLPaTbEQjuFbI#-mFYd$D({gKXvWkCrs`=5`I;p3COLRYB)|Vb)F=w2UNzVh2k-javhw;$ zPhvKi;Wf(tLzQVZb^l>=e zu#S&#Swpm1_^3lRMlw_C_YW}*u-ERNE^`@|Jb=USW=;Y(-nOFvc^%Gr(!s9|m~Rtz*JABU~IWBWSsa80|bTjqXF?yd(~3^Tqi z#Npx~G6T8*7NOg4e=d%w&EDHXZM~V_YScC9wQQwVC#9RJ#0-BMii|bd#B*roWN#cl zdu`Xlzhu&xbKP`oWqX^7a#;WWQUH@!G)w)M(*6EW>fNnp2p(37M3!qifh19cm-n?} zbB7#>SvnZSUEz-*y@HlV`e?WHpX2(P?0S3{TMnjo_4;?S72d`D@e6Bl&rVpPtKp_F zbTwSuR~$@2jri%sVpktwciIVAmkZcvH8gDA2n0&Py63#9?xnJ6@cs`P`$tVDEAAN_ z4>xE2$>@RF?Qh?Fg9&24<;z_M#83*F>rZO0;zwBpTagp05=+1P)OTozeWrm7o?eo! zq~G=Uy!2T*%JT`_Us_{RIUIcCGcBJ9Lh_f^oz|c&M0SDpjILS-mo&F5as(*Gl5}Q$ zRvu2d^Zd6|=1=u31wf=*P7)Ml2$@pxc2uVAlR=s99z}JT|Ix{uUU9H+*BDA0{Z*+@ z&Mr3`oIo1w*?qZ1y;Xx)2g?ovFaGhh-c87%x$<7Up3rMSN1;@}00GYTEi%!8GWSPq z-UBX`(x+NizFZoVXPm06$+Czyul7`mSOB90TOEv=IiC7{~bDm_ZQ!L2O)CQEGj09iR zAyQ|vQr;F|3DrzwTuisz^B}Z!1(e?!Cx=y4O}jvOhW^?FuQ}M!g#JROv%t;n{7VHG zV^%~A{o?Q{iP5>$H+Nf=8_FzI)6&NrM+86QXc@Y=WA~PWZ_4@CH#sMfBCtzYf2Gor zAd*Trl{ov4sn+w0>V~Xc(cTb@P4BHM;hOAb)o(5g^DR+mO(n+OWIyK6X}@<_>kUK_ zf^%N-fC}I?%ZC+ouJq?qrd~_+u1g9RMY>Qo&hFsk?F{r-8nyr6o|(`%*6ZVb{$Tnf z;?BA0#4Vq1N+|J-iojuJ!uzYN}XkkdJBXEmPE*LD@OtI>@ zq}&_-CG6gQr0Anl>LhfjyXVJ-(Q>|N6ud@N*2@+8PAW`ePn6KZ;`04%fo=~?F40~Q zH}vm&n!7euqW)L@X>Y|BvFX(-#OofKY|e6~yzRE{&Fp80n>v?LuUd*bar+fu!@qTs z=zr%xWlPRT#q`Io_qDSGE~nM+oEtpv`X>K#>?~KRx{%_1$~AWe`vb+HVd7Aq=;zD; z=eNbR}3I_exBSdub#u|`^>{8V7s7t1z{ zNoBFrvlZdpNl-h`LOax0f-D~M_|ohygWn1yPo2yI%3h5Bbq)%}ftN|7O%HxFcgQ{G z29{=SfT(h~tk69p9u05^y0J;Flx0&}?sZmTA*!}H^-*n&ljsc4r@z#k&rSU{v(w`D zgpAZyKK3$wSdL`dA8@G1QS@`_B=i1xGmAUc&qfUA%T9?zv*d9k|geB;pIF<2)7_+T0>80Fwt#4>Fv;pxW5cNla z^N6SH-S?Yi6rziz_~J2WQg#Eo^0v7F-!Io?(OE$RayIw})Fdm8~!)@BmQ)E{fq1pDna$Y8!J@h?XM^J^)!HT{<|@9W3>@<-L)^rOo4qn8&i?`oytr

YlL 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 Dc9TY$ diff --git a/features/project/find_file.feature b/features/project/find_file.feature new file mode 100644 index 00000000000..ae8fa245923 --- /dev/null +++ b/features/project/find_file.feature @@ -0,0 +1,42 @@ +@dashboard +Feature: Project Find File + Background: + Given I sign in as a user + And I own a project + And I visit my project's files page + + @javascript + Scenario: Navigate to find file by shortcut + Given I press "t" + Then I should see "find file" page + + Scenario: Navigate to find file + Given I click Find File button + Then I should see "find file" page + + @javascript + Scenario: I search file + Given I visit project find file page + And I fill in file find with "change" + Then I should not see ".gitignore" in files + And I should not see ".gitmodules" in files + And I should see "CHANGELOG" in files + And I should not see "VERSION" in files + + @javascript + Scenario: I search file that not exist + Given I visit project find file page + And I fill in file find with "asdfghjklqwertyuizxcvbnm" + Then I should not see ".gitignore" in files + And I should not see ".gitmodules" in files + And I should not see "CHANGELOG" in files + And I should not see "VERSION" in files + + @javascript + Scenario: I search file that partially matches + Given I visit project find file page + And I fill in file find with "git" + Then I should see ".gitignore" in files + And I should see ".gitmodules" in files + And I should not see "CHANGELOG" in files + And I should not see "VERSION" in files diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb new file mode 100644 index 00000000000..8c1d09d6cc6 --- /dev/null +++ b/features/steps/project/project_find_file.rb @@ -0,0 +1,73 @@ +class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedProject + include SharedProjectTab + + step 'I press "t"' do + find('body').native.send_key('t') + end + + step 'I click Find File button' do + click_link 'Find File' + end + + step 'I should see "find file" page' do + ensure_active_main_tab('Files') + expect(page).to have_selector('.file-finder-holder', count: 1) + end + + step 'I fill in Find by path with "git"' do + ensure_active_main_tab('Files') + expect(page).to have_selector('.file-finder-holder', count: 1) + end + + step 'I fill in file find with "git"' do + find_file "git" + end + + step 'I fill in file find with "change"' do + find_file "change" + end + + step 'I fill in file find with "asdfghjklqwertyuizxcvbnm"' do + find_file "asdfghjklqwertyuizxcvbnm" + end + + step 'I should see "VERSION" in files' do + expect(page).to have_content("VERSION") + end + + step 'I should not see "VERSION" in files' do + expect(page).not_to have_content("VERSION") + end + + step 'I should see "CHANGELOG" in files' do + expect(page).to have_content("CHANGELOG") + end + + step 'I should not see "CHANGELOG" in files' do + expect(page).not_to have_content("CHANGELOG") + end + + step 'I should see ".gitmodules" in files' do + expect(page).to have_content(".gitmodules") + end + + step 'I should not see ".gitmodules" in files' do + expect(page).not_to have_content(".gitmodules") + end + + step 'I should see ".gitignore" in files' do + expect(page).to have_content(".gitignore") + end + + step 'I should not see ".gitignore" in files' do + expect(page).not_to have_content(".gitignore") + end + + + def find_file(text) + fill_in 'file_find', with: text + end +end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index b33bd332655..4264c9c6f1a 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -259,6 +259,10 @@ module SharedPaths visit namespace_project_deploy_keys_path(@project.namespace, @project) end + step 'I visit project find file page' do + visit namespace_project_find_file_path(@project.namespace, @project, root_ref) + end + # ---------------------------------------- # "Shop" Project # ---------------------------------------- diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb new file mode 100644 index 00000000000..038dfeb8466 --- /dev/null +++ b/spec/controllers/projects/find_file_controller_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Projects::FindFileController do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + sign_in(user) + + project.team << [user, :master] + controller.instance_variable_set(:@project, project) + end + + describe "GET #show" do + # Make sure any errors accessing the tree in our views bubble up to this spec + render_views + + before do + get(:show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id) + end + + context "valid branch" do + let(:id) { 'master' } + it { is_expected.to respond_with(:success) } + end + + context "invalid branch" do + let(:id) { 'invalid-branch' } + it { is_expected.to respond_with(:not_found) } + end + end + + describe "GET #list" do + def go(format: 'json') + get :list, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id, + format: format + end + + context "valid branch" do + let(:id) { 'master' } + it 'returns an array of file path list' do + go + + json = JSON.parse(response.body) + is_expected.to respond_with(:success) + expect(json).not_to eq(nil) + expect(json.length).to be >= 0 + end + end + + context "invalid branch" do + let(:id) { 'invalid-branch' } + + it 'responds with status 404' do + go + is_expected.to respond_with(:not_found) + end + end + end +end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 82f62a8709c..2a70c190337 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -434,6 +434,18 @@ describe Projects::TreeController, 'routing' do end end +# project_find_file GET /:namespace_id/:project_id/find_file/*id(.:format) projects/find_file#show {:id=>/.+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?/html/} +# project_files GET /:namespace_id/:project_id/files/*id(.:format) projects/find_file#list {:id=>/(?:[^.]|\.(?!json$))+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?/json/} +describe Projects::FindFileController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/find_file/master')).to route_to('projects/find_file#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') + end + + it 'to #list' do + expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') + end +end + describe Projects::BlobController, 'routing' do it 'to #edit' do expect(get('/gitlab/gitlabhq/edit/master/app/models/project.rb')).to( diff --git a/vendor/assets/javascripts/fuzzaldrin-plus.min.js b/vendor/assets/javascripts/fuzzaldrin-plus.min.js new file mode 100644 index 00000000000..3f25c2d8373 --- /dev/null +++ b/vendor/assets/javascripts/fuzzaldrin-plus.min.js @@ -0,0 +1 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0?maxInners:candidates.length;bAllowErrors=!!allowErrors;bKey=key!=null;prepQuery=scorer.prepQuery(query);if(!legacy){for(i=0,len=candidates.length;i0){scoredCandidates.push({candidate:candidate,score:score});if(!--spotLeft){break}}}}else{queryHasSlashes=prepQuery.depth>0;coreQuery=prepQuery.core;for(j=0,len1=candidates.length;j0){scoredCandidates.push({candidate:candidate,score:score})}}}scoredCandidates.sort(sortCandidates);candidates=scoredCandidates.map(pluckCandidates);if(maxResults!=null){candidates=candidates.slice(0,maxResults)}return candidates}}).call(this)},{"./legacy":4,"./scorer":6,"path":7}],2:[function(require,module,exports){(function(){var PathSeparator,filter,legacy_scorer,matcher,prepQueryCache,scorer;scorer=require('./scorer');legacy_scorer=require('./legacy');filter=require('./filter');matcher=require('./matcher');PathSeparator=require('path').sep;prepQueryCache=null;module.exports={filter:function(candidates,query,options){if(!((query!=null?query.length:void 0)&&(candidates!=null?candidates.length:void 0))){return[]}return filter(candidates,query,options)},prepQuery:function(query){return scorer.prepQuery(query)},score:function(string,query,prepQuery,arg){var allowErrors,coreQuery,legacy,queryHasSlashes,ref,score;ref=arg!=null?arg:{},allowErrors=ref.allowErrors,legacy=ref.legacy;if(!((string!=null?string.length:void 0)&&(query!=null?query.length:void 0))){return 0}if(prepQuery==null){prepQuery=prepQueryCache&&prepQueryCache.query===query?prepQueryCache:(prepQueryCache=scorer.prepQuery(query))}if(!legacy){score=scorer.score(string,query,prepQuery,!!allowErrors)}else{queryHasSlashes=prepQuery.depth>0;coreQuery=prepQuery.core;score=legacy_scorer.score(string,coreQuery,queryHasSlashes);if(!queryHasSlashes){score=legacy_scorer.basenameScore(string,coreQuery,score)}}return score},match:function(string,query,prepQuery,arg){var allowErrors,baseMatches,i,matches,query_lw,ref,results,string_lw;allowErrors=(arg!=null?arg:{}).allowErrors;if(!string){return[]}if(!query){return[]}if(string===query){return(function(){results=[];for(var i=0,ref=string.length;0<=ref?iref;0<=ref?i++:i--){results.push(i)}return results}).apply(this)}if(prepQuery==null){prepQuery=prepQueryCache&&prepQueryCache.query===query?prepQueryCache:(prepQueryCache=scorer.prepQuery(query))}if(!(allowErrors||scorer.isMatch(string,prepQuery.core_lw,prepQuery.core_up))){return[]}string_lw=string.toLowerCase();query_lw=prepQuery.query_lw;matches=matcher.match(string,string_lw,prepQuery);if(matches.length===0){return matches}if(string.indexOf(PathSeparator)>-1){baseMatches=matcher.basenameMatch(string,string_lw,prepQuery);matches=matcher.mergeMatches(matches,baseMatches)}return matches}}}).call(this)},{"./filter":1,"./legacy":4,"./matcher":5,"./scorer":6,"path":7}],3:[function(require,module,exports){fuzzaldrinPlus=require('./fuzzaldrin')},{"./fuzzaldrin":2}],4:[function(require,module,exports){(function(){var PathSeparator,queryIsLastPathSegment;PathSeparator=require('path').sep;exports.basenameScore=function(string,query,score){var base,depth,index,lastCharacter,segmentCount,slashCount;index=string.length-1;while(string[index]===PathSeparator){index--}slashCount=0;lastCharacter=index;base=null;while(index>=0){if(string[index]===PathSeparator){slashCount++;if(base==null){base=string.substring(index+1,lastCharacter+1)}}else if(index===0){if(lastCharacterref;stringOffset<=ref?i++:i--){results.push(i)}return results}).apply(this)}queryLength=query.length;stringLength=string.length;indexInQuery=0;indexInString=0;matches=[];while(indexInQuery0){basePos=subject.lastIndexOf(PathSeparator,basePos-1);if(basePos===-1){return[]}}basePos++;end++;return exports.match(subject.slice(basePos,end),subject_lw.slice(basePos,end),prepQuery,basePos)};exports.mergeMatches=function(a,b){var ai,bj,i,j,m,n,out;m=a.length;n=b.length;if(n===0){return a.slice()}if(m===0){return b.slice()}i=-1;j=0;bj=b[j];out=[];while(++i0?csc_diag:scorer.scoreConsecutives(subject,subject_lw,query,query_lw,i,j,start);align=score_diag+scorer.scoreCharacter(i,j,start,acro_score,csc_score)}score_up=score_row[j];csc_diag=csc_row[j];if(score>score_up){move=LEFT}else{score=score_up;move=UP}if(align>score){score=align;move=DIAGONAL}else{csc_score=0}score_row[j]=score;csc_row[j]=csc_score;trace[++pos]=score>0?move:STOP}}i=m-1;j=n-1;pos=i*n+j;backtrack=true;matches=[];while(backtrack&&i>=0&&j>=0){switch(trace[pos]){case UP:i--;pos-=n;break;case LEFT:j--;pos--;break;case DIAGONAL:matches.push(i+offset);j--;i--;pos-=n+1;break;default:backtrack=false}}matches.reverse();return matches}}).call(this)},{"./scorer":6,"path":7}],6:[function(require,module,exports){(function(){var AcronymResult,PathSeparator,Query,basenameScore,coreChars,countDir,doScore,emptyAcronymResult,file_coeff,isMatch,isSeparator,isWordEnd,isWordStart,miss_coeff,opt_char_re,pos_bonus,scoreAcronyms,scoreCharacter,scoreConsecutives,scoreExact,scoreExactMatch,scorePattern,scorePosition,scoreSize,tau_depth,tau_size,truncatedUpperCase,wm;PathSeparator=require('path').sep;wm=150;pos_bonus=20;tau_depth=13;tau_size=85;file_coeff=1.2;miss_coeff=0.75;opt_char_re=/[ _\-:\/\\]/g;exports.coreChars=coreChars=function(query){return query.replace(opt_char_re,'')};exports.score=function(string,query,prepQuery,allowErrors){var score,string_lw;if(prepQuery==null){prepQuery=new Query(query)}if(allowErrors==null){allowErrors=false}if(!(allowErrors||isMatch(string,prepQuery.core_lw,prepQuery.core_up))){return 0}string_lw=string.toLowerCase();score=doScore(string,string_lw,prepQuery);return Math.ceil(basenameScore(string,string_lw,prepQuery,score))};Query=(function(){function Query(query){if(!(query!=null?query.length:void 0)){return null}this.query=query;this.query_lw=query.toLowerCase();this.core=coreChars(query);this.core_lw=this.core.toLowerCase();this.core_up=truncatedUpperCase(this.core);this.depth=countDir(query,query.length)}return Query})();exports.prepQuery=function(query){return new Query(query)};exports.isMatch=isMatch=function(subject,query_lw,query_up){var i,j,m,n,qj_lw,qj_up,si;m=subject.length;n=query_lw.length;if(!m||!n||n>m){return false}i=-1;j=-1;while(++j-1){return scoreExactMatch(subject,subject_lw,query,query_lw,pos,n,m)}score_row=new Array(n);csc_row=new Array(n);sz=scoreSize(n,m);miss_budget=Math.ceil(miss_coeff*n)+5;miss_left=miss_budget;j=-1;while(++j-1){i--}mm=subject_lw.lastIndexOf(query_lw[n-1],m);if(mm>i){m=mm+1}while(++iscore){score=score_up}csc_score=0;if(query_lw[j]===si_lw){start=isWordStart(i,subject,subject_lw);csc_score=csc_diag>0?csc_diag:scoreConsecutives(subject,subject_lw,query,query_lw,i,j,start);align=score_diag+scoreCharacter(i,j,start,acro_score,csc_score);if(align>score){score=align;miss_left=miss_budget}else{if(record_miss&&--miss_left<=0){return score_row[n-1]*sz}record_miss=false}}score_diag=score_up;csc_diag=csc_row[j];csc_row[j]=csc_score;score_row[j]=score}}return score*sz};exports.isWordStart=isWordStart=function(pos,subject,subject_lw){var curr_s,prev_s;if(pos===0){return true}curr_s=subject[pos];prev_s=subject[pos-1];return isSeparator(curr_s)||isSeparator(prev_s)||(curr_s!==subject_lw[pos]&&prev_s===subject_lw[pos-1])};exports.isWordEnd=isWordEnd=function(pos,subject,subject_lw,len){var curr_s,next_s;if(pos===len-1){return true}curr_s=subject[pos];next_s=subject[pos+1];return isSeparator(curr_s)||isSeparator(next_s)||(curr_s===subject_lw[pos]&&next_s!==subject_lw[pos+1])};isSeparator=function(c){return c===' '||c==='.'||c==='-'||c==='_'||c==='/'||c==='\\'};scorePosition=function(pos){var sc;if(poscsc_score?acro_score:csc_score)+10)}return posBonus+wm*csc_score};exports.scoreConsecutives=scoreConsecutives=function(subject,subject_lw,query,query_lw,i,j,start){var k,m,mi,n,nj,sameCase,startPos,sz;m=subject.length;n=query.length;mi=m-i;nj=n-j;k=mi-1){start=isWordStart(pos2,subject,subject_lw);if(start){pos=pos2}}}i=-1;sameCase=0;while(++i1&&n>1)){return emptyAcronymResult}count=0;pos=0;sameCase=0;i=-1;j=-1;while(++j0){basePos=subject.lastIndexOf(PathSeparator,basePos-1);if(basePos===-1){return fullPathScore}}basePos++;end++;basePathScore=doScore(subject.slice(basePos,end),subject_lw.slice(basePos,end),prepQuery);alpha=0.5*tau_depth/(tau_depth+countDir(subject,end+1));return alpha*basePathScore+(1-alpha)*fullPathScore*scoreSize(0,file_coeff*(end-basePos))};exports.countDir=countDir=function(path,end){var count,i;if(end<1){return 0}count=0;i=-1;while(++i=0;i--){var last=parts[i];if(last==='.'){parts.splice(i,1)}else if(last==='..'){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up--;up){parts.unshift('..')}}return parts}var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;var splitPath=function(filename){return splitPathRe.exec(filename).slice(1)};exports.resolve=function(){var resolvedPath='',resolvedAbsolute=false;for(var i=arguments.length-1;i>=-1&&!resolvedAbsolute;i--){var path=(i>=0)?arguments[i]:process.cwd();if(typeof path!=='string'){throw new TypeError('Arguments to path.resolve must be strings');}else if(!path){continue}resolvedPath=path+'/'+resolvedPath;resolvedAbsolute=path.charAt(0)==='/'}resolvedPath=normalizeArray(filter(resolvedPath.split('/'),function(p){return!!p}),!resolvedAbsolute).join('/');return((resolvedAbsolute?'/':'')+resolvedPath)||'.'};exports.normalize=function(path){var isAbsolute=exports.isAbsolute(path),trailingSlash=substr(path,-1)==='/';path=normalizeArray(filter(path.split('/'),function(p){return!!p}),!isAbsolute).join('/');if(!path&&!isAbsolute){path='.'}if(path&&trailingSlash){path+='/'}return(isAbsolute?'/':'')+path};exports.isAbsolute=function(path){return path.charAt(0)==='/'};exports.join=function(){var paths=Array.prototype.slice.call(arguments,0);return exports.normalize(filter(paths,function(p,index){if(typeof p!=='string'){throw new TypeError('Arguments to path.join must be strings');}return p}).join('/'))};exports.relative=function(from,to){from=exports.resolve(from).substr(1);to=exports.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=='')break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split('/'));var toParts=trim(to.split('/'));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i1){for(var i=1;i Date: Thu, 7 Jan 2016 10:39:02 +0100 Subject: [PATCH 146/280] Update ./doc/api --- doc/api/projects.md | 36 +++++++++++++++++++++++++++++++++--- lib/api/projects.rb | 9 +++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/doc/api/projects.md b/doc/api/projects.md index 0ca81ffd49e..96a3f08490c 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -76,7 +76,14 @@ Parameters: "updated_at": "2013-09-30T13: 46: 02Z" }, "archived": false, - "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png" + "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png", + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 0, + "build_allow_git_fetch": true, + "build_coverage_regex": null, + "build_timeout": 3600, + "runners_token": "4f9e77be0eed5ef29548fccda3b371" }, { "id": 6, @@ -129,7 +136,14 @@ Parameters: } }, "archived": false, - "avatar_url": null + "avatar_url": null, + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 0, + "build_allow_git_fetch": true, + "build_coverage_regex": null, + "build_timeout": 3600, + "runners_token": "b8547b1dc37721d05889db52fa2f02" } ] ``` @@ -244,7 +258,14 @@ Parameters: } }, "archived": false, - "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png" + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 0, + "build_allow_git_fetch": true, + "build_coverage_regex": null, + "build_timeout": 3600, + "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b" } ``` @@ -409,6 +430,9 @@ Parameters: - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) +- `build_allow_git_fetch` (optional) +- `build_timeout` (optional) +- `build_coverage_regex` (optional) ### Create project for user @@ -431,6 +455,9 @@ Parameters: - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) +- `build_allow_git_fetch` (optional) +- `build_timeout` (optional) +- `build_coverage_regex` (optional) ### Edit project @@ -454,6 +481,9 @@ Parameters: - `snippets_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) +- `build_allow_git_fetch` (optional) +- `build_timeout` (optional) +- `build_coverage_regex` (optional) On success, method returns 200 with the updated project. If parameters are invalid, 400 is returned. diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 7dd6b133f9b..31b081266a8 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -98,6 +98,9 @@ module API # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - 0 by default # import_url (optional) + # build_allow_git_fetch (optional) + # build_timeout (optional) + # build_coverage_regex (optional) # Example Request # POST /projects post do @@ -146,6 +149,9 @@ module API # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) # import_url (optional) + # build_allow_git_fetch (optional) + # build_timeout (optional) + # build_coverage_regex (optional) # Example Request # POST /projects/user/:user_id post "user/:user_id" do @@ -207,6 +213,9 @@ module API # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - visibility level of a project + # build_allow_git_fetch (optional) + # build_timeout (optional) + # build_coverage_regex (optional) # Example Request # PUT /projects/:id put ':id' do From 7ed3a5a240e4997b24d11b96e27126dfaa575abe Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 7 Jan 2016 11:47:06 +0100 Subject: [PATCH 147/280] Revert "Store SQL/view timings in milliseconds" This reverts commit 7549102bb727daecc51da84af39956b32fc41537. Apparently I was wrong about ActiveSupport::Notifications::Event#duration returning the duration in seconds, instead it returns it in milliseconds already. --- lib/gitlab/metrics/subscribers/action_view.rb | 8 ++------ lib/gitlab/metrics/subscribers/active_record.rb | 6 +----- spec/lib/gitlab/metrics/subscribers/action_view_spec.rb | 4 ++-- spec/lib/gitlab/metrics/subscribers/active_record_spec.rb | 2 +- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb index 84d9e383625..7c0105d543a 100644 --- a/lib/gitlab/metrics/subscribers/action_view.rb +++ b/lib/gitlab/metrics/subscribers/action_view.rb @@ -19,7 +19,7 @@ module Gitlab values = values_for(event) tags = tags_for(event) - current_transaction.increment(:view_duration, duration(event)) + current_transaction.increment(:view_duration, event.duration) current_transaction.add_metric(SERIES, values, tags) end @@ -28,7 +28,7 @@ module Gitlab end def values_for(event) - { duration: duration(event) } + { duration: event.duration } end def tags_for(event) @@ -48,10 +48,6 @@ module Gitlab def current_transaction Transaction.current end - - def duration(event) - event.duration * 1000.0 - end end end end diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb index 6fa73e7a3be..8008b3bc895 100644 --- a/lib/gitlab/metrics/subscribers/active_record.rb +++ b/lib/gitlab/metrics/subscribers/active_record.rb @@ -8,7 +8,7 @@ module Gitlab def sql(event) return unless current_transaction - current_transaction.increment(:sql_duration, duration(event)) + current_transaction.increment(:sql_duration, event.duration) end private @@ -16,10 +16,6 @@ module Gitlab def current_transaction Transaction.current end - - def duration(event) - event.duration * 1000.0 - end end end end diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb index 0a4cc5e929b..05e4fbbeb51 100644 --- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb @@ -21,7 +21,7 @@ describe Gitlab::Metrics::Subscribers::ActionView do describe '#render_template' do it 'tracks rendering of a template' do - values = { duration: 2100 } + values = { duration: 2.1 } tags = { view: 'app/views/x.html.haml', file: 'app/views/x.html.haml', @@ -29,7 +29,7 @@ describe Gitlab::Metrics::Subscribers::ActionView do } expect(transaction).to receive(:increment). - with(:view_duration, 2100) + with(:view_duration, 2.1) expect(transaction).to receive(:add_metric). with(described_class::SERIES, values, tags) diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index ca86142a2f4..7bc070a4d09 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -26,7 +26,7 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do and_return(transaction) expect(transaction).to receive(:increment). - with(:sql_duration, 200) + with(:sql_duration, 0.2) subscriber.sql(event) end From b38eabdaf69c4da19f39c26b7626e9ce4b51158b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 7 Jan 2016 12:10:35 +0100 Subject: [PATCH 148/280] Fix changelog --- CHANGELOG | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index ed839b8fc82..de8d2c43424 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,7 +8,6 @@ v 8.4.0 (unreleased) - Add API support for looking up a user by username (Stan Hu) - Add project permissions to all project API endpoints (Stan Hu) - Link to milestone in "Milestone changed" system note - - Expose Git's version in the admin area - Only allow group/project members to mention `@all` - Expose Git's version in the admin area (Trey Davis) - Add "Frequently used" category to emoji picker From 539b41929bddf0e82d986f9e823208dd92707a21 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 7 Jan 2016 12:26:05 +0100 Subject: [PATCH 149/280] Milestone reference is a Markdown link --- app/models/milestone.rb | 6 +++++- spec/fixtures/markdown.md.erb | 1 - spec/lib/banzai/filter/milestone_reference_filter_spec.rb | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index eaa2db2e247..550d14d4c39 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -71,8 +71,12 @@ class Milestone < ActiveRecord::Base end def to_reference(from_project = nil) + escaped_title = self.title.gsub("]", "\\]") + h = Gitlab::Application.routes.url_helpers - h.namespace_project_milestone_url(self.project.namespace, self.project, self) + url = h.namespace_project_milestone_url(self.project.namespace, self.project, self) + + "[#{escaped_title}](#{url})" end def reference_link_text(from_project = nil) diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 302b750aee5..0620096d689 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -219,7 +219,6 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Milestone: <%= milestone.to_reference %> - Milestone in another project: <%= xmilestone.to_reference(project) %> - Ignored in code: `<%= milestone.to_reference %>` -- Ignored in links: [Link to <%= milestone.to_reference %>](#milestone-link) - Link to milestone by URL: [Milestone](<%= urls.namespace_project_milestone_url(milestone.project.namespace, milestone.project, milestone) %>) ### Task Lists diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index 86b71210100..ebf3d7489b5 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -18,7 +18,9 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do end context 'internal reference' do - let(:reference) { milestone.to_reference } + # Convert the Markdown link to only the URL, since these tests aren't run through the regular Markdown pipeline. + # Milestone reference behavior in the full Markdown pipeline is tested elsewhere. + let(:reference) { milestone.to_reference.gsub(/\[([^\]]+)\]\(([^)]+)\)/, '\2') } it 'links to a valid reference' do doc = reference_filter("See #{reference}") From 364b07cff0183956ea11962b94c70448767351d3 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 7 Jan 2016 12:44:15 +0100 Subject: [PATCH 150/280] Removed UUIDs from metrics transactions While useful for finding out what methods/views belong to a transaction this might result in too much data being stored in InfluxDB. --- lib/gitlab/metrics/transaction.rb | 4 +--- spec/lib/gitlab/metrics/transaction_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb index 68b86de0655..306656d30fe 100644 --- a/lib/gitlab/metrics/transaction.rb +++ b/lib/gitlab/metrics/transaction.rb @@ -4,7 +4,7 @@ module Gitlab class Transaction THREAD_KEY = :_gitlab_metrics_transaction - attr_reader :uuid, :tags + attr_reader :tags def self.current Thread.current[THREAD_KEY] @@ -12,7 +12,6 @@ module Gitlab def initialize @metrics = [] - @uuid = SecureRandom.uuid @started_at = nil @finished_at = nil @@ -38,7 +37,6 @@ module Gitlab end def add_metric(series, values, tags = {}) - tags = tags.merge(transaction_id: @uuid) prefix = sidekiq? ? 'sidekiq_' : 'rails_' @metrics << Metric.new("#{prefix}#{series}", values, tags) diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index b9b94947afa..0c98b8f0127 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -30,9 +30,9 @@ describe Gitlab::Metrics::Transaction do end describe '#add_metric' do - it 'adds a metric tagged with the transaction UUID' do + it 'adds a metric to the transaction' do expect(Gitlab::Metrics::Metric).to receive(:new). - with('rails_foo', { number: 10 }, { transaction_id: transaction.uuid }) + with('rails_foo', { number: 10 }, {}) transaction.add_metric('foo', number: 10) end From 41b8a238ce4bd7f091d46fb9b89b7456fde17ddf Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 7 Jan 2016 12:56:18 +0100 Subject: [PATCH 151/280] Merge branch 'master' of github.com:gitlabhq/gitlabhq --- CHANGELOG | 1 + Gemfile | 2 +- Gemfile.lock | 2 +- app/assets/javascripts/application.js.coffee | 1 + app/assets/javascripts/dispatcher.js.coffee | 4 +- .../javascripts/project_find_file.js.coffee | 125 ++++++++++++++++++ .../javascripts/shortcuts_find_file.js.coffee | 19 +++ app/assets/javascripts/shortcuts_tree.coffee | 4 + app/assets/stylesheets/pages/tree.scss | 8 ++ .../projects/find_file_controller.rb | 26 ++++ app/controllers/projects/refs_controller.rb | 2 + app/models/repository.rb | 5 + app/views/help/_shortcuts.html.haml | 26 ++++ app/views/layouts/nav/_project.html.haml | 3 +- app/views/projects/_find_file_link.html.haml | 3 + app/views/projects/find_file/show.html.haml | 27 ++++ app/views/projects/tree/show.html.haml | 8 +- config/routes.rb | 18 +++ doc/workflow/shortcuts.png | Bin 78736 -> 48782 bytes features/project/find_file.feature | 42 ++++++ features/steps/project/project_find_file.rb | 73 ++++++++++ features/steps/shared/paths.rb | 4 + .../projects/find_file_controller_spec.rb | 66 +++++++++ spec/routing/project_routing_spec.rb | 12 ++ .../assets/javascripts/fuzzaldrin-plus.min.js | 1 + 25 files changed, 473 insertions(+), 9 deletions(-) create mode 100644 app/assets/javascripts/project_find_file.js.coffee create mode 100644 app/assets/javascripts/shortcuts_find_file.js.coffee create mode 100644 app/assets/javascripts/shortcuts_tree.coffee create mode 100644 app/controllers/projects/find_file_controller.rb create mode 100644 app/views/projects/_find_file_link.html.haml create mode 100644 app/views/projects/find_file/show.html.haml create mode 100644 features/project/find_file.feature create mode 100644 features/steps/project/project_find_file.rb create mode 100644 spec/controllers/projects/find_file_controller_spec.rb create mode 100644 vendor/assets/javascripts/fuzzaldrin-plus.min.js diff --git a/CHANGELOG b/CHANGELOG index e7f1d2b67da..22fb91baaf0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -106,6 +106,7 @@ v 8.3.0 - Fix online editor should not remove newlines at the end of the file - Expose Git's version in the admin area - Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye) + - Add file finder feature in tree view v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) diff --git a/Gemfile b/Gemfile index 6145745b6f3..6b0bc241494 100644 --- a/Gemfile +++ b/Gemfile @@ -49,7 +49,7 @@ gem "browser", '~> 1.0.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.2.20' +gem "gitlab_git", '~> 7.2.22' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes diff --git a/Gemfile.lock b/Gemfile.lock index 2b42f325503..a1168ed3b7a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -887,7 +887,7 @@ DEPENDENCIES github-markup (~> 1.3.1) gitlab-flowdock-git-hook (~> 1.0.1) gitlab_emoji (~> 0.2.0) - gitlab_git (~> 7.2.20) + gitlab_git (~> 7.2.22) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.1.0) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index b9b095e004a..c095e5ae2b1 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -40,6 +40,7 @@ #= require shortcuts_network #= require jquery.nicescroll.min #= require_tree . +#= require fuzzaldrin-plus.min window.slugify = (text) -> text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 69e061ce6e9..58d6b9d4060 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -87,7 +87,9 @@ class Dispatcher new GroupAvatar() when 'projects:tree:show' new TreeView() - shortcut_handler = new ShortcutsNavigation() + shortcut_handler = new ShortcutsTree() + when 'projects:find_file:show' + shortcut_handler = true when 'projects:blob:show' new LineHighlighter() shortcut_handler = new ShortcutsNavigation() diff --git a/app/assets/javascripts/project_find_file.js.coffee b/app/assets/javascripts/project_find_file.js.coffee new file mode 100644 index 00000000000..0dd32352c34 --- /dev/null +++ b/app/assets/javascripts/project_find_file.js.coffee @@ -0,0 +1,125 @@ +class @ProjectFindFile + constructor: (@element, @options)-> + @filePaths = {} + @inputElement = @element.find(".file-finder-input") + + # init event + @initEvent() + + # focus text input box + @inputElement.focus() + + # load file list + @load(@options.url) + + # init event + initEvent: -> + @inputElement.off "keyup" + @inputElement.on "keyup", (event) => + target = $(event.target) + value = target.val() + oldValue = target.data("oldValue") ? "" + + if value != oldValue + target.data("oldValue", value) + @findFile() + @element.find("tr.tree-item").eq(0).addClass("selected").focus() + + @element.find(".tree-content-holder .tree-table").on "click", (event) -> + if (event.target.nodeName != "A") + path = @element.find(".tree-item-file-name a", this).attr("href") + location.href = path if path + + # find file + findFile: -> + searchText = @inputElement.val() + result = if searchText.length > 0 then fuzzaldrinPlus.filter(@filePaths, searchText) else @filePaths + @renderList result, searchText + + # files pathes load + load: (url) -> + $.ajax + url: url + method: "get" + dataType: "json" + success: (data) => + @element.find(".loading").hide() + @filePaths = data + @findFile() + @element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus() + + # render result + renderList: (filePaths, searchText) -> + @element.find(".tree-table > tbody").empty() + + for filePath, i in filePaths + break if i == 20 + + if searchText + matches = fuzzaldrinPlus.match(filePath, searchText) + + blobItemUrl = "#{@options.blobUrlTemplate}/#{filePath}" + + html = @makeHtml filePath, matches, blobItemUrl + @element.find(".tree-table > tbody").append(html) + + # highlight text(awefwbwgtc -> awefwbwgtc ) + highlighter = (element, text, matches) -> + lastIndex = 0 + highlightText = "" + matchedChars = [] + + for matchIndex in matches + unmatched = text.substring(lastIndex, matchIndex) + + if unmatched + element.append(matchedChars.join("").bold()) if matchedChars.length + matchedChars = [] + element.append(document.createTextNode(unmatched)) + + matchedChars.push(text[matchIndex]) + lastIndex = matchIndex + 1 + + element.append(matchedChars.join("").bold()) if matchedChars.length + element.append(document.createTextNode(text.substring(lastIndex))) + + # make tbody row html + makeHtml: (filePath, matches, blobItemUrl) -> + $tr = $("") + if matches + $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl)) + else + $tr.find("a").attr("href", blobItemUrl).text(filePath) + + return $tr + + selectRow: (type) -> + rows = @element.find(".files-slider tr.tree-item") + selectedRow = @element.find(".files-slider tr.tree-item.selected") + + if rows && rows.length > 0 + if selectedRow && selectedRow.length > 0 + if type == "UP" + next = selectedRow.prev() + else if type == "DOWN" + next = selectedRow.next() + + if next.length > 0 + selectedRow.removeClass "selected" + selectedRow = next + else + selectedRow = rows.eq(0) + selectedRow.addClass("selected").focus() + + selectRowUp: => + @selectRow "UP" + + selectRowDown: => + @selectRow "DOWN" + + goToTree: => + location.href = @options.treeUrl + + goToBlob: => + path = @element.find(".tree-item.selected .tree-item-file-name a").attr("href") + location.href = path if path diff --git a/app/assets/javascripts/shortcuts_find_file.js.coffee b/app/assets/javascripts/shortcuts_find_file.js.coffee new file mode 100644 index 00000000000..311e80bae19 --- /dev/null +++ b/app/assets/javascripts/shortcuts_find_file.js.coffee @@ -0,0 +1,19 @@ +#= require shortcuts_navigation + +class @ShortcutsFindFile extends ShortcutsNavigation + constructor: (@projectFindFile) -> + super() + _oldStopCallback = Mousetrap.stopCallback + # override to fire shortcuts action when focus in textbox + Mousetrap.stopCallback = (event, element, combo) => + if element == @projectFindFile.inputElement[0] and (combo == 'up' or combo == 'down' or combo == 'esc' or combo == 'enter') + # when press up/down key in textbox, cusor prevent to move to home/end + event.preventDefault() + return false + + return _oldStopCallback(event, element, combo) + + Mousetrap.bind('up', @projectFindFile.selectRowUp) + Mousetrap.bind('down', @projectFindFile.selectRowDown) + Mousetrap.bind('esc', @projectFindFile.goToTree) + Mousetrap.bind('enter', @projectFindFile.goToBlob) diff --git a/app/assets/javascripts/shortcuts_tree.coffee b/app/assets/javascripts/shortcuts_tree.coffee new file mode 100644 index 00000000000..ba0839c9fc0 --- /dev/null +++ b/app/assets/javascripts/shortcuts_tree.coffee @@ -0,0 +1,4 @@ +class @ShortcutsTree extends ShortcutsNavigation + constructor: -> + super() + Mousetrap.bind('t', -> ShortcutsTree.findAndFollowLink('.shortcuts-find-file')) diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index d4ab6967ccd..97505edeabf 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,5 +1,13 @@ .tree-holder { + .file-finder { + width: 50%; + .file-finder-input { + width: 95%; + display: inline-block; + } + } + .tree-table { margin-bottom: 0; diff --git a/app/controllers/projects/find_file_controller.rb b/app/controllers/projects/find_file_controller.rb new file mode 100644 index 00000000000..54a0c447aee --- /dev/null +++ b/app/controllers/projects/find_file_controller.rb @@ -0,0 +1,26 @@ +# Controller for viewing a repository's file structure +class Projects::FindFileController < Projects::ApplicationController + include ExtractsPath + include ActionView::Helpers::SanitizeHelper + include TreeHelper + + before_action :require_non_empty_project + before_action :assign_ref_vars + before_action :authorize_download_code! + + def show + return render_404 unless @repository.commit(@ref) + + respond_to do |format| + format.html + end + end + + def list + file_paths = @repo.ls_files(@ref) + + respond_to do |format| + format.json { render json: file_paths } + end + end +end diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index c4e18c17077..a8f091819ca 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -20,6 +20,8 @@ class Projects::RefsController < Projects::ApplicationController namespace_project_network_path(@project.namespace, @project, @id, @options) when "graphs" namespace_project_graph_path(@project.namespace, @project, @id) + when "find_file" + namespace_project_find_file_path(@project.namespace, @project, @id) when "graphs_commits" commits_namespace_project_graph_path(@project.namespace, @project, @id) else diff --git a/app/models/repository.rb b/app/models/repository.rb index 6ecd2d2f27e..9deb08d93b8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -681,6 +681,11 @@ class Repository end end + def ls_files(ref) + actual_ref = ref || root_ref + raw_repository.ls_files(actual_ref) + end + private def cache diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index e8e331dd109..9ee6f07b26b 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -40,6 +40,32 @@ %td.shortcut .key enter %td Open Selection + %tr + %td.shortcut + .key t + %td Go to finding file + %tbody + %tr + %th + %th Finding Project File + %tr + %td.shortcut + .key + %i.fa.fa-arrow-up + %td Move selection up + %tr + %td.shortcut + .key + %i.fa.fa-arrow-down + %td Move selection down + %tr + %td.shortcut + .key enter + %td Open Selection + %tr + %td.shortcut + .key esc + %td Go back .col-lg-4 %table.shortcut-mappings diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index d3eaf0f3209..270ccfd387f 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -25,7 +25,7 @@ %span Activity - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do = icon('files-o fw') %span @@ -117,4 +117,3 @@ %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/_find_file_link.html.haml b/app/views/projects/_find_file_link.html.haml new file mode 100644 index 00000000000..08e2fc48be7 --- /dev/null +++ b/app/views/projects/_find_file_link.html.haml @@ -0,0 +1,3 @@ += link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do + = icon('search') + %span Find File diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml new file mode 100644 index 00000000000..2930209fb56 --- /dev/null +++ b/app/views/projects/find_file/show.html.haml @@ -0,0 +1,27 @@ +- page_title "Find File", @ref +- header_title project_title(@project, "Files", project_files_path(@project)) + +.file-finder-holder.tree-holder.clearfix + .gray-content-block.top-block + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'find_file', path: @path + %ul.breadcrumb.repo-breadcrumb + %li + = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do + = @project.path + %li.file-finder + %input#file_find.form-control.file-finder-input{type: "text", placeholder: 'Find by path'} + + %div.tree-content-holder + .table-holder + %table.table.files-slider{class: "table_#{@hex_path} tree-table table-striped" } + %tbody + = spinner nil, true + +:coffeescript + projectFindFile = new ProjectFindFile($(".file-finder-holder"), { + url: "#{escape_javascript(namespace_project_files_path(@project.namespace, @project, @ref, @options.merge(format: :json)))}" + treeUrl: "#{escape_javascript(namespace_project_tree_path(@project.namespace, @project, @ref))}" + blobUrlTemplate: "#{escape_javascript(namespace_project_blob_path(@project.namespace, @project, @id || @commit.id))}" + }) + new ShortcutsFindFile(projectFindFile) diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index ec14bd7f65a..c57570afa09 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -3,12 +3,12 @@ = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") - = render 'projects/last_push' -- if can? current_user, :download_code, @project - .tree-download-holder - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true +.pull-right + = render 'projects/find_file_link' + - if can? current_user, :download_code, @project + = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true #tree-holder.tree-holder.clearfix .gray-content-block.top-block diff --git a/config/routes.rb b/config/routes.rb index 3e7d9f78710..5b69d06eb76 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -440,6 +440,24 @@ Rails.application.routes.draw do ) end + scope do + get( + '/find_file/*id', + to: 'find_file#show', + constraints: { id: /.+/, format: /html/ }, + as: :find_file + ) + end + + scope do + get( + '/files/*id', + to: 'find_file#list', + constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ }, + as: :files + ) + end + scope do post( '/create_dir/*id', diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png index 68756ed1f98dd474ada0b8da8d98840a25c3a123..e5914aa8e67556518de69841e4c06472a29d4d3e 100644 GIT binary patch literal 48782 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

YlL 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 Dc9TY$ diff --git a/features/project/find_file.feature b/features/project/find_file.feature new file mode 100644 index 00000000000..ae8fa245923 --- /dev/null +++ b/features/project/find_file.feature @@ -0,0 +1,42 @@ +@dashboard +Feature: Project Find File + Background: + Given I sign in as a user + And I own a project + And I visit my project's files page + + @javascript + Scenario: Navigate to find file by shortcut + Given I press "t" + Then I should see "find file" page + + Scenario: Navigate to find file + Given I click Find File button + Then I should see "find file" page + + @javascript + Scenario: I search file + Given I visit project find file page + And I fill in file find with "change" + Then I should not see ".gitignore" in files + And I should not see ".gitmodules" in files + And I should see "CHANGELOG" in files + And I should not see "VERSION" in files + + @javascript + Scenario: I search file that not exist + Given I visit project find file page + And I fill in file find with "asdfghjklqwertyuizxcvbnm" + Then I should not see ".gitignore" in files + And I should not see ".gitmodules" in files + And I should not see "CHANGELOG" in files + And I should not see "VERSION" in files + + @javascript + Scenario: I search file that partially matches + Given I visit project find file page + And I fill in file find with "git" + Then I should see ".gitignore" in files + And I should see ".gitmodules" in files + And I should not see "CHANGELOG" in files + And I should not see "VERSION" in files diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb new file mode 100644 index 00000000000..8c1d09d6cc6 --- /dev/null +++ b/features/steps/project/project_find_file.rb @@ -0,0 +1,73 @@ +class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedProject + include SharedProjectTab + + step 'I press "t"' do + find('body').native.send_key('t') + end + + step 'I click Find File button' do + click_link 'Find File' + end + + step 'I should see "find file" page' do + ensure_active_main_tab('Files') + expect(page).to have_selector('.file-finder-holder', count: 1) + end + + step 'I fill in Find by path with "git"' do + ensure_active_main_tab('Files') + expect(page).to have_selector('.file-finder-holder', count: 1) + end + + step 'I fill in file find with "git"' do + find_file "git" + end + + step 'I fill in file find with "change"' do + find_file "change" + end + + step 'I fill in file find with "asdfghjklqwertyuizxcvbnm"' do + find_file "asdfghjklqwertyuizxcvbnm" + end + + step 'I should see "VERSION" in files' do + expect(page).to have_content("VERSION") + end + + step 'I should not see "VERSION" in files' do + expect(page).not_to have_content("VERSION") + end + + step 'I should see "CHANGELOG" in files' do + expect(page).to have_content("CHANGELOG") + end + + step 'I should not see "CHANGELOG" in files' do + expect(page).not_to have_content("CHANGELOG") + end + + step 'I should see ".gitmodules" in files' do + expect(page).to have_content(".gitmodules") + end + + step 'I should not see ".gitmodules" in files' do + expect(page).not_to have_content(".gitmodules") + end + + step 'I should see ".gitignore" in files' do + expect(page).to have_content(".gitignore") + end + + step 'I should not see ".gitignore" in files' do + expect(page).not_to have_content(".gitignore") + end + + + def find_file(text) + fill_in 'file_find', with: text + end +end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index b33bd332655..4264c9c6f1a 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -259,6 +259,10 @@ module SharedPaths visit namespace_project_deploy_keys_path(@project.namespace, @project) end + step 'I visit project find file page' do + visit namespace_project_find_file_path(@project.namespace, @project, root_ref) + end + # ---------------------------------------- # "Shop" Project # ---------------------------------------- diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb new file mode 100644 index 00000000000..038dfeb8466 --- /dev/null +++ b/spec/controllers/projects/find_file_controller_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Projects::FindFileController do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + sign_in(user) + + project.team << [user, :master] + controller.instance_variable_set(:@project, project) + end + + describe "GET #show" do + # Make sure any errors accessing the tree in our views bubble up to this spec + render_views + + before do + get(:show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id) + end + + context "valid branch" do + let(:id) { 'master' } + it { is_expected.to respond_with(:success) } + end + + context "invalid branch" do + let(:id) { 'invalid-branch' } + it { is_expected.to respond_with(:not_found) } + end + end + + describe "GET #list" do + def go(format: 'json') + get :list, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id, + format: format + end + + context "valid branch" do + let(:id) { 'master' } + it 'returns an array of file path list' do + go + + json = JSON.parse(response.body) + is_expected.to respond_with(:success) + expect(json).not_to eq(nil) + expect(json.length).to be >= 0 + end + end + + context "invalid branch" do + let(:id) { 'invalid-branch' } + + it 'responds with status 404' do + go + is_expected.to respond_with(:not_found) + end + end + end +end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 82f62a8709c..2a70c190337 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -434,6 +434,18 @@ describe Projects::TreeController, 'routing' do end end +# project_find_file GET /:namespace_id/:project_id/find_file/*id(.:format) projects/find_file#show {:id=>/.+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?/html/} +# project_files GET /:namespace_id/:project_id/files/*id(.:format) projects/find_file#list {:id=>/(?:[^.]|\.(?!json$))+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?/json/} +describe Projects::FindFileController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/find_file/master')).to route_to('projects/find_file#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') + end + + it 'to #list' do + expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') + end +end + describe Projects::BlobController, 'routing' do it 'to #edit' do expect(get('/gitlab/gitlabhq/edit/master/app/models/project.rb')).to( diff --git a/vendor/assets/javascripts/fuzzaldrin-plus.min.js b/vendor/assets/javascripts/fuzzaldrin-plus.min.js new file mode 100644 index 00000000000..3f25c2d8373 --- /dev/null +++ b/vendor/assets/javascripts/fuzzaldrin-plus.min.js @@ -0,0 +1 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0?maxInners:candidates.length;bAllowErrors=!!allowErrors;bKey=key!=null;prepQuery=scorer.prepQuery(query);if(!legacy){for(i=0,len=candidates.length;i0){scoredCandidates.push({candidate:candidate,score:score});if(!--spotLeft){break}}}}else{queryHasSlashes=prepQuery.depth>0;coreQuery=prepQuery.core;for(j=0,len1=candidates.length;j0){scoredCandidates.push({candidate:candidate,score:score})}}}scoredCandidates.sort(sortCandidates);candidates=scoredCandidates.map(pluckCandidates);if(maxResults!=null){candidates=candidates.slice(0,maxResults)}return candidates}}).call(this)},{"./legacy":4,"./scorer":6,"path":7}],2:[function(require,module,exports){(function(){var PathSeparator,filter,legacy_scorer,matcher,prepQueryCache,scorer;scorer=require('./scorer');legacy_scorer=require('./legacy');filter=require('./filter');matcher=require('./matcher');PathSeparator=require('path').sep;prepQueryCache=null;module.exports={filter:function(candidates,query,options){if(!((query!=null?query.length:void 0)&&(candidates!=null?candidates.length:void 0))){return[]}return filter(candidates,query,options)},prepQuery:function(query){return scorer.prepQuery(query)},score:function(string,query,prepQuery,arg){var allowErrors,coreQuery,legacy,queryHasSlashes,ref,score;ref=arg!=null?arg:{},allowErrors=ref.allowErrors,legacy=ref.legacy;if(!((string!=null?string.length:void 0)&&(query!=null?query.length:void 0))){return 0}if(prepQuery==null){prepQuery=prepQueryCache&&prepQueryCache.query===query?prepQueryCache:(prepQueryCache=scorer.prepQuery(query))}if(!legacy){score=scorer.score(string,query,prepQuery,!!allowErrors)}else{queryHasSlashes=prepQuery.depth>0;coreQuery=prepQuery.core;score=legacy_scorer.score(string,coreQuery,queryHasSlashes);if(!queryHasSlashes){score=legacy_scorer.basenameScore(string,coreQuery,score)}}return score},match:function(string,query,prepQuery,arg){var allowErrors,baseMatches,i,matches,query_lw,ref,results,string_lw;allowErrors=(arg!=null?arg:{}).allowErrors;if(!string){return[]}if(!query){return[]}if(string===query){return(function(){results=[];for(var i=0,ref=string.length;0<=ref?iref;0<=ref?i++:i--){results.push(i)}return results}).apply(this)}if(prepQuery==null){prepQuery=prepQueryCache&&prepQueryCache.query===query?prepQueryCache:(prepQueryCache=scorer.prepQuery(query))}if(!(allowErrors||scorer.isMatch(string,prepQuery.core_lw,prepQuery.core_up))){return[]}string_lw=string.toLowerCase();query_lw=prepQuery.query_lw;matches=matcher.match(string,string_lw,prepQuery);if(matches.length===0){return matches}if(string.indexOf(PathSeparator)>-1){baseMatches=matcher.basenameMatch(string,string_lw,prepQuery);matches=matcher.mergeMatches(matches,baseMatches)}return matches}}}).call(this)},{"./filter":1,"./legacy":4,"./matcher":5,"./scorer":6,"path":7}],3:[function(require,module,exports){fuzzaldrinPlus=require('./fuzzaldrin')},{"./fuzzaldrin":2}],4:[function(require,module,exports){(function(){var PathSeparator,queryIsLastPathSegment;PathSeparator=require('path').sep;exports.basenameScore=function(string,query,score){var base,depth,index,lastCharacter,segmentCount,slashCount;index=string.length-1;while(string[index]===PathSeparator){index--}slashCount=0;lastCharacter=index;base=null;while(index>=0){if(string[index]===PathSeparator){slashCount++;if(base==null){base=string.substring(index+1,lastCharacter+1)}}else if(index===0){if(lastCharacterref;stringOffset<=ref?i++:i--){results.push(i)}return results}).apply(this)}queryLength=query.length;stringLength=string.length;indexInQuery=0;indexInString=0;matches=[];while(indexInQuery0){basePos=subject.lastIndexOf(PathSeparator,basePos-1);if(basePos===-1){return[]}}basePos++;end++;return exports.match(subject.slice(basePos,end),subject_lw.slice(basePos,end),prepQuery,basePos)};exports.mergeMatches=function(a,b){var ai,bj,i,j,m,n,out;m=a.length;n=b.length;if(n===0){return a.slice()}if(m===0){return b.slice()}i=-1;j=0;bj=b[j];out=[];while(++i0?csc_diag:scorer.scoreConsecutives(subject,subject_lw,query,query_lw,i,j,start);align=score_diag+scorer.scoreCharacter(i,j,start,acro_score,csc_score)}score_up=score_row[j];csc_diag=csc_row[j];if(score>score_up){move=LEFT}else{score=score_up;move=UP}if(align>score){score=align;move=DIAGONAL}else{csc_score=0}score_row[j]=score;csc_row[j]=csc_score;trace[++pos]=score>0?move:STOP}}i=m-1;j=n-1;pos=i*n+j;backtrack=true;matches=[];while(backtrack&&i>=0&&j>=0){switch(trace[pos]){case UP:i--;pos-=n;break;case LEFT:j--;pos--;break;case DIAGONAL:matches.push(i+offset);j--;i--;pos-=n+1;break;default:backtrack=false}}matches.reverse();return matches}}).call(this)},{"./scorer":6,"path":7}],6:[function(require,module,exports){(function(){var AcronymResult,PathSeparator,Query,basenameScore,coreChars,countDir,doScore,emptyAcronymResult,file_coeff,isMatch,isSeparator,isWordEnd,isWordStart,miss_coeff,opt_char_re,pos_bonus,scoreAcronyms,scoreCharacter,scoreConsecutives,scoreExact,scoreExactMatch,scorePattern,scorePosition,scoreSize,tau_depth,tau_size,truncatedUpperCase,wm;PathSeparator=require('path').sep;wm=150;pos_bonus=20;tau_depth=13;tau_size=85;file_coeff=1.2;miss_coeff=0.75;opt_char_re=/[ _\-:\/\\]/g;exports.coreChars=coreChars=function(query){return query.replace(opt_char_re,'')};exports.score=function(string,query,prepQuery,allowErrors){var score,string_lw;if(prepQuery==null){prepQuery=new Query(query)}if(allowErrors==null){allowErrors=false}if(!(allowErrors||isMatch(string,prepQuery.core_lw,prepQuery.core_up))){return 0}string_lw=string.toLowerCase();score=doScore(string,string_lw,prepQuery);return Math.ceil(basenameScore(string,string_lw,prepQuery,score))};Query=(function(){function Query(query){if(!(query!=null?query.length:void 0)){return null}this.query=query;this.query_lw=query.toLowerCase();this.core=coreChars(query);this.core_lw=this.core.toLowerCase();this.core_up=truncatedUpperCase(this.core);this.depth=countDir(query,query.length)}return Query})();exports.prepQuery=function(query){return new Query(query)};exports.isMatch=isMatch=function(subject,query_lw,query_up){var i,j,m,n,qj_lw,qj_up,si;m=subject.length;n=query_lw.length;if(!m||!n||n>m){return false}i=-1;j=-1;while(++j-1){return scoreExactMatch(subject,subject_lw,query,query_lw,pos,n,m)}score_row=new Array(n);csc_row=new Array(n);sz=scoreSize(n,m);miss_budget=Math.ceil(miss_coeff*n)+5;miss_left=miss_budget;j=-1;while(++j-1){i--}mm=subject_lw.lastIndexOf(query_lw[n-1],m);if(mm>i){m=mm+1}while(++iscore){score=score_up}csc_score=0;if(query_lw[j]===si_lw){start=isWordStart(i,subject,subject_lw);csc_score=csc_diag>0?csc_diag:scoreConsecutives(subject,subject_lw,query,query_lw,i,j,start);align=score_diag+scoreCharacter(i,j,start,acro_score,csc_score);if(align>score){score=align;miss_left=miss_budget}else{if(record_miss&&--miss_left<=0){return score_row[n-1]*sz}record_miss=false}}score_diag=score_up;csc_diag=csc_row[j];csc_row[j]=csc_score;score_row[j]=score}}return score*sz};exports.isWordStart=isWordStart=function(pos,subject,subject_lw){var curr_s,prev_s;if(pos===0){return true}curr_s=subject[pos];prev_s=subject[pos-1];return isSeparator(curr_s)||isSeparator(prev_s)||(curr_s!==subject_lw[pos]&&prev_s===subject_lw[pos-1])};exports.isWordEnd=isWordEnd=function(pos,subject,subject_lw,len){var curr_s,next_s;if(pos===len-1){return true}curr_s=subject[pos];next_s=subject[pos+1];return isSeparator(curr_s)||isSeparator(next_s)||(curr_s===subject_lw[pos]&&next_s!==subject_lw[pos+1])};isSeparator=function(c){return c===' '||c==='.'||c==='-'||c==='_'||c==='/'||c==='\\'};scorePosition=function(pos){var sc;if(poscsc_score?acro_score:csc_score)+10)}return posBonus+wm*csc_score};exports.scoreConsecutives=scoreConsecutives=function(subject,subject_lw,query,query_lw,i,j,start){var k,m,mi,n,nj,sameCase,startPos,sz;m=subject.length;n=query.length;mi=m-i;nj=n-j;k=mi-1){start=isWordStart(pos2,subject,subject_lw);if(start){pos=pos2}}}i=-1;sameCase=0;while(++i1&&n>1)){return emptyAcronymResult}count=0;pos=0;sameCase=0;i=-1;j=-1;while(++j0){basePos=subject.lastIndexOf(PathSeparator,basePos-1);if(basePos===-1){return fullPathScore}}basePos++;end++;basePathScore=doScore(subject.slice(basePos,end),subject_lw.slice(basePos,end),prepQuery);alpha=0.5*tau_depth/(tau_depth+countDir(subject,end+1));return alpha*basePathScore+(1-alpha)*fullPathScore*scoreSize(0,file_coeff*(end-basePos))};exports.countDir=countDir=function(path,end){var count,i;if(end<1){return 0}count=0;i=-1;while(++i=0;i--){var last=parts[i];if(last==='.'){parts.splice(i,1)}else if(last==='..'){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up--;up){parts.unshift('..')}}return parts}var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;var splitPath=function(filename){return splitPathRe.exec(filename).slice(1)};exports.resolve=function(){var resolvedPath='',resolvedAbsolute=false;for(var i=arguments.length-1;i>=-1&&!resolvedAbsolute;i--){var path=(i>=0)?arguments[i]:process.cwd();if(typeof path!=='string'){throw new TypeError('Arguments to path.resolve must be strings');}else if(!path){continue}resolvedPath=path+'/'+resolvedPath;resolvedAbsolute=path.charAt(0)==='/'}resolvedPath=normalizeArray(filter(resolvedPath.split('/'),function(p){return!!p}),!resolvedAbsolute).join('/');return((resolvedAbsolute?'/':'')+resolvedPath)||'.'};exports.normalize=function(path){var isAbsolute=exports.isAbsolute(path),trailingSlash=substr(path,-1)==='/';path=normalizeArray(filter(path.split('/'),function(p){return!!p}),!isAbsolute).join('/');if(!path&&!isAbsolute){path='.'}if(path&&trailingSlash){path+='/'}return(isAbsolute?'/':'')+path};exports.isAbsolute=function(path){return path.charAt(0)==='/'};exports.join=function(){var paths=Array.prototype.slice.call(arguments,0);return exports.normalize(filter(paths,function(p,index){if(typeof p!=='string'){throw new TypeError('Arguments to path.join must be strings');}return p}).join('/'))};exports.relative=function(from,to){from=exports.resolve(from).substr(1);to=exports.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=='')break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split('/'));var toParts=trim(to.split('/'));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i1){for(var i=1;i Date: Thu, 7 Jan 2016 13:04:09 +0100 Subject: [PATCH 152/280] Credit koreamic for creating Find File --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 22fb91baaf0..32308becba0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -21,6 +21,7 @@ v 8.4.0 (unreleased) - Update version check images to use SVG - Validate README format before displaying - Enable Microsoft Azure OAuth2 support (Janis Meybohm) + - Add file finder feature in tree view (koreamic) v 8.3.3 (unreleased) - Get "Merge when build succeeds" to work when commits were pushed to MR target branch while builds were running @@ -106,7 +107,6 @@ v 8.3.0 - Fix online editor should not remove newlines at the end of the file - Expose Git's version in the admin area - Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye) - - Add file finder feature in tree view v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) From 7b10cb6f0f5758c17dd950587982ff400d7aa971 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 7 Jan 2016 13:05:00 +0100 Subject: [PATCH 153/280] Store request methods/URIs as values Since filtering by these values is very rare (they're mostly just displayed as-is) we don't need to waste any index space by saving them as tags. By storing them as values we also greatly reduce the number of series in InfluxDB. --- lib/gitlab/metrics/rack_middleware.rb | 4 ++-- lib/gitlab/metrics/transaction.rb | 6 +++++- spec/lib/gitlab/metrics/rack_middleware_spec.rb | 6 +++--- spec/lib/gitlab/metrics/transaction_spec.rb | 11 +++++++++++ 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb index 5c0587c4c51..e7a2f26d48b 100644 --- a/lib/gitlab/metrics/rack_middleware.rb +++ b/lib/gitlab/metrics/rack_middleware.rb @@ -32,8 +32,8 @@ module Gitlab def transaction_from_env(env) trans = Transaction.new - trans.add_tag(:request_method, env['REQUEST_METHOD']) - trans.add_tag(:request_uri, env['REQUEST_URI']) + trans.set(:request_uri, env['REQUEST_URI']) + trans.set(:request_method, env['REQUEST_METHOD']) trans end diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb index 306656d30fe..73131cc6ef2 100644 --- a/lib/gitlab/metrics/transaction.rb +++ b/lib/gitlab/metrics/transaction.rb @@ -4,7 +4,7 @@ module Gitlab class Transaction THREAD_KEY = :_gitlab_metrics_transaction - attr_reader :tags + attr_reader :tags, :values def self.current Thread.current[THREAD_KEY] @@ -46,6 +46,10 @@ module Gitlab @values[name] += value end + def set(name, value) + @values[name] = value + end + def add_tag(key, value) @tags[key] = value end diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb index a143fe4cfcd..4e6dfc73df2 100644 --- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb @@ -40,9 +40,9 @@ describe Gitlab::Metrics::RackMiddleware do expect(transaction).to be_an_instance_of(Gitlab::Metrics::Transaction) end - it 'tags the transaction with the request method and URI' do - expect(transaction.tags[:request_method]).to eq('GET') - expect(transaction.tags[:request_uri]).to eq('/foo') + it 'stores the request method and URI in the transaction as values' do + expect(transaction.values[:request_method]).to eq('GET') + expect(transaction.values[:request_uri]).to eq('/foo') end end diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index 0c98b8f0127..3a27f897735 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -50,6 +50,17 @@ describe Gitlab::Metrics::Transaction do end end + describe '#set' do + it 'sets a value' do + transaction.set(:number, 10) + + expect(transaction).to receive(:add_metric). + with('transactions', { duration: 0.0, number: 10 }, {}) + + transaction.track_self + end + end + describe '#add_tag' do it 'adds a tag' do transaction.add_tag(:foo, 'bar') From 1886d727f738895bb552151d59d4024f405522e2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 7 Jan 2016 13:37:14 +0100 Subject: [PATCH 154/280] Add API project upload endpoint --- .../javascripts/dropzone_input.js.coffee | 14 ++-------- app/services/projects/download_service.rb | 7 ++++- app/services/projects/upload_service.rb | 7 ++++- doc/api/projects.md | 28 +++++++++++++++++++ lib/api/projects.rb | 12 +++++++- lib/gitlab/email/receiver.rb | 7 ++--- lib/gitlab/fogbugz_import/importer.rb | 4 +-- spec/requests/api/projects_spec.rb | 14 ++++++++++ 8 files changed, 70 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index 30a35a04339..c714c0fa939 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -66,7 +66,7 @@ class @DropzoneInput success: (header, response) -> child = $(dropzone[0]).children("textarea") - $(child).val $(child).val() + formatLink(response.link) + "\n" + $(child).val $(child).val() + response.link.markdown + "\n" return error: (temp, errorMessage) -> @@ -99,11 +99,6 @@ class @DropzoneInput child = $(dropzone[0]).children("textarea") - formatLink = (link) -> - text = "[#{link.alt}](#{link.url})" - text = "!#{text}" if link.is_image - text - handlePaste = (event) -> pasteEvent = event.originalEvent if pasteEvent.clipboardData and pasteEvent.clipboardData.items @@ -162,7 +157,7 @@ class @DropzoneInput closeAlertMessage() success: (e, textStatus, response) -> - insertToTextArea(filename, formatLink(response.responseJSON.link)) + insertToTextArea(filename, response.responseJSON.link.markdown) error: (response) -> showError(response.responseJSON.message) @@ -202,8 +197,3 @@ class @DropzoneInput e.preventDefault() $(@).closest('.gfm-form').find('.div-dropzone').click() return - - formatLink: (link) -> - text = "[#{link.alt}](#{link.url})" - text = "!#{text}" if link.is_image - text diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb index 99f22293d0d..b846a59ed94 100644 --- a/app/services/projects/download_service.rb +++ b/app/services/projects/download_service.rb @@ -18,10 +18,15 @@ module Projects filename = uploader.image? ? uploader.file.basename : uploader.file.filename + escaped_filename = filename.gsub("]", "\\]") + markdown = "[#{escaped_filename}](#{uploader.secure_url})" + markdown.prepend("!") if uploader.image? + { 'alt' => filename, 'url' => uploader.secure_url, - 'is_image' => uploader.image? + 'is_image' => uploader.image?, + 'markdown' => markdown } end diff --git a/app/services/projects/upload_service.rb b/app/services/projects/upload_service.rb index 279550d6f4a..36ccf1cda12 100644 --- a/app/services/projects/upload_service.rb +++ b/app/services/projects/upload_service.rb @@ -12,10 +12,15 @@ module Projects filename = uploader.image? ? uploader.file.basename : uploader.file.filename + escaped_filename = filename.gsub("]", "\\]") + markdown = "[#{escaped_filename}](#{uploader.secure_url})" + markdown.prepend("!") if uploader.image? + { alt: filename, url: uploader.secure_url, - is_image: uploader.image? + is_image: uploader.image?, + markdown: markdown } end diff --git a/doc/api/projects.md b/doc/api/projects.md index 0ca81ffd49e..37d74216c1b 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -482,6 +482,34 @@ Parameters: - `id` (required) - The ID of a project +## Uploads + +### Upload a file + +Uploads a file to the specified project to be used in an issue or merge request description, or a comment. + +``` +POST /projects/:id/uploads +``` + +Parameters: + +- `id` (required) - The ID of the project +- `file` (required) - The file to be uploaded + +```json +{ + "alt": "dk", + "url": "/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png", + "is_image": true, + "markdown": "![dk](/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png)" +} +``` + +**Note**: The returned `url` is relative to the project path. +In Markdown contexts, the link is automatically expanded when the format in `markdown` is used. + + ## Team members ### List project team members diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 0781236cf6d..8b1390e3289 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -269,7 +269,7 @@ module API # Remove a forked_from relationship # # Parameters: - # id: (required) - The ID of the project being marked as a fork + # id: (required) - The ID of the project being marked as a fork # Example Request: # DELETE /projects/:id/fork delete ":id/fork" do @@ -278,6 +278,16 @@ module API user_project.forked_project_link.destroy end end + + # Upload a file + # + # Parameters: + # id: (required) - The ID of the project + # file: (required) - The file to be uploaded + post ":id/uploads" do + ::Projects::UploadService.new(user_project, params[:file]).execute + end + # search for projects current_user has access to # # Parameters: diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index 2b252b32887..2ca21af5bc8 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -74,7 +74,7 @@ module Gitlab def sent_notification return nil unless reply_key - + SentNotification.for(reply_key) end @@ -82,10 +82,7 @@ module Gitlab attachments = Email::AttachmentUploader.new(message).execute(sent_notification.project) attachments.each do |link| - text = "[#{link[:alt]}](#{link[:url]})" - text.prepend("!") if link[:is_image] - - reply << "\n\n#{text}" + reply << "\n\n#{link[:markdown]}" end reply diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb index 403ebeec474..d5f755f90e5 100644 --- a/lib/gitlab/fogbugz_import/importer.rb +++ b/lib/gitlab/fogbugz_import/importer.rb @@ -232,9 +232,7 @@ module Gitlab return nil if res.nil? - text = "[#{res['alt']}](#{res['url']})" - text = "!#{text}" if res['is_image'] - text + text = res['markdown'] end def build_attachment_url(rel_url) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index ab2530859ea..6f4c336b66c 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -353,6 +353,20 @@ describe API::API, api: true do end end + describe "POST /projects/:id/uploads" do + before { project } + + it "uploads the file and returns its info" do + post api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") + + expect(response.status).to be(201) + expect(json_response['alt']).to eq("dk") + expect(json_response['url']).to start_with("/uploads/") + expect(json_response['url']).to end_with("/dk.png") + expect(json_response['is_image']).to eq(true) + end + end + describe 'GET /projects/:id' do before { project } before { project_member } From 706d7eb0b7a1d54604a11e1202e44069c6acccee Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 7 Jan 2016 13:37:59 +0100 Subject: [PATCH 155/280] Satisfy Rubocp --- lib/gitlab/fogbugz_import/importer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb index d5f755f90e5..0e6bee732f1 100644 --- a/lib/gitlab/fogbugz_import/importer.rb +++ b/lib/gitlab/fogbugz_import/importer.rb @@ -232,7 +232,7 @@ module Gitlab return nil if res.nil? - text = res['markdown'] + res['markdown'] end def build_attachment_url(rel_url) From 892d5dbb87614156bb9ad7ce8aa817fdb6b9c79b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 7 Jan 2016 14:10:23 +0100 Subject: [PATCH 156/280] Update Gemfile.lock --- Gemfile.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a1168ed3b7a..3c7cb6cf439 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -443,6 +443,10 @@ GEM omniauth (1.2.2) hashie (>= 1.2, < 4) rack (~> 1.0) + omniauth-azure-oauth2 (0.0.6) + jwt (~> 1.0) + omniauth (~> 1.0) + omniauth-oauth2 (~> 1.1) omniauth-bitbucket (0.0.2) multi_json (~> 1.7) omniauth (~> 1.1) @@ -488,10 +492,6 @@ GEM activesupport nokogiri (>= 1.4.4) omniauth (~> 1.0) - omniauth-azure-oauth2 (0.0.6) - jwt (~> 1.0) - omniauth (~> 1.0) - omniauth-oauth2 (~> 1.1) opennebula (4.14.2) json nokogiri @@ -920,6 +920,7 @@ DEPENDENCIES oauth2 (~> 1.0.0) octokit (~> 3.7.0) omniauth (~> 1.2.2) + omniauth-azure-oauth2 omniauth-bitbucket (~> 0.0.2) omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 3.0.0) @@ -931,7 +932,6 @@ DEPENDENCIES omniauth-shibboleth (~> 1.2.0) omniauth-twitter (~> 1.2.0) omniauth_crowd - omniauth-azure-oauth2 org-ruby (~> 0.9.12) paranoia (~> 2.0) pg (~> 0.18.2) From 7a240397afc56ab366b6c3504761fbf531b78ec1 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 6 Jan 2016 17:27:47 +0100 Subject: [PATCH 157/280] Added an index on milestones.title Certain pages (e.g. the group wide issues page) filter miletones by their title. Without an index this will result in a sequence scan on a large dataset increasing the total loading time of a page. --- db/migrate/20160106162223_add_index_milestones_title.rb | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 db/migrate/20160106162223_add_index_milestones_title.rb diff --git a/db/migrate/20160106162223_add_index_milestones_title.rb b/db/migrate/20160106162223_add_index_milestones_title.rb new file mode 100644 index 00000000000..767885e2aac --- /dev/null +++ b/db/migrate/20160106162223_add_index_milestones_title.rb @@ -0,0 +1,5 @@ +class AddIndexMilestonesTitle < ActiveRecord::Migration + def change + add_index :milestones, :title + end +end From 9dacc3bc568c6c8cfc4a1bc1af23eb96f9eae9b0 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 6 Jan 2016 17:29:44 +0100 Subject: [PATCH 158/280] Sort by ID when sorting using "Recently created" Sorting by "id" has the same effect as sorting by created_at while performing far better and without the need of an extra index (in case one wanted to speed up sorting by "created_at"). Sorting by "Recently updated" still uses the physical "updated_at" column as there's no way to use the "id" column for this instead. --- app/controllers/application_controller.rb | 2 +- app/helpers/sorting_helper.rb | 4 ++-- app/models/concerns/sortable.rb | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d9a37a4d45f..81cb1367e2c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -286,7 +286,7 @@ class ApplicationController < ActionController::Base end def set_filters_params - params[:sort] ||= 'created_desc' + params[:sort] ||= 'id_desc' params[:scope] = 'all' if params[:scope].blank? params[:state] = 'opened' if params[:state].blank? diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index bb12d43f397..9a7c26d1ccf 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -63,11 +63,11 @@ module SortingHelper end def sort_value_oldest_created - 'created_asc' + 'id_asc' end def sort_value_recently_created - 'created_desc' + 'id_desc' end def sort_value_milestone_soon diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 7391a77383c..8b47b9e0abd 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -11,6 +11,7 @@ module Sortable default_scope { order_id_desc } scope :order_id_desc, -> { reorder(id: :desc) } + scope :order_id_asc, -> { reorder(id: :asc) } scope :order_created_desc, -> { reorder(created_at: :desc) } scope :order_created_asc, -> { reorder(created_at: :asc) } scope :order_updated_desc, -> { reorder(updated_at: :desc) } @@ -28,6 +29,8 @@ module Sortable when 'updated_desc' then order_updated_desc when 'created_asc' then order_created_asc when 'created_desc' then order_created_desc + when 'id_desc' then order_id_desc + when 'id_asc' then order_id_asc else all end From 0d0049c0584be2d358048c3cc545406b64bf3826 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 6 Jan 2016 17:32:25 +0100 Subject: [PATCH 159/280] Don't pluck IDs when getting issues/MRs per group This replaces plucking of IDs with a sub-query, saving the overhead of loading the data in Ruby and then mapping the rows to an Array of IDs. This also scales much better when dealing with a large amount of IDs that would be involved. --- app/models/issue.rb | 2 +- app/models/merge_request.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/issue.rb b/app/models/issue.rb index 80ecd15077f..21cc2105161 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -33,7 +33,7 @@ class Issue < ActiveRecord::Base belongs_to :project validates :project, presence: true - scope :of_group, ->(group) { where(project_id: group.project_ids) } + scope :of_group, ->(group) { where(project_id: group.projects.select(:id)) } scope :cared, ->(user) { where(assignee_id: user) } scope :open_for, ->(user) { opened.assigned_to(user) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 30d0c2b5961..3c41bebc807 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -131,7 +131,7 @@ class MergeRequest < ActiveRecord::Base validate :validate_branches validate :validate_fork - scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.project_ids) } + scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.projects.select(:id)) } scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } From fc443ea7bcdd7255745cd1811c8bc95546ae3b12 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 6 Jan 2016 17:33:43 +0100 Subject: [PATCH 160/280] Drop projects order in IssuableFinder When grabbing the projects to filter issues by we don't care about the order they're returned in. By removing the ORDER BY the resulting query can be quite a bit faster. --- app/finders/issuable_finder.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 3d5e8b6fbe7..4d56b48e3f8 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -79,9 +79,9 @@ class IssuableFinder if project? @projects = project elsif current_user && params[:authorized_only].presence && !current_user_related? - @projects = current_user.authorized_projects + @projects = current_user.authorized_projects.reorder(nil) else - @projects = ProjectsFinder.new.execute(current_user) + @projects = ProjectsFinder.new.execute(current_user).reorder(nil) end end From 5dc708e11c912acebf739851df4e9e2e11188d02 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 7 Jan 2016 11:19:10 +0100 Subject: [PATCH 161/280] Updated Gemfile.lock due to Bundler re-ordering Bundler keeps re-ordering this particular Gem every time something is executed using "bundle exec". --- Gemfile.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a1168ed3b7a..3c7cb6cf439 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -443,6 +443,10 @@ GEM omniauth (1.2.2) hashie (>= 1.2, < 4) rack (~> 1.0) + omniauth-azure-oauth2 (0.0.6) + jwt (~> 1.0) + omniauth (~> 1.0) + omniauth-oauth2 (~> 1.1) omniauth-bitbucket (0.0.2) multi_json (~> 1.7) omniauth (~> 1.1) @@ -488,10 +492,6 @@ GEM activesupport nokogiri (>= 1.4.4) omniauth (~> 1.0) - omniauth-azure-oauth2 (0.0.6) - jwt (~> 1.0) - omniauth (~> 1.0) - omniauth-oauth2 (~> 1.1) opennebula (4.14.2) json nokogiri @@ -920,6 +920,7 @@ DEPENDENCIES oauth2 (~> 1.0.0) octokit (~> 3.7.0) omniauth (~> 1.2.2) + omniauth-azure-oauth2 omniauth-bitbucket (~> 0.0.2) omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 3.0.0) @@ -931,7 +932,6 @@ DEPENDENCIES omniauth-shibboleth (~> 1.2.0) omniauth-twitter (~> 1.2.0) omniauth_crowd - omniauth-azure-oauth2 org-ruby (~> 0.9.12) paranoia (~> 2.0) pg (~> 0.18.2) From 9b0c360bd5f8dd0538ecb5ffcb39da9a618b0231 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 7 Jan 2016 12:38:21 +0100 Subject: [PATCH 162/280] Fixed issue sorting specs for ID changes These specs assumed data was still sorted by timestamp, instead of by ID. --- spec/features/issues_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index a2fb3e4c75d..e844e681ebf 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -127,15 +127,15 @@ describe 'Issues', feature: true do it 'sorts by newest' do visit namespace_project_issues_path(project.namespace, project, sort: sort_value_recently_created) - expect(first_issue).to include('foo') - expect(last_issue).to include('baz') + expect(first_issue).to include('baz') + expect(last_issue).to include('foo') end it 'sorts by oldest' do visit namespace_project_issues_path(project.namespace, project, sort: sort_value_oldest_created) - expect(first_issue).to include('baz') - expect(last_issue).to include('foo') + expect(first_issue).to include('foo') + expect(last_issue).to include('baz') end it 'sorts by most recently updated' do @@ -190,8 +190,8 @@ describe 'Issues', feature: true do sort: sort_value_oldest_created, assignee_id: user2.id) - expect(first_issue).to include('bar') - expect(last_issue).to include('foo') + expect(first_issue).to include('foo') + expect(last_issue).to include('bar') expect(page).not_to have_content 'baz' end end From 9a250ad6d85678d7f9197a55c69cb724c81008e0 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 6 Jan 2016 11:27:23 +0100 Subject: [PATCH 163/280] Filter commits by search parameter Signed-off-by: Dmitriy Zaporozhets --- app/controllers/projects/commits_controller.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 04a88990bf4..66e27524a23 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -8,10 +8,16 @@ class Projects::CommitsController < Projects::ApplicationController before_action :authorize_download_code! def show - @repo = @project.repository @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i + search = params[:search] + + @commits = + if search.present? + @repository.find_commits_by_message(search).compact + else + @repository.commits(@ref, @path, @limit, @offset) + end - @commits = @repo.commits(@ref, @path, @limit, @offset) @note_counts = project.notes.where(commit_id: @commits.map(&:id)). group(:commit_id).count From 5e93f912c3d77e8e22ff563d1701e89b36ddf733 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 6 Jan 2016 11:40:03 +0100 Subject: [PATCH 164/280] Add search field to commits page Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/commits.scss | 10 +++------- app/views/projects/commits/show.html.haml | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 879bd287470..800df95cff3 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -28,10 +28,6 @@ } } -.commits-feed-holder { - float: right; -} - li.commit { list-style: none; @@ -126,14 +122,14 @@ li.commit { .divergence-graph { padding: 12px 12px 0 0; float: right; - + .graph-side { position: relative; width: 80px; height: 22px; padding: 5px 0 13px; float: left; - + .bar { position: absolute; height: 4px; @@ -149,7 +145,7 @@ li.commit { left: 0; border-radius: 0 3px 3px 0; } - + .count { padding-top: 6px; padding-bottom: 0px; diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 2dd99cc8215..0d652a832f5 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -10,15 +10,21 @@ .tree-ref-holder = render 'shared/ref_switcher', destination: 'commits' - .commits-feed-holder.hidden-xs.hidden-sm + .pull-right.hidden-xs.hidden-sm - if create_mr_button?(@repository.root_ref, @ref) - = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do - = icon('plus') - Create Merge Request + .pull-left.prepend-left-10 + = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do + = icon('plus') + Create Merge Request + + .pull-left.prepend-left-10 + = form_tag(namespace_project_commits_path(@project.namespace, @project, @ref), method: :get, class: 'pull-left commits-search-form') do + = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', class: 'form-control search-text-input', spellcheck: false } - if current_user && current_user.private_token - = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'prepend-left-10 btn' do - = icon("rss") + .pull-left.prepend-left-10 + = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do + = icon("rss") %ul.breadcrumb.repo-breadcrumb From d1a40e06cc34a83f196345635e5b5ed16685ab62 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 6 Jan 2016 12:58:15 +0100 Subject: [PATCH 165/280] Add ajax filtering for commits list Also handle commits list with Pager class to prevent code duplication Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/commits.js.coffee | 67 +++++++++-------------- app/views/projects/commits/show.html.haml | 10 ++-- 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee index c183e78e513..2d33e0f6ebe 100644 --- a/app/assets/javascripts/commits.js.coffee +++ b/app/assets/javascripts/commits.js.coffee @@ -1,15 +1,5 @@ class @CommitsList - @data = - ref: null - limit: 0 - offset: 0 - @disable = false - - @showProgress: -> - $('.loading').show() - - @hideProgress: -> - $('.loading').hide() + @timer = null @init: (ref, limit) -> $("body").on "click", ".day-commits-table li.commit", (event) -> @@ -18,38 +8,35 @@ class @CommitsList e.stopPropagation() return false - @data.ref = ref - @data.limit = limit - @data.offset = limit + Pager.init limit, true - this.initLoadMore() - this.showProgress() + @content = $("#commits-list") + @searchField = $("#commits-search") + @initSearch() + + @initSearch: -> + @timer = null + @searchField.keyup => + clearTimeout(@timer) + @timer = setTimeout(@filterResults, 500) + + @filterResults: => + form = $(".commits-search-form") + search = @searchField.val() + commitsUrl = form.attr("action") + '?' + form.serialize() + @setOpacitiy("0.5") - @getOld: -> - this.showProgress() $.ajax type: "GET" - url: location.href - data: @data - complete: this.hideProgress - success: (data) -> - CommitsList.append(data.count, data.html) + url: form.attr("action") + data: form.serialize() + complete: => + @setOpacitiy("1.0") + success: (data) => + @content.html(data.html) + # Change url so if user reload a page - search results are saved + history.replaceState {page: commitsUrl}, document.title, commitsUrl dataType: "json" - @append: (count, html) -> - $("#commits-list").append(html) - if count > 0 - @data.offset += count - else - @disable = true - - @initLoadMore: -> - $(document).unbind('scroll') - $(document).endlessScroll - bottomPixels: 400 - fireDelay: 1000 - fireOnce: true - ceaseFire: => - @disable - callback: => - this.getOld() + @setOpacitiy: (opacity) -> + @content.css("opacity", opacity) diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 0d652a832f5..5e59afcd783 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -19,7 +19,7 @@ .pull-left.prepend-left-10 = form_tag(namespace_project_commits_path(@project.namespace, @project, @ref), method: :get, class: 'pull-left commits-search-form') do - = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', class: 'form-control search-text-input', spellcheck: false } + = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input', spellcheck: false } - if current_user && current_user.private_token .pull-left.prepend-left-10 @@ -31,11 +31,9 @@ = commits_breadcrumbs %div{id: dom_id(@project)} - #commits-list= render "commits", project: @project + #commits-list.content_list= render "commits", project: @project .clear = spinner -- if @commits.count == @limit - :javascript - CommitsList.init("#{@ref}", #{@limit}); - +:javascript + CommitsList.init("#{@ref}", #{@limit}); From 0456855911448bca5996428283e0c3e9b2d27c3b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 6 Jan 2016 17:12:25 +0100 Subject: [PATCH 166/280] Add CHANGELOG item and tests Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + features/project/commits/commits.feature | 5 +++++ features/steps/project/commits/commits.rb | 9 +++++++++ 3 files changed, 15 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 94a776a35eb..879d057fff0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -23,6 +23,7 @@ v 8.4.0 (unreleased) - Validate README format before displaying - Enable Microsoft Azure OAuth2 support (Janis Meybohm) - Add file finder feature in tree view (koreamic) + - Ajax filter by message for commits page v 8.3.3 (unreleased) - Get "Merge when build succeeds" to work when commits were pushed to MR target branch while builds were running diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index 5bb2d0e976b..01c10721312 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -55,3 +55,8 @@ Feature: Project Commits Scenario: I browse a commit with an image Given I visit a commit with an image that changed Then The diff links to both the previous and current image + + @javascript + Scenario: I filter commits by message + When I search "submodules" commits + Then I should see only "submodules" commits diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index a3141fe3be1..daf6cdaaac8 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -124,4 +124,13 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps expect(page).to have_content "build: pending" expect(page).to have_content "1 build" end + + step 'I search "submodules" commits' do + fill_in 'commits-search', with: 'submodules' + end + + step 'I should see only "submodules" commits' do + expect(page).to have_content "More submodules" + expect(page).not_to have_content "Change some files" + end end From b2b4e9a7eb925bec2a09e44940cab31199f1de5c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 7 Jan 2016 12:38:35 +0100 Subject: [PATCH 167/280] Prevent loading first 20 commits twice Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/commits.js.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee index 2d33e0f6ebe..bddcf83d4dd 100644 --- a/app/assets/javascripts/commits.js.coffee +++ b/app/assets/javascripts/commits.js.coffee @@ -8,7 +8,7 @@ class @CommitsList e.stopPropagation() return false - Pager.init limit, true + Pager.init limit, false @content = $("#commits-list") @searchField = $("#commits-search") @@ -24,14 +24,14 @@ class @CommitsList form = $(".commits-search-form") search = @searchField.val() commitsUrl = form.attr("action") + '?' + form.serialize() - @setOpacitiy("0.5") + @setOpacitiy(0.5) $.ajax type: "GET" url: form.attr("action") data: form.serialize() complete: => - @setOpacitiy("1.0") + @setOpacitiy(1.0) success: (data) => @content.html(data.html) # Change url so if user reload a page - search results are saved From 4443a5f3c76015f7bf083248b6910d01839cfc88 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 7 Jan 2016 13:00:47 +0100 Subject: [PATCH 168/280] Add support for ref and path to commits filtering Signed-off-by: Dmitriy Zaporozhets --- .../projects/commits_controller.rb | 2 +- app/models/repository.rb | 21 +++++++++++-------- app/views/projects/commits/show.html.haml | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 66e27524a23..bf5b54c8cb7 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -13,7 +13,7 @@ class Projects::CommitsController < Projects::ApplicationController @commits = if search.present? - @repository.find_commits_by_message(search).compact + @repository.find_commits_by_message(search, @ref, @path, @limit, @offset).compact else @repository.commits(@ref, @path, @limit, @offset) end diff --git a/app/models/repository.rb b/app/models/repository.rb index 9deb08d93b8..d9ff71c01ed 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -92,9 +92,12 @@ class Repository commits end - def find_commits_by_message(query) + def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0) + ref ||= root_ref + # Limited to 1000 commits for now, could be parameterized? - args = %W(#{Gitlab.config.git.bin_path} log --pretty=%H --max-count 1000 --grep=#{query}) + args = %W(#{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} --max-count #{limit} --grep=#{query}) + args = args.concat(%W(-- #{path})) if path.present? git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp) commits = git_log_results.map { |c| commit(c) } @@ -175,7 +178,7 @@ class Repository def size cache.fetch(:size) { raw_repository.size } end - + def diverging_commit_counts(branch) root_ref_hash = raw_repository.rev_parse_target(root_ref).oid cache.fetch(:"diverging_commit_counts_#{branch.name}") do @@ -183,7 +186,7 @@ class Repository # than SHA-1 hashes number_commits_behind = commits_between(branch.target, root_ref_hash).size number_commits_ahead = commits_between(root_ref_hash, branch.target).size - + { behind: number_commits_behind, ahead: number_commits_ahead } end end @@ -192,7 +195,7 @@ class Repository %i(size branch_names tag_names commit_count readme version contribution_guide changelog license) end - + def branch_cache_keys branches.map do |branch| :"diverging_commit_counts_#{branch.name}" @@ -205,7 +208,7 @@ class Repository send(key) end end - + branches.each do |branch| unless cache.exist?(:"diverging_commit_counts_#{branch.name}") send(:diverging_commit_counts, branch) @@ -227,10 +230,10 @@ class Repository cache_keys.each do |key| cache.expire(key) end - + expire_branch_cache end - + def expire_branch_cache branches.each do |branch| cache.expire(:"diverging_commit_counts_#{branch.name}") @@ -242,7 +245,7 @@ class Repository cache.expire(key) send(key) end - + branches.each do |branch| cache.expire(:"diverging_commit_counts_#{branch.name}") diverging_commit_counts(branch) diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 5e59afcd783..8f6625fef8a 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -18,7 +18,7 @@ Create Merge Request .pull-left.prepend-left-10 - = form_tag(namespace_project_commits_path(@project.namespace, @project, @ref), method: :get, class: 'pull-left commits-search-form') do + = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'pull-left commits-search-form') do = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input', spellcheck: false } - if current_user && current_user.private_token From 44dc9aa69e19d90564fdb279f9e8d51b5caf2cae Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 7 Jan 2016 13:04:25 +0100 Subject: [PATCH 169/280] Fix typo in js method and some repeating css Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/commits.js.coffee | 6 +++--- app/assets/stylesheets/framework/blocks.scss | 9 +++++++++ app/views/projects/commits/show.html.haml | 8 ++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee index bddcf83d4dd..c337038e446 100644 --- a/app/assets/javascripts/commits.js.coffee +++ b/app/assets/javascripts/commits.js.coffee @@ -24,19 +24,19 @@ class @CommitsList form = $(".commits-search-form") search = @searchField.val() commitsUrl = form.attr("action") + '?' + form.serialize() - @setOpacitiy(0.5) + @setOpacity(0.5) $.ajax type: "GET" url: form.attr("action") data: form.serialize() complete: => - @setOpacitiy(1.0) + @setOpacity(1.0) success: (data) => @content.html(data.html) # Change url so if user reload a page - search results are saved history.replaceState {page: commitsUrl}, document.title, commitsUrl dataType: "json" - @setOpacitiy: (opacity) -> + @setOpacity: (opacity) -> @content.css("opacity", opacity) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 206d39cc9b3..fa0e70847f3 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -72,6 +72,15 @@ > p:last-child { margin-bottom: 0; } + + .block-controls { + float: right; + + .control { + float: left; + margin-left: 10px; + } + } } .cover-block { diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 8f6625fef8a..034057da42e 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -10,19 +10,19 @@ .tree-ref-holder = render 'shared/ref_switcher', destination: 'commits' - .pull-right.hidden-xs.hidden-sm + .block-controls.hidden-xs.hidden-sm - if create_mr_button?(@repository.root_ref, @ref) - .pull-left.prepend-left-10 + .control = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do = icon('plus') Create Merge Request - .pull-left.prepend-left-10 + .control = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'pull-left commits-search-form') do = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input', spellcheck: false } - if current_user && current_user.private_token - .pull-left.prepend-left-10 + .control = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do = icon("rss") From c7fc84f5162f58d1f643beaf88196a3333c75980 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 7 Jan 2016 15:15:21 +0100 Subject: [PATCH 170/280] Use fadeTo instead of css Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/commits.js.coffee | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee index c337038e446..ffd3627b1b0 100644 --- a/app/assets/javascripts/commits.js.coffee +++ b/app/assets/javascripts/commits.js.coffee @@ -24,19 +24,16 @@ class @CommitsList form = $(".commits-search-form") search = @searchField.val() commitsUrl = form.attr("action") + '?' + form.serialize() - @setOpacity(0.5) + @content.fadeTo('fast', 0.5) $.ajax type: "GET" url: form.attr("action") data: form.serialize() complete: => - @setOpacity(1.0) + @content.fadeTo('fast', 1.0) success: (data) => @content.html(data.html) # Change url so if user reload a page - search results are saved history.replaceState {page: commitsUrl}, document.title, commitsUrl dataType: "json" - - @setOpacity: (opacity) -> - @content.css("opacity", opacity) From 8386edafd13c8cca1c6ed45abbbc554351300e9d Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 7 Jan 2016 06:28:24 -0800 Subject: [PATCH 171/280] Accept 2xx status codes for successful Web hook triggers Closes https://github.com/gitlabhq/gitlabhq/issues/9956 --- CHANGELOG | 1 + app/models/hooks/web_hook.rb | 2 +- spec/models/hooks/web_hook_spec.rb | 12 ++++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 94a776a35eb..a849d5efbf0 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.4.0 (unreleased) + - Accept 2xx status codes for successful Web hook triggers (Stan Hu) - Fix missing date of month in network graph when commits span a month (Stan Hu) - Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu) - Don't notify users twice if they are both project watchers and subscribers (Stan Hu) diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 40eb0e20b4b..b12a45e922a 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -60,7 +60,7 @@ class WebHook < ActiveRecord::Base basic_auth: auth) end - [response.code == 200, ActionView::Base.full_sanitizer.sanitize(response.to_s)] + [(response.code >= 200 && response.code < 300), 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, e.to_s] diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 2d90b0793cc..7070aa4ac62 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -77,5 +77,17 @@ describe ProjectHook, models: true do expect(@project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error']) end + + it "handles 200 status code" do + WebMock.stub_request(:post, @project_hook.url).to_return(status: 200, body: "Success") + + expect(@project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) + end + + it "handles 2xx status codes" do + WebMock.stub_request(:post, @project_hook.url).to_return(status: 201, body: "Success") + + expect(@project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) + end end end From 19a0db30ba14cb07a8f542abec56bd9cb2fa417f Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 7 Jan 2016 15:34:37 +0100 Subject: [PATCH 172/280] Removed ORDER BY in "of_group" scopes These scopes don't care about the order. Removing the explicit "ORDER BY" can speed up the queries by a little bit. --- app/models/issue.rb | 4 +++- app/models/merge_request.rb | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/issue.rb b/app/models/issue.rb index 21cc2105161..f52e47f3e62 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -33,7 +33,9 @@ class Issue < ActiveRecord::Base belongs_to :project validates :project, presence: true - scope :of_group, ->(group) { where(project_id: group.projects.select(:id)) } + scope :of_group, + ->(group) { where(project_id: group.projects.select(:id).reorder(nil)) } + scope :cared, ->(user) { where(assignee_id: user) } scope :open_for, ->(user) { opened.assigned_to(user) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 3c41bebc807..4e685ad3b7c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -131,7 +131,7 @@ class MergeRequest < ActiveRecord::Base validate :validate_branches validate :validate_fork - scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.projects.select(:id)) } + scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.projects.select(:id).reorder(nil)) } scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } From 1e927d39b4bf6d1177dee0dd4a6c60bf270db3f2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 7 Jan 2016 15:51:12 +0100 Subject: [PATCH 173/280] Update spec --- Gemfile.lock | 10 +++++----- spec/lib/gitlab/email/receiver_spec.rb | 11 +++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a1168ed3b7a..3c7cb6cf439 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -443,6 +443,10 @@ GEM omniauth (1.2.2) hashie (>= 1.2, < 4) rack (~> 1.0) + omniauth-azure-oauth2 (0.0.6) + jwt (~> 1.0) + omniauth (~> 1.0) + omniauth-oauth2 (~> 1.1) omniauth-bitbucket (0.0.2) multi_json (~> 1.7) omniauth (~> 1.1) @@ -488,10 +492,6 @@ GEM activesupport nokogiri (>= 1.4.4) omniauth (~> 1.0) - omniauth-azure-oauth2 (0.0.6) - jwt (~> 1.0) - omniauth (~> 1.0) - omniauth-oauth2 (~> 1.1) opennebula (4.14.2) json nokogiri @@ -920,6 +920,7 @@ DEPENDENCIES oauth2 (~> 1.0.0) octokit (~> 3.7.0) omniauth (~> 1.2.2) + omniauth-azure-oauth2 omniauth-bitbucket (~> 0.0.2) omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 3.0.0) @@ -931,7 +932,6 @@ DEPENDENCIES omniauth-shibboleth (~> 1.2.0) omniauth-twitter (~> 1.2.0) omniauth_crowd - omniauth-azure-oauth2 org-ruby (~> 0.9.12) paranoia (~> 2.0) pg (~> 0.18.2) diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index b535413bbd4..abe179cd4af 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -42,7 +42,7 @@ describe Gitlab::Email::Receiver, lib: true do context "when the email was auto generated" do let!(:reply_key) { '636ca428858779856c226bb145ef4fad' } let!(:email_raw) { fixture_file("emails/auto_reply.eml") } - + it "raises an AutoGeneratedEmailError" do expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::AutoGeneratedEmailError) end @@ -90,7 +90,7 @@ describe Gitlab::Email::Receiver, lib: true do context "when the reply is blank" do let!(:email_raw) { fixture_file("emails/no_content_reply.eml") } - + it "raises an EmptyEmailError" do expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError) end @@ -107,13 +107,16 @@ describe Gitlab::Email::Receiver, lib: true do end context "when everything is fine" do + let(:markdown) { "![image](uploads/image.png)" } + before do allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return( [ { url: "uploads/image.png", is_image: true, - alt: "image" + alt: "image", + markdown: markdown } ] ) @@ -132,7 +135,7 @@ describe Gitlab::Email::Receiver, lib: true do note = noteable.notes.last - expect(note.note).to include("![image](uploads/image.png)") + expect(note.note).to include(markdown) end end end From c3865bda0257506e3e9ba8352913eb288f0e3e34 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 7 Jan 2016 14:04:01 +0100 Subject: [PATCH 174/280] Properly set task-list class on single item task lists --- CHANGELOG | 1 + lib/banzai/filter/task_list_filter.rb | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 22fb91baaf0..c034344e7f9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -21,6 +21,7 @@ v 8.4.0 (unreleased) - Update version check images to use SVG - Validate README format before displaying - Enable Microsoft Azure OAuth2 support (Janis Meybohm) + - Properly set task-list class on single item task lists v 8.3.3 (unreleased) - Get "Merge when build succeeds" to work when commits were pushed to MR target branch while builds were running diff --git a/lib/banzai/filter/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb index bdf7c2ebdfc..d0ce13003a5 100644 --- a/lib/banzai/filter/task_list_filter.rb +++ b/lib/banzai/filter/task_list_filter.rb @@ -12,13 +12,18 @@ module Banzai # # See https://github.com/github/task_list/pull/60 class TaskListFilter < TaskList::Filter - def add_css_class(node, *new_class_names) + def add_css_class_with_fix(node, *new_class_names) if new_class_names.include?('task-list') - super if node.children.any? { |c| c['class'] == 'task-list-item' } - else - super + # Don't add class to all lists + return + elsif new_class_names.include?('task-list-item') + add_css_class_without_fix(node.parent, 'task-list') end + + add_css_class_without_fix(node, *new_class_names) end + + alias_method_chain :add_css_class, :fix end end end From ab9612df8dd97d83a94a8290f1530760e5cc7d2e Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 7 Jan 2016 11:47:39 -0500 Subject: [PATCH 175/280] initial json requests instead of HTML --- .../javascripts/merge_request.js.coffee | 6 +++ .../merge_request_widget.js.coffee | 5 ++- .../projects/merge_requests_controller.rb | 24 ++++++++++-- app/helpers/merge_requests_helper.rb | 9 +++++ .../merge_requests/widget/_closed.html.haml | 2 +- .../merge_requests/widget/_locked.html.haml | 2 +- .../merge_requests/widget/_merged.html.haml | 2 +- .../merge_requests/widget/_open.html.haml | 39 +++++++++---------- .../merge_requests/widget/_show.html.haml | 14 +++---- .../widget/open/_archived.html.haml | 2 +- 10 files changed, 66 insertions(+), 39 deletions(-) diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 8c321319b30..c6c7f37707f 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -63,12 +63,18 @@ class @MergeRequest $('a.btn-reopen').removeClass('hidden') $('div.status-box-closed').removeClass('hidden') $('div.status-box-open').addClass('hidden') + + $('div.mr-state-widget-closed').removeClass('hidden') + $('div.mr-state-widget-opened').addClass('hidden') else $('a.btn-reopen').addClass('hidden') $('a.issuable-edit').removeClass('hidden') $('a.btn-close').removeClass('hidden') $('div.status-box-closed').addClass('hidden') $('div.status-box-open').removeClass('hidden') + + $('div.mr-state-widget-closed').addClass('hidden') + $('div.mr-state-widget-opened').removeClass('hidden') else new Flash(mergeRequestFailMessage, 'alert') $this.prop('disabled', false) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 738ffc8343b..f0a687f79b1 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -28,8 +28,9 @@ class @MergeRequestWidget getMergeStatus: -> $.get @opts.url_to_automerge_check, (data) -> - $('.mr-state-widget').replaceWith(data) - + console.log("data",data); + # $('div.mr-state-widget.mr-state-widget-opened').replaceWith(data) + getCiStatus: -> if @opts.ci_enable $.get @opts.url_to_ci_check, (data) => diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index ab5c953189c..e3c4aa4873a 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -48,9 +48,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController @note_counts = Note.where(commit_id: @merge_request.commits.map(&:id)). group(:commit_id).count + json_merge_request = @merge_requests.as_json + respond_to do |format| format.html - format.json { render json: @merge_request } + format.json do + render json: { + hi: "yes" + } + end format.diff { render text: @merge_request.to_diff(current_user) } format.patch { render text: @merge_request.to_patch(current_user) } end @@ -143,7 +149,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController format.json do render json: { saved: @merge_request.valid?, - assignee_avatar_url: @merge_request.assignee.try(:avatar_url) + assignee_avatar_url: @merge_request.assignee.try(:avatar_url), + closed_event: @merge_request.closed_event } end end @@ -154,8 +161,17 @@ class Projects::MergeRequestsController < Projects::ApplicationController def merge_check @merge_request.check_if_can_be_merged if @merge_request.unchecked? - - render partial: "projects/merge_requests/widget/show.html.haml", layout: false + puts @merge_request.merge_status + respond_to do |format| + format.json do + render json: { + can_be_merged: @merge_request.merge_status == :can_be_merged + } + end + format.html do + render partial: "projects/merge_requests/widget/show.html.haml", layout: false + end + end end def cancel_merge_when_build_succeeds diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index fafe2acd538..6306450ca26 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -23,6 +23,15 @@ module MergeRequestsHelper return 'hidden' if mr.closed? == closed end + def merge_request_widget_visibility(mr, *states) + states.each do |state| + if mr.state == state + return + end + end + return 'hidden' + end + def mr_css_classes(mr) classes = "merge-request" classes << " closed" if mr.closed? diff --git a/app/views/projects/merge_requests/widget/_closed.html.haml b/app/views/projects/merge_requests/widget/_closed.html.haml index f3cc0e7e8a1..46ee22ec873 100644 --- a/app/views/projects/merge_requests/widget/_closed.html.haml +++ b/app/views/projects/merge_requests/widget/_closed.html.haml @@ -1,4 +1,4 @@ -.mr-state-widget +.mr-state-widget.mr-state-widget-closed{class: merge_request_widget_visibility(@merge_request, 'closed')} = render 'projects/merge_requests/widget/heading' .mr-widget-body %h4 diff --git a/app/views/projects/merge_requests/widget/_locked.html.haml b/app/views/projects/merge_requests/widget/_locked.html.haml index 78d0783cba0..55ecd69a6ce 100644 --- a/app/views/projects/merge_requests/widget/_locked.html.haml +++ b/app/views/projects/merge_requests/widget/_locked.html.haml @@ -1,4 +1,4 @@ -.mr-state-widget +.mr-state-widget.mr-state-widget-locked{class: merge_request_widget_visibility(@merge_request, 'locked')} = render 'projects/merge_requests/widget/heading' .mr-widget-body %h4 diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index d1d602eecdc..2bb50967023 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -1,4 +1,4 @@ -.mr-state-widget +.mr-state-widget.mr-state-widget-merged{class: merge_request_widget_visibility(@merge_request, 'merged')} = render 'projects/merge_requests/widget/heading' .mr-widget-body %h4 diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 55dbae598d3..2257a166460 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -1,24 +1,23 @@ -.mr-state-widget +.mr-state-widget.mr-state-widget-opened{class: merge_request_widget_visibility(@merge_request, "opened","reopened")} = render 'projects/merge_requests/widget/heading' - .mr-widget-body - - if @project.archived? - = render 'projects/merge_requests/widget/open/archived' - - elsif @merge_request.commits.blank? - = render 'projects/merge_requests/widget/open/nothing' - - elsif @merge_request.branch_missing? - = render 'projects/merge_requests/widget/open/missing_branch' - - elsif @merge_request.unchecked? - = render 'projects/merge_requests/widget/open/check' - - elsif @merge_request.cannot_be_merged? - = 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? - = render 'projects/merge_requests/widget/open/accept' + .mr-widget-body.merge-request-archived{class: ("hidden" unless @project.archived?)} + = render 'projects/merge_requests/widget/open/archived' + .mr-widget-body.merge-request-blank{class: ("hidden" unless @merge_request.commits.blank?)} + = render 'projects/merge_requests/widget/open/nothing' + .mr-widget-body.merge-request-branch-missing{class: ("hidden" unless @merge_request.branch_missing?)} + = render 'projects/merge_requests/widget/open/missing_branch' + .mr-widget-body.merge-request-unchecked{class: ("hidden" unless @merge_request.unchecked?)} + = render 'projects/merge_requests/widget/open/check' + .mr-widget-body.merge-request-cannot-be-merged{class: ("hidden" unless @merge_request.cannot_be_merged?)} + = render 'projects/merge_requests/widget/open/conflicts' + .mr-widget-body.merge-request-work-in-progress{class: ("hidden" unless @merge_request.work_in_progress?)} + = render 'projects/merge_requests/widget/open/wip' + .mr-widget-body.merge-request-merge-when-build-succeeds{class: ("hidden" unless @merge_request.merge_when_build_succeeds?)} + = render 'projects/merge_requests/widget/open/merge_when_build_succeeds' + .mr-widget-body.not-allowed{class: ("hidden" if @merge_request.can_be_merged_by?(current_user))} + = render 'projects/merge_requests/widget/open/not_allowed' + .mr-widget-body.merge-request-archived.can-be-merged{class: ("hidden" unless @merge_request.can_be_merged?)} + = render 'projects/merge_requests/widget/open/accept' - if @closes_issues.present? .mr-widget-footer diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index a489d4f9b24..d8f81dab067 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -1,17 +1,13 @@ -- if @merge_request.open? - = render 'projects/merge_requests/widget/open' -- elsif @merge_request.merged? - = render 'projects/merge_requests/widget/merged' -- elsif @merge_request.closed? - = render 'projects/merge_requests/widget/closed' -- elsif @merge_request.locked? - = render 'projects/merge_requests/widget/locked' += render 'projects/merge_requests/widget/open' += render 'projects/merge_requests/widget/merged' += render 'projects/merge_requests/widget/closed' += render 'projects/merge_requests/widget/locked' :javascript var merge_request_widget; merge_request_widget = new MergeRequestWidget({ - url_to_automerge_check: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + url_to_automerge_check: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :json)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", ci_enable: #{@project.ci_service ? "true" : "false"}, diff --git a/app/views/projects/merge_requests/widget/open/_archived.html.haml b/app/views/projects/merge_requests/widget/open/_archived.html.haml index ab30fa6b243..0d61e56d8fb 100644 --- a/app/views/projects/merge_requests/widget/open/_archived.html.haml +++ b/app/views/projects/merge_requests/widget/open/_archived.html.haml @@ -1,4 +1,4 @@ -%h4 +%h4 Project is archived %p This merge request cannot be merged because archived projects cannot be written to. From 8f6ca700553bdb88fa924dba21f5cb00a3c515f1 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 7 Jan 2016 12:27:01 -0500 Subject: [PATCH 176/280] fixes ajax issue with issue spec --- Gemfile.lock | 12 +++++----- spec/javascripts/issue_spec.js.coffee | 32 +++++++++++++-------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a1168ed3b7a..f5391ef9e48 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -443,6 +443,10 @@ GEM omniauth (1.2.2) hashie (>= 1.2, < 4) rack (~> 1.0) + omniauth-azure-oauth2 (0.0.6) + jwt (~> 1.0) + omniauth (~> 1.0) + omniauth-oauth2 (~> 1.1) omniauth-bitbucket (0.0.2) multi_json (~> 1.7) omniauth (~> 1.1) @@ -488,10 +492,6 @@ GEM activesupport nokogiri (>= 1.4.4) omniauth (~> 1.0) - omniauth-azure-oauth2 (0.0.6) - jwt (~> 1.0) - omniauth (~> 1.0) - omniauth-oauth2 (~> 1.1) opennebula (4.14.2) json nokogiri @@ -920,6 +920,7 @@ DEPENDENCIES oauth2 (~> 1.0.0) octokit (~> 3.7.0) omniauth (~> 1.2.2) + omniauth-azure-oauth2 omniauth-bitbucket (~> 0.0.2) omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 3.0.0) @@ -931,7 +932,6 @@ DEPENDENCIES omniauth-shibboleth (~> 1.2.0) omniauth-twitter (~> 1.2.0) omniauth_crowd - omniauth-azure-oauth2 org-ruby (~> 0.9.12) paranoia (~> 2.0) pg (~> 0.18.2) @@ -999,4 +999,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.10.6 + 1.11.2 diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee index 7e67c778861..b85fadcbe82 100644 --- a/spec/javascripts/issue_spec.js.coffee +++ b/spec/javascripts/issue_spec.js.coffee @@ -26,10 +26,10 @@ describe 'reopen/close issue', -> fixture.load('issues_show.html') @issue = new Issue() it 'closes an issue', -> - $.ajax = (obj) -> - expect(obj.type).toBe('PUT') - expect(obj.url).toBe('http://gitlab.com/issues/6/close') - obj.success saved: true + spyOn(jQuery, 'ajax').and.callFake (req) -> + expect(req.type).toBe('PUT') + expect(req.url).toBe('http://gitlab.com/issues/6/close') + req.success saved: true $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') @@ -46,10 +46,10 @@ describe 'reopen/close issue', -> it 'fails to closes an issue with success:false', -> - $.ajax = (obj) -> - expect(obj.type).toBe('PUT') - expect(obj.url).toBe('http://goesnowhere.nothing/whereami') - obj.success saved: false + spyOn(jQuery, 'ajax').and.callFake (req) -> + expect(req.type).toBe('PUT') + expect(req.url).toBe('http://goesnowhere.nothing/whereami') + req.success saved: false $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') @@ -69,10 +69,10 @@ describe 'reopen/close issue', -> it 'fails to closes an issue with HTTP error', -> - $.ajax = (obj) -> - expect(obj.type).toBe('PUT') - expect(obj.url).toBe('http://goesnowhere.nothing/whereami') - obj.error() + spyOn(jQuery, 'ajax').and.callFake (req) -> + expect(req.type).toBe('PUT') + expect(req.url).toBe('http://goesnowhere.nothing/whereami') + req.error() $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') @@ -91,10 +91,10 @@ describe 'reopen/close issue', -> expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.') it 'reopens an issue', -> - $.ajax = (obj) -> - expect(obj.type).toBe('PUT') - expect(obj.url).toBe('http://gitlab.com/issues/6/reopen') - obj.success saved: true + spyOn(jQuery, 'ajax').and.callFake (req) -> + expect(req.type).toBe('PUT') + expect(req.url).toBe('http://gitlab.com/issues/6/reopen') + req.success saved: true $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') From dada25d4472ec9ad601447fdd12da2301ac9ee79 Mon Sep 17 00:00:00 2001 From: Tommy Beadle Date: Thu, 7 Jan 2016 12:54:35 -0500 Subject: [PATCH 177/280] Include the username in user_create/destroy system hooks. --- app/services/system_hooks_service.rb | 3 ++- doc/system_hooks/system_hooks.md | 2 ++ spec/services/system_hooks_service_spec.rb | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 6dc854ec33d..2bd8223fbd8 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -47,7 +47,8 @@ class SystemHooksService data.merge!({ name: model.name, email: model.email, - user_id: model.id + user_id: model.id, + username: model.username }) when ProjectMember data.merge!(project_member_data(model)) diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index 49f98ded046..0539f30e802 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -129,6 +129,7 @@ X-Gitlab-Event: System Hook "email": "js@gitlabhq.com", "event_name": "user_create", "name": "John Smith", + "username": "js", "user_id": 41 } ``` @@ -142,6 +143,7 @@ X-Gitlab-Event: System Hook "email": "js@gitlabhq.com", "event_name": "user_destroy", "name": "John Smith", + "username": "js", "user_id": 41 } ``` diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index 4455ae7b321..1824e51ebfe 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -9,8 +9,8 @@ describe SystemHooksService, services: true do let(:group_member) { create(:group_member) } context 'event data' do - it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id) } - it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id) } + it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) } + it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) } it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) } From 1f64332e11949a5954b1e4ac7c6667b03ea70a0b Mon Sep 17 00:00:00 2001 From: Tommy Beadle Date: Thu, 7 Jan 2016 12:54:54 -0500 Subject: [PATCH 178/280] Include user_username in user_(add_to/remove_from)_(project/group) system hooks. --- app/services/system_hooks_service.rb | 2 ++ doc/system_hooks/system_hooks.md | 4 ++++ spec/services/system_hooks_service_spec.rb | 8 ++++---- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 2bd8223fbd8..bc39ae9f2cb 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -100,6 +100,7 @@ class SystemHooksService project_path: model.project.path, project_path_with_namespace: model.project.path_with_namespace, project_id: model.project.id, + user_username: model.user.username, user_name: model.user.name, user_email: model.user.email, access_level: model.human_access, @@ -112,6 +113,7 @@ class SystemHooksService group_name: model.group.name, group_path: model.group.path, group_id: model.group.id, + user_username: model.user.username, user_name: model.user.name, user_email: model.user.email, user_id: model.user.id, diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index 0539f30e802..612376e3a49 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -96,6 +96,7 @@ X-Gitlab-Event: System Hook "project_path_with_namespace": "jsmith/storecloud", "user_email": "johnsmith@gmail.com", "user_name": "John Smith", + "user_username": "johnsmith", "user_id": 41, "project_visibility": "private", } @@ -115,6 +116,7 @@ X-Gitlab-Event: System Hook "project_path_with_namespace": "jsmith/storecloud", "user_email": "johnsmith@gmail.com", "user_name": "John Smith", + "user_username": "johnsmith", "user_id": 41, "project_visibility": "private", } @@ -217,6 +219,7 @@ X-Gitlab-Event: System Hook "group_path": "storecloud", "user_email": "johnsmith@gmail.com", "user_name": "John Smith", + "user_username": "johnsmith", "user_id": 41 } ``` @@ -233,6 +236,7 @@ X-Gitlab-Event: System Hook "group_path": "storecloud", "user_email": "johnsmith@gmail.com", "user_name": "John Smith", + "user_username": "johnsmith", "user_id": 41 } ``` diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index 1824e51ebfe..eb066fe97f3 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -13,8 +13,8 @@ describe SystemHooksService, services: true do it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) } it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) } - it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) } + it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :access_level, :project_visibility) } + it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :access_level, :project_visibility) } it { expect(event_data(key, :create)).to include(:username, :key, :id) } it { expect(event_data(key, :destroy)).to include(:username, :key, :id) } @@ -50,13 +50,13 @@ describe SystemHooksService, services: true do it do expect(event_data(group_member, :create)).to include( :event_name, :created_at, :updated_at, :group_name, :group_path, - :group_id, :user_id, :user_name, :user_email, :group_access + :group_id, :user_id, :user_username, :user_name, :user_email, :group_access ) end it do expect(event_data(group_member, :destroy)).to include( :event_name, :created_at, :updated_at, :group_name, :group_path, - :group_id, :user_id, :user_name, :user_email, :group_access + :group_id, :user_id, :user_username, :user_name, :user_email, :group_access ) end end From df18441e09e99d79d228c58d63bb9247c516163b Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 7 Jan 2016 12:59:45 -0500 Subject: [PATCH 179/280] changes titles --- Gemfile.lock | 12 ++++++------ app/helpers/sorting_helper.rb | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a1168ed3b7a..f5391ef9e48 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -443,6 +443,10 @@ GEM omniauth (1.2.2) hashie (>= 1.2, < 4) rack (~> 1.0) + omniauth-azure-oauth2 (0.0.6) + jwt (~> 1.0) + omniauth (~> 1.0) + omniauth-oauth2 (~> 1.1) omniauth-bitbucket (0.0.2) multi_json (~> 1.7) omniauth (~> 1.1) @@ -488,10 +492,6 @@ GEM activesupport nokogiri (>= 1.4.4) omniauth (~> 1.0) - omniauth-azure-oauth2 (0.0.6) - jwt (~> 1.0) - omniauth (~> 1.0) - omniauth-oauth2 (~> 1.1) opennebula (4.14.2) json nokogiri @@ -920,6 +920,7 @@ DEPENDENCIES oauth2 (~> 1.0.0) octokit (~> 3.7.0) omniauth (~> 1.2.2) + omniauth-azure-oauth2 omniauth-bitbucket (~> 0.0.2) omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 3.0.0) @@ -931,7 +932,6 @@ DEPENDENCIES omniauth-shibboleth (~> 1.2.0) omniauth-twitter (~> 1.2.0) omniauth_crowd - omniauth-azure-oauth2 org-ruby (~> 0.9.12) paranoia (~> 2.0) pg (~> 0.18.2) @@ -999,4 +999,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.10.6 + 1.11.2 diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index bb12d43f397..99d7df64a83 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -19,7 +19,7 @@ module SortingHelper end def sort_title_recently_updated - 'Recently updated' + 'Last updated' end def sort_title_oldest_created @@ -27,7 +27,7 @@ module SortingHelper end def sort_title_recently_created - 'Recently created' + 'Last created' end def sort_title_milestone_soon From ad8366ed84b9c32c24c0ffe1c0a071f726cb119f Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 7 Jan 2016 13:23:17 -0500 Subject: [PATCH 180/280] css change to height to make gray container fit --- Gemfile.lock | 12 ++++++------ app/assets/stylesheets/pages/events.scss | 1 + app/assets/stylesheets/pages/projects.scss | 1 + 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a1168ed3b7a..f5391ef9e48 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -443,6 +443,10 @@ GEM omniauth (1.2.2) hashie (>= 1.2, < 4) rack (~> 1.0) + omniauth-azure-oauth2 (0.0.6) + jwt (~> 1.0) + omniauth (~> 1.0) + omniauth-oauth2 (~> 1.1) omniauth-bitbucket (0.0.2) multi_json (~> 1.7) omniauth (~> 1.1) @@ -488,10 +492,6 @@ GEM activesupport nokogiri (>= 1.4.4) omniauth (~> 1.0) - omniauth-azure-oauth2 (0.0.6) - jwt (~> 1.0) - omniauth (~> 1.0) - omniauth-oauth2 (~> 1.1) opennebula (4.14.2) json nokogiri @@ -920,6 +920,7 @@ DEPENDENCIES oauth2 (~> 1.0.0) octokit (~> 3.7.0) omniauth (~> 1.2.2) + omniauth-azure-oauth2 omniauth-bitbucket (~> 0.0.2) omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 3.0.0) @@ -931,7 +932,6 @@ DEPENDENCIES omniauth-shibboleth (~> 1.2.0) omniauth-twitter (~> 1.2.0) omniauth_crowd - omniauth-azure-oauth2 org-ruby (~> 0.9.12) paranoia (~> 2.0) pg (~> 0.18.2) @@ -999,4 +999,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.10.6 + 1.11.2 diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 282aaf2219b..984b4b91216 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -138,6 +138,7 @@ */ .event-last-push { overflow: auto; + width: 100%; .event-last-push-text { @include str-truncated(100%); padding: 5px 0; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index be6ef43e49c..0133a0d6822 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -415,6 +415,7 @@ ul.nav.nav-projects-tabs { border-bottom: 1px solid #EEE; margin: 0 -16px; padding: 0 $gl-padding; + height: 57px; ul.left-top-menu { display: inline-block; From 69209612e1793fcebcdb784074056d7a02b0f6f7 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 22 Dec 2015 12:15:06 -0800 Subject: [PATCH 181/280] Suppress e-mails on failed builds if allow_failure is set Every time I push to GitLab, I get > 2 emails saying a spec failed when I don't care about benchmarks and other specs that have `allow_failure` set to `true`. --- CHANGELOG | 1 + .../project_services/builds_email_service.rb | 6 ++++- lib/gitlab/build_data_builder.rb | 1 + spec/lib/gitlab/build_data_builder_spec.rb | 1 + .../builds_email_service_spec.rb | 23 +++++++++++++++++++ 5 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 spec/models/project_services/builds_email_service_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 879d057fff0..a6726eb1ec2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ v 8.4.0 (unreleased) v 8.3.3 (unreleased) - Get "Merge when build succeeds" to work when commits were pushed to MR target branch while builds were running + - Suppress e-mails on failed builds if allow_failure is set (Stan Hu) - Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu) - Enable "Add key" button when user fills in a proper key (Stan Hu) diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 92c9b13c9b9..f6313255cbb 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -73,12 +73,16 @@ class BuildsEmailService < Service when 'success' !notify_only_broken_builds? when 'failed' - true + !allow_failure?(data) else false end end + def allow_failure?(data) + data[:build_allow_failure] == true + end + def all_recipients(data) all_recipients = recipients.split(',') diff --git a/lib/gitlab/build_data_builder.rb b/lib/gitlab/build_data_builder.rb index 86bfa0a4378..34e949130da 100644 --- a/lib/gitlab/build_data_builder.rb +++ b/lib/gitlab/build_data_builder.rb @@ -23,6 +23,7 @@ module Gitlab build_started_at: build.started_at, build_finished_at: build.finished_at, build_duration: build.duration, + build_allow_failure: build.allow_failure, # TODO: do we still need it? project_id: project.id, diff --git a/spec/lib/gitlab/build_data_builder_spec.rb b/spec/lib/gitlab/build_data_builder_spec.rb index 839b30f1ff4..38be9448794 100644 --- a/spec/lib/gitlab/build_data_builder_spec.rb +++ b/spec/lib/gitlab/build_data_builder_spec.rb @@ -14,6 +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[:build_allow_failure]).to eq(false) } it { expect(data[:project_id]).to eq(build.project.id) } it { expect(data[:project_name]).to eq(build.project.name_with_namespace) } end diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb new file mode 100644 index 00000000000..905379a64e3 --- /dev/null +++ b/spec/models/project_services/builds_email_service_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe BuildsEmailService do + let(:build) { create(:ci_build) } + let(:data) { Gitlab::BuildDataBuilder.build(build) } + let(:service) { BuildsEmailService.new } + + describe :execute do + it "sends email" do + service.recipients = 'test@gitlab.com' + data[:build_status] = 'failed' + expect(BuildEmailWorker).to receive(:perform_async) + service.execute(data) + end + + it "does not sends email with failed build and allowed_failure on" do + data[:build_status] = 'failed' + data[:build_allow_failure] = true + expect(BuildEmailWorker).not_to receive(:perform_async) + service.execute(data) + end + end +end From 60aedb46d9e6d6c084ddfdc62805b7b1031fbea5 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 7 Jan 2016 15:18:07 -0500 Subject: [PATCH 182/280] adds reference to the bottom of sidebar --- app/assets/stylesheets/pages/issuable.scss | 10 +++++++++- app/views/shared/issuable/_sidebar.html.haml | 19 ++++++++++--------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 9da273a0b6b..81925283f11 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -94,8 +94,16 @@ } .cross-project-reference { - font-weight: bold; color: $gl-link-color; + + span { + white-space: nowrap; + width: 80%; + overflow: hidden; + position: relative; + display: inline-block; + text-overflow: ellipsis; + } button { float: right; diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 79c5cc7f40a..78c52938bd9 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -54,14 +54,6 @@ = 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" } - .block - .title - Cross-project reference - .cross-project-reference - %span#cross-project-reference - = cross_project_reference(@project, issuable) - = clipboard_button(clipboard_target: 'span#cross-project-reference') - = render "shared/issuable/participants", participants: issuable.participants(current_user) - if current_user @@ -77,7 +69,16 @@ You're not receiving notifications from this thread. .subscribed{class: ( 'hidden' unless subscribed )} You're receiving notifications because you're subscribed to this thread. + - project_ref = cross_project_reference(@project, issuable) + .block + .title + .cross-project-reference + %span#cross-project-reference + References: + %a{href: '#', title:project_ref} + = project_ref + = clipboard_button(clipboard_target: 'span#cross-project-reference') :javascript new Subscription("#{toggle_subscription_path(issuable)}"); - new IssuableContext(); + new IssuableContext(); \ No newline at end of file From a652c563663a0f5ed101fd5f9bcb14dfe111a9eb Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 7 Jan 2016 15:25:38 -0500 Subject: [PATCH 183/280] adds 85% width for text --- app/assets/stylesheets/pages/issuable.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 81925283f11..d4b44004f4f 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -98,7 +98,7 @@ span { white-space: nowrap; - width: 80%; + width: 85%; overflow: hidden; position: relative; display: inline-block; From 65308a9c15cd371986999d38eb6f359d8f601687 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 7 Jan 2016 15:23:29 -0500 Subject: [PATCH 184/280] Add spec for single-item task lists --- spec/lib/banzai/filter/task_list_filter_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/lib/banzai/filter/task_list_filter_spec.rb b/spec/lib/banzai/filter/task_list_filter_spec.rb index f2e3a44478d..569cbc885c7 100644 --- a/spec/lib/banzai/filter/task_list_filter_spec.rb +++ b/spec/lib/banzai/filter/task_list_filter_spec.rb @@ -7,4 +7,10 @@ describe Banzai::Filter::TaskListFilter, lib: true do exp = act = %(

  • Item
) expect(filter(act).to_html).to eq exp end + + it 'applies `task-list` to single-item task lists' do + act = filter('
  • [ ] Task 1
') + + expect(act.to_html).to start_with '