From e35cb5a025e3f47fd7ab237b5cf368aa3b9b319d Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 28 Mar 2016 21:40:49 +0200 Subject: [PATCH 001/187] Docs for the new branch button [ci-skip] --- .../basicsimages/new_branch_button.png | Bin 0 -> 120622 bytes doc/gitlab-basics/create-branch.md | 9 +++++++++ 2 files changed, 9 insertions(+) create mode 100644 doc/gitlab-basics/basicsimages/new_branch_button.png diff --git a/doc/gitlab-basics/basicsimages/new_branch_button.png b/doc/gitlab-basics/basicsimages/new_branch_button.png new file mode 100644 index 0000000000000000000000000000000000000000..394c139e17eadc81e8699aca05c727bf48c6a86d GIT binary patch literal 120622 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 literal 0 HcmV?d00001 diff --git a/doc/gitlab-basics/create-branch.md b/doc/gitlab-basics/create-branch.md index 7556b0f663e..9d688b9389b 100644 --- a/doc/gitlab-basics/create-branch.md +++ b/doc/gitlab-basics/create-branch.md @@ -32,6 +32,15 @@ Fill out the information required: ![Branch info](basicsimages/branch_info.png) +## From an issue +When an issue should be resolved one could also create a branch on the issue page. A button is displayed after the description unless there is already a branch or a referenced merge request. + +![New Branch Button](basicsimages/new_branch_button.png) + +The branch created diverges from the default branch of the project, usually `master`. The branch name will be based on the title of the issue and as suffix its ID. Thus the example screenshot above will yield a branch named `et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum-2`. +After the branch is created the user can edit files in the repository to fix the issue. When a merge request is created the +description field will display `Closes #2` to use the issue closing pattern. This will close the issue once the merge request is merged. + ### Note: You will be able to find and select the name of your branch in the white box next to a project's name: From 31b0e53015e38e51d9c02cca85c9279600b1bf85 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 28 Mar 2016 13:41:00 +0200 Subject: [PATCH 002/187] Introduce NotificationSetting model It will hold notification setting per group or per project. It will allow get rid of notification level stored in Member model Signed-off-by: Dmitriy Zaporozhets --- app/models/notification_setting.rb | 14 ++++++++++++++ ...20160328112808_create_notification_settings.rb | 12 ++++++++++++ db/schema.rb | 15 ++++++++++++--- spec/models/notification_setting_spec.rb | 15 +++++++++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 app/models/notification_setting.rb create mode 100644 db/migrate/20160328112808_create_notification_settings.rb create mode 100644 spec/models/notification_setting_spec.rb diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb new file mode 100644 index 00000000000..0dce146b7a9 --- /dev/null +++ b/app/models/notification_setting.rb @@ -0,0 +1,14 @@ +class NotificationSetting < ActiveRecord::Base + belongs_to :user + belongs_to :source, polymorphic: true + + validates :user, presence: true + validates :source, presence: true + validates :level, presence: true + validates :user_id, uniqueness: { scope: [:source_type, :source_id], + message: "already exists in source", + allow_nil: true } + # Notification level + # Note: When adding an option, it MUST go on the end of the array. + enum level: [:disabled, :participating, :watch, :global, :mention] +end diff --git a/db/migrate/20160328112808_create_notification_settings.rb b/db/migrate/20160328112808_create_notification_settings.rb new file mode 100644 index 00000000000..88652821ac3 --- /dev/null +++ b/db/migrate/20160328112808_create_notification_settings.rb @@ -0,0 +1,12 @@ +class CreateNotificationSettings < ActiveRecord::Migration + def change + create_table :notification_settings do |t| + t.integer :user_id + t.integer :level + t.integer :source_id + t.string :source_type + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index dce2bfe62ca..a9a68e723ab 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: 20160320204112) do +ActiveRecord::Schema.define(version: 20160328112808) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -417,9 +417,9 @@ ActiveRecord::Schema.define(version: 20160320204112) do t.string "state" t.integer "iid" t.integer "updated_by_id" - t.integer "moved_to_id" - t.boolean "confidential", default: false + t.boolean "confidential", default: false t.datetime "deleted_at" + t.integer "moved_to_id" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -638,6 +638,15 @@ ActiveRecord::Schema.define(version: 20160320204112) do add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree + create_table "notification_settings", force: :cascade do |t| + t.integer "user_id" + t.integer "level" + t.integer "source_id" + t.string "source_type" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "oauth_access_grants", force: :cascade do |t| t.integer "resource_owner_id", null: false t.integer "application_id", null: false diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb new file mode 100644 index 00000000000..f9d668ed75b --- /dev/null +++ b/spec/models/notification_setting_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +RSpec.describe NotificationSetting, type: :model do + describe "Associations" do + it { is_expected.to belong_to(:user) } + end + + describe "Validation" do + subject { NotificationSetting.new } + + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:source) } + it { is_expected.to validate_presence_of(:level) } + end +end From 73c5a3410596165244bfa3d2f657c313ec1c558c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 28 Mar 2016 14:27:30 +0200 Subject: [PATCH 003/187] Migrate notification setting from members table Signed-off-by: Dmitriy Zaporozhets --- ...160328115649_migrate_new_notification_setting.rb | 13 +++++++++++++ ...20160328121138_add_notification_setting_index.rb | 6 ++++++ db/schema.rb | 5 ++++- 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20160328115649_migrate_new_notification_setting.rb create mode 100644 db/migrate/20160328121138_add_notification_setting_index.rb diff --git a/db/migrate/20160328115649_migrate_new_notification_setting.rb b/db/migrate/20160328115649_migrate_new_notification_setting.rb new file mode 100644 index 00000000000..331c35535f2 --- /dev/null +++ b/db/migrate/20160328115649_migrate_new_notification_setting.rb @@ -0,0 +1,13 @@ +# This migration will create one row of NotificationSetting for each Member row +# It can take long time on big instances. Its unclear yet if this migration can be done online. +# This comment should be updated by @dzaporozhets before 8.7 release. If not - please ask him to do so. +class MigrateNewNotificationSetting < ActiveRecord::Migration + def up + timestamp = Time.now + execute "INSERT INTO notification_settings ( user_id, source_id, source_type, level, created_at, updated_at ) SELECT user_id, source_id, source_type, notification_level, '#{timestamp}', '#{timestamp}' FROM members" + end + + def down + NotificationSetting.delete_all + end +end diff --git a/db/migrate/20160328121138_add_notification_setting_index.rb b/db/migrate/20160328121138_add_notification_setting_index.rb new file mode 100644 index 00000000000..8aebce0244d --- /dev/null +++ b/db/migrate/20160328121138_add_notification_setting_index.rb @@ -0,0 +1,6 @@ +class AddNotificationSettingIndex < ActiveRecord::Migration + def change + add_index :notification_settings, :user_id + add_index :notification_settings, [:source_id, :source_type] + end +end diff --git a/db/schema.rb b/db/schema.rb index a9a68e723ab..29639abb6fc 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: 20160328112808) do +ActiveRecord::Schema.define(version: 20160328121138) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -647,6 +647,9 @@ ActiveRecord::Schema.define(version: 20160328112808) do t.datetime "updated_at", null: false end + add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree + add_index "notification_settings", ["user_id"], name: "index_notification_settings_on_user_id", using: :btree + create_table "oauth_access_grants", force: :cascade do |t| t.integer "resource_owner_id", null: false t.integer "application_id", null: false From 359157c097993a9b917ca590e128e85cf358d95d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 28 Mar 2016 18:17:42 +0200 Subject: [PATCH 004/187] Introduce NotificationSetting to user interface * visiting project will create notification setting if missing * change notification setting per project even without membership * use notification settings instead of membership on profile page Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/project.js.coffee | 8 +- .../profiles/notifications_controller.rb | 14 ++-- app/controllers/projects_controller.rb | 17 ++++- app/helpers/notifications_helper.rb | 74 +++++++++++-------- app/models/notification.rb | 4 +- app/models/notification_setting.rb | 7 ++ app/models/user.rb | 7 +- .../notifications/_settings.html.haml | 18 ++--- .../profiles/notifications/show.html.haml | 30 ++++---- .../projects/buttons/_notifications.html.haml | 20 ++--- 10 files changed, 106 insertions(+), 93 deletions(-) diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index 87d313ed67c..f171442d05a 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -37,15 +37,9 @@ class @Project $('.update-notification').on 'click', (e) -> e.preventDefault() notification_level = $(@).data 'notification-level' + label = $(@).data 'notification-title' $('#notification_level').val(notification_level) $('#notification-form').submit() - label = null - switch notification_level - when 0 then label = ' Disabled ' - when 1 then label = ' Participating ' - when 2 then label = ' Watching ' - when 3 then label = ' Global ' - when 4 then label = ' On Mention ' $('#notifications-button').empty().append("" + label + "") $(@).parents('ul').find('li.active').removeClass 'active' $(@).parent().addClass 'active' diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index 1fd1d6882df..6ca7537300f 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -2,8 +2,8 @@ class Profiles::NotificationsController < Profiles::ApplicationController def show @user = current_user @notification = current_user.notification - @project_members = current_user.project_members - @group_members = current_user.group_members + @group_notifications = current_user.notification_settings.for_groups + @project_notifications = current_user.notification_settings.for_projects end def update @@ -11,14 +11,10 @@ class Profiles::NotificationsController < Profiles::ApplicationController @saved = if type == 'global' current_user.update_attributes(user_params) - elsif type == 'group' - group_member = current_user.group_members.find(params[:notification_id]) - group_member.notification_level = params[:notification_level] - group_member.save else - project_member = current_user.project_members.find(params[:notification_id]) - project_member.notification_level = params[:notification_level] - project_member.save + notification_setting = current_user.notification_settings.find(params[:notification_id]) + notification_setting.level = params[:notification_level] + notification_setting.save end respond_to do |format| diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 928817ba811..77122f59128 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -98,14 +98,23 @@ class ProjectsController < Projects::ApplicationController respond_to do |format| format.html do + if current_user + @membership = @project.team.find_member(current_user.id) + + if @membership + @notification_setting = current_user.notification_settings.find_or_initialize_by(source: @project) + + unless @notification_setting.persisted? + @notification_setting.set_defaults + @notification_setting.save + end + end + end + if @project.repository_exists? if @project.empty_repo? render 'projects/empty' else - if current_user - @membership = @project.team.find_member(current_user.id) - end - render :show end else diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 499c655d2bf..a0e91a63d2d 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,48 +1,60 @@ module NotificationsHelper include IconsHelper - def notification_icon(notification) - if notification.disabled? - icon('volume-off', class: 'ns-mute') - elsif notification.participating? - icon('volume-down', class: 'ns-part') - elsif notification.watch? - icon('volume-up', class: 'ns-watch') - else - icon('circle-o', class: 'ns-default') + def notification_icon_class(level) + case level.to_sym + when :disabled + 'microphone-slash' + when :participating + 'volume-up' + when :watch + 'eye' + when :mention + 'at' + when :global + 'globe' end end - def notification_list_item(notification_level, user_membership) - case notification_level - when Notification::N_DISABLED - update_notification_link(Notification::N_DISABLED, user_membership, 'Disabled', 'microphone-slash') - when Notification::N_PARTICIPATING - update_notification_link(Notification::N_PARTICIPATING, user_membership, 'Participate', 'volume-up') - when Notification::N_WATCH - update_notification_link(Notification::N_WATCH, user_membership, 'Watch', 'eye') - when Notification::N_MENTION - update_notification_link(Notification::N_MENTION, user_membership, 'On mention', 'at') - when Notification::N_GLOBAL - update_notification_link(Notification::N_GLOBAL, user_membership, 'Global', 'globe') - else - # do nothing + def notification_icon(level) + icon("#{notification_icon_class(level)} fw") + end + + def notification_title(level) + case level.to_sym + when :disabled + 'Disabled' + when :participating + 'Participate' + when :watch + 'Watch' + when :mention + 'On mention' + when :global + 'Global' end end - def update_notification_link(notification_level, user_membership, title, icon) - content_tag(:li, class: active_level_for(user_membership, notification_level)) do - link_to '#', class: 'update-notification', data: { notification_level: notification_level } do - icon("#{icon} fw", text: title) + def notification_list_item(level, setting) + title = notification_title(level) + + data = { + notification_level: level, + notification_title: title + } + + content_tag(:li, class: active_level_for(setting, level)) do + link_to '#', class: 'update-notification', data: data do + icon("#{notification_icon_class(level)} fw", text: title) end end end - def notification_label(user_membership) - Notification.new(user_membership).to_s + def notification_label(setting) + notification_title(setting.level) end - def active_level_for(user_membership, level) - 'active' if user_membership.notification_level == level + def active_level_for(setting, level) + 'active' if setting.level == level end end diff --git a/app/models/notification.rb b/app/models/notification.rb index 171b8df45c2..379f041969b 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -57,7 +57,7 @@ class Notification def level target.notification_level end - + def to_s case level when N_DISABLED @@ -71,7 +71,7 @@ class Notification when N_GLOBAL 'Global' else - # do nothing + # do nothing end end end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 0dce146b7a9..287862a01bc 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -11,4 +11,11 @@ class NotificationSetting < ActiveRecord::Base # Notification level # Note: When adding an option, it MUST go on the end of the array. enum level: [:disabled, :participating, :watch, :global, :mention] + + scope :for_groups, -> { where(source_type: 'Namespace') } + scope :for_projects, -> { where(source_type: 'Project') } + + def set_defaults + self.level = :global + end end diff --git a/app/models/user.rb b/app/models/user.rb index 128ddc2a694..59493e6f90c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -143,6 +143,7 @@ class User < ActiveRecord::Base has_many :spam_logs, dependent: :destroy has_many :builds, dependent: :nullify, class_name: 'Ci::Build' has_many :todos, dependent: :destroy + has_many :notification_settings, dependent: :destroy # # Validations @@ -157,7 +158,7 @@ class User < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false } - validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true + validates :notification_level, presence: true validate :namespace_uniq, if: ->(user) { user.username_changed? } validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :unique_email, if: ->(user) { user.email_changed? } @@ -190,6 +191,10 @@ class User < ActiveRecord::Base # Note: When adding an option, it MUST go on the end of the array. enum project_view: [:readme, :activity, :files] + # Notification level + # Note: When adding an option, it MUST go on the end of the array. + enum notification_level: [:disabled, :participating, :watch, :global, :mention] + alias_attribute :private_token, :authentication_token delegate :path, to: :namespace, allow_nil: true, prefix: true diff --git a/app/views/profiles/notifications/_settings.html.haml b/app/views/profiles/notifications/_settings.html.haml index d0d044136f6..c32de0b9925 100644 --- a/app/views/profiles/notifications/_settings.html.haml +++ b/app/views/profiles/notifications/_settings.html.haml @@ -1,17 +1,17 @@ %li.notification-list-item %span.notification.fa.fa-holder.append-right-5 - - if notification.global? - = notification_icon(@notification) + - if setting.global? + = notification_icon(current_user.notification_level) - else - = notification_icon(notification) + = notification_icon(setting.level) %span.str-truncated - - if membership.kind_of? GroupMember - = link_to membership.group.name, membership.group + - if setting.source.kind_of? Project + = link_to_project(setting.source) - else - = link_to_project(membership.project) + = link_to setting.source.name, group_path(setting.source) .pull-right = form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do - = hidden_field_tag :notification_type, type, id: dom_id(membership, 'notification_type') - = hidden_field_tag :notification_id, membership.id, id: dom_id(membership, 'notification_id') - = select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'form-control trigger-submit' + = hidden_field_tag :notification_id, setting.id + = hidden_field_tag :notification_level, setting.level + = select_tag :notification_level, options_for_select(User.notification_levels.keys, setting.level), class: 'form-control trigger-submit' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index de80abd7f4d..f6900f61b2d 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -26,29 +26,29 @@ .form-group = f.label :notification_level, class: 'label-light' .radio - = f.label :notification_level, value: Notification::N_DISABLED do - = f.radio_button :notification_level, Notification::N_DISABLED + = f.label :notification_level, value: :disabled do + = f.radio_button :notification_level, :disabled .level-title Disabled %p You will not get any notifications via email .radio - = f.label :notification_level, value: Notification::N_MENTION do - = f.radio_button :notification_level, Notification::N_MENTION + = f.label :notification_level, value: :mention do + = f.radio_button :notification_level, :mention .level-title On Mention %p You will receive notifications only for comments in which you were @mentioned .radio - = f.label :notification_level, value: Notification::N_PARTICIPATING do - = f.radio_button :notification_level, Notification::N_PARTICIPATING + = f.label :notification_level, value: :participating do + = f.radio_button :notification_level, :participating .level-title Participating %p You will only receive notifications from related resources (e.g. from your commits or assigned issues) .radio - = f.label :notification_level, value: Notification::N_WATCH do - = f.radio_button :notification_level, Notification::N_WATCH + = f.label :notification_level, value: :watch do + = f.radio_button :notification_level, :watch .level-title Watch %p You will receive notifications for any activity @@ -57,18 +57,16 @@ = f.submit 'Update settings', class: "btn btn-create" %hr %h5 - Groups (#{@group_members.count}) + Groups (#{@group_notifications.count}) %div %ul.bordered-list - - @group_members.each do |group_member| - - notification = Notification.new(group_member) - = render 'settings', type: 'group', membership: group_member, notification: notification + - @group_notifications.each do |setting| + = render 'settings', setting: setting %h5 - Projects (#{@project_members.count}) + Projects (#{@project_notifications.count}) %p.account-well To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group. .append-bottom-default %ul.bordered-list - - @project_members.each do |project_member| - - notification = Notification.new(project_member) - = render 'settings', type: 'project', membership: project_member, notification: notification + - @project_notifications.each do |setting| + = render 'settings', setting: setting diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index a3786c35a1f..4b8a10f0819 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -1,20 +1,12 @@ -- case @membership -- when ProjectMember +- if @notification_setting = form_tag profile_notifications_path, method: :put, remote: true, class: 'inline', id: 'notification-form' do - = hidden_field_tag :notification_type, 'project' - = hidden_field_tag :notification_id, @membership.id - = hidden_field_tag :notification_level + = hidden_field_tag :notification_id, @notification_setting.id + = hidden_field_tag :notification_level, @notification_setting.level %span.dropdown %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"} = icon('bell') - = notification_label(@membership) + = notification_title(@notification_setting.level) = icon('angle-down') %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - - Notification.project_notification_levels.each do |level| - = notification_list_item(level, @membership) - -- when GroupMember - .btn.disabled.notifications-btn.has-tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."} - = icon('bell') - = notification_label(@membership) - = icon('angle-down') + - NotificationSetting.levels.each do |level| + = notification_list_item(level.first, @notification_setting) From 7ea1bcab45697556d4ffd79ab680872ed823d4a3 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 28 Mar 2016 18:25:57 +0200 Subject: [PATCH 005/187] Create notification setting when membership created Signed-off-by: Dmitriy Zaporozhets --- app/models/member.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/models/member.rb b/app/models/member.rb index ca08007b7eb..177f37c3bbd 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -56,6 +56,7 @@ class Member < ActiveRecord::Base before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } after_create :send_invite, if: :invite? + after_create :create_notification_setting, unless: :invite? after_create :post_create_hook, unless: :invite? after_update :post_update_hook, unless: :invite? after_destroy :post_destroy_hook, unless: :invite? @@ -160,6 +161,15 @@ class Member < ActiveRecord::Base send_invite end + def create_notification_setting + notification_setting = user.notification_settings.find_or_initialize_by(source: source) + + unless notification_setting.persisted? + notification_setting.set_defaults + notification_setting.save + end + end + private def send_invite From b8f38437900cdddac9d19d5c48a2a8e5bb037f41 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 28 Mar 2016 20:31:36 +0200 Subject: [PATCH 006/187] Update NotificationService to use NotificationSettings instead of membership Signed-off-by: Dmitriy Zaporozhets --- app/models/concerns/notifiable.rb | 15 ---------- app/models/group.rb | 1 + app/models/member.rb | 5 +++- app/models/notification.rb | 22 ++------------ app/models/project.rb | 1 + app/models/user.rb | 3 ++ app/services/notification_service.rb | 34 ++++++++++------------ spec/services/notification_service_spec.rb | 26 ++++++++--------- 8 files changed, 40 insertions(+), 67 deletions(-) delete mode 100644 app/models/concerns/notifiable.rb diff --git a/app/models/concerns/notifiable.rb b/app/models/concerns/notifiable.rb deleted file mode 100644 index d7dcd97911d..00000000000 --- a/app/models/concerns/notifiable.rb +++ /dev/null @@ -1,15 +0,0 @@ -# == Notifiable concern -# -# Contains notification functionality -# -module Notifiable - extend ActiveSupport::Concern - - included do - validates :notification_level, inclusion: { in: Notification.project_notification_levels }, presence: true - end - - def notification - @notification ||= Notification.new(self) - end -end diff --git a/app/models/group.rb b/app/models/group.rb index b332601c59b..9a04ac70d35 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -27,6 +27,7 @@ class Group < Namespace has_many :users, through: :group_members has_many :project_group_links, dependent: :destroy has_many :shared_projects, through: :project_group_links, source: :project + has_many :notification_settings, dependent: :destroy, as: :source validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :visibility_level_allowed_by_projects diff --git a/app/models/member.rb b/app/models/member.rb index 177f37c3bbd..e665ba6fb75 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -19,7 +19,6 @@ class Member < ActiveRecord::Base include Sortable - include Notifiable include Gitlab::Access attr_accessor :raw_invite_token @@ -170,6 +169,10 @@ class Member < ActiveRecord::Base end end + def notification + @notification ||= user.notification_settings.find_by(source: source) + end + private def send_invite diff --git a/app/models/notification.rb b/app/models/notification.rb index 379f041969b..8a90b456cc2 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -30,30 +30,12 @@ class Notification end end + delegate :disabled?, :participating?, :watch?, :global?, :mention?, to: :target + def initialize(target) @target = target end - def disabled? - target.notification_level == N_DISABLED - end - - def participating? - target.notification_level == N_PARTICIPATING - end - - def watch? - target.notification_level == N_WATCH - end - - def global? - target.notification_level == N_GLOBAL - end - - def mention? - target.notification_level == N_MENTION - end - def level target.notification_level end diff --git a/app/models/project.rb b/app/models/project.rb index 2285063ab50..2f9621809b6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -154,6 +154,7 @@ class Project < ActiveRecord::Base has_many :project_group_links, dependent: :destroy has_many :invited_groups, through: :project_group_links, source: :group has_many :todos, dependent: :destroy + has_many :notification_settings, dependent: :destroy, as: :source has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" diff --git a/app/models/user.rb b/app/models/user.rb index 59493e6f90c..f556dc5903d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -193,6 +193,9 @@ class User < ActiveRecord::Base # Notification level # Note: When adding an option, it MUST go on the end of the array. + # + # TODO: Add '_prefix: :notification' to enum when update to Rails 5. https://github.com/rails/rails/pull/19813 + # Because user.notification_disabled? is much better than user.disabled? enum notification_level: [:disabled, :participating, :watch, :global, :mention] alias_attribute :private_token, :authentication_token diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index eff0d96f93d..9628843c230 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -253,8 +253,8 @@ class NotificationService def project_watchers(project) project_members = project_member_notification(project) - users_with_project_level_global = project_member_notification(project, Notification::N_GLOBAL) - users_with_group_level_global = group_member_notification(project, Notification::N_GLOBAL) + users_with_project_level_global = project_member_notification(project, :global) + users_with_group_level_global = group_member_notification(project, :global) users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq) users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users) @@ -264,18 +264,16 @@ class NotificationService end def project_member_notification(project, notification_level=nil) - project_members = project.project_members - if notification_level - project_members.where(notification_level: notification_level).pluck(:user_id) + project.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id) else - project_members.pluck(:user_id) + project.notification_settings.pluck(:user_id) end end def group_member_notification(project, notification_level) if project.group - project.group.group_members.where(notification_level: notification_level).pluck(:user_id) + project.group.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id) else [] end @@ -284,13 +282,13 @@ class NotificationService def users_with_global_level_watch(ids) User.where( id: ids, - notification_level: Notification::N_WATCH + notification_level: NotificationSetting.levels[:watch] ).pluck(:id) end # Build a list of users based on project notifcation settings def select_project_member_setting(project, global_setting, users_global_level_watch) - users = project_member_notification(project, Notification::N_WATCH) + users = project_member_notification(project, :watch) # If project setting is global, add to watch list if global setting is watch global_setting.each do |user_id| @@ -304,7 +302,7 @@ class NotificationService # Build a list of users based on group notification settings def select_group_member_setting(project, project_members, global_setting, users_global_level_watch) - uids = group_member_notification(project, Notification::N_WATCH) + uids = group_member_notification(project, :watch) # Group setting is watch, add to users list if user is not project member users = [] @@ -351,20 +349,20 @@ class NotificationService users.reject do |user| next user.notification.send(method_name) unless project - member = project.project_members.find_by(user_id: user.id) + setting = user.notification_settings.find_by(source: project) - if !member && project.group - member = project.group.group_members.find_by(user_id: user.id) + if !setting && project.group + setting = user.notification_settings.find_by(source: group) end - # reject users who globally set mention notification and has no membership - next user.notification.send(method_name) unless member + # reject users who globally set mention notification and has no setting per project/group + next user.notification.send(method_name) unless setting # reject users who set mention notification in project - next true if member.notification.send(method_name) + next true if setting.send(method_name) - # reject users who have N_MENTION in project and disabled in global settings - member.notification.global? && user.notification.send(method_name) + # reject users who have mention level in project and disabled in global settings + setting.global? && user.notification.send(method_name) end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 0f2aa3ae73c..c01851a8a24 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -89,11 +89,11 @@ describe NotificationService, services: true do note.project.group.add_user(@u_watcher, GroupMember::MASTER) note.project.save user_project = note.project.project_members.find_by_user_id(@u_watcher.id) - user_project.notification_level = Notification::N_PARTICIPATING + user_project.notification.level = :participating user_project.save group_member = note.project.group.group_members.find_by_user_id(@u_watcher.id) - group_member.notification_level = Notification::N_GLOBAL - group_member.save + group_member.notification.level = :global + group_member.notification.save ActionMailer::Base.deliveries.clear end @@ -215,7 +215,7 @@ describe NotificationService, services: true do end it do - @u_committer.update_attributes(notification_level: Notification::N_MENTION) + @u_committer.update_attributes(notification_level: :mention) notification.new_note(note) should_not_email(@u_committer) end @@ -246,7 +246,7 @@ describe NotificationService, services: true do end it do - issue.assignee.update_attributes(notification_level: Notification::N_MENTION) + issue.assignee.update_attributes(notification_level: :mention) notification.new_issue(issue, @u_disabled) should_not_email(issue.assignee) @@ -596,13 +596,13 @@ describe NotificationService, services: true do end def build_team(project) - @u_watcher = create(:user, notification_level: Notification::N_WATCH) - @u_participating = create(:user, notification_level: Notification::N_PARTICIPATING) - @u_participant_mentioned = create(:user, username: 'participant', notification_level: Notification::N_PARTICIPATING) - @u_disabled = create(:user, notification_level: Notification::N_DISABLED) - @u_mentioned = create(:user, username: 'mention', notification_level: Notification::N_MENTION) + @u_watcher = create(:user, notification_level: :watch) + @u_participating = create(:user, notification_level: :participating) + @u_participant_mentioned = create(:user, username: 'participant', notification_level: :participating) + @u_disabled = create(:user, notification_level: :disabled) + @u_mentioned = create(:user, username: 'mention', notification_level: :mention) @u_committer = create(:user, username: 'committer') - @u_not_mentioned = create(:user, username: 'regular', notification_level: Notification::N_PARTICIPATING) + @u_not_mentioned = create(:user, username: 'regular', notification_level: :participating) @u_outsider_mentioned = create(:user, username: 'outsider') project.team << [@u_watcher, :master] @@ -617,8 +617,8 @@ describe NotificationService, services: true do def add_users_with_subscription(project, issuable) @subscriber = create :user @unsubscriber = create :user - @subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: Notification::N_PARTICIPATING) - @watcher_and_subscriber = create(:user, notification_level: Notification::N_WATCH) + @subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: :participating) + @watcher_and_subscriber = create(:user, notification_level: :watch) project.team << [@subscribed_participant, :master] project.team << [@subscriber, :master] From 4ca73f56cb59b86f25b55ff02800571fb82c742f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 28 Mar 2016 23:22:28 +0200 Subject: [PATCH 007/187] Small refactoring and cleanup of notification logic Signed-off-by: Dmitriy Zaporozhets --- app/helpers/notifications_helper.rb | 12 +----- app/models/member.rb | 2 + app/models/members/group_member.rb | 1 - app/models/members/project_member.rb | 1 - app/models/notification.rb | 46 ---------------------- spec/models/notification_setting_spec.rb | 1 + spec/services/notification_service_spec.rb | 9 ++--- 7 files changed, 8 insertions(+), 64 deletions(-) diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index a0e91a63d2d..8816cc5d164 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -22,16 +22,12 @@ module NotificationsHelper def notification_title(level) case level.to_sym - when :disabled - 'Disabled' when :participating 'Participate' - when :watch - 'Watch' when :mention 'On mention' - when :global - 'Global' + else + level.to_s.titlecase end end @@ -50,10 +46,6 @@ module NotificationsHelper end end - def notification_label(setting) - notification_title(setting.level) - end - def active_level_for(setting, level) 'active' if setting.level == level end diff --git a/app/models/member.rb b/app/models/member.rb index e665ba6fb75..799f28c3fdf 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -62,6 +62,8 @@ class Member < ActiveRecord::Base delegate :name, :username, :email, to: :user, prefix: true + default_value_for :notification_level, NotificationSetting.levels[:global] + class << self def find_by_invite_token(invite_token) invite_token = Devise.token_generator.digest(self, :invite_token, invite_token) diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 65d2ea00570..9fb474a1a93 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -24,7 +24,6 @@ class GroupMember < Member # Make sure group member points only to group as it source default_value_for :source_type, SOURCE_TYPE - default_value_for :notification_level, Notification::N_GLOBAL validates_format_of :source_type, with: /\ANamespace\z/ default_scope { where(source_type: SOURCE_TYPE) } diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 560d1690e14..07ddb02ae9d 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -27,7 +27,6 @@ class ProjectMember < Member # Make sure project member points only to project as it source default_value_for :source_type, SOURCE_TYPE - default_value_for :notification_level, Notification::N_GLOBAL validates_format_of :source_type, with: /\AProject\z/ default_scope { where(source_type: SOURCE_TYPE) } diff --git a/app/models/notification.rb b/app/models/notification.rb index 8a90b456cc2..3805bde88b0 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -1,35 +1,6 @@ class Notification - # - # Notification levels - # - N_DISABLED = 0 - N_PARTICIPATING = 1 - N_WATCH = 2 - N_GLOBAL = 3 - N_MENTION = 4 - attr_accessor :target - class << self - def notification_levels - [N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH] - end - - def options_with_labels - { - disabled: N_DISABLED, - participating: N_PARTICIPATING, - watch: N_WATCH, - mention: N_MENTION, - global: N_GLOBAL - } - end - - def project_notification_levels - [N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH, N_GLOBAL] - end - end - delegate :disabled?, :participating?, :watch?, :global?, :mention?, to: :target def initialize(target) @@ -39,21 +10,4 @@ class Notification def level target.notification_level end - - def to_s - case level - when N_DISABLED - 'Disabled' - when N_PARTICIPATING - 'Participating' - when N_WATCH - 'Watching' - when N_MENTION - 'On mention' - when N_GLOBAL - 'Global' - else - # do nothing - end - end end diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb index f9d668ed75b..f31b2a3cd6f 100644 --- a/spec/models/notification_setting_spec.rb +++ b/spec/models/notification_setting_spec.rb @@ -3,6 +3,7 @@ require 'rails_helper' RSpec.describe NotificationSetting, type: :model do describe "Associations" do it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:source) } end describe "Validation" do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index c01851a8a24..c4d52584a4b 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -88,12 +88,9 @@ describe NotificationService, services: true do note.project.namespace_id = group.id note.project.group.add_user(@u_watcher, GroupMember::MASTER) note.project.save - user_project = note.project.project_members.find_by_user_id(@u_watcher.id) - user_project.notification.level = :participating - user_project.save - group_member = note.project.group.group_members.find_by_user_id(@u_watcher.id) - group_member.notification.level = :global - group_member.notification.save + + @u_watcher.notification_settings.find_by(source: note.project).participating! + @u_watcher.notification_settings.find_by(source: note.project.group).global! ActionMailer::Base.deliveries.clear end From 855b2820c16c6e569d5c38b7def8ead18c86cecd Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 28 Mar 2016 23:39:49 +0200 Subject: [PATCH 008/187] Improve db migrations for notification settings Signed-off-by: Dmitriy Zaporozhets --- ...60328112808_create_notification_settings.rb | 7 +++---- ...8115649_migrate_new_notification_setting.rb | 2 +- db/schema.rb | 18 +++++++++--------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/db/migrate/20160328112808_create_notification_settings.rb b/db/migrate/20160328112808_create_notification_settings.rb index 88652821ac3..4755da8b806 100644 --- a/db/migrate/20160328112808_create_notification_settings.rb +++ b/db/migrate/20160328112808_create_notification_settings.rb @@ -1,10 +1,9 @@ class CreateNotificationSettings < ActiveRecord::Migration def change create_table :notification_settings do |t| - t.integer :user_id - t.integer :level - t.integer :source_id - t.string :source_type + t.references :user, null: false + t.references :source, polymorphic: true, null: false + t.integer :level, default: 0, null: false t.timestamps null: false end diff --git a/db/migrate/20160328115649_migrate_new_notification_setting.rb b/db/migrate/20160328115649_migrate_new_notification_setting.rb index 331c35535f2..aff866b5f46 100644 --- a/db/migrate/20160328115649_migrate_new_notification_setting.rb +++ b/db/migrate/20160328115649_migrate_new_notification_setting.rb @@ -4,7 +4,7 @@ class MigrateNewNotificationSetting < ActiveRecord::Migration def up timestamp = Time.now - execute "INSERT INTO notification_settings ( user_id, source_id, source_type, level, created_at, updated_at ) SELECT user_id, source_id, source_type, notification_level, '#{timestamp}', '#{timestamp}' FROM members" + execute "INSERT INTO notification_settings ( user_id, source_id, source_type, level, created_at, updated_at ) SELECT user_id, source_id, source_type, notification_level, '#{timestamp}', '#{timestamp}' FROM members WHERE user_id IS NOT NULL" end def down diff --git a/db/schema.rb b/db/schema.rb index 29639abb6fc..e946ecd3f2b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -639,12 +639,12 @@ ActiveRecord::Schema.define(version: 20160328121138) do add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree create_table "notification_settings", force: :cascade do |t| - t.integer "user_id" - t.integer "level" - t.integer "source_id" - t.string "source_type" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.integer "user_id", null: false + t.integer "source_id", null: false + t.string "source_type", null: false + t.integer "level", default: 0, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree @@ -798,9 +798,9 @@ ActiveRecord::Schema.define(version: 20160328121138) do t.string "type" t.string "title" t.integer "project_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "active", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.boolean "active", default: false, null: false t.text "properties" t.boolean "template", default: false t.boolean "push_events", default: true From 08b3d7f6ef23b7a8a83c7e71e2d04f6416e73406 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 29 Mar 2016 13:08:30 +0200 Subject: [PATCH 009/187] Refactor notification helper and fix notification service Signed-off-by: Dmitriy Zaporozhets --- app/helpers/notifications_helper.rb | 12 +++----- app/services/notification_service.rb | 2 +- spec/helpers/notifications_helper_spec.rb | 37 ++++++----------------- 3 files changed, 14 insertions(+), 37 deletions(-) diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 8816cc5d164..54ab9179efc 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -16,8 +16,8 @@ module NotificationsHelper end end - def notification_icon(level) - icon("#{notification_icon_class(level)} fw") + def notification_icon(level, text = nil) + icon("#{notification_icon_class(level)} fw", text: text) end def notification_title(level) @@ -39,14 +39,10 @@ module NotificationsHelper notification_title: title } - content_tag(:li, class: active_level_for(setting, level)) do + content_tag(:li, class: ('active' if setting.level == level)) do link_to '#', class: 'update-notification', data: data do - icon("#{notification_icon_class(level)} fw", text: title) + notification_icon(level, title) end end end - - def active_level_for(setting, level) - 'active' if setting.level == level - end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 9628843c230..23f211dfcd2 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -352,7 +352,7 @@ class NotificationService setting = user.notification_settings.find_by(source: project) if !setting && project.group - setting = user.notification_settings.find_by(source: group) + setting = user.notification_settings.find_by(source: project.group) end # reject users who globally set mention notification and has no setting per project/group diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index f1aba4cfdf3..9d5f009ebe1 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -2,34 +2,15 @@ require 'spec_helper' describe NotificationsHelper do describe 'notification_icon' do - let(:notification) { double(disabled?: false, participating?: false, watch?: false) } + it { expect(notification_icon(:disabled)).to match('class="fa fa-microphone-slash fa-fw"') } + it { expect(notification_icon(:participating)).to match('class="fa fa-volume-up fa-fw"') } + it { expect(notification_icon(:mention)).to match('class="fa fa-at fa-fw"') } + it { expect(notification_icon(:global)).to match('class="fa fa-globe fa-fw"') } + it { expect(notification_icon(:watch)).to match('class="fa fa-eye fa-fw"') } + end - context "disabled notification" do - before { allow(notification).to receive(:disabled?).and_return(true) } - - it "has a red icon" do - expect(notification_icon(notification)).to match('class="fa fa-volume-off ns-mute"') - end - end - - context "participating notification" do - before { allow(notification).to receive(:participating?).and_return(true) } - - it "has a blue icon" do - expect(notification_icon(notification)).to match('class="fa fa-volume-down ns-part"') - end - end - - context "watched notification" do - before { allow(notification).to receive(:watch?).and_return(true) } - - it "has a green icon" do - expect(notification_icon(notification)).to match('class="fa fa-volume-up ns-watch"') - end - end - - it "has a blue icon" do - expect(notification_icon(notification)).to match('class="fa fa-circle-o ns-default"') - end + describe 'notification_title' do + it { expect(notification_title(:watch)).to match('Watch') } + it { expect(notification_title(:mention)).to match('On mention') } end end From 86418c475be1b2a37e89682bc87055b7372bbcfb Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 29 Mar 2016 13:37:43 +0200 Subject: [PATCH 010/187] Remove useless Notification model Signed-off-by: Dmitriy Zaporozhets --- app/models/member.rb | 4 ++-- app/models/notification.rb | 13 ------------- app/models/user.rb | 4 ---- app/services/notification_service.rb | 24 +++++++++++++++--------- 4 files changed, 17 insertions(+), 28 deletions(-) delete mode 100644 app/models/notification.rb diff --git a/app/models/member.rb b/app/models/member.rb index 799f28c3fdf..cbcc54c0250 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -171,8 +171,8 @@ class Member < ActiveRecord::Base end end - def notification - @notification ||= user.notification_settings.find_by(source: source) + def notification_setting + @notification_setting ||= user.notification_settings.find_by(source: source) end private diff --git a/app/models/notification.rb b/app/models/notification.rb deleted file mode 100644 index 3805bde88b0..00000000000 --- a/app/models/notification.rb +++ /dev/null @@ -1,13 +0,0 @@ -class Notification - attr_accessor :target - - delegate :disabled?, :participating?, :watch?, :global?, :mention?, to: :target - - def initialize(target) - @target = target - end - - def level - target.notification_level - end -end diff --git a/app/models/user.rb b/app/models/user.rb index f556dc5903d..af6b86bfa70 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -357,10 +357,6 @@ class User < ActiveRecord::Base "#{self.class.reference_prefix}#{username}" end - def notification - @notification ||= Notification.new(self) - end - def generate_password if self.force_random_password self.password = self.password_confirmation = Devise.friendly_token.first(8) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 23f211dfcd2..0928dda349e 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -329,25 +329,31 @@ class NotificationService # Remove users with disabled notifications from array # Also remove duplications and nil recipients def reject_muted_users(users, project = nil) - reject_users(users, :disabled?, project) + reject_users(users, :disabled, project) end # Remove users with notification level 'Mentioned' def reject_mention_users(users, project = nil) - reject_users(users, :mention?, project) + reject_users(users, :mention, project) end - # Reject users which method_name from notification object returns true. + # Reject users which has certain notification level # # Example: - # reject_users(users, :watch?, project) + # reject_users(users, :watch, project) # - def reject_users(users, method_name, project = nil) + def reject_users(users, level, project = nil) + level = level.to_s + + unless NotificationSetting.levels.keys.include?(level) + raise 'Invalid notification level' + end + users = users.to_a.compact.uniq users = users.reject(&:blocked?) users.reject do |user| - next user.notification.send(method_name) unless project + next user.notification_level == level unless project setting = user.notification_settings.find_by(source: project) @@ -356,13 +362,13 @@ class NotificationService end # reject users who globally set mention notification and has no setting per project/group - next user.notification.send(method_name) unless setting + next user.notification_level == level unless setting # reject users who set mention notification in project - next true if setting.send(method_name) + next true if setting.level == level # reject users who have mention level in project and disabled in global settings - setting.global? && user.notification.send(method_name) + setting.global? && user.notification_level == level end end From 630c86a7a36cee36ed6b9c93644a6cb51e2b3b23 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 29 Mar 2016 13:42:48 +0200 Subject: [PATCH 011/187] Add spec for user_id uniq in NotificationSetting model Signed-off-by: Dmitriy Zaporozhets --- spec/models/notification_setting_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb index f31b2a3cd6f..295081e9da1 100644 --- a/spec/models/notification_setting_spec.rb +++ b/spec/models/notification_setting_spec.rb @@ -7,10 +7,11 @@ RSpec.describe NotificationSetting, type: :model do end describe "Validation" do - subject { NotificationSetting.new } + subject { NotificationSetting.new(source_id: 1, source_type: 'Project') } it { is_expected.to validate_presence_of(:user) } it { is_expected.to validate_presence_of(:source) } it { is_expected.to validate_presence_of(:level) } + it { is_expected.to validate_uniqueness_of(:user_id).scoped_to([:source_id, :source_type]).with_message(/already exists in source/) } end end From 71e7b398431506c8bac2e8e6014b0f3891a41f95 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 29 Mar 2016 13:52:42 +0200 Subject: [PATCH 012/187] Refactor creating notification setting with defaults Signed-off-by: Dmitriy Zaporozhets --- app/controllers/projects_controller.rb | 7 +------ app/models/member.rb | 7 +------ app/models/notification_setting.rb | 11 +++++++++++ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 77122f59128..e2dc6309d26 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -102,12 +102,7 @@ class ProjectsController < Projects::ApplicationController @membership = @project.team.find_member(current_user.id) if @membership - @notification_setting = current_user.notification_settings.find_or_initialize_by(source: @project) - - unless @notification_setting.persisted? - @notification_setting.set_defaults - @notification_setting.save - end + @notification_setting = current_user.notification_settings.find_or_create_for(@project) end end diff --git a/app/models/member.rb b/app/models/member.rb index cbcc54c0250..747d0f16d8d 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -163,12 +163,7 @@ class Member < ActiveRecord::Base end def create_notification_setting - notification_setting = user.notification_settings.find_or_initialize_by(source: source) - - unless notification_setting.persisted? - notification_setting.set_defaults - notification_setting.save - end + user.notification_setting.find_or_create_for(source) end def notification_setting diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 287862a01bc..13a8995b036 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -15,6 +15,17 @@ class NotificationSetting < ActiveRecord::Base scope :for_groups, -> { where(source_type: 'Namespace') } scope :for_projects, -> { where(source_type: 'Project') } + def self.find_or_create_for(source) + setting = find_or_initialize_by(source: source) + + unless setting.persisted? + setting.set_defaults + setting.save + end + + setting + end + def set_defaults self.level = :global end From f8f68d6b8c5b5d85b4798257ae3ae4bf4ec8fadc Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 29 Mar 2016 14:03:23 +0200 Subject: [PATCH 013/187] Fix few bugs related to recent notifications refactoring Signed-off-by: Dmitriy Zaporozhets --- app/controllers/profiles/notifications_controller.rb | 1 - app/models/member.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index 6ca7537300f..0cca5d1e330 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -1,7 +1,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController def show @user = current_user - @notification = current_user.notification @group_notifications = current_user.notification_settings.for_groups @project_notifications = current_user.notification_settings.for_projects end diff --git a/app/models/member.rb b/app/models/member.rb index 747d0f16d8d..7d5af1d5c8a 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -163,7 +163,7 @@ class Member < ActiveRecord::Base end def create_notification_setting - user.notification_setting.find_or_create_for(source) + user.notification_settings.find_or_create_for(source) end def notification_setting From 5583197e091e8f75ad9c99a1bbc46e6a0b7279d4 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 29 Mar 2016 17:42:38 +0200 Subject: [PATCH 014/187] Create NotificationSettings object only when user change value in dropdown Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/project.js.coffee | 9 +++++++- .../notification_settings_controller.rb | 22 +++++++++++++++++++ app/controllers/projects_controller.rb | 3 ++- .../projects/buttons/_notifications.html.haml | 5 ++--- config/routes.rb | 1 + 5 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 app/controllers/projects/notification_settings_controller.rb diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index f171442d05a..07be85a32a5 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -38,12 +38,19 @@ class @Project e.preventDefault() notification_level = $(@).data 'notification-level' label = $(@).data 'notification-title' - $('#notification_level').val(notification_level) + $('#notification_setting_level').val(notification_level) $('#notification-form').submit() $('#notifications-button').empty().append("" + label + "") $(@).parents('ul').find('li.active').removeClass 'active' $(@).parent().addClass 'active' + $('#notification-form').on 'ajax:success', (e, data) -> + if data.saved + new Flash("Notification settings saved", "notice") + else + new Flash("Failed to save new settings", "alert") + + @projectSelectDropdown() projectSelectDropdown: -> diff --git a/app/controllers/projects/notification_settings_controller.rb b/app/controllers/projects/notification_settings_controller.rb new file mode 100644 index 00000000000..3ecf63d107f --- /dev/null +++ b/app/controllers/projects/notification_settings_controller.rb @@ -0,0 +1,22 @@ +class Projects::NotificationSettingsController < Projects::ApplicationController + def create + notification_setting = project.notification_settings.new(notification_setting_params) + notification_setting.user = current_user + saved = notification_setting.save + + render json: { saved: saved } + end + + def update + notification_setting = project.notification_settings.where(user_id: current_user).find(params[:id]) + saved = notification_setting.update_attributes(notification_setting_params) + + render json: { saved: saved } + end + + private + + def notification_setting_params + params.require(:notification_setting).permit(:level) + end +end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index e2dc6309d26..ec5318f2d2c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -102,7 +102,8 @@ class ProjectsController < Projects::ApplicationController @membership = @project.team.find_member(current_user.id) if @membership - @notification_setting = current_user.notification_settings.find_or_create_for(@project) + @notification_setting = current_user.notification_settings.find_or_initialize_by(source: @project) + @notification_setting.set_defaults unless @notification_setting.persisted? end end diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index 4b8a10f0819..2b9d8f2ac81 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -1,7 +1,6 @@ - if @notification_setting - = form_tag profile_notifications_path, method: :put, remote: true, class: 'inline', id: 'notification-form' do - = hidden_field_tag :notification_id, @notification_setting.id - = hidden_field_tag :notification_level, @notification_setting.level + = form_for [@project.namespace.becomes(Namespace), @project, @notification_setting], remote: true, html: { class: 'inline', id: 'notification-form' } do |f| + = f.hidden_field :level %span.dropdown %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"} = icon('bell') diff --git a/config/routes.rb b/config/routes.rb index 6bf22fb4456..c665cefbb57 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -606,6 +606,7 @@ Rails.application.routes.draw do resources :forks, only: [:index, :new, :create] resource :import, only: [:new, :create, :show] + resources :notification_settings, only: [:create, :update] resources :refs, only: [] do collection do From 729fe42bff474535c9eebb0b73974a79756372b8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 29 Mar 2016 17:56:12 +0200 Subject: [PATCH 015/187] Improve project notification settings explanation Signed-off-by: Dmitriy Zaporozhets --- app/views/profiles/notifications/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index f6900f61b2d..e9c8ae28544 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -65,7 +65,7 @@ %h5 Projects (#{@project_notifications.count}) %p.account-well - To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group. + To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there. .append-bottom-default %ul.bordered-list - @project_notifications.each do |setting| From 26631f9981a826ebe4aeba726e9be2b813d2c5c5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 29 Mar 2016 18:59:03 +0200 Subject: [PATCH 016/187] Change how notification settings in profile are rendered and updated Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/profile.js.coffee | 7 +- .../notification_settings_controller.rb | 14 ++++ .../profiles/notifications_controller.rb | 28 ++------ .../notifications/_group_settings.html.haml | 13 ++++ .../notifications/_project_settings.html.haml | 13 ++++ .../notifications/_settings.html.haml | 17 ----- .../profiles/notifications/show.html.haml | 72 ++++++++++--------- .../profiles/notifications/update.js.haml | 6 -- config/routes.rb | 1 + 9 files changed, 89 insertions(+), 82 deletions(-) create mode 100644 app/controllers/groups/notification_settings_controller.rb create mode 100644 app/views/profiles/notifications/_group_settings.html.haml create mode 100644 app/views/profiles/notifications/_project_settings.html.haml delete mode 100644 app/views/profiles/notifications/_settings.html.haml delete mode 100644 app/views/profiles/notifications/update.js.haml diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index ae87c6c4e40..f4a2562885d 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -18,8 +18,11 @@ class @Profile $(this).find('.btn-save').enable() $(this).find('.loading-gif').hide() - $('.update-notifications').on 'ajax:complete', -> - $(this).find('.btn-save').enable() + $('.update-notifications').on 'ajax:success', (e, data) -> + if data.saved + new Flash("Notification settings saved", "notice") + else + new Flash("Failed to save new settings", "alert") @bindEvents() diff --git a/app/controllers/groups/notification_settings_controller.rb b/app/controllers/groups/notification_settings_controller.rb new file mode 100644 index 00000000000..78e43c83aba --- /dev/null +++ b/app/controllers/groups/notification_settings_controller.rb @@ -0,0 +1,14 @@ +class Groups::NotificationSettingsController < Groups::ApplicationController + def update + notification_setting = group.notification_settings.where(user_id: current_user).find(params[:id]) + saved = notification_setting.update_attributes(notification_setting_params) + + render json: { saved: saved } + end + + private + + def notification_setting_params + params.require(:notification_setting).permit(:level) + end +end diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index 0cca5d1e330..18ee55c839a 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -6,29 +6,13 @@ class Profiles::NotificationsController < Profiles::ApplicationController end def update - type = params[:notification_type] - - @saved = if type == 'global' - current_user.update_attributes(user_params) - else - notification_setting = current_user.notification_settings.find(params[:notification_id]) - notification_setting.level = params[:notification_level] - notification_setting.save - end - - respond_to do |format| - format.html do - if @saved - flash[:notice] = "Notification settings saved" - else - flash[:alert] = "Failed to save new settings" - end - - redirect_back_or_default(default: profile_notifications_path) - end - - format.js + if current_user.update_attributes(user_params) + flash[:notice] = "Notification settings saved" + else + flash[:alert] = "Failed to save new settings" end + + redirect_back_or_default(default: profile_notifications_path) end def user_params diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml new file mode 100644 index 00000000000..89ae7ffda2b --- /dev/null +++ b/app/views/profiles/notifications/_group_settings.html.haml @@ -0,0 +1,13 @@ +%li.notification-list-item + %span.notification.fa.fa-holder.append-right-5 + - if setting.global? + = notification_icon(current_user.notification_level) + - else + = notification_icon(setting.level) + + %span.str-truncated + = link_to group.name, group_path(group) + + .pull-right + = form_for [group, setting], remote: true, html: { class: 'update-notifications' } do |f| + = f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit' diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml new file mode 100644 index 00000000000..17c097154da --- /dev/null +++ b/app/views/profiles/notifications/_project_settings.html.haml @@ -0,0 +1,13 @@ +%li.notification-list-item + %span.notification.fa.fa-holder.append-right-5 + - if setting.global? + = notification_icon(current_user.notification_level) + - else + = notification_icon(setting.level) + + %span.str-truncated + = link_to_project(project) + + .pull-right + = form_for [project.namespace.becomes(Namespace), project, setting], remote: true, html: { class: 'update-notifications' } do |f| + = f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit' diff --git a/app/views/profiles/notifications/_settings.html.haml b/app/views/profiles/notifications/_settings.html.haml deleted file mode 100644 index c32de0b9925..00000000000 --- a/app/views/profiles/notifications/_settings.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -%li.notification-list-item - %span.notification.fa.fa-holder.append-right-5 - - if setting.global? - = notification_icon(current_user.notification_level) - - else - = notification_icon(setting.level) - - %span.str-truncated - - if setting.source.kind_of? Project - = link_to_project(setting.source) - - else - = link_to setting.source.name, group_path(setting.source) - .pull-right - = form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do - = hidden_field_tag :notification_id, setting.id - = hidden_field_tag :notification_level, setting.level - = select_tag :notification_level, options_for_select(User.notification_levels.keys, setting.level), class: 'form-control trigger-submit' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index e9c8ae28544..a2a505c082b 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -1,8 +1,8 @@ - page_title "Notifications" - header_title page_title, profile_notifications_path -= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f| - -if @user.errors.any? +%div + - if @user.errors.any? %div.alert.alert-danger %ul - @user.errors.full_messages.each do |msg| @@ -20,48 +20,50 @@ .col-lg-9 %h5 Global notification settings - .form-group - = f.label :notification_email, class: "label-light" - = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2" - .form-group - = f.label :notification_level, class: 'label-light' - .radio - = f.label :notification_level, value: :disabled do - = f.radio_button :notification_level, :disabled - .level-title - Disabled - %p You will not get any notifications via email - .radio - = f.label :notification_level, value: :mention do - = f.radio_button :notification_level, :mention - .level-title - On Mention - %p You will receive notifications only for comments in which you were @mentioned + = form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f| + .form-group + = f.label :notification_email, class: "label-light" + = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2" + .form-group + = f.label :notification_level, class: 'label-light' + .radio + = f.label :notification_level, value: :disabled do + = f.radio_button :notification_level, :disabled + .level-title + Disabled + %p You will not get any notifications via email - .radio - = f.label :notification_level, value: :participating do - = f.radio_button :notification_level, :participating - .level-title - Participating - %p You will only receive notifications from related resources (e.g. from your commits or assigned issues) + .radio + = f.label :notification_level, value: :mention do + = f.radio_button :notification_level, :mention + .level-title + On Mention + %p You will receive notifications only for comments in which you were @mentioned - .radio - = f.label :notification_level, value: :watch do - = f.radio_button :notification_level, :watch - .level-title - Watch - %p You will receive notifications for any activity + .radio + = f.label :notification_level, value: :participating do + = f.radio_button :notification_level, :participating + .level-title + Participating + %p You will only receive notifications from related resources (e.g. from your commits or assigned issues) - .prepend-top-default - = f.submit 'Update settings', class: "btn btn-create" + .radio + = f.label :notification_level, value: :watch do + = f.radio_button :notification_level, :watch + .level-title + Watch + %p You will receive notifications for any activity + + .prepend-top-default + = f.submit 'Update settings', class: "btn btn-create" %hr %h5 Groups (#{@group_notifications.count}) %div %ul.bordered-list - @group_notifications.each do |setting| - = render 'settings', setting: setting + = render 'group_settings', setting: setting, group: setting.source %h5 Projects (#{@project_notifications.count}) %p.account-well @@ -69,4 +71,4 @@ .append-bottom-default %ul.bordered-list - @project_notifications.each do |setting| - = render 'settings', setting: setting + = render 'project_settings', setting: setting, project: setting.source diff --git a/app/views/profiles/notifications/update.js.haml b/app/views/profiles/notifications/update.js.haml deleted file mode 100644 index 84c6ab25599..00000000000 --- a/app/views/profiles/notifications/update.js.haml +++ /dev/null @@ -1,6 +0,0 @@ -- if @saved - :plain - new Flash("Notification settings saved", "notice") -- else - :plain - new Flash("Failed to save new settings", "alert") diff --git a/config/routes.rb b/config/routes.rb index c665cefbb57..7f03fbf6af9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -406,6 +406,7 @@ Rails.application.routes.draw do resource :avatar, only: [:destroy] resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] + resources :notification_settings, only: [:update] end end From 949431fa02da18257c8b7c7a24c03faa04c02c5e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 29 Mar 2016 22:19:00 +0200 Subject: [PATCH 017/187] Update API to use notification_level from notification_setting Signed-off-by: Dmitriy Zaporozhets --- lib/api/entities.rb | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index f686c568bee..f414c1f9885 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -255,14 +255,19 @@ module API expose :id, :path, :kind end - class ProjectAccess < Grape::Entity + class Member < Grape::Entity expose :access_level - expose :notification_level + expose :notification_level do |member, options| + if member.notification_setting + NotificationSetting.levels[member.notification_setting.level] + end + end end - class GroupAccess < Grape::Entity - expose :access_level - expose :notification_level + class ProjectAccess < Member + end + + class GroupAccess < Member end class ProjectService < Grape::Entity From 49f9873ce2a88fb38f23f7eb09471e8b58aebe1d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 29 Mar 2016 22:20:59 +0200 Subject: [PATCH 018/187] Add changelog item Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 2cd06d90257..88edf1e8484 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.7.0 (unreleased) - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) - Gracefully handle notes on deleted commits in merge requests (Stan Hu) + - Decouple membership and notifications v 8.6.2 (unreleased) - Comments on confidential issues don't show up in activity feed to non-members From 065faac3a390f29b57db5261e9eab4efa076554c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 30 Mar 2016 10:32:19 +0200 Subject: [PATCH 019/187] Test changing notification settings per project fron notificaitons page Signed-off-by: Dmitriy Zaporozhets --- features/profile/notifications.feature | 6 ++++++ features/steps/profile/notifications.rb | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/features/profile/notifications.feature b/features/profile/notifications.feature index 55997d44dec..ef8743932f5 100644 --- a/features/profile/notifications.feature +++ b/features/profile/notifications.feature @@ -7,3 +7,9 @@ Feature: Profile Notifications Scenario: I visit notifications tab When I visit profile notifications page Then I should see global notifications settings + + @javascript + Scenario: I edit Project Notifications + Given I visit profile notifications page + When I select Mention setting from dropdown + Then I should see Notification saved message diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb index 447ea6d9d10..a96f35ada51 100644 --- a/features/steps/profile/notifications.rb +++ b/features/steps/profile/notifications.rb @@ -9,4 +9,14 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps step 'I should see global notifications settings' do expect(page).to have_content "Notifications" end + + step 'I select Mention setting from dropdown' do + select 'mention', from: 'notification_setting_level' + end + + step 'I should see Notification saved message' do + page.within '.flash-container' do + expect(page).to have_content 'Notification settings saved' + end + end end From 1a293a43847b30d4357116f830a1b22d370c4a6f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 30 Mar 2016 10:38:03 +0200 Subject: [PATCH 020/187] Update migration comment Signed-off-by: Dmitriy Zaporozhets --- .../20160328115649_migrate_new_notification_setting.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/db/migrate/20160328115649_migrate_new_notification_setting.rb b/db/migrate/20160328115649_migrate_new_notification_setting.rb index aff866b5f46..6a68890f5b1 100644 --- a/db/migrate/20160328115649_migrate_new_notification_setting.rb +++ b/db/migrate/20160328115649_migrate_new_notification_setting.rb @@ -1,6 +1,10 @@ # This migration will create one row of NotificationSetting for each Member row -# It can take long time on big instances. Its unclear yet if this migration can be done online. -# This comment should be updated by @dzaporozhets before 8.7 release. If not - please ask him to do so. +# It can take long time on big instances. +# +# This migration can be done online but with following effects: +# - during migration some users will receive notifications based on their global settings (project/group settings will be ignored) +# - its possible to get duplicate records for notification settings since we don't create uniq index yet +# class MigrateNewNotificationSetting < ActiveRecord::Migration def up timestamp = Time.now From bf9526739b5c90790907c1d8b9410dd339e3d395 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 4 Apr 2016 17:23:43 +0200 Subject: [PATCH 021/187] Rebase repo check MR --- app/controllers/admin/projects_controller.rb | 24 +++++++++- app/mailers/repo_check_mailer.rb | 16 +++++++ app/views/admin/logs/show.html.haml | 3 +- app/views/admin/projects/index.html.haml | 15 +++++- app/views/admin/projects/show.html.haml | 32 +++++++++++++ app/views/repo_check_mailer/notify.html.haml | 5 ++ app/views/repo_check_mailer/notify.text.haml | 3 ++ app/workers/admin_email_worker.rb | 12 +++++ app/workers/repo_check_worker.rb | 46 +++++++++++++++++++ app/workers/single_repo_check_worker.rb | 34 ++++++++++++++ config/gitlab.yml.example | 7 +++ config/initializers/1_settings.rb | 7 ++- config/routes.rb | 5 ++ .../20160315135439_project_add_repo_check.rb | 6 +++ db/schema.rb | 2 + doc/administration/repo_checks.md | 39 ++++++++++++++++ lib/gitlab/repo_check_logger.rb | 7 +++ spec/features/admin/admin_projects_spec.rb | 37 ++++++++++++++- spec/mailers/repo_check_mailer_spec.rb | 21 +++++++++ spec/workers/repo_check_worker_spec.rb | 31 +++++++++++++ 20 files changed, 346 insertions(+), 6 deletions(-) create mode 100644 app/mailers/repo_check_mailer.rb create mode 100644 app/views/repo_check_mailer/notify.html.haml create mode 100644 app/views/repo_check_mailer/notify.text.haml create mode 100644 app/workers/admin_email_worker.rb create mode 100644 app/workers/repo_check_worker.rb create mode 100644 app/workers/single_repo_check_worker.rb create mode 100644 db/migrate/20160315135439_project_add_repo_check.rb create mode 100644 doc/administration/repo_checks.md create mode 100644 lib/gitlab/repo_check_logger.rb create mode 100644 spec/mailers/repo_check_mailer_spec.rb create mode 100644 spec/workers/repo_check_worker_spec.rb diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 4089091d569..b8c276fb1bb 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -1,5 +1,5 @@ class Admin::ProjectsController < Admin::ApplicationController - before_action :project, only: [:show, :transfer] + before_action :project, only: [:show, :transfer, :repo_check] before_action :group, only: [:show, :transfer] def index @@ -8,6 +8,7 @@ class Admin::ProjectsController < Admin::ApplicationController @projects = @projects.where("visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? @projects = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? + @projects = @projects.where(last_repo_check_failed: true) if params[:last_repo_check_failed].present? @projects = @projects.non_archived unless params[:with_archived].present? @projects = @projects.search(params[:name]) if params[:name].present? @projects = @projects.sort(@sort = params[:sort]) @@ -30,6 +31,27 @@ class Admin::ProjectsController < Admin::ApplicationController redirect_to admin_namespace_project_path(@project.namespace, @project) end + def repo_check + SingleRepoCheckWorker.perform_async(@project.id) + + redirect_to( + admin_namespace_project_path(@project.namespace, @project), + notice: 'Repo check was triggered' + ) + end + + def clear_repo_check_states + Project.update_all( + last_repo_check_failed: false, + last_repo_check_at: nil + ) + + redirect_to( + admin_namespaces_projects_path, + notice: 'All project repo check states were cleared' + ) + end + protected def project diff --git a/app/mailers/repo_check_mailer.rb b/app/mailers/repo_check_mailer.rb new file mode 100644 index 00000000000..d98533f120d --- /dev/null +++ b/app/mailers/repo_check_mailer.rb @@ -0,0 +1,16 @@ +class RepoCheckMailer < BaseMailer + include ActionView::Helpers::TextHelper + + def notify(failed_count) + if failed_count == 1 + @message = "One project failed its last repository check" + else + @message = "#{failed_count} projects failed their last repository check" + end + + mail( + to: User.admins.pluck(:email), + subject: @message + ) + end +end diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index af9fdeb0734..abcc93f4046 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -1,6 +1,7 @@ - page_title "Logs" - loggers = [Gitlab::GitLogger, Gitlab::AppLogger, - Gitlab::ProductionLogger, Gitlab::SidekiqLogger] + Gitlab::ProductionLogger, Gitlab::SidekiqLogger, + Gitlab::RepoCheckLogger] %ul.nav-links.log-tabs - loggers.each do |klass| %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index d39c0f44031..ed360f2f012 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -3,7 +3,7 @@ .row.prepend-top-default %aside.col-md-3 - .admin-filter + .panel.admin-filter = form_tag admin_namespaces_projects_path, method: :get, class: '' do .form-group = label_tag :name, 'Name:' @@ -38,11 +38,22 @@ %span.descr = visibility_level_icon(level) = label - %hr + %fieldset + %strong Problems + .checkbox + = label_tag :last_repo_check_failed do + = check_box_tag :last_repo_check_failed, 1, params[:last_repo_check_failed] + %span Last repo check failed + = hidden_field_tag :sort, params[:sort] = button_tag "Search", class: "btn submit btn-primary" = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel" + .panel.panel-default.repo-check-states + .panel-heading + Repo check states + .panel-body + = link_to 'Clear all', clear_repo_check_states_admin_namespace_projects_path(0), data: { confirm: 'This will clear repo check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" %section.col-md-9 .panel.panel-default .panel-heading diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index c638c32a654..a7a3f6349ef 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -5,6 +5,14 @@ %i.fa.fa-pencil-square-o Edit %hr +- if @project.last_repo_check_failed? + .row + .col-md-12 + .panel + .panel-heading.alert.alert-danger + Last repo check failed. See + = link_to 'repocheck.log', admin_logs_path + for error messages. .row .col-md-6 .panel.panel-default @@ -95,6 +103,30 @@ .col-sm-offset-2.col-sm-10 = f.submit 'Transfer', class: 'btn btn-primary' + .panel.panel-default.repo-check + .panel-heading + Repo check + .panel-body + = form_for @project, url: repo_check_admin_namespace_project_path(@project.namespace, @project), method: :post do |f| + .form-group + - if @project.last_repo_check_at.nil? + This repository has never been checked. + - else + This repository was last checked + = @project.last_repo_check_at.to_s(:medium) + '.' + The check + - if @project.last_repo_check_failed? + = succeed '.' do + %strong.cred failed + See + = link_to 'repocheck.log', admin_logs_path + for error messages. + - else + passed. + + .form-group + = f.submit 'Trigger repo check', class: 'btn btn-primary' + .col-md-6 - if @group .panel.panel-default diff --git a/app/views/repo_check_mailer/notify.html.haml b/app/views/repo_check_mailer/notify.html.haml new file mode 100644 index 00000000000..ef0016f9d3e --- /dev/null +++ b/app/views/repo_check_mailer/notify.html.haml @@ -0,0 +1,5 @@ +%p + #{@message}. + +%p + = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repo_check_failed: 1) diff --git a/app/views/repo_check_mailer/notify.text.haml b/app/views/repo_check_mailer/notify.text.haml new file mode 100644 index 00000000000..bdf8c2ad675 --- /dev/null +++ b/app/views/repo_check_mailer/notify.text.haml @@ -0,0 +1,3 @@ +#{@message}. +\ +View details: #{admin_namespaces_projects_url(last_repo_check_failed: 1)} diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb new file mode 100644 index 00000000000..fcccb9ea669 --- /dev/null +++ b/app/workers/admin_email_worker.rb @@ -0,0 +1,12 @@ +class AdminEmailWorker + include Sidekiq::Worker + + sidekiq_options retry: false # this job auto-repeats via sidekiq-cron + + def perform + repo_check_failed_count = Project.where(last_repo_check_failed: true).count + return if repo_check_failed_count.zero? + + RepoCheckMailer.notify(repo_check_failed_count).deliver_now + end +end diff --git a/app/workers/repo_check_worker.rb b/app/workers/repo_check_worker.rb new file mode 100644 index 00000000000..9be795309e0 --- /dev/null +++ b/app/workers/repo_check_worker.rb @@ -0,0 +1,46 @@ +class RepoCheckWorker + include Sidekiq::Worker + + RUN_TIME = 3600 + + sidekiq_options retry: false + + def perform + start = Time.now + + # This loop will break after a little more than one hour ('a little + # more' because `git fsck` may take a few minutes), or if it runs out of + # projects to check. By default sidekiq-cron will start a new + # RepoCheckWorker each hour so that as long as there are repositories to + # check, only one (or two) will be checked at a time. + project_ids.each do |project_id| + break if Time.now - start >= RUN_TIME + + next if !try_obtain_lease(project_id) + + SingleRepoCheckWorker.new.perform(project_id) + end + end + + private + + # In an ideal world we would use Project.where(...).find_each. + # Unfortunately, calling 'find_each' drops the 'where', so we must build + # an array of IDs instead. + def project_ids + limit = 10_000 + never_checked_projects = Project.where('last_repo_check_at IS NULL').limit(limit). + pluck(:id) + old_check_projects = Project.where('last_repo_check_at < ?', 1.week.ago). + reorder('last_repo_check_at ASC').limit(limit).pluck(:id) + never_checked_projects + old_check_projects + end + + def try_obtain_lease(id) + lease = Gitlab::ExclusiveLease.new( + "project_repo_check:#{id}", + timeout: RUN_TIME + ) + lease.try_obtain + end +end diff --git a/app/workers/single_repo_check_worker.rb b/app/workers/single_repo_check_worker.rb new file mode 100644 index 00000000000..f8b245247c5 --- /dev/null +++ b/app/workers/single_repo_check_worker.rb @@ -0,0 +1,34 @@ +class SingleRepoCheckWorker + include Sidekiq::Worker + + sidekiq_options retry: false + + def perform(project_id) + project = Project.find(project_id) + update(project, success: check(project)) + end + + private + + def check(project) + [project.repository.path_to_repo, project.wiki.wiki.path].all? do |path| + git_fsck(path) + end + end + + def git_fsck(path) + cmd = %W(nice git --git-dir=#{path} fsck) + output, status = Gitlab::Popen.popen(cmd) + return true if status.zero? + + Gitlab::RepoCheckLogger.error("command failed: #{cmd.join(' ')}\n#{output}") + false + end + + def update(project, success:) + project.update_columns( + last_repo_check_failed: !success, + last_repo_check_at: Time.now, + ) + end +end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index fb1c3476f65..cf20fc9c63a 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -155,6 +155,13 @@ production: &base # Flag stuck CI builds as failed stuck_ci_builds_worker: cron: "0 0 * * *" + # Periodically run 'git fsck' on all repositories. If started more than + # once per hour you will have concurrent 'git fsck' jobs. + repo_check_worker: + cron: "20 * * * *" + # Send admin emails once a day + admin_email_worker: + cron: "0 0 * * *" # diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 2b989015279..8240978ef06 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -239,7 +239,12 @@ Settings['cron_jobs'] ||= Settingslogic.new({}) Settings.cron_jobs['stuck_ci_builds_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['stuck_ci_builds_worker']['cron'] ||= '0 0 * * *' Settings.cron_jobs['stuck_ci_builds_worker']['job_class'] = 'StuckCiBuildsWorker' - +Settings.cron_jobs['repo_check_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['repo_check_worker']['cron'] ||= '20 * * * *' +Settings.cron_jobs['repo_check_worker']['job_class'] = 'RepoCheckWorker' +Settings.cron_jobs['admin_email_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['admin_email_worker']['cron'] ||= '0 0 * * *' +Settings.cron_jobs['admin_email_worker']['job_class'] = 'AdminEmailWorker' # # GitLab Shell diff --git a/config/routes.rb b/config/routes.rb index 6bf22fb4456..fad8600b77d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -264,6 +264,11 @@ Rails.application.routes.draw do member do put :transfer + post :repo_check + end + + collection do + put :clear_repo_check_states end resources :runner_projects diff --git a/db/migrate/20160315135439_project_add_repo_check.rb b/db/migrate/20160315135439_project_add_repo_check.rb new file mode 100644 index 00000000000..ba7ddc9ecb4 --- /dev/null +++ b/db/migrate/20160315135439_project_add_repo_check.rb @@ -0,0 +1,6 @@ +class ProjectAddRepoCheck < ActiveRecord::Migration + def change + add_column :projects, :last_repo_check_failed, :boolean, default: false + add_column :projects, :last_repo_check_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index e63e22ce864..d2c183f968b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -732,6 +732,8 @@ ActiveRecord::Schema.define(version: 20160331133914) do t.boolean "public_builds", default: true, null: false t.string "main_language" t.integer "pushes_since_gc", default: 0 + t.boolean "last_repo_check_failed", default: false + t.datetime "last_repo_check_at" end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree diff --git a/doc/administration/repo_checks.md b/doc/administration/repo_checks.md new file mode 100644 index 00000000000..9c2c01594e8 --- /dev/null +++ b/doc/administration/repo_checks.md @@ -0,0 +1,39 @@ +# Repo checks + +_**Note:** This feature was [introduced][ce-3232] in GitLab 8.7_ + +--- + +Git has a built-in mechanism [git fsck][git-fsck] to verify the +integrity of all data commited to a repository. GitLab administrators can +trigger such a check for a project via the admin panel. The checks run +asynchronously so it may take a few minutes before the check result is +visible on the project admin page. If the checks failed you can see their +output on the admin log page under 'repocheck.log'. + +## Periodical checks + +GitLab periodically runs a repo check on all project repositories and +wiki repositories in order to detect data corruption problems. A +project will be checked no more than once per week. If any projects +fail their repo checks all GitLab administrators will receive an email +notification of the situation. This notification is sent out no more +than once a day. + + +## What to do if a check failed + +If the repo check fails for some repository you should look up the error +in repocheck.log (in the admin panel or on disk; see +`/var/log/gitlab/gitlab-rails` for Omnibus installations or +`/home/git/gitlab/log` for installations from source). Once you have +resolved the issue use the admin panel to trigger a new repo check on +the project. This will clear the 'check failed' state. + +If for some reason the periodical repo check caused a lot of false +alarms you can choose to clear ALL repo check states from the admin +project index page. + +--- +[ce-3232]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3232 "Auto git fsck" +[git-fsck]: https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html "git fsck documentation" \ No newline at end of file diff --git a/lib/gitlab/repo_check_logger.rb b/lib/gitlab/repo_check_logger.rb new file mode 100644 index 00000000000..9409f68722c --- /dev/null +++ b/lib/gitlab/repo_check_logger.rb @@ -0,0 +1,7 @@ +module Gitlab + class RepoCheckLogger < Gitlab::Logger + def self.file_name_noext + 'repocheck' + end + end +end diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index 101d955d693..e3991d48ed6 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' +require 'rails_helper' -describe "Admin::Projects", feature: true do +describe "Admin Projects", feature: true do before do @project = create(:project) login_as :admin @@ -31,4 +32,38 @@ describe "Admin::Projects", feature: true do expect(page).to have_content(@project.name) end end + + feature 'repo checks' do + scenario 'trigger repo check' do + visit_admin_project_page + + page.within('.repo-check') do + click_button 'Trigger repo check' + end + + expect(page).to have_content('Repo check was triggered') + end + + scenario 'see failed repo check' do + @project.update_column(:last_repo_check_failed, true) + visit_admin_project_page + + expect(page).to have_content('Last repo check failed') + end + + scenario 'clear repo checks', js: true do + @project.update_column(:last_repo_check_failed, true) + visit admin_namespaces_projects_path + + page.within('.repo-check-states') do + click_link 'Clear all' # pop-up should be auto confirmed + end + + expect(@project.reload.last_repo_check_failed).to eq(false) + end + end + + def visit_admin_project_page + visit admin_namespace_project_path(@project.namespace, @project) + end end diff --git a/spec/mailers/repo_check_mailer_spec.rb b/spec/mailers/repo_check_mailer_spec.rb new file mode 100644 index 00000000000..d49a6ae0c05 --- /dev/null +++ b/spec/mailers/repo_check_mailer_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe RepoCheckMailer do + include EmailSpec::Matchers + + describe '.notify' do + it 'emails all admins' do + admins = 3.times.map { create(:admin) } + + mail = described_class.notify(1) + + expect(mail).to deliver_to admins.map(&:email) + end + + it 'mentions the number of failed checks' do + mail = described_class.notify(3) + + expect(mail).to have_subject '3 projects failed their last repository check' + end + end +end diff --git a/spec/workers/repo_check_worker_spec.rb b/spec/workers/repo_check_worker_spec.rb new file mode 100644 index 00000000000..7ef3eba9ac5 --- /dev/null +++ b/spec/workers/repo_check_worker_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe RepoCheckWorker do + subject { RepoCheckWorker.new } + + it 'prefers projects that have never been checked' do + projects = 3.times.map { create(:project) } + projects[0].update_column(:last_repo_check_at, 1.month.ago) + projects[2].update_column(:last_repo_check_at, 3.weeks.ago) + + expect(subject.perform).to eq(projects.values_at(1, 0, 2).map(&:id)) + end + + it 'sorts projects by last_repo_check_at' do + projects = 3.times.map { create(:project) } + projects[0].update_column(:last_repo_check_at, 2.weeks.ago) + projects[1].update_column(:last_repo_check_at, 1.month.ago) + projects[2].update_column(:last_repo_check_at, 3.weeks.ago) + + expect(subject.perform).to eq(projects.values_at(1, 2, 0).map(&:id)) + end + + it 'excludes projects that were checked recently' do + projects = 3.times.map { create(:project) } + projects[0].update_column(:last_repo_check_at, 2.days.ago) + projects[1].update_column(:last_repo_check_at, 1.month.ago) + projects[2].update_column(:last_repo_check_at, 3.days.ago) + + expect(subject.perform).to eq([projects[1].id]) + end +end From e3558ed67e7e829fe5148c3fb2fe80ed045fe1b4 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 6 Apr 2016 12:26:29 +0200 Subject: [PATCH 022/187] Document how to disable repo checks --- doc/administration/repo_checks.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/doc/administration/repo_checks.md b/doc/administration/repo_checks.md index 9c2c01594e8..81087f3ac1c 100644 --- a/doc/administration/repo_checks.md +++ b/doc/administration/repo_checks.md @@ -20,6 +20,22 @@ fail their repo checks all GitLab administrators will receive an email notification of the situation. This notification is sent out no more than once a day. +## Disabling periodic checks + +You can disable the periodic checks by giving them an empty cron +schedule in gitlab.yml. + +``` +# For omnibus installations, in /etc/gitlab/gitlab.rb: +gitlab_rails['cron_jobs_repo_check_worker_cron'] = '' +``` + +``` +# For installations from source, in config/gitlab.yml: + cron_jobs: + repo_check_worker: + cron: "" +``` ## What to do if a check failed From 5cf56e56470e695b10db02dff70d0f0b50060518 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 6 Apr 2016 13:47:05 +0200 Subject: [PATCH 023/187] Rename almost all the things --- app/controllers/admin/projects_controller.rb | 18 +++++------ ...k_mailer.rb => repository_check_mailer.rb} | 2 +- app/views/admin/logs/show.html.haml | 2 +- app/views/admin/projects/index.html.haml | 12 +++---- app/views/admin/projects/show.html.haml | 18 +++++------ app/views/repo_check_mailer/notify.text.haml | 3 -- .../notify.html.haml | 2 +- .../repository_check_mailer/notify.text.haml | 3 ++ app/workers/admin_email_worker.rb | 6 ++-- ...k_worker.rb => repository_check_worker.rb} | 14 ++++----- ...r.rb => single_repository_check_worker.rb} | 8 ++--- config/gitlab.yml.example | 2 +- config/initializers/1_settings.rb | 6 ++-- config/routes.rb | 4 +-- .../20160315135439_project_add_repo_check.rb | 6 ---- ...0315135439_project_add_repository_check.rb | 6 ++++ db/schema.rb | 4 +-- .../{repo_checks.md => repository_checks.md} | 18 +++++------ ...k_logger.rb => repository_check_logger.rb} | 2 +- spec/features/admin/admin_projects_spec.rb | 24 +++++++------- ...pec.rb => repository_check_mailer_spec.rb} | 2 +- spec/workers/repo_check_worker_spec.rb | 31 ------------------- spec/workers/repository_check_worker_spec.rb | 31 +++++++++++++++++++ 23 files changed, 112 insertions(+), 112 deletions(-) rename app/mailers/{repo_check_mailer.rb => repository_check_mailer.rb} (89%) delete mode 100644 app/views/repo_check_mailer/notify.text.haml rename app/views/{repo_check_mailer => repository_check_mailer}/notify.html.haml (56%) create mode 100644 app/views/repository_check_mailer/notify.text.haml rename app/workers/{repo_check_worker.rb => repository_check_worker.rb} (67%) rename app/workers/{single_repo_check_worker.rb => single_repository_check_worker.rb} (72%) delete mode 100644 db/migrate/20160315135439_project_add_repo_check.rb create mode 100644 db/migrate/20160315135439_project_add_repository_check.rb rename doc/administration/{repo_checks.md => repository_checks.md} (70%) rename lib/gitlab/{repo_check_logger.rb => repository_check_logger.rb} (62%) rename spec/mailers/{repo_check_mailer_spec.rb => repository_check_mailer_spec.rb} (92%) delete mode 100644 spec/workers/repo_check_worker_spec.rb create mode 100644 spec/workers/repository_check_worker_spec.rb diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index b8c276fb1bb..01257a68616 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -1,5 +1,5 @@ class Admin::ProjectsController < Admin::ApplicationController - before_action :project, only: [:show, :transfer, :repo_check] + before_action :project, only: [:show, :transfer, :repository_check] before_action :group, only: [:show, :transfer] def index @@ -8,7 +8,7 @@ class Admin::ProjectsController < Admin::ApplicationController @projects = @projects.where("visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? @projects = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? - @projects = @projects.where(last_repo_check_failed: true) if params[:last_repo_check_failed].present? + @projects = @projects.where(last_repository_check_failed: true) if params[:last_repository_check_failed].present? @projects = @projects.non_archived unless params[:with_archived].present? @projects = @projects.search(params[:name]) if params[:name].present? @projects = @projects.sort(@sort = params[:sort]) @@ -31,24 +31,24 @@ class Admin::ProjectsController < Admin::ApplicationController redirect_to admin_namespace_project_path(@project.namespace, @project) end - def repo_check - SingleRepoCheckWorker.perform_async(@project.id) + def repository_check + SingleRepositoryCheckWorker.perform_async(@project.id) redirect_to( admin_namespace_project_path(@project.namespace, @project), - notice: 'Repo check was triggered' + notice: 'Repository check was triggered' ) end - def clear_repo_check_states + def clear_repository_check_states Project.update_all( - last_repo_check_failed: false, - last_repo_check_at: nil + last_repository_check_failed: false, + last_repository_check_at: nil ) redirect_to( admin_namespaces_projects_path, - notice: 'All project repo check states were cleared' + notice: 'All project states were cleared' ) end diff --git a/app/mailers/repo_check_mailer.rb b/app/mailers/repository_check_mailer.rb similarity index 89% rename from app/mailers/repo_check_mailer.rb rename to app/mailers/repository_check_mailer.rb index d98533f120d..994054c8769 100644 --- a/app/mailers/repo_check_mailer.rb +++ b/app/mailers/repository_check_mailer.rb @@ -1,4 +1,4 @@ -class RepoCheckMailer < BaseMailer +class RepositoryCheckMailer < BaseMailer include ActionView::Helpers::TextHelper def notify(failed_count) diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index abcc93f4046..4b475a4d8fa 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -1,7 +1,7 @@ - page_title "Logs" - loggers = [Gitlab::GitLogger, Gitlab::AppLogger, Gitlab::ProductionLogger, Gitlab::SidekiqLogger, - Gitlab::RepoCheckLogger] + Gitlab::RepositoryCheckLogger] %ul.nav-links.log-tabs - loggers.each do |klass| %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index ed360f2f012..c2bf0659841 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -41,19 +41,19 @@ %fieldset %strong Problems .checkbox - = label_tag :last_repo_check_failed do - = check_box_tag :last_repo_check_failed, 1, params[:last_repo_check_failed] - %span Last repo check failed + = label_tag :last_repository_check_failed do + = check_box_tag :last_repository_check_failed, 1, params[:last_repository_check_failed] + %span Last repository check failed = hidden_field_tag :sort, params[:sort] = button_tag "Search", class: "btn submit btn-primary" = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel" - .panel.panel-default.repo-check-states + .panel.panel-default.repository-check-states .panel-heading - Repo check states + Repository check states .panel-body - = link_to 'Clear all', clear_repo_check_states_admin_namespace_projects_path(0), data: { confirm: 'This will clear repo check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" + = link_to 'Clear all', clear_repository_check_states_admin_namespace_projects_path(0), data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" %section.col-md-9 .panel.panel-default .panel-heading diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index a7a3f6349ef..5bef8e3ad57 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -5,12 +5,12 @@ %i.fa.fa-pencil-square-o Edit %hr -- if @project.last_repo_check_failed? +- if @project.last_repository_check_failed? .row .col-md-12 .panel .panel-heading.alert.alert-danger - Last repo check failed. See + Last repository check failed. See = link_to 'repocheck.log', admin_logs_path for error messages. .row @@ -103,19 +103,19 @@ .col-sm-offset-2.col-sm-10 = f.submit 'Transfer', class: 'btn btn-primary' - .panel.panel-default.repo-check + .panel.panel-default.repository-check .panel-heading - Repo check + Repository check .panel-body - = form_for @project, url: repo_check_admin_namespace_project_path(@project.namespace, @project), method: :post do |f| + = form_for @project, url: repository_check_admin_namespace_project_path(@project.namespace, @project), method: :post do |f| .form-group - - if @project.last_repo_check_at.nil? + - if @project.last_repository_check_at.nil? This repository has never been checked. - else This repository was last checked - = @project.last_repo_check_at.to_s(:medium) + '.' + = @project.last_repository_check_at.to_s(:medium) + '.' The check - - if @project.last_repo_check_failed? + - if @project.last_repository_check_failed? = succeed '.' do %strong.cred failed See @@ -125,7 +125,7 @@ passed. .form-group - = f.submit 'Trigger repo check', class: 'btn btn-primary' + = f.submit 'Trigger repository check', class: 'btn btn-primary' .col-md-6 - if @group diff --git a/app/views/repo_check_mailer/notify.text.haml b/app/views/repo_check_mailer/notify.text.haml deleted file mode 100644 index bdf8c2ad675..00000000000 --- a/app/views/repo_check_mailer/notify.text.haml +++ /dev/null @@ -1,3 +0,0 @@ -#{@message}. -\ -View details: #{admin_namespaces_projects_url(last_repo_check_failed: 1)} diff --git a/app/views/repo_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml similarity index 56% rename from app/views/repo_check_mailer/notify.html.haml rename to app/views/repository_check_mailer/notify.html.haml index ef0016f9d3e..df16f503570 100644 --- a/app/views/repo_check_mailer/notify.html.haml +++ b/app/views/repository_check_mailer/notify.html.haml @@ -2,4 +2,4 @@ #{@message}. %p - = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repo_check_failed: 1) + = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1) diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml new file mode 100644 index 00000000000..02f3f80288a --- /dev/null +++ b/app/views/repository_check_mailer/notify.text.haml @@ -0,0 +1,3 @@ +#{@message}. +\ +View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)} diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb index fcccb9ea669..667fff031dd 100644 --- a/app/workers/admin_email_worker.rb +++ b/app/workers/admin_email_worker.rb @@ -4,9 +4,9 @@ class AdminEmailWorker sidekiq_options retry: false # this job auto-repeats via sidekiq-cron def perform - repo_check_failed_count = Project.where(last_repo_check_failed: true).count - return if repo_check_failed_count.zero? + repository_check_failed_count = Project.where(last_repository_check_failed: true).count + return if repository_check_failed_count.zero? - RepoCheckMailer.notify(repo_check_failed_count).deliver_now + RepositoryCheckMailer.notify(repository_check_failed_count).deliver_now end end diff --git a/app/workers/repo_check_worker.rb b/app/workers/repository_check_worker.rb similarity index 67% rename from app/workers/repo_check_worker.rb rename to app/workers/repository_check_worker.rb index 9be795309e0..2d75c8bafde 100644 --- a/app/workers/repo_check_worker.rb +++ b/app/workers/repository_check_worker.rb @@ -1,4 +1,4 @@ -class RepoCheckWorker +class RepositoryCheckWorker include Sidekiq::Worker RUN_TIME = 3600 @@ -11,14 +11,14 @@ class RepoCheckWorker # This loop will break after a little more than one hour ('a little # more' because `git fsck` may take a few minutes), or if it runs out of # projects to check. By default sidekiq-cron will start a new - # RepoCheckWorker each hour so that as long as there are repositories to + # RepositoryCheckWorker each hour so that as long as there are repositories to # check, only one (or two) will be checked at a time. project_ids.each do |project_id| break if Time.now - start >= RUN_TIME next if !try_obtain_lease(project_id) - SingleRepoCheckWorker.new.perform(project_id) + SingleRepositoryCheckWorker.new.perform(project_id) end end @@ -29,16 +29,16 @@ class RepoCheckWorker # an array of IDs instead. def project_ids limit = 10_000 - never_checked_projects = Project.where('last_repo_check_at IS NULL').limit(limit). + never_checked_projects = Project.where('last_repository_check_at IS NULL').limit(limit). pluck(:id) - old_check_projects = Project.where('last_repo_check_at < ?', 1.week.ago). - reorder('last_repo_check_at ASC').limit(limit).pluck(:id) + old_check_projects = Project.where('last_repository_check_at < ?', 1.week.ago). + reorder('last_repository_check_at ASC').limit(limit).pluck(:id) never_checked_projects + old_check_projects end def try_obtain_lease(id) lease = Gitlab::ExclusiveLease.new( - "project_repo_check:#{id}", + "project_repository_check:#{id}", timeout: RUN_TIME ) lease.try_obtain diff --git a/app/workers/single_repo_check_worker.rb b/app/workers/single_repository_check_worker.rb similarity index 72% rename from app/workers/single_repo_check_worker.rb rename to app/workers/single_repository_check_worker.rb index f8b245247c5..d9eed9bd708 100644 --- a/app/workers/single_repo_check_worker.rb +++ b/app/workers/single_repository_check_worker.rb @@ -1,4 +1,4 @@ -class SingleRepoCheckWorker +class SingleRepositoryCheckWorker include Sidekiq::Worker sidekiq_options retry: false @@ -21,14 +21,14 @@ class SingleRepoCheckWorker output, status = Gitlab::Popen.popen(cmd) return true if status.zero? - Gitlab::RepoCheckLogger.error("command failed: #{cmd.join(' ')}\n#{output}") + Gitlab::RepositoryCheckLogger.error("command failed: #{cmd.join(' ')}\n#{output}") false end def update(project, success:) project.update_columns( - last_repo_check_failed: !success, - last_repo_check_at: Time.now, + last_repository_check_failed: !success, + last_repository_check_at: Time.now, ) end end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index cf20fc9c63a..4fbef653bc1 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -157,7 +157,7 @@ production: &base cron: "0 0 * * *" # Periodically run 'git fsck' on all repositories. If started more than # once per hour you will have concurrent 'git fsck' jobs. - repo_check_worker: + repository_check_worker: cron: "20 * * * *" # Send admin emails once a day admin_email_worker: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 8240978ef06..23771553c45 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -239,9 +239,9 @@ Settings['cron_jobs'] ||= Settingslogic.new({}) Settings.cron_jobs['stuck_ci_builds_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['stuck_ci_builds_worker']['cron'] ||= '0 0 * * *' Settings.cron_jobs['stuck_ci_builds_worker']['job_class'] = 'StuckCiBuildsWorker' -Settings.cron_jobs['repo_check_worker'] ||= Settingslogic.new({}) -Settings.cron_jobs['repo_check_worker']['cron'] ||= '20 * * * *' -Settings.cron_jobs['repo_check_worker']['job_class'] = 'RepoCheckWorker' +Settings.cron_jobs['repository_check_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['repository_check_worker']['cron'] ||= '20 * * * *' +Settings.cron_jobs['repository_check_worker']['job_class'] = 'RepositoryCheckWorker' Settings.cron_jobs['admin_email_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['admin_email_worker']['cron'] ||= '0 0 * * *' Settings.cron_jobs['admin_email_worker']['job_class'] = 'AdminEmailWorker' diff --git a/config/routes.rb b/config/routes.rb index fad8600b77d..c0ed99b1964 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -264,11 +264,11 @@ Rails.application.routes.draw do member do put :transfer - post :repo_check + post :repository_check end collection do - put :clear_repo_check_states + put :clear_repository_check_states end resources :runner_projects diff --git a/db/migrate/20160315135439_project_add_repo_check.rb b/db/migrate/20160315135439_project_add_repo_check.rb deleted file mode 100644 index ba7ddc9ecb4..00000000000 --- a/db/migrate/20160315135439_project_add_repo_check.rb +++ /dev/null @@ -1,6 +0,0 @@ -class ProjectAddRepoCheck < ActiveRecord::Migration - def change - add_column :projects, :last_repo_check_failed, :boolean, default: false - add_column :projects, :last_repo_check_at, :datetime - end -end diff --git a/db/migrate/20160315135439_project_add_repository_check.rb b/db/migrate/20160315135439_project_add_repository_check.rb new file mode 100644 index 00000000000..5a0859a30b2 --- /dev/null +++ b/db/migrate/20160315135439_project_add_repository_check.rb @@ -0,0 +1,6 @@ +class ProjectAddRepositoryCheck < ActiveRecord::Migration + def change + add_column :projects, :last_repository_check_failed, :boolean, default: false + add_column :projects, :last_repository_check_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index d2c183f968b..53509956888 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -732,8 +732,8 @@ ActiveRecord::Schema.define(version: 20160331133914) do t.boolean "public_builds", default: true, null: false t.string "main_language" t.integer "pushes_since_gc", default: 0 - t.boolean "last_repo_check_failed", default: false - t.datetime "last_repo_check_at" + t.boolean "last_repository_check_failed", default: false + t.datetime "last_repository_check_at" end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree diff --git a/doc/administration/repo_checks.md b/doc/administration/repository_checks.md similarity index 70% rename from doc/administration/repo_checks.md rename to doc/administration/repository_checks.md index 81087f3ac1c..77f28209f2f 100644 --- a/doc/administration/repo_checks.md +++ b/doc/administration/repository_checks.md @@ -1,4 +1,4 @@ -# Repo checks +# Repository checks _**Note:** This feature was [introduced][ce-3232] in GitLab 8.7_ @@ -13,10 +13,10 @@ output on the admin log page under 'repocheck.log'. ## Periodical checks -GitLab periodically runs a repo check on all project repositories and +GitLab periodically runs a repository check on all project repositories and wiki repositories in order to detect data corruption problems. A project will be checked no more than once per week. If any projects -fail their repo checks all GitLab administrators will receive an email +fail their repository checks all GitLab administrators will receive an email notification of the situation. This notification is sent out no more than once a day. @@ -27,27 +27,27 @@ schedule in gitlab.yml. ``` # For omnibus installations, in /etc/gitlab/gitlab.rb: -gitlab_rails['cron_jobs_repo_check_worker_cron'] = '' +gitlab_rails['cron_jobs_repository_check_worker_cron'] = '' ``` ``` # For installations from source, in config/gitlab.yml: cron_jobs: - repo_check_worker: + repository_check_worker: cron: "" ``` ## What to do if a check failed -If the repo check fails for some repository you should look up the error +If the repository check fails for some repository you should look up the error in repocheck.log (in the admin panel or on disk; see `/var/log/gitlab/gitlab-rails` for Omnibus installations or `/home/git/gitlab/log` for installations from source). Once you have -resolved the issue use the admin panel to trigger a new repo check on +resolved the issue use the admin panel to trigger a new repository check on the project. This will clear the 'check failed' state. -If for some reason the periodical repo check caused a lot of false -alarms you can choose to clear ALL repo check states from the admin +If for some reason the periodical repository check caused a lot of false +alarms you can choose to clear ALL repository check states from the admin project index page. --- diff --git a/lib/gitlab/repo_check_logger.rb b/lib/gitlab/repository_check_logger.rb similarity index 62% rename from lib/gitlab/repo_check_logger.rb rename to lib/gitlab/repository_check_logger.rb index 9409f68722c..485b596ca57 100644 --- a/lib/gitlab/repo_check_logger.rb +++ b/lib/gitlab/repository_check_logger.rb @@ -1,5 +1,5 @@ module Gitlab - class RepoCheckLogger < Gitlab::Logger + class RepositoryCheckLogger < Gitlab::Logger def self.file_name_noext 'repocheck' end diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index e3991d48ed6..95a230a72c3 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -33,33 +33,33 @@ describe "Admin Projects", feature: true do end end - feature 'repo checks' do - scenario 'trigger repo check' do + feature 'repository checks' do + scenario 'trigger repository check' do visit_admin_project_page - page.within('.repo-check') do - click_button 'Trigger repo check' + page.within('.repository-check') do + click_button 'Trigger repository check' end - expect(page).to have_content('Repo check was triggered') + expect(page).to have_content('Repository check was triggered') end - scenario 'see failed repo check' do - @project.update_column(:last_repo_check_failed, true) + scenario 'see failed repository check' do + @project.update_column(:last_repository_check_failed, true) visit_admin_project_page - expect(page).to have_content('Last repo check failed') + expect(page).to have_content('Last repository check failed') end - scenario 'clear repo checks', js: true do - @project.update_column(:last_repo_check_failed, true) + scenario 'clear repository checks', js: true do + @project.update_column(:last_repository_check_failed, true) visit admin_namespaces_projects_path - page.within('.repo-check-states') do + page.within('.repository-check-states') do click_link 'Clear all' # pop-up should be auto confirmed end - expect(@project.reload.last_repo_check_failed).to eq(false) + expect(@project.reload.last_repository_check_failed).to eq(false) end end diff --git a/spec/mailers/repo_check_mailer_spec.rb b/spec/mailers/repository_check_mailer_spec.rb similarity index 92% rename from spec/mailers/repo_check_mailer_spec.rb rename to spec/mailers/repository_check_mailer_spec.rb index d49a6ae0c05..6ae9a93aaac 100644 --- a/spec/mailers/repo_check_mailer_spec.rb +++ b/spec/mailers/repository_check_mailer_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe RepoCheckMailer do +describe RepositoryCheckMailer do include EmailSpec::Matchers describe '.notify' do diff --git a/spec/workers/repo_check_worker_spec.rb b/spec/workers/repo_check_worker_spec.rb deleted file mode 100644 index 7ef3eba9ac5..00000000000 --- a/spec/workers/repo_check_worker_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'spec_helper' - -describe RepoCheckWorker do - subject { RepoCheckWorker.new } - - it 'prefers projects that have never been checked' do - projects = 3.times.map { create(:project) } - projects[0].update_column(:last_repo_check_at, 1.month.ago) - projects[2].update_column(:last_repo_check_at, 3.weeks.ago) - - expect(subject.perform).to eq(projects.values_at(1, 0, 2).map(&:id)) - end - - it 'sorts projects by last_repo_check_at' do - projects = 3.times.map { create(:project) } - projects[0].update_column(:last_repo_check_at, 2.weeks.ago) - projects[1].update_column(:last_repo_check_at, 1.month.ago) - projects[2].update_column(:last_repo_check_at, 3.weeks.ago) - - expect(subject.perform).to eq(projects.values_at(1, 2, 0).map(&:id)) - end - - it 'excludes projects that were checked recently' do - projects = 3.times.map { create(:project) } - projects[0].update_column(:last_repo_check_at, 2.days.ago) - projects[1].update_column(:last_repo_check_at, 1.month.ago) - projects[2].update_column(:last_repo_check_at, 3.days.ago) - - expect(subject.perform).to eq([projects[1].id]) - end -end diff --git a/spec/workers/repository_check_worker_spec.rb b/spec/workers/repository_check_worker_spec.rb new file mode 100644 index 00000000000..d1849321f56 --- /dev/null +++ b/spec/workers/repository_check_worker_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe RepositoryCheckWorker do + subject { RepositoryCheckWorker.new } + + it 'prefers projects that have never been checked' do + projects = 3.times.map { create(:project) } + projects[0].update_column(:last_repository_check_at, 1.month.ago) + projects[2].update_column(:last_repository_check_at, 3.weeks.ago) + + expect(subject.perform).to eq(projects.values_at(1, 0, 2).map(&:id)) + end + + it 'sorts projects by last_repository_check_at' do + projects = 3.times.map { create(:project) } + projects[0].update_column(:last_repository_check_at, 2.weeks.ago) + projects[1].update_column(:last_repository_check_at, 1.month.ago) + projects[2].update_column(:last_repository_check_at, 3.weeks.ago) + + expect(subject.perform).to eq(projects.values_at(1, 2, 0).map(&:id)) + end + + it 'excludes projects that were checked recently' do + projects = 3.times.map { create(:project) } + projects[0].update_column(:last_repository_check_at, 2.days.ago) + projects[1].update_column(:last_repository_check_at, 1.month.ago) + projects[2].update_column(:last_repository_check_at, 3.days.ago) + + expect(subject.perform).to eq([projects[1].id]) + end +end From 3d145ce8d513207988d523288334a61a575d4c01 Mon Sep 17 00:00:00 2001 From: Arinde Eniola Date: Thu, 24 Mar 2016 12:28:06 +0100 Subject: [PATCH 024/187] fix labels not showing on todos page --- app/assets/stylesheets/framework/variables.scss | 1 + app/assets/stylesheets/pages/events.scss | 11 ++++++++++- app/assets/stylesheets/pages/todos.scss | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 463ce4ecdd7..6d188659524 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -28,6 +28,7 @@ $gl-link-color: #3084bb; $gl-dark-link-color: #333; $gl-placeholder-color: #8f8f8f; $gl-icon-color: $gl-placeholder-color; +$gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-header-color: $gl-title-color; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index c66efe978cd..6fe57c737b3 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -41,8 +41,17 @@ word-wrap: break-word; .md { - color: #7f8fa4; + color: $gl-grayish-blue; font-size: $gl-font-size; + + .label { + color: $gl-text-color; + font-size: inherit; + } + + iframe.twitter-share-button { + vertical-align: bottom; + } } pre { diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index e83fa9e3d52..75f78569e3c 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -34,6 +34,11 @@ color: #7f8fa4; font-size: $gl-font-size; + .label { + color: $gl-text-color; + font-size: inherit; + } + p { color: #5c5d5e; } From c11c574d3ef4384b38f310c8d8568a116e0d0dfd Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 7 Apr 2016 00:00:58 -0500 Subject: [PATCH 025/187] Convert time param to Time --- app/helpers/application_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e6ceb213532..16e5b8ac223 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -184,7 +184,7 @@ module ApplicationHelper element = content_tag :time, time.to_s, class: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}", datetime: time.to_time.getutc.iso8601, - title: time.in_time_zone.to_s(:medium), + title: time.to_time.in_time_zone.to_s(:medium), data: { toggle: 'tooltip', placement: placement, container: 'body' } unless skip_js From af3d7d3134033a50dbe41d88654bfea9847524a7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 7 Apr 2016 11:00:36 +0100 Subject: [PATCH 026/187] Fixed issue with any label & no label filters missing Closes #15008 --- app/views/shared/issuable/_label_dropdown.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index fd5e58c1f1f..f722e61eeac 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,7 +1,7 @@ - if params[:label_name].present? = hidden_field_tag(:label_name, params[:label_name]) .dropdown - %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} + %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} %span.dropdown-toggle-text = h(params[:label_name].presence || "Label") = icon('chevron-down') From 122b4148f764952fc5f365fee77e9e3241ca1093 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 7 Apr 2016 11:24:12 +0100 Subject: [PATCH 027/187] Added label filter tests --- spec/features/issues/filter_issues_spec.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 90822a8c123..91de06e31f9 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -76,6 +76,28 @@ describe 'Filter issues', feature: true do end end + describe 'Filter issues for label from issues#index', js: true do + before do + visit namespace_project_issues_path(project.namespace, project) + find('.js-label-select').click + end + + it 'should filter by any label' do + find('.dropdown-menu-labels a', text: 'Any Label').click + expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Label') + end + + it 'should filter by no label' do + find('.dropdown-menu-labels a', text: 'No Label').click + expect(find('.js-label-select .dropdown-toggle-text')).to have_content('No Label') + end + + it 'should filter by no label' do + find('.dropdown-menu-labels a', text: label.title).click + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + end + describe 'Filter issues for assignee and label from issues#index' do before do From 656d8d8865a817e9c01e11f875d6ce1a3d8adebf Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 7 Apr 2016 15:56:59 +0100 Subject: [PATCH 028/187] Fixed colour of dropdown link hover --- app/assets/stylesheets/pages/issuable.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 88c1b614c74..8b6f37f21b5 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -263,6 +263,12 @@ } } + .dropdown-content { + a:hover { + color: inherit; + } + } + .dropdown-menu-toggle { width: 100%; padding-top: 6px; From 24f18c8e52b55c96f20dbe0b3b9a9c74baff7813 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 7 Apr 2016 16:19:26 +0100 Subject: [PATCH 029/187] Updated meqia query for admin/groups search box --- app/assets/stylesheets/framework/nav.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 94f5a12ff6a..192d53b048a 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -58,12 +58,12 @@ .nav-search { display: inline-block; - width: 50%; + width: 100%; padding: 11px 0; /* Small devices (phones, tablets, 768px and lower) */ - @media (max-width: $screen-sm-min) { - width: 100%; + @media (min-width: $screen-sm-min) { + width: 50%; } } From 0dbf2df341f6d71218c927e2d5f4ac5cf67794be Mon Sep 17 00:00:00 2001 From: Arinde Eniola Date: Thu, 7 Apr 2016 16:42:01 +0100 Subject: [PATCH 030/187] fix quick submit missing in edit merge request page --- app/views/projects/merge_requests/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 3e4ab09c6d4..1e6724fc92b 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input js-quick-submit' } do |f| = render 'shared/issuable/form', f: f, issuable: @merge_request :javascript From 088368ae4d1c41b6e55a8c5e7e5439a9ea323eb2 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 7 Apr 2016 11:59:56 -0500 Subject: [PATCH 031/187] Add date.format.js --- vendor/assets/javascripts/date.format.js | 125 +++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 vendor/assets/javascripts/date.format.js diff --git a/vendor/assets/javascripts/date.format.js b/vendor/assets/javascripts/date.format.js new file mode 100644 index 00000000000..f5dc4abcd80 --- /dev/null +++ b/vendor/assets/javascripts/date.format.js @@ -0,0 +1,125 @@ +/* + * Date Format 1.2.3 + * (c) 2007-2009 Steven Levithan + * MIT license + * + * Includes enhancements by Scott Trenda + * and Kris Kowal + * + * Accepts a date, a mask, or a date and a mask. + * Returns a formatted version of the given date. + * The date defaults to the current date/time. + * The mask defaults to dateFormat.masks.default. + */ + +var dateFormat = function () { + var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, + timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, + timezoneClip = /[^-+\dA-Z]/g, + pad = function (val, len) { + val = String(val); + len = len || 2; + while (val.length < len) val = "0" + val; + return val; + }; + + // Regexes and supporting functions are cached through closure + return function (date, mask, utc) { + var dF = dateFormat; + + // You can't provide utc if you skip other args (use the "UTC:" mask prefix) + if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { + mask = date; + date = undefined; + } + + // Passing date through Date applies Date.parse, if necessary + date = date ? new Date(date) : new Date; + if (isNaN(date)) throw SyntaxError("invalid date"); + + mask = String(dF.masks[mask] || mask || dF.masks["default"]); + + // Allow setting the utc argument via the mask + if (mask.slice(0, 4) == "UTC:") { + mask = mask.slice(4); + utc = true; + } + + var _ = utc ? "getUTC" : "get", + d = date[_ + "Date"](), + D = date[_ + "Day"](), + m = date[_ + "Month"](), + y = date[_ + "FullYear"](), + H = date[_ + "Hours"](), + M = date[_ + "Minutes"](), + s = date[_ + "Seconds"](), + L = date[_ + "Milliseconds"](), + o = utc ? 0 : date.getTimezoneOffset(), + flags = { + d: d, + dd: pad(d), + ddd: dF.i18n.dayNames[D], + dddd: dF.i18n.dayNames[D + 7], + m: m + 1, + mm: pad(m + 1), + mmm: dF.i18n.monthNames[m], + mmmm: dF.i18n.monthNames[m + 12], + yy: String(y).slice(2), + yyyy: y, + h: H % 12 || 12, + hh: pad(H % 12 || 12), + H: H, + HH: pad(H), + M: M, + MM: pad(M), + s: s, + ss: pad(s), + l: pad(L, 3), + L: pad(L > 99 ? Math.round(L / 10) : L), + t: H < 12 ? "a" : "p", + tt: H < 12 ? "am" : "pm", + T: H < 12 ? "A" : "P", + TT: H < 12 ? "AM" : "PM", + Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), + o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), + S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] + }; + + return mask.replace(token, function ($0) { + return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); + }); + }; +}(); + +// Some common format strings +dateFormat.masks = { + "default": "ddd mmm dd yyyy HH:MM:ss", + shortDate: "m/d/yy", + mediumDate: "mmm d, yyyy", + longDate: "mmmm d, yyyy", + fullDate: "dddd, mmmm d, yyyy", + shortTime: "h:MM TT", + mediumTime: "h:MM:ss TT", + longTime: "h:MM:ss TT Z", + isoDate: "yyyy-mm-dd", + isoTime: "HH:MM:ss", + isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", + isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" +}; + +// Internationalization strings +dateFormat.i18n = { + dayNames: [ + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" + ], + monthNames: [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" + ] +}; + +// For convenience... +Date.prototype.format = function (mask, utc) { + return dateFormat(this, mask, utc); +}; From b19ccdeed45605a7bb79509092b297087d6c6e8c Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 7 Apr 2016 12:00:19 -0500 Subject: [PATCH 032/187] Add datetime_utility.js.coffee --- .../javascripts/lib/datetime_utility.js.coffee | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 app/assets/javascripts/lib/datetime_utility.js.coffee diff --git a/app/assets/javascripts/lib/datetime_utility.js.coffee b/app/assets/javascripts/lib/datetime_utility.js.coffee new file mode 100644 index 00000000000..ef9406fc331 --- /dev/null +++ b/app/assets/javascripts/lib/datetime_utility.js.coffee @@ -0,0 +1,15 @@ +((w) -> + + w.gl ?= {} + w.gl.utils ?= {} + + w.gl.utils.formatDate = (datetime) -> + dateFormat(datetime, 'mmm d, yyyy h:MMtt Z') + + w.gl.utils.updateFormatDate = ($timeagoEls) -> + $timeagoEls.each( -> + $el = $(@) + $el.attr('title', gl.utils.formatDate($el.attr('datetime'))) + ) + +) window From 3440c0e61f570e9f42a81fb125a021b138b5bebc Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 7 Apr 2016 12:02:43 -0500 Subject: [PATCH 033/187] Update datetime in .timeago elements This should be done before .timeago() is called on the element --- app/assets/javascripts/application.js.coffee | 5 ++++- .../javascripts/merge_request_tabs.js.coffee | 12 +++++++++--- app/assets/javascripts/notes.js.coffee | 17 +++++++++++++++-- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index f01c67e9474..922a28b4ef5 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -41,6 +41,7 @@ #= require shortcuts_issuable #= require shortcuts_network #= require jquery.nicescroll +#= require date.format #= require_tree . #= require fuzzaldrin-plus #= require cropper @@ -163,7 +164,9 @@ $ -> $('.trigger-submit').on 'change', -> $(@).parents('form').submit() - $('abbr.timeago, .js-timeago').timeago() + $timeago = $('abbr.timeago, .js-timeago') + gl.utils.updateFormatDate($timeago) + $timeago.timeago() # Flash if (flash = $(".flash-container")).length > 0 diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 839e6ec2c08..fdf084a8a82 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -141,7 +141,9 @@ class @MergeRequestTabs url: "#{source}.json" success: (data) => document.querySelector("div#commits").innerHTML = data.html - $('.js-timeago').timeago() + $timeago = $('.js-timeago', 'div#commits') + gl.utils.updateFormatDate($timeago) + $timeago.timeago() @commitsLoaded = true @scrollToElement("#commits") @@ -152,7 +154,9 @@ class @MergeRequestTabs url: "#{source}.json" + @_location.search success: (data) => document.querySelector("div#diffs").innerHTML = data.html - $('.js-timeago').timeago() + $timeago = $('.js-timeago', 'div#diffs') + gl.utils.updateFormatDate($timeago) + $timeago.timeago() $('div#diffs .js-syntax-highlight').syntaxHighlight() @expandViewContainer() if @diffViewType() is 'parallel' @diffsLoaded = true @@ -165,7 +169,9 @@ class @MergeRequestTabs url: "#{source}.json" success: (data) => document.querySelector("div#builds").innerHTML = data.html - $('.js-timeago').timeago() + $timeago = $('.js-timeago', 'div#builds') + gl.utils.updateFormatDate($timeago) + $timeago.timeago() @buildsLoaded = true @scrollToElement("#builds") diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 86e3b860fcb..02e52040e3c 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -163,9 +163,16 @@ class @Notes else if @isNewNote(note) @note_ids.push(note.id) - $('ul.main-notes-list') + $notesList = $('ul.main-notes-list') + + $notesList .append(note.html) .syntaxHighlight() + + # Update datetime format on the recent note + $timeago = $notesList.find("#note_#{note.id} .js-timeago") + gl.utils.updateFormatDate($timeago) + @initTaskList() @updateNotesCount(1) @@ -217,6 +224,8 @@ class @Notes # append new note to all matching discussions discussionContainer.append note_html + gl.utils.updateFormatDate($('.js-timeago', note_html)) + @updateNotesCount(1) ### @@ -345,7 +354,11 @@ class @Notes updateNote: (_xhr, note, _status) => # Convert returned HTML to a jQuery object so we can modify it further $html = $(note.html) - $('.js-timeago', $html).timeago() + + $timeago = $('.js-timeago', $html) + gl.utils.updateFormatDate($timeago) + $timeago.timeago() + $html.syntaxHighlight() $html.find('.js-task-list-container').taskList('enable') From 451314df964455577e08f653cc17d70fedf3f49b Mon Sep 17 00:00:00 2001 From: Arinde Eniola Date: Thu, 7 Apr 2016 20:51:16 +0100 Subject: [PATCH 034/187] add test --- spec/features/merge_requests/edit_mr_spec.rb | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 spec/features/merge_requests/edit_mr_spec.rb diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb new file mode 100644 index 00000000000..27f7bca2af0 --- /dev/null +++ b/spec/features/merge_requests/edit_mr_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +feature 'Create New Merge Request', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) } + + before do + project.team << [user, :master] + + login_as user + + visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + context 'editing a MR', js: true do + it 'should be able submit with quick_submit' do + fill_in "merge_request_title", with: "Orphaned MR test" + + keypress = "var e = $.Event('keydown', { keyCode: 13, ctrlKey: true }); $('.merge-request-form').trigger(e);" + page.driver.execute_script(keypress) + sleep 2 + + expect(find('h2.title')).to have_text('Orphaned MR test') + end + end +end \ No newline at end of file From 400a1ae04d28388e7dfbd7c03db857d58a7d8776 Mon Sep 17 00:00:00 2001 From: Arinde Eniola Date: Thu, 7 Apr 2016 23:44:59 +0100 Subject: [PATCH 035/187] add some changes to the test --- spec/features/merge_requests/edit_mr_spec.rb | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb index 27f7bca2af0..9e007ab7635 100644 --- a/spec/features/merge_requests/edit_mr_spec.rb +++ b/spec/features/merge_requests/edit_mr_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Create New Merge Request', feature: true do +feature 'Edit Merge Request', feature: true do let(:user) { create(:user) } let(:project) { create(:project, :public) } let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) } @@ -13,15 +13,9 @@ feature 'Create New Merge Request', feature: true do visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request) end - context 'editing a MR', js: true do - it 'should be able submit with quick_submit' do - fill_in "merge_request_title", with: "Orphaned MR test" - - keypress = "var e = $.Event('keydown', { keyCode: 13, ctrlKey: true }); $('.merge-request-form').trigger(e);" - page.driver.execute_script(keypress) - sleep 2 - - expect(find('h2.title')).to have_text('Orphaned MR test') + context 'editing a MR' do + it 'form should have class js-quick-submit' do + expect(page).to have_selector('.js-quick-submit') end end -end \ No newline at end of file +end From 6dacf0fd6d2627e05e50e0116566946e3b81bc5f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 8 Apr 2016 16:02:13 +0100 Subject: [PATCH 036/187] Fixed issue with member access not being visible on notes Fixes #15049 --- app/views/projects/notes/_note.html.haml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index a681d6dece4..b4fa917718b 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -10,12 +10,12 @@ = "#{note.author.to_reference} commented" %a{ href: "##{dom_id(note)}" } = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - - if note_editable?(note) - .note-actions - - access = note.project.team.human_max_access(note.author.id) - - if access - %span.note-role - = access + .note-actions + - access = note.project.team.human_max_access(note.author.id) + - if access + %span.note-role + = access + - if note_editable?(note) = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = icon('pencil-square-o') = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete' do From d11288be9580633baa3b21b4c70a5fc01e53d094 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 8 Apr 2016 16:49:01 -0300 Subject: [PATCH 037/187] Use query instead of model on migrations --- db/migrate/20160328115649_migrate_new_notification_setting.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20160328115649_migrate_new_notification_setting.rb b/db/migrate/20160328115649_migrate_new_notification_setting.rb index 6a68890f5b1..0a110869027 100644 --- a/db/migrate/20160328115649_migrate_new_notification_setting.rb +++ b/db/migrate/20160328115649_migrate_new_notification_setting.rb @@ -12,6 +12,6 @@ class MigrateNewNotificationSetting < ActiveRecord::Migration end def down - NotificationSetting.delete_all + execute "DELETE FROM notification_settings" end end From 127119f2c4db9038a7f34d1cc73ae1ed19cf0b8d Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 8 Apr 2016 16:59:06 -0300 Subject: [PATCH 038/187] Simplify query to retrieve NotificationSetting on controllers --- app/controllers/groups/notification_settings_controller.rb | 2 +- app/controllers/projects/notification_settings_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/groups/notification_settings_controller.rb b/app/controllers/groups/notification_settings_controller.rb index 78e43c83aba..20405a05190 100644 --- a/app/controllers/groups/notification_settings_controller.rb +++ b/app/controllers/groups/notification_settings_controller.rb @@ -1,6 +1,6 @@ class Groups::NotificationSettingsController < Groups::ApplicationController def update - notification_setting = group.notification_settings.where(user_id: current_user).find(params[:id]) + notification_setting = group.notification_settings.find_by(user_id: current_user) saved = notification_setting.update_attributes(notification_setting_params) render json: { saved: saved } diff --git a/app/controllers/projects/notification_settings_controller.rb b/app/controllers/projects/notification_settings_controller.rb index 3ecf63d107f..da9034380af 100644 --- a/app/controllers/projects/notification_settings_controller.rb +++ b/app/controllers/projects/notification_settings_controller.rb @@ -8,7 +8,7 @@ class Projects::NotificationSettingsController < Projects::ApplicationController end def update - notification_setting = project.notification_settings.where(user_id: current_user).find(params[:id]) + notification_setting = project.notification_settings.find_by(user_id: current_user) saved = notification_setting.update_attributes(notification_setting_params) render json: { saved: saved } From 069724cef5873b83720004772d1e874030cc9fff Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 8 Apr 2016 17:11:13 -0300 Subject: [PATCH 039/187] Use singular resource for NotificationSetting Since a user cannot have multiple NotificationSettings records for one group/project we can use singular resource. --- config/routes.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index fade07c0500..552385110dd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -406,7 +406,7 @@ Rails.application.routes.draw do resource :avatar, only: [:destroy] resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] - resources :notification_settings, only: [:update] + resource :notification_setting, only: [:update] end end @@ -608,7 +608,7 @@ Rails.application.routes.draw do resources :forks, only: [:index, :new, :create] resource :import, only: [:new, :create, :show] - resources :notification_settings, only: [:create, :update] + resource :notification_setting, only: [:create, :update] resources :refs, only: [] do collection do From ee497599cc69b126cc2e4a929f1799d3d3eb989d Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 8 Apr 2016 17:24:27 -0300 Subject: [PATCH 040/187] Use default_value_for to set default NotificationSetting#level --- app/controllers/projects_controller.rb | 1 - app/models/notification_setting.rb | 14 ++++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 41a4c41cf80..39f436f6f4e 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -106,7 +106,6 @@ class ProjectsController < Projects::ApplicationController if @membership @notification_setting = current_user.notification_settings.find_or_initialize_by(source: @project) - @notification_setting.set_defaults unless @notification_setting.persisted? end end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 13a8995b036..d89194b5a12 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -1,4 +1,10 @@ class NotificationSetting < ActiveRecord::Base + # Notification level + # Note: When adding an option, it MUST go on the end of the array. + enum level: [:disabled, :participating, :watch, :global, :mention] + + default_value_for :level, NotificationSetting.levels[:global] + belongs_to :user belongs_to :source, polymorphic: true @@ -8,9 +14,6 @@ class NotificationSetting < ActiveRecord::Base validates :user_id, uniqueness: { scope: [:source_type, :source_id], message: "already exists in source", allow_nil: true } - # Notification level - # Note: When adding an option, it MUST go on the end of the array. - enum level: [:disabled, :participating, :watch, :global, :mention] scope :for_groups, -> { where(source_type: 'Namespace') } scope :for_projects, -> { where(source_type: 'Project') } @@ -19,14 +22,9 @@ class NotificationSetting < ActiveRecord::Base setting = find_or_initialize_by(source: source) unless setting.persisted? - setting.set_defaults setting.save end setting end - - def set_defaults - self.level = :global - end end From 635b65d1206293d0b92322df18d458447ee73d5a Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 8 Apr 2016 17:35:21 -0300 Subject: [PATCH 041/187] Add method to return the user notification setting for a group, or a project --- app/controllers/projects_controller.rb | 2 +- app/models/user.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 39f436f6f4e..3768efe142a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -105,7 +105,7 @@ class ProjectsController < Projects::ApplicationController @membership = @project.team.find_member(current_user.id) if @membership - @notification_setting = current_user.notification_settings.find_or_initialize_by(source: @project) + @notification_setting = current_user.notification_settings_for(@project) end end diff --git a/app/models/user.rb b/app/models/user.rb index f68379fd050..031315debd7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -831,6 +831,10 @@ class User < ActiveRecord::Base end end + def notification_settings_for(source) + notification_settings.find_or_initialize_by(source: source) + end + private def projects_union From e3d4ebdd543f17d91e5c67be2f30bb97c687b448 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 7 Apr 2016 16:40:51 +0200 Subject: [PATCH 042/187] Update gitlab-shell to 2.7.2 --- GITLAB_SHELL_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 24ba9a38de6..37c2961c243 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.7.0 +2.7.2 From 47c8b7f3037c3e464e34d6f978a27e591f09e687 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 8 Apr 2016 17:44:55 -0300 Subject: [PATCH 043/187] Fix CHANGELOG --- CHANGELOG | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c072cada732..eae6cb90700 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -25,10 +25,6 @@ v 8.7.0 (unreleased) - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) - Gracefully handle notes on deleted commits in merge requests (Stan Hu) - Decouple membership and notifications - -v 8.6.2 (unreleased) - - Comments on confidential issues don't show up in activity feed to non-members - - Fix NoMethodError when visiting CI root path at `/ci` - Fix creation of merge requests for orphaned branches (Stan Hu) - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) From 99067a505ca62dc069189118d7d4c4e91de83917 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 8 Apr 2016 18:06:36 -0300 Subject: [PATCH 044/187] Fix schema.rb --- db/schema.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 4c7673511fb..a66274dc5a1 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: 20160331133914) do +ActiveRecord::Schema.define(version: 20160331223143) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -799,9 +799,9 @@ ActiveRecord::Schema.define(version: 20160331133914) do t.string "type" t.string "title" t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" - t.boolean "active", default: false, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "active", null: false t.text "properties" t.boolean "template", default: false t.boolean "push_events", default: true From 1d1ca8b9c257f79ae75740cacad7d361635312b6 Mon Sep 17 00:00:00 2001 From: Arinde Eniola Date: Sat, 9 Apr 2016 13:29:37 +0100 Subject: [PATCH 045/187] fix emoji aliases not showing in autocomplete --- fixtures/emojis/digests.json | 2485 ++++++++++++++++++++++++++++++++++ lib/tasks/gemojione.rake | 15 +- 2 files changed, 2498 insertions(+), 2 deletions(-) diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json index 18d6e93e0f4..41ca617847e 100644 --- a/fixtures/emojis/digests.json +++ b/fixtures/emojis/digests.json @@ -64,21 +64,41 @@ "unicode": "1F6EA", "digest": "fdddc2cd3618ec6661612581b8b93553cb086b0bb197e96aedf1bee8055e7bb4" }, + { + "name": "northeast_pointing_airplane", + "unicode": "1F6EA", + "digest": "fdddc2cd3618ec6661612581b8b93553cb086b0bb197e96aedf1bee8055e7bb4" + }, { "name": "airplane_small", "unicode": "1F6E9", "digest": "f98b44422d6bf505b50330805ecf68013d035341f0b6487c3c05ad913eb5abd3" }, + { + "name": "small_airplane", + "unicode": "1F6E9", + "digest": "f98b44422d6bf505b50330805ecf68013d035341f0b6487c3c05ad913eb5abd3" + }, { "name": "airplane_small_up", "unicode": "1F6E8", "digest": "029752b29a757c087dec60f45ea242e974fc181129e20390d5d4a2f90442091a" }, + { + "name": "up_pointing_small_airplane", + "unicode": "1F6E8", + "digest": "029752b29a757c087dec60f45ea242e974fc181129e20390d5d4a2f90442091a" + }, { "name": "airplane_up", "unicode": "1F6E7", "digest": "ec45d4dbfce1f75dc59339417b1dcf5f1e1359cd9d04ff233babf359a3330e77" }, + { + "name": "up_pointing_airplane", + "unicode": "1F6E7", + "digest": "ec45d4dbfce1f75dc59339417b1dcf5f1e1359cd9d04ff233babf359a3330e77" + }, { "name": "alarm_clock", "unicode": "23F0", @@ -149,11 +169,21 @@ "unicode": "1F5EE", "digest": "f2711991e8b386b2d5b12f296ce20a9b4b00ef91d6d67af2cf4e06abf2faa1dc" }, + { + "name": "left_anger_bubble", + "unicode": "1F5EE", + "digest": "f2711991e8b386b2d5b12f296ce20a9b4b00ef91d6d67af2cf4e06abf2faa1dc" + }, { "name": "anger_right", "unicode": "1F5EF", "digest": "24b572d64c519251a3ae8844e8d66fd6955752aff99aebe7dc20179505a466c4" }, + { + "name": "right_anger_bubble", + "unicode": "1F5EF", + "digest": "24b572d64c519251a3ae8844e8d66fd6955752aff99aebe7dc20179505a466c4" + }, { "name": "angry", "unicode": "1F620", @@ -304,6 +334,11 @@ "unicode": "002A-20E3", "digest": "0b7f27f545b616677c83d40ff957337477b2881459b4d3c839ae55e23797419f" }, + { + "name": "keycap_asterisk", + "unicode": "002A-20E3", + "digest": "0b7f27f545b616677c83d40ff957337477b2881459b4d3c839ae55e23797419f" + }, { "name": "astonished", "unicode": "1F632", @@ -324,6 +359,11 @@ "unicode": "269B", "digest": "cbce1725602efbb77a935cfae5407e4d75489ee988910296c7f6140665afc669" }, + { + "name": "atom_symbol", + "unicode": "269B", + "digest": "cbce1725602efbb77a935cfae5407e4d75489ee988910296c7f6140665afc669" + }, { "name": "b", "unicode": "1F171", @@ -399,11 +439,21 @@ "unicode": "1F5F3", "digest": "0455ea75612efe78354315b4c345953d2d559bb471d5b01c1adc1d6b74ed693a" }, + { + "name": "ballot_box_with_ballot", + "unicode": "1F5F3", + "digest": "0455ea75612efe78354315b4c345953d2d559bb471d5b01c1adc1d6b74ed693a" + }, { "name": "ballot_box_check", "unicode": "1F5F9", "digest": "fc3ba16c009d963a4a0ea20a348ac98eee3c4c18c481df19a5ada0d1de7fcc15" }, + { + "name": "ballot_box_with_bold_check", + "unicode": "1F5F9", + "digest": "fc3ba16c009d963a4a0ea20a348ac98eee3c4c18c481df19a5ada0d1de7fcc15" + }, { "name": "ballot_box_with_check", "unicode": "2611", @@ -414,11 +464,21 @@ "unicode": "1F5F5", "digest": "861dcfc2361298262587b5d0e163fed96a55c44636361f5b4a9ab1d6502b8928" }, + { + "name": "ballot_box_with_script_x", + "unicode": "1F5F5", + "digest": "861dcfc2361298262587b5d0e163fed96a55c44636361f5b4a9ab1d6502b8928" + }, { "name": "ballot_x", "unicode": "1F5F4", "digest": "0b73b89847eb82bcad5664644c8af237e0aef6c3d8c94b7a5df94e05d0ebf4e1" }, + { + "name": "ballot_script_x", + "unicode": "1F5F4", + "digest": "0b73b89847eb82bcad5664644c8af237e0aef6c3d8c94b7a5df94e05d0ebf4e1" + }, { "name": "bamboo", "unicode": "1F38D", @@ -464,31 +524,61 @@ "unicode": "26F9", "digest": "e94beb69f631667479a80095bf313ceb3aa109d6ebb80f182722360a6d2a214e" }, + { + "name": "person_with_ball", + "unicode": "26F9", + "digest": "e94beb69f631667479a80095bf313ceb3aa109d6ebb80f182722360a6d2a214e" + }, { "name": "basketball_player_tone1", "unicode": "26F9-1F3FB", "digest": "6fc77cf2f26ee18e9a3faea500d4277839f77633f31ee618a68c301f1ad32d90" }, + { + "name": "person_with_ball_tone1", + "unicode": "26F9-1F3FB", + "digest": "6fc77cf2f26ee18e9a3faea500d4277839f77633f31ee618a68c301f1ad32d90" + }, { "name": "basketball_player_tone2", "unicode": "26F9-1F3FC", "digest": "6ee9060c24d92708e12a854fb0bdf5c717c90b8c0350d8aa40c278b41bfa12fc" }, + { + "name": "person_with_ball_tone2", + "unicode": "26F9-1F3FC", + "digest": "6ee9060c24d92708e12a854fb0bdf5c717c90b8c0350d8aa40c278b41bfa12fc" + }, { "name": "basketball_player_tone3", "unicode": "26F9-1F3FD", "digest": "752e90dbfa7c7a9ae3f37de924e22f3c3d5a7e54dd41c8e8eb99cabb0dad73cf" }, + { + "name": "person_with_ball_tone3", + "unicode": "26F9-1F3FD", + "digest": "752e90dbfa7c7a9ae3f37de924e22f3c3d5a7e54dd41c8e8eb99cabb0dad73cf" + }, { "name": "basketball_player_tone4", "unicode": "26F9-1F3FE", "digest": "38bedc3074e6243454d568d9b665f5764f1a3d983875651ce7a1cdb53da9f6c8" }, + { + "name": "person_with_ball_tone4", + "unicode": "26F9-1F3FE", + "digest": "38bedc3074e6243454d568d9b665f5764f1a3d983875651ce7a1cdb53da9f6c8" + }, { "name": "basketball_player_tone5", "unicode": "26F9-1F3FF", "digest": "25ee1e84670d3db96d3ad098c859abd6b3448f55f668ce0c195ee2337a215de7" }, + { + "name": "person_with_ball_tone5", + "unicode": "26F9-1F3FF", + "digest": "25ee1e84670d3db96d3ad098c859abd6b3448f55f668ce0c195ee2337a215de7" + }, { "name": "bath", "unicode": "1F6C0", @@ -534,11 +624,21 @@ "unicode": "1F3D6", "digest": "52855d75cfa4476ccc23c58b4afcb76ee48abb22a9a6081210c8accefdf33099" }, + { + "name": "beach_with_umbrella", + "unicode": "1F3D6", + "digest": "52855d75cfa4476ccc23c58b4afcb76ee48abb22a9a6081210c8accefdf33099" + }, { "name": "beach_umbrella", "unicode": "26F1", "digest": "cefe8e195d21d3e0769d3bfe15170db9e57c86db9d31cacb19fcdc8d2191b661" }, + { + "name": "umbrella_on_ground", + "unicode": "26F1", + "digest": "cefe8e195d21d3e0769d3bfe15170db9e57c86db9d31cacb19fcdc8d2191b661" + }, { "name": "bear", "unicode": "1F43B", @@ -584,6 +684,11 @@ "unicode": "1F6CE", "digest": "c15455f1b52ac26404b5c13a0e1070212ed1830026422873f4f6335e26e31259" }, + { + "name": "bellhop_bell", + "unicode": "1F6CE", + "digest": "c15455f1b52ac26404b5c13a0e1070212ed1830026422873f4f6335e26e31259" + }, { "name": "bento", "unicode": "1F371", @@ -634,6 +739,11 @@ "unicode": "2623", "digest": "81f8309318051255ed4dc18855a3cd3f8657a6f3b2d368caa531a57ce0e34235" }, + { + "name": "biohazard_sign", + "unicode": "2623", + "digest": "81f8309318051255ed4dc18855a3cd3f8657a6f3b2d368caa531a57ce0e34235" + }, { "name": "bird", "unicode": "1F426", @@ -769,6 +879,11 @@ "unicode": "1F395", "digest": "1643ec51ff26fc1ac0c67859e202386398650bf2a996c82b68e1b73fa52abf7d" }, + { + "name": "bouquet_of_flowers", + "unicode": "1F395", + "digest": "1643ec51ff26fc1ac0c67859e202386398650bf2a996c82b68e1b73fa52abf7d" + }, { "name": "bow", "unicode": "1F647", @@ -779,6 +894,11 @@ "unicode": "1F3F9", "digest": "1c23469256331ea4ff03c036f89f0e63ad3228c51faecba50129da99b7eaddf3" }, + { + "name": "archery", + "unicode": "1F3F9", + "digest": "1c23469256331ea4ff03c036f89f0e63ad3228c51faecba50129da99b7eaddf3" + }, { "name": "bow_tone1", "unicode": "1F647-1F3FB", @@ -924,6 +1044,11 @@ "unicode": "1F56C", "digest": "92493636cf086205d1e12cc19e613b84152ef10b8cd0215619a0fc813bfc9a7c" }, + { + "name": "bullhorn_with_sound_waves", + "unicode": "1F56C", + "digest": "92493636cf086205d1e12cc19e613b84152ef10b8cd0215619a0fc813bfc9a7c" + }, { "name": "burrito", "unicode": "1F32F", @@ -964,6 +1089,11 @@ "unicode": "1F5A9", "digest": "01b47b5c69c12b65fa4f4c0d580f2a98280d6116f4ad2cf8be378759008bcc3c" }, + { + "name": "pocket calculator", + "unicode": "1F5A9", + "digest": "01b47b5c69c12b65fa4f4c0d580f2a98280d6116f4ad2cf8be378759008bcc3c" + }, { "name": "calendar", "unicode": "1F4C6", @@ -974,6 +1104,11 @@ "unicode": "1F5D3", "digest": "1dd5da98bb435c0c3f632bc0a5c9fdde694de7aee752bf4bb85def086e788a2a" }, + { + "name": "spiral_calendar_pad", + "unicode": "1F5D3", + "digest": "1dd5da98bb435c0c3f632bc0a5c9fdde694de7aee752bf4bb85def086e788a2a" + }, { "name": "calling", "unicode": "1F4F2", @@ -1034,6 +1169,11 @@ "unicode": "1F5C3", "digest": "7d760ae1d44e6f4b2aac00895ca86b5743f8b5ca157ec2bd21ce2665e50ad23a" }, + { + "name": "card_file_box", + "unicode": "1F5C3", + "digest": "7d760ae1d44e6f4b2aac00895ca86b5743f8b5ca157ec2bd21ce2665e50ad23a" + }, { "name": "card_index", "unicode": "1F4C7", @@ -1049,6 +1189,11 @@ "unicode": "1F5AD", "digest": "0b1625eea118060b51a70905c1eb3313ed632e989f70943eca16aa29fe8a34f2" }, + { + "name": "tape_cartridge", + "unicode": "1F5AD", + "digest": "0b1625eea118060b51a70905c1eb3313ed632e989f70943eca16aa29fe8a34f2" + }, { "name": "cat", "unicode": "1F431", @@ -1079,6 +1224,11 @@ "unicode": "1F37E", "digest": "77395d3afe5cc10bfdc381120bae2ae4aefdaa96c529536413873a696c5fa713" }, + { + "name": "bottle_with_popping_cork", + "unicode": "1F37E", + "digest": "77395d3afe5cc10bfdc381120bae2ae4aefdaa96c529536413873a696c5fa713" + }, { "name": "chart", "unicode": "1F4B9", @@ -1104,6 +1254,11 @@ "unicode": "1F9C0", "digest": "5897036ba97b557868bb314fcee83b9d8a609c8447b270a0b3d34a29ce7496d1" }, + { + "name": "cheese_wedge", + "unicode": "1F9C0", + "digest": "5897036ba97b557868bb314fcee83b9d8a609c8447b270a0b3d34a29ce7496d1" + }, { "name": "cherries", "unicode": "1F352", @@ -1169,6 +1324,11 @@ "unicode": "1F307", "digest": "c2530d12204eb518c5a3c8d7deba11170b1412fdf406aea05a69d4c026210d1b" }, + { + "name": "city_sunrise", + "unicode": "1F307", + "digest": "c2530d12204eb518c5a3c8d7deba11170b1412fdf406aea05a69d4c026210d1b" + }, { "name": "cityscape", "unicode": "1F3D9", @@ -1229,6 +1389,11 @@ "unicode": "1F570", "digest": "c48314ccde8bf01acc2b1bc9a6b5aa7d796fc0c8769f80398bc74545fcef31ed" }, + { + "name": "mantlepiece_clock", + "unicode": "1F570", + "digest": "c48314ccde8bf01acc2b1bc9a6b5aa7d796fc0c8769f80398bc74545fcef31ed" + }, { "name": "clock1", "unicode": "1F550", @@ -1354,6 +1519,11 @@ "unicode": "1F5D8", "digest": "67027b7e1a4d800a3ce7d731c21c098d1109d217159a27665eebb7e080fc2622" }, + { + "name": "clockwise_right_and_left_semicircle_arrows", + "unicode": "1F5D8", + "digest": "67027b7e1a4d800a3ce7d731c21c098d1109d217159a27665eebb7e080fc2622" + }, { "name": "closed_book", "unicode": "1F4D5", @@ -1379,21 +1549,41 @@ "unicode": "1F329", "digest": "fc9c85cc95f9c456635692c974f72b6d40e14943824b8129a21c47265c3416f4" }, + { + "name": "cloud_with_lightning", + "unicode": "1F329", + "digest": "fc9c85cc95f9c456635692c974f72b6d40e14943824b8129a21c47265c3416f4" + }, { "name": "cloud_rain", "unicode": "1F327", "digest": "f4406e62ed98f6141ab70736f6d5c540023e805396db0346ee6b7082c3f5e8e2" }, + { + "name": "cloud_with_rain", + "unicode": "1F327", + "digest": "f4406e62ed98f6141ab70736f6d5c540023e805396db0346ee6b7082c3f5e8e2" + }, { "name": "cloud_snow", "unicode": "1F328", "digest": "948990cd13dd927917208c026089519fcf8e258a8a284684ace67c9a2f9a8149" }, + { + "name": "cloud_with_snow", + "unicode": "1F328", + "digest": "948990cd13dd927917208c026089519fcf8e258a8a284684ace67c9a2f9a8149" + }, { "name": "cloud_tornado", "unicode": "1F32A", "digest": "44753516d0bd05d47cfa6eb922aba570ba6a87f805f325772b2cff071460ead1" }, + { + "name": "cloud_with_tornado", + "unicode": "1F32A", + "digest": "44753516d0bd05d47cfa6eb922aba570ba6a87f805f325772b2cff071460ead1" + }, { "name": "clubs", "unicode": "2663", @@ -1439,6 +1629,11 @@ "unicode": "1F5B3", "digest": "b27c30d74f205a8a3bd00a55ca17da7cf6ae3b65ae33e949755a4c6bd69a9fd3" }, + { + "name": "old_personal_computer", + "unicode": "1F5B3", + "digest": "b27c30d74f205a8a3bd00a55ca17da7cf6ae3b65ae33e949755a4c6bd69a9fd3" + }, { "name": "confetti_ball", "unicode": "1F38A", @@ -1509,6 +1704,11 @@ "unicode": "1F3D7", "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9" }, + { + "name": "building_construction", + "unicode": "1F3D7", + "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9" + }, { "name": "convenience_store", "unicode": "1F3EA", @@ -1569,6 +1769,11 @@ "unicode": "1F6CB", "digest": "a93fffed194b404200495abda8772bb35539cfc8499eb0a9bf09c508afad6676" }, + { + "name": "couch_and_lamp", + "unicode": "1F6CB", + "digest": "a93fffed194b404200495abda8772bb35539cfc8499eb0a9bf09c508afad6676" + }, { "name": "couple", "unicode": "1F46B", @@ -1579,6 +1784,11 @@ "unicode": "1F468-2764-1F468", "digest": "3ae6fbf3ba168256ea85c756ac1e7b83fdb8b780d33f06128ed80706ff627eea" }, + { + "name": "couple_with_heart_mm", + "unicode": "1F468-2764-1F468", + "digest": "3ae6fbf3ba168256ea85c756ac1e7b83fdb8b780d33f06128ed80706ff627eea" + }, { "name": "couple_with_heart", "unicode": "1F491", @@ -1589,6 +1799,11 @@ "unicode": "1F469-2764-1F469", "digest": "d2a2ec29c1a1234ea0aa1d9fc6707cf8be8bb36ea8b92523ffa1c3071bcf0b06" }, + { + "name": "couple_with_heart_ww", + "unicode": "1F469-2764-1F469", + "digest": "d2a2ec29c1a1234ea0aa1d9fc6707cf8be8bb36ea8b92523ffa1c3071bcf0b06" + }, { "name": "couplekiss", "unicode": "1F48F", @@ -1614,6 +1829,11 @@ "unicode": "1F58D", "digest": "0f3351c2e68a8d47d27b45a9901be6160de0f9a291bd8680df84d0fc679bcb31" }, + { + "name": "lower_left_crayon", + "unicode": "1F58D", + "digest": "0f3351c2e68a8d47d27b45a9901be6160de0f9a291bd8680df84d0fc679bcb31" + }, { "name": "credit_card", "unicode": "1F4B3", @@ -1629,6 +1849,11 @@ "unicode": "1F3CF", "digest": "00eb11254e887c71db5e8945ad211e9e0280f1e02f4b77a4799b64bba2bbe9b3" }, + { + "name": "cricket_bat_ball", + "unicode": "1F3CF", + "digest": "00eb11254e887c71db5e8945ad211e9e0280f1e02f4b77a4799b64bba2bbe9b3" + }, { "name": "crocodile", "unicode": "1F40A", @@ -1639,21 +1864,41 @@ "unicode": "271D", "digest": "a6e3c345cf6aa2ce690b66454066b53ef5b1dab2ed635e21f1586b1dffc5df42" }, + { + "name": "latin_cross", + "unicode": "271D", + "digest": "a6e3c345cf6aa2ce690b66454066b53ef5b1dab2ed635e21f1586b1dffc5df42" + }, { "name": "cross_heavy", "unicode": "1F547", "digest": "2e37c26b9bad0beb019c7f3e7a3892352d0ad9ca1b90c4333d42e8d56680be70" }, + { + "name": "heavy_latin_cross", + "unicode": "1F547", + "digest": "2e37c26b9bad0beb019c7f3e7a3892352d0ad9ca1b90c4333d42e8d56680be70" + }, { "name": "cross_white", "unicode": "1F546", "digest": "3452e667010d7e49a51d7e1f4ba8ed4f303e33ed43255a051e9a18832a1efba6" }, + { + "name": "white_latin_cross", + "unicode": "1F546", + "digest": "3452e667010d7e49a51d7e1f4ba8ed4f303e33ed43255a051e9a18832a1efba6" + }, { "name": "crossbones", "unicode": "1F571", "digest": "f5e7ce293c1a3282711073e68f033a3876e8428d1218cb2f8294630f9124e584" }, + { + "name": "black_skull_and_crossbones", + "unicode": "1F571", + "digest": "f5e7ce293c1a3282711073e68f033a3876e8428d1218cb2f8294630f9124e584" + }, { "name": "crossed_flags", "unicode": "1F38C", @@ -1674,6 +1919,11 @@ "unicode": "1F6F3", "digest": "90519c46ddfb63e71bc76661953da9041e5f0b97e9f8a7a8696518b4d529f3dd" }, + { + "name": "passenger_ship", + "unicode": "1F6F3", + "digest": "90519c46ddfb63e71bc76661953da9041e5f0b97e9f8a7a8696518b4d529f3dd" + }, { "name": "cry", "unicode": "1F622", @@ -1729,6 +1979,11 @@ "unicode": "1F5E1", "digest": "377060a7ce930566a4732b361be98e8a193a546846dfbba2a00abeeef41d1976" }, + { + "name": "dagger_knife", + "unicode": "1F5E1", + "digest": "377060a7ce930566a4732b361be98e8a193a546846dfbba2a00abeeef41d1976" + }, { "name": "dancer", "unicode": "1F483", @@ -1814,6 +2069,11 @@ "unicode": "1F5A5", "digest": "ba46323e695918e7253f1013cb991efb09790581c74c07c38bc5e10a20b8e8de" }, + { + "name": "desktop_computer", + "unicode": "1F5A5", + "digest": "ba46323e695918e7253f1013cb991efb09790581c74c07c38bc5e10a20b8e8de" + }, { "name": "desktop_window", "unicode": "1F5D4", @@ -1844,6 +2104,11 @@ "unicode": "1F5C2", "digest": "bf4c303452a4c0b4986925041dbec5b7e478060d560630b7c5bc2f997fcad668" }, + { + "name": "card_index_dividers", + "unicode": "1F5C2", + "digest": "bf4c303452a4c0b4986925041dbec5b7e478060d560630b7c5bc2f997fcad668" + }, { "name": "dizzy", "unicode": "1F4AB", @@ -1869,6 +2134,11 @@ "unicode": "1F5B9", "digest": "29407b12409c9673f3d89ef1f86ee50cbc7ed53b1870e33b4a29bbc609017f72" }, + { + "name": "document_with_text", + "unicode": "1F5B9", + "digest": "29407b12409c9673f3d89ef1f86ee50cbc7ed53b1870e33b4a29bbc609017f72" + }, { "name": "dog", "unicode": "1F436", @@ -1909,6 +2179,11 @@ "unicode": "1F54A", "digest": "4e2e9c47e5632efe6ccf945d61dbc2f1155a2e52905e17f307b502a2c951bdb8" }, + { + "name": "dove_of_peace", + "unicode": "1F54A", + "digest": "4e2e9c47e5632efe6ccf945d61dbc2f1155a2e52905e17f307b502a2c951bdb8" + }, { "name": "dragon", "unicode": "1F409", @@ -1944,6 +2219,11 @@ "unicode": "1F4E7", "digest": "12135310cfedc091d120426f5b132df82b538c5fcad458bf6b21588f353c3adb" }, + { + "name": "email", + "unicode": "1F4E7", + "digest": "12135310cfedc091d120426f5b132df82b538c5fcad458bf6b21588f353c3adb" + }, { "name": "ear", "unicode": "1F442", @@ -2044,21 +2324,41 @@ "unicode": "1F582", "digest": "bc60b6d375feee00758a94a05b42eeb165f4084b20eb3e6012b72faa221f7e75" }, + { + "name": "back_of_envelope", + "unicode": "1F582", + "digest": "bc60b6d375feee00758a94a05b42eeb165f4084b20eb3e6012b72faa221f7e75" + }, { "name": "envelope_flying", "unicode": "1F585", "digest": "9d6b6ca4c08006062a6f11948de3e15b13cf5c458967e39a9358665a8e13e214" }, + { + "name": "flying_envelope", + "unicode": "1F585", + "digest": "9d6b6ca4c08006062a6f11948de3e15b13cf5c458967e39a9358665a8e13e214" + }, { "name": "envelope_stamped", "unicode": "1F583", "digest": "f6102aea7283ddc136bfeb09589573420b9279105045fc6b965c1633c1297468" }, + { + "name": "stamped_envelope", + "unicode": "1F583", + "digest": "f6102aea7283ddc136bfeb09589573420b9279105045fc6b965c1633c1297468" + }, { "name": "envelope_stamped_pen", "unicode": "1F586", "digest": "80ea471318d1e04f8e525ff236b3cd4a4c864e66c6246b6aad77d92f56895f33" }, + { + "name": "pen_over_stamped_envelope", + "unicode": "1F586", + "digest": "80ea471318d1e04f8e525ff236b3cd4a4c864e66c6246b6aad77d92f56895f33" + }, { "name": "envelope_with_arrow", "unicode": "1F4E9", @@ -2254,31 +2554,61 @@ "unicode": "1F597", "digest": "0c542ac3141e8f2e74767acd0eb399c2d68c779cb78bf16d437ad3b1f8134ad9" }, + { + "name": "white_down_pointing_left_hand_index", + "unicode": "1F597", + "digest": "0c542ac3141e8f2e74767acd0eb399c2d68c779cb78bf16d437ad3b1f8134ad9" + }, { "name": "finger_pointing_down2", "unicode": "1F59F", "digest": "c5b128a232cbf518544802a2ae1459368274297163721fa05d0103cf95b2b1ee" }, + { + "name": "sideways_white_down_pointing_index", + "unicode": "1F59F", + "digest": "c5b128a232cbf518544802a2ae1459368274297163721fa05d0103cf95b2b1ee" + }, { "name": "finger_pointing_left", "unicode": "1F598", "digest": "d178ece691e2091be08db77fda9cf05462934628557358a8cb6222587b291f7e" }, + { + "name": "sideways_white_left_pointing_index", + "unicode": "1F598", + "digest": "d178ece691e2091be08db77fda9cf05462934628557358a8cb6222587b291f7e" + }, { "name": "finger_pointing_right", "unicode": "1F599", "digest": "a412a47544d8f401f9181f8826c5fa3d6b42a1d76f6926963c2d9cd2a01be06d" }, + { + "name": "sideways_white_right_pointing_index", + "unicode": "1F599", + "digest": "a412a47544d8f401f9181f8826c5fa3d6b42a1d76f6926963c2d9cd2a01be06d" + }, { "name": "finger_pointing_up", "unicode": "1F59E", "digest": "32c2ccab52aa318a47c816d1bcf9c076e667c9ef3e64ce37d7ba7e827238690d" }, + { + "name": "sideways_white_up_pointing_index", + "unicode": "1F59E", + "digest": "32c2ccab52aa318a47c816d1bcf9c076e667c9ef3e64ce37d7ba7e827238690d" + }, { "name": "fire", "unicode": "1F525", "digest": "b44311874681135acbb5e7226febe4365c732da3a9617f10d7074a3b1ade1641" }, + { + "name": "flame", + "unicode": "1F525", + "digest": "b44311874681135acbb5e7226febe4365c732da3a9617f10d7074a3b1ade1641" + }, { "name": "fire_engine", "unicode": "1F692", @@ -2289,6 +2619,11 @@ "unicode": "1F6F1", "digest": "e2482c450136d373f74dfafddf502e0b675eb5d2e1e1c645f163db0e4d15fbb6" }, + { + "name": "oncoming_fire_engine", + "unicode": "1F6F1", + "digest": "e2482c450136d373f74dfafddf502e0b675eb5d2e1e1c645f163db0e4d15fbb6" + }, { "name": "fireworks", "unicode": "1F386", @@ -2359,1296 +2694,2596 @@ "unicode": "1F1E6-1F1E8", "digest": "d9db1edeb709824a1083c2bba79ca5f683ed0edded35918bb167d1ee7396c8da" }, + { + "name": "ac", + "unicode": "1F1E6-1F1E8", + "digest": "d9db1edeb709824a1083c2bba79ca5f683ed0edded35918bb167d1ee7396c8da" + }, { "name": "flag_ad", "unicode": "1F1E6-1F1E9", "digest": "04a8c1745d9b8b20e903302379f2557e8082f72e33878db4cb2cd6b33eb97952" }, + { + "name": "ad", + "unicode": "1F1E6-1F1E9", + "digest": "04a8c1745d9b8b20e903302379f2557e8082f72e33878db4cb2cd6b33eb97952" + }, { "name": "flag_ae", "unicode": "1F1E6-1F1EA", "digest": "868324ac2e7bea1547f5de95f39633b77b8d62f3b3433b3d1a4ee96d169a09cd" }, + { + "name": "ae", + "unicode": "1F1E6-1F1EA", + "digest": "868324ac2e7bea1547f5de95f39633b77b8d62f3b3433b3d1a4ee96d169a09cd" + }, { "name": "flag_af", "unicode": "1F1E6-1F1EB", "digest": "9a94458519e9db5d6cf1557e54fdf62d7e48aaf7de25744a093ec8f284656226" }, + { + "name": "af", + "unicode": "1F1E6-1F1EB", + "digest": "9a94458519e9db5d6cf1557e54fdf62d7e48aaf7de25744a093ec8f284656226" + }, { "name": "flag_ag", "unicode": "1F1E6-1F1EC", "digest": "ea59fabc2bd9024df06a59a34412f52bebfeb03eb6abd73d8fe153e3a68e28f4" }, + { + "name": "ag", + "unicode": "1F1E6-1F1EC", + "digest": "ea59fabc2bd9024df06a59a34412f52bebfeb03eb6abd73d8fe153e3a68e28f4" + }, { "name": "flag_ai", "unicode": "1F1E6-1F1EE", "digest": "75676ded736ad2ebb921e9fd8ebfef49819a35c3dcf005bbc3b7e8c6e75178f2" }, + { + "name": "ai", + "unicode": "1F1E6-1F1EE", + "digest": "75676ded736ad2ebb921e9fd8ebfef49819a35c3dcf005bbc3b7e8c6e75178f2" + }, { "name": "flag_al", "unicode": "1F1E6-1F1F1", "digest": "77b835dcff399b609e2479cbf10f08344c8fc277370ba8e4540165ca15563847" }, + { + "name": "al", + "unicode": "1F1E6-1F1F1", + "digest": "77b835dcff399b609e2479cbf10f08344c8fc277370ba8e4540165ca15563847" + }, { "name": "flag_am", "unicode": "1F1E6-1F1F2", "digest": "3b820c628dd5a93137f7288a43553778f60b0beea4c0a239d063893c0723e73d" }, + { + "name": "am", + "unicode": "1F1E6-1F1F2", + "digest": "3b820c628dd5a93137f7288a43553778f60b0beea4c0a239d063893c0723e73d" + }, { "name": "flag_ao", "unicode": "1F1E6-1F1F4", "digest": "d26439d4ecbe8b67bb1ae9753454505358ebb6b802624f19800471e53ee27187" }, + { + "name": "ao", + "unicode": "1F1E6-1F1F4", + "digest": "d26439d4ecbe8b67bb1ae9753454505358ebb6b802624f19800471e53ee27187" + }, { "name": "flag_aq", "unicode": "1F1E6-1F1F6", "digest": "6b0b4e800d88ab289ae4b6d449bfa115e92543958b477d13ad348468a74e4616" }, + { + "name": "aq", + "unicode": "1F1E6-1F1F6", + "digest": "6b0b4e800d88ab289ae4b6d449bfa115e92543958b477d13ad348468a74e4616" + }, { "name": "flag_ar", "unicode": "1F1E6-1F1F7", "digest": "ca76db601dd3f5794f1caace8ab5641fe3786b86e4ae030706162f0ce07d27b3" }, + { + "name": "ar", + "unicode": "1F1E6-1F1F7", + "digest": "ca76db601dd3f5794f1caace8ab5641fe3786b86e4ae030706162f0ce07d27b3" + }, { "name": "flag_as", "unicode": "1F1E6-1F1F8", "digest": "170e1dde0e3fd2e0f2149de5cc8845e15580cc0412e81a643d61bd387de16141" }, + { + "name": "as", + "unicode": "1F1E6-1F1F8", + "digest": "170e1dde0e3fd2e0f2149de5cc8845e15580cc0412e81a643d61bd387de16141" + }, { "name": "flag_at", "unicode": "1F1E6-1F1F9", "digest": "0ab3675a16b4988e87c81e87453c160d6616c7be76247f54c471dc63aa8b42ba" }, + { + "name": "at", + "unicode": "1F1E6-1F1F9", + "digest": "0ab3675a16b4988e87c81e87453c160d6616c7be76247f54c471dc63aa8b42ba" + }, { "name": "flag_au", "unicode": "1F1E6-1F1FA", "digest": "b6f17d3dfd3547c069a0b6cddd4cf44fb8ce1d1d300e24284fb292ac142537e3" }, + { + "name": "au", + "unicode": "1F1E6-1F1FA", + "digest": "b6f17d3dfd3547c069a0b6cddd4cf44fb8ce1d1d300e24284fb292ac142537e3" + }, { "name": "flag_aw", "unicode": "1F1E6-1F1FC", "digest": "7857bc907f04dfb7ccc4401c05034ad8afb6383a022db77973cfcafa4d6c16c8" }, + { + "name": "aw", + "unicode": "1F1E6-1F1FC", + "digest": "7857bc907f04dfb7ccc4401c05034ad8afb6383a022db77973cfcafa4d6c16c8" + }, { "name": "flag_ax", "unicode": "1F1E6-1F1FD", "digest": "ab8f1fd4af7c220a54d478cec5a9f7f3beb5fc83439c448f3ac9848af8391ac1" }, + { + "name": "ax", + "unicode": "1F1E6-1F1FD", + "digest": "ab8f1fd4af7c220a54d478cec5a9f7f3beb5fc83439c448f3ac9848af8391ac1" + }, { "name": "flag_az", "unicode": "1F1E6-1F1FF", "digest": "187cc7b6d39800c5910a34409db1e6b1d8aac808c72a93e922a419d9b054fd0b" }, + { + "name": "az", + "unicode": "1F1E6-1F1FF", + "digest": "187cc7b6d39800c5910a34409db1e6b1d8aac808c72a93e922a419d9b054fd0b" + }, { "name": "flag_ba", "unicode": "1F1E7-1F1E6", "digest": "cd22c744213087384cf79ed314742026787212c9ceb6999ed166534670f7864a" }, + { + "name": "ba", + "unicode": "1F1E7-1F1E6", + "digest": "cd22c744213087384cf79ed314742026787212c9ceb6999ed166534670f7864a" + }, { "name": "flag_bb", "unicode": "1F1E7-1F1E7", "digest": "44ff0a48ac2d2180374baa58b1b7c64f26d0d151a48811eb08ffa20758104512" }, + { + "name": "bb", + "unicode": "1F1E7-1F1E7", + "digest": "44ff0a48ac2d2180374baa58b1b7c64f26d0d151a48811eb08ffa20758104512" + }, { "name": "flag_bd", "unicode": "1F1E7-1F1E9", "digest": "c18793d2b963458607a0bab94c57e62c8278fce870e96fd8dda78067a8fbde18" }, + { + "name": "bd", + "unicode": "1F1E7-1F1E9", + "digest": "c18793d2b963458607a0bab94c57e62c8278fce870e96fd8dda78067a8fbde18" + }, { "name": "flag_be", "unicode": "1F1E7-1F1EA", "digest": "6e6ccfca064a43b93c8acc04a9425f95af204198022ca20b9ee6c491e99ad950" }, + { + "name": "be", + "unicode": "1F1E7-1F1EA", + "digest": "6e6ccfca064a43b93c8acc04a9425f95af204198022ca20b9ee6c491e99ad950" + }, { "name": "flag_bf", "unicode": "1F1E7-1F1EB", "digest": "d69c0394a1c7cb6323f54f024b7d740c728f229ca5e1b54ac374d5024f5470a5" }, + { + "name": "bf", + "unicode": "1F1E7-1F1EB", + "digest": "d69c0394a1c7cb6323f54f024b7d740c728f229ca5e1b54ac374d5024f5470a5" + }, { "name": "flag_bg", "unicode": "1F1E7-1F1EC", "digest": "413a270caf4a9155e84bdba6c9512277f5642246f6ba8d701383a5eeb02f7e95" }, + { + "name": "bg", + "unicode": "1F1E7-1F1EC", + "digest": "413a270caf4a9155e84bdba6c9512277f5642246f6ba8d701383a5eeb02f7e95" + }, { "name": "flag_bh", "unicode": "1F1E7-1F1ED", "digest": "9243ed65d7f24c824c2a3207335a2d4ad25251258547c16d0b7b7cbb9df6f8de" }, + { + "name": "bh", + "unicode": "1F1E7-1F1ED", + "digest": "9243ed65d7f24c824c2a3207335a2d4ad25251258547c16d0b7b7cbb9df6f8de" + }, { "name": "flag_bi", "unicode": "1F1E7-1F1EE", "digest": "63056519030524b2d2dcd47448267d817205dbd6b98075c97f011a8f1d4d1a4b" }, + { + "name": "bi", + "unicode": "1F1E7-1F1EE", + "digest": "63056519030524b2d2dcd47448267d817205dbd6b98075c97f011a8f1d4d1a4b" + }, { "name": "flag_bj", "unicode": "1F1E7-1F1EF", "digest": "93b245eed85d22260d27d1a8c77f51fb3439309e09b2aeca6cd504dbea77b509" }, + { + "name": "bj", + "unicode": "1F1E7-1F1EF", + "digest": "93b245eed85d22260d27d1a8c77f51fb3439309e09b2aeca6cd504dbea77b509" + }, { "name": "flag_bl", "unicode": "1F1E7-1F1F1", "digest": "5e1e478deaf02bbaa26595e4cefc5f5c9bec6105ce521b7b9ab4fa5e7a452c14" }, + { + "name": "bl", + "unicode": "1F1E7-1F1F1", + "digest": "5e1e478deaf02bbaa26595e4cefc5f5c9bec6105ce521b7b9ab4fa5e7a452c14" + }, { "name": "flag_black", "unicode": "1F3F4", "digest": "df131e5c28e9f51dea53fe7f33551f91d420f7d686b7a62980f0154c6b5357a6" }, + { + "name": "waving_black_flag", + "unicode": "1F3F4", + "digest": "df131e5c28e9f51dea53fe7f33551f91d420f7d686b7a62980f0154c6b5357a6" + }, { "name": "flag_bm", "unicode": "1F1E7-1F1F2", "digest": "9dcd9e60faebe7f93eb19157e99f2ad654a8145c61738de96e6ecd11a246764a" }, + { + "name": "bm", + "unicode": "1F1E7-1F1F2", + "digest": "9dcd9e60faebe7f93eb19157e99f2ad654a8145c61738de96e6ecd11a246764a" + }, { "name": "flag_bn", "unicode": "1F1E7-1F1F3", "digest": "078af6ca481a77871ba005e251a46ce63951c27b1b0cd33b9c1d0d31d349bc1a" }, + { + "name": "bn", + "unicode": "1F1E7-1F1F3", + "digest": "078af6ca481a77871ba005e251a46ce63951c27b1b0cd33b9c1d0d31d349bc1a" + }, { "name": "flag_bo", "unicode": "1F1E7-1F1F4", "digest": "92516d04e922a3bcbabe2e7619194bc972c09ba97576e8155f9829c397a71d8c" }, + { + "name": "bo", + "unicode": "1F1E7-1F1F4", + "digest": "92516d04e922a3bcbabe2e7619194bc972c09ba97576e8155f9829c397a71d8c" + }, { "name": "flag_bq", "unicode": "1F1E7-1F1F6", "digest": "7832df5267a2bb8dddb83aeb11162ce79aeebdb718f2ac0e54adcf3d87936171" }, + { + "name": "bq", + "unicode": "1F1E7-1F1F6", + "digest": "7832df5267a2bb8dddb83aeb11162ce79aeebdb718f2ac0e54adcf3d87936171" + }, { "name": "flag_br", "unicode": "1F1E7-1F1F7", "digest": "aabcc1c082124045ed214f7d9778d8e2ed791ebb8433defea91db458658abeec" }, + { + "name": "br", + "unicode": "1F1E7-1F1F7", + "digest": "aabcc1c082124045ed214f7d9778d8e2ed791ebb8433defea91db458658abeec" + }, { "name": "flag_bs", "unicode": "1F1E7-1F1F8", "digest": "f628f39003608e181696634929522884165e27ccef55270293f92eeef991635f" }, + { + "name": "bs", + "unicode": "1F1E7-1F1F8", + "digest": "f628f39003608e181696634929522884165e27ccef55270293f92eeef991635f" + }, { "name": "flag_bt", "unicode": "1F1E7-1F1F9", "digest": "af24a8ab34815da04c3e5af49a47449e0de93b068957cbda695816d0f830ca12" }, + { + "name": "bt", + "unicode": "1F1E7-1F1F9", + "digest": "af24a8ab34815da04c3e5af49a47449e0de93b068957cbda695816d0f830ca12" + }, { "name": "flag_bv", "unicode": "1F1E7-1F1FB", "digest": "ff0037f6eed95d4bb5f2b502902360e1ff41426e2896daf3e0730cef1f8f7e41" }, + { + "name": "bv", + "unicode": "1F1E7-1F1FB", + "digest": "ff0037f6eed95d4bb5f2b502902360e1ff41426e2896daf3e0730cef1f8f7e41" + }, { "name": "flag_bw", "unicode": "1F1E7-1F1FC", "digest": "3e3241ecb97946cc3e467b083d113a57dd305595e1512d4da18cc403e8689c1d" }, + { + "name": "bw", + "unicode": "1F1E7-1F1FC", + "digest": "3e3241ecb97946cc3e467b083d113a57dd305595e1512d4da18cc403e8689c1d" + }, { "name": "flag_by", "unicode": "1F1E7-1F1FE", "digest": "bdd21885c6fac475241884a44149b887297772e17617ee59dd9fe8518d52cf3d" }, + { + "name": "by", + "unicode": "1F1E7-1F1FE", + "digest": "bdd21885c6fac475241884a44149b887297772e17617ee59dd9fe8518d52cf3d" + }, { "name": "flag_bz", "unicode": "1F1E7-1F1FF", "digest": "21c16e1da641af004576000bf1db44b9a1e0fccfddc775e96022721c2f18eeea" }, + { + "name": "bz", + "unicode": "1F1E7-1F1FF", + "digest": "21c16e1da641af004576000bf1db44b9a1e0fccfddc775e96022721c2f18eeea" + }, { "name": "flag_ca", "unicode": "1F1E8-1F1E6", "digest": "0d00e459084d58d3ea9c60488a9e51bf45f71b77f1600f190225d5ca6ca6c796" }, + { + "name": "ca", + "unicode": "1F1E8-1F1E6", + "digest": "0d00e459084d58d3ea9c60488a9e51bf45f71b77f1600f190225d5ca6ca6c796" + }, { "name": "flag_cc", "unicode": "1F1E8-1F1E8", "digest": "86ab27164603ef0f1f83fe898eda6fbb7bc5709f2518f5577f00817860806a7b" }, + { + "name": "cc", + "unicode": "1F1E8-1F1E8", + "digest": "86ab27164603ef0f1f83fe898eda6fbb7bc5709f2518f5577f00817860806a7b" + }, { "name": "flag_cd", "unicode": "1F1E8-1F1E9", "digest": "fdc2796530ada4bd0bae37ace4bbe707b321b287dcd64568f8e01d3a9df56066" }, + { + "name": "congo", + "unicode": "1F1E8-1F1E9", + "digest": "fdc2796530ada4bd0bae37ace4bbe707b321b287dcd64568f8e01d3a9df56066" + }, { "name": "flag_cf", "unicode": "1F1E8-1F1EB", "digest": "5943bec02bede0931e21e7c34a68f375499f60a34883cc1edf2f21e9834b15ce" }, + { + "name": "cf", + "unicode": "1F1E8-1F1EB", + "digest": "5943bec02bede0931e21e7c34a68f375499f60a34883cc1edf2f21e9834b15ce" + }, { "name": "flag_cg", "unicode": "1F1E8-1F1EC", "digest": "54498482e2772371e148e05cfb7c5eb55f6a22cd528662abdea10bad47d157da" }, + { + "name": "cg", + "unicode": "1F1E8-1F1EC", + "digest": "54498482e2772371e148e05cfb7c5eb55f6a22cd528662abdea10bad47d157da" + }, { "name": "flag_ch", "unicode": "1F1E8-1F1ED", "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386" }, + { + "name": "ch", + "unicode": "1F1E8-1F1ED", + "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386" + }, { "name": "flag_ci", "unicode": "1F1E8-1F1EE", "digest": "3a173a3058a5c0174dc88750852cafec264e901ce82a6c69db122c8c0ea71a3a" }, + { + "name": "ci", + "unicode": "1F1E8-1F1EE", + "digest": "3a173a3058a5c0174dc88750852cafec264e901ce82a6c69db122c8c0ea71a3a" + }, { "name": "flag_ck", "unicode": "1F1E8-1F1F0", "digest": "42f395ff53c618b72b8a224cd4343d1a32f5ad82ced56bf590170a5ff0d5134c" }, + { + "name": "ck", + "unicode": "1F1E8-1F1F0", + "digest": "42f395ff53c618b72b8a224cd4343d1a32f5ad82ced56bf590170a5ff0d5134c" + }, { "name": "flag_cl", "unicode": "1F1E8-1F1F1", "digest": "9d6255feb690596904d800e72d5acdb5cda941c5a741b031ea39a3c7650ac46f" }, + { + "name": "chile", + "unicode": "1F1E8-1F1F1", + "digest": "9d6255feb690596904d800e72d5acdb5cda941c5a741b031ea39a3c7650ac46f" + }, { "name": "flag_cm", "unicode": "1F1E8-1F1F2", "digest": "ffc99d14e0a8b46a980331090ed9f36f31a87f1b0f8dd8c09007a31c6127c69e" }, + { + "name": "cm", + "unicode": "1F1E8-1F1F2", + "digest": "ffc99d14e0a8b46a980331090ed9f36f31a87f1b0f8dd8c09007a31c6127c69e" + }, { "name": "flag_cn", "unicode": "1F1E8-1F1F3", "digest": "869a98c52bdc33591f87e2aab6cb4f13e98bb19136250ff25805d0312a8b7c8a" }, + { + "name": "cn", + "unicode": "1F1E8-1F1F3", + "digest": "869a98c52bdc33591f87e2aab6cb4f13e98bb19136250ff25805d0312a8b7c8a" + }, { "name": "flag_co", "unicode": "1F1E8-1F1F4", "digest": "6aa458440eb2500ad307fea40fd8f1171a1506a6e32af144a4fd51545bb56151" }, + { + "name": "co", + "unicode": "1F1E8-1F1F4", + "digest": "6aa458440eb2500ad307fea40fd8f1171a1506a6e32af144a4fd51545bb56151" + }, { "name": "flag_cp", "unicode": "1F1E8-1F1F5", "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" }, + { + "name": "cp", + "unicode": "1F1E8-1F1F5", + "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" + }, { "name": "flag_cr", "unicode": "1F1E8-1F1F7", "digest": "0f3b54d8330c5bb136647547dafc598bda755697cfd6b7d872a2443ba7b5cad4" }, + { + "name": "cr", + "unicode": "1F1E8-1F1F7", + "digest": "0f3b54d8330c5bb136647547dafc598bda755697cfd6b7d872a2443ba7b5cad4" + }, { "name": "flag_cu", "unicode": "1F1E8-1F1FA", "digest": "69bc973002475bb3d9b54cb0ba9ec9cb85f144c1cf54689da0ee8f414ebb0d83" }, + { + "name": "cu", + "unicode": "1F1E8-1F1FA", + "digest": "69bc973002475bb3d9b54cb0ba9ec9cb85f144c1cf54689da0ee8f414ebb0d83" + }, { "name": "flag_cv", "unicode": "1F1E8-1F1FB", "digest": "af2e135cf3c1b03a5937c068a75061b5cd332e95902fd0f8dffb2ac2dc89692a" }, + { + "name": "cv", + "unicode": "1F1E8-1F1FB", + "digest": "af2e135cf3c1b03a5937c068a75061b5cd332e95902fd0f8dffb2ac2dc89692a" + }, { "name": "flag_cw", "unicode": "1F1E8-1F1FC", "digest": "df4b2228a82f766c5c64c13c1388482a68549e59dd843671ee0eb43506e33411" }, + { + "name": "cw", + "unicode": "1F1E8-1F1FC", + "digest": "df4b2228a82f766c5c64c13c1388482a68549e59dd843671ee0eb43506e33411" + }, { "name": "flag_cx", "unicode": "1F1E8-1F1FD", "digest": "db12e513345a7be53954167d359ede0b3effbfb292508ee4d726123e3a8f83d7" }, + { + "name": "cx", + "unicode": "1F1E8-1F1FD", + "digest": "db12e513345a7be53954167d359ede0b3effbfb292508ee4d726123e3a8f83d7" + }, { "name": "flag_cy", "unicode": "1F1E8-1F1FE", "digest": "0cea41d4820746e2c6eb408f7ec7419afba9f7396401d92e6c1d77382f721d0b" }, + { + "name": "cy", + "unicode": "1F1E8-1F1FE", + "digest": "0cea41d4820746e2c6eb408f7ec7419afba9f7396401d92e6c1d77382f721d0b" + }, { "name": "flag_cz", "unicode": "1F1E8-1F1FF", "digest": "a1c2405916963be306f761539123486a2845af53716c9dfe94ad5420e14d36c4" }, + { + "name": "cz", + "unicode": "1F1E8-1F1FF", + "digest": "a1c2405916963be306f761539123486a2845af53716c9dfe94ad5420e14d36c4" + }, { "name": "flag_de", "unicode": "1F1E9-1F1EA", "digest": "74a80b64437bc4e31bdd7cbb753ecd2d719bf34c506cbac535db83a644174cce" }, + { + "name": "de", + "unicode": "1F1E9-1F1EA", + "digest": "74a80b64437bc4e31bdd7cbb753ecd2d719bf34c506cbac535db83a644174cce" + }, { "name": "flag_dg", "unicode": "1F1E9-1F1EC", "digest": "13cb5ea872f94a9c3fb579cef417e2d1ed38e8cbe95059576380cacd59bc4b9d" }, + { + "name": "dg", + "unicode": "1F1E9-1F1EC", + "digest": "13cb5ea872f94a9c3fb579cef417e2d1ed38e8cbe95059576380cacd59bc4b9d" + }, { "name": "flag_dj", "unicode": "1F1E9-1F1EF", "digest": "5b479654c28d3eeb70055c5e25dc46ccaba9eeea7537cc45ca9dbb8186b743b6" }, + { + "name": "dj", + "unicode": "1F1E9-1F1EF", + "digest": "5b479654c28d3eeb70055c5e25dc46ccaba9eeea7537cc45ca9dbb8186b743b6" + }, { "name": "flag_dk", "unicode": "1F1E9-1F1F0", "digest": "dee7fa9644a9b447417518a353e7edcbb37b2af8bc7d13a6ed71d7210c43ca3c" }, + { + "name": "dk", + "unicode": "1F1E9-1F1F0", + "digest": "dee7fa9644a9b447417518a353e7edcbb37b2af8bc7d13a6ed71d7210c43ca3c" + }, { "name": "flag_dm", "unicode": "1F1E9-1F1F2", "digest": "2e339190a8a0a238140f42e329f6646af5be75763a787ea268488a2e0440dc4c" }, + { + "name": "dm", + "unicode": "1F1E9-1F1F2", + "digest": "2e339190a8a0a238140f42e329f6646af5be75763a787ea268488a2e0440dc4c" + }, { "name": "flag_do", "unicode": "1F1E9-1F1F4", "digest": "be5dafcd32d7197a96d37299a91835a8009299452f05a66d91c5fdec17448230" }, + { + "name": "do", + "unicode": "1F1E9-1F1F4", + "digest": "be5dafcd32d7197a96d37299a91835a8009299452f05a66d91c5fdec17448230" + }, { "name": "flag_dz", "unicode": "1F1E9-1F1FF", "digest": "cf525d56bac45fe689f92d441274fc0ecbed4f95591d2c066598f72b1ee8d618" }, + { + "name": "dz", + "unicode": "1F1E9-1F1FF", + "digest": "cf525d56bac45fe689f92d441274fc0ecbed4f95591d2c066598f72b1ee8d618" + }, { "name": "flag_ea", "unicode": "1F1EA-1F1E6", "digest": "1acb13950f7c3692f9a36e618d8ec10a73ead5d7fa80fb52b6b2a18e3d456002" }, + { + "name": "ea", + "unicode": "1F1EA-1F1E6", + "digest": "1acb13950f7c3692f9a36e618d8ec10a73ead5d7fa80fb52b6b2a18e3d456002" + }, { "name": "flag_ec", "unicode": "1F1EA-1F1E8", "digest": "4d9d35450efc6026651ccc2278e70fb90b001ca5e5eecd31361b1e4e23253dbd" }, + { + "name": "ec", + "unicode": "1F1EA-1F1E8", + "digest": "4d9d35450efc6026651ccc2278e70fb90b001ca5e5eecd31361b1e4e23253dbd" + }, { "name": "flag_ee", "unicode": "1F1EA-1F1EA", "digest": "86ec7b2f618fe71dddec3d5a621b56b878d683780f1e0ad446f965326d42df48" }, + { + "name": "ee", + "unicode": "1F1EA-1F1EA", + "digest": "86ec7b2f618fe71dddec3d5a621b56b878d683780f1e0ad446f965326d42df48" + }, { "name": "flag_eg", "unicode": "1F1EA-1F1EC", "digest": "f06d36a6fec15af4c1a76de30e8469847dde2728bb5a48956b4e466098b778a4" }, + { + "name": "eg", + "unicode": "1F1EA-1F1EC", + "digest": "f06d36a6fec15af4c1a76de30e8469847dde2728bb5a48956b4e466098b778a4" + }, { "name": "flag_eh", "unicode": "1F1EA-1F1ED", "digest": "eb63f5b92c62c98dc008dfa7ad8830aa17fa23964f812a28055bd8b6f5960c5b" }, + { + "name": "eh", + "unicode": "1F1EA-1F1ED", + "digest": "eb63f5b92c62c98dc008dfa7ad8830aa17fa23964f812a28055bd8b6f5960c5b" + }, { "name": "flag_er", "unicode": "1F1EA-1F1F7", "digest": "e901195f7b37b22a6872d36713de0ec176f6424c209e261e5c849ce318c772f6" }, + { + "name": "er", + "unicode": "1F1EA-1F1F7", + "digest": "e901195f7b37b22a6872d36713de0ec176f6424c209e261e5c849ce318c772f6" + }, { "name": "flag_es", "unicode": "1F1EA-1F1F8", "digest": "27ab5cc6c2e9f26ccdfa632887533eebcd9b514f80cec9e721cf8e5e2544339c" }, + { + "name": "es", + "unicode": "1F1EA-1F1F8", + "digest": "27ab5cc6c2e9f26ccdfa632887533eebcd9b514f80cec9e721cf8e5e2544339c" + }, { "name": "flag_et", "unicode": "1F1EA-1F1F9", "digest": "6cdb3718c9b3ec713258dd36781db58b7da53f3017445056c1a76233e3b4a7de" }, + { + "name": "et", + "unicode": "1F1EA-1F1F9", + "digest": "6cdb3718c9b3ec713258dd36781db58b7da53f3017445056c1a76233e3b4a7de" + }, { "name": "flag_eu", "unicode": "1F1EA-1F1FA", "digest": "363f60e8a747166d5cec8d70bfdf266411eec2ff07933b6187975075caadfd74" }, + { + "name": "eu", + "unicode": "1F1EA-1F1FA", + "digest": "363f60e8a747166d5cec8d70bfdf266411eec2ff07933b6187975075caadfd74" + }, { "name": "flag_fi", "unicode": "1F1EB-1F1EE", "digest": "1a1959cb551a0e8bdaee8c04657fb7387a4d83173f7759f89468da12e1818a9e" }, + { + "name": "fi", + "unicode": "1F1EB-1F1EE", + "digest": "1a1959cb551a0e8bdaee8c04657fb7387a4d83173f7759f89468da12e1818a9e" + }, { "name": "flag_fj", "unicode": "1F1EB-1F1EF", "digest": "f26dc36ea9c1f32d9bb54874ea384e7118b6e2585be69245fdd73acd8304ae78" }, + { + "name": "fj", + "unicode": "1F1EB-1F1EF", + "digest": "f26dc36ea9c1f32d9bb54874ea384e7118b6e2585be69245fdd73acd8304ae78" + }, { "name": "flag_fk", "unicode": "1F1EB-1F1F0", "digest": "0479e233499b704f91a9b13d083e66296efe2f28ed917ab1496b223bfb09adb8" }, + { + "name": "fk", + "unicode": "1F1EB-1F1F0", + "digest": "0479e233499b704f91a9b13d083e66296efe2f28ed917ab1496b223bfb09adb8" + }, { "name": "flag_fm", "unicode": "1F1EB-1F1F2", "digest": "142ea7b4b4a7004329925b495da43ab82351cbaac383c8da6e614b39ba58d05e" }, + { + "name": "fm", + "unicode": "1F1EB-1F1F2", + "digest": "142ea7b4b4a7004329925b495da43ab82351cbaac383c8da6e614b39ba58d05e" + }, { "name": "flag_fo", "unicode": "1F1EB-1F1F4", "digest": "f1c800d4f4d39e2aead9a11ed500f16108d6bc48bd24bd2a1af7b966d8e76752" }, + { + "name": "fo", + "unicode": "1F1EB-1F1F4", + "digest": "f1c800d4f4d39e2aead9a11ed500f16108d6bc48bd24bd2a1af7b966d8e76752" + }, { "name": "flag_fr", "unicode": "1F1EB-1F1F7", "digest": "6f52f36b5199c65ab1cad13ff4e77d2d8b48a8ff79b92166976674ffdc7829ee" }, + { + "name": "fr", + "unicode": "1F1EB-1F1F7", + "digest": "6f52f36b5199c65ab1cad13ff4e77d2d8b48a8ff79b92166976674ffdc7829ee" + }, { "name": "flag_ga", "unicode": "1F1EC-1F1E6", "digest": "50a0d5a07466e419b74a4d532738f7958de9baa37df6191be4f3755dccc3b326" }, + { + "name": "ga", + "unicode": "1F1EC-1F1E6", + "digest": "50a0d5a07466e419b74a4d532738f7958de9baa37df6191be4f3755dccc3b326" + }, { "name": "flag_gb", "unicode": "1F1EC-1F1E7", "digest": "220f7da6d5a231b766c79f2e1b7d3fdb74ec0c0c17558cc00a8a8ccdf2afc2e0" }, + { + "name": "gb", + "unicode": "1F1EC-1F1E7", + "digest": "220f7da6d5a231b766c79f2e1b7d3fdb74ec0c0c17558cc00a8a8ccdf2afc2e0" + }, { "name": "flag_gd", "unicode": "1F1EC-1F1E9", "digest": "3e162b0d13f4ceea7f663b1d425f13863d104e80df75a640f526e276bcd04081" }, + { + "name": "gd", + "unicode": "1F1EC-1F1E9", + "digest": "3e162b0d13f4ceea7f663b1d425f13863d104e80df75a640f526e276bcd04081" + }, { "name": "flag_ge", "unicode": "1F1EC-1F1EA", "digest": "35897f8254675d2efe9e3070c88af9ef214f08440e6ee75ebe81d28cdb57ea2b" }, + { + "name": "ge", + "unicode": "1F1EC-1F1EA", + "digest": "35897f8254675d2efe9e3070c88af9ef214f08440e6ee75ebe81d28cdb57ea2b" + }, { "name": "flag_gf", "unicode": "1F1EC-1F1EB", "digest": "3a34df321635f71a0f2cc4e1eda58d85c29230c77456362345196351bf56533d" }, + { + "name": "gf", + "unicode": "1F1EC-1F1EB", + "digest": "3a34df321635f71a0f2cc4e1eda58d85c29230c77456362345196351bf56533d" + }, { "name": "flag_gg", "unicode": "1F1EC-1F1EC", "digest": "c972f8d190b4e9ca8890df41503d202ffd73981833d3f3750f563302167bcd66" }, + { + "name": "gg", + "unicode": "1F1EC-1F1EC", + "digest": "c972f8d190b4e9ca8890df41503d202ffd73981833d3f3750f563302167bcd66" + }, { "name": "flag_gh", "unicode": "1F1EC-1F1ED", "digest": "9c3d3569bd411389fa0af7c6938d4325cedeb9c0e8f059dc1d5a74c6b8d6d01b" }, + { + "name": "gh", + "unicode": "1F1EC-1F1ED", + "digest": "9c3d3569bd411389fa0af7c6938d4325cedeb9c0e8f059dc1d5a74c6b8d6d01b" + }, { "name": "flag_gi", "unicode": "1F1EC-1F1EE", "digest": "ede638bc6fedc30a01821025d87ec19297500da9c04a7a155984fca186118649" }, + { + "name": "gi", + "unicode": "1F1EC-1F1EE", + "digest": "ede638bc6fedc30a01821025d87ec19297500da9c04a7a155984fca186118649" + }, { "name": "flag_gl", "unicode": "1F1EC-1F1F1", "digest": "a2ce3371eff1da8331671925f707232aa593ac7400d59555c9ca689729ce24ec" }, + { + "name": "gl", + "unicode": "1F1EC-1F1F1", + "digest": "a2ce3371eff1da8331671925f707232aa593ac7400d59555c9ca689729ce24ec" + }, { "name": "flag_gm", "unicode": "1F1EC-1F1F2", "digest": "932bf6eb75ddd4278268dd2f09d8fffcfef89f8fd6b6e86a08a414cd3ceec94d" }, + { + "name": "gm", + "unicode": "1F1EC-1F1F2", + "digest": "932bf6eb75ddd4278268dd2f09d8fffcfef89f8fd6b6e86a08a414cd3ceec94d" + }, { "name": "flag_gn", "unicode": "1F1EC-1F1F3", "digest": "ebf543713895adaa09d64897f24bd461191191b8fcbbcede52bdaf4bd2dc67a8" }, + { + "name": "gn", + "unicode": "1F1EC-1F1F3", + "digest": "ebf543713895adaa09d64897f24bd461191191b8fcbbcede52bdaf4bd2dc67a8" + }, { "name": "flag_gp", "unicode": "1F1EC-1F1F5", "digest": "2e6c48d80c571b34f31fa9b3622dcc51e1707c0118e991e9c177742ff02a8a96" }, + { + "name": "gp", + "unicode": "1F1EC-1F1F5", + "digest": "2e6c48d80c571b34f31fa9b3622dcc51e1707c0118e991e9c177742ff02a8a96" + }, { "name": "flag_gq", "unicode": "1F1EC-1F1F6", "digest": "b0f5810180d12fc48faf75e73f882dc59072d7bf957f8455bf7e1e336539dc41" }, + { + "name": "gq", + "unicode": "1F1EC-1F1F6", + "digest": "b0f5810180d12fc48faf75e73f882dc59072d7bf957f8455bf7e1e336539dc41" + }, { "name": "flag_gr", "unicode": "1F1EC-1F1F7", "digest": "8d60d6f8910f5179d851dbea0798b56a492c6be85f3d55e1a1126cd1d6663a3b" }, + { + "name": "gr", + "unicode": "1F1EC-1F1F7", + "digest": "8d60d6f8910f5179d851dbea0798b56a492c6be85f3d55e1a1126cd1d6663a3b" + }, { "name": "flag_gs", "unicode": "1F1EC-1F1F8", "digest": "7b07915af0e2364ebc386a162d44846f3a7986fdd24e20ad2bc56d64a103fe9c" }, + { + "name": "gs", + "unicode": "1F1EC-1F1F8", + "digest": "7b07915af0e2364ebc386a162d44846f3a7986fdd24e20ad2bc56d64a103fe9c" + }, { "name": "flag_gt", "unicode": "1F1EC-1F1F9", "digest": "0c78108ede45bf34917b409a0867f5ec8253c74b694beda083f3e8d04d7a10d8" }, + { + "name": "gt", + "unicode": "1F1EC-1F1F9", + "digest": "0c78108ede45bf34917b409a0867f5ec8253c74b694beda083f3e8d04d7a10d8" + }, { "name": "flag_gu", "unicode": "1F1EC-1F1FA", "digest": "909f1bc98fa1507adb787eb3875503b21ea937d6ae8bb152153916c2da5e13bb" }, + { + "name": "gu", + "unicode": "1F1EC-1F1FA", + "digest": "909f1bc98fa1507adb787eb3875503b21ea937d6ae8bb152153916c2da5e13bb" + }, { "name": "flag_gw", "unicode": "1F1EC-1F1FC", "digest": "f5f34410c7b22d5ed9994b47d0e7a9d9a6a1f05c4d3142f7fef3e4409725f5e6" }, + { + "name": "gw", + "unicode": "1F1EC-1F1FC", + "digest": "f5f34410c7b22d5ed9994b47d0e7a9d9a6a1f05c4d3142f7fef3e4409725f5e6" + }, { "name": "flag_gy", "unicode": "1F1EC-1F1FE", "digest": "4939cf52ab34a924a31032b42668960a2c7d8d4f998b16b065c247110df334be" }, + { + "name": "gy", + "unicode": "1F1EC-1F1FE", + "digest": "4939cf52ab34a924a31032b42668960a2c7d8d4f998b16b065c247110df334be" + }, { "name": "flag_hk", "unicode": "1F1ED-1F1F0", "digest": "bde0916df6d62f6b1cf8f85a8a39526c97fc6ef6fedb0b0cae2adb127a08eafe" }, + { + "name": "hk", + "unicode": "1F1ED-1F1F0", + "digest": "bde0916df6d62f6b1cf8f85a8a39526c97fc6ef6fedb0b0cae2adb127a08eafe" + }, { "name": "flag_hm", "unicode": "1F1ED-1F1F2", "digest": "603e6c9bff9a0dc941970a313fe98fbf53ff5a57028f1a2766420be4211711cc" }, + { + "name": "hm", + "unicode": "1F1ED-1F1F2", + "digest": "603e6c9bff9a0dc941970a313fe98fbf53ff5a57028f1a2766420be4211711cc" + }, { "name": "flag_hn", "unicode": "1F1ED-1F1F3", "digest": "2953ad0909bc32c02615f6ad5a4e5f331ba794a41632b1f0fc366e1c640cc2b9" }, + { + "name": "hn", + "unicode": "1F1ED-1F1F3", + "digest": "2953ad0909bc32c02615f6ad5a4e5f331ba794a41632b1f0fc366e1c640cc2b9" + }, { "name": "flag_hr", "unicode": "1F1ED-1F1F7", "digest": "41c9ffc4f0faaa2d77e5cffb781329e7d2489ce879bd8eb9c503621e834abc50" }, + { + "name": "hr", + "unicode": "1F1ED-1F1F7", + "digest": "41c9ffc4f0faaa2d77e5cffb781329e7d2489ce879bd8eb9c503621e834abc50" + }, { "name": "flag_ht", "unicode": "1F1ED-1F1F9", "digest": "6a56c3d71b4f858e1774aa2134a9f5584087fec968e9ee8bb1046d2ec93bf059" }, + { + "name": "ht", + "unicode": "1F1ED-1F1F9", + "digest": "6a56c3d71b4f858e1774aa2134a9f5584087fec968e9ee8bb1046d2ec93bf059" + }, { "name": "flag_hu", "unicode": "1F1ED-1F1FA", "digest": "72f5809818d4cab8c0cee73df7f67b820fb8471eea4199911a5917ac099795e8" }, + { + "name": "hu", + "unicode": "1F1ED-1F1FA", + "digest": "72f5809818d4cab8c0cee73df7f67b820fb8471eea4199911a5917ac099795e8" + }, { "name": "flag_ic", "unicode": "1F1EE-1F1E8", "digest": "7e2a7667fcd05f927af47e64c5790c104a9956dd9f1a45f03cb0fdcc85d866d3" }, + { + "name": "ic", + "unicode": "1F1EE-1F1E8", + "digest": "7e2a7667fcd05f927af47e64c5790c104a9956dd9f1a45f03cb0fdcc85d866d3" + }, { "name": "flag_id", "unicode": "1F1EE-1F1E9", "digest": "4721f616fae2e443e52f1e9cc96e4835bddca16a2d75d7d5afea57cdee866b7f" }, + { + "name": "indonesia", + "unicode": "1F1EE-1F1E9", + "digest": "4721f616fae2e443e52f1e9cc96e4835bddca16a2d75d7d5afea57cdee866b7f" + }, { "name": "flag_ie", "unicode": "1F1EE-1F1EA", "digest": "84b19833e6c9fb43187f8a28d85045a3df58816f20a07edab90474323174b1f3" }, + { + "name": "ie", + "unicode": "1F1EE-1F1EA", + "digest": "84b19833e6c9fb43187f8a28d85045a3df58816f20a07edab90474323174b1f3" + }, { "name": "flag_il", "unicode": "1F1EE-1F1F1", "digest": "c99d4bd8c2541cf3a7392c4faf4477d96bc47065dd1423b9e06450483e69b34f" }, + { + "name": "il", + "unicode": "1F1EE-1F1F1", + "digest": "c99d4bd8c2541cf3a7392c4faf4477d96bc47065dd1423b9e06450483e69b34f" + }, { "name": "flag_im", "unicode": "1F1EE-1F1F2", "digest": "5eeb12c0315b527ce61649a38b64d76af726a73b2d381d1a1ddd1366bafb1bfc" }, + { + "name": "im", + "unicode": "1F1EE-1F1F2", + "digest": "5eeb12c0315b527ce61649a38b64d76af726a73b2d381d1a1ddd1366bafb1bfc" + }, { "name": "flag_in", "unicode": "1F1EE-1F1F3", "digest": "ecc3cfcff3368fe0875a51a8be9f4dfd449a187e5beb41a2b34241736247f73b" }, + { + "name": "in", + "unicode": "1F1EE-1F1F3", + "digest": "ecc3cfcff3368fe0875a51a8be9f4dfd449a187e5beb41a2b34241736247f73b" + }, { "name": "flag_io", "unicode": "1F1EE-1F1F4", "digest": "26243d60e04ba3bc9eb8f008bfc77b2a64bcf1a3d0073eb0449a8c8121618c9c" }, + { + "name": "io", + "unicode": "1F1EE-1F1F4", + "digest": "26243d60e04ba3bc9eb8f008bfc77b2a64bcf1a3d0073eb0449a8c8121618c9c" + }, { "name": "flag_iq", "unicode": "1F1EE-1F1F6", "digest": "a1fb5e59575081920b3be5290f654d57a9be099deb56d4ed69eba81a2b531cb3" }, + { + "name": "iq", + "unicode": "1F1EE-1F1F6", + "digest": "a1fb5e59575081920b3be5290f654d57a9be099deb56d4ed69eba81a2b531cb3" + }, { "name": "flag_ir", "unicode": "1F1EE-1F1F7", "digest": "ab89488b934af1d4bdae7ed16dfc74fffe658bb8e95d5161b48cdd06de44ae85" }, + { + "name": "ir", + "unicode": "1F1EE-1F1F7", + "digest": "ab89488b934af1d4bdae7ed16dfc74fffe658bb8e95d5161b48cdd06de44ae85" + }, { "name": "flag_is", "unicode": "1F1EE-1F1F8", "digest": "55db1fc9e6c56d4c9bcb9a46e5e4300cf2a0c32fa91dc24b487a1d56c8097268" }, + { + "name": "is", + "unicode": "1F1EE-1F1F8", + "digest": "55db1fc9e6c56d4c9bcb9a46e5e4300cf2a0c32fa91dc24b487a1d56c8097268" + }, { "name": "flag_it", "unicode": "1F1EE-1F1F9", "digest": "36fc993fb00ab607578a4d0e573e988e17b9459a68a000a48de905a8238589d0" }, + { + "name": "it", + "unicode": "1F1EE-1F1F9", + "digest": "36fc993fb00ab607578a4d0e573e988e17b9459a68a000a48de905a8238589d0" + }, { "name": "flag_je", "unicode": "1F1EF-1F1EA", "digest": "c608dbfd1259330e2f8c40dc5d12ffd0489396f4fc5f3ca57bcb2f0d9d05c20c" }, + { + "name": "je", + "unicode": "1F1EF-1F1EA", + "digest": "c608dbfd1259330e2f8c40dc5d12ffd0489396f4fc5f3ca57bcb2f0d9d05c20c" + }, { "name": "flag_jm", "unicode": "1F1EF-1F1F2", "digest": "a8224b68b2d324f848d75e4376875ef76a8174e6ba32790d9ca622fe1eabfd5f" }, + { + "name": "jm", + "unicode": "1F1EF-1F1F2", + "digest": "a8224b68b2d324f848d75e4376875ef76a8174e6ba32790d9ca622fe1eabfd5f" + }, { "name": "flag_jo", "unicode": "1F1EF-1F1F4", "digest": "2403563dc2ab4ed0e7e3a0761cc09f96801550bba6b177b54d651d8804ad987d" }, + { + "name": "jo", + "unicode": "1F1EF-1F1F4", + "digest": "2403563dc2ab4ed0e7e3a0761cc09f96801550bba6b177b54d651d8804ad987d" + }, { "name": "flag_jp", "unicode": "1F1EF-1F1F5", "digest": "aea8eebd0a0139818cb7629d9c9a8e55160b458eb8ffeee2f36c5cff4b507fd3" }, + { + "name": "jp", + "unicode": "1F1EF-1F1F5", + "digest": "aea8eebd0a0139818cb7629d9c9a8e55160b458eb8ffeee2f36c5cff4b507fd3" + }, { "name": "flag_ke", "unicode": "1F1F0-1F1EA", "digest": "9c8365f74858743bcdce4a9cf6a6f4110faf2dc6433e5dc7d98c24bb3b32a36d" }, + { + "name": "ke", + "unicode": "1F1F0-1F1EA", + "digest": "9c8365f74858743bcdce4a9cf6a6f4110faf2dc6433e5dc7d98c24bb3b32a36d" + }, { "name": "flag_kg", "unicode": "1F1F0-1F1EC", "digest": "0c72bdb1d64b1e3be3d9516a50655a6162d8501851d2cf2fadb8c6ef7740df4e" }, + { + "name": "kg", + "unicode": "1F1F0-1F1EC", + "digest": "0c72bdb1d64b1e3be3d9516a50655a6162d8501851d2cf2fadb8c6ef7740df4e" + }, { "name": "flag_kh", "unicode": "1F1F0-1F1ED", "digest": "49e41e488732d789e395091e144cd6215c6818ba2073e5e22ea21203a737d03c" }, + { + "name": "kh", + "unicode": "1F1F0-1F1ED", + "digest": "49e41e488732d789e395091e144cd6215c6818ba2073e5e22ea21203a737d03c" + }, { "name": "flag_ki", "unicode": "1F1F0-1F1EE", "digest": "9d7f168adbcf5f4cfe28470addfdb0a8b231438d593edb70f633981bfa4c7638" }, + { + "name": "ki", + "unicode": "1F1F0-1F1EE", + "digest": "9d7f168adbcf5f4cfe28470addfdb0a8b231438d593edb70f633981bfa4c7638" + }, { "name": "flag_km", "unicode": "1F1F0-1F1F2", "digest": "9318c28957fa7a19eba5ec452c1cbce01a5a83d41d29d081614d3abb0585d478" }, + { + "name": "km", + "unicode": "1F1F0-1F1F2", + "digest": "9318c28957fa7a19eba5ec452c1cbce01a5a83d41d29d081614d3abb0585d478" + }, { "name": "flag_kn", "unicode": "1F1F0-1F1F3", "digest": "eac7e7d0f023dee5c0c8559bc2c9a96273adda54ce47598025120b30d8d6ebc1" }, + { + "name": "kn", + "unicode": "1F1F0-1F1F3", + "digest": "eac7e7d0f023dee5c0c8559bc2c9a96273adda54ce47598025120b30d8d6ebc1" + }, { "name": "flag_kp", "unicode": "1F1F0-1F1F5", "digest": "d4d53db6f8363174de6db864c056267ba8a7d7e87b5527f2f42bb9b8ac3f362b" }, + { + "name": "kp", + "unicode": "1F1F0-1F1F5", + "digest": "d4d53db6f8363174de6db864c056267ba8a7d7e87b5527f2f42bb9b8ac3f362b" + }, { "name": "flag_kr", "unicode": "1F1F0-1F1F7", "digest": "5c7e61ab4a2aae70cbe51f0ca4718516002bc943b35d870bd853a0c98c4e0ed5" }, + { + "name": "kr", + "unicode": "1F1F0-1F1F7", + "digest": "5c7e61ab4a2aae70cbe51f0ca4718516002bc943b35d870bd853a0c98c4e0ed5" + }, { "name": "flag_kw", "unicode": "1F1F0-1F1FC", "digest": "5d229cd99d25f4285bd30d98cfcc3cd8346648897476e2905a1811ceeef48d37" }, + { + "name": "kw", + "unicode": "1F1F0-1F1FC", + "digest": "5d229cd99d25f4285bd30d98cfcc3cd8346648897476e2905a1811ceeef48d37" + }, { "name": "flag_ky", "unicode": "1F1F0-1F1FE", "digest": "9ce3d8dfc273d3a400960876c434b702f93df92c6c00682dbed2ec8e3966d8a8" }, + { + "name": "ky", + "unicode": "1F1F0-1F1FE", + "digest": "9ce3d8dfc273d3a400960876c434b702f93df92c6c00682dbed2ec8e3966d8a8" + }, { "name": "flag_kz", "unicode": "1F1F0-1F1FF", "digest": "a6f0be0a767fa4824495d568d9fc2bd8d4c1a26f363873d3b65362e9383e2a50" }, + { + "name": "kz", + "unicode": "1F1F0-1F1FF", + "digest": "a6f0be0a767fa4824495d568d9fc2bd8d4c1a26f363873d3b65362e9383e2a50" + }, { "name": "flag_la", "unicode": "1F1F1-1F1E6", "digest": "ab2ae96da87f7b53ab212f8dcd897a591cff9ea6666270097a8e739ee0b8f8cb" }, + { + "name": "la", + "unicode": "1F1F1-1F1E6", + "digest": "ab2ae96da87f7b53ab212f8dcd897a591cff9ea6666270097a8e739ee0b8f8cb" + }, { "name": "flag_lb", "unicode": "1F1F1-1F1E7", "digest": "0c3fcab22e9fae1c78658290aff97de785d0b6adb5e3702d00073ce774b7ed54" }, + { + "name": "lb", + "unicode": "1F1F1-1F1E7", + "digest": "0c3fcab22e9fae1c78658290aff97de785d0b6adb5e3702d00073ce774b7ed54" + }, { "name": "flag_lc", "unicode": "1F1F1-1F1E8", "digest": "e154b0b3a1635a36e0d9ad518c0ea12259320e5f1ebbda982248486492065d28" }, + { + "name": "lc", + "unicode": "1F1F1-1F1E8", + "digest": "e154b0b3a1635a36e0d9ad518c0ea12259320e5f1ebbda982248486492065d28" + }, { "name": "flag_li", "unicode": "1F1F1-1F1EE", "digest": "bbc393a89e73cc8c29a0a9297428d07aa1d4717ea9b7d4dd9d69f21ac7d0605d" }, + { + "name": "li", + "unicode": "1F1F1-1F1EE", + "digest": "bbc393a89e73cc8c29a0a9297428d07aa1d4717ea9b7d4dd9d69f21ac7d0605d" + }, { "name": "flag_lk", "unicode": "1F1F1-1F1F0", "digest": "376bd501d113a844971ca1006ab31aa086cd55d74842ea5f3dedaba997b58693" }, + { + "name": "lk", + "unicode": "1F1F1-1F1F0", + "digest": "376bd501d113a844971ca1006ab31aa086cd55d74842ea5f3dedaba997b58693" + }, { "name": "flag_lr", "unicode": "1F1F1-1F1F7", "digest": "9a6ebe1c9d9a53079ee77292a5ad0965f96409b0417f92876a1c3bd463d6a9bc" }, + { + "name": "lr", + "unicode": "1F1F1-1F1F7", + "digest": "9a6ebe1c9d9a53079ee77292a5ad0965f96409b0417f92876a1c3bd463d6a9bc" + }, { "name": "flag_ls", "unicode": "1F1F1-1F1F8", "digest": "e2f4b05414f6e0c3d629a92b0534d4145475f0214a83a62c902fe0884c833c89" }, + { + "name": "ls", + "unicode": "1F1F1-1F1F8", + "digest": "e2f4b05414f6e0c3d629a92b0534d4145475f0214a83a62c902fe0884c833c89" + }, { "name": "flag_lt", "unicode": "1F1F1-1F1F9", "digest": "d5e2f8b2ffa820a33ea6d612fccd61e32467d25154342f5be134d3520e48387f" }, + { + "name": "lt", + "unicode": "1F1F1-1F1F9", + "digest": "d5e2f8b2ffa820a33ea6d612fccd61e32467d25154342f5be134d3520e48387f" + }, { "name": "flag_lu", "unicode": "1F1F1-1F1FA", "digest": "f43277103292195b51981d08e2dde68eab660a65c7875f510e09a8b2370f1b5c" }, + { + "name": "lu", + "unicode": "1F1F1-1F1FA", + "digest": "f43277103292195b51981d08e2dde68eab660a65c7875f510e09a8b2370f1b5c" + }, { "name": "flag_lv", "unicode": "1F1F1-1F1FB", "digest": "e1288ac5c80d6e9d577d652e34be247ca39bf9d3d7cfc8a6cae13c1f9ac9dc47" }, + { + "name": "lv", + "unicode": "1F1F1-1F1FB", + "digest": "e1288ac5c80d6e9d577d652e34be247ca39bf9d3d7cfc8a6cae13c1f9ac9dc47" + }, { "name": "flag_ly", "unicode": "1F1F1-1F1FE", "digest": "5122294b769a174e3b6e3d238bb846b3e760929f5bb3c1a708d8a429f3f32f68" }, + { + "name": "ly", + "unicode": "1F1F1-1F1FE", + "digest": "5122294b769a174e3b6e3d238bb846b3e760929f5bb3c1a708d8a429f3f32f68" + }, { "name": "flag_ma", "unicode": "1F1F2-1F1E6", "digest": "615a6447ff284de7689b4fd7b04fdda308f65dbbec958cfb96d2977514981d16" }, + { + "name": "ma", + "unicode": "1F1F2-1F1E6", + "digest": "615a6447ff284de7689b4fd7b04fdda308f65dbbec958cfb96d2977514981d16" + }, { "name": "flag_mc", "unicode": "1F1F2-1F1E8", "digest": "08b48b28938acbfc0fbc15c25ee14dbad7164c5165d03df2eee370755ee7b4cf" }, + { + "name": "mc", + "unicode": "1F1F2-1F1E8", + "digest": "08b48b28938acbfc0fbc15c25ee14dbad7164c5165d03df2eee370755ee7b4cf" + }, { "name": "flag_md", "unicode": "1F1F2-1F1E9", "digest": "93d61de68f821e1e08b30e63d91e8b4a657766475128538894cf9da9a3b4e3c0" }, + { + "name": "md", + "unicode": "1F1F2-1F1E9", + "digest": "93d61de68f821e1e08b30e63d91e8b4a657766475128538894cf9da9a3b4e3c0" + }, { "name": "flag_me", "unicode": "1F1F2-1F1EA", "digest": "ee55c0eb78241aec2baf1822a47fa46d63209ceae3db7617ae886b823ae229ff" }, + { + "name": "me", + "unicode": "1F1F2-1F1EA", + "digest": "ee55c0eb78241aec2baf1822a47fa46d63209ceae3db7617ae886b823ae229ff" + }, { "name": "flag_mf", "unicode": "1F1F2-1F1EB", "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" }, + { + "name": "mf", + "unicode": "1F1F2-1F1EB", + "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" + }, { "name": "flag_mg", "unicode": "1F1F2-1F1EC", "digest": "86ec8140e2c4854f52cff74757baf0cbb75a4aacca8be6af8c8f9c939a7b866c" }, + { + "name": "mg", + "unicode": "1F1F2-1F1EC", + "digest": "86ec8140e2c4854f52cff74757baf0cbb75a4aacca8be6af8c8f9c939a7b866c" + }, { "name": "flag_mh", "unicode": "1F1F2-1F1ED", "digest": "8311ea3422c9d5e94b55e19b03bedd6fe6e2a191b7657e15ac75a48932958a5b" }, + { + "name": "mh", + "unicode": "1F1F2-1F1ED", + "digest": "8311ea3422c9d5e94b55e19b03bedd6fe6e2a191b7657e15ac75a48932958a5b" + }, { "name": "flag_mk", "unicode": "1F1F2-1F1F0", "digest": "5c6f504f88c5a875c06ac8b26fa6e81a9d79c42a1c7d1fad9a5d4c8ad06ca502" }, + { + "name": "mk", + "unicode": "1F1F2-1F1F0", + "digest": "5c6f504f88c5a875c06ac8b26fa6e81a9d79c42a1c7d1fad9a5d4c8ad06ca502" + }, { "name": "flag_ml", "unicode": "1F1F2-1F1F1", "digest": "d08a4973db40cf28e58ca3c80e8bd4e50d68ba1080b31917aeefdb0e210b5c50" }, + { + "name": "ml", + "unicode": "1F1F2-1F1F1", + "digest": "d08a4973db40cf28e58ca3c80e8bd4e50d68ba1080b31917aeefdb0e210b5c50" + }, { "name": "flag_mm", "unicode": "1F1F2-1F1F2", "digest": "5e95089514ca09bb93afb481b317477c9d053adcf450e0b711d78ed1078c7470" }, + { + "name": "mm", + "unicode": "1F1F2-1F1F2", + "digest": "5e95089514ca09bb93afb481b317477c9d053adcf450e0b711d78ed1078c7470" + }, { "name": "flag_mn", "unicode": "1F1F2-1F1F3", "digest": "7a0ca72715dd2a36eeeed2f8c888497cb752f0000af8f07d6930743caf6e4273" }, + { + "name": "mn", + "unicode": "1F1F2-1F1F3", + "digest": "7a0ca72715dd2a36eeeed2f8c888497cb752f0000af8f07d6930743caf6e4273" + }, { "name": "flag_mo", "unicode": "1F1F2-1F1F4", "digest": "d2c7c2191bc1bc83d85f2270968cb4de5cf26a11f70e166a8b32c108287ef729" }, + { + "name": "mo", + "unicode": "1F1F2-1F1F4", + "digest": "d2c7c2191bc1bc83d85f2270968cb4de5cf26a11f70e166a8b32c108287ef729" + }, { "name": "flag_mp", "unicode": "1F1F2-1F1F5", "digest": "89ad06121fd7981338fe188464491bea371f85125bfb4fc01fb5cad606613b1e" }, + { + "name": "mp", + "unicode": "1F1F2-1F1F5", + "digest": "89ad06121fd7981338fe188464491bea371f85125bfb4fc01fb5cad606613b1e" + }, { "name": "flag_mq", "unicode": "1F1F2-1F1F6", "digest": "98176f3af823b26a3657a17c5073ee22367898b40bd3973de76329aa87ca5a2e" }, + { + "name": "mq", + "unicode": "1F1F2-1F1F6", + "digest": "98176f3af823b26a3657a17c5073ee22367898b40bd3973de76329aa87ca5a2e" + }, { "name": "flag_mr", "unicode": "1F1F2-1F1F7", "digest": "cc3e705ad84f83fe2d544385c39564743024dab26595d62469b35fdb791f6015" }, + { + "name": "mr", + "unicode": "1F1F2-1F1F7", + "digest": "cc3e705ad84f83fe2d544385c39564743024dab26595d62469b35fdb791f6015" + }, { "name": "flag_ms", "unicode": "1F1F2-1F1F8", "digest": "465e3d5700b557f2589bd6e34a0c6b12c634a6ed4dcfbee3c1c841c5de3413f0" }, + { + "name": "ms", + "unicode": "1F1F2-1F1F8", + "digest": "465e3d5700b557f2589bd6e34a0c6b12c634a6ed4dcfbee3c1c841c5de3413f0" + }, { "name": "flag_mt", "unicode": "1F1F2-1F1F9", "digest": "e610ba22d8d8ad750ed10dff8e1b4d89bc34f066c3424bfa77dbdc1a5d79743a" }, + { + "name": "mt", + "unicode": "1F1F2-1F1F9", + "digest": "e610ba22d8d8ad750ed10dff8e1b4d89bc34f066c3424bfa77dbdc1a5d79743a" + }, { "name": "flag_mu", "unicode": "1F1F2-1F1FA", "digest": "3daf015d3b95218677dafbb282b7804686aa68875a6bd1d70c165b7b149e19cb" }, + { + "name": "mu", + "unicode": "1F1F2-1F1FA", + "digest": "3daf015d3b95218677dafbb282b7804686aa68875a6bd1d70c165b7b149e19cb" + }, { "name": "flag_mv", "unicode": "1F1F2-1F1FB", "digest": "d30e4bfd04f08177de92f3c175600aaafa89b9668bbe2b83f35f07a74382065c" }, + { + "name": "mv", + "unicode": "1F1F2-1F1FB", + "digest": "d30e4bfd04f08177de92f3c175600aaafa89b9668bbe2b83f35f07a74382065c" + }, { "name": "flag_mw", "unicode": "1F1F2-1F1FC", "digest": "f364b1c8bfda3f86b5e26422eedc571ba11e312dcc634197631a6840cb22aede" }, + { + "name": "mw", + "unicode": "1F1F2-1F1FC", + "digest": "f364b1c8bfda3f86b5e26422eedc571ba11e312dcc634197631a6840cb22aede" + }, { "name": "flag_mx", "unicode": "1F1F2-1F1FD", "digest": "eafb02ec0be9cefab7cef7c426c7d860d98e4947f4da04054154dc86d8f487c4" }, + { + "name": "mx", + "unicode": "1F1F2-1F1FD", + "digest": "eafb02ec0be9cefab7cef7c426c7d860d98e4947f4da04054154dc86d8f487c4" + }, { "name": "flag_my", "unicode": "1F1F2-1F1FE", "digest": "9a690b357bc6b970781bd122c1e546ade3ccb73d930c2af1008b82027e36c7cf" }, + { + "name": "my", + "unicode": "1F1F2-1F1FE", + "digest": "9a690b357bc6b970781bd122c1e546ade3ccb73d930c2af1008b82027e36c7cf" + }, { "name": "flag_mz", "unicode": "1F1F2-1F1FF", "digest": "36d0548ebfef9e0443ec1d0597ebfa6e95c25b997381f30c8c74008820743bb9" }, + { + "name": "mz", + "unicode": "1F1F2-1F1FF", + "digest": "36d0548ebfef9e0443ec1d0597ebfa6e95c25b997381f30c8c74008820743bb9" + }, { "name": "flag_na", "unicode": "1F1F3-1F1E6", "digest": "4989dc9452b0bdfa101cfd3b7c83ef1195a7e45128b9ed00193fe712a6d02fca" }, + { + "name": "na", + "unicode": "1F1F3-1F1E6", + "digest": "4989dc9452b0bdfa101cfd3b7c83ef1195a7e45128b9ed00193fe712a6d02fca" + }, { "name": "flag_nc", "unicode": "1F1F3-1F1E8", "digest": "7fc9d865eebf729d5496c4cd7576476ec599f65b379d4a6df66b4e399553c2eb" }, + { + "name": "nc", + "unicode": "1F1F3-1F1E8", + "digest": "7fc9d865eebf729d5496c4cd7576476ec599f65b379d4a6df66b4e399553c2eb" + }, { "name": "flag_ne", "unicode": "1F1F3-1F1EA", "digest": "d3f10fb44ec44a04112bc66d05f0a44c6ec46dae73cfd3fe26cdc8b32ec06713" }, + { + "name": "ne", + "unicode": "1F1F3-1F1EA", + "digest": "d3f10fb44ec44a04112bc66d05f0a44c6ec46dae73cfd3fe26cdc8b32ec06713" + }, { "name": "flag_nf", "unicode": "1F1F3-1F1EB", "digest": "d390e0d52215a025380af221ba9e955e5886edbb4c9f4b124f2fb60a8e019e42" }, + { + "name": "nf", + "unicode": "1F1F3-1F1EB", + "digest": "d390e0d52215a025380af221ba9e955e5886edbb4c9f4b124f2fb60a8e019e42" + }, { "name": "flag_ng", "unicode": "1F1F3-1F1EC", "digest": "e69d1bb8f1db4a0c295c90dda23d8f97c2dea59f9a2da2ecb0e9a1dc4dbea101" }, + { + "name": "nigeria", + "unicode": "1F1F3-1F1EC", + "digest": "e69d1bb8f1db4a0c295c90dda23d8f97c2dea59f9a2da2ecb0e9a1dc4dbea101" + }, { "name": "flag_ni", "unicode": "1F1F3-1F1EE", "digest": "dbaccc942637469b0ee75bd5f956958c3c5a89d8f69b69c96f02ab6594124894" }, + { + "name": "ni", + "unicode": "1F1F3-1F1EE", + "digest": "dbaccc942637469b0ee75bd5f956958c3c5a89d8f69b69c96f02ab6594124894" + }, { "name": "flag_nl", "unicode": "1F1F3-1F1F1", "digest": "bda2eb0315763c3c19d37c664dab1ee4280f20888a0ca57677fd33cfa4240910" }, + { + "name": "nl", + "unicode": "1F1F3-1F1F1", + "digest": "bda2eb0315763c3c19d37c664dab1ee4280f20888a0ca57677fd33cfa4240910" + }, { "name": "flag_no", "unicode": "1F1F3-1F1F4", "digest": "42b49dec756a220781ea271ca8fbcaba524dc3b38d5d8f999bfaa40ef9ebd302" }, + { + "name": "no", + "unicode": "1F1F3-1F1F4", + "digest": "42b49dec756a220781ea271ca8fbcaba524dc3b38d5d8f999bfaa40ef9ebd302" + }, { "name": "flag_np", "unicode": "1F1F3-1F1F5", "digest": "b5259257db079235310d5d9537d2b5b61ae0326bc8920ba13084b009844e2957" }, + { + "name": "np", + "unicode": "1F1F3-1F1F5", + "digest": "b5259257db079235310d5d9537d2b5b61ae0326bc8920ba13084b009844e2957" + }, { "name": "flag_nr", "unicode": "1F1F3-1F1F7", "digest": "1bd7d1fe2c3a5e98cfd4dff6e8d6dd6d3c74f0051ad615587d77d2291a9784cc" }, + { + "name": "nr", + "unicode": "1F1F3-1F1F7", + "digest": "1bd7d1fe2c3a5e98cfd4dff6e8d6dd6d3c74f0051ad615587d77d2291a9784cc" + }, { "name": "flag_nu", "unicode": "1F1F3-1F1FA", "digest": "e2a7a398e07d2232147cc0917d72d18b519246d3d314e9f6f03dcf98d312d4ce" }, + { + "name": "nu", + "unicode": "1F1F3-1F1FA", + "digest": "e2a7a398e07d2232147cc0917d72d18b519246d3d314e9f6f03dcf98d312d4ce" + }, { "name": "flag_nz", "unicode": "1F1F3-1F1FF", "digest": "ce8b1cb87dae3a3ec865575b57a0b4987a7f4bd3f170e7b210dd764fc2588cd4" }, + { + "name": "nz", + "unicode": "1F1F3-1F1FF", + "digest": "ce8b1cb87dae3a3ec865575b57a0b4987a7f4bd3f170e7b210dd764fc2588cd4" + }, { "name": "flag_om", "unicode": "1F1F4-1F1F2", "digest": "29da72505a276a8a372a00c197388ebc5098c221cab26b3ff755bd62b10f740f" }, + { + "name": "om", + "unicode": "1F1F4-1F1F2", + "digest": "29da72505a276a8a372a00c197388ebc5098c221cab26b3ff755bd62b10f740f" + }, { "name": "flag_pa", "unicode": "1F1F5-1F1E6", "digest": "180b673c9aceea43a8b55823a82d80600257e4982d0757d129860e3d8a14f458" }, + { + "name": "pa", + "unicode": "1F1F5-1F1E6", + "digest": "180b673c9aceea43a8b55823a82d80600257e4982d0757d129860e3d8a14f458" + }, { "name": "flag_pe", "unicode": "1F1F5-1F1EA", "digest": "b61823ea2cd91e371e40832df5764558b81d44fac41030827a3f6d2564643c00" }, + { + "name": "pe", + "unicode": "1F1F5-1F1EA", + "digest": "b61823ea2cd91e371e40832df5764558b81d44fac41030827a3f6d2564643c00" + }, { "name": "flag_pf", "unicode": "1F1F5-1F1EB", "digest": "e560421911f4af90c73a0dbdf8f42e69316003799304c9394fb127e3b83326fa" }, + { + "name": "pf", + "unicode": "1F1F5-1F1EB", + "digest": "e560421911f4af90c73a0dbdf8f42e69316003799304c9394fb127e3b83326fa" + }, { "name": "flag_pg", "unicode": "1F1F5-1F1EC", "digest": "880e87db2ce0eac38db037683a5db46fd6ce30623cf56ae4a93a747103570044" }, + { + "name": "pg", + "unicode": "1F1F5-1F1EC", + "digest": "880e87db2ce0eac38db037683a5db46fd6ce30623cf56ae4a93a747103570044" + }, { "name": "flag_ph", "unicode": "1F1F5-1F1ED", "digest": "49aae2f56bfd1385741dc76857aa1f1459778b2d39a1c955e469c5367585bfd5" }, + { + "name": "ph", + "unicode": "1F1F5-1F1ED", + "digest": "49aae2f56bfd1385741dc76857aa1f1459778b2d39a1c955e469c5367585bfd5" + }, { "name": "flag_pk", "unicode": "1F1F5-1F1F0", "digest": "64379dbfc932df3a07935b5cfa11ca151f761d3728939e982604e12c663cd646" }, + { + "name": "pk", + "unicode": "1F1F5-1F1F0", + "digest": "64379dbfc932df3a07935b5cfa11ca151f761d3728939e982604e12c663cd646" + }, { "name": "flag_pl", "unicode": "1F1F5-1F1F1", "digest": "3b688b074c2735d3dea0b7ab74b80eba243ce50cb05d68e585c9d701c1f14617" }, + { + "name": "pl", + "unicode": "1F1F5-1F1F1", + "digest": "3b688b074c2735d3dea0b7ab74b80eba243ce50cb05d68e585c9d701c1f14617" + }, { "name": "flag_pm", "unicode": "1F1F5-1F1F2", "digest": "a13a69ee3131501dd8138173cfb669a35ee8039d84aa665e69dd7f0d0aa3e717" }, + { + "name": "pm", + "unicode": "1F1F5-1F1F2", + "digest": "a13a69ee3131501dd8138173cfb669a35ee8039d84aa665e69dd7f0d0aa3e717" + }, { "name": "flag_pn", "unicode": "1F1F5-1F1F3", "digest": "d7ae3985cf66024e4a3001e79a8efbb3e75571f2b0abbd0fb87fc1efc795a2b3" }, + { + "name": "pn", + "unicode": "1F1F5-1F1F3", + "digest": "d7ae3985cf66024e4a3001e79a8efbb3e75571f2b0abbd0fb87fc1efc795a2b3" + }, { "name": "flag_pr", "unicode": "1F1F5-1F1F7", "digest": "4910dc984bc908158506b770f28af56150cbb4509a4291947dfa2479b9e4b308" }, + { + "name": "pr", + "unicode": "1F1F5-1F1F7", + "digest": "4910dc984bc908158506b770f28af56150cbb4509a4291947dfa2479b9e4b308" + }, { "name": "flag_ps", "unicode": "1F1F5-1F1F8", "digest": "b2bca7619fced25de94d7bd398537857460348a552e7d73d189aef3f428e6a13" }, + { + "name": "ps", + "unicode": "1F1F5-1F1F8", + "digest": "b2bca7619fced25de94d7bd398537857460348a552e7d73d189aef3f428e6a13" + }, { "name": "flag_pt", "unicode": "1F1F5-1F1F9", "digest": "177282613b4b8b4d9551f1da6a1c3f66f1b96cf67c71c7d164213b26b3237395" }, + { + "name": "pt", + "unicode": "1F1F5-1F1F9", + "digest": "177282613b4b8b4d9551f1da6a1c3f66f1b96cf67c71c7d164213b26b3237395" + }, { "name": "flag_pw", "unicode": "1F1F5-1F1FC", "digest": "2ff42a14bdc7df76b5f989dca381f94765032b26ae47d47b97844abde458cefe" }, + { + "name": "pw", + "unicode": "1F1F5-1F1FC", + "digest": "2ff42a14bdc7df76b5f989dca381f94765032b26ae47d47b97844abde458cefe" + }, { "name": "flag_py", "unicode": "1F1F5-1F1FE", "digest": "80169b69a46c4c67d0090dc2c6bf05d1a14f133ac7ae56f811547e8e8f70d81b" }, + { + "name": "py", + "unicode": "1F1F5-1F1FE", + "digest": "80169b69a46c4c67d0090dc2c6bf05d1a14f133ac7ae56f811547e8e8f70d81b" + }, { "name": "flag_qa", "unicode": "1F1F6-1F1E6", "digest": "589b44b975aa97426afb8db7f8b355491fca246b693903485824bf0f5a6953a2" }, + { + "name": "qa", + "unicode": "1F1F6-1F1E6", + "digest": "589b44b975aa97426afb8db7f8b355491fca246b693903485824bf0f5a6953a2" + }, { "name": "flag_re", "unicode": "1F1F7-1F1EA", "digest": "77d242261742831a142c9ec74cd17d76b1e6d1af751ff3c6a356646744bc798a" }, + { + "name": "re", + "unicode": "1F1F7-1F1EA", + "digest": "77d242261742831a142c9ec74cd17d76b1e6d1af751ff3c6a356646744bc798a" + }, { "name": "flag_ro", "unicode": "1F1F7-1F1F4", "digest": "d7d17026ea81f27456983722540f9a23343a3a1b22e7697c4fba118ce8b4719e" }, + { + "name": "ro", + "unicode": "1F1F7-1F1F4", + "digest": "d7d17026ea81f27456983722540f9a23343a3a1b22e7697c4fba118ce8b4719e" + }, { "name": "flag_rs", "unicode": "1F1F7-1F1F8", "digest": "e466a18cc0368e623d3fe33a036c1e88db91ae24f7510e17caacc85c41f1bac8" }, + { + "name": "rs", + "unicode": "1F1F7-1F1F8", + "digest": "e466a18cc0368e623d3fe33a036c1e88db91ae24f7510e17caacc85c41f1bac8" + }, { "name": "flag_ru", "unicode": "1F1F7-1F1FA", "digest": "86bf53a62dfc4c434d910f43df70f430fc67c0070fe3fc466c4fbfd6a5d8e646" }, + { + "name": "ru", + "unicode": "1F1F7-1F1FA", + "digest": "86bf53a62dfc4c434d910f43df70f430fc67c0070fe3fc466c4fbfd6a5d8e646" + }, { "name": "flag_rw", "unicode": "1F1F7-1F1FC", "digest": "38ec5a01896c9747a8dbf865d5e8584770e587253b7af3d3b9c36cd993f67518" }, + { + "name": "rw", + "unicode": "1F1F7-1F1FC", + "digest": "38ec5a01896c9747a8dbf865d5e8584770e587253b7af3d3b9c36cd993f67518" + }, { "name": "flag_sa", "unicode": "1F1F8-1F1E6", "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0" }, + { + "name": "saudiarabia", + "unicode": "1F1F8-1F1E6", + "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0" + }, + { + "name": "saudi", + "unicode": "1F1F8-1F1E6", + "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0" + }, { "name": "flag_sb", "unicode": "1F1F8-1F1E7", "digest": "8ffa24c5cb92be4dbe43f6cd85b61b9608a3101bd78ebccff4fe99c209b3e241" }, + { + "name": "sb", + "unicode": "1F1F8-1F1E7", + "digest": "8ffa24c5cb92be4dbe43f6cd85b61b9608a3101bd78ebccff4fe99c209b3e241" + }, { "name": "flag_sc", "unicode": "1F1F8-1F1E8", "digest": "227d090ac2cbf317e594567b6114b5063a13cfe33abf990d37b200debcfadabb" }, + { + "name": "sc", + "unicode": "1F1F8-1F1E8", + "digest": "227d090ac2cbf317e594567b6114b5063a13cfe33abf990d37b200debcfadabb" + }, { "name": "flag_sd", "unicode": "1F1F8-1F1E9", "digest": "350f3332e8ea1138e54facc870dd0fea5f2ab7d3fd4baa02ed8627ae79642f6c" }, + { + "name": "sd", + "unicode": "1F1F8-1F1E9", + "digest": "350f3332e8ea1138e54facc870dd0fea5f2ab7d3fd4baa02ed8627ae79642f6c" + }, { "name": "flag_se", "unicode": "1F1F8-1F1EA", "digest": "c1b09f36c263727de83b54376f05e083a17a61941af9a1640b826629256a280d" }, + { + "name": "se", + "unicode": "1F1F8-1F1EA", + "digest": "c1b09f36c263727de83b54376f05e083a17a61941af9a1640b826629256a280d" + }, { "name": "flag_sg", "unicode": "1F1F8-1F1EC", "digest": "e6fc26920dfc07e4fd3c8d897de9c607e0bf48a3b64a13630c858d707a8e7660" }, + { + "name": "sg", + "unicode": "1F1F8-1F1EC", + "digest": "e6fc26920dfc07e4fd3c8d897de9c607e0bf48a3b64a13630c858d707a8e7660" + }, { "name": "flag_sh", "unicode": "1F1F8-1F1ED", "digest": "f2c22ab0eb49e3104c35f1c0268b1e63c3a67f41b0cfa9861b189525988e53b6" }, + { + "name": "sh", + "unicode": "1F1F8-1F1ED", + "digest": "f2c22ab0eb49e3104c35f1c0268b1e63c3a67f41b0cfa9861b189525988e53b6" + }, { "name": "flag_si", "unicode": "1F1F8-1F1EE", "digest": "1ef0b10e498f71591322f9d8ec122d39838f479370cf7ee922560986ef6c4f2e" }, + { + "name": "si", + "unicode": "1F1F8-1F1EE", + "digest": "1ef0b10e498f71591322f9d8ec122d39838f479370cf7ee922560986ef6c4f2e" + }, { "name": "flag_sj", "unicode": "1F1F8-1F1EF", "digest": "ce913b007f84a9cba2add8d754aa791901624c60e4200de426dfa25271cb0f78" }, + { + "name": "sj", + "unicode": "1F1F8-1F1EF", + "digest": "ce913b007f84a9cba2add8d754aa791901624c60e4200de426dfa25271cb0f78" + }, { "name": "flag_sk", "unicode": "1F1F8-1F1F0", "digest": "d8f8fc4024c82f906effe98facbef9d543fb3708b1134dc502c74dc4a442b30a" }, + { + "name": "sk", + "unicode": "1F1F8-1F1F0", + "digest": "d8f8fc4024c82f906effe98facbef9d543fb3708b1134dc502c74dc4a442b30a" + }, { "name": "flag_sl", "unicode": "1F1F8-1F1F1", "digest": "dd7fd0452498d8d1c894cf0d5a662ddff9c5bcc02148bdc3dc7e6f25d0bb586e" }, + { + "name": "sl", + "unicode": "1F1F8-1F1F1", + "digest": "dd7fd0452498d8d1c894cf0d5a662ddff9c5bcc02148bdc3dc7e6f25d0bb586e" + }, { "name": "flag_sm", "unicode": "1F1F8-1F1F2", "digest": "2b499606aee2b5cbf4037338753c80a4c8f75f4abcef2c8657bd9337e602bbd3" }, + { + "name": "sm", + "unicode": "1F1F8-1F1F2", + "digest": "2b499606aee2b5cbf4037338753c80a4c8f75f4abcef2c8657bd9337e602bbd3" + }, { "name": "flag_sn", "unicode": "1F1F8-1F1F3", "digest": "03b46a9d8b129da13f60c23b820b04fba52050ca58a41b859ad57d5c3cc2515d" }, + { + "name": "sn", + "unicode": "1F1F8-1F1F3", + "digest": "03b46a9d8b129da13f60c23b820b04fba52050ca58a41b859ad57d5c3cc2515d" + }, { "name": "flag_so", "unicode": "1F1F8-1F1F4", "digest": "ea416b6a05ddc5b16291ebe5101735360b08c834d55ac82c663ac1dd3e459048" }, + { + "name": "so", + "unicode": "1F1F8-1F1F4", + "digest": "ea416b6a05ddc5b16291ebe5101735360b08c834d55ac82c663ac1dd3e459048" + }, { "name": "flag_sr", "unicode": "1F1F8-1F1F7", "digest": "012179fbcbcb7343e7b09d33e283fb63c7964a6eca35ccb9407d468e495a9874" }, + { + "name": "sr", + "unicode": "1F1F8-1F1F7", + "digest": "012179fbcbcb7343e7b09d33e283fb63c7964a6eca35ccb9407d468e495a9874" + }, { "name": "flag_ss", "unicode": "1F1F8-1F1F8", "digest": "6723150482c640643c9dd7e33ea749f4a8b46aceacbd4f5e11aa33b3ee13aab7" }, + { + "name": "ss", + "unicode": "1F1F8-1F1F8", + "digest": "6723150482c640643c9dd7e33ea749f4a8b46aceacbd4f5e11aa33b3ee13aab7" + }, { "name": "flag_st", "unicode": "1F1F8-1F1F9", "digest": "0947fcec2e3cb1b0e9943c3d00891e8ee226e8d0532e9b1fe807ddf2e8fbc49d" }, + { + "name": "st", + "unicode": "1F1F8-1F1F9", + "digest": "0947fcec2e3cb1b0e9943c3d00891e8ee226e8d0532e9b1fe807ddf2e8fbc49d" + }, { "name": "flag_sv", "unicode": "1F1F8-1F1FB", "digest": "ce7e583db833c4b10e2f7a2d09b97bb522c02e96ea0b3f3a48a955f7d8f970d8" }, + { + "name": "sv", + "unicode": "1F1F8-1F1FB", + "digest": "ce7e583db833c4b10e2f7a2d09b97bb522c02e96ea0b3f3a48a955f7d8f970d8" + }, { "name": "flag_sx", "unicode": "1F1F8-1F1FD", "digest": "c01fb238c7ba439f24a5ef821b6457f2a0fd0b99a1b2d02395bed87f0a4a88e5" }, + { + "name": "sx", + "unicode": "1F1F8-1F1FD", + "digest": "c01fb238c7ba439f24a5ef821b6457f2a0fd0b99a1b2d02395bed87f0a4a88e5" + }, { "name": "flag_sy", "unicode": "1F1F8-1F1FE", "digest": "a77d87ef98c96140c59998d10d94837e2a056dd3ac5c7522e89e5c62eac69e69" }, + { + "name": "sy", + "unicode": "1F1F8-1F1FE", + "digest": "a77d87ef98c96140c59998d10d94837e2a056dd3ac5c7522e89e5c62eac69e69" + }, { "name": "flag_sz", "unicode": "1F1F8-1F1FF", "digest": "2904ad01040a9107ad556ec4c2561781d96746005cca250babb1127b8ba21050" }, + { + "name": "sz", + "unicode": "1F1F8-1F1FF", + "digest": "2904ad01040a9107ad556ec4c2561781d96746005cca250babb1127b8ba21050" + }, { "name": "flag_ta", "unicode": "1F1F9-1F1E6", "digest": "eda84db90e1a8854e8ff3c15b3b38ee65f7d6532b76970a6fbac304c30d8c959" }, + { + "name": "ta", + "unicode": "1F1F9-1F1E6", + "digest": "eda84db90e1a8854e8ff3c15b3b38ee65f7d6532b76970a6fbac304c30d8c959" + }, { "name": "flag_tc", "unicode": "1F1F9-1F1E8", "digest": "4628fdf6dc598a2846beefe97f7d4c6812f4961394cec132924b44bbe79b3322" }, + { + "name": "tc", + "unicode": "1F1F9-1F1E8", + "digest": "4628fdf6dc598a2846beefe97f7d4c6812f4961394cec132924b44bbe79b3322" + }, { "name": "flag_td", "unicode": "1F1F9-1F1E9", "digest": "125ff31e4285cb2a5493a52a2703ebe8e7138b918ec4dae3d0f8693632372df6" }, + { + "name": "td", + "unicode": "1F1F9-1F1E9", + "digest": "125ff31e4285cb2a5493a52a2703ebe8e7138b918ec4dae3d0f8693632372df6" + }, { "name": "flag_tf", "unicode": "1F1F9-1F1EB", "digest": "489d591e11764ac341f2234020f7879db782b8f673fc9aae425fd713e4082334" }, + { + "name": "tf", + "unicode": "1F1F9-1F1EB", + "digest": "489d591e11764ac341f2234020f7879db782b8f673fc9aae425fd713e4082334" + }, { "name": "flag_tg", "unicode": "1F1F9-1F1EC", "digest": "4ceedfcfcc22cd14d9add9d86d6748447995f19f7095fa4be883e21eb1aa86bc" }, + { + "name": "tg", + "unicode": "1F1F9-1F1EC", + "digest": "4ceedfcfcc22cd14d9add9d86d6748447995f19f7095fa4be883e21eb1aa86bc" + }, { "name": "flag_th", "unicode": "1F1F9-1F1ED", "digest": "2798cc660af1c5dc4891c30aded3a53d7cfa0af128cc495df8141907b165902d" }, + { + "name": "th", + "unicode": "1F1F9-1F1ED", + "digest": "2798cc660af1c5dc4891c30aded3a53d7cfa0af128cc495df8141907b165902d" + }, { "name": "flag_tj", "unicode": "1F1F9-1F1EF", "digest": "0483506fc5b5f2d4fc18ea3cd2f8a5da985d68fe4bf90bd3fd05e67e38f32398" }, + { + "name": "tj", + "unicode": "1F1F9-1F1EF", + "digest": "0483506fc5b5f2d4fc18ea3cd2f8a5da985d68fe4bf90bd3fd05e67e38f32398" + }, { "name": "flag_tk", "unicode": "1F1F9-1F1F0", "digest": "d5d4a8c6ce3207731b7c154a9d8d8fa2af055a48f03b3cbbcfd3317d3b8a75f2" }, + { + "name": "tk", + "unicode": "1F1F9-1F1F0", + "digest": "d5d4a8c6ce3207731b7c154a9d8d8fa2af055a48f03b3cbbcfd3317d3b8a75f2" + }, { "name": "flag_tl", "unicode": "1F1F9-1F1F1", "digest": "7a2ba8f91a6b627c60c88244223a9b9d0c12707f50b174f9c2eca07dd3440df7" }, + { + "name": "tl", + "unicode": "1F1F9-1F1F1", + "digest": "7a2ba8f91a6b627c60c88244223a9b9d0c12707f50b174f9c2eca07dd3440df7" + }, { "name": "flag_tm", "unicode": "1F1F9-1F1F2", "digest": "adcf5f23adcf983ce626b44559482f8728251eab34b3ff5d8b125112f3a1010f" }, + { + "name": "turkmenistan", + "unicode": "1F1F9-1F1F2", + "digest": "adcf5f23adcf983ce626b44559482f8728251eab34b3ff5d8b125112f3a1010f" + }, { "name": "flag_tn", "unicode": "1F1F9-1F1F3", "digest": "5ee690ee1f3c3c0cba9b36efdef902894ec59cefbc60c4baa341efd3d7bb9ba2" }, + { + "name": "tn", + "unicode": "1F1F9-1F1F3", + "digest": "5ee690ee1f3c3c0cba9b36efdef902894ec59cefbc60c4baa341efd3d7bb9ba2" + }, { "name": "flag_to", "unicode": "1F1F9-1F1F4", "digest": "cde8672ca25b0e3a423865283fab9bc3ab10f472e04979b3b2f8032b71e96300" }, + { + "name": "to", + "unicode": "1F1F9-1F1F4", + "digest": "cde8672ca25b0e3a423865283fab9bc3ab10f472e04979b3b2f8032b71e96300" + }, { "name": "flag_tr", "unicode": "1F1F9-1F1F7", "digest": "3d83c03ed084cfc81fa633310382acd7213e1eaa19d0ed97d142e7824032b55d" }, + { + "name": "tr", + "unicode": "1F1F9-1F1F7", + "digest": "3d83c03ed084cfc81fa633310382acd7213e1eaa19d0ed97d142e7824032b55d" + }, { "name": "flag_tt", "unicode": "1F1F9-1F1F9", "digest": "d66d272ac27e2b398289d6b60128ccd3508aeb1f4a00a3920c5e6a21bfe357ed" }, + { + "name": "tt", + "unicode": "1F1F9-1F1F9", + "digest": "d66d272ac27e2b398289d6b60128ccd3508aeb1f4a00a3920c5e6a21bfe357ed" + }, { "name": "flag_tv", "unicode": "1F1F9-1F1FB", "digest": "8716527383854cf1569f737d0f0f9ad77b46747255f24e02f5b2fbc850c2e35c" }, + { + "name": "tuvalu", + "unicode": "1F1F9-1F1FB", + "digest": "8716527383854cf1569f737d0f0f9ad77b46747255f24e02f5b2fbc850c2e35c" + }, { "name": "flag_tw", "unicode": "1F1F9-1F1FC", "digest": "fb17b97e18e4423c5f60d60ec3ec60b917be579fc4dd9b5b23236786dcb35108" }, + { + "name": "tw", + "unicode": "1F1F9-1F1FC", + "digest": "fb17b97e18e4423c5f60d60ec3ec60b917be579fc4dd9b5b23236786dcb35108" + }, { "name": "flag_tz", "unicode": "1F1F9-1F1FF", "digest": "a8a8cf57ae5227cb54620bf31d2d6e154d2067d6d049b8db64bc4e538222948b" }, + { + "name": "tz", + "unicode": "1F1F9-1F1FF", + "digest": "a8a8cf57ae5227cb54620bf31d2d6e154d2067d6d049b8db64bc4e538222948b" + }, { "name": "flag_ua", "unicode": "1F1FA-1F1E6", "digest": "03aca4b3ffd60d944a5793eb7530f8d8ae527782f642f6606194e46ee314b12c" }, + { + "name": "ua", + "unicode": "1F1FA-1F1E6", + "digest": "03aca4b3ffd60d944a5793eb7530f8d8ae527782f642f6606194e46ee314b12c" + }, { "name": "flag_ug", "unicode": "1F1FA-1F1EC", "digest": "70226a1585e88390b3b815b8b79a0ddb36d2961c6b465c4ff72aa444abfe982e" }, + { + "name": "ug", + "unicode": "1F1FA-1F1EC", + "digest": "70226a1585e88390b3b815b8b79a0ddb36d2961c6b465c4ff72aa444abfe982e" + }, { "name": "flag_um", "unicode": "1F1FA-1F1F2", "digest": "aa83bf051149acf907140a860de5de1700710e4164ae5549ad1040b24d0a142b" }, + { + "name": "um", + "unicode": "1F1FA-1F1F2", + "digest": "aa83bf051149acf907140a860de5de1700710e4164ae5549ad1040b24d0a142b" + }, { "name": "flag_us", "unicode": "1F1FA-1F1F8", "digest": "32ba2aa09a30514247e91d60762791b582f547a37d9151f98b700dff50f355ea" }, + { + "name": "us", + "unicode": "1F1FA-1F1F8", + "digest": "32ba2aa09a30514247e91d60762791b582f547a37d9151f98b700dff50f355ea" + }, { "name": "flag_uy", "unicode": "1F1FA-1F1FE", "digest": "0e01b3f1df4bdf6d616dacc9c5825151b941bf074be750e8b24a07ea5d5bcacb" }, + { + "name": "uy", + "unicode": "1F1FA-1F1FE", + "digest": "0e01b3f1df4bdf6d616dacc9c5825151b941bf074be750e8b24a07ea5d5bcacb" + }, { "name": "flag_uz", "unicode": "1F1FA-1F1FF", "digest": "903029ce83812a2134f24b65db35b183443a440ea5fecaa6ef7dcaaf65b2519c" }, + { + "name": "uz", + "unicode": "1F1FA-1F1FF", + "digest": "903029ce83812a2134f24b65db35b183443a440ea5fecaa6ef7dcaaf65b2519c" + }, { "name": "flag_va", "unicode": "1F1FB-1F1E6", "digest": "fd3c1c5d0ac030e838f807288912c98a3e258f87901e252e46942a4dab9f8cb7" }, + { + "name": "va", + "unicode": "1F1FB-1F1E6", + "digest": "fd3c1c5d0ac030e838f807288912c98a3e258f87901e252e46942a4dab9f8cb7" + }, { "name": "flag_vc", "unicode": "1F1FB-1F1E8", "digest": "7cd554ea8ca817b5366701160274587ab44167ae5a89c430bbaf237ea18b7421" }, + { + "name": "vc", + "unicode": "1F1FB-1F1E8", + "digest": "7cd554ea8ca817b5366701160274587ab44167ae5a89c430bbaf237ea18b7421" + }, { "name": "flag_ve", "unicode": "1F1FB-1F1EA", "digest": "72930094fb088c1facabea07616035ec4771374358a90c3045219d087b350dd8" }, + { + "name": "ve", + "unicode": "1F1FB-1F1EA", + "digest": "72930094fb088c1facabea07616035ec4771374358a90c3045219d087b350dd8" + }, { "name": "flag_vg", "unicode": "1F1FB-1F1EC", "digest": "78a59afd368b7a8312bfdb2f49927ff09e6b8f46aab0136c0453e3319e81df49" }, + { + "name": "vg", + "unicode": "1F1FB-1F1EC", + "digest": "78a59afd368b7a8312bfdb2f49927ff09e6b8f46aab0136c0453e3319e81df49" + }, { "name": "flag_vi", "unicode": "1F1FB-1F1EE", "digest": "e070879f9605a9bae66bb84f2abf5a40c8b264baee65cd4f7a6720b826739f29" }, + { + "name": "vi", + "unicode": "1F1FB-1F1EE", + "digest": "e070879f9605a9bae66bb84f2abf5a40c8b264baee65cd4f7a6720b826739f29" + }, { "name": "flag_vn", "unicode": "1F1FB-1F1F3", "digest": "100ddf06e0f239b170f4d6cb459450bf4945281ee818f7d3c061828b80562219" }, + { + "name": "vn", + "unicode": "1F1FB-1F1F3", + "digest": "100ddf06e0f239b170f4d6cb459450bf4945281ee818f7d3c061828b80562219" + }, { "name": "flag_vu", "unicode": "1F1FB-1F1FA", "digest": "59fc9d16818295bba4f7f551598f85378cd07f2bd7e31a4eef2589aaa3847563" }, + { + "name": "vu", + "unicode": "1F1FB-1F1FA", + "digest": "59fc9d16818295bba4f7f551598f85378cd07f2bd7e31a4eef2589aaa3847563" + }, { "name": "flag_wf", "unicode": "1F1FC-1F1EB", "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" }, + { + "name": "wf", + "unicode": "1F1FC-1F1EB", + "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" + }, { "name": "flag_white", "unicode": "1F3F3", "digest": "96307e3a28e92d1e7147a06f154ffc291ee3cd1765cf8b7bfb06412294112559" }, + { + "name": "waving_white_flag", + "unicode": "1F3F3", + "digest": "96307e3a28e92d1e7147a06f154ffc291ee3cd1765cf8b7bfb06412294112559" + }, { "name": "flag_ws", "unicode": "1F1FC-1F1F8", "digest": "0c95271d0f4b23f0d215ee0fba05cf08ecb70665d4c028e17463ecda2754b164" }, + { + "name": "ws", + "unicode": "1F1FC-1F1F8", + "digest": "0c95271d0f4b23f0d215ee0fba05cf08ecb70665d4c028e17463ecda2754b164" + }, { "name": "flag_xk", "unicode": "1F1FD-1F1F0", "digest": "713aa7d228e96f4a06d58d1fb8c2a55296c3e56842f8177ca936f3e09f50da1e" }, + { + "name": "xk", + "unicode": "1F1FD-1F1F0", + "digest": "713aa7d228e96f4a06d58d1fb8c2a55296c3e56842f8177ca936f3e09f50da1e" + }, { "name": "flag_ye", "unicode": "1F1FE-1F1EA", "digest": "3bb65bae9c913357bcae8b8b5878efc9e194ca308442ab69639c29716b49f078" }, + { + "name": "ye", + "unicode": "1F1FE-1F1EA", + "digest": "3bb65bae9c913357bcae8b8b5878efc9e194ca308442ab69639c29716b49f078" + }, { "name": "flag_yt", "unicode": "1F1FE-1F1F9", "digest": "f86c86f4c194610a3af78971fcf221ad97b9499d08f6d64476e417a2f52a611e" }, + { + "name": "yt", + "unicode": "1F1FE-1F1F9", + "digest": "f86c86f4c194610a3af78971fcf221ad97b9499d08f6d64476e417a2f52a611e" + }, { "name": "flag_za", "unicode": "1F1FF-1F1E6", "digest": "4dd4fa49a01fdcfc7c1c099a7869e0e9acba83a6a3debf6c8505ada4c796b872" }, + { + "name": "za", + "unicode": "1F1FF-1F1E6", + "digest": "4dd4fa49a01fdcfc7c1c099a7869e0e9acba83a6a3debf6c8505ada4c796b872" + }, { "name": "flag_zm", "unicode": "1F1FF-1F1F2", "digest": "ab6790d89875447de3d1c7f4713b102761bc3e9afdd714b818689e175ca03011" }, + { + "name": "zm", + "unicode": "1F1FF-1F1F2", + "digest": "ab6790d89875447de3d1c7f4713b102761bc3e9afdd714b818689e175ca03011" + }, { "name": "flag_zw", "unicode": "1F1FF-1F1FC", "digest": "9d39b934fe922174b2250f2cd1b174a548d2904091d3298f35b7cc59fbceb181" }, + { + "name": "zw", + "unicode": "1F1FF-1F1FC", + "digest": "9d39b934fe922174b2250f2cd1b174a548d2904091d3298f35b7cc59fbceb181" + }, { "name": "flags", "unicode": "1F38F", @@ -3669,11 +5304,21 @@ "unicode": "1F581", "digest": "be59efba4bc0759af5a726c06619090ef5071bf2541611d71691dedecee6c697" }, + { + "name": "clamshell_mobile_phone", + "unicode": "1F581", + "digest": "be59efba4bc0759af5a726c06619090ef5071bf2541611d71691dedecee6c697" + }, { "name": "floppy_black", "unicode": "1F5AA", "digest": "9022f51bb09c5130c6d46bb2accb159bed6f54d6fbffda6ecad62965ebc958ea" }, + { + "name": "black_hard_shell_floppy_disk", + "unicode": "1F5AA", + "digest": "9022f51bb09c5130c6d46bb2accb159bed6f54d6fbffda6ecad62965ebc958ea" + }, { "name": "floppy_disk", "unicode": "1F4BE", @@ -3684,6 +5329,11 @@ "unicode": "1F5AB", "digest": "ec79c400117c4506ef8cf3eebef6c42dd37e60b3079d3e98b6ccd06e517e2af0" }, + { + "name": "white_hard_shell_floppy_disk", + "unicode": "1F5AB", + "digest": "ec79c400117c4506ef8cf3eebef6c42dd37e60b3079d3e98b6ccd06e517e2af0" + }, { "name": "flower_playing_cards", "unicode": "1F3B4", @@ -3714,6 +5364,11 @@ "unicode": "1F5C1", "digest": "74f3b484771c3d6ef61cf003de25c1a59b875afa46c057b5b1d92d9f99460685" }, + { + "name": "open_folder", + "unicode": "1F5C1", + "digest": "74f3b484771c3d6ef61cf003de25c1a59b875afa46c057b5b1d92d9f99460685" + }, { "name": "football", "unicode": "1F3C8", @@ -3734,6 +5389,11 @@ "unicode": "1F37D", "digest": "b4081b9edea6cdab5112fdd17535051ba17710953013f5020c7c40f84a1e3247" }, + { + "name": "fork_and_knife_with_plate", + "unicode": "1F37D", + "digest": "b4081b9edea6cdab5112fdd17535051ba17710953013f5020c7c40f84a1e3247" + }, { "name": "fountain", "unicode": "26F2", @@ -3754,16 +5414,31 @@ "unicode": "1F5BC", "digest": "6ff21063063989c6ae7dd69f4d6a781c676f9dba380d8e6f1dbac5d53b24f349" }, + { + "name": "frame_with_picture", + "unicode": "1F5BC", + "digest": "6ff21063063989c6ae7dd69f4d6a781c676f9dba380d8e6f1dbac5d53b24f349" + }, { "name": "frame_tiles", "unicode": "1F5BD", "digest": "34a5bb044b4b3ad94b116ad106f7b6747fb8612dc0e9f8ccd4313c2920508df0" }, + { + "name": "frame_with_tiles", + "unicode": "1F5BD", + "digest": "34a5bb044b4b3ad94b116ad106f7b6747fb8612dc0e9f8ccd4313c2920508df0" + }, { "name": "frame_x", "unicode": "1F5BE", "digest": "2e427688fd70361c8c59787d0722ad68abe1c3f968258ee99c0c77ce4b8a8e15" }, + { + "name": "frame_with_an_x", + "unicode": "1F5BE", + "digest": "2e427688fd70361c8c59787d0722ad68abe1c3f968258ee99c0c77ce4b8a8e15" + }, { "name": "free", "unicode": "1F193", @@ -3789,11 +5464,21 @@ "unicode": "1F626", "digest": "fb39f5c2aea98054adb02a3a0ac34a2e38d83f32cd590e9d2449e06a9702f2f5" }, + { + "name": "anguished", + "unicode": "1F626", + "digest": "fb39f5c2aea98054adb02a3a0ac34a2e38d83f32cd590e9d2449e06a9702f2f5" + }, { "name": "frowning2", "unicode": "2639", "digest": "7bb6c682a6c9f98bf3a5ae986e317fd26d1af497c857500deec2f06b6a3af5da" }, + { + "name": "white_frowning_face", + "unicode": "2639", + "digest": "7bb6c682a6c9f98bf3a5ae986e317fd26d1af497c857500deec2f06b6a3af5da" + }, { "name": "fuelpump", "unicode": "26FD", @@ -4029,6 +5714,11 @@ "unicode": "2692", "digest": "2e4fe33406ca03fbb0df1596d63e903d8ee6bd78ecc3ec38a67dd2cecbc584e2" }, + { + "name": "hammer_and_pick", + "unicode": "2692", + "digest": "2e4fe33406ca03fbb0df1596d63e903d8ee6bd78ecc3ec38a67dd2cecbc584e2" + }, { "name": "hamster", "unicode": "1F439", @@ -4039,41 +5729,81 @@ "unicode": "1F590", "digest": "a43e52f7cdec5e9d51497888b0988d7bbd42846ad7e492b196293fbce576d197" }, + { + "name": "raised_hand_with_fingers_splayed", + "unicode": "1F590", + "digest": "a43e52f7cdec5e9d51497888b0988d7bbd42846ad7e492b196293fbce576d197" + }, { "name": "hand_splayed_reverse", "unicode": "1F591", "digest": "ff0af0fe9def7388adca6836e5958492282b1afae99f1b6e1e65d11ba68b96db" }, + { + "name": "reversed_raised_hand_with_fingers_splayed", + "unicode": "1F591", + "digest": "ff0af0fe9def7388adca6836e5958492282b1afae99f1b6e1e65d11ba68b96db" + }, { "name": "hand_splayed_tone1", "unicode": "1F590-1F3FB", "digest": "73cceec7117280d330f8a149979190f0f355dd8d0a92821be89fb70344bb8dfe" }, + { + "name": "raised_hand_with_fingers_splayed_tone1", + "unicode": "1F590-1F3FB", + "digest": "73cceec7117280d330f8a149979190f0f355dd8d0a92821be89fb70344bb8dfe" + }, { "name": "hand_splayed_tone2", "unicode": "1F590-1F3FC", "digest": "b06fac698128f4c3a7b8ea56e8bc4de088bb5461aa0f9c84553f16b43d347145" }, + { + "name": "raised_hand_with_fingers_splayed_tone2", + "unicode": "1F590-1F3FC", + "digest": "b06fac698128f4c3a7b8ea56e8bc4de088bb5461aa0f9c84553f16b43d347145" + }, { "name": "hand_splayed_tone3", "unicode": "1F590-1F3FD", "digest": "a94ee9a2f8cdec6d2f7dd6887d1c7b8e064fcad63030c2c7c001742d72b5603e" }, + { + "name": "raised_hand_with_fingers_splayed_tone3", + "unicode": "1F590-1F3FD", + "digest": "a94ee9a2f8cdec6d2f7dd6887d1c7b8e064fcad63030c2c7c001742d72b5603e" + }, { "name": "hand_splayed_tone4", "unicode": "1F590-1F3FE", "digest": "501792b4126c6f32e755accee0fc8b4d1915e1d36c4ceaa40f3bd0066efe76c3" }, + { + "name": "raised_hand_with_fingers_splayed_tone4", + "unicode": "1F590-1F3FE", + "digest": "501792b4126c6f32e755accee0fc8b4d1915e1d36c4ceaa40f3bd0066efe76c3" + }, { "name": "hand_splayed_tone5", "unicode": "1F590-1F3FF", "digest": "22ed533d587cf44f286e2d6ad77be20b4b5f133c422af4ca51e9af86a75002d8" }, + { + "name": "raised_hand_with_fingers_splayed_tone5", + "unicode": "1F590-1F3FF", + "digest": "22ed533d587cf44f286e2d6ad77be20b4b5f133c422af4ca51e9af86a75002d8" + }, { "name": "hand_victory", "unicode": "1F594", "digest": "2d512ced4e8a438f2a346aed67310d3080f9828c748ade1be95943c32ba1c735" }, + { + "name": "reversed_victory_hand", + "unicode": "1F594", + "digest": "2d512ced4e8a438f2a346aed67310d3080f9828c748ade1be95943c32ba1c735" + }, { "name": "handbag", "unicode": "1F45C", @@ -4104,6 +5834,11 @@ "unicode": "1F915", "digest": "d690b740ff4f58e89dfc764c6411a4e84cfedffd7694eb5efa839a642dbabd08" }, + { + "name": "face_with_head_bandage", + "unicode": "1F915", + "digest": "d690b740ff4f58e89dfc764c6411a4e84cfedffd7694eb5efa839a642dbabd08" + }, { "name": "headphones", "unicode": "1F3A7", @@ -4129,6 +5864,11 @@ "unicode": "2763", "digest": "9751c89dcf10805f2011949ff3ddcb6bcb13de8c32ae5de9e03955e8a4235df2" }, + { + "name": "heavy_heart_exclamation_mark_ornament", + "unicode": "2763", + "digest": "9751c89dcf10805f2011949ff3ddcb6bcb13de8c32ae5de9e03955e8a4235df2" + }, { "name": "heart_eyes", "unicode": "1F60D", @@ -4144,6 +5884,11 @@ "unicode": "1F394", "digest": "2178829e2c85accda55d2f685544587f6de5c8398a127ae1e08ff1c4ab282204" }, + { + "name": "heart_with_tip_on_the_left", + "unicode": "1F394", + "digest": "2178829e2c85accda55d2f685544587f6de5c8398a127ae1e08ff1c4ab282204" + }, { "name": "heartbeat", "unicode": "1F493", @@ -4199,6 +5944,11 @@ "unicode": "26D1", "digest": "affbe9dd87b87ff9235b4858c59c2a73e9ed30dd5221e5b666b8d7747378a9c4" }, + { + "name": "helmet_with_white_cross", + "unicode": "26D1", + "digest": "affbe9dd87b87ff9235b4858c59c2a73e9ed30dd5221e5b666b8d7747378a9c4" + }, { "name": "herb", "unicode": "1F33F", @@ -4234,6 +5984,11 @@ "unicode": "1F3D8", "digest": "9980d6dd6cbd23b820747ecac4cb10974dd24b0c94b4acfe21fa87793ad065c9" }, + { + "name": "house_buildings", + "unicode": "1F3D8", + "digest": "9980d6dd6cbd23b820747ecac4cb10974dd24b0c94b4acfe21fa87793ad065c9" + }, { "name": "honey_pot", "unicode": "1F36F", @@ -4289,6 +6044,11 @@ "unicode": "1F32D", "digest": "58b829e26b5c4642942898d9c7873cb08e048fd7deaacba8292899d5d895cb2b" }, + { + "name": "hot_dog", + "unicode": "1F32D", + "digest": "58b829e26b5c4642942898d9c7873cb08e048fd7deaacba8292899d5d895cb2b" + }, { "name": "hotel", "unicode": "1F3E8", @@ -4319,6 +6079,11 @@ "unicode": "1F3DA", "digest": "e404631e3a296bdeae3de7510da8934c32327bc0fa0f7ae4e676b61932165668" }, + { + "name": "derelict_house_building", + "unicode": "1F3DA", + "digest": "e404631e3a296bdeae3de7510da8934c32327bc0fa0f7ae4e676b61932165668" + }, { "name": "house_with_garden", "unicode": "1F3E1", @@ -4329,6 +6094,11 @@ "unicode": "1F917", "digest": "68ed6c4e0eae9071cf67770a39e07a2290b4f7763170f765b3cd3ac67ae43240" }, + { + "name": "hugging_face", + "unicode": "1F917", + "digest": "68ed6c4e0eae9071cf67770a39e07a2290b4f7763170f765b3cd3ac67ae43240" + }, { "name": "hushed", "unicode": "1F62F", @@ -4379,6 +6149,11 @@ "unicode": "1F6C8", "digest": "59c35e77d5ee663c5d56f7d8af845ce8aeb9935e526ae4a06e02ae70e71212ca" }, + { + "name": "circled_information_source", + "unicode": "1F6C8", + "digest": "59c35e77d5ee663c5d56f7d8af845ce8aeb9935e526ae4a06e02ae70e71212ca" + }, { "name": "information_desk_person", "unicode": "1F481", @@ -4434,6 +6209,11 @@ "unicode": "1F3DD", "digest": "17f02b309b62ed9542b1d8943168302846040e420f413e56d799bb5fba7064fa" }, + { + "name": "desert_island", + "unicode": "1F3DD", + "digest": "17f02b309b62ed9542b1d8943168302846040e420f413e56d799bb5fba7064fa" + }, { "name": "izakaya_lantern", "unicode": "1F3EE", @@ -4474,6 +6254,11 @@ "unicode": "1F6E6", "digest": "3708e5e034b1c64d1268d66527e13c369aa0f8903bce9172bef773b2d1940948" }, + { + "name": "up_pointing_military_airplane", + "unicode": "1F6E6", + "digest": "3708e5e034b1c64d1268d66527e13c369aa0f8903bce9172bef773b2d1940948" + }, { "name": "joy", "unicode": "1F602", @@ -4504,21 +6289,41 @@ "unicode": "1F5DD", "digest": "87a7d42531d7a11dcb11b0d6d1be611ee8cec35b5d22226a8ac6083fedef4f5d" }, + { + "name": "old_key", + "unicode": "1F5DD", + "digest": "87a7d42531d7a11dcb11b0d6d1be611ee8cec35b5d22226a8ac6083fedef4f5d" + }, { "name": "keyboard", "unicode": "1F5AE", "digest": "3b254cbf19946df3af05e501d11653d89fcda91684b7248d86186f842b83bf16" }, + { + "name": "wired_keyboard", + "unicode": "1F5AE", + "digest": "3b254cbf19946df3af05e501d11653d89fcda91684b7248d86186f842b83bf16" + }, { "name": "keyboard_mouse", "unicode": "1F5A6", "digest": "95b523e55d8afeaeb06442bbe20e47f49643bb0c32d89a8cdbbccdead20532b3" }, + { + "name": "keyboard_and_mouse", + "unicode": "1F5A6", + "digest": "95b523e55d8afeaeb06442bbe20e47f49643bb0c32d89a8cdbbccdead20532b3" + }, { "name": "keyboard_with_jacks", "unicode": "1F398", "digest": "e29a0d0b8018d13458469edca13c60a882a2817957c1aa11b050684c995a47ee" }, + { + "name": "musical_keyboard_with_jacks", + "unicode": "1F398", + "digest": "e29a0d0b8018d13458469edca13c60a882a2817957c1aa11b050684c995a47ee" + }, { "name": "keycap_ten", "unicode": "1F51F", @@ -4539,11 +6344,21 @@ "unicode": "1F468-2764-1F48B-1F468", "digest": "381364ad988ec07cc3708fd60f71838092224009088fff587069b4e8ab01ee63" }, + { + "name": "couplekiss_mm", + "unicode": "1F468-2764-1F48B-1F468", + "digest": "381364ad988ec07cc3708fd60f71838092224009088fff587069b4e8ab01ee63" + }, { "name": "kiss_ww", "unicode": "1F469-2764-1F48B-1F469", "digest": "7705ca707b73f44c856ea324bdfe30ed05244c8d192d1111f6e1d62ab3f2f8a5" }, + { + "name": "couplekiss_ww", + "unicode": "1F469-2764-1F48B-1F469", + "digest": "7705ca707b73f44c856ea324bdfe30ed05244c8d192d1111f6e1d62ab3f2f8a5" + }, { "name": "kissing", "unicode": "1F617", @@ -4619,6 +6434,11 @@ "unicode": "1F606", "digest": "f22d3be77f1daf058d04c3cbc1fd7f76b4dc069d2d300b45e63e768b08d269c5" }, + { + "name": "satisfied", + "unicode": "1F606", + "digest": "f22d3be77f1daf058d04c3cbc1fd7f76b4dc069d2d300b45e63e768b08d269c5" + }, { "name": "leaves", "unicode": "1F343", @@ -4639,6 +6459,11 @@ "unicode": "1F57B", "digest": "8052e44951afee04c87296128744b5019ec783c9ed1a231f659af6c8ddaa50f3" }, + { + "name": "left_hand_telephone_receiver", + "unicode": "1F57B", + "digest": "8052e44951afee04c87296128744b5019ec783c9ed1a231f659af6c8ddaa50f3" + }, { "name": "left_right_arrow", "unicode": "2194", @@ -4674,6 +6499,11 @@ "unicode": "1F574", "digest": "3e4e9a5ac6a8dbd7909c58a9d915f16f1a0fc59cc019714ae5935f18e4704044" }, + { + "name": "man_in_business_suit_levitating", + "unicode": "1F574", + "digest": "3e4e9a5ac6a8dbd7909c58a9d915f16f1a0fc59cc019714ae5935f18e4704044" + }, { "name": "libra", "unicode": "264E", @@ -4684,36 +6514,71 @@ "unicode": "1F3CB", "digest": "f64db037fd21e5918e5de35d6a561ef4b44668e307ed351338de00fcf3e771e3" }, + { + "name": "weight_lifter", + "unicode": "1F3CB", + "digest": "f64db037fd21e5918e5de35d6a561ef4b44668e307ed351338de00fcf3e771e3" + }, { "name": "lifter_tone1", "unicode": "1F3CB-1F3FB", "digest": "f9e0d161b12c4908ac3409b11c1a77ee38f33ba018f12416545876214bfb7c01" }, + { + "name": "weight_lifter_tone1", + "unicode": "1F3CB-1F3FB", + "digest": "f9e0d161b12c4908ac3409b11c1a77ee38f33ba018f12416545876214bfb7c01" + }, { "name": "lifter_tone2", "unicode": "1F3CB-1F3FC", "digest": "631eb6ed5bd147dc6f1f8b94149abe44d62a0f78e7809e37a4bfe127c40ed98f" }, + { + "name": "weight_lifter_tone2", + "unicode": "1F3CB-1F3FC", + "digest": "631eb6ed5bd147dc6f1f8b94149abe44d62a0f78e7809e37a4bfe127c40ed98f" + }, { "name": "lifter_tone3", "unicode": "1F3CB-1F3FD", "digest": "406b5707a47d9066f016acf0b64fa695e3505acc2453758a0428de21efd7eb6d" }, + { + "name": "weight_lifter_tone3", + "unicode": "1F3CB-1F3FD", + "digest": "406b5707a47d9066f016acf0b64fa695e3505acc2453758a0428de21efd7eb6d" + }, { "name": "lifter_tone4", "unicode": "1F3CB-1F3FE", "digest": "d917164ed8c4bb1ffcc887ca256ec329e7fa1b9516eaf8c159f8b43fdb071ed6" }, + { + "name": "weight_lifter_tone4", + "unicode": "1F3CB-1F3FE", + "digest": "d917164ed8c4bb1ffcc887ca256ec329e7fa1b9516eaf8c159f8b43fdb071ed6" + }, { "name": "lifter_tone5", "unicode": "1F3CB-1F3FF", "digest": "f79ea93e8a40b3c895b693bf49eb4ce6e7b3f4413595e5881ea44839fd7fe8e5" }, + { + "name": "weight_lifter_tone5", + "unicode": "1F3CB-1F3FF", + "digest": "f79ea93e8a40b3c895b693bf49eb4ce6e7b3f4413595e5881ea44839fd7fe8e5" + }, { "name": "light_check_mark", "unicode": "1F5F8", "digest": "7842b0df8c2b6703bed0cce5d2790d394eec7120b2a245a76f375528f2729a7b" }, + { + "name": "light_mark", + "unicode": "1F5F8", + "digest": "7842b0df8c2b6703bed0cce5d2790d394eec7120b2a245a76f375528f2729a7b" + }, { "name": "light_rail", "unicode": "1F688", @@ -4729,6 +6594,11 @@ "unicode": "1F981", "digest": "935b1076815f51fafcd860a395d0a03c536acfcea61ffcf542a377da046fa7d9" }, + { + "name": "lion", + "unicode": "1F981", + "digest": "935b1076815f51fafcd860a395d0a03c536acfcea61ffcf542a377da046fa7d9" + }, { "name": "lips", "unicode": "1F444", @@ -4929,6 +6799,11 @@ "unicode": "1F5FA", "digest": "f56116d09996d6d08fb5cdfb46622b545253f2649008170fc2011a9713fa875b" }, + { + "name": "world_map", + "unicode": "1F5FA", + "digest": "f56116d09996d6d08fb5cdfb46622b545253f2649008170fc2011a9713fa875b" + }, { "name": "maple_leaf", "unicode": "1F341", @@ -4979,6 +6854,11 @@ "unicode": "1F3C5", "digest": "270d438b6e2155e944dc734ea3e4d02409e51f59db2db636398fbf96e5edb0e6" }, + { + "name": "sports_medal", + "unicode": "1F3C5", + "digest": "270d438b6e2155e944dc734ea3e4d02409e51f59db2db636398fbf96e5edb0e6" + }, { "name": "mega", "unicode": "1F4E3", @@ -5004,31 +6884,61 @@ "unicode": "1F918", "digest": "45e5fac0b9b019cf217dcfd1380cafb0d03063454612178278dac1ca5f8476a6" }, + { + "name": "sign_of_the_horns", + "unicode": "1F918", + "digest": "45e5fac0b9b019cf217dcfd1380cafb0d03063454612178278dac1ca5f8476a6" + }, { "name": "metal_tone1", "unicode": "1F918-1F3FB", "digest": "9b3596fe7c063df838f0a43fb680ce10fb88e2b73c5c3324abfa357a224c17aa" }, + { + "name": "sign_of_the_horns_tone1", + "unicode": "1F918-1F3FB", + "digest": "9b3596fe7c063df838f0a43fb680ce10fb88e2b73c5c3324abfa357a224c17aa" + }, { "name": "metal_tone2", "unicode": "1F918-1F3FC", "digest": "e15a4898a0efca4354ac48d6b01ff0618ce8b110b1246a4f5d78e19b54658be6" }, + { + "name": "sign_of_the_horns_tone2", + "unicode": "1F918-1F3FC", + "digest": "e15a4898a0efca4354ac48d6b01ff0618ce8b110b1246a4f5d78e19b54658be6" + }, { "name": "metal_tone3", "unicode": "1F918-1F3FD", "digest": "c159e8179cb1907c246b432d87c5253b914fd7cebb6ac05292c4e38eff4815b0" }, + { + "name": "sign_of_the_horns_tone3", + "unicode": "1F918-1F3FD", + "digest": "c159e8179cb1907c246b432d87c5253b914fd7cebb6ac05292c4e38eff4815b0" + }, { "name": "metal_tone4", "unicode": "1F918-1F3FE", "digest": "a8a43a88028c97074321e3da56df1045db41ede58bf286c21d7ae90f222f2011" }, + { + "name": "sign_of_the_horns_tone4", + "unicode": "1F918-1F3FE", + "digest": "a8a43a88028c97074321e3da56df1045db41ede58bf286c21d7ae90f222f2011" + }, { "name": "metal_tone5", "unicode": "1F918-1F3FF", "digest": "e6611e826e867e2c73a8cadb138e4aa6365e3583dd229ff24b3e8f161904bf56" }, + { + "name": "sign_of_the_horns_tone5", + "unicode": "1F918-1F3FF", + "digest": "e6611e826e867e2c73a8cadb138e4aa6365e3583dd229ff24b3e8f161904bf56" + }, { "name": "metro", "unicode": "1F687", @@ -5044,6 +6954,11 @@ "unicode": "1F399", "digest": "f9df32cd207808f67a895d3460a215d1ecc42e377907bcd64731c02b697d4f32" }, + { + "name": "studio_microphone", + "unicode": "1F399", + "digest": "f9df32cd207808f67a895d3460a215d1ecc42e377907bcd64731c02b697d4f32" + }, { "name": "microscope", "unicode": "1F52C", @@ -5054,31 +6969,61 @@ "unicode": "1F595", "digest": "c6320b236a4a9593aeade511b52dd3114207e947458cb3b818c78737a505fdf6" }, + { + "name": "reversed_hand_with_middle_finger_extended", + "unicode": "1F595", + "digest": "c6320b236a4a9593aeade511b52dd3114207e947458cb3b818c78737a505fdf6" + }, { "name": "middle_finger_tone1", "unicode": "1F595-1F3FB", "digest": "93c7aa994856185519d576cb779bdcff3a33f7077eef98e70968125f92f02448" }, + { + "name": "reversed_hand_with_middle_finger_extended_tone1", + "unicode": "1F595-1F3FB", + "digest": "93c7aa994856185519d576cb779bdcff3a33f7077eef98e70968125f92f02448" + }, { "name": "middle_finger_tone2", "unicode": "1F595-1F3FC", "digest": "a0de802294717b80e08d9d30f5fd64eacb90b5b3b9d7a0c27d6226a22822597f" }, + { + "name": "reversed_hand_with_middle_finger_extended_tone2", + "unicode": "1F595-1F3FC", + "digest": "a0de802294717b80e08d9d30f5fd64eacb90b5b3b9d7a0c27d6226a22822597f" + }, { "name": "middle_finger_tone3", "unicode": "1F595-1F3FD", "digest": "8bbbab07c838257416bbf8377904362c07019fca9d5abf9fd048ccf6370178da" }, + { + "name": "reversed_hand_with_middle_finger_extended_tone3", + "unicode": "1F595-1F3FD", + "digest": "8bbbab07c838257416bbf8377904362c07019fca9d5abf9fd048ccf6370178da" + }, { "name": "middle_finger_tone4", "unicode": "1F595-1F3FE", "digest": "d9eed8db540fdb669c6ae5ef168b77659660589f5ddd9b66062274d335a3ef04" }, + { + "name": "reversed_hand_with_middle_finger_extended_tone4", + "unicode": "1F595-1F3FE", + "digest": "d9eed8db540fdb669c6ae5ef168b77659660589f5ddd9b66062274d335a3ef04" + }, { "name": "middle_finger_tone5", "unicode": "1F595-1F3FF", "digest": "0519c3298040e57db202294476df239edb9b23b44848bab296bc45eda7cf8664" }, + { + "name": "reversed_hand_with_middle_finger_extended_tone5", + "unicode": "1F595-1F3FF", + "digest": "0519c3298040e57db202294476df239edb9b23b44848bab296bc45eda7cf8664" + }, { "name": "military_medal", "unicode": "1F396", @@ -5109,6 +7054,11 @@ "unicode": "1F911", "digest": "3ac2f9b5409e1426eef6966938ca04cf78aeffefd43f44b6c86af4af7836e22f" }, + { + "name": "money_mouth_face", + "unicode": "1F911", + "digest": "3ac2f9b5409e1426eef6966938ca04cf78aeffefd43f44b6c86af4af7836e22f" + }, { "name": "money_with_wings", "unicode": "1F4B8", @@ -5144,11 +7094,21 @@ "unicode": "1F5F1", "digest": "4af3e4e53eaa328b0d20542ab31705a74bf9fd368cd0673b706838ce1681d3c9" }, + { + "name": "lightning_mood_bubble", + "unicode": "1F5F1", + "digest": "4af3e4e53eaa328b0d20542ab31705a74bf9fd368cd0673b706838ce1681d3c9" + }, { "name": "mood_lightning", "unicode": "1F5F2", "digest": "6784635e81ec722fd50a1c2a23b0f9679e4bf1b5ae2b5a01eeb995bc1f7a426f" }, + { + "name": "lightning_mood", + "unicode": "1F5F2", + "digest": "6784635e81ec722fd50a1c2a23b0f9679e4bf1b5ae2b5a01eeb995bc1f7a426f" + }, { "name": "mortar_board", "unicode": "1F393", @@ -5169,6 +7129,11 @@ "unicode": "1F3CD", "digest": "8429fb6dfeb873abdffcc179c32d4f23e91c9e6b27b06cd204fd2e83cc11189e" }, + { + "name": "racing_motorcycle", + "unicode": "1F3CD", + "digest": "8429fb6dfeb873abdffcc179c32d4f23e91c9e6b27b06cd204fd2e83cc11189e" + }, { "name": "motorway", "unicode": "1F6E3", @@ -5229,6 +7194,11 @@ "unicode": "1F3D4", "digest": "9939aade3d4d972ba3af16fcc6cc2454978f5426e4c92838734a44db065ce0ff" }, + { + "name": "snow_capped_mountain", + "unicode": "1F3D4", + "digest": "9939aade3d4d972ba3af16fcc6cc2454978f5426e4c92838734a44db065ce0ff" + }, { "name": "mouse", "unicode": "1F42D", @@ -5244,11 +7214,21 @@ "unicode": "1F5AF", "digest": "e0d2055ccba489d24e0c0b6d2f22793efe48a734b0fd50f5af88f721b40665c0" }, + { + "name": "one_button_mouse", + "unicode": "1F5AF", + "digest": "e0d2055ccba489d24e0c0b6d2f22793efe48a734b0fd50f5af88f721b40665c0" + }, { "name": "mouse_three_button", "unicode": "1F5B1", "digest": "6a5629fee01145211cc8f4e8f59c5f1e61affed38c650502213d76c7d8861b01" }, + { + "name": "three_button_mouse", + "unicode": "1F5B1", + "digest": "6a5629fee01145211cc8f4e8f59c5f1e61affed38c650502213d76c7d8861b01" + }, { "name": "movie_camera", "unicode": "1F3A5", @@ -5364,11 +7344,21 @@ "unicode": "1F913", "digest": "94efd551700aae8909b8dd7a78a54a33e070d24b2e0a10534353645084614e98" }, + { + "name": "nerd_face", + "unicode": "1F913", + "digest": "94efd551700aae8909b8dd7a78a54a33e070d24b2e0a10534353645084614e98" + }, { "name": "network", "unicode": "1F5A7", "digest": "1dbaa54deeb2328fd8a3f044e450c97ac3ff39627c598bb2f4312d677482ee06" }, + { + "name": "three_networked_computers", + "unicode": "1F5A7", + "digest": "1dbaa54deeb2328fd8a3f044e450c97ac3ff39627c598bb2f4312d677482ee06" + }, { "name": "neutral_face", "unicode": "1F610", @@ -5399,6 +7389,11 @@ "unicode": "1F5DE", "digest": "0ca6b5850091f23295c970815a8e64a52e3c3dae492029ecb1e0726c2693f9bf" }, + { + "name": "rolled_up_newspaper", + "unicode": "1F5DE", + "digest": "0ca6b5850091f23295c970815a8e64a52e3c3dae492029ecb1e0726c2693f9bf" + }, { "name": "ng", "unicode": "1F196", @@ -5524,11 +7519,21 @@ "unicode": "1F5C9", "digest": "073660fdaa02ecf98d04f61f8d65d6cc447ccae3825fccaff19a2c99ebba52af" }, + { + "name": "note_page", + "unicode": "1F5C9", + "digest": "073660fdaa02ecf98d04f61f8d65d6cc447ccae3825fccaff19a2c99ebba52af" + }, { "name": "note_empty", "unicode": "1F5C6", "digest": "06b56eeaca6349bbcf1020bea98f937450a7e086db65cd5d7497748e0fb607be" }, + { + "name": "empty_note_page", + "unicode": "1F5C6", + "digest": "06b56eeaca6349bbcf1020bea98f937450a7e086db65cd5d7497748e0fb607be" + }, { "name": "notebook", "unicode": "1F4D3", @@ -5544,16 +7549,31 @@ "unicode": "1F5CA", "digest": "85069e2d13540886457368a57295072aec44c7137d9223bfcf908ce1f0e5124e" }, + { + "name": "note_pad", + "unicode": "1F5CA", + "digest": "85069e2d13540886457368a57295072aec44c7137d9223bfcf908ce1f0e5124e" + }, { "name": "notepad_empty", "unicode": "1F5C7", "digest": "8be5053e74c13d8220917c5aee1f4afdecb001612886438f283b0c2a0fecf6af" }, + { + "name": "empty_note_pad", + "unicode": "1F5C7", + "digest": "8be5053e74c13d8220917c5aee1f4afdecb001612886438f283b0c2a0fecf6af" + }, { "name": "notepad_spiral", "unicode": "1F5D2", "digest": "c181b6c1cc6063ec1848e46cbbf1d8b890c53b59cdc5218311ce06889570e727" }, + { + "name": "spiral_note_pad", + "unicode": "1F5D2", + "digest": "c181b6c1cc6063ec1848e46cbbf1d8b890c53b59cdc5218311ce06889570e727" + }, { "name": "notes", "unicode": "1F3B6", @@ -5599,6 +7619,11 @@ "unicode": "1F6E2", "digest": "f8b7626cb09e229203105b9c8c7f3fbb38c0650021092fc50115ad517248644a" }, + { + "name": "oil_drum", + "unicode": "1F6E2", + "digest": "f8b7626cb09e229203105b9c8c7f3fbb38c0650021092fc50115ad517248644a" + }, { "name": "ok", "unicode": "1F197", @@ -5699,31 +7724,61 @@ "unicode": "1F475", "digest": "3ed599443eed25399aac999fc234c9e97f8fb6ec567e37a553c26e01021b097c" }, + { + "name": "grandma", + "unicode": "1F475", + "digest": "3ed599443eed25399aac999fc234c9e97f8fb6ec567e37a553c26e01021b097c" + }, { "name": "older_woman_tone1", "unicode": "1F475-1F3FB", "digest": "7421c5dba67cfd1eeabb2fa8faf4aa0d615d23f191cf7d7c0ad9c1fa884edfda" }, + { + "name": "grandma_tone1", + "unicode": "1F475-1F3FB", + "digest": "7421c5dba67cfd1eeabb2fa8faf4aa0d615d23f191cf7d7c0ad9c1fa884edfda" + }, { "name": "older_woman_tone2", "unicode": "1F475-1F3FC", "digest": "65edeef25648ac7f8be535df06af1286441691fa15176e99a6e83fc779aa2cde" }, + { + "name": "grandma_tone2", + "unicode": "1F475-1F3FC", + "digest": "65edeef25648ac7f8be535df06af1286441691fa15176e99a6e83fc779aa2cde" + }, { "name": "older_woman_tone3", "unicode": "1F475-1F3FD", "digest": "5d27bbcc5796227a9caec1c7612d3f691055655b96f7303e420839463d76c269" }, + { + "name": "grandma_tone3", + "unicode": "1F475-1F3FD", + "digest": "5d27bbcc5796227a9caec1c7612d3f691055655b96f7303e420839463d76c269" + }, { "name": "older_woman_tone4", "unicode": "1F475-1F3FE", "digest": "75b858e910175fc0233503d672120fd43ac035ba3fd2052fbb44df39f6e3695c" }, + { + "name": "grandma_tone4", + "unicode": "1F475-1F3FE", + "digest": "75b858e910175fc0233503d672120fd43ac035ba3fd2052fbb44df39f6e3695c" + }, { "name": "older_woman_tone5", "unicode": "1F475-1F3FF", "digest": "9da1cf10a605c470877d7f4a840f99344b1ec2e7b1ec7db61e930cde77025e3b" }, + { + "name": "grandma_tone5", + "unicode": "1F475-1F3FF", + "digest": "9da1cf10a605c470877d7f4a840f99344b1ec2e7b1ec7db61e930cde77025e3b" + }, { "name": "om_symbol", "unicode": "1F549", @@ -5809,6 +7864,11 @@ "unicode": "1F5B8", "digest": "df8c10028d29d65f144a6b789d1c3294e7b3293554c4c30d28d72dc7ba8d9a5d" }, + { + "name": "optical_disc_icon", + "unicode": "1F5B8", + "digest": "df8c10028d29d65f144a6b789d1c3294e7b3293554c4c30d28d72dc7ba8d9a5d" + }, { "name": "orange_book", "unicode": "1F4D9", @@ -5864,6 +7924,11 @@ "unicode": "1F58C", "digest": "73eb33184f5f495d6c2699fafc1a8680069f82a70fbe519290c3a2ce30d1aee9" }, + { + "name": "lower_left_paintbrush", + "unicode": "1F58C", + "digest": "73eb33184f5f495d6c2699fafc1a8680069f82a70fbe519290c3a2ce30d1aee9" + }, { "name": "palm_tree", "unicode": "1F334", @@ -5884,11 +7949,21 @@ "unicode": "1F587", "digest": "7071e031f4a100c3cb3573fbfa375360043f0276289a0818f2ffaf71b3580040" }, + { + "name": "linked_paperclips", + "unicode": "1F587", + "digest": "7071e031f4a100c3cb3573fbfa375360043f0276289a0818f2ffaf71b3580040" + }, { "name": "park", "unicode": "1F3DE", "digest": "d257f0f1b1a0134573f80ba1a5f522a91c320ee7f93a1cb64877c077e7e19b50" }, + { + "name": "national_park", + "unicode": "1F3DE", + "digest": "d257f0f1b1a0134573f80ba1a5f522a91c320ee7f93a1cb64877c077e7e19b50" + }, { "name": "parking", "unicode": "1F17F", @@ -5914,11 +7989,21 @@ "unicode": "23F8", "digest": "edd605ffaa39a7905ed0958b7cc69f00f5b271e579198d2df1746ad1b3648272" }, + { + "name": "double_vertical_bar", + "unicode": "23F8", + "digest": "edd605ffaa39a7905ed0958b7cc69f00f5b271e579198d2df1746ad1b3648272" + }, { "name": "peace", "unicode": "262E", "digest": "e0ee8a5c9fb18d5db6841b21527ed8fd955abdff9ffdb7b2684dca22107015fc" }, + { + "name": "peace_symbol", + "unicode": "262E", + "digest": "e0ee8a5c9fb18d5db6841b21527ed8fd955abdff9ffdb7b2684dca22107015fc" + }, { "name": "peach", "unicode": "1F351", @@ -5934,16 +8019,31 @@ "unicode": "1F58A", "digest": "6becdc6f622c774bb09b7e7592bba2123ecccc9de32a35f0b18b50d7d54109cb" }, + { + "name": "lower_left_ballpoint_pen", + "unicode": "1F58A", + "digest": "6becdc6f622c774bb09b7e7592bba2123ecccc9de32a35f0b18b50d7d54109cb" + }, { "name": "pen_fountain", "unicode": "1F58B", "digest": "8c78cf0c2bd1d5e309d2d3356ff207e3fc76ca18dd6b90762cb62f6afbc95c6a" }, + { + "name": "lower_left_fountain_pen", + "unicode": "1F58B", + "digest": "8c78cf0c2bd1d5e309d2d3356ff207e3fc76ca18dd6b90762cb62f6afbc95c6a" + }, { "name": "pencil", "unicode": "1F4DD", "digest": "62b7ee5d9352114d09ee6f2c9a4c5e8b79f775a6c509e82ddfcdd61e13716249" }, + { + "name": "memo", + "unicode": "1F4DD", + "digest": "62b7ee5d9352114d09ee6f2c9a4c5e8b79f775a6c509e82ddfcdd61e13716249" + }, { "name": "pencil2", "unicode": "270F", @@ -5954,6 +8054,11 @@ "unicode": "1F589", "digest": "52c1ba1228917eb491ac1745a495e0fdafba6b985a81caba250f71d1f94c725c" }, + { + "name": "lower_left_pencil", + "unicode": "1F589", + "digest": "52c1ba1228917eb491ac1745a495e0fdafba6b985a81caba250f71d1f94c725c" + }, { "name": "penguin", "unicode": "1F427", @@ -5964,11 +8069,21 @@ "unicode": "1F3F2", "digest": "cd3c33bfc3c7fbe84b98d2d481d56a7bf5488ff94afadd8b5a0e454768b80269" }, + { + "name": "black_pennant", + "unicode": "1F3F2", + "digest": "cd3c33bfc3c7fbe84b98d2d481d56a7bf5488ff94afadd8b5a0e454768b80269" + }, { "name": "pennant_white", "unicode": "1F3F1", "digest": "818b1be73540f2cfeb1c514e1ee75d18715af317f0db817d9ae081b9ea33d4b0" }, + { + "name": "white_pennant", + "unicode": "1F3F1", + "digest": "818b1be73540f2cfeb1c514e1ee75d18715af317f0db817d9ae081b9ea33d4b0" + }, { "name": "pensive", "unicode": "1F614", @@ -6109,11 +8224,21 @@ "unicode": "1F3D3", "digest": "dd2a84716c93410a285ff759bfbc2dc31a10f90b203c7a657b908e5949e89a39" }, + { + "name": "table_tennis", + "unicode": "1F3D3", + "digest": "dd2a84716c93410a285ff759bfbc2dc31a10f90b203c7a657b908e5949e89a39" + }, { "name": "piracy", "unicode": "1F572", "digest": "f42955ba75c598392e5e258be49968d858c876e0d6e7aa9dc795f7e8cff42be9" }, + { + "name": "no_piracy", + "unicode": "1F572", + "digest": "f42955ba75c598392e5e258be49968d858c876e0d6e7aa9dc795f7e8cff42be9" + }, { "name": "pisces", "unicode": "2653", @@ -6129,6 +8254,11 @@ "unicode": "1F6D0", "digest": "4fabc307b7e35f94288f6d53985485662a4814b11a9a382f0a3873d41b1290d3" }, + { + "name": "worship_symbol", + "unicode": "1F6D0", + "digest": "4fabc307b7e35f94288f6d53985485662a4814b11a9a382f0a3873d41b1290d3" + }, { "name": "play_pause", "unicode": "23EF", @@ -6299,6 +8429,21 @@ "unicode": "1F4A9", "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258" }, + { + "name": "shit", + "unicode": "1F4A9", + "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258" + }, + { + "name": "hankey", + "unicode": "1F4A9", + "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258" + }, + { + "name": "poo", + "unicode": "1F4A9", + "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258" + }, { "name": "popcorn", "unicode": "1F37F", @@ -6419,11 +8564,21 @@ "unicode": "1F6C7", "digest": "bc6cdea2269a0ec39576d98dc4cda2bd9efa4dc330dde870148c6a85ad9cc63f" }, + { + "name": "prohibited_sign", + "unicode": "1F6C7", + "digest": "bc6cdea2269a0ec39576d98dc4cda2bd9efa4dc330dde870148c6a85ad9cc63f" + }, { "name": "projector", "unicode": "1F4FD", "digest": "fc361282f367926254c08150b02cb8fda7fa8d2c9c939d9360c78bf19a4f982e" }, + { + "name": "film_projector", + "unicode": "1F4FD", + "digest": "fc361282f367926254c08150b02cb8fda7fa8d2c9c939d9360c78bf19a4f982e" + }, { "name": "punch", "unicode": "1F44A", @@ -6499,6 +8654,11 @@ "unicode": "1F3CE", "digest": "2e9828e3884c79ad7e9e1173d3470790f3f56cfa08ef4e38deff45db0728c66c" }, + { + "name": "racing_car", + "unicode": "1F3CE", + "digest": "2e9828e3884c79ad7e9e1173d3470790f3f56cfa08ef4e38deff45db0728c66c" + }, { "name": "racehorse", "unicode": "1F40E", @@ -6519,6 +8679,11 @@ "unicode": "2622", "digest": "5ad8e8594617c0153672a76421deb836e05c6098020c33af3f975f8fcfe216e4" }, + { + "name": "radioactive_sign", + "unicode": "2622", + "digest": "5ad8e8594617c0153672a76421deb836e05c6098020c33af3f975f8fcfe216e4" + }, { "name": "rage", "unicode": "1F621", @@ -6534,6 +8699,11 @@ "unicode": "1F6E4", "digest": "63ee881cc775d5b2711082b6c96ab44d5204c5d390afd6d8ee97e52aeeaa5e5e" }, + { + "name": "railroad_track", + "unicode": "1F6E4", + "digest": "63ee881cc775d5b2711082b6c96ab44d5204c5d390afd6d8ee97e52aeeaa5e5e" + }, { "name": "rainbow", "unicode": "1F308", @@ -6744,11 +8914,21 @@ "unicode": "1F569", "digest": "5b92daa87bdf6ee15e798bec382a2ee885f4e6e77a68a3f626adcfe4c782b375" }, + { + "name": "right_speaker_with_one_sound_wave", + "unicode": "1F569", + "digest": "5b92daa87bdf6ee15e798bec382a2ee885f4e6e77a68a3f626adcfe4c782b375" + }, { "name": "right_speaker_three", "unicode": "1F56A", "digest": "4d00b720a65bd0f4c3682b290b1976ec2388d6ae61225398f4e70556ae9e5f80" }, + { + "name": "right_speaker_with_three_sound_waves", + "unicode": "1F56A", + "digest": "4d00b720a65bd0f4c3682b290b1976ec2388d6ae61225398f4e70556ae9e5f80" + }, { "name": "ring", "unicode": "1F48D", @@ -6764,6 +8944,11 @@ "unicode": "1F916", "digest": "cc0e363774b86e21a5b2cea7f7af85bca9e92c124ebcd39c6067c125048baa60" }, + { + "name": "robot_face", + "unicode": "1F916", + "digest": "cc0e363774b86e21a5b2cea7f7af85bca9e92c124ebcd39c6067c125048baa60" + }, { "name": "rocket", "unicode": "1F680", @@ -6779,6 +8964,11 @@ "unicode": "1F644", "digest": "f596f203030b6c9bd743848512aa3fc7919447020d35ae5c2bf13ccb16fa2dbe" }, + { + "name": "face_with_rolling_eyes", + "unicode": "1F644", + "digest": "f596f203030b6c9bd743848512aa3fc7919447020d35ae5c2bf13ccb16fa2dbe" + }, { "name": "rooster", "unicode": "1F413", @@ -7099,11 +9289,21 @@ "unicode": "1F480", "digest": "dfd169764b192ac7c6e5101277dd9f1e010e86bdd32ad37e00ed4499fc0a5dd6" }, + { + "name": "skeleton", + "unicode": "1F480", + "digest": "dfd169764b192ac7c6e5101277dd9f1e010e86bdd32ad37e00ed4499fc0a5dd6" + }, { "name": "skull_crossbones", "unicode": "2620", "digest": "e2acf0f36b6a6800c1829a1c6551b5d0eb6dcdef4b7f02070cf69570aeab608c" }, + { + "name": "skull_and_crossbones", + "unicode": "2620", + "digest": "e2acf0f36b6a6800c1829a1c6551b5d0eb6dcdef4b7f02070cf69570aeab608c" + }, { "name": "sleeping", "unicode": "1F634", @@ -7124,11 +9324,21 @@ "unicode": "1F641", "digest": "3ae82b38b58ffa50eddebd87153428d880ca181f4f4178a9ca3bd813ea15ccbc" }, + { + "name": "slightly_frowning_face", + "unicode": "1F641", + "digest": "3ae82b38b58ffa50eddebd87153428d880ca181f4f4178a9ca3bd813ea15ccbc" + }, { "name": "slight_smile", "unicode": "1F642", "digest": "5eee09f634a4e2031927d008a6530a258a00e611ead0c386dd5b7ebb5e75a306" }, + { + "name": "slightly_smiling_face", + "unicode": "1F642", + "digest": "5eee09f634a4e2031927d008a6530a258a00e611ead0c386dd5b7ebb5e75a306" + }, { "name": "slot_machine", "unicode": "1F3B0", @@ -7299,6 +9509,11 @@ "unicode": "1F5E3", "digest": "d92cfe1200887300b2f05f9576448a2f2a79d0accd51f323a65ce3db0aa5639b" }, + { + "name": "speaking_head_in_silhouette", + "unicode": "1F5E3", + "digest": "d92cfe1200887300b2f05f9576448a2f2a79d0accd51f323a65ce3db0aa5639b" + }, { "name": "speech_balloon", "unicode": "1F4AC", @@ -7309,21 +9524,41 @@ "unicode": "1F5E8", "digest": "478b0b07460a9f54b7d0050f886da59fde5e428daa11e899fc31477fda1707ed" }, + { + "name": "left_speech_bubble", + "unicode": "1F5E8", + "digest": "478b0b07460a9f54b7d0050f886da59fde5e428daa11e899fc31477fda1707ed" + }, { "name": "speech_right", "unicode": "1F5E9", "digest": "8439b13779163c15e678a78b08ebeeb7d131632df21d2a7868de7fed38ca9d8a" }, + { + "name": "right_speech_bubble", + "unicode": "1F5E9", + "digest": "8439b13779163c15e678a78b08ebeeb7d131632df21d2a7868de7fed38ca9d8a" + }, { "name": "speech_three", "unicode": "1F5EB", "digest": "55a934f3659b6e75fdce0d0c4e2ea56dd34a43892c85a6666bd1882a0bfb92a9" }, + { + "name": "three_speech_bubbles", + "unicode": "1F5EB", + "digest": "55a934f3659b6e75fdce0d0c4e2ea56dd34a43892c85a6666bd1882a0bfb92a9" + }, { "name": "speech_two", "unicode": "1F5EA", "digest": "0563ef0591da243673cf877462acc5d8e1d980a56e81668ac627de74d0c33983" }, + { + "name": "two_speech_bubbles", + "unicode": "1F5EA", + "digest": "0563ef0591da243673cf877462acc5d8e1d980a56e81668ac627de74d0c33983" + }, { "name": "speedboat", "unicode": "1F6A4", @@ -7344,31 +9579,61 @@ "unicode": "1F575", "digest": "eaa570a36d83119d0a596228e74affe84d7355714ff6901d88a89410d26dec2a" }, + { + "name": "sleuth_or_spy", + "unicode": "1F575", + "digest": "eaa570a36d83119d0a596228e74affe84d7355714ff6901d88a89410d26dec2a" + }, { "name": "spy_tone1", "unicode": "1F575-1F3FB", "digest": "abdc066d4cad6a17047faf7806c45feb43ae1e2056cf500536f08f4173dbfa94" }, + { + "name": "sleuth_or_spy_tone1", + "unicode": "1F575-1F3FB", + "digest": "abdc066d4cad6a17047faf7806c45feb43ae1e2056cf500536f08f4173dbfa94" + }, { "name": "spy_tone2", "unicode": "1F575-1F3FC", "digest": "72a3313ef12364105e764cc3deabd47eb6bd086f261c435682ae1cd29dc8230b" }, + { + "name": "sleuth_or_spy_tone2", + "unicode": "1F575-1F3FC", + "digest": "72a3313ef12364105e764cc3deabd47eb6bd086f261c435682ae1cd29dc8230b" + }, { "name": "spy_tone3", "unicode": "1F575-1F3FD", "digest": "2a1108d3d2e778f88aa5b3ae36705c877b84d0bf6b421409582ba748aeb2aee7" }, + { + "name": "sleuth_or_spy_tone3", + "unicode": "1F575-1F3FD", + "digest": "2a1108d3d2e778f88aa5b3ae36705c877b84d0bf6b421409582ba748aeb2aee7" + }, { "name": "spy_tone4", "unicode": "1F575-1F3FE", "digest": "1d4fe62912384bc0d687bcf4565752caf0ed6146c903a156d1c6ba6ea239b154" }, + { + "name": "sleuth_or_spy_tone4", + "unicode": "1F575-1F3FE", + "digest": "1d4fe62912384bc0d687bcf4565752caf0ed6146c903a156d1c6ba6ea239b154" + }, { "name": "spy_tone5", "unicode": "1F575-1F3FF", "digest": "69c1baac73783edb9e2d0c951f922dc7dddac34d0a9c818fee8d1021bc17db0d" }, + { + "name": "sleuth_or_spy_tone5", + "unicode": "1F575-1F3FF", + "digest": "69c1baac73783edb9e2d0c951f922dc7dddac34d0a9c818fee8d1021bc17db0d" + }, { "name": "stadium", "unicode": "1F3DF", @@ -7419,6 +9684,11 @@ "unicode": "1F4FE", "digest": "1ce1f9a83867514b8351ad4fd80c46bba04ad67dfb9874e63d7296e1a21161a5" }, + { + "name": "portable_stereo", + "unicode": "1F4FE", + "digest": "1ce1f9a83867514b8351ad4fd80c46bba04ad67dfb9874e63d7296e1a21161a5" + }, { "name": "stew", "unicode": "1F372", @@ -7644,6 +9914,11 @@ "unicode": "1F57F", "digest": "c3a42a653a91d90c6b668f678419d5438f2e546050914b841623e57107e805db" }, + { + "name": "black_touchtone_telephone", + "unicode": "1F57F", + "digest": "c3a42a653a91d90c6b668f678419d5438f2e546050914b841623e57107e805db" + }, { "name": "telephone_receiver", "unicode": "1F4DE", @@ -7654,6 +9929,11 @@ "unicode": "1F57E", "digest": "62a7e0e50c53e9f85eba51a92882e6064be05997910d3f7700e1e957dbaf0581" }, + { + "name": "white_touchtone_telephone", + "unicode": "1F57E", + "digest": "62a7e0e50c53e9f85eba51a92882e6064be05997910d3f7700e1e957dbaf0581" + }, { "name": "telescope", "unicode": "1F52D", @@ -7684,11 +9964,21 @@ "unicode": "1F912", "digest": "f19c489d89dd2d39770a6c8725a20f3e98f9e5216774af60c0665fd6a03a7687" }, + { + "name": "face_with_thermometer", + "unicode": "1F912", + "digest": "f19c489d89dd2d39770a6c8725a20f3e98f9e5216774af60c0665fd6a03a7687" + }, { "name": "thinking", "unicode": "1F914", "digest": "f64a9a18dca4c502b46f933838753a818b604a9d0268aa32eda26cbd31abc58c" }, + { + "name": "thinking_face", + "unicode": "1F914", + "digest": "f64a9a18dca4c502b46f933838753a818b604a9d0268aa32eda26cbd31abc58c" + }, { "name": "thought_balloon", "unicode": "1F4AD", @@ -7699,11 +9989,21 @@ "unicode": "1F5EC", "digest": "4fd591bf4318df73d1b17f434a449d8e95f49cca53a3d8f4d1ca983f3809ef46" }, + { + "name": "left_thought_bubble", + "unicode": "1F5EC", + "digest": "4fd591bf4318df73d1b17f434a449d8e95f49cca53a3d8f4d1ca983f3809ef46" + }, { "name": "thought_right", "unicode": "1F5ED", "digest": "0e8c0ce26e2d0e30894f5394b0736456e8268f775e0e7eda4c7dc3c2ff9231ae" }, + { + "name": "right_thought_bubble", + "unicode": "1F5ED", + "digest": "0e8c0ce26e2d0e30894f5394b0736456e8268f775e0e7eda4c7dc3c2ff9231ae" + }, { "name": "three", "unicode": "0033-20E3", @@ -7714,76 +10014,151 @@ "unicode": "1F593", "digest": "a8b561e389bc4e4b07fba70994f6445e5ddc6afe68922fcb6e9e7282d19ad958" }, + { + "name": "reversed_thumbs_down_sign", + "unicode": "1F593", + "digest": "a8b561e389bc4e4b07fba70994f6445e5ddc6afe68922fcb6e9e7282d19ad958" + }, { "name": "thumbs_up_reverse", "unicode": "1F592", "digest": "b6e52715c5ce590bfd08f6e05058ec3765ea2da341b11f9825d100608b173837" }, + { + "name": "reversed_thumbs_up_sign", + "unicode": "1F592", + "digest": "b6e52715c5ce590bfd08f6e05058ec3765ea2da341b11f9825d100608b173837" + }, { "name": "thumbsdown", "unicode": "1F44E", "digest": "a98f742c9773e0d95c0de5e1c10d1ab373fa761378a205f27d095e85debe69a3" }, + { + "name": "-1", + "unicode": "1F44E", + "digest": "a98f742c9773e0d95c0de5e1c10d1ab373fa761378a205f27d095e85debe69a3" + }, { "name": "thumbsdown_tone1", "unicode": "1F44E-1F3FB", "digest": "5d0a7c63d52eafe6267c552168c5557a66622009d565c3cf7b5378c1f6e84bce" }, + { + "name": "-1_tone1", + "unicode": "1F44E-1F3FB", + "digest": "5d0a7c63d52eafe6267c552168c5557a66622009d565c3cf7b5378c1f6e84bce" + }, { "name": "thumbsdown_tone2", "unicode": "1F44E-1F3FC", "digest": "ca5c15dc516660b2989a1c717bf3745fdfb6964c7acf3b938285ff6c7caf2ca2" }, + { + "name": "-1_tone2", + "unicode": "1F44E-1F3FC", + "digest": "ca5c15dc516660b2989a1c717bf3745fdfb6964c7acf3b938285ff6c7caf2ca2" + }, { "name": "thumbsdown_tone3", "unicode": "1F44E-1F3FD", "digest": "05740e3568795270674dac9134198bf75b1b778c11daa71649c88c231859ec16" }, + { + "name": "-1_tone3", + "unicode": "1F44E-1F3FD", + "digest": "05740e3568795270674dac9134198bf75b1b778c11daa71649c88c231859ec16" + }, { "name": "thumbsdown_tone4", "unicode": "1F44E-1F3FE", "digest": "5ee93bcc2f515806462a7b303064beade2b22a3f43a8162e39fd65d15d772e27" }, + { + "name": "-1_tone4", + "unicode": "1F44E-1F3FE", + "digest": "5ee93bcc2f515806462a7b303064beade2b22a3f43a8162e39fd65d15d772e27" + }, { "name": "thumbsdown_tone5", "unicode": "1F44E-1F3FF", "digest": "5c9ef8d53cf6f755668ab6dabfbfcdfd4b95fd59db3b3dd60290efefe9c33994" }, + { + "name": "-1_tone5", + "unicode": "1F44E-1F3FF", + "digest": "5c9ef8d53cf6f755668ab6dabfbfcdfd4b95fd59db3b3dd60290efefe9c33994" + }, { "name": "thumbsup", "unicode": "1F44D", "digest": "28b31df963773ba42a1a089f43cd89d0ce1ab0981e5410f41242e9a125fc1aee" }, + { + "name": "+1", + "unicode": "1F44D", + "digest": "28b31df963773ba42a1a089f43cd89d0ce1ab0981e5410f41242e9a125fc1aee" + }, { "name": "thumbsup_tone1", "unicode": "1F44D-1F3FB", "digest": "f6365942738d2128b6959d6672b3d295757dc8240703cb84a2b014ad78d67de3" }, + { + "name": "+1_tone1", + "unicode": "1F44D-1F3FB", + "digest": "f6365942738d2128b6959d6672b3d295757dc8240703cb84a2b014ad78d67de3" + }, { "name": "thumbsup_tone2", "unicode": "1F44D-1F3FC", "digest": "771d30146e4dc947a69057b05d32c765c8457ab02b5342889c5489acf27ef356" }, + { + "name": "+1_tone2", + "unicode": "1F44D-1F3FC", + "digest": "771d30146e4dc947a69057b05d32c765c8457ab02b5342889c5489acf27ef356" + }, { "name": "thumbsup_tone3", "unicode": "1F44D-1F3FD", "digest": "0bb7bbfb654c6139260e1786e7ffa5a33f31e19410c1d4d15737fdf5dd4c721d" }, + { + "name": "+1_tone3", + "unicode": "1F44D-1F3FD", + "digest": "0bb7bbfb654c6139260e1786e7ffa5a33f31e19410c1d4d15737fdf5dd4c721d" + }, { "name": "thumbsup_tone4", "unicode": "1F44D-1F3FE", "digest": "df0927c5342f0075fbf4ea83b724e6f70c0466c54769c9ce4a5c2deb602b28aa" }, + { + "name": "+1_tone4", + "unicode": "1F44D-1F3FE", + "digest": "df0927c5342f0075fbf4ea83b724e6f70c0466c54769c9ce4a5c2deb602b28aa" + }, { "name": "thumbsup_tone5", "unicode": "1F44D-1F3FF", "digest": "0683ae08c50aaf186c6406680a60617679c7b4bccd0817f24b15911dbb06866f" }, + { + "name": "+1_tone5", + "unicode": "1F44D-1F3FF", + "digest": "0683ae08c50aaf186c6406680a60617679c7b4bccd0817f24b15911dbb06866f" + }, { "name": "thunder_cloud_rain", "unicode": "26C8", "digest": "dd836f06b41a10d6ed9bcbdae291d2886847ff66dc3ede2427382e469f60674c" }, + { + "name": "thunder_cloud_and_rain", + "unicode": "26C8", + "digest": "dd836f06b41a10d6ed9bcbdae291d2886847ff66dc3ede2427382e469f60674c" + }, { "name": "ticket", "unicode": "1F3AB", @@ -7794,6 +10169,11 @@ "unicode": "1F39F", "digest": "ccafcc9583a84e847ff1eaa3d53187c5ab150a7d27c6a19363e59b9bc046b567" }, + { + "name": "admission_tickets", + "unicode": "1F39F", + "digest": "ccafcc9583a84e847ff1eaa3d53187c5ab150a7d27c6a19363e59b9bc046b567" + }, { "name": "tiger", "unicode": "1F42F", @@ -7809,6 +10189,11 @@ "unicode": "23F2", "digest": "c48199312ed42ff53a33bb2791db19e2e2521223cd49d8f758ea95b9b379c5ff" }, + { + "name": "timer_clock", + "unicode": "23F2", + "digest": "c48199312ed42ff53a33bb2791db19e2e2521223cd49d8f758ea95b9b379c5ff" + }, { "name": "tired_face", "unicode": "1F62B", @@ -7869,6 +10254,11 @@ "unicode": "1F6E0", "digest": "9b0a36dfdb475621d326359662b22cbdb80563c4f476aa5e7d7c00cdba605bd9" }, + { + "name": "hammer_and_wrench", + "unicode": "1F6E0", + "digest": "9b0a36dfdb475621d326359662b22cbdb80563c4f476aa5e7d7c00cdba605bd9" + }, { "name": "top", "unicode": "1F51D", @@ -7884,11 +10274,21 @@ "unicode": "23ED", "digest": "d5415ed140933f345fea8023a3d8fca30dcfcf7d19d9dc9771fa2cae9df62a3b" }, + { + "name": "next_track", + "unicode": "23ED", + "digest": "d5415ed140933f345fea8023a3d8fca30dcfcf7d19d9dc9771fa2cae9df62a3b" + }, { "name": "track_previous", "unicode": "23EE", "digest": "97ff4a59a236e5cf506fa3577b20715b3b0197e0f343a50615b36185d5b835f1" }, + { + "name": "previous_track", + "unicode": "23EE", + "digest": "97ff4a59a236e5cf506fa3577b20715b3b0197e0f343a50615b36185d5b835f1" + }, { "name": "trackball", "unicode": "1F5B2", @@ -7919,6 +10319,11 @@ "unicode": "1F6F2", "digest": "621bb967cd93fa9f8fd4b155965cc7572d3f91f88d94938ba10c8626718b623c" }, + { + "name": "diesel_locomotive", + "unicode": "1F6F2", + "digest": "621bb967cd93fa9f8fd4b155965cc7572d3f91f88d94938ba10c8626718b623c" + }, { "name": "tram", "unicode": "1F68A", @@ -7929,6 +10334,11 @@ "unicode": "1F6C6", "digest": "e24bb39ecfaaa746b03dc8418697d09ef327d5b077db39014f39d5fb87e23bd5" }, + { + "name": "triangle_with_rounded_corners", + "unicode": "1F6C6", + "digest": "e24bb39ecfaaa746b03dc8418697d09ef327d5b077db39014f39d5fb87e23bd5" + }, { "name": "triangular_flag_on_post", "unicode": "1F6A9", @@ -7994,6 +10404,11 @@ "unicode": "1F58F", "digest": "8a6c5b7d4c737866e7e32c6d9f7f447a48a0ac57a8909d43f87367d4a9b59246" }, + { + "name": "turned_ok_hand_sign", + "unicode": "1F58F", + "digest": "8a6c5b7d4c737866e7e32c6d9f7f447a48a0ac57a8909d43f87367d4a9b59246" + }, { "name": "turtle", "unicode": "1F422", @@ -8109,6 +10524,11 @@ "unicode": "1F984", "digest": "1b1e9c209dabe619db76fd346c3fb51b28ace0e4102697fe0973fe2d46aa9f08" }, + { + "name": "unicorn_face", + "unicode": "1F984", + "digest": "1b1e9c209dabe619db76fd346c3fb51b28ace0e4102697fe0973fe2d46aa9f08" + }, { "name": "unlock", "unicode": "1F513", @@ -8124,11 +10544,21 @@ "unicode": "1F643", "digest": "763fe2baf07a9b04f96958adf38a43c7dd2bc70d57398f49604307bd835cbb53" }, + { + "name": "upside_down_face", + "unicode": "1F643", + "digest": "763fe2baf07a9b04f96958adf38a43c7dd2bc70d57398f49604307bd835cbb53" + }, { "name": "urn", "unicode": "26B1", "digest": "dbfd5b90709d1b812d2fff71a5cfa10f84a4579866c2d7cd0e80759a22b2ba0e" }, + { + "name": "funeral_urn", + "unicode": "26B1", + "digest": "dbfd5b90709d1b812d2fff71a5cfa10f84a4579866c2d7cd0e80759a22b2ba0e" + }, { "name": "v", "unicode": "270C", @@ -8214,31 +10644,61 @@ "unicode": "1F596", "digest": "ca800fce797e652c5f47bf44992e8fbe19554688a36423fdf7c29ca6defae1e0" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers", + "unicode": "1F596", + "digest": "ca800fce797e652c5f47bf44992e8fbe19554688a36423fdf7c29ca6defae1e0" + }, { "name": "vulcan_tone1", "unicode": "1F596-1F3FB", "digest": "84bafdaca43426b053f5caa4e868ca109d99113a28ea9799db09d3c5d5f645c8" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone1", + "unicode": "1F596-1F3FB", + "digest": "84bafdaca43426b053f5caa4e868ca109d99113a28ea9799db09d3c5d5f645c8" + }, { "name": "vulcan_tone2", "unicode": "1F596-1F3FC", "digest": "e7cedf63ead957ee5c287e4cb0828ba70673e17b604f92b529875c32d094e7e3" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone2", + "unicode": "1F596-1F3FC", + "digest": "e7cedf63ead957ee5c287e4cb0828ba70673e17b604f92b529875c32d094e7e3" + }, { "name": "vulcan_tone3", "unicode": "1F596-1F3FD", "digest": "e124fef20f289921553274cf834f6dcc1a012889d30d9874dc5ad01afb8235b8" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone3", + "unicode": "1F596-1F3FD", + "digest": "e124fef20f289921553274cf834f6dcc1a012889d30d9874dc5ad01afb8235b8" + }, { "name": "vulcan_tone4", "unicode": "1F596-1F3FE", "digest": "ea2115f549e4680467521bbf362b229f4a8f0fdadbfaf231378d801f9b369f08" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone4", + "unicode": "1F596-1F3FE", + "digest": "ea2115f549e4680467521bbf362b229f4a8f0fdadbfaf231378d801f9b369f08" + }, { "name": "vulcan_tone5", "unicode": "1F596-1F3FF", "digest": "1b322e1252491f35ae02f0b279b6529dad867f2a6b3c2c3e77f981bed07e447d" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone5", + "unicode": "1F596-1F3FF", + "digest": "1b322e1252491f35ae02f0b279b6529dad867f2a6b3c2c3e77f981bed07e447d" + }, { "name": "walking", "unicode": "1F6B6", @@ -8429,16 +10889,31 @@ "unicode": "1F325", "digest": "d8ce416e6bdb0e59e06e2fceac3177dbe59fefc248fd8c6d76b80d1418141070" }, + { + "name": "white_sun_behind_cloud", + "unicode": "1F325", + "digest": "d8ce416e6bdb0e59e06e2fceac3177dbe59fefc248fd8c6d76b80d1418141070" + }, { "name": "white_sun_rain_cloud", "unicode": "1F326", "digest": "d2b132518261864ac4a95707eaeea335dd8351ed2b8ef4e2272ced456e309bf1" }, + { + "name": "white_sun_behind_cloud_with_rain", + "unicode": "1F326", + "digest": "d2b132518261864ac4a95707eaeea335dd8351ed2b8ef4e2272ced456e309bf1" + }, { "name": "white_sun_small_cloud", "unicode": "1F324", "digest": "b86a72f1cdb4d24fd3ab180aae9db012ca51fc01f3786aab596c2e330066b185" }, + { + "name": "white_sun_with_small_cloud", + "unicode": "1F324", + "digest": "b86a72f1cdb4d24fd3ab180aae9db012ca51fc01f3786aab596c2e330066b185" + }, { "name": "wind_blowing_face", "unicode": "1F32C", @@ -8524,6 +10999,11 @@ "unicode": "1F58E", "digest": "c4fc18ece6778339ebe14438aaf570e22385c3010c2d341824fa72ac6068cfeb" }, + { + "name": "left_writing_hand", + "unicode": "1F58E", + "digest": "c4fc18ece6778339ebe14438aaf570e22385c3010c2d341824fa72ac6068cfeb" + }, { "name": "writing_hand_tone1", "unicode": "270D-1F3FB", @@ -8589,6 +11069,11 @@ "unicode": "1F910", "digest": "8396249161b6d865861b56aabd17cae2c821b0d814f4249bf8cab0bb21fa8ee9" }, + { + "name": "zipper_mouth_face", + "unicode": "1F910", + "digest": "8396249161b6d865861b56aabd17cae2c821b0d814f4249bf8cab0bb21fa8ee9" + }, { "name": "zzz", "unicode": "1F4A4", diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake index 7ec00a898fd..030ee8bafcb 100644 --- a/lib/tasks/gemojione.rake +++ b/lib/tasks/gemojione.rake @@ -5,12 +5,23 @@ namespace :gemojione do require 'json' dir = Gemojione.index.images_path + digests = [] + aliases = Hash.new { |hash, key| hash[key] = [] } + aliases_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json') - digests = AwardEmoji.emojis.map do |name, emoji_hash| + JSON.parse(File.read(aliases_path)).each do |alias_name, real_name| + aliases[real_name] << alias_name + end + + AwardEmoji.emojis.map do |name, emoji_hash| fpath = File.join(dir, "#{emoji_hash['unicode']}.png") digest = Digest::SHA256.file(fpath).hexdigest - { name: name, unicode: emoji_hash['unicode'], digest: digest } + digests << { name: name, unicode: emoji_hash['unicode'], digest: digest } + + aliases[name].each do |alias_name| + digests << { name: alias_name, unicode: emoji_hash['unicode'], digest: digest } + end end out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') From 5ffa8f057095fb2fe12a60ffa0dd3a611d2f1aeb Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 9 Apr 2016 18:40:15 -0400 Subject: [PATCH 046/187] Escape the query argument provided to `git grep` by `search_files` Closes #14963. --- app/models/repository.rb | 2 +- spec/models/repository_spec.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 8dead3a5884..090cccd2c72 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -795,7 +795,7 @@ class Repository def search_files(query, ref) offset = 2 - args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref}) + args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{Regexp.escape(query)} #{ref || root_ref}) Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 4e49c413f23..bce30aafc4c 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -94,6 +94,12 @@ describe Repository, models: true do it { is_expected.to be_an Array } + it 'regex-escapes the query string' do + results = repository.search_files("test\\", 'master') + + expect(results.first).not_to start_with('fatal:') + end + describe 'result' do subject { results.first } From 667d44c25ccefb511fc0d206eaa5990117032236 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 11 Apr 2016 12:46:19 +0200 Subject: [PATCH 047/187] Fix high CPU usage when PostReceive receives refs/merge-requests/ --- CHANGELOG | 1 + app/workers/post_receive.rb | 2 +- spec/workers/post_receive_spec.rb | 43 ++++++++++++++++++++++++++++--- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3561c541df0..6a196dd9dce 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,7 @@ v 8.7.0 (unreleased) - API: Ability to filter milestones by state `active` and `closed` (Robert Schilling) - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) - Better errors handling when creating milestones inside groups + - Fix high CPU usage when PostReceive receives refs/merge-requests/ - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) - Gracefully handle notes on deleted commits in merge requests (Stan Hu) - Fix creation of merge requests for orphaned branches (Stan Hu) diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 3cc232ef1ae..9e1215b21a6 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -40,7 +40,7 @@ class PostReceive if Gitlab::Git.tag_ref?(ref) GitTagPushService.new.execute(post_received.project, @user, oldrev, newrev, ref) - else + elsif Gitlab::Git.branch_ref?(ref) GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute end end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 0265dbe9c66..94ff3457902 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -4,6 +4,9 @@ describe PostReceive do let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" } let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") } let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) } + let(:project) { create(:project) } + let(:key) { create(:key, user: project.owner) } + let(:key_id) { key.shell_id } context "as a resque worker" do it "reponds to #perform" do @@ -11,11 +14,43 @@ describe PostReceive do end end - context "webhook" do - let(:project) { create(:project) } - let(:key) { create(:key, user: project.owner) } - let(:key_id) { key.shell_id } + describe "#process_project_changes" do + before do + allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) + end + context "branches" do + let(:changes) { "123456 789012 refs/heads/tést" } + + it "should call GitTagPushService" do + expect_any_instance_of(GitPushService).to receive(:execute).and_return(true) + expect_any_instance_of(GitTagPushService).not_to receive(:execute) + PostReceive.new.perform(pwd(project), key_id, base64_changes) + end + end + + context "tags" do + let(:changes) { "123456 789012 refs/tags/tag" } + + it "should call GitTagPushService" do + expect_any_instance_of(GitPushService).not_to receive(:execute) + expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true) + PostReceive.new.perform(pwd(project), key_id, base64_changes) + end + end + + context "merge-requests" do + let(:changes) { "123456 789012 refs/merge-requests/123" } + + it "should not call any of the services" do + expect_any_instance_of(GitPushService).not_to receive(:execute) + expect_any_instance_of(GitTagPushService).not_to receive(:execute) + PostReceive.new.perform(pwd(project), key_id, base64_changes) + end + end + end + + context "webhook" do it "fetches the correct project" do expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project) PostReceive.new.perform(pwd(project), key_id, base64_changes) From 73fdd4b83d76998fef9770dbeaf05981d4500b8c Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 11 Apr 2016 10:23:40 -0300 Subject: [PATCH 048/187] Use Hash instead of Array on NotificationSetting#level enum --- app/models/notification_setting.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index d89194b5a12..5001738f411 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -1,7 +1,5 @@ class NotificationSetting < ActiveRecord::Base - # Notification level - # Note: When adding an option, it MUST go on the end of the array. - enum level: [:disabled, :participating, :watch, :global, :mention] + enum level: { disabled: 0, participating: 1, watch: 2, global: 3, mention: 4 } default_value_for :level, NotificationSetting.levels[:global] From de4d98fd120fd43bd744abc116c62708577b5673 Mon Sep 17 00:00:00 2001 From: Arinde Eniola Date: Mon, 11 Apr 2016 15:19:11 +0100 Subject: [PATCH 049/187] fix bug causing comment form in issue to submit twice when CTRL+Enter is pressed twice --- app/assets/javascripts/behaviors/quick_submit.js.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee index 6e29d374267..3cb96bacaa7 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js.coffee +++ b/app/assets/javascripts/behaviors/quick_submit.js.coffee @@ -29,7 +29,11 @@ $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) -> e.preventDefault() $form = $(e.target).closest('form') - $form.find('input[type=submit], button[type=submit]').disable() + $submit_button = $form.find('input[type=submit], button[type=submit]') + + return if $submit_button.attr('disabled') + + $submit_button.disable() $form.submit() # If the user tabs to a submit button on a `js-quick-submit` form, display a From 12e6084667f8750c263b4a2e324e9a283697b52e Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Mon, 11 Apr 2016 10:16:15 -0500 Subject: [PATCH 050/187] Allow `external_providers` for Omniauth to be defined to mark these users as external --- config/initializers/1_settings.rb | 1 + lib/gitlab/o_auth/user.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 72c4d8d61ce..94612997ead 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -129,6 +129,7 @@ Settings['omniauth'] ||= Settingslogic.new({}) Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil? Settings.omniauth['allow_single_sign_on'] = false if Settings.omniauth['allow_single_sign_on'].nil? +Settings.omniauth['external_providers'] = [] if Settings.omniauth['external_providers'].nil? Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block_auto_created_users'].nil? Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? Settings.omniauth['auto_link_saml_user'] = false if Settings.omniauth['auto_link_saml_user'].nil? diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index 832fb08a526..6e099c26d8c 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -54,6 +54,14 @@ module Gitlab @user ||= build_new_user end + unless @user.nil? + if external_provider? + @user.external = true + else + @user.external = false + end + end + @user end @@ -113,6 +121,10 @@ module Gitlab end end + def external_provider? + Gitlab.config.omniauth.external_providers.include?(auth_hash.provider) + end + def block_after_signup? if creating_linked_ldap_user? ldap_config.block_auto_created_users From ea04b0191d624fdc3e6f82840825bd265a4c3f59 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Mon, 11 Apr 2016 10:16:42 -0500 Subject: [PATCH 051/187] Added default setting for `external_providers` --- spec/lib/gitlab/o_auth/user_spec.rb | 37 ++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 3a769acfdc0..6727a83e58a 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -15,20 +15,20 @@ describe Gitlab::OAuth::User, lib: true do end let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') } - describe :persisted? do + describe '#persisted?' do let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } it "finds an existing user based on uid and provider (facebook)" do expect( oauth_user.persisted? ).to be_truthy end - it "returns false if use is not found in database" do + it 'returns false if user is not found in database' do allow(auth_hash).to receive(:uid).and_return('non-existing') expect( oauth_user.persisted? ).to be_falsey end end - describe :save do + describe '#save' do def stub_omniauth_config(messages) allow(Gitlab.config.omniauth).to receive_messages(messages) end @@ -40,8 +40,27 @@ describe Gitlab::OAuth::User, lib: true do let(:provider) { 'twitter' } describe 'signup' do - shared_examples "to verify compliance with allow_single_sign_on" do - context "with new allow_single_sign_on enabled syntax" do + shared_examples 'to verify compliance with allow_single_sign_on' do + context 'provider is marked as external' do + it 'should mark user as external' do + stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter']) + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_truthy + end + end + + context 'provider was external, now has been removed' do + it 'should mark existing user internal' do + create(:omniauth_user, extern_uid: 'my-uid', provider: 'twitter', external: true) + stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['facebook']) + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_falsey + end + end + + context 'with new allow_single_sign_on enabled syntax' do before { stub_omniauth_config(allow_single_sign_on: ['twitter']) } it "creates a user from Omniauth" do @@ -67,16 +86,16 @@ describe Gitlab::OAuth::User, lib: true do end end - context "with new allow_single_sign_on disabled syntax" do + context 'with new allow_single_sign_on disabled syntax' do before { stub_omniauth_config(allow_single_sign_on: []) } - it "throws an error" do + it 'throws an error' do expect{ oauth_user.save }.to raise_error StandardError end end - context "with old allow_single_sign_on disabled (Default)" do + context 'with old allow_single_sign_on disabled (Default)' do before { stub_omniauth_config(allow_single_sign_on: false) } - it "throws an error" do + it 'throws an error' do expect{ oauth_user.save }.to raise_error StandardError end end From cedfe9d22851d6765b9737c2489e8ff166a7d238 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Mon, 11 Apr 2016 10:16:56 -0500 Subject: [PATCH 052/187] Documentation of feature --- config/gitlab.yml.example | 7 +++++++ doc/integration/omniauth.md | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 35c7c425a5a..75aba2544b5 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -304,6 +304,13 @@ production: &base # (default: false) auto_link_saml_user: false + # Set different Omniauth providers as external so that all users creating accounts + # via these providers will not be able to have access to internal projects. You + # will need to use the full name of the provider, like `google_oauth2` for Google. + # Refer to the examples below for the full names of the supported providers. + # (default: []) + external_providers: [] + ## Auth providers # Uncomment the following lines and fill in the data of the auth provider you want to use # If your favorite auth provider is not listed you can use others: diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 25f35988305..cab329c0dec 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -120,6 +120,29 @@ OmniAuth provider for an existing user. The chosen OmniAuth provider is now active and can be used to sign in to GitLab from then on. +## Configure OmniAuth Providers as External + +>**Note:** +This setting was introduced with version 8.7 of GitLab + +You can define which OmniAuth providers you want to be `external` so that all users +creating accounts via these providers will not be able to have access to internal +projects. You will need to use the full name of the provider, like `google_oauth2` +for Google. Refer to the examples for the full names of the supported providers. + +**For Omnibus installations** + +```ruby + gitlab_rails['omniauth_external_providers'] = ['twitter', 'google_oauth2'] +``` + +**For installations from source** + +```yaml + omniauth: + external_providers: ['twitter', 'google_oauth2'] +``` + ## Using Custom Omniauth Providers >**Note:** From 979dedba8a68f33b8e2078f7e2980bf048a8a25a Mon Sep 17 00:00:00 2001 From: Arinde Eniola Date: Mon, 11 Apr 2016 16:24:49 +0100 Subject: [PATCH 053/187] make milestone labels in labels tab similar to that of the labels page --- app/assets/stylesheets/pages/labels.scss | 25 +++++++++++++------ .../shared/milestones/_labels_tab.html.haml | 13 +++++----- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 3e0a3140be7..da20fa28802 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -79,19 +79,30 @@ color: $white-light; } +@mixin labels-mobile { + @media (max-width: $screen-xs-min) { + display: block; + width: 100%; + margin-left: 0; + padding: 10px 0; + } +} + + .manage-labels-list { - .prepend-left-10 { + .prepend-left-10, .prepend-description-left { display: inline-block; width: 40%; vertical-align: middle; - @media (max-width: $screen-xs-min) { - display: block; - width: 100%; - margin-left: 0; - padding: 10px 0; - } + @include labels-mobile; + } + + .prepend-description-left { + width: 57%; + + @include labels-mobile; } .pull-info-right { diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml index 868b2357003..b15e8ea73fe 100644 --- a/app/views/shared/milestones/_labels_tab.html.haml +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -4,15 +4,16 @@ %li %span.label-row - = link_to milestones_label_path(options) do - - render_colored_label(label, tooltip: false) - %span.prepend-left-10 + %span.label-name + = link_to milestones_label_path(options) do + - render_colored_label(label, tooltip: false) + %span.prepend-description-left = markdown(label.description, pipeline: :single_line) - .pull-right - %strong.issues-count + .pull-info-right + %span.append-right-20 = link_to milestones_label_path(options.merge(state: 'opened')) do - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue' - %strong.issues-count + %span.append-right-20 = link_to milestones_label_path(options.merge(state: 'closed')) do - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue' From 31bcd9f8793d972bd59fc75c686ab03974a1d631 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Mon, 11 Apr 2016 10:25:53 -0500 Subject: [PATCH 054/187] Added CHANGELOG item --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 3561c541df0..08ee7ec89ab 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 8.7.0 (unreleased) - Fix avatar stretching by providing a cropping feature - API: Expose `subscribed` for issues and merge requests (Robert Schilling) - Allow SAML to handle external users based on user's information !3530 + - Allow Omniauth providers to be marked as `external` !3657 - Add endpoints to archive or unarchive a project !3372 - Add links to CI setup documentation from project settings and builds pages - Handle nil descriptions in Slack issue messages (Stan Hu) From 450a39ededbf93d0bfcec1d4774c3562b87fc190 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 11 Apr 2016 17:04:01 +0100 Subject: [PATCH 055/187] Fixed alignment on issuable new form Fixes #13802 --- app/assets/stylesheets/pages/issuable.scss | 6 ++++++ app/views/shared/issuable/_form.html.haml | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 88c1b614c74..999b9a2e79a 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -316,3 +316,9 @@ color: #8c8c8c; } } + +.issuable-form-padding-top { + @media (min-width: $screen-sm-min) { + padding-top: 7px; + } +} diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 757a3812deb..1c89a2ee7f3 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -70,13 +70,13 @@ - if can? current_user, :admin_milestone, issuable.project = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank .form-group + - has_labels = issuable.project.labels.any? = f.label :label_ids, "Labels", class: 'control-label' - .col-sm-10 - - if issuable.project.labels.any? + .col-sm-10{ class: ('issuable-form-padding-top' if !has_labels) } + - if has_labels = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } - else - .prepend-top-10 %span.light No labels yet.   - if can? current_user, :admin_label, issuable.project From 38a4f5cec6ab85525ab9db1d7d2669a77171f768 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 11 Apr 2016 16:45:03 -0500 Subject: [PATCH 056/187] Rename method and initialize .timeago inside --- app/assets/javascripts/application.js.coffee | 4 +--- .../javascripts/lib/datetime_utility.js.coffee | 4 +++- app/assets/javascripts/merge_request_tabs.js.coffee | 12 +++--------- app/assets/javascripts/notes.js.coffee | 9 +++------ 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 922a28b4ef5..b05138ac1ac 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -164,9 +164,7 @@ $ -> $('.trigger-submit').on 'change', -> $(@).parents('form').submit() - $timeago = $('abbr.timeago, .js-timeago') - gl.utils.updateFormatDate($timeago) - $timeago.timeago() + gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), false) # Flash if (flash = $(".flash-container")).length > 0 diff --git a/app/assets/javascripts/lib/datetime_utility.js.coffee b/app/assets/javascripts/lib/datetime_utility.js.coffee index ef9406fc331..ad1d1c70481 100644 --- a/app/assets/javascripts/lib/datetime_utility.js.coffee +++ b/app/assets/javascripts/lib/datetime_utility.js.coffee @@ -6,10 +6,12 @@ w.gl.utils.formatDate = (datetime) -> dateFormat(datetime, 'mmm d, yyyy h:MMtt Z') - w.gl.utils.updateFormatDate = ($timeagoEls) -> + w.gl.utils.localTimeAgo = ($timeagoEls, setTimeago = true) -> $timeagoEls.each( -> $el = $(@) $el.attr('title', gl.utils.formatDate($el.attr('datetime'))) ) + $timeagoEls.timeago() if setTimeago + ) window diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index fdf084a8a82..0ae6e244602 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -141,9 +141,7 @@ class @MergeRequestTabs url: "#{source}.json" success: (data) => document.querySelector("div#commits").innerHTML = data.html - $timeago = $('.js-timeago', 'div#commits') - gl.utils.updateFormatDate($timeago) - $timeago.timeago() + gl.utils.localTimeAgo($('.js-timeago', 'div#commits')) @commitsLoaded = true @scrollToElement("#commits") @@ -154,9 +152,7 @@ class @MergeRequestTabs url: "#{source}.json" + @_location.search success: (data) => document.querySelector("div#diffs").innerHTML = data.html - $timeago = $('.js-timeago', 'div#diffs') - gl.utils.updateFormatDate($timeago) - $timeago.timeago() + gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')) $('div#diffs .js-syntax-highlight').syntaxHighlight() @expandViewContainer() if @diffViewType() is 'parallel' @diffsLoaded = true @@ -169,9 +165,7 @@ class @MergeRequestTabs url: "#{source}.json" success: (data) => document.querySelector("div#builds").innerHTML = data.html - $timeago = $('.js-timeago', 'div#builds') - gl.utils.updateFormatDate($timeago) - $timeago.timeago() + gl.utils.localTimeAgo($('.js-timeago', 'div#builds')) @buildsLoaded = true @scrollToElement("#builds") diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 02e52040e3c..a67890200dd 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -170,8 +170,7 @@ class @Notes .syntaxHighlight() # Update datetime format on the recent note - $timeago = $notesList.find("#note_#{note.id} .js-timeago") - gl.utils.updateFormatDate($timeago) + gl.utils.localTimeAgo($notesList.find("#note_#{note.id} .js-timeago"), false) @initTaskList() @updateNotesCount(1) @@ -224,7 +223,7 @@ class @Notes # append new note to all matching discussions discussionContainer.append note_html - gl.utils.updateFormatDate($('.js-timeago', note_html)) + gl.utils.localTimeAgo($('.js-timeago', note_html), false) @updateNotesCount(1) @@ -355,9 +354,7 @@ class @Notes # Convert returned HTML to a jQuery object so we can modify it further $html = $(note.html) - $timeago = $('.js-timeago', $html) - gl.utils.updateFormatDate($timeago) - $timeago.timeago() + gl.utils.localTimeAgo($('.js-timeago', $html)) $html.syntaxHighlight() $html.find('.js-task-list-container').taskList('enable') From 476cf23fc37d6db8d3fb412ce0b646f228d9aac4 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 11 Apr 2016 16:21:32 -0300 Subject: [PATCH 057/187] Allow to close invalid merge request --- app/models/commit.rb | 8 +++-- app/models/merge_request.rb | 9 ++++-- .../merge_requests_controller_spec.rb | 29 +++++++++++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index d09876a07d9..11ecfcace14 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -150,13 +150,11 @@ class Commit end def hook_attrs(with_changed_files: false) - path_with_namespace = project.path_with_namespace - data = { id: id, message: safe_message, timestamp: committed_date.xmlschema, - url: "#{Gitlab.config.gitlab.url}/#{path_with_namespace}/commit/#{id}", + url: commit_url, author: { name: author_name, email: author_email @@ -170,6 +168,10 @@ class Commit data end + def commit_url + project.present? ? "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/#{id}" : "" + end + # Discover issues should be closed when this commit is pushed to a project's # default branch. def closes_issues(current_user = self.committer) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index bf185cb5dd8..8292445bcac 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -213,6 +213,8 @@ class MergeRequest < ActiveRecord::Base end def validate_branches + return if allow_broken + if target_project == source_project && target_branch == source_branch errors.add :branch_conflict, "You can not use same project/branch for source and target" end @@ -344,9 +346,12 @@ class MergeRequest < ActiveRecord::Base end def hook_attrs + source_hook_attrs = source_project.hook_attrs if source_project.present? + target_hook_attrs = target_project.hook_attrs if target_project.present? + attrs = { - source: source_project.hook_attrs, - target: target_project.hook_attrs, + source: source_hook_attrs, + target: target_hook_attrs, last_commit: nil, work_in_progress: work_in_progress? } diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 75e6b6f45a7..0f2cd34132a 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -157,6 +157,35 @@ describe Projects::MergeRequestsController do end end + describe 'PUT #update' do + context 'there is no source project' do + let(:project) { create(:project) } + let(:fork_project) { create(:forked_project_with_submodules) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } + + before do + fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) + fork_project.save + merge_request.reload + end + + it 'closes MR without errors' do + fork_project.destroy + + post :update, + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + merge_request: { + state_event: 'close' + } + + expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request]) + expect(merge_request.reload.closed?).to be_truthy + end + end + end + describe "DELETE #destroy" do it "denies access to users unless they're admin or project owner" do delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid From 72e2c1db19fe9a3f45e5df89c03e8077e064ec8b Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 11 Apr 2016 17:20:38 -0500 Subject: [PATCH 058/187] Update delete button --- app/views/shared/issuable/_form.html.haml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 757a3812deb..4b4078cb8a0 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -128,8 +128,6 @@ - else .pull-right - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project) - = link_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" }, - method: :delete, class: 'btn btn-grouped' do - = icon('trash-o') - Delete + = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" }, + method: :delete, class: 'btn btn-danger btn-grouped' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' From 158bba238f06f43f2988552f02b32feb2bc245e6 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 11 Apr 2016 17:42:51 -0500 Subject: [PATCH 059/187] Set tooltips for new added labels --- app/assets/javascripts/labels_select.js.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index d1fe116397a..cf0d4f9aae3 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -34,7 +34,7 @@ class @LabelsSelect labelHTMLTemplate = _.template( '<% _.each(labels, function(label){ %> issues?label_name=<%= label.title %>"> - + <%= label.title %> @@ -165,6 +165,8 @@ class @LabelsSelect .html(template) $sidebarCollapsedValue.text(labelCount) + $('.has-tooltip', $value).tooltip(container: 'body') + $value .find('a') .each((i) -> From 61fc9aa87ea3752f3c7b853ab1cb102e53d392f2 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Mon, 11 Apr 2016 17:26:01 -0500 Subject: [PATCH 060/187] Better control flow. --- lib/gitlab/o_auth/user.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index 6e099c26d8c..356e96fcbab 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -54,12 +54,10 @@ module Gitlab @user ||= build_new_user end - unless @user.nil? - if external_provider? - @user.external = true - else - @user.external = false - end + if external_provider? && @user + @user.external = true + elsif @user + @user.external = false end @user From d88d6e7619c3f976361df638e765472b499f1986 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 11 Apr 2016 18:04:42 -0500 Subject: [PATCH 061/187] Hide top search form on the search page --- app/views/layouts/header/_default.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 0f3b8119379..17502148dce 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -8,7 +8,7 @@ .navbar-collapse.collapse %ul.nav.navbar-nav %li.hidden-sm.hidden-xs - = render 'layouts/search' + = render 'layouts/search' unless current_controller?(:search) %li.visible-sm.visible-xs = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('search') From 93a10f17e0c84074580eaf1b101af2a0fffd19ed Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 11 Apr 2016 18:23:12 -0300 Subject: [PATCH 062/187] Reuse `User#notification_settings_for` when it's possible --- app/controllers/groups/notification_settings_controller.rb | 2 +- .../projects/notification_settings_controller.rb | 7 +++---- app/models/member.rb | 2 +- app/services/notification_service.rb | 4 ++-- spec/services/notification_service_spec.rb | 4 ++-- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/controllers/groups/notification_settings_controller.rb b/app/controllers/groups/notification_settings_controller.rb index 20405a05190..1b46f26a378 100644 --- a/app/controllers/groups/notification_settings_controller.rb +++ b/app/controllers/groups/notification_settings_controller.rb @@ -1,6 +1,6 @@ class Groups::NotificationSettingsController < Groups::ApplicationController def update - notification_setting = group.notification_settings.find_by(user_id: current_user) + notification_setting = current_user.notification_settings_for(group) saved = notification_setting.update_attributes(notification_setting_params) render json: { saved: saved } diff --git a/app/controllers/projects/notification_settings_controller.rb b/app/controllers/projects/notification_settings_controller.rb index da9034380af..90d294a4624 100644 --- a/app/controllers/projects/notification_settings_controller.rb +++ b/app/controllers/projects/notification_settings_controller.rb @@ -1,14 +1,13 @@ class Projects::NotificationSettingsController < Projects::ApplicationController def create - notification_setting = project.notification_settings.new(notification_setting_params) - notification_setting.user = current_user - saved = notification_setting.save + notification_setting = current_user.notification_settings_for(project) + saved = notification_setting.update_attributes(notification_setting_params) render json: { saved: saved } end def update - notification_setting = project.notification_settings.find_by(user_id: current_user) + notification_setting = current_user.notification_settings_for(project) saved = notification_setting.update_attributes(notification_setting_params) render json: { saved: saved } diff --git a/app/models/member.rb b/app/models/member.rb index 7d5af1d5c8a..60efafef211 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -167,7 +167,7 @@ class Member < ActiveRecord::Base end def notification_setting - @notification_setting ||= user.notification_settings.find_by(source: source) + @notification_setting ||= user.notification_settings_for(source) end private diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 0928dda349e..42ec1ac9e1a 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -355,10 +355,10 @@ class NotificationService users.reject do |user| next user.notification_level == level unless project - setting = user.notification_settings.find_by(source: project) + setting = user.notification_settings_for(project) if !setting && project.group - setting = user.notification_settings.find_by(source: project.group) + setting = user.notification_settings_for(project.group) end # reject users who globally set mention notification and has no setting per project/group diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index c4d52584a4b..d7c72dc0811 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -89,8 +89,8 @@ describe NotificationService, services: true do note.project.group.add_user(@u_watcher, GroupMember::MASTER) note.project.save - @u_watcher.notification_settings.find_by(source: note.project).participating! - @u_watcher.notification_settings.find_by(source: note.project.group).global! + @u_watcher.notification_settings_for(note.project).participating! + @u_watcher.notification_settings_for(note.project.group).global! ActionMailer::Base.deliveries.clear end From bee28e1785ad7844bd518c19106beee7d8a4c560 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 11 Apr 2016 18:57:18 -0300 Subject: [PATCH 063/187] Requires user to be signed in when changing notification settings --- .../notification_settings_controller.rb | 2 ++ .../notification_settings_controller.rb | 2 ++ .../notification_settings_controller_spec.rb | 17 ++++++++++ .../notification_settings_controller_spec.rb | 31 +++++++++++++++++++ 4 files changed, 52 insertions(+) create mode 100644 spec/controllers/groups/notification_settings_controller_spec.rb create mode 100644 spec/controllers/projects/notification_settings_controller_spec.rb diff --git a/app/controllers/groups/notification_settings_controller.rb b/app/controllers/groups/notification_settings_controller.rb index 1b46f26a378..de13b16ccf2 100644 --- a/app/controllers/groups/notification_settings_controller.rb +++ b/app/controllers/groups/notification_settings_controller.rb @@ -1,4 +1,6 @@ class Groups::NotificationSettingsController < Groups::ApplicationController + before_action :authenticate_user! + def update notification_setting = current_user.notification_settings_for(group) saved = notification_setting.update_attributes(notification_setting_params) diff --git a/app/controllers/projects/notification_settings_controller.rb b/app/controllers/projects/notification_settings_controller.rb index 90d294a4624..e536725c5b1 100644 --- a/app/controllers/projects/notification_settings_controller.rb +++ b/app/controllers/projects/notification_settings_controller.rb @@ -1,4 +1,6 @@ class Projects::NotificationSettingsController < Projects::ApplicationController + before_action :authenticate_user! + def create notification_setting = current_user.notification_settings_for(project) saved = notification_setting.update_attributes(notification_setting_params) diff --git a/spec/controllers/groups/notification_settings_controller_spec.rb b/spec/controllers/groups/notification_settings_controller_spec.rb new file mode 100644 index 00000000000..3572535d61c --- /dev/null +++ b/spec/controllers/groups/notification_settings_controller_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Groups::NotificationSettingsController do + let(:group) { create(:group) } + + describe '#update' do + context 'when not authorized' do + it 'redirects to sign in page' do + put :update, + group_id: group.to_param, + notification_setting: { level: NotificationSetting.levels[:participating] } + + expect(response).to redirect_to(new_user_session_path) + end + end + end +end diff --git a/spec/controllers/projects/notification_settings_controller_spec.rb b/spec/controllers/projects/notification_settings_controller_spec.rb new file mode 100644 index 00000000000..7e32a75b812 --- /dev/null +++ b/spec/controllers/projects/notification_settings_controller_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Projects::NotificationSettingsController do + let(:project) { create(:empty_project) } + + describe '#create' do + context 'when not authorized' do + it 'redirects to sign in page' do + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + notification_setting: { level: NotificationSetting.levels[:participating] } + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + describe '#update' do + context 'when not authorized' do + it 'redirects to sign in page' do + put :update, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + notification_setting: { level: NotificationSetting.levels[:participating] } + + expect(response).to redirect_to(new_user_session_path) + end + end + end +end From fe58c1f13cc0758bbbd8f85b8794b458b3a72b55 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 11 Apr 2016 19:29:31 -0300 Subject: [PATCH 064/187] Fix partial for update project notifications --- app/views/projects/buttons/_notifications.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index 2b9d8f2ac81..49f541399f2 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -1,5 +1,5 @@ - if @notification_setting - = form_for [@project.namespace.becomes(Namespace), @project, @notification_setting], remote: true, html: { class: 'inline', id: 'notification-form' } do |f| + = form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), remote: true, html: { class: 'inline', id: 'notification-form' } do |f| = f.hidden_field :level %span.dropdown %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"} From c162e0278cb845f6209e926d49474926b6a45956 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 11 Apr 2016 23:07:06 -0700 Subject: [PATCH 065/187] Check and report import job status to help diagnose issues with forking --- app/models/project.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 3e1f04b4158..6298dc8d1c8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -388,9 +388,15 @@ class Project < ActiveRecord::Base def add_import_job if forked? - RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) + job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) else - RepositoryImportWorker.perform_async(self.id) + job_id = RepositoryImportWorker.perform_async(self.id) + end + + if job_id + Rails.logger.info "Import job started for #{path_with_namespace} with job ID #{job_id}" + else + Rails.logger.error "Import job failed to start for #{path_with_namespace}" end end From a6ba94dbd109637b996246601f1bc2b62dc0a8d7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 12 Apr 2016 09:32:12 +0100 Subject: [PATCH 066/187] Filtering by any label keeps the text on the toggle button --- app/assets/javascripts/labels_select.js.coffee | 2 +- spec/features/issues/filter_issues_spec.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index d1fe116397a..90385621879 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -218,7 +218,7 @@ class @LabelsSelect selectable: true toggleLabel: (selected) -> - if selected and selected.title isnt 'Any Label' + if selected and selected.title? selected.title else defaultLabel diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 91de06e31f9..69b22232f10 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -84,16 +84,25 @@ describe 'Filter issues', feature: true do it 'should filter by any label' do find('.dropdown-menu-labels a', text: 'Any Label').click + page.within '.labels-filter' do + expect(page).to have_content 'Any Label' + end expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Label') end it 'should filter by no label' do find('.dropdown-menu-labels a', text: 'No Label').click + page.within '.labels-filter' do + expect(page).to have_content 'No Label' + end expect(find('.js-label-select .dropdown-toggle-text')).to have_content('No Label') end it 'should filter by no label' do find('.dropdown-menu-labels a', text: label.title).click + page.within '.labels-filter' do + expect(page).to have_content label.title + end expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) end end From 02cfff4623cc447c20f1990d3e5d9b452e5a7190 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 12 Apr 2016 13:34:03 +0100 Subject: [PATCH 067/187] Removed references to subscribe-button CSS class These were being blocked by adblocks Closes #15043 --- app/assets/javascripts/subscription.js.coffee | 2 +- app/assets/stylesheets/pages/issuable.scss | 6 ------ app/assets/stylesheets/pages/labels.scss | 2 +- app/views/projects/labels/_label.html.haml | 2 +- app/views/shared/issuable/_sidebar.html.haml | 4 ++-- features/steps/project/issues/issues.rb | 4 ++-- features/steps/project/labels.rb | 2 +- features/steps/project/merge_requests.rb | 4 ++-- 8 files changed, 10 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index e4b7a3172ec..3894806d61d 100644 --- a/app/assets/javascripts/subscription.js.coffee +++ b/app/assets/javascripts/subscription.js.coffee @@ -2,7 +2,7 @@ class @Subscription constructor: (container) -> $container = $(container) @url = $container.attr('data-url') - @subscribe_button = $container.find('.subscribe-button') + @subscribe_button = $container.find('.issuable-subscribe-button') @subscription_status = $container.find('.subscription-status') @subscribe_button.unbind('click').click(@toggleSubscription) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 88c1b614c74..f6bdcacda99 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -173,12 +173,6 @@ } } - .subscribe-button { - span { - margin-top: 0; - } - } - &.right-sidebar-collapsed { /* Extra small devices (phones, less than 768px) */ display: none; diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 3e0a3140be7..4f67981975a 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -106,7 +106,7 @@ padding: 6px; color: $gl-text-color; - &.subscribe-button { + &.label-subscribe-button { padding-left: 0; } } diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 097a65969a6..979726b8def 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -14,7 +14,7 @@ .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}} .subscription-status{data: {status: label_subscription_status(label)}} - %a.subscribe-button.btn.action-buttons{data: {toggle: "tooltip"}} + %a.label-subscribe-button.btn.action-buttons{data: {toggle: "tooltip"}} %span= label_subscription_toggle_button_text(label) - if can? current_user, :admin_label, @project diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 94affa4b59a..89ce356fedc 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -128,7 +128,7 @@ .title.hide-collapsed Notifications - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' - %button.btn.btn-block.btn-gray.subscribe-button.hide-collapsed{:type => 'button'} + %button.btn.btn-block.btn-gray.issuable-subscribe-button.hide-collapsed{:type => 'button'} %span= subscribed ? 'Unsubscribe' : 'Subscribe' .subscription-status.hide-collapsed{data: {status: subscribtion_status}} .unsubscribed{class: ( 'hidden' if subscribed )} @@ -152,4 +152,4 @@ new LabelsSelect(); new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}'); new Subscription('.subscription') - new Sidebar(); \ No newline at end of file + new Sidebar(); diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index aff5ca676be..fc12843ea5c 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -20,11 +20,11 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I should see that I am subscribed' do - expect(find('.subscribe-button span')).to have_content 'Unsubscribe' + expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe' end step 'I should see that I am unsubscribed' do - expect(find('.subscribe-button span')).to have_content 'Subscribe' + expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe' end step 'I click link "Closed"' do diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb index 17944527e3a..5bb02189021 100644 --- a/features/steps/project/labels.rb +++ b/features/steps/project/labels.rb @@ -29,6 +29,6 @@ class Spinach::Features::Labels < Spinach::FeatureSteps private def subscribe_button - first('.subscribe-button span') + first('.label-subscribe-button span') end end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index f0af0d097fa..4f883fe7c27 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -77,11 +77,11 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see that I am subscribed' do - expect(find('.subscribe-button span')).to have_content 'Unsubscribe' + expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe' end step 'I should see that I am unsubscribed' do - expect(find('.subscribe-button span')).to have_content 'Subscribe' + expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe' end step 'I click button "Unsubscribe"' do From d0cdc2ee73c8421906fbd011d0f44d638616a864 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Thu, 7 Apr 2016 10:12:49 +0200 Subject: [PATCH 068/187] API: Ability to update a group --- CHANGELOG | 1 + doc/api/groups.md | 81 ++++++++++++++++++++++++++++++++ lib/api/groups.rb | 30 +++++++++++- spec/requests/api/groups_spec.rb | 36 ++++++++++++++ 4 files changed, 146 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 593e8f77ab4..ac686c4bde6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.7.0 (unreleased) - Make HTTP(s) label consistent on clone bar (Stan Hu) - Expose label description in API (Mariusz Jachimowicz) - Allow back dating on issues when created through the API + - API: Ability to update a group (Robert Schilling) - Fix Error 500 after renaming a project path (Stan Hu) - Fix avatar stretching by providing a cropping feature - API: Expose `subscribed` for issues and merge requests (Robert Schilling) diff --git a/doc/api/groups.md b/doc/api/groups.md index d1b5c9f5f04..59046190d0f 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -126,6 +126,87 @@ Parameters: - `id` (required) - The ID or path of a group - `project_id` (required) - The ID of a project +## Update group + +Updates the project group. Only available to group owners and administrators. + +``` +PUT /groups/:id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the group | +| `name` | string | no | The name of the group | +| `path` | string | no | The path of the group | +| `description` | string | no | The description of the group | +| `visibility_level` | integer | no | The visibility_level of the group. 0 for private, 10 for internal, 20 for public. | + +```bash +curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental" + +``` + +Example response: + +```json +{ + "id": 5, + "name": "Experimental", + "path": "h5bp", + "description": "foo", + "visibility_level": 10, + "avatar_url": null, + "web_url": "http://gitlab.example.com/groups/h5bp", + "projects": [ + { + "id": 9, + "description": "foo", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 10, + "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git", + "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git", + "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate", + "name": "Html5 Boilerplate", + "name_with_namespace": "Experimental / Html5 Boilerplate", + "path": "html5-boilerplate", + "path_with_namespace": "h5bp/html5-boilerplate", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": true, + "created_at": "2016-04-05T21:40:50.169Z", + "last_activity_at": "2016-04-06T16:52:08.432Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 5, + "name": "Experimental", + "path": "h5bp", + "owner_id": null, + "created_at": "2016-04-05T21:40:49.152Z", + "updated_at": "2016-04-07T08:07:48.466Z", + "description": "foo", + "avatar": { + "url": null + }, + "share_with_group_lock": false, + "visibility_level": 10 + }, + "avatar_url": null, + "star_count": 1, + "forks_count": 0, + "open_issues_count": 3, + "public_builds": true + } + ] +} +``` + ## Remove group Removes group with all projects inside. diff --git a/lib/api/groups.rb b/lib/api/groups.rb index c165de21a75..964f691afcc 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -23,8 +23,10 @@ module API # Create group. Available only for users who can create groups. # # Parameters: - # name (required) - The name of the group - # path (required) - The path of the group + # name (required) - The name of the group + # path (required) - The path of the group + # description (optional) - The description of the group + # visibility_level (optional) - The visibility level of the group # Example Request: # POST /groups post do @@ -42,6 +44,30 @@ module API end end + # Update group. Available only for users who can administrate groups. + # + # Parameters: + # id (required) - The ID of a group + # path (optional) - The path of the group + # description (optional) - The description of the group + # visibility_level (optional) - The visibility level of the group + # Example Request: + # PUT /groups/:id + put ':id' do + group = find_group(params[:id]) + authorize! :admin_group, group + + attrs = attributes_for_keys [:name, :path, :description, :visibility_level] + + ::Groups::UpdateService.new(group, current_user, attrs).execute + + if group.errors.any? + render_validation_error!(group) + else + present group, with: Entities::GroupDetail + end + end + # Get a single group, with containing projects # # Parameters: diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 41c9cacd455..e7ccbff7ae2 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -97,6 +97,42 @@ describe API::API, api: true do end end + describe 'PUT /groups/:id' do + let(:new_group_name) { 'New Group'} + + context "when authenticated the group owner" do + it 'updates the group' do + put api("/groups/#{group1.id}", user1), name: new_group_name + + expect(response.status).to eq(200) + expect(json_response['name']).to eq(new_group_name) + end + + it 'returns 404 for a non existing group' do + put api('/groups/1328', user1) + + expect(response.status).to eq(404) + end + end + + context "when authenticated the admin" do + it 'updates the group' do + put api("/groups/#{group1.id}", admin), name: new_group_name + + expect(response.status).to eq(200) + expect(json_response['name']).to eq(new_group_name) + end + end + + context "when authenticated an user" do + it 'updates the group' do + put api("/groups/#{group1.id}", user2), name: new_group_name + + expect(response.status).to eq(403) + end + end + end + describe "GET /groups/:id/projects" do context "when authenticated as user" do it "should return the group's projects" do From 318314154e09d051cccd8b8391f2065133906bd7 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 12 Apr 2016 15:56:44 +0200 Subject: [PATCH 069/187] Increase fsck lock timeout to 24 hours --- app/workers/repository_check_worker.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/workers/repository_check_worker.rb b/app/workers/repository_check_worker.rb index 2d75c8bafde..afdc6a4c63a 100644 --- a/app/workers/repository_check_worker.rb +++ b/app/workers/repository_check_worker.rb @@ -37,9 +37,11 @@ class RepositoryCheckWorker end def try_obtain_lease(id) + # Use a 24-hour timeout because on servers/projects where 'git fsck' is + # super slow we definitely do not want to run it twice in parallel. lease = Gitlab::ExclusiveLease.new( "project_repository_check:#{id}", - timeout: RUN_TIME + timeout: 24.hours ) lease.try_obtain end From b37d3b9423991763ad03fca791a1daf473dafed1 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 12 Apr 2016 16:21:23 +0200 Subject: [PATCH 070/187] Add repository_checks_enabled setting --- app/models/application_setting.rb | 3 ++- .../20160412140240_add_repository_checks_enabled_setting.rb | 5 +++++ db/schema.rb | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20160412140240_add_repository_checks_enabled_setting.rb diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 052cd874733..36f88154232 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -153,7 +153,8 @@ class ApplicationSetting < ActiveRecord::Base require_two_factor_authentication: false, two_factor_grace_period: 48, recaptcha_enabled: false, - akismet_enabled: false + akismet_enabled: false, + repository_checks_enabled: true, ) end diff --git a/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb b/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb new file mode 100644 index 00000000000..ebfa4bcbc7b --- /dev/null +++ b/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb @@ -0,0 +1,5 @@ +class AddRepositoryChecksEnabledSetting < ActiveRecord::Migration + def change + add_column :application_settings, :repository_checks_enabled, :boolean, default: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 53509956888..a9c595fe36d 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: 20160331133914) do +ActiveRecord::Schema.define(version: 20160412140240) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -78,6 +78,7 @@ ActiveRecord::Schema.define(version: 20160331133914) do t.string "akismet_api_key" t.boolean "email_author_in_body", default: false t.integer "default_group_visibility" + t.boolean "repository_checks_enabled", default: true end create_table "audit_events", force: :cascade do |t| From beaee0a71fbb1b08676107b3e619e833dc8902c0 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 12 Apr 2016 15:54:26 +0100 Subject: [PATCH 071/187] Fixed issue with failing tests --- app/assets/javascripts/subscription.js.coffee | 2 +- app/views/projects/labels/_label.html.haml | 2 +- app/views/shared/issuable/_sidebar.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index 3894806d61d..1a430f3aa47 100644 --- a/app/assets/javascripts/subscription.js.coffee +++ b/app/assets/javascripts/subscription.js.coffee @@ -2,7 +2,7 @@ class @Subscription constructor: (container) -> $container = $(container) @url = $container.attr('data-url') - @subscribe_button = $container.find('.issuable-subscribe-button') + @subscribe_button = $container.find('.js-subscribe-button') @subscription_status = $container.find('.subscription-status') @subscribe_button.unbind('click').click(@toggleSubscription) diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 979726b8def..c7b4bb1f6e6 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -14,7 +14,7 @@ .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}} .subscription-status{data: {status: label_subscription_status(label)}} - %a.label-subscribe-button.btn.action-buttons{data: {toggle: "tooltip"}} + %button.js-subscribe-button.label-subscribe-button.btn.action-buttons{ type: 'button', data: {toggle: "tooltip"} } %span= label_subscription_toggle_button_text(label) - if can? current_user, :admin_label, @project diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 89ce356fedc..fe6e4128003 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -128,7 +128,7 @@ .title.hide-collapsed Notifications - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' - %button.btn.btn-block.btn-gray.issuable-subscribe-button.hide-collapsed{:type => 'button'} + %button.btn.btn-block.btn-gray.js-subscribe-button.issuable-subscribe-button.hide-collapsed{:type => 'button'} %span= subscribed ? 'Unsubscribe' : 'Subscribe' .subscription-status.hide-collapsed{data: {status: subscribtion_status}} .unsubscribed{class: ( 'hidden' if subscribed )} From 97f4ffff1e7b5da94e18edc20c009ffb46784187 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 12 Apr 2016 17:07:54 +0200 Subject: [PATCH 072/187] Add a 'circuit breaker' for repo checks --- .../admin/application_settings/_form.html.haml | 13 +++++++++++++ app/workers/repository_check_worker.rb | 8 ++++++++ lib/gitlab/current_settings.rb | 3 ++- spec/workers/repository_check_worker_spec.rb | 8 ++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index de86dacbb12..afd88465a78 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -275,5 +275,18 @@ .col-sm-10 = f.text_field :sentry_dsn, class: 'form-control' + %fieldset + %legend Repository Checks + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :repository_checks_enabled do + = f.check_box :repository_checks_enabled + Enable Repository Checks + .help-block + GitLab will periodically run + %a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck' + in all project and wiki repositories to look for silent disk corruption issues. + .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/workers/repository_check_worker.rb b/app/workers/repository_check_worker.rb index afdc6a4c63a..017f49de08c 100644 --- a/app/workers/repository_check_worker.rb +++ b/app/workers/repository_check_worker.rb @@ -15,6 +15,7 @@ class RepositoryCheckWorker # check, only one (or two) will be checked at a time. project_ids.each do |project_id| break if Time.now - start >= RUN_TIME + break unless current_settings.repository_checks_enabled next if !try_obtain_lease(project_id) @@ -45,4 +46,11 @@ class RepositoryCheckWorker ) lease.try_obtain end + + def current_settings + # No caching of the settings! If we cache them and an admin disables + # this feature, an active RepositoryCheckWorker would keep going for up + # to 1 hour after the feature was disabled. + ApplicationSetting.current || Gitlab::CurrentSettings.fake_application_settings + end end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 1acc22fe5bf..f44d1b3a44e 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -34,7 +34,8 @@ module Gitlab max_artifacts_size: Settings.artifacts['max_size'], require_two_factor_authentication: false, two_factor_grace_period: 48, - akismet_enabled: false + akismet_enabled: false, + repository_checks_enabled: true, ) end diff --git a/spec/workers/repository_check_worker_spec.rb b/spec/workers/repository_check_worker_spec.rb index d1849321f56..13493ad2c6a 100644 --- a/spec/workers/repository_check_worker_spec.rb +++ b/spec/workers/repository_check_worker_spec.rb @@ -28,4 +28,12 @@ describe RepositoryCheckWorker do expect(subject.perform).to eq([projects[1].id]) end + + it 'does nothing when repository checks are disabled' do + create(:empty_project) + current_settings = double('settings', repository_checks_enabled: false) + expect(subject).to receive(:current_settings) { current_settings } + + expect(subject.perform).to eq(nil) + end end From ef22b76b732c2bf4ce52b8a73570ac2921f9caa4 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 11 Apr 2016 19:33:26 -0300 Subject: [PATCH 073/187] Simplify Projects::NotificationSettingsController --- .../projects/notification_settings_controller.rb | 7 ------- app/views/projects/buttons/_notifications.html.haml | 2 +- config/routes.rb | 2 +- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/app/controllers/projects/notification_settings_controller.rb b/app/controllers/projects/notification_settings_controller.rb index e536725c5b1..7d81cc03c73 100644 --- a/app/controllers/projects/notification_settings_controller.rb +++ b/app/controllers/projects/notification_settings_controller.rb @@ -1,13 +1,6 @@ class Projects::NotificationSettingsController < Projects::ApplicationController before_action :authenticate_user! - def create - notification_setting = current_user.notification_settings_for(project) - saved = notification_setting.update_attributes(notification_setting_params) - - render json: { saved: saved } - end - def update notification_setting = current_user.notification_settings_for(project) saved = notification_setting.update_attributes(notification_setting_params) diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index 49f541399f2..c1e3e5b73a2 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -1,5 +1,5 @@ - if @notification_setting - = form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), remote: true, html: { class: 'inline', id: 'notification-form' } do |f| + = form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f| = f.hidden_field :level %span.dropdown %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"} diff --git a/config/routes.rb b/config/routes.rb index 552385110dd..48601b7567b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -608,7 +608,7 @@ Rails.application.routes.draw do resources :forks, only: [:index, :new, :create] resource :import, only: [:new, :create, :show] - resource :notification_setting, only: [:create, :update] + resource :notification_setting, only: [:update] resources :refs, only: [] do collection do From aabb466e5b35477b39cc57642083df361cd5d112 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 11 Apr 2016 19:54:13 -0300 Subject: [PATCH 074/187] Improve specs for group/project notification controller --- .../notification_settings_controller_spec.rb | 17 +++++++- .../notification_settings_controller_spec.rb | 39 ++++++++++++++++++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/spec/controllers/groups/notification_settings_controller_spec.rb b/spec/controllers/groups/notification_settings_controller_spec.rb index 3572535d61c..0786e45515a 100644 --- a/spec/controllers/groups/notification_settings_controller_spec.rb +++ b/spec/controllers/groups/notification_settings_controller_spec.rb @@ -2,16 +2,31 @@ require 'spec_helper' describe Groups::NotificationSettingsController do let(:group) { create(:group) } + let(:user) { create(:user) } describe '#update' do context 'when not authorized' do it 'redirects to sign in page' do put :update, group_id: group.to_param, - notification_setting: { level: NotificationSetting.levels[:participating] } + notification_setting: { level: :participating } expect(response).to redirect_to(new_user_session_path) end end + + context 'when authorized' do + before do + sign_in(user) + end + + it 'returns success' do + put :update, + group_id: group.to_param, + notification_setting: { level: :participating } + + expect(response.status).to eq 200 + end + end end end diff --git a/spec/controllers/projects/notification_settings_controller_spec.rb b/spec/controllers/projects/notification_settings_controller_spec.rb index 7e32a75b812..385877a26df 100644 --- a/spec/controllers/projects/notification_settings_controller_spec.rb +++ b/spec/controllers/projects/notification_settings_controller_spec.rb @@ -2,6 +2,11 @@ require 'spec_helper' describe Projects::NotificationSettingsController do let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + before do + project.team << [user, :developer] + end describe '#create' do context 'when not authorized' do @@ -9,11 +14,26 @@ describe Projects::NotificationSettingsController do post :create, namespace_id: project.namespace.to_param, project_id: project.to_param, - notification_setting: { level: NotificationSetting.levels[:participating] } + notification_setting: { level: :participating } expect(response).to redirect_to(new_user_session_path) end end + + context 'when authorized' do + before do + sign_in(user) + end + + it 'returns success' do + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + notification_setting: { level: :participating } + + expect(response.status).to eq 200 + end + end end describe '#update' do @@ -22,10 +42,25 @@ describe Projects::NotificationSettingsController do put :update, namespace_id: project.namespace.to_param, project_id: project.to_param, - notification_setting: { level: NotificationSetting.levels[:participating] } + notification_setting: { level: :participating } expect(response).to redirect_to(new_user_session_path) end end + + context 'when authorized' do + before do + sign_in(user) + end + + it 'returns success' do + put :update, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + notification_setting: { level: :participating } + + expect(response.status).to eq 200 + end + end end end From cba2c437e582dd5880ec45cc4ff2fccda2315ad5 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 11 Apr 2016 15:38:36 -0400 Subject: [PATCH 075/187] Move RepositoryArchiveCacheWorker to sidekiq-cron Closes #15105 --- app/controllers/projects/repositories_controller.rb | 1 - config/gitlab.yml.example | 3 +++ config/initializers/1_settings.rb | 3 +++ lib/api/repositories.rb | 1 - 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index 5c7614cfbaf..bb7a6b6a5ab 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -11,7 +11,6 @@ class Projects::RepositoriesController < Projects::ApplicationController end def archive - RepositoryArchiveCacheWorker.perform_async headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format])) head :ok rescue => ex diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 35c7c425a5a..1a512a2227f 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -156,6 +156,9 @@ production: &base stuck_ci_builds_worker: cron: "0 0 * * *" + # Remove outdated repository archives + repository_archive_cache_worker: + cron: "0 * * * *" # # 2. GitLab CI settings diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 72c4d8d61ce..ca74349e85d 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -239,6 +239,9 @@ Settings['cron_jobs'] ||= Settingslogic.new({}) Settings.cron_jobs['stuck_ci_builds_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['stuck_ci_builds_worker']['cron'] ||= '0 0 * * *' Settings.cron_jobs['stuck_ci_builds_worker']['job_class'] = 'StuckCiBuildsWorker' +Settings.cron_jobs['repository_archive_cache_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['repository_archive_cache_worker']['cron'] ||= '0 * * * *' +Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'RepositoryArchiveCacheWorker' # diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 0d0f0d4616d..62161aadb9a 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -98,7 +98,6 @@ module API authorize! :download_code, user_project begin - RepositoryArchiveCacheWorker.perform_async header *Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format]) rescue not_found!('File') From ea787165b3a9604aa86304e29778066bb014824e Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 12 Apr 2016 17:32:58 +0200 Subject: [PATCH 076/187] Move 'clear checks' button to applicatoin settings --- .../admin/application_settings_controller.rb | 14 ++++++++ app/controllers/admin/projects_controller.rb | 12 ------- .../application_settings/_form.html.haml | 6 ++++ app/views/admin/projects/index.html.haml | 5 --- config/routes.rb | 5 +-- doc/administration/repository_checks.md | 33 +++++++------------ 6 files changed, 32 insertions(+), 43 deletions(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index f010436bd36..993a70e63bc 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -19,6 +19,19 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController redirect_to admin_runners_path end + def clear_repository_check_states + Project.update_all( + last_repository_check_failed: false, + last_repository_check_at: nil + ) + + redirect_to( + admin_application_settings_path, + notice: 'All repository check states were cleared' + ) + end + + private def set_application_setting @@ -82,6 +95,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :akismet_enabled, :akismet_api_key, :email_author_in_body, + :repository_checks_enabled, restricted_visibility_levels: [], import_sources: [] ) diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 01257a68616..d7cd9520cc6 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -40,18 +40,6 @@ class Admin::ProjectsController < Admin::ApplicationController ) end - def clear_repository_check_states - Project.update_all( - last_repository_check_failed: false, - last_repository_check_at: nil - ) - - redirect_to( - admin_namespaces_projects_path, - notice: 'All project states were cleared' - ) - end - protected def project diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index afd88465a78..c7c82da72c7 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -287,6 +287,12 @@ GitLab will periodically run %a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck' in all project and wiki repositories to look for silent disk corruption issues. + .form-group + .col-sm-offset-2.col-sm-10 + = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" + .help-block + If you got a lot of false alarms from repository checks (maybe your fileserver was temporarily unavailable) you can choose to clear all repository check information from the database. + .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index c2bf0659841..aa07afa0d62 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -49,11 +49,6 @@ = button_tag "Search", class: "btn submit btn-primary" = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel" - .panel.panel-default.repository-check-states - .panel-heading - Repository check states - .panel-body - = link_to 'Clear all', clear_repository_check_states_admin_namespace_projects_path(0), data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" %section.col-md-9 .panel.panel-default .panel-heading diff --git a/config/routes.rb b/config/routes.rb index c0ed99b1964..c163602126d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -267,10 +267,6 @@ Rails.application.routes.draw do post :repository_check end - collection do - put :clear_repository_check_states - end - resources :runner_projects end end @@ -286,6 +282,7 @@ Rails.application.routes.draw do resource :application_settings, only: [:show, :update] do resources :services put :reset_runners_token + put :clear_repository_check_states end resources :labels diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md index 77f28209f2f..e22b04928cc 100644 --- a/doc/administration/repository_checks.md +++ b/doc/administration/repository_checks.md @@ -4,12 +4,13 @@ _**Note:** This feature was [introduced][ce-3232] in GitLab 8.7_ --- -Git has a built-in mechanism [git fsck][git-fsck] to verify the -integrity of all data commited to a repository. GitLab administrators can -trigger such a check for a project via the admin panel. The checks run -asynchronously so it may take a few minutes before the check result is -visible on the project admin page. If the checks failed you can see their -output on the admin log page under 'repocheck.log'. +Git has a built-in mechanism \[git fsck\]\[git-fsck\] to verify the +integrity of all data commited to a repository. GitLab administrators +can trigger such a check for a project via the project page under the +admin panel. The checks run asynchronously so it may take a few minutes +before the check result is visible on the project admin page. If the +checks failed you can see their output on the admin log page under +'repocheck.log'. ## Periodical checks @@ -22,20 +23,8 @@ than once a day. ## Disabling periodic checks -You can disable the periodic checks by giving them an empty cron -schedule in gitlab.yml. - -``` -# For omnibus installations, in /etc/gitlab/gitlab.rb: -gitlab_rails['cron_jobs_repository_check_worker_cron'] = '' -``` - -``` -# For installations from source, in config/gitlab.yml: - cron_jobs: - repository_check_worker: - cron: "" -``` +You can disable the periodic checks on the 'Settings' page of the admin +panel. ## What to do if a check failed @@ -47,8 +36,8 @@ resolved the issue use the admin panel to trigger a new repository check on the project. This will clear the 'check failed' state. If for some reason the periodical repository check caused a lot of false -alarms you can choose to clear ALL repository check states from the admin -project index page. +alarms you can choose to clear ALL repository check states from the +'Settings' page of the admin panel. --- [ce-3232]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3232 "Auto git fsck" From 61a62e00e3b08e6ed962b029564e3a2446e169fd Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 12 Apr 2016 12:57:39 -0300 Subject: [PATCH 077/187] Fix specs for Projects::NotificationSettingsController --- .../notification_settings_controller_spec.rb | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/spec/controllers/projects/notification_settings_controller_spec.rb b/spec/controllers/projects/notification_settings_controller_spec.rb index 385877a26df..4908b545648 100644 --- a/spec/controllers/projects/notification_settings_controller_spec.rb +++ b/spec/controllers/projects/notification_settings_controller_spec.rb @@ -8,34 +8,6 @@ describe Projects::NotificationSettingsController do project.team << [user, :developer] end - describe '#create' do - context 'when not authorized' do - it 'redirects to sign in page' do - post :create, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - notification_setting: { level: :participating } - - expect(response).to redirect_to(new_user_session_path) - end - end - - context 'when authorized' do - before do - sign_in(user) - end - - it 'returns success' do - post :create, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - notification_setting: { level: :participating } - - expect(response.status).to eq 200 - end - end - end - describe '#update' do context 'when not authorized' do it 'redirects to sign in page' do From 061370790e415361c920e1404063955f4932e5ef Mon Sep 17 00:00:00 2001 From: Charles May Date: Tue, 5 Jan 2016 21:26:31 +0000 Subject: [PATCH 078/187] Fix a bug with trailing slash in teamcity_url See https://gitlab.com/gitlab-org/gitlab-ce/issues/3515 --- CHANGELOG | 3 ++ .../project_services/teamcity_service.rb | 31 +++++++++++-------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3561c541df0..c6ca0ea3de1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -123,6 +123,9 @@ v 8.6.0 - Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner) - HTTP error pages work independently from location and config (Artem Sidorenko) - Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set + - Fix avatar stretching by providing a cropping feature (Johann Pardanaud) + - Fix a bug whith trailing slash in teamcity_url (Charles May) + - Don't load all of GitLab in mail_room - Memoize @group in Admin::GroupsController (Yatish Mehta) - Indicate how much an MR diverged from the target branch (Pierre de La Morinerie) - Added omniauth-auth0 Gem (Daniel Carraro) diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index b8e9416131a..246c5eb4a82 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -85,13 +85,15 @@ class TeamcityService < CiService end def build_info(sha) - url = URI.parse("#{teamcity_url}/httpAuth/app/rest/builds/"\ - "branch:unspecified:any,number:#{sha}") + url = URI.join( + teamcity_url, + "/httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}" + ).to_s auth = { username: username, password: password, } - @response = HTTParty.get("#{url}", verify: false, basic_auth: auth) + @response = HTTParty.get(url, verify: false, basic_auth: auth) end def build_page(sha, ref) @@ -100,12 +102,14 @@ class TeamcityService < CiService if @response.code != 200 # If actual build link can't be determined, # send user to build summary page. - "#{teamcity_url}/viewLog.html?buildTypeId=#{build_type}" + URI.join(teamcity_url, "/viewLog.html?buildTypeId=#{build_type}").to_s else # If actual build link is available, go to build result page. built_id = @response['build']['id'] - "#{teamcity_url}/viewLog.html?buildId=#{built_id}"\ - "&buildTypeId=#{build_type}" + URI.join( + teamcity_url, + "#{teamcity_url}/viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}" + ).to_s end end @@ -140,12 +144,13 @@ class TeamcityService < CiService branch = Gitlab::Git.ref_name(data[:ref]) - self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue", - body: ""\ - ""\ - '', - headers: { 'Content-type' => 'application/xml' }, - basic_auth: auth - ) + self.class.post( + URI.join(teamcity_url, "/httpAuth/app/rest/buildQueue").to_s, + body: ""\ + ""\ + '', + headers: { 'Content-type' => 'application/xml' }, + basic_auth: auth + ) end end From 3170e5d226ee107409b4345b827519da64ba967e Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 12 Apr 2016 18:09:45 +0200 Subject: [PATCH 079/187] Basta --- app/controllers/admin/application_settings_controller.rb | 2 +- app/controllers/admin/projects_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 993a70e63bc..a54864480a2 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -27,7 +27,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController redirect_to( admin_application_settings_path, - notice: 'All repository check states were cleared' + notice: 'All repository check states were cleared.' ) end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 0719c90b19b..6854e57b650 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -36,7 +36,7 @@ class Admin::ProjectsController < Admin::ApplicationController redirect_to( admin_namespace_project_path(@project.namespace, @project), - notice: 'Repository check was triggered' + notice: 'Repository check was triggered.' ) end From 525ab25ac81a6b81cca56d3cba403ab2a5f372eb Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 12 Apr 2016 18:15:15 +0200 Subject: [PATCH 080/187] Changes suggested by Robert --- app/mailers/repository_check_mailer.rb | 2 -- app/views/admin/application_settings/_form.html.haml | 2 +- app/workers/repository_check_worker.rb | 2 +- spec/workers/repository_check_worker_spec.rb | 6 +++--- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb index 994054c8769..2bff5b63cc4 100644 --- a/app/mailers/repository_check_mailer.rb +++ b/app/mailers/repository_check_mailer.rb @@ -1,6 +1,4 @@ class RepositoryCheckMailer < BaseMailer - include ActionView::Helpers::TextHelper - def notify(failed_count) if failed_count == 1 @message = "One project failed its last repository check" diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 533a2f42973..555aea554f0 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -287,7 +287,7 @@ .col-sm-offset-2.col-sm-10 = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" .help-block - If you got a lot of false alarms from repository checks (maybe your fileserver was temporarily unavailable) you can choose to clear all repository check information from the database. + If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database. .form-actions diff --git a/app/workers/repository_check_worker.rb b/app/workers/repository_check_worker.rb index 017f49de08c..bdacdd4c6b0 100644 --- a/app/workers/repository_check_worker.rb +++ b/app/workers/repository_check_worker.rb @@ -17,7 +17,7 @@ class RepositoryCheckWorker break if Time.now - start >= RUN_TIME break unless current_settings.repository_checks_enabled - next if !try_obtain_lease(project_id) + next unless try_obtain_lease(project_id) SingleRepositoryCheckWorker.new.perform(project_id) end diff --git a/spec/workers/repository_check_worker_spec.rb b/spec/workers/repository_check_worker_spec.rb index 13493ad2c6a..7a757658e97 100644 --- a/spec/workers/repository_check_worker_spec.rb +++ b/spec/workers/repository_check_worker_spec.rb @@ -4,7 +4,7 @@ describe RepositoryCheckWorker do subject { RepositoryCheckWorker.new } it 'prefers projects that have never been checked' do - projects = 3.times.map { create(:project) } + projects = create_list(:project, 3) projects[0].update_column(:last_repository_check_at, 1.month.ago) projects[2].update_column(:last_repository_check_at, 3.weeks.ago) @@ -12,7 +12,7 @@ describe RepositoryCheckWorker do end it 'sorts projects by last_repository_check_at' do - projects = 3.times.map { create(:project) } + projects = create_list(:project, 3) projects[0].update_column(:last_repository_check_at, 2.weeks.ago) projects[1].update_column(:last_repository_check_at, 1.month.ago) projects[2].update_column(:last_repository_check_at, 3.weeks.ago) @@ -21,7 +21,7 @@ describe RepositoryCheckWorker do end it 'excludes projects that were checked recently' do - projects = 3.times.map { create(:project) } + projects = create_list(:project, 3) projects[0].update_column(:last_repository_check_at, 2.days.ago) projects[1].update_column(:last_repository_check_at, 1.month.ago) projects[2].update_column(:last_repository_check_at, 3.days.ago) From 5fb572417e0c331afb62c8bbaa561b0fe7836fc5 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Tue, 12 Apr 2016 19:08:35 +0200 Subject: [PATCH 081/187] Fix minor issues according development guidelines --- doc/api/groups.md | 2 +- lib/api/groups.rb | 10 ++++------ spec/requests/api/groups_spec.rb | 16 ++++++++++++---- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/doc/api/groups.md b/doc/api/groups.md index 59046190d0f..2821bc21b81 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -140,7 +140,7 @@ PUT /groups/:id | `name` | string | no | The name of the group | | `path` | string | no | The path of the group | | `description` | string | no | The description of the group | -| `visibility_level` | integer | no | The visibility_level of the group. 0 for private, 10 for internal, 20 for public. | +| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. | ```bash curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental" diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 964f691afcc..91e420832f3 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -47,7 +47,7 @@ module API # Update group. Available only for users who can administrate groups. # # Parameters: - # id (required) - The ID of a group + # id (required) - The ID of a group # path (optional) - The path of the group # description (optional) - The description of the group # visibility_level (optional) - The visibility level of the group @@ -59,12 +59,10 @@ module API attrs = attributes_for_keys [:name, :path, :description, :visibility_level] - ::Groups::UpdateService.new(group, current_user, attrs).execute - - if group.errors.any? - render_validation_error!(group) - else + if ::Groups::UpdateService.new(group, current_user, attrs).execute present group, with: Entities::GroupDetail + else + render_validation_error!(group) end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index e7ccbff7ae2..7383c7d11aa 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -100,7 +100,7 @@ describe API::API, api: true do describe 'PUT /groups/:id' do let(:new_group_name) { 'New Group'} - context "when authenticated the group owner" do + context 'when authenticated as the group owner' do it 'updates the group' do put api("/groups/#{group1.id}", user1), name: new_group_name @@ -115,7 +115,7 @@ describe API::API, api: true do end end - context "when authenticated the admin" do + context 'when authenticated as the admin' do it 'updates the group' do put api("/groups/#{group1.id}", admin), name: new_group_name @@ -124,13 +124,21 @@ describe API::API, api: true do end end - context "when authenticated an user" do - it 'updates the group' do + context 'when authenticated as an user that can see the group' do + it 'does not updates the group' do put api("/groups/#{group1.id}", user2), name: new_group_name expect(response.status).to eq(403) end end + + context 'when authenticated as an user that cannot see the group' do + it 'returns 403 when trying to update the group' do + put api("/groups/#{group2.id}", user1), name: new_group_name + + expect(response.status).to eq(403) + end + end end describe "GET /groups/:id/projects" do From dca50ac1d4a6da5724b66643bcb18a4a2e3f5558 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 22 Mar 2016 11:33:38 +0000 Subject: [PATCH 082/187] Project dropdown in header uses new dropdown --- app/assets/javascripts/gl_dropdown.js.coffee | 4 ++- .../javascripts/project_select.js.coffee | 32 +++++++++++++++++++ app/assets/stylesheets/framework/header.scss | 5 +++ app/helpers/projects_helper.rb | 14 ++------ app/views/layouts/header/_default.html.haml | 2 ++ app/views/layouts/project.html.haml | 7 ++++ 6 files changed, 51 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index ee1d0fad289..2dc37257e22 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -122,7 +122,9 @@ class GitLabDropdown FILTER_INPUT = '.dropdown-input .dropdown-input-field' constructor: (@el, @options) -> - @dropdown = $(@el).parent() + self = @ + selector = $(@el).data "target" + @dropdown = if selector? then $(selector) else $(@el).parent() # Set Defaults { diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee index be8ab9b428d..704bd8dee53 100644 --- a/app/assets/javascripts/project_select.js.coffee +++ b/app/assets/javascripts/project_select.js.coffee @@ -1,5 +1,37 @@ class @ProjectSelect constructor: -> + $('.js-projects-dropdown-toggle').each (i, dropdown) -> + $dropdown = $(dropdown) + + $dropdown.glDropdown( + filterable: true + filterRemote: true + search: + fields: ['name_with_namespace'] + data: (term, callback) -> + finalCallback = (projects) -> + callback projects + + if @includeGroups + projectsCallback = (projects) -> + groupsCallback = (groups) -> + data = groups.concat(projects) + finalCallback(data) + + Api.groups term, false, groupsCallback + else + projectsCallback = finalCallback + + if @groupId + Api.groupProjects @groupId, term, projectsCallback + else + Api.projects term, @orderBy, projectsCallback + url: (project) -> + project.web_url + text: (project) -> + project.name_with_namespace + ) + $('.ajax-project-select').each (i, select) -> @groupId = $(select).data('group-id') @includeGroups = $(select).data('include-groups') diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index b3397d16016..3f015427d07 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -69,6 +69,7 @@ header { } .header-content { + position: relative; height: $header-height; padding-right: 20px; @@ -76,6 +77,10 @@ header { padding-right: 0; } + .dropdown-menu { + margin-top: -5px; + } + .title { margin: 0; font-size: 19px; diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 4e4c6e301d5..3621b943f3c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -65,18 +65,8 @@ module ProjectsHelper link_to(simple_sanitize(owner.name), user_path(owner)) end - project_link = link_to project_path(project), { class: "project-item-select-holder" } do - link_output = simple_sanitize(project.name) - - if current_user - link_output += project_select_tag :project_path, - class: "project-item-select js-projects-dropdown", - data: { include_groups: false, order_by: 'last_activity_at' } - end - - link_output - end - project_link += icon "chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle" if current_user + project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder"} + project_link += icon "chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", data: { target: ".header-content", toggle: "dropdown" } if current_user full_title = namespace_link + ' / ' + project_link full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 0f3b8119379..44339293095 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -45,6 +45,8 @@ %h1.title= title + = yield :header_content + = render 'shared/outdated_browser' - if @project && !@project.empty_repo? diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index a7ef31acd3d..35be616b174 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -17,4 +17,11 @@ - content_for :scripts_body do = render "layouts/init_auto_complete" if current_user +- content_for :header_content do + .dropdown-menu.dropdown-select + = dropdown_title("Go to a project") + = dropdown_filter("Search your projects") + = dropdown_content + = dropdown_loading + = render template: "layouts/application" From f870857dddd7630dc19a77ded7784a2261e046e3 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 24 Mar 2016 16:28:46 +0000 Subject: [PATCH 083/187] Updated tests --- app/helpers/projects_helper.rb | 2 +- app/views/layouts/project.html.haml | 2 +- spec/features/projects_spec.rb | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 3621b943f3c..83ebc124171 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -65,7 +65,7 @@ module ProjectsHelper link_to(simple_sanitize(owner.name), user_path(owner)) end - project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder"} + project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } project_link += icon "chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", data: { target: ".header-content", toggle: "dropdown" } if current_user full_title = namespace_link + ' / ' + project_link diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 35be616b174..2c5911fa2fb 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -18,7 +18,7 @@ = render "layouts/init_auto_complete" if current_user - content_for :header_content do - .dropdown-menu.dropdown-select + .dropdown-menu.dropdown-select.dropdown-menu-projects = dropdown_title("Go to a project") = dropdown_filter("Search your projects") = dropdown_content diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index ed97b6cb577..782c0bfe666 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -100,8 +100,7 @@ feature 'Project', feature: true do it 'click toggle and show dropdown', js: true do find('.js-projects-dropdown-toggle').click - wait_for_ajax - expect(page).to have_css('.select2-results li', count: 1) + expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 1) end end From 14b124faca9518167798069aaaedbcc67994dd2f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 29 Mar 2016 13:21:22 +0100 Subject: [PATCH 084/187] Tests update --- .../fixtures/project_title.html.haml | 26 ++++++++++++++----- spec/javascripts/project_title_spec.js.coffee | 20 ++++---------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/spec/javascripts/fixtures/project_title.html.haml b/spec/javascripts/fixtures/project_title.html.haml index e5850b62659..3696f241c21 100644 --- a/spec/javascripts/fixtures/project_title.html.haml +++ b/spec/javascripts/fixtures/project_title.html.haml @@ -1,7 +1,19 @@ -%h1.title - %a - GitLab Org - %a.project-item-select-holder{href: "/gitlab-org/gitlab-test"} - GitLab Test - %input#project_path.project-item-select.js-projects-dropdown.ajax-project-select{type: "hidden", name: "project_path", "data-include-groups" => "false"} - %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle +.header-content + %h1.title + %a + GitLab Org + %a.project-item-select-holder{href: "/gitlab-org/gitlab-test"} + GitLab Test + %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle{ "data-toggle" => "dropdown", "data-target" => ".header-content" } + .dropdown-menu.dropdown-select.dropdown-menu-projects + .dropdown-title + %span Go to a project + %button.dropdown-title-button.dropdown-menu-close{"aria-label" => "Close", type: "button"} + %i.fa.fa-times.dropdown-menu-close-icon + .dropdown-input + %input.dropdown-input-field{id: "", placeholder: "Search your projects", type: "search", value: ""} + %i.fa.fa-search.dropdown-input-search + %i.fa.fa-times.dropdown-input-clear.js-dropdown-input-clear{role: "button"} + .dropdown-content + .dropdown-loading + %i.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee index 47c7b7febe3..3d8de2ff989 100644 --- a/spec/javascripts/project_title_spec.js.coffee +++ b/spec/javascripts/project_title_spec.js.coffee @@ -1,4 +1,6 @@ +#= require bootstrap #= require select2 +#= require gl_dropdown #= require api #= require project_select #= require project @@ -14,9 +16,6 @@ describe 'Project Title', -> fixture.load('project_title.html') @project = new Project() - spyOn(@project, 'changeProject').and.callFake (url) -> - window.current_project_url = url - describe 'project list', -> beforeEach => @projects_data = fixture.load('projects.json')[0] @@ -29,18 +28,9 @@ describe 'Project Title', -> it 'to show on toggle click', => $('.js-projects-dropdown-toggle').click() - - expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(true) - expect($('.ajax-project-dropdown li').length).toBe(@projects_data.length) + expect($('.header-content').hasClass('open')).toBe(true) it 'hide dropdown', -> - $("#select2-drop-mask").click() + $(".dropdown-menu-close-icon").click() - expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(false) - - it 'change project when clicking item', -> - $('.js-projects-dropdown-toggle').click() - $('.ajax-project-dropdown li:nth-child(2)').trigger('mouseup') - - expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(false) - expect(window.current_project_url).toBe('http://localhost:3000/h5bp/html5-boilerplate') + expect($('.header-content').hasClass('open')).toBe(false) From 6416f8eab17556871984118c2dde04714a52bdf6 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 4 Apr 2016 10:44:51 +0100 Subject: [PATCH 085/187] Updated based on Ruby feedback --- app/helpers/projects_helper.rb | 4 +-- app/views/layouts/project.html.haml | 11 ++++---- .../fixtures/project_title.html.haml | 25 ++++++++++--------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 83ebc124171..cc411da459f 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -66,10 +66,10 @@ module ProjectsHelper end project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } - project_link += icon "chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", data: { target: ".header-content", toggle: "dropdown" } if current_user + project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) if current_user full_title = namespace_link + ' / ' + project_link - full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name + full_title << ' · '.html_safe + link_to(simple_sanitize(name), url) if name full_title end diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 2c5911fa2fb..6dfe7fbdae8 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -18,10 +18,11 @@ = render "layouts/init_auto_complete" if current_user - content_for :header_content do - .dropdown-menu.dropdown-select.dropdown-menu-projects - = dropdown_title("Go to a project") - = dropdown_filter("Search your projects") - = dropdown_content - = dropdown_loading + .js-dropdown-menu-projects + .dropdown-menu.dropdown-select.dropdown-menu-projects + = dropdown_title("Go to a project") + = dropdown_filter("Search your projects") + = dropdown_content + = dropdown_loading = render template: "layouts/application" diff --git a/spec/javascripts/fixtures/project_title.html.haml b/spec/javascripts/fixtures/project_title.html.haml index 3696f241c21..4547feeb212 100644 --- a/spec/javascripts/fixtures/project_title.html.haml +++ b/spec/javascripts/fixtures/project_title.html.haml @@ -5,15 +5,16 @@ %a.project-item-select-holder{href: "/gitlab-org/gitlab-test"} GitLab Test %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle{ "data-toggle" => "dropdown", "data-target" => ".header-content" } - .dropdown-menu.dropdown-select.dropdown-menu-projects - .dropdown-title - %span Go to a project - %button.dropdown-title-button.dropdown-menu-close{"aria-label" => "Close", type: "button"} - %i.fa.fa-times.dropdown-menu-close-icon - .dropdown-input - %input.dropdown-input-field{id: "", placeholder: "Search your projects", type: "search", value: ""} - %i.fa.fa-search.dropdown-input-search - %i.fa.fa-times.dropdown-input-clear.js-dropdown-input-clear{role: "button"} - .dropdown-content - .dropdown-loading - %i.fa.fa-spinner.fa-spin + .js-dropdown-menu-projects + .dropdown-menu.dropdown-select.dropdown-menu-projects + .dropdown-title + %span Go to a project + %button.dropdown-title-button.dropdown-menu-close{"aria-label" => "Close", type: "button"} + %i.fa.fa-times.dropdown-menu-close-icon + .dropdown-input + %input.dropdown-input-field{id: "", placeholder: "Search your projects", type: "search", value: ""} + %i.fa.fa-search.dropdown-input-search + %i.fa.fa-times.dropdown-input-clear.js-dropdown-input-clear{role: "button"} + .dropdown-content + .dropdown-loading + %i.fa.fa-spinner.fa-spin From 4293485a22bfdbd2afdbb20dcf5d777f379fac87 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 7 Apr 2016 15:20:22 +0100 Subject: [PATCH 086/187] Updated Ruby Added CHANGELOG item --- CHANGELOG | 1 + app/helpers/projects_helper.rb | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 07274ab5c1d..d12f703c7bc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -59,6 +59,7 @@ v 8.6.5 v 8.6.4 - Don't attempt to fetch any tags from a forked repo (Stan Hu) - Redesign the Labels page + - Project switcher uses new dropdown styling v 8.6.3 - Mentions on confidential issues doesn't create todos for non-members. !3374 diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index cc411da459f..ab77853da1a 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -66,10 +66,13 @@ module ProjectsHelper end project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } - project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) if current_user - full_title = namespace_link + ' / ' + project_link - full_title << ' · '.html_safe + link_to(simple_sanitize(name), url) if name + if current_user + project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) + end + + full_title = namespace_link + ' / ' << project_link + full_title << ' · '.html_safe << link_to(simple_sanitize(name), url) if name full_title end From 63e54f1555f02b93347588fbf332c7521d19d2a6 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 8 Apr 2016 19:44:23 +0100 Subject: [PATCH 087/187] Updated based on Ruby feedback --- CHANGELOG | 8 ++++++++ app/helpers/projects_helper.rb | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d12f703c7bc..ab2ef90fb08 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -60,6 +60,14 @@ v 8.6.4 - Don't attempt to fetch any tags from a forked repo (Stan Hu) - Redesign the Labels page - Project switcher uses new dropdown styling + - Project switcher uses new dropdown styling + +v 8.6.5 (unreleased) + - Only update repository language if it is not set to improve performance + - Check permissions when user attempts to import members from another project + +v 8.6.4 + - Don't attempt to fetch any tags from a forked repo (Stan Hu) v 8.6.3 - Mentions on confidential issues doesn't create todos for non-members. !3374 diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index ab77853da1a..7e00aacceaa 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -71,7 +71,7 @@ module ProjectsHelper project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) end - full_title = namespace_link + ' / ' << project_link + full_title = "#{namespace_link} / #{project_link}".html_safe full_title << ' · '.html_safe << link_to(simple_sanitize(name), url) if name full_title From 1ac6bdb5c85f14557ed41b5b81d6ee9d577739a1 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 11 Apr 2016 13:23:29 +0100 Subject: [PATCH 088/187] Updated CHANGELOG --- CHANGELOG | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ab2ef90fb08..071e35167fa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -46,6 +46,7 @@ v 8.7.0 (unreleased) v 8.6.6 - Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk) + - Project switcher uses new dropdown styling v 8.6.5 - Fix importing from GitHub Enterprise. !3529 @@ -59,15 +60,6 @@ v 8.6.5 v 8.6.4 - Don't attempt to fetch any tags from a forked repo (Stan Hu) - Redesign the Labels page - - Project switcher uses new dropdown styling - - Project switcher uses new dropdown styling - -v 8.6.5 (unreleased) - - Only update repository language if it is not set to improve performance - - Check permissions when user attempts to import members from another project - -v 8.6.4 - - Don't attempt to fetch any tags from a forked repo (Stan Hu) v 8.6.3 - Mentions on confidential issues doesn't create todos for non-members. !3374 From 38cff18af0ed48bcd5916b6b6bb6ceeb9ab062fd Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Fri, 1 Apr 2016 15:04:03 -0700 Subject: [PATCH 089/187] Adjust the default trusted_proxies to only include localhost, and allow other trusted proxies to be configured. --- config/initializers/1_settings.rb | 1 + config/initializers/trusted_proxies.rb | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 config/initializers/trusted_proxies.rb diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 72c4d8d61ce..2167da306f2 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -190,6 +190,7 @@ Settings.gitlab.default_projects_features['visibility_level'] = Settings.send Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['restricted_signup_domains'] ||= [] Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'] +Settings.gitlab['trusted_proxies'] ||= [] # diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb new file mode 100644 index 00000000000..b8cc025bae2 --- /dev/null +++ b/config/initializers/trusted_proxies.rb @@ -0,0 +1,2 @@ +Rails.application.config.action_dispatch.trusted_proxies = + [ '127.0.0.1', '::1' ] + Array(Gitlab.config.gitlab.trusted_proxies) From bb372ac97f733c45f22dc31e09b98a78411d4f86 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Wed, 6 Apr 2016 06:49:46 -0700 Subject: [PATCH 090/187] Add changelog entries, install docs, and gitlab.yml.example entry for the trusted_proxies setting --- CHANGELOG | 2 ++ config/gitlab.yml.example | 9 +++++++++ doc/install/installation.md | 9 +++++++++ 3 files changed, 20 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 07274ab5c1d..584e60a0e06 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,8 @@ v 8.7.0 (unreleased) - Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea) - All images in discussions and wikis now link to their source files !3464 (Connor Shea). - Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu) + - Add setting for customizing the list of trusted proxies !3524 + - Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524 - Improved Markdown rendering performance !3389 (Yorick Peterse) - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu) - Expose project badges in project settings diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 35c7c425a5a..56caee47c97 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -46,6 +46,15 @@ production: &base # # relative_url_root: /gitlab + # Trusted Proxies + # Customize if you have GitLab behind a reverse proxy which is running on a different machine. + # Add the IP address for your reverse proxy to the list, otherwise users will appear signed in from that address. + trusted_proxies: + # Examples: + #- 192.168.1.0/24 + #- 192.168.2.1 + #- 2001:0db8::/32 + # Uncomment and customize if you can't use the default user to run GitLab (default: 'git') # user: git diff --git a/doc/install/installation.md b/doc/install/installation.md index f8f7d6a9ebe..bfea4ce193e 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -530,6 +530,15 @@ See the [omniauth integration document](../integration/omniauth.md) GitLab can build your projects. To enable that feature you need GitLab Runners to do that for you. Checkout the [GitLab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it +### Adding your Trusted Proxies + +If you are using a reverse proxy on an separate machine, you may want to add the +proxy to the trusted proxies list. Otherwise users will appear signed in from the +proxy's IP address. + +You can add trusted proxies in `config/gitlab.yml` by customizing the `trusted_proxies` +option in section 1. Please restart GitLab after editing this file. + ### Custom Redis Connection If you'd like Resque to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file. From e18f20d7118b7c8f2ff18a6e4255d6c7c0995b04 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Tue, 12 Apr 2016 11:02:41 -0700 Subject: [PATCH 091/187] Updated trusted proxies doc section to link to the restart GitLab docs --- doc/install/installation.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index bfea4ce193e..e721e70a596 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -537,7 +537,8 @@ proxy to the trusted proxies list. Otherwise users will appear signed in from th proxy's IP address. You can add trusted proxies in `config/gitlab.yml` by customizing the `trusted_proxies` -option in section 1. Please restart GitLab after editing this file. +option in section 1. Save the file and [reconfigure GitLab](../administration/restart_gitlab.md) +for the changes to take effect. ### Custom Redis Connection From 42a391f744a2ea43272483f998395c910153e1d2 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 12 Apr 2016 14:28:07 -0400 Subject: [PATCH 092/187] Update spring and spring-commands-spinach Spring changelog: https://git.io/vVAUY --- Gemfile | 4 ++-- Gemfile.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 258b5612cd5..199ef65d922 100644 --- a/Gemfile +++ b/Gemfile @@ -285,9 +285,9 @@ group :development, :test do gem 'teaspoon', '~> 1.1.0' gem 'teaspoon-jasmine', '~> 2.2.0' - gem 'spring', '~> 1.6.4' + gem 'spring', '~> 1.7.0' gem 'spring-commands-rspec', '~> 1.0.4' - gem 'spring-commands-spinach', '~> 1.0.0' + gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'rubocop', '~> 0.38.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 9da44a46583..ad7d7c18559 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -769,10 +769,10 @@ GEM spinach (>= 0.4) spinach-rerun-reporter (0.0.2) spinach (~> 0.8) - spring (1.6.4) + spring (1.7.1) spring-commands-rspec (1.0.4) spring (>= 0.9.1) - spring-commands-spinach (1.0.0) + spring-commands-spinach (1.1.0) spring (>= 0.9.1) spring-commands-teaspoon (0.0.2) spring (>= 0.9.1) @@ -1030,9 +1030,9 @@ DEPENDENCIES slack-notifier (~> 1.2.0) spinach-rails (~> 0.2.1) spinach-rerun-reporter (~> 0.0.2) - spring (~> 1.6.4) + spring (~> 1.7.0) spring-commands-rspec (~> 1.0.4) - spring-commands-spinach (~> 1.0.0) + spring-commands-spinach (~> 1.1.0) spring-commands-teaspoon (~> 0.0.2) sprockets (~> 3.6.0) state_machines-activerecord (~> 0.3.0) From a64f1c763615c049e551c82a9f3a7c53525a172c Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Tue, 12 Apr 2016 15:39:33 -0300 Subject: [PATCH 093/187] Add changelog entry, improve specs and model code --- CHANGELOG | 1 + app/models/merge_request.rb | 13 ++++--------- .../projects/merge_requests_controller_spec.rb | 9 ++++----- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 54d79259b30..cf84ce8116e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ v 8.7.0 (unreleased) - Add links to CI setup documentation from project settings and builds pages - Handle nil descriptions in Slack issue messages (Stan Hu) - Add default scope to projects to exclude projects pending deletion + - Allow to close merge requests which source projects(forks) are deleted. - Ensure empty recipients are rejected in BuildsEmailService - API: Ability to filter milestones by state `active` and `closed` (Robert Schilling) - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 8292445bcac..e410febdfff 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -128,7 +128,7 @@ class MergeRequest < ActiveRecord::Base validates :target_project, presence: true validates :target_branch, presence: true validates :merge_user, presence: true, if: :merge_when_build_succeeds? - validate :validate_branches + validate :validate_branches, unless: :allow_broken validate :validate_fork scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } @@ -213,14 +213,12 @@ class MergeRequest < ActiveRecord::Base end def validate_branches - return if allow_broken - if target_project == source_project && target_branch == source_branch errors.add :branch_conflict, "You can not use same project/branch for source and target" end if opened? || reopened? - similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened + similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id if similar_mrs.any? errors.add :validate_branches, @@ -346,12 +344,9 @@ class MergeRequest < ActiveRecord::Base end def hook_attrs - source_hook_attrs = source_project.hook_attrs if source_project.present? - target_hook_attrs = target_project.hook_attrs if target_project.present? - attrs = { - source: source_hook_attrs, - target: target_hook_attrs, + source: source_project.try(:hook_attrs), + target: target_project.hook_attrs, last_commit: nil, work_in_progress: work_in_progress? } diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 0f2cd34132a..c54e83339a1 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -159,19 +159,18 @@ describe Projects::MergeRequestsController do describe 'PUT #update' do context 'there is no source project' do - let(:project) { create(:project) } - let(:fork_project) { create(:forked_project_with_submodules) } - let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } + let(:project) { create(:project) } + let(:fork_project) { create(:forked_project_with_submodules) } + let(:merge_request) { create(:merge_request, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } before do fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) fork_project.save merge_request.reload + fork_project.destroy end it 'closes MR without errors' do - fork_project.destroy - post :update, namespace_id: project.namespace.path, project_id: project.path, From 447f3613b78ac4ba4ad6bda1811447b48e126b0c Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 8 Apr 2016 15:48:18 -0700 Subject: [PATCH 094/187] Wrap text in notes box if longer than code in diff --- app/assets/stylesheets/pages/notes.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 7295fe51121..88ba5e53a0d 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -155,6 +155,7 @@ ul.notes { border-width: 1px 0; padding: 0; vertical-align: top; + white-space: normal; &.parallel { border-width: 1px; } From d3ff7ca0846a88961d3f954c62398c54aa69c059 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 11 Apr 2016 10:00:45 -0500 Subject: [PATCH 095/187] Input updates --- .../stylesheets/pages/merge_requests.scss | 1 + app/assets/stylesheets/pages/note_form.scss | 22 ++++++++++++- app/assets/stylesheets/pages/notes.scss | 31 ++++++++++++++++--- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index b79335eab91..4ef548ffbe7 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -142,6 +142,7 @@ overflow: hidden; font-size: 90%; margin: 0 3px; + word-break: break-all; } .mr-list { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 4d4d508396d..5e5722c2c33 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -16,6 +16,7 @@ .new-note { margin: 0; border: none; + border-right: 1px solid $table-border-gray; } } @@ -71,12 +72,25 @@ border-color: $focus-border-color; } } + + p { + code { + white-space: normal; + } + + pre { + code { + white-space: pre; + } + } + } } } .discussion-form { padding: $gl-padding-top $gl-padding; - background-color: #fff; + background-color: $white-light; + border-right: 1px solid $table-border-gray; } .note-edit-form { @@ -118,7 +132,13 @@ .discussion-reply-holder { background-color: $white-light; + border-right: 1px solid $table-border-gray; padding: 10px 16px; + max-width: 800px; + + .new-note { + border-right: none; + } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 88ba5e53a0d..07dd0292453 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -81,9 +81,15 @@ ul.notes { @include md-typography; // On diffs code should wrap nicely and not overflow - pre { + p { code { - white-space: pre; + white-space: normal; + } + + pre { + code { + white-space: pre; + } } } @@ -112,6 +118,10 @@ ul.notes { margin: 10px 0; } } + + a { + word-break: break-all; + } } .note-header { @@ -127,7 +137,7 @@ ul.notes { margin-right: 10px; } .line_content { - white-space: pre-wrap; + white-space: pre; } } @@ -145,20 +155,33 @@ ul.notes { background: $background-color; color: $text-color; } + &.notes_line2 { text-align: center; padding: 10px 0; border-left: 1px solid #ddd !important; } + &.notes_content { - background-color: #fff; + background-color: $background-color; border-width: 1px 0; padding: 0; vertical-align: top; white-space: normal; + &.parallel { border-width: 1px; } + + .new-note { + max-width: 800px; + } + + .notes { + max-width: 800px; + background-color: $white-light; + border-right: 1px solid $table-border-gray; + } } } } From 4e5ae5a281be109ca22929bb21732f0ee1f6630d Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Tue, 12 Apr 2016 15:40:53 -0500 Subject: [PATCH 096/187] Fix Grafana docs and link from Influx page --- doc/monitoring/performance/gitlab_configuration.md | 1 + doc/monitoring/performance/grafana_configuration.md | 6 +++--- doc/monitoring/performance/influxdb_configuration.md | 1 + doc/monitoring/performance/influxdb_schema.md | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/monitoring/performance/gitlab_configuration.md b/doc/monitoring/performance/gitlab_configuration.md index b856e7935a3..90e99302210 100644 --- a/doc/monitoring/performance/gitlab_configuration.md +++ b/doc/monitoring/performance/gitlab_configuration.md @@ -37,3 +37,4 @@ Read more on: - [Introduction to GitLab Performance Monitoring](introduction.md) - [InfluxDB Configuration](influxdb_configuration.md) - [InfluxDB Schema](influxdb_schema.md) +- [Grafana Install/Configuration](grafana_configuration.md diff --git a/doc/monitoring/performance/grafana_configuration.md b/doc/monitoring/performance/grafana_configuration.md index 416c9870aa0..10ef1009818 100644 --- a/doc/monitoring/performance/grafana_configuration.md +++ b/doc/monitoring/performance/grafana_configuration.md @@ -91,18 +91,18 @@ JSON file. Open the dashboard dropdown menu and click 'Import' -![Grafana dashboard dropdown](/img/grafana_dashboard_dropdown.png) +![Grafana dashboard dropdown](img/grafana_dashboard_dropdown.png) Click 'Choose file' and browse to the location where you downloaded or cloned the dashboard repository. Pick one of the JSON files to import. -![Grafana dashboard import](/img/grafana_dashboard_import.png) +![Grafana dashboard import](img/grafana_dashboard_import.png) Once the dashboard is imported, be sure to click save icon in the top bar. If you do not save the dashboard after importing it will be removed when you navigate away. -![Grafana save icon](/img/grafana_save_icon.png) +![Grafana save icon](img/grafana_save_icon.png) Repeat this process for each dashboard you wish to import. diff --git a/doc/monitoring/performance/influxdb_configuration.md b/doc/monitoring/performance/influxdb_configuration.md index 3a2b598b78f..63aa03985ef 100644 --- a/doc/monitoring/performance/influxdb_configuration.md +++ b/doc/monitoring/performance/influxdb_configuration.md @@ -181,6 +181,7 @@ Read more on: - [Introduction to GitLab Performance Monitoring](introduction.md) - [GitLab Configuration](gitlab_configuration.md) - [InfluxDB Schema](influxdb_schema.md) +- [Grafana Install/Configuration](grafana_configuration.md [influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management [influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/ diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md index a5a8aebd2d1..d31b3788f36 100644 --- a/doc/monitoring/performance/influxdb_schema.md +++ b/doc/monitoring/performance/influxdb_schema.md @@ -85,3 +85,4 @@ Read more on: - [Introduction to GitLab Performance Monitoring](introduction.md) - [GitLab Configuration](gitlab_configuration.md) - [InfluxDB Configuration](influxdb_configuration.md) +- [Grafana Install/Configuration](grafana_configuration.md From 60736db429666f4145004408b722799ef144eb5f Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 12 Apr 2016 15:07:24 -0500 Subject: [PATCH 097/187] Remove max-width from comments --- app/assets/stylesheets/pages/note_form.scss | 8 -------- app/assets/stylesheets/pages/notes.scss | 6 ------ 2 files changed, 14 deletions(-) diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 5e5722c2c33..f4da17fadaa 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -16,7 +16,6 @@ .new-note { margin: 0; border: none; - border-right: 1px solid $table-border-gray; } } @@ -90,7 +89,6 @@ .discussion-form { padding: $gl-padding-top $gl-padding; background-color: $white-light; - border-right: 1px solid $table-border-gray; } .note-edit-form { @@ -132,13 +130,7 @@ .discussion-reply-holder { background-color: $white-light; - border-right: 1px solid $table-border-gray; padding: 10px 16px; - max-width: 800px; - - .new-note { - border-right: none; - } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 07dd0292453..e421a31549a 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -173,14 +173,8 @@ ul.notes { border-width: 1px; } - .new-note { - max-width: 800px; - } - .notes { - max-width: 800px; background-color: $white-light; - border-right: 1px solid $table-border-gray; } } } From 6a238c37e002c9d8dcc51af2cb3dff44c900139c Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 12 Apr 2016 18:06:52 -0300 Subject: [PATCH 098/187] Fix todo_target_path for todos where target was removed --- app/assets/javascripts/todos.js.coffee | 2 ++ app/helpers/todos_helper.rb | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee index 886da72e261..00d2b641723 100644 --- a/app/assets/javascripts/todos.js.coffee +++ b/app/assets/javascripts/todos.js.coffee @@ -59,6 +59,8 @@ class @Todos goToTodoUrl: (e)-> todoLink = $(this).data('url') + return unless todoLink + if e.metaKey e.preventDefault() window.open(todoLink,'_blank') diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index edc5686cf08..2f066682180 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -20,6 +20,8 @@ module TodosHelper end def todo_target_path(todo) + return unless todo.target.present? + anchor = dom_id(todo.note) if todo.note.present? if todo.for_commit? From fd248b0f068ad0239d0d4ddc462f091eecfe981e Mon Sep 17 00:00:00 2001 From: Lee Date: Tue, 12 Apr 2016 16:13:31 -0500 Subject: [PATCH 099/187] (doc) fix typo to ssh keys doc url --- doc/ci/ssh_keys/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index d790015aca1..7f825e6a065 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -30,7 +30,7 @@ This is the universal solution which works with any type of executor ## SSH keys when using the Docker executor You will first need to create an SSH key pair. For more information, follow the -instructions to [generate an SSH key](../ssh/README.md). +instructions to [generate an SSH key](../../ssh/README.md). Then, create a new **Secret Variable** in your project settings on GitLab following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY` @@ -63,7 +63,7 @@ before_script: As a final step, add the _public_ key from the one you created earlier to the services that you want to have an access to from within the build environment. If you are accessing a private GitLab repository you need to add it as a -[deploy key](../ssh/README.md#deploy-keys). +[deploy key](../../ssh/README.md#deploy-keys). That's it! You can now have access to private servers or repositories in your build environment. @@ -79,12 +79,12 @@ on, and use that key for all projects that are run on this machine. First, you need to login to the server that runs your builds. Then from the terminal login as the `gitlab-runner` user and generate the SSH -key pair as described in the [SSH keys documentation](../ssh/README.md). +key pair as described in the [SSH keys documentation](../../ssh/README.md). As a final step, add the _public_ key from the one you created earlier to the services that you want to have an access to from within the build environment. If you are accessing a private GitLab repository you need to add it as a -[deploy key](../ssh/README.md#deploy-keys). +[deploy key](../../ssh/README.md#deploy-keys). Once done, try to login to the remote server in order to accept the fingerprint: From 2e13f6c326b920f1b78ca592dc1b938b62d5eef3 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 12 Apr 2016 14:39:08 -0400 Subject: [PATCH 100/187] Add `Gitlab.com?` method To be used as a feature flag for GitLab.com-only features, such as welcome emails. We will be careful to only use this to disable features or functionality that do not make sense for any installations that aren't GitLab.com. We will not use this to restrict features from other installations or keep them "exclusive" to GitLab.com. --- lib/gitlab.rb | 3 +++ spec/lib/gitlab_spec.rb | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 spec/lib/gitlab_spec.rb diff --git a/lib/gitlab.rb b/lib/gitlab.rb index 6108697bc20..7479e729db1 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -1,4 +1,7 @@ require 'gitlab/git' module Gitlab + def self.com? + Gitlab.config.gitlab.url == 'https://gitlab.com' + end end diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb new file mode 100644 index 00000000000..c59dfea5c55 --- /dev/null +++ b/spec/lib/gitlab_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe Gitlab, lib: true do + describe '.com?' do + it 'is true when on GitLab.com' do + stub_config_setting(url: 'https://gitlab.com') + + expect(described_class.com?).to eq true + end + + it 'is false when not on GitLab.com' do + stub_config_setting(url: 'http://example.com') + + expect(described_class.com?).to eq false + end + end +end From d8296f873871120b7f4134bdcf8854a09b9e8be8 Mon Sep 17 00:00:00 2001 From: connorshea Date: Tue, 12 Apr 2016 16:11:58 -0600 Subject: [PATCH 101/187] Remove Bootstrap Carousel The Bootstrap carousel module is used for image carousels, and we don't use it anywhere on the site. Also separated the Bootstrap JavaScript into separate components and removed the carousel component. Fixes #14670. --- app/assets/javascripts/application.js.coffee | 12 +++++++++++- app/assets/stylesheets/framework/tw_bootstrap.scss | 1 - 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index b05138ac1ac..6f435e4c542 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -22,7 +22,17 @@ #= require cal-heatmap #= require turbolinks #= require autosave -#= require bootstrap +#= require bootstrap/affix +#= require bootstrap/alert +#= require bootstrap/button +#= require bootstrap/collapse +#= require bootstrap/dropdown +#= require bootstrap/modal +#= require bootstrap/scrollspy +#= require bootstrap/tab +#= require bootstrap/transition +#= require bootstrap/tooltip +#= require bootstrap/popover #= require select2 #= require raphael #= require g.raphael diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index dd42db1840f..96bab7880c2 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -43,7 +43,6 @@ @import "bootstrap/modals"; @import "bootstrap/tooltip"; @import "bootstrap/popovers"; -@import "bootstrap/carousel"; // Utility classes .clearfix { From a0008f2720a1dc1d5ba79dcf2e27c041ed52fb52 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Tue, 12 Apr 2016 23:57:42 +0000 Subject: [PATCH 102/187] improve formatting --- doc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README.md b/doc/README.md index 724c7cca0f1..d2660930653 100644 --- a/doc/README.md +++ b/doc/README.md @@ -3,7 +3,7 @@ ## User documentation - [API](api/README.md) Automate GitLab via a simple and powerful API. -- [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, .gitlab-ci.yml options, and examples. +- [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, `.gitlab-ci.yml` options, and examples. - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. - [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. - [Importing to GitLab](workflow/importing/README.md). From 2ce7559d70e228963280df1f50176e9b2fa1e7b8 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 13 Apr 2016 00:00:15 -0700 Subject: [PATCH 103/187] Fix repository cache invalidation issue when project is recreated with an empty repo To reproduce: 1. Create a project with some content 2. Rename the project 3. Create a new project with the same name. 4. Boom - 404. After step 2, the branch and tag counts were not being cleared. This would cause `repository.has_visible_content?` to erroneously return `true` for the newly-created project. Closes #13384 --- CHANGELOG | 1 + app/models/repository.rb | 2 ++ spec/models/repository_spec.rb | 2 ++ 3 files changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 071e35167fa..21a4aea91b1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,6 +43,7 @@ v 8.7.0 (unreleased) - Update number of Todos in the sidebar when it's marked as "Done". !3600 - API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling) - API: User can leave a project through the API when not master or owner. !3613 + - Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu) v 8.6.6 - Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk) diff --git a/app/models/repository.rb b/app/models/repository.rb index 462b48118ef..0b2289cfa39 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -253,6 +253,8 @@ class Repository # This ensures this particular cache is flushed after the first commit to a # new repository. expire_emptiness_caches if empty? + expire_branch_count_cache + expire_tag_count_cache end def expire_branch_cache(branch_name = nil) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 4e49c413f23..c3a4016fa49 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -393,6 +393,8 @@ describe Repository, models: true do describe '#expire_cache' do it 'expires all caches' do expect(repository).to receive(:expire_branch_cache) + expect(repository).to receive(:expire_branch_count_cache) + expect(repository).to receive(:expire_tag_count_cache) repository.expire_cache end From 3ea955a637127e6e11bc9fe270f87f63226b9d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 5 Apr 2016 15:11:31 +0200 Subject: [PATCH 104/187] Improve TeamcityService and its specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- CHANGELOG | 4 +- .../project_services/teamcity_service.rb | 6 +- .../project_services/teamcity_service_spec.rb | 255 ++++++++++++++---- 3 files changed, 205 insertions(+), 60 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c6ca0ea3de1..df40a1e1b81 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ v 8.7.0 (unreleased) - Expose label description in API (Mariusz Jachimowicz) - Allow back dating on issues when created through the API - Fix Error 500 after renaming a project path (Stan Hu) + - Fix a bug whith trailing slash in teamcity_url (Charles May) - Fix avatar stretching by providing a cropping feature - API: Expose `subscribed` for issues and merge requests (Robert Schilling) - Allow SAML to handle external users based on user's information !3530 @@ -123,9 +124,6 @@ v 8.6.0 - Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner) - HTTP error pages work independently from location and config (Artem Sidorenko) - Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set - - Fix avatar stretching by providing a cropping feature (Johann Pardanaud) - - Fix a bug whith trailing slash in teamcity_url (Charles May) - - Don't load all of GitLab in mail_room - Memoize @group in Admin::GroupsController (Yatish Mehta) - Indicate how much an MR diverged from the target branch (Pierre de La Morinerie) - Added omniauth-auth0 Gem (Daniel Carraro) diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 246c5eb4a82..8dceee5e2c5 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -91,7 +91,7 @@ class TeamcityService < CiService ).to_s auth = { username: username, - password: password, + password: password } @response = HTTParty.get(url, verify: false, basic_auth: auth) end @@ -108,7 +108,7 @@ class TeamcityService < CiService built_id = @response['build']['id'] URI.join( teamcity_url, - "#{teamcity_url}/viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}" + "/viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}" ).to_s end end @@ -145,7 +145,7 @@ class TeamcityService < CiService branch = Gitlab::Git.ref_name(data[:ref]) self.class.post( - URI.join(teamcity_url, "/httpAuth/app/rest/buildQueue").to_s, + URI.join(teamcity_url, '/httpAuth/app/rest/buildQueue').to_s, body: ""\ ""\ '', diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index f26b47a856c..bc7423cee69 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -21,73 +21,220 @@ require 'spec_helper' describe TeamcityService, models: true do - describe "Associations" do + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } end - describe "Execute" do - let(:user) { create(:user) } - let(:project) { create(:project) } + describe 'Validations' do + describe '#teamcity_url' do + it 'does not validate the presence of teamcity_url if service is not active' do + teamcity_service = service + teamcity_service.active = false - context "when a password was previously set" do - before do - @teamcity_service = TeamcityService.create( - project: create(:project), - properties: { - teamcity_url: 'http://gitlab.com', - username: 'mic', - password: "password" - } - ) - end - - it "reset password if url changed" do - @teamcity_service.teamcity_url = 'http://gitlab1.com' - @teamcity_service.save - expect(@teamcity_service.password).to be_nil - end - - it "does not reset password if username changed" do - @teamcity_service.username = "some_name" - @teamcity_service.save - expect(@teamcity_service.password).to eq("password") + expect(teamcity_service).not_to validate_presence_of(:teamcity_url) end - it "does not reset password if new url is set together with password, even if it's the same password" do - @teamcity_service.teamcity_url = 'http://gitlab_edited.com' - @teamcity_service.password = 'password' - @teamcity_service.save - expect(@teamcity_service.password).to eq("password") - expect(@teamcity_service.teamcity_url).to eq("http://gitlab_edited.com") - end + it 'validates the presence of teamcity_url if service is active' do + teamcity_service = service + teamcity_service.active = true - it "should reset password if url changed, even if setter called multiple times" do - @teamcity_service.teamcity_url = 'http://gitlab1.com' - @teamcity_service.teamcity_url = 'http://gitlab1.com' - @teamcity_service.save - expect(@teamcity_service.password).to be_nil + expect(teamcity_service).to validate_presence_of(:teamcity_url) end end - - context "when no password was previously set" do - before do - @teamcity_service = TeamcityService.create( - project: create(:project), - properties: { - teamcity_url: 'http://gitlab.com', - username: 'mic' - } - ) + + describe '#build_type' do + it 'does not validate the presence of build_type if service is not active' do + teamcity_service = service + teamcity_service.active = false + + expect(teamcity_service).not_to validate_presence_of(:build_type) end - it "saves password if new url is set together with password" do - @teamcity_service.teamcity_url = 'http://gitlab_edited.com' - @teamcity_service.password = 'password' - @teamcity_service.save - expect(@teamcity_service.password).to eq("password") - expect(@teamcity_service.teamcity_url).to eq("http://gitlab_edited.com") + it 'validates the presence of build_type if service is active' do + teamcity_service = service + teamcity_service.active = true + + expect(teamcity_service).to validate_presence_of(:build_type) + end + end + + describe '#username' do + it 'does not validate the presence of username if service is not active' do + teamcity_service = service + teamcity_service.active = false + + expect(teamcity_service).not_to validate_presence_of(:username) + end + + it 'does not validate the presence of username if username is nil' do + teamcity_service = service + teamcity_service.active = true + teamcity_service.password = nil + + expect(teamcity_service).not_to validate_presence_of(:username) + end + + it 'validates the presence of username if service is active and username is present' do + teamcity_service = service + teamcity_service.active = true + teamcity_service.password = 'secret' + + expect(teamcity_service).to validate_presence_of(:username) + end + end + + describe '#password' do + it 'does not validate the presence of password if service is not active' do + teamcity_service = service + teamcity_service.active = false + + expect(teamcity_service).not_to validate_presence_of(:password) + end + + it 'does not validate the presence of password if username is nil' do + teamcity_service = service + teamcity_service.active = true + teamcity_service.username = nil + + expect(teamcity_service).not_to validate_presence_of(:password) + end + + it 'validates the presence of password if service is active and username is present' do + teamcity_service = service + teamcity_service.active = true + teamcity_service.username = 'john' + + expect(teamcity_service).to validate_presence_of(:password) end end end + + describe 'Callbacks' do + describe 'before_update :reset_password' do + context 'when a password was previously set' do + it 'resets password if url changed' do + teamcity_service = service + + teamcity_service.teamcity_url = 'http://gitlab1.com' + teamcity_service.save + + expect(teamcity_service.password).to be_nil + end + + it 'does not reset password if username changed' do + teamcity_service = service + + teamcity_service.username = 'some_name' + teamcity_service.save + + expect(teamcity_service.password).to eq('password') + end + + it "does not reset password if new url is set together with password, even if it's the same password" do + teamcity_service = service + + teamcity_service.teamcity_url = 'http://gitlab_edited.com' + teamcity_service.password = 'password' + teamcity_service.save + + expect(teamcity_service.password).to eq('password') + expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com') + end + end + + it 'saves password if new url is set together with password when no password was previously set' do + teamcity_service = service + teamcity_service.password = nil + + teamcity_service.teamcity_url = 'http://gitlab_edited.com' + teamcity_service.password = 'password' + teamcity_service.save + + expect(teamcity_service.password).to eq('password') + expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com') + end + end + end + + describe '#build_page' do + it 'returns a specific URL when status is 500' do + stub_request(status: 500) + + expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildTypeId=foo') + end + + it 'returns a build URL when teamcity_url has no trailing slash' do + stub_request(body: %Q({"build":{"id":"666"}})) + + expect(service(teamcity_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo') + end + + it 'returns a build URL when teamcity_url has a trailing slash' do + stub_request(body: %Q({"build":{"id":"666"}})) + + expect(service(teamcity_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo') + end + end + + describe '#commit_status' do + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) + + expect(service.commit_status('123', 'unused')).to eq(:error) + end + + it 'sets commit status to "pending" when status is 404' do + stub_request(status: 404) + + expect(service.commit_status('123', 'unused')).to eq('pending') + end + + it 'sets commit status to "success" when build status contains SUCCESS' do + stub_request(build_status: 'YAY SUCCESS!') + + expect(service.commit_status('123', 'unused')).to eq('success') + end + + it 'sets commit status to "failed" when build status contains FAILURE' do + stub_request(build_status: 'NO FAILURE!') + + expect(service.commit_status('123', 'unused')).to eq('failed') + end + + it 'sets commit status to "pending" when build status contains Pending' do + stub_request(build_status: 'NO Pending!') + + expect(service.commit_status('123', 'unused')).to eq('pending') + end + + it 'sets commit status to :error when build status is unknown' do + stub_request(build_status: 'FOO BAR!') + + expect(service.commit_status('123', 'unused')).to eq(:error) + end + end + + def service(teamcity_url: 'http://gitlab.com') + described_class.create( + project: build_stubbed(:empty_project), + properties: { + teamcity_url: teamcity_url, + username: 'mic', + password: 'password', + build_type: 'foo' + } + ) + end + + def stub_request(status: 200, body: nil, build_status: 'success') + teamcity_full_url = 'http://mic:password@gitlab.com/httpAuth/app/rest/builds/branch:unspecified:any,number:123' + body ||= %Q({"build":{"status":"#{build_status}","id":"666"}}) + + WebMock.stub_request(:get, teamcity_full_url).to_return( + status: status, + headers: { 'Content-Type' => 'application/json' }, + body: body + ) + end end From 4a09a6c6f21c0bcd48123759cd6804276d810929 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 13 Apr 2016 11:31:06 +0300 Subject: [PATCH 105/187] Move 'New branch from issue' feature doc to web_editor.md [ci skip] --- doc/gitlab-basics/create-branch.md | 9 ----- .../img/new_branch_from_issue.png} | Bin doc/workflow/web_editor.md | 32 ++++++++++++++++++ 3 files changed, 32 insertions(+), 9 deletions(-) rename doc/{gitlab-basics/basicsimages/new_branch_button.png => workflow/img/new_branch_from_issue.png} (100%) diff --git a/doc/gitlab-basics/create-branch.md b/doc/gitlab-basics/create-branch.md index 9d688b9389b..7556b0f663e 100644 --- a/doc/gitlab-basics/create-branch.md +++ b/doc/gitlab-basics/create-branch.md @@ -32,15 +32,6 @@ Fill out the information required: ![Branch info](basicsimages/branch_info.png) -## From an issue -When an issue should be resolved one could also create a branch on the issue page. A button is displayed after the description unless there is already a branch or a referenced merge request. - -![New Branch Button](basicsimages/new_branch_button.png) - -The branch created diverges from the default branch of the project, usually `master`. The branch name will be based on the title of the issue and as suffix its ID. Thus the example screenshot above will yield a branch named `et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum-2`. -After the branch is created the user can edit files in the repository to fix the issue. When a merge request is created the -description field will display `Closes #2` to use the issue closing pattern. This will close the issue once the merge request is merged. - ### Note: You will be able to find and select the name of your branch in the white box next to a project's name: diff --git a/doc/gitlab-basics/basicsimages/new_branch_button.png b/doc/workflow/img/new_branch_from_issue.png similarity index 100% rename from doc/gitlab-basics/basicsimages/new_branch_button.png rename to doc/workflow/img/new_branch_from_issue.png diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md index 4a451d98953..5685a9d89dd 100644 --- a/doc/workflow/web_editor.md +++ b/doc/workflow/web_editor.md @@ -66,6 +66,35 @@ the target branch. Click **Create directory** to finish. ## Create a new branch +There are multiple ways to create a branch from GitLab's web interface. + +### Create a new branch from an issue + +>**Note:** +This feature was [introduced][ce-2808] in GitLab 8.6. + +In case your development workflow dictates to have an issue for every merge +request, you can quickly create a branch right on the issue page which will be +tied with the issue itself. You can see a **New Branch** button after the issue +description, unless there is already a branch with the same name or a referenced +merge request. + +![New Branch Button](img/new_branch_from_issue.png) + +Once you click it, a new branch will be created that diverges from the default +branch of your project, by default `master`. The branch name will be based on +the title of the issue and as suffix it will have its ID. Thus, the example +screenshot above will yield a branch named +`et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum-2`. + +After the branch is created, you can edit files in the repository to fix +the issue. When a merge request is created based on the newly created branch, +the description field will automatically display the [issue closing pattern] +`Closes #ID`, where `ID` the ID of the issue. This will close the issue once the +merge request is merged. + +### Create a new branch from a project's dashboard + If you want to make changes to several files before creating a new merge request, you can create a new branch up front. From a project's files page, choose **New branch** from the dropdown. @@ -118,3 +147,6 @@ appear that is labeled **Start a new merge request with these changes**. After you commit the changes you will be taken to a new merge request form. ![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png) + +[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808 +[issue closing pattern]: ../customization/issue_closing.md From b2f48d8c46cebcf2a576c18b661c3481b3450f3b Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Tue, 12 Apr 2016 21:34:24 +0200 Subject: [PATCH 106/187] API: Return 404 if user does not have access to group --- CHANGELOG | 1 + lib/api/helpers.rb | 3 +-- spec/requests/api/groups_spec.rb | 15 ++++++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f3fc54219e4..77a88714517 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -40,6 +40,7 @@ v 8.7.0 (unreleased) - Fix admin/projects when using visibility levels on search (PotHix) - Build status notifications - API: Expose user location (Robert Schilling) + - API: Do not leak group existence via return code (Robert Schilling) - ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591 - Update number of Todos in the sidebar when it's marked as "Done". !3600 - API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 4921ae99e78..96af7d7675c 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -91,8 +91,7 @@ module API if can?(current_user, :read_group, group) group else - forbidden!("#{current_user.username} lacks sufficient "\ - "access to #{group.name}") + not_found!('Group') end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 7383c7d11aa..083d5c459c6 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -61,7 +61,8 @@ describe API::API, api: true do it "should not return a group not attached to user1" do get api("/groups/#{group2.id}", user1) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end @@ -92,7 +93,8 @@ describe API::API, api: true do it 'should not return a group not attached to user1' do get api("/groups/#{group2.path}", user1) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end end @@ -157,7 +159,8 @@ describe API::API, api: true do it "should not return a group not attached to user1" do get api("/groups/#{group2.id}/projects", user1) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end @@ -189,7 +192,8 @@ describe API::API, api: true do it 'should not return a group not attached to user1' do get api("/groups/#{group2.path}/projects", user1) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end end @@ -247,7 +251,8 @@ describe API::API, api: true do it "should not remove a group not attached to user1" do delete api("/groups/#{group2.id}", user1) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end From a0bede92edf1f1df66e977c22540fced3414f3b7 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 13 Apr 2016 11:12:43 +0200 Subject: [PATCH 107/187] More code changes, thanks Robert --- app/workers/repository_check_worker.rb | 19 ++++++++------ app/workers/single_repository_check_worker.rb | 25 +++++++++---------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/app/workers/repository_check_worker.rb b/app/workers/repository_check_worker.rb index bdacdd4c6b0..d7ead91f94e 100644 --- a/app/workers/repository_check_worker.rb +++ b/app/workers/repository_check_worker.rb @@ -25,9 +25,11 @@ class RepositoryCheckWorker private - # In an ideal world we would use Project.where(...).find_each. - # Unfortunately, calling 'find_each' drops the 'where', so we must build - # an array of IDs instead. + # Project.find_each does not support WHERE clauses and + # Project.find_in_batches does not support ordering. So we just build an + # array of ID's. This is OK because we do it only once an hour, because + # getting ID's from Postgres is not terribly slow, and because no user + # has to sit and wait for this query to finish. def project_ids limit = 10_000 never_checked_projects = Project.where('last_repository_check_at IS NULL').limit(limit). @@ -40,17 +42,20 @@ class RepositoryCheckWorker def try_obtain_lease(id) # Use a 24-hour timeout because on servers/projects where 'git fsck' is # super slow we definitely do not want to run it twice in parallel. - lease = Gitlab::ExclusiveLease.new( + Gitlab::ExclusiveLease.new( "project_repository_check:#{id}", timeout: 24.hours - ) - lease.try_obtain + ).try_obtain end def current_settings # No caching of the settings! If we cache them and an admin disables # this feature, an active RepositoryCheckWorker would keep going for up # to 1 hour after the feature was disabled. - ApplicationSetting.current || Gitlab::CurrentSettings.fake_application_settings + if Rails.env.test? + Gitlab::CurrentSettings.fake_application_settings + else + ApplicationSetting.current + end end end diff --git a/app/workers/single_repository_check_worker.rb b/app/workers/single_repository_check_worker.rb index d9eed9bd708..6257f382d86 100644 --- a/app/workers/single_repository_check_worker.rb +++ b/app/workers/single_repository_check_worker.rb @@ -5,30 +5,29 @@ class SingleRepositoryCheckWorker def perform(project_id) project = Project.find(project_id) - update(project, success: check(project)) + project.update_columns( + last_repository_check_failed: !check(project), + last_repository_check_at: Time.now, + ) end private def check(project) - [project.repository.path_to_repo, project.wiki.wiki.path].all? do |path| - git_fsck(path) + [project.repository, project.wiki.repository].all? do |repository| + git_fsck(repository.path_to_repo) end end def git_fsck(path) cmd = %W(nice git --git-dir=#{path} fsck) output, status = Gitlab::Popen.popen(cmd) - return true if status.zero? - Gitlab::RepositoryCheckLogger.error("command failed: #{cmd.join(' ')}\n#{output}") - false - end - - def update(project, success:) - project.update_columns( - last_repository_check_failed: !success, - last_repository_check_at: Time.now, - ) + if status.zero? + true + else + Gitlab::RepositoryCheckLogger.error("command failed: #{cmd.join(' ')}\n#{output}") + false + end end end From 74783358268a6ce69375780400e661d9aa1fa741 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 13 Apr 2016 11:15:36 +0200 Subject: [PATCH 108/187] Improve English, thanks Robert --- doc/administration/repository_checks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md index e22b04928cc..f4c5a84fd37 100644 --- a/doc/administration/repository_checks.md +++ b/doc/administration/repository_checks.md @@ -4,7 +4,7 @@ _**Note:** This feature was [introduced][ce-3232] in GitLab 8.7_ --- -Git has a built-in mechanism \[git fsck\]\[git-fsck\] to verify the +Git has a built-in mechanism, \[git fsck\]\[git-fsck\], to verify the integrity of all data commited to a repository. GitLab administrators can trigger such a check for a project via the project page under the admin panel. The checks run asynchronously so it may take a few minutes @@ -12,7 +12,7 @@ before the check result is visible on the project admin page. If the checks failed you can see their output on the admin log page under 'repocheck.log'. -## Periodical checks +## Periodic checks GitLab periodically runs a repository check on all project repositories and wiki repositories in order to detect data corruption problems. A @@ -35,7 +35,7 @@ in repocheck.log (in the admin panel or on disk; see resolved the issue use the admin panel to trigger a new repository check on the project. This will clear the 'check failed' state. -If for some reason the periodical repository check caused a lot of false +If for some reason the periodic repository check caused a lot of false alarms you can choose to clear ALL repository check states from the 'Settings' page of the admin panel. From ca40479c512f327c12adf51b47be46d75e4e333c Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Wed, 13 Apr 2016 11:20:45 +0200 Subject: [PATCH 109/187] API: Avoid group leak while updating the group --- spec/requests/api/groups_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 083d5c459c6..37ddab83c30 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -135,10 +135,10 @@ describe API::API, api: true do end context 'when authenticated as an user that cannot see the group' do - it 'returns 403 when trying to update the group' do + it 'returns 404 when trying to update the group' do put api("/groups/#{group2.id}", user1), name: new_group_name - expect(response.status).to eq(403) + expect(response.status).to eq(404) end end end From 3240ecfbefc7ae5994be6ef01b52c1cbdaa09057 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 12 Apr 2016 12:00:21 +0200 Subject: [PATCH 110/187] Added ability to add custom tags to transactions One use case for this is manually setting the "action" tag for Grape API calls. Due to Grape running blocks there are no human readable method names that can be used for the "action" tag, thus we have to set these manually on a case by case basis. --- CHANGELOG | 1 + lib/gitlab/metrics.rb | 10 ++++++++++ spec/lib/gitlab/metrics_spec.rb | 25 +++++++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 9b0c6ba4609..ac5c10a8a4f 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.7.0 (unreleased) - All service classes (those residing in app/services) are now instrumented (Yorick Peterse) + - Developers can now add custom tags to transactions (Yorick Peterse) - Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea) - Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea) - All images in discussions and wikis now link to their source files !3464 (Connor Shea). diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 2a0a5629be5..484970c5a10 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -104,6 +104,16 @@ module Gitlab retval end + # Adds a tag to the current transaction (if any) + # + # name - The name of the tag to add. + # value - The value of the tag. + def self.tag_transaction(name, value) + trans = current_transaction + + trans.add_tag(name, value) if trans + end + # When enabled this should be set before being used as the usual pattern # "@foo ||= bar" is _not_ thread-safe. if enabled? diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 3dee13e27f4..10177c0e8dd 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -98,4 +98,29 @@ describe Gitlab::Metrics do end end end + + describe '.tag_transaction' do + context 'without a transaction' do + it 'does nothing' do + expect_any_instance_of(Gitlab::Metrics::Transaction). + not_to receive(:add_tag) + + Gitlab::Metrics.tag_transaction(:foo, 'bar') + end + end + + context 'with a transaction' do + let(:transaction) { Gitlab::Metrics::Transaction.new } + + it 'adds the tag to the transaction' do + expect(Gitlab::Metrics).to receive(:current_transaction). + and_return(transaction) + + expect(transaction).to receive(:add_tag). + with(:foo, 'bar') + + Gitlab::Metrics.tag_transaction(:foo, 'bar') + end + end + end end From 482f67edb46423d4fc567e061d6546d8dfafc7bb Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Thu, 7 Apr 2016 14:07:17 +0200 Subject: [PATCH 111/187] API: Ability to move an issue --- CHANGELOG | 1 + doc/api/issues.md | 51 ++++++++++++++++++++++++++++++ lib/api/issues.rb | 23 ++++++++++++++ spec/requests/api/issues_spec.rb | 53 +++++++++++++++++++++++++++++++- 4 files changed, 127 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 9b0c6ba4609..771d7e4799d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ v 8.7.0 (unreleased) - Expose label description in API (Mariusz Jachimowicz) - Allow back dating on issues when created through the API - API: Ability to update a group (Robert Schilling) + - API: Ability to move issues - Fix Error 500 after renaming a project path (Stan Hu) - Fix avatar stretching by providing a cropping feature - API: Expose `subscribed` for issues and merge requests (Robert Schilling) diff --git a/doc/api/issues.md b/doc/api/issues.md index 1c635a6cdcf..a540a27ce11 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -351,6 +351,57 @@ DELETE /projects/:id/issues/:issue_id curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85 ``` +## Move an issue + +Moves an issue to a different project. If the operation is successful, a status code `200` together with moved issue is returned. If the project, issue, or target project is not found, error `404` is returned. If the target project equals the source project or the user has insufficient permissions to move an issue, error `400` together with an explaining error message is returned. + +``` +POST /projects/:id/issues/:issue_id/move +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | +| `new_project_id` | integer | yes | The ID the new project | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move +``` + +Example response: + +```json +{ + "id": 92, + "iid": 11, + "project_id": 5, + "title": "Sit voluptas tempora quisquam aut doloribus et.", + "description": "Repellat voluptas quibusdam voluptatem exercitationem.", + "state": "opened", + "created_at": "2016-04-05T21:41:45.652Z", + "updated_at": "2016-04-07T12:20:17.596Z", + "labels": [], + "milestone": null, + "assignee": { + "name": "Miss Monserrate Beier", + "username": "axel.block", + "id": 12, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/axel.block" + }, + "author": { + "name": "Kris Steuber", + "username": "solon.cremin", + "id": 10, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/solon.cremin" + } +} +``` + ## Comments on issues Comments are done via the [notes](notes.md) resource. diff --git a/lib/api/issues.rb b/lib/api/issues.rb index c4ea05ee6cf..894d9794322 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -195,6 +195,29 @@ module API end end + # Move an existing issue + # + # Parameters: + # id (required) - The ID of a project + # issue_id (required) - The ID of a project issue + # new_project_id (required) - The ID of the new project + # Example Request: + # POST /projects/:id/issues/:issue_id/move + post ":id/issues/:issue_id/move" do + required_attributes! [:new_project_id] + + issue = user_project.issues.find(params[:issue_id]) + new_project = Project.find(params[:new_project_id]) + + begin + issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project) + present issue, with: Entities::Issue + rescue ::Issues::MoveService::MoveError => error + render_api_error!(error.message, 400) + end + end + + # # Delete a project issue # # Parameters: diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 822d3ad3017..db4ee46975a 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -7,7 +7,7 @@ describe API::API, api: true do let(:author) { create(:author) } let(:assignee) { create(:assignee) } let(:admin) { create(:user, :admin) } - let!(:project) { create(:project, :public, namespace: user.namespace ) } + let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } let!(:closed_issue) do create :closed_issue, author: user, @@ -501,4 +501,55 @@ describe API::API, api: true do end end end + + describe '/projects/:id/issues/:issue_id/move' do + let!(:target_project) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace ) } + let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) } + + it 'moves an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + new_project_id: target_project.id + + expect(response.status).to eq(201) + expect(json_response['project_id']).to eq(target_project.id) + end + + it 'returns an error if target and source project are the same' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + new_project_id: project.id + + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Cannot move issue to project it originates from!') + end + + it "returns an error if I don't have the permission" do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + new_project_id: target_project2.id + + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!') + end + + it 'moves the issue to another namespace if I am admin' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", admin), + new_project_id: target_project2.id + + expect(response.status).to eq(201) + expect(json_response['project_id']).to eq(target_project2.id) + end + + it 'returns 404 if the source issue is not found' do + post api("/projects/#{project.id}/issues/123/move", user), + new_project_id: target_project.id + + expect(response.status).to eq(404) + end + + it 'returns 404 if the target project is not found' do + post api("/projects/1234/issues/#{issue.id}/move", user), + new_project_id: target_project.id + + expect(response.status).to eq(404) + end + end end From 244219376d84f0c78db3b898f0b7a18c88f9a840 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 13 Apr 2016 12:28:07 +0300 Subject: [PATCH 112/187] Tie example config to JIRA screenshot Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/15203 --- .../img/jira_service_page.png | Bin 35496 -> 49122 bytes doc/project_services/jira.md | 22 ++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png index 2b37eda35202160a69048732d759435e074f3b82..c225daa81e18d6c7e2ff26e30d7695fe218b2e1e 100644 GIT binary patch literal 49122 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 E2S^%+Z~y=R diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 27170c1eb19..b626c746c79 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -1,9 +1,9 @@ # GitLab JIRA integration -_**Note:** +>**Note:** Full JIRA integration was previously exclusive to GitLab Enterprise Edition. With [GitLab 8.3 forward][8_3_post], this feature in now [backported][jira-ce] -to GitLab Community Edition as well._ +to GitLab Community Edition as well. --- @@ -88,8 +88,9 @@ password as they will be needed when configuring GitLab in the next section. ### Configuring GitLab -_**Note:** The currently supported JIRA versions are v6.x and v7.x. and GitLab -7.8 or higher is required._ +>**Note:** +The currently supported JIRA versions are v6.x and v7.x. and GitLab +7.8 or higher is required. --- @@ -113,13 +114,24 @@ Fill in the required details on the page, as described in the table below. | `Api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https:///rest/api/2`. | | `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). | | `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | -| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot](img/jira_issues_workflow.png)). By default, this ID is set to `2` | +| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | After saving the configuration, your GitLab project will be able to interact with the linked JIRA project. +For example, given the settings below: + +- the JIRA URL is `https://jira.example.com` +- the project is named `GITLAB` +- the user is named `gitlab` +- the JIRA issue transition is 151 (based on the [JIRA issue transition][trans]) + +the following screenshot shows how the JIRA service settings should look like. + ![JIRA service page](img/jira_service_page.png) +[trans]: img/jira_issues_workflow.png + --- ## JIRA issues From 2b036025d619c51cff74e4eb430f50d43d1d8cdb Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Tue, 12 Apr 2016 18:38:18 +0200 Subject: [PATCH 113/187] Update tests for moving issues via API --- CHANGELOG | 2 +- doc/api/issues.md | 8 +++-- lib/api/issues.rb | 14 ++++---- spec/requests/api/issues_spec.rb | 57 +++++++++++++++++++++----------- 4 files changed, 51 insertions(+), 30 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 771d7e4799d..b86c9d1fa39 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,7 +14,7 @@ v 8.7.0 (unreleased) - Expose label description in API (Mariusz Jachimowicz) - Allow back dating on issues when created through the API - API: Ability to update a group (Robert Schilling) - - API: Ability to move issues + - API: Ability to move issues (Robert Schilling) - Fix Error 500 after renaming a project path (Stan Hu) - Fix avatar stretching by providing a cropping feature - API: Expose `subscribed` for issues and merge requests (Robert Schilling) diff --git a/doc/api/issues.md b/doc/api/issues.md index a540a27ce11..a3ac48fba7e 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -353,7 +353,11 @@ curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.c ## Move an issue -Moves an issue to a different project. If the operation is successful, a status code `200` together with moved issue is returned. If the project, issue, or target project is not found, error `404` is returned. If the target project equals the source project or the user has insufficient permissions to move an issue, error `400` together with an explaining error message is returned. +Moves an issue to a different project. If the operation is successful, a status +code `201` together with moved issue is returned. If the project, issue, or +target project is not found, error `404` is returned. If the target project +equals the source project or the user has insufficient permissions to move an +issue, error `400` together with an explaining error message is returned. ``` POST /projects/:id/issues/:issue_id/move @@ -363,7 +367,7 @@ POST /projects/:id/issues/:issue_id/move | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of a project | | `issue_id` | integer | yes | The ID of a project's issue | -| `new_project_id` | integer | yes | The ID the new project | +| `to_project_id` | integer | yes | The ID the new project | ```bash curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 894d9794322..850e99981ff 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -198,20 +198,20 @@ module API # Move an existing issue # # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # new_project_id (required) - The ID of the new project + # id (required) - The ID of a project + # issue_id (required) - The ID of a project issue + # to_project_id (required) - The ID of the new project # Example Request: # POST /projects/:id/issues/:issue_id/move - post ":id/issues/:issue_id/move" do - required_attributes! [:new_project_id] + post ':id/issues/:issue_id/move' do + required_attributes! [:to_project_id] issue = user_project.issues.find(params[:issue_id]) - new_project = Project.find(params[:new_project_id]) + new_project = Project.find(params[:to_project_id]) begin issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project) - present issue, with: Entities::Issue + present issue, with: Entities::Issue, current_user: current_user rescue ::Issues::MoveService::MoveError => error render_api_error!(error.message, 400) end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index db4ee46975a..3d7a31cbb6a 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -508,48 +508,65 @@ describe API::API, api: true do it 'moves an issue' do post api("/projects/#{project.id}/issues/#{issue.id}/move", user), - new_project_id: target_project.id + to_project_id: target_project.id expect(response.status).to eq(201) expect(json_response['project_id']).to eq(target_project.id) end - it 'returns an error if target and source project are the same' do - post api("/projects/#{project.id}/issues/#{issue.id}/move", user), - new_project_id: project.id + context 'when source and target projects are the same' do + it 'returns 400 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: project.id - expect(response.status).to eq(400) - expect(json_response['message']).to eq('Cannot move issue to project it originates from!') + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Cannot move issue to project it originates from!') + end end - it "returns an error if I don't have the permission" do - post api("/projects/#{project.id}/issues/#{issue.id}/move", user), - new_project_id: target_project2.id + context 'when the user does not have the permission to move issues' do + it 'returns 400 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: target_project2.id - expect(response.status).to eq(400) - expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!') + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!') + end end it 'moves the issue to another namespace if I am admin' do post api("/projects/#{project.id}/issues/#{issue.id}/move", admin), - new_project_id: target_project2.id + to_project_id: target_project2.id expect(response.status).to eq(201) expect(json_response['project_id']).to eq(target_project2.id) end - it 'returns 404 if the source issue is not found' do - post api("/projects/#{project.id}/issues/123/move", user), - new_project_id: target_project.id + context 'when issue does not exist' do + it 'returns 404 when trying to move an issue' do + post api("/projects/#{project.id}/issues/123/move", user), + to_project_id: target_project.id - expect(response.status).to eq(404) + expect(response.status).to eq(404) + end end - it 'returns 404 if the target project is not found' do - post api("/projects/1234/issues/#{issue.id}/move", user), - new_project_id: target_project.id + context 'when source project does not exist' do + it 'returns 404 when trying to move an issue' do + post api("/projects/123/issues/#{issue.id}/move", user), + to_project_id: target_project.id - expect(response.status).to eq(404) + expect(response.status).to eq(404) + end + end + + context 'when target project does not exist' do + it 'returns 404 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: 123 + + expect(response.status).to eq(404) + end end end end From da87fa8a9fb2e786a4115d2da3898e0ec83c446c Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 13 Apr 2016 11:50:39 +0200 Subject: [PATCH 114/187] Make check result staleness more obvious --- app/views/admin/projects/show.html.haml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 5bef8e3ad57..1e44172f066 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -10,7 +10,9 @@ .col-md-12 .panel .panel-heading.alert.alert-danger - Last repository check failed. See + Last repository check + = "(#{time_ago_in_words(@project.last_repository_check_at)} ago)" + failed. See = link_to 'repocheck.log', admin_logs_path for error messages. .row From b60b9094d3581e72fa614771456fa9003bee6426 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 13 Apr 2016 11:50:55 +0200 Subject: [PATCH 115/187] Always check the wiki too --- app/workers/single_repository_check_worker.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/workers/single_repository_check_worker.rb b/app/workers/single_repository_check_worker.rb index 6257f382d86..f6c345df8b5 100644 --- a/app/workers/single_repository_check_worker.rb +++ b/app/workers/single_repository_check_worker.rb @@ -14,9 +14,10 @@ class SingleRepositoryCheckWorker private def check(project) - [project.repository, project.wiki.repository].all? do |repository| + # Use 'map do', not 'all? do', to prevent short-circuiting + [project.repository, project.wiki.repository].map do |repository| git_fsck(repository.path_to_repo) - end + end.all? end def git_fsck(path) From fdbf3682023a2ed647c625ec0609dac3227218b2 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Wed, 13 Apr 2016 12:03:05 +0200 Subject: [PATCH 116/187] Fix doc for moving an issue --- doc/api/issues.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/issues.md b/doc/api/issues.md index a3ac48fba7e..f09847aef95 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -367,7 +367,7 @@ POST /projects/:id/issues/:issue_id/move | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of a project | | `issue_id` | integer | yes | The ID of a project's issue | -| `to_project_id` | integer | yes | The ID the new project | +| `to_project_id` | integer | yes | The ID of the new project | ```bash curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move From 4cd04443f5f69665ce1139726751af678e0e55c3 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Wed, 13 Apr 2016 12:10:12 +0200 Subject: [PATCH 117/187] Fix group_member_spec to not leak information --- spec/requests/api/group_members_spec.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb index 3e8b4aa1f88..96d89e69209 100644 --- a/spec/requests/api/group_members_spec.rb +++ b/spec/requests/api/group_members_spec.rb @@ -42,9 +42,10 @@ describe API::API, api: true do end end - it "users not part of the group should get access error" do + it 'users not part of the group should get access error' do get api("/groups/#{group_with_members.id}/members", stranger) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end end @@ -165,12 +166,13 @@ describe API::API, api: true do end end - describe "DELETE /groups/:id/members/:user_id" do - context "when not a member of the group" do + describe 'DELETE /groups/:id/members/:user_id' do + context 'when not a member of the group' do it "should not delete guest's membership of group_with_members" do random_user = create(:user) delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end From 33f786b5d32d02a032af9d258167ddd2bb61d44a Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 13 Apr 2016 13:12:05 +0300 Subject: [PATCH 118/187] clean up ExclusiveLease --- lib/gitlab/exclusive_lease.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index c2260a5f7ac..ffe49364379 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -52,11 +52,6 @@ module Gitlab private - def redis - # Maybe someday we want to use a connection pool... - @redis ||= Redis.new(url: Gitlab::RedisConfig.url) - end - def redis_key "gitlab:exclusive_lease:#{@key}" end From fa864716a80eba26f75a1d69ca3e5183bee96542 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 13 Apr 2016 12:13:53 +0200 Subject: [PATCH 119/187] Use new "clear all" button in tests --- spec/features/admin/admin_projects_spec.rb | 37 +------------- .../admin_uses_repository_checks_spec.rb | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 36 deletions(-) create mode 100644 spec/features/admin/admin_uses_repository_checks_spec.rb diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index 95a230a72c3..101d955d693 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -1,7 +1,6 @@ require 'spec_helper' -require 'rails_helper' -describe "Admin Projects", feature: true do +describe "Admin::Projects", feature: true do before do @project = create(:project) login_as :admin @@ -32,38 +31,4 @@ describe "Admin Projects", feature: true do expect(page).to have_content(@project.name) end end - - feature 'repository checks' do - scenario 'trigger repository check' do - visit_admin_project_page - - page.within('.repository-check') do - click_button 'Trigger repository check' - end - - expect(page).to have_content('Repository check was triggered') - end - - scenario 'see failed repository check' do - @project.update_column(:last_repository_check_failed, true) - visit_admin_project_page - - expect(page).to have_content('Last repository check failed') - end - - scenario 'clear repository checks', js: true do - @project.update_column(:last_repository_check_failed, true) - visit admin_namespaces_projects_path - - page.within('.repository-check-states') do - click_link 'Clear all' # pop-up should be auto confirmed - end - - expect(@project.reload.last_repository_check_failed).to eq(false) - end - end - - def visit_admin_project_page - visit admin_namespace_project_path(@project.namespace, @project) - end end diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb new file mode 100644 index 00000000000..69b82441916 --- /dev/null +++ b/spec/features/admin/admin_uses_repository_checks_spec.rb @@ -0,0 +1,48 @@ +require 'rails_helper' + +feature 'Admin uses repository checks', feature: true do + before do + login_as :admin + end + + scenario 'to trigger a single check' do + project = create(:empty_project) + visit_admin_project_page(project) + + page.within('.repository-check') do + click_button 'Trigger repository check' + end + + expect(page).to have_content('Repository check was triggered') + end + + scenario 'to see a single failed repository check' do + visit_admin_project_page(broken_project) + + page.within('.alert') do + expect(page.text).to match(/Last repository check \(.* ago\) failed/) + end + end + + scenario 'to clear all repository checks', js: true do + project = broken_project + visit admin_application_settings_path + + click_link 'Clear all repository checks' # pop-up should be auto confirmed + + expect(project.reload.last_repository_check_failed).to eq(false) + end + + def visit_admin_project_page(project) + visit admin_namespace_project_path(project.namespace, project) + end + + def broken_project + project = create(:empty_project) + project.update_columns( + last_repository_check_failed: true, + last_repository_check_at: Time.now, + ) + project + end +end From 9a30d3b5aef732e782e9496b2e8ae62069ba521a Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 13 Apr 2016 12:20:43 +0200 Subject: [PATCH 120/187] Remove \f. WT\F ?? --- doc/administration/repository_checks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md index f4c5a84fd37..16f4627a596 100644 --- a/doc/administration/repository_checks.md +++ b/doc/administration/repository_checks.md @@ -28,7 +28,7 @@ panel. ## What to do if a check failed -If the repository check fails for some repository you should look up the error +If the repository check fails for some repository you should look up the error in repocheck.log (in the admin panel or on disk; see `/var/log/gitlab/gitlab-rails` for Omnibus installations or `/home/git/gitlab/log` for installations from source). Once you have From bd0be13f5b52b8eaee019d722980b29acbc55b05 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Fri, 8 Apr 2016 14:17:42 +0200 Subject: [PATCH 121/187] API: Ability to subscribe and unsubscribe from an issue --- CHANGELOG | 1 + doc/api/issues.md | 108 +++++++++++++++++++++++++++++++ lib/api/helpers.rb | 4 ++ lib/api/issues.rb | 53 +++++++++++++++ spec/requests/api/issues_spec.rb | 31 +++++++++ 5 files changed, 197 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index c633c4bb35f..01fffcc3696 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ v 8.7.0 (unreleased) - Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524 - Improved Markdown rendering performance !3389 (Yorick Peterse) - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu) + - API: Ability to subscribe and unsubscribe from an issue (Robert Schilling) - Expose project badges in project settings - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) diff --git a/doc/api/issues.md b/doc/api/issues.md index f09847aef95..4f7d948eba1 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -406,6 +406,114 @@ Example response: } ``` +## Subscribe to an issue + +Subscribes to an issue to receive notifications. If the operation is successful, +status code `201` together with the updated issue is returned. If the user is +already subscribed to the issue, the status code `304` is returned. If the +project or issue is not found, status code `404` is returned. + +``` +POST /projects/:id/issues/:issue_id/subscribe +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscribe +``` + +Example response: + +```json +{ + "id": 92, + "iid": 11, + "project_id": 5, + "title": "Sit voluptas tempora quisquam aut doloribus et.", + "description": "Repellat voluptas quibusdam voluptatem exercitationem.", + "state": "opened", + "created_at": "2016-04-05T21:41:45.652Z", + "updated_at": "2016-04-07T12:20:17.596Z", + "labels": [], + "milestone": null, + "assignee": { + "name": "Miss Monserrate Beier", + "username": "axel.block", + "id": 12, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/axel.block" + }, + "author": { + "name": "Kris Steuber", + "username": "solon.cremin", + "id": 10, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/solon.cremin" + } +} +``` + +## Unsubscribe from an issue + +Unsubscribes from an issue to not receive notifications from that issue. If the +operation is successful, status code `201` together with the updated issue is +returned. If the user is not subscribed to the issue, the status code `304` +is returned. If the project or issue is not found, status code `404` is +returned. + +``` +POST /projects/:id/issues/:issue_id/unsubscribe +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/unsubscribe +``` + +Example response: + +```json +{ + "id": 93, + "iid": 12, + "project_id": 5, + "title": "Incidunt et rerum ea expedita iure quibusdam.", + "description": "Et cumque architecto sed aut ipsam.", + "state": "opened", + "created_at": "2016-04-05T21:41:45.217Z", + "updated_at": "2016-04-07T13:02:37.905Z", + "labels": [], + "milestone": null, + "assignee": { + "name": "Edwardo Grady", + "username": "keyon", + "id": 21, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/keyon" + }, + "author": { + "name": "Vivian Hermann", + "username": "orville", + "id": 11, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", + "web_url": "http://lgitlab.example.com/u/orville" + }, + "subscribed": false +} +``` + ## Comments on issues Comments are done via the [notes](notes.md) resource. diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 4921ae99e78..aa0597564ed 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -241,6 +241,10 @@ module API render_api_error!('413 Request Entity Too Large', 413) end + def not_modified! + render_api_error!('304 Not modified', 304) + end + def render_validation_error!(model) if model.errors.any? render_api_error!(model.errors.messages || '400 Bad Request', 400) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 850e99981ff..49911fa2988 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -231,6 +231,59 @@ module API authorize!(:destroy_issue, issue) issue.destroy end + + # Subscribes to a project issue + # + # Parameters: + # id (required) - The ID of a project + # issue_id (required) - The ID of a project issue + # Example Request: + # POST /projects/:id/issues/:issue_id + post ":id/issues/:issue_idsubscribe" do + issue = user_project.issues.find_by(id: params[:issue_id]) + + if !issue.subscribed?(current_user) + present issue, with: Entities::Issue, current_user: current_user + else + not_modified! + end + end + + # Subscribes to a project issue + # + # Parameters: + # id (required) - The ID of a project + # issue_id (required) - The ID of a project issue + # Example Request: + # POST /projects/:id/issues/:issue_id/subscribe + post ":id/issues/:issue_id/subscribe" do + issue = user_project.issues.find_by(id: params[:issue_id]) + + if !issue.subscribed?(current_user) + issue.toggle_subscription(current_user) + present issue, with: Entities::Issue, current_user: current_user + else + not_modified! + end + end + + # Unsubscribes from a project issue + # + # Parameters: + # id (required) - The ID of a project + # issue_id (required) - The ID of a project issue + # Example Request: + # POST /projects/:id/issues/:issue_id/unsubscribe + post ":id/issues/:issue_id/unsubscribe" do + issue = user_project.issues.find_by(id: params[:issue_id]) + + if issue.subscribed?(current_user) + issue.unsubscribe(current_user) + present issue, with: Entities::Issue, current_user: current_user + else + not_modified! + end + end end end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 3d7a31cbb6a..437caf466b0 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } + let(:user2) { create(:user) } let(:non_member) { create(:user) } let(:author) { create(:author) } let(:assignee) { create(:assignee) } @@ -569,4 +570,34 @@ describe API::API, api: true do end end end + + describe 'POST :id/issues/:issue_id/subscribe' do + it 'subscribes to an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user2) + + expect(response.status).to eq(201) + expect(json_response['subscribed']).to eq(true) + end + + it 'returns 304 if already subscribed' do + post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user) + + expect(response.status).to eq(304) + end + end + + describe 'POST :id/issues/:issue_id/unsubscribe' do + it 'unsubscribes from an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user) + + expect(response.status).to eq(201) + expect(json_response['subscribed']).to eq(false) + end + + it 'returns 304 if not subscribed' do + post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user2) + + expect(response.status).to eq(304) + end + end end From f875189b3962bde6e4b7b8c4ffdd18af83cbc922 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Fri, 8 Apr 2016 15:03:34 +0200 Subject: [PATCH 122/187] API: Ability to subscribe and unsubscribe from a merge request --- CHANGELOG | 2 +- doc/api/merge_requests.md | 147 +++++++++++++++++++++++ lib/api/issues.rb | 4 +- lib/api/merge_requests.rb | 36 ++++++ spec/requests/api/merge_requests_spec.rb | 30 +++++ 5 files changed, 216 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 01fffcc3696..1184ffce76c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,7 +11,7 @@ v 8.7.0 (unreleased) - Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524 - Improved Markdown rendering performance !3389 (Yorick Peterse) - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu) - - API: Ability to subscribe and unsubscribe from an issue (Robert Schilling) + - API: Ability to subscribe and unsubscribe from issues and merge requests (Robert Schilling) - Expose project badges in project settings - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 20db73ea6c0..16cb8926265 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -606,3 +606,150 @@ Example response: }, ] ``` + +## Subscribe to a merge request + +Subscribes to a merge request to receive notification. If the operation is +successful, status code `201` together with the updated merge request is +returned. If the user is already subscribed to the merge request, the status +code `304` is returned. If the project or merge request is not found, status +code `404` is returned. + +``` +POST /projects/:id/merge_requests/:merge_request_id/subscribe +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of the merge request | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscribe +``` + +Example response: + +```json +{ + "id": 17, + "iid": 1, + "project_id": 5, + "title": "Et et sequi est impedit nulla ut rem et voluptatem.", + "description": "Consequatur velit eos rerum optio autem. Quia id officia quaerat dolorum optio. Illo laudantium aut ipsum dolorem.", + "state": "opened", + "created_at": "2016-04-05T21:42:23.233Z", + "updated_at": "2016-04-05T22:11:52.900Z", + "target_branch": "ui-dev-kit", + "source_branch": "version-1-9", + "upvotes": 0, + "downvotes": 0, + "author": { + "name": "Eileen Skiles", + "username": "leila", + "id": 19, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/leila" + }, + "assignee": { + "name": "Celine Wehner", + "username": "carli", + "id": 16, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/carli" + }, + "source_project_id": 5, + "target_project_id": 5, + "labels": [], + "work_in_progress": false, + "milestone": { + "id": 7, + "iid": 1, + "project_id": 5, + "title": "v2.0", + "description": "Corrupti eveniet et velit occaecati dolorem est rerum aut.", + "state": "closed", + "created_at": "2016-04-05T21:41:40.905Z", + "updated_at": "2016-04-05T21:41:40.905Z", + "due_date": null + }, + "merge_when_build_succeeds": false, + "merge_status": "cannot_be_merged", + "subscribed": true +} +``` +## Unsubscribe from a merge request + +Unsubscribes from a merge request to not receive notifications from that merge +request. If the operation is successful, status code `201` together with the +updated merge request is returned. If the user is not subscribed to the merge +request, the status code `304` is returned. If the project or merge request is +not found, status code `404` is returned. + +``` +POST /projects/:id/merge_requests/:merge_request_id/unsubscribe +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of the merge request | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscribe +``` + +Example response: + +```json +{ + "id": 17, + "iid": 1, + "project_id": 5, + "title": "Et et sequi est impedit nulla ut rem et voluptatem.", + "description": "Consequatur velit eos rerum optio autem. Quia id officia quaerat dolorum optio. Illo laudantium aut ipsum dolorem.", + "state": "opened", + "created_at": "2016-04-05T21:42:23.233Z", + "updated_at": "2016-04-05T22:11:52.900Z", + "target_branch": "ui-dev-kit", + "source_branch": "version-1-9", + "upvotes": 0, + "downvotes": 0, + "author": { + "name": "Eileen Skiles", + "username": "leila", + "id": 19, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/leila" + }, + "assignee": { + "name": "Celine Wehner", + "username": "carli", + "id": 16, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/carli" + }, + "source_project_id": 5, + "target_project_id": 5, + "labels": [], + "work_in_progress": false, + "milestone": { + "id": 7, + "iid": 1, + "project_id": 5, + "title": "v2.0", + "description": "Corrupti eveniet et velit occaecati dolorem est rerum aut.", + "state": "closed", + "created_at": "2016-04-05T21:41:40.905Z", + "updated_at": "2016-04-05T21:41:40.905Z", + "due_date": null + }, + "merge_when_build_succeeds": false, + "merge_status": "cannot_be_merged", + "subscribed": false +} +``` diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 49911fa2988..049618c00f7 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -235,7 +235,7 @@ module API # Subscribes to a project issue # # Parameters: - # id (required) - The ID of a project + # id (required) - The ID of a project # issue_id (required) - The ID of a project issue # Example Request: # POST /projects/:id/issues/:issue_id @@ -270,7 +270,7 @@ module API # Unsubscribes from a project issue # # Parameters: - # id (required) - The ID of a project + # id (required) - The ID of a project # issue_id (required) - The ID of a project issue # Example Request: # POST /projects/:id/issues/:issue_id/unsubscribe diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 4e7de8867b4..d166484ba54 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -327,6 +327,42 @@ module API issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) present paginate(issues), with: Entities::Issue, current_user: current_user end + + # Subscribes to a merge request + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - The ID of a merge request + # Example Request: + # POST /projects/:id/issues/:merge_request_id/subscribe + post "#{path}/subscribe" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + + if !merge_request.subscribed?(current_user) + merge_request.toggle_subscription(current_user) + present merge_request, with: Entities::MergeRequest, current_user: current_user + else + not_modified! + end + end + + # Unsubscribes from a merge request + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - The ID of a merge request + # Example Request: + # POST /projects/:id/merge_requests/:merge_request_id/unsubscribe + post "#{path}/unsubscribe" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + + if merge_request.subscribed?(current_user) + merge_request.unsubscribe(current_user) + present merge_request, with: Entities::MergeRequest, current_user: current_user + else + not_modified! + end + end end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 25fa30b2f21..b71c72a3829 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -516,6 +516,36 @@ describe API::API, api: true do end end + describe 'POST :id/merge_requests/:merge_request_id/subscribe' do + it 'subscribes to a merge request' do + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", admin) + + expect(response.status).to eq(201) + expect(json_response['subscribed']).to eq(true) + end + + it 'returns 304 if already subscribed' do + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user) + + expect(response.status).to eq(304) + end + end + + describe 'POST :id/merge_requests/:merge_request_id/unsubscribe' do + it 'unsubscribes from a merge request' do + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user) + + expect(response.status).to eq(201) + expect(json_response['subscribed']).to eq(false) + end + + it 'returns 304 if not subscribed' do + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", admin) + + expect(response.status).to eq(304) + end + end + def mr_with_later_created_and_updated_at_time merge_request merge_request.created_at += 1.hour From fa3009095fbb995550a20e5d0cbc994f4290fbea Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Tue, 12 Apr 2016 14:46:59 +0200 Subject: [PATCH 123/187] Make subscription API more RESTful --- doc/api/issues.md | 27 +++++++++++----------- doc/api/merge_requests.md | 28 +++++++++++------------ lib/api/helpers.rb | 2 +- lib/api/issues.rb | 29 +++++------------------- lib/api/merge_requests.rb | 14 ++++++------ spec/requests/api/issues_spec.rb | 26 +++++++++++++++------ spec/requests/api/merge_requests_spec.rb | 14 ++++++------ 7 files changed, 68 insertions(+), 72 deletions(-) diff --git a/doc/api/issues.md b/doc/api/issues.md index 4f7d948eba1..42024becc36 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -408,13 +408,14 @@ Example response: ## Subscribe to an issue -Subscribes to an issue to receive notifications. If the operation is successful, -status code `201` together with the updated issue is returned. If the user is -already subscribed to the issue, the status code `304` is returned. If the -project or issue is not found, status code `404` is returned. +Subscribes the authenticated user to an issue to receive notifications. If the +operation is successful, status code `201` together with the updated issue is +returned. If the user is already subscribed to the issue, the status code `304` +is returned. If the project or issue is not found, status code `404` is +returned. ``` -POST /projects/:id/issues/:issue_id/subscribe +POST /projects/:id/issues/:issue_id/subscription ``` | Attribute | Type | Required | Description | @@ -423,7 +424,7 @@ POST /projects/:id/issues/:issue_id/subscribe | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscribe +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription ``` Example response: @@ -461,14 +462,14 @@ Example response: ## Unsubscribe from an issue -Unsubscribes from an issue to not receive notifications from that issue. If the -operation is successful, status code `201` together with the updated issue is -returned. If the user is not subscribed to the issue, the status code `304` -is returned. If the project or issue is not found, status code `404` is -returned. +Unsubscribes the authenticated user from the issue to not receive notifications +from it. If the operation is successful, status code `200` together with the +updated issue is returned. If the user is not subscribed to the issue, the +status code `304` is returned. If the project or issue is not found, status code +`404` is returned. ``` -POST /projects/:id/issues/:issue_id/unsubscribe +DELETE /projects/:id/issues/:issue_id/subscription ``` | Attribute | Type | Required | Description | @@ -477,7 +478,7 @@ POST /projects/:id/issues/:issue_id/unsubscribe | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/unsubscribe +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription ``` Example response: diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 16cb8926265..3c18ebfa31e 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -609,14 +609,14 @@ Example response: ## Subscribe to a merge request -Subscribes to a merge request to receive notification. If the operation is -successful, status code `201` together with the updated merge request is -returned. If the user is already subscribed to the merge request, the status -code `304` is returned. If the project or merge request is not found, status -code `404` is returned. +Subscribes the authenticated user to a merge request to receive notification. If +the operation is successful, status code `201` together with the updated merge +request is returned. If the user is already subscribed to the merge request, the +status code `304` is returned. If the project or merge request is not found, +status code `404` is returned. ``` -POST /projects/:id/merge_requests/:merge_request_id/subscribe +POST /projects/:id/merge_requests/:merge_request_id/subscription ``` | Attribute | Type | Required | Description | @@ -625,7 +625,7 @@ POST /projects/:id/merge_requests/:merge_request_id/subscribe | `merge_request_id` | integer | yes | The ID of the merge request | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscribe +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription ``` Example response: @@ -682,14 +682,14 @@ Example response: ``` ## Unsubscribe from a merge request -Unsubscribes from a merge request to not receive notifications from that merge -request. If the operation is successful, status code `201` together with the -updated merge request is returned. If the user is not subscribed to the merge -request, the status code `304` is returned. If the project or merge request is -not found, status code `404` is returned. +Unsubscribes the authenticated user from a merge request to not receive +notifications from that merge request. If the operation is successful, status +code `200` together with the updated merge request is returned. If the user is +not subscribed to the merge request, the status code `304` is returned. If the +project or merge request is not found, status code `404` is returned. ``` -POST /projects/:id/merge_requests/:merge_request_id/unsubscribe +DELETE /projects/:id/merge_requests/:merge_request_id/subscription ``` | Attribute | Type | Required | Description | @@ -698,7 +698,7 @@ POST /projects/:id/merge_requests/:merge_request_id/unsubscribe | `merge_request_id` | integer | yes | The ID of the merge request | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscribe +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription ``` Example response: diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index aa0597564ed..54452f763a6 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -242,7 +242,7 @@ module API end def not_modified! - render_api_error!('304 Not modified', 304) + render_api_error!('304 Not Modified', 304) end def render_validation_error!(model) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 049618c00f7..37d25073074 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -238,32 +238,15 @@ module API # id (required) - The ID of a project # issue_id (required) - The ID of a project issue # Example Request: - # POST /projects/:id/issues/:issue_id - post ":id/issues/:issue_idsubscribe" do + # POST /projects/:id/issues/:issue_id/subscription + post ":id/issues/:issue_id/subscription" do issue = user_project.issues.find_by(id: params[:issue_id]) - if !issue.subscribed?(current_user) - present issue, with: Entities::Issue, current_user: current_user - else + if issue.subscribed?(current_user) not_modified! - end - end - - # Subscribes to a project issue - # - # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # Example Request: - # POST /projects/:id/issues/:issue_id/subscribe - post ":id/issues/:issue_id/subscribe" do - issue = user_project.issues.find_by(id: params[:issue_id]) - - if !issue.subscribed?(current_user) + else issue.toggle_subscription(current_user) present issue, with: Entities::Issue, current_user: current_user - else - not_modified! end end @@ -273,8 +256,8 @@ module API # id (required) - The ID of a project # issue_id (required) - The ID of a project issue # Example Request: - # POST /projects/:id/issues/:issue_id/unsubscribe - post ":id/issues/:issue_id/unsubscribe" do + # DELETE /projects/:id/issues/:issue_id/subscription + delete ":id/issues/:issue_id/subscription" do issue = user_project.issues.find_by(id: params[:issue_id]) if issue.subscribed?(current_user) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index d166484ba54..7e78609ecb9 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -334,15 +334,15 @@ module API # id (required) - The ID of a project # merge_request_id (required) - The ID of a merge request # Example Request: - # POST /projects/:id/issues/:merge_request_id/subscribe - post "#{path}/subscribe" do + # POST /projects/:id/issues/:merge_request_id/subscription + post "#{path}/subscription" do merge_request = user_project.merge_requests.find(params[:merge_request_id]) - if !merge_request.subscribed?(current_user) + if merge_request.subscribed?(current_user) + not_modified! + else merge_request.toggle_subscription(current_user) present merge_request, with: Entities::MergeRequest, current_user: current_user - else - not_modified! end end @@ -352,8 +352,8 @@ module API # id (required) - The ID of a project # merge_request_id (required) - The ID of a merge request # Example Request: - # POST /projects/:id/merge_requests/:merge_request_id/unsubscribe - post "#{path}/unsubscribe" do + # DELETE /projects/:id/merge_requests/:merge_request_id/subscription + delete "#{path}/subscription" do merge_request = user_project.merge_requests.find(params[:merge_request_id]) if merge_request.subscribed?(current_user) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 437caf466b0..8361a1649e0 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -571,33 +571,45 @@ describe API::API, api: true do end end - describe 'POST :id/issues/:issue_id/subscribe' do + describe 'POST :id/issues/:issue_id/subscription' do it 'subscribes to an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user2) + post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) expect(response.status).to eq(201) expect(json_response['subscribed']).to eq(true) end it 'returns 304 if already subscribed' do - post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user) + post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) expect(response.status).to eq(304) end + + it 'returns 404 if the issue is not found' do + post api("/projects/#{project.id}/issues/123/subscription", user) + + expect(response.status).to eq(404) + end end - describe 'POST :id/issues/:issue_id/unsubscribe' do + describe 'DELETE :id/issues/:issue_id/subscription' do it 'unsubscribes from an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user) + post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) - expect(response.status).to eq(201) + expect(response.status).to eq(200) expect(json_response['subscribed']).to eq(false) end it 'returns 304 if not subscribed' do - post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user2) + post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) expect(response.status).to eq(304) end + + it 'returns 404 if the issue is not found' do + post api("/projects/#{project.id}/issues/123/subscription", user) + + expect(response.status).to eq(404) + end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index b71c72a3829..c247fcf9c96 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -516,31 +516,31 @@ describe API::API, api: true do end end - describe 'POST :id/merge_requests/:merge_request_id/subscribe' do + describe 'POST :id/merge_requests/:merge_request_id/subscription' do it 'subscribes to a merge request' do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", admin) + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) expect(response.status).to eq(201) expect(json_response['subscribed']).to eq(true) end it 'returns 304 if already subscribed' do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user) + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) expect(response.status).to eq(304) end end - describe 'POST :id/merge_requests/:merge_request_id/unsubscribe' do + describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do it 'unsubscribes from a merge request' do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user) + delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) - expect(response.status).to eq(201) + expect(response.status).to eq(200) expect(json_response['subscribed']).to eq(false) end it 'returns 304 if not subscribed' do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", admin) + delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) expect(response.status).to eq(304) end From ea2193aaeb1127746dc78d2dda7037d998911662 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Wed, 6 Apr 2016 15:52:16 +0200 Subject: [PATCH 124/187] API: Star and unstar a project --- CHANGELOG | 1 + doc/api/README.md | 1 + doc/api/projects.md | 127 +++++++++++++++++++++++++++++ lib/api/helpers.rb | 4 + lib/api/projects.rb | 33 ++++++++ spec/requests/api/projects_spec.rb | 50 ++++++++++++ 6 files changed, 216 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index c633c4bb35f..7a2af5a0eb8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 8.7.0 (unreleased) - Add links to CI setup documentation from project settings and builds pages - Handle nil descriptions in Slack issue messages (Stan Hu) - API: Expose open_issues_count, closed_issues_count, open_merge_requests_count for labels (Robert Schilling) + - API: Ability to star and unstar a project (Robert Schilling) - Add default scope to projects to exclude projects pending deletion - Allow to close merge requests which source projects(forks) are deleted. - Ensure empty recipients are rejected in BuildsEmailService diff --git a/doc/api/README.md b/doc/api/README.md index 7629ef294ac..3a8fa6cebd1 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -108,6 +108,7 @@ The following table shows the possible return codes for API requests. | ------------- | ----------- | | `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. | | `201 Created` | The `POST` request was successful and the resource is returned as JSON. | +| `304 Not Modified` | Indicates that the resource has not been modified since the last request. | | `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. | | `401 Unauthorized` | The user is not authenticated, a valid [user token](#authentication) is necessary. | | `403 Forbidden` | The request is not allowed, e.g., the user is not allowed to delete a project. | diff --git a/doc/api/projects.md b/doc/api/projects.md index ab716c229dc..c5ffc5514c7 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -491,6 +491,133 @@ Parameters: - `id` (required) - The ID of the project to be forked +### Star a project + +Stars a given project. Returns status code 201 and the project on success and +304 if the project is already starred. + +``` +POST /projects/:id/star +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" +``` + +Example response: + +```json +{ + "id": 3, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 10, + "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site", + "tag_list": [ + "example", + "disapora project" + ], + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "issues_enabled": true, + "open_issues_count": 1, + "merge_requests_enabled": true, + "builds_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-09-30T13: 46: 02Z", + "last_activity_at": "2013-09-30T13: 46: 02Z", + "creator_id": 3, + "namespace": { + "created_at": "2013-09-30T13: 46: 02Z", + "description": "", + "id": 3, + "name": "Diaspora", + "owner_id": 1, + "path": "diaspora", + "updated_at": "2013-09-30T13: 46: 02Z" + }, + "archived": true, + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 1 +} +``` + +### Unstar a project + +Unstars a given project. Returns status code 201 and the project on success +and 304 if the project is already unstarred. + +``` +POST /projects/:id/unstar +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unstar" +``` + +Example response: + +```json +{ + "id": 3, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 10, + "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site", + "tag_list": [ + "example", + "disapora project" + ], + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "issues_enabled": true, + "open_issues_count": 1, + "merge_requests_enabled": true, + "builds_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-09-30T13: 46: 02Z", + "last_activity_at": "2013-09-30T13: 46: 02Z", + "creator_id": 3, + "namespace": { + "created_at": "2013-09-30T13: 46: 02Z", + "description": "", + "id": 3, + "name": "Diaspora", + "owner_id": 1, + "path": "diaspora", + "updated_at": "2013-09-30T13: 46: 02Z" + }, + "archived": true, + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 0 +} +``` + + ### Archive a project Archives the project if the user is either admin or the project owner of this project. This action is diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 4921ae99e78..aa0597564ed 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -241,6 +241,10 @@ module API render_api_error!('413 Request Entity Too Large', 413) end + def not_modified! + render_api_error!('304 Not modified', 304) + end + def render_validation_error!(model) if model.errors.any? render_api_error!(model.errors.messages || '400 Bad Request', 400) diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 24b31005475..ebcf7a4eedd 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -272,6 +272,39 @@ module API present user_project, with: Entities::Project end + # Star project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # POST /projects/:id/star + post ':id/star' do + if !current_user.starred?(user_project) + current_user.toggle_star(user_project) + user_project.reload + present user_project, with: Entities::Project + + else + not_modified! + end + end + + # Unstar project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # POST /projects/:id/unstar + post ':id/unstar' do + if current_user.starred?(user_project) + current_user.toggle_star(user_project) + user_project.reload + present user_project, with: Entities::Project + else + not_modified! + end + end + # Remove project # # Parameters: diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index be2034e0f39..f05622f77fe 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1020,6 +1020,56 @@ describe API::API, api: true do end end + describe 'POST /projects/:id/star' do + context 'on an unstarred project' do + it 'stars the project' do + post api("/projects/#{project.id}/star", user) + + expect(response.status).to eq(201) + expect(json_response['star_count']).to eq(1) + end + end + + context 'on a starred project' do + before do + user.toggle_star(project) + project.reload + end + + it 'does not modify the star count' do + post api("/projects/#{project.id}/star", user) + + expect(response.status).to eq(304) + expect(project.star_count).to eq(1) + end + end + end + + describe 'POST /projects/:id/unstar' do + context 'on a starred project' do + before do + user.toggle_star(project) + project.reload + end + + it 'unstars the project' do + post api("/projects/#{project.id}/unstar", user) + + expect(response.status).to eq(201) + expect(json_response['star_count']).to eq(0) + end + end + + context 'on an unstarred project' do + it 'does not modify the star count' do + post api("/projects/#{project.id}/unstar", user) + + expect(response.status).to eq(304) + expect(project.star_count).to eq(0) + end + end + end + describe 'DELETE /projects/:id' do context 'when authenticated as user' do it 'should remove project' do From 3ab9ea8dae1edc6ab8c8563843342890736eb24c Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Tue, 12 Apr 2016 18:52:43 +0200 Subject: [PATCH 125/187] Make staring API more restful --- doc/api/projects.md | 12 ++++++------ lib/api/projects.rb | 11 +++++------ spec/requests/api/projects_spec.rb | 8 ++++---- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/doc/api/projects.md b/doc/api/projects.md index c5ffc5514c7..b25c9080080 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -493,8 +493,8 @@ Parameters: ### Star a project -Stars a given project. Returns status code 201 and the project on success and -304 if the project is already starred. +Stars a given project. Returns status code `201` and the project on success and +`304` if the project is already starred. ``` POST /projects/:id/star @@ -556,11 +556,11 @@ Example response: ### Unstar a project -Unstars a given project. Returns status code 201 and the project on success -and 304 if the project is already unstarred. +Unstars a given project. Returns status code `200` and the project on success +and `304` if the project is not starred. ``` -POST /projects/:id/unstar +DELETE /projects/:id/star ``` | Attribute | Type | Required | Description | @@ -568,7 +568,7 @@ POST /projects/:id/unstar | `id` | integer | yes | The ID of the project | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unstar" +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" ``` Example response: diff --git a/lib/api/projects.rb b/lib/api/projects.rb index ebcf7a4eedd..c7fdfbfe57b 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -279,13 +279,12 @@ module API # Example Request: # POST /projects/:id/star post ':id/star' do - if !current_user.starred?(user_project) + if current_user.starred?(user_project) + not_modified! + else current_user.toggle_star(user_project) user_project.reload present user_project, with: Entities::Project - - else - not_modified! end end @@ -294,8 +293,8 @@ module API # Parameters: # id (required) - The ID of a project # Example Request: - # POST /projects/:id/unstar - post ':id/unstar' do + # DELETE /projects/:id/unstar + delete ':id/star' do if current_user.starred?(user_project) current_user.toggle_star(user_project) user_project.reload diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index f05622f77fe..2a7c55fe65e 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1045,7 +1045,7 @@ describe API::API, api: true do end end - describe 'POST /projects/:id/unstar' do + describe 'DELETE /projects/:id/star' do context 'on a starred project' do before do user.toggle_star(project) @@ -1053,16 +1053,16 @@ describe API::API, api: true do end it 'unstars the project' do - post api("/projects/#{project.id}/unstar", user) + delete api("/projects/#{project.id}/star", user) - expect(response.status).to eq(201) + expect(response.status).to eq(200) expect(json_response['star_count']).to eq(0) end end context 'on an unstarred project' do it 'does not modify the star count' do - post api("/projects/#{project.id}/unstar", user) + delete api("/projects/#{project.id}/star", user) expect(response.status).to eq(304) expect(project.star_count).to eq(0) From 3b9edce803c91b5f51675291fdf22f1159cea456 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 13 Apr 2016 15:19:50 +0200 Subject: [PATCH 126/187] Instrument the HousekeepingService class This allows us to track how much time is spent in updating the "pushes_since_gc" column as well as the time needed to obtain the lease. --- CHANGELOG | 1 + app/services/projects/housekeeping_service.rb | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5399e3e5b8b..ec8db471572 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.7.0 (unreleased) + - The Projects::HousekeepingService class has extra instrumentation (Yorick Peterse) - All service classes (those residing in app/services) are now instrumented (Yorick Peterse) - Developers can now add custom tags to transactions (Yorick Peterse) - Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea) diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index a0973c5d260..3b7c36f0908 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -26,7 +26,9 @@ module Projects GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace) ensure - @project.update_column(:pushes_since_gc, 0) + Gitlab::Metrics.measure(:reset_pushes_since_gc) do + @project.update_column(:pushes_since_gc, 0) + end end def needed? @@ -34,14 +36,18 @@ module Projects end def increment! - @project.increment!(:pushes_since_gc) + Gitlab::Metrics.measure(:increment_pushes_since_gc) do + @project.increment!(:pushes_since_gc) + end end private def try_obtain_lease - lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT) - lease.try_obtain + Gitlab::Metrics.measure(:obtain_housekeeping_lease) do + lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT) + lease.try_obtain + end end end end From 0f602be99f99f1ae493478a8a28df2907cfa0082 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 13 Apr 2016 15:56:05 +0200 Subject: [PATCH 127/187] Clear repository check columns asynchronously --- .../admin/application_settings_controller.rb | 8 +-- app/controllers/admin/projects_controller.rb | 2 +- app/workers/repository_check/batch_worker.rb | 63 +++++++++++++++++++ app/workers/repository_check/clear_worker.rb | 17 +++++ .../single_repository_worker.rb | 36 +++++++++++ app/workers/repository_check_worker.rb | 61 ------------------ app/workers/single_repository_check_worker.rb | 34 ---------- config/initializers/1_settings.rb | 2 +- .../admin_uses_repository_checks_spec.rb | 27 ++++---- .../batch_worker_spec.rb} | 4 +- .../repository_check/clear_worker_spec.rb | 17 +++++ 11 files changed, 150 insertions(+), 121 deletions(-) create mode 100644 app/workers/repository_check/batch_worker.rb create mode 100644 app/workers/repository_check/clear_worker.rb create mode 100644 app/workers/repository_check/single_repository_worker.rb delete mode 100644 app/workers/repository_check_worker.rb delete mode 100644 app/workers/single_repository_check_worker.rb rename spec/workers/{repository_check_worker_spec.rb => repository_check/batch_worker_spec.rb} (94%) create mode 100644 spec/workers/repository_check/clear_worker_spec.rb diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index a54864480a2..b4a28b8dd3f 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -20,18 +20,14 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end def clear_repository_check_states - Project.update_all( - last_repository_check_failed: false, - last_repository_check_at: nil - ) + RepositoryCheck::ClearWorker.perform_async redirect_to( admin_application_settings_path, - notice: 'All repository check states were cleared.' + notice: 'Started asynchronous removal of all repository check states.' ) end - private def set_application_setting diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 6854e57b650..87986fdf8b1 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -32,7 +32,7 @@ class Admin::ProjectsController < Admin::ApplicationController end def repository_check - SingleRepositoryCheckWorker.perform_async(@project.id) + RepositoryCheck::SingleRepositoryWorker.perform_async(@project.id) redirect_to( admin_namespace_project_path(@project.namespace, @project), diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb new file mode 100644 index 00000000000..16cd77a9bf0 --- /dev/null +++ b/app/workers/repository_check/batch_worker.rb @@ -0,0 +1,63 @@ +module RepositoryCheck + class BatchWorker + include Sidekiq::Worker + + RUN_TIME = 3600 + + sidekiq_options retry: false + + def perform + start = Time.now + + # This loop will break after a little more than one hour ('a little + # more' because `git fsck` may take a few minutes), or if it runs out of + # projects to check. By default sidekiq-cron will start a new + # RepositoryCheckWorker each hour so that as long as there are repositories to + # check, only one (or two) will be checked at a time. + project_ids.each do |project_id| + break if Time.now - start >= RUN_TIME + break unless current_settings.repository_checks_enabled + + next unless try_obtain_lease(project_id) + + SingleRepositoryWorker.new.perform(project_id) + end + end + + private + + # Project.find_each does not support WHERE clauses and + # Project.find_in_batches does not support ordering. So we just build an + # array of ID's. This is OK because we do it only once an hour, because + # getting ID's from Postgres is not terribly slow, and because no user + # has to sit and wait for this query to finish. + def project_ids + limit = 10_000 + never_checked_projects = Project.where('last_repository_check_at IS NULL').limit(limit). + pluck(:id) + old_check_projects = Project.where('last_repository_check_at < ?', 1.week.ago). + reorder('last_repository_check_at ASC').limit(limit).pluck(:id) + never_checked_projects + old_check_projects + end + + def try_obtain_lease(id) + # Use a 24-hour timeout because on servers/projects where 'git fsck' is + # super slow we definitely do not want to run it twice in parallel. + Gitlab::ExclusiveLease.new( + "project_repository_check:#{id}", + timeout: 24.hours + ).try_obtain + end + + def current_settings + # No caching of the settings! If we cache them and an admin disables + # this feature, an active RepositoryCheckWorker would keep going for up + # to 1 hour after the feature was disabled. + if Rails.env.test? + Gitlab::CurrentSettings.fake_application_settings + else + ApplicationSetting.current + end + end + end +end diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb new file mode 100644 index 00000000000..fe0cce9aab7 --- /dev/null +++ b/app/workers/repository_check/clear_worker.rb @@ -0,0 +1,17 @@ +module RepositoryCheck + class ClearWorker + include Sidekiq::Worker + + sidekiq_options retry: false + + def perform + # Do batched updates because these updates will be slow and locking + Project.select(:id).find_in_batches(batch_size: 1000) do |batch| + Project.where(id: batch.map(&:id)).update_all( + last_repository_check_failed: nil, + last_repository_check_at: nil, + ) + end + end + end +end \ No newline at end of file diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb new file mode 100644 index 00000000000..e54ae86d06c --- /dev/null +++ b/app/workers/repository_check/single_repository_worker.rb @@ -0,0 +1,36 @@ +module RepositoryCheck + class SingleRepositoryWorker + include Sidekiq::Worker + + sidekiq_options retry: false + + def perform(project_id) + project = Project.find(project_id) + project.update_columns( + last_repository_check_failed: !check(project), + last_repository_check_at: Time.now, + ) + end + + private + + def check(project) + # Use 'map do', not 'all? do', to prevent short-circuiting + [project.repository, project.wiki.repository].map do |repository| + git_fsck(repository.path_to_repo) + end.all? + end + + def git_fsck(path) + cmd = %W(nice git --git-dir=#{path} fsck) + output, status = Gitlab::Popen.popen(cmd) + + if status.zero? + true + else + Gitlab::RepositoryCheckLogger.error("command failed: #{cmd.join(' ')}\n#{output}") + false + end + end + end +end diff --git a/app/workers/repository_check_worker.rb b/app/workers/repository_check_worker.rb deleted file mode 100644 index d7ead91f94e..00000000000 --- a/app/workers/repository_check_worker.rb +++ /dev/null @@ -1,61 +0,0 @@ -class RepositoryCheckWorker - include Sidekiq::Worker - - RUN_TIME = 3600 - - sidekiq_options retry: false - - def perform - start = Time.now - - # This loop will break after a little more than one hour ('a little - # more' because `git fsck` may take a few minutes), or if it runs out of - # projects to check. By default sidekiq-cron will start a new - # RepositoryCheckWorker each hour so that as long as there are repositories to - # check, only one (or two) will be checked at a time. - project_ids.each do |project_id| - break if Time.now - start >= RUN_TIME - break unless current_settings.repository_checks_enabled - - next unless try_obtain_lease(project_id) - - SingleRepositoryCheckWorker.new.perform(project_id) - end - end - - private - - # Project.find_each does not support WHERE clauses and - # Project.find_in_batches does not support ordering. So we just build an - # array of ID's. This is OK because we do it only once an hour, because - # getting ID's from Postgres is not terribly slow, and because no user - # has to sit and wait for this query to finish. - def project_ids - limit = 10_000 - never_checked_projects = Project.where('last_repository_check_at IS NULL').limit(limit). - pluck(:id) - old_check_projects = Project.where('last_repository_check_at < ?', 1.week.ago). - reorder('last_repository_check_at ASC').limit(limit).pluck(:id) - never_checked_projects + old_check_projects - end - - def try_obtain_lease(id) - # Use a 24-hour timeout because on servers/projects where 'git fsck' is - # super slow we definitely do not want to run it twice in parallel. - Gitlab::ExclusiveLease.new( - "project_repository_check:#{id}", - timeout: 24.hours - ).try_obtain - end - - def current_settings - # No caching of the settings! If we cache them and an admin disables - # this feature, an active RepositoryCheckWorker would keep going for up - # to 1 hour after the feature was disabled. - if Rails.env.test? - Gitlab::CurrentSettings.fake_application_settings - else - ApplicationSetting.current - end - end -end diff --git a/app/workers/single_repository_check_worker.rb b/app/workers/single_repository_check_worker.rb deleted file mode 100644 index f6c345df8b5..00000000000 --- a/app/workers/single_repository_check_worker.rb +++ /dev/null @@ -1,34 +0,0 @@ -class SingleRepositoryCheckWorker - include Sidekiq::Worker - - sidekiq_options retry: false - - def perform(project_id) - project = Project.find(project_id) - project.update_columns( - last_repository_check_failed: !check(project), - last_repository_check_at: Time.now, - ) - end - - private - - def check(project) - # Use 'map do', not 'all? do', to prevent short-circuiting - [project.repository, project.wiki.repository].map do |repository| - git_fsck(repository.path_to_repo) - end.all? - end - - def git_fsck(path) - cmd = %W(nice git --git-dir=#{path} fsck) - output, status = Gitlab::Popen.popen(cmd) - - if status.zero? - true - else - Gitlab::RepositoryCheckLogger.error("command failed: #{cmd.join(' ')}\n#{output}") - false - end - end -end diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 5eb7fdff551..3124dc43065 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -241,7 +241,7 @@ Settings.cron_jobs['stuck_ci_builds_worker']['cron'] ||= '0 0 * * *' Settings.cron_jobs['stuck_ci_builds_worker']['job_class'] = 'StuckCiBuildsWorker' Settings.cron_jobs['repository_check_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['repository_check_worker']['cron'] ||= '20 * * * *' -Settings.cron_jobs['repository_check_worker']['job_class'] = 'RepositoryCheckWorker' +Settings.cron_jobs['repository_check_worker']['job_class'] = 'RepositoryCheck::BatchWorker' Settings.cron_jobs['admin_email_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['admin_email_worker']['cron'] ||= '0 0 * * *' Settings.cron_jobs['admin_email_worker']['job_class'] = 'AdminEmailWorker' diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb index 69b82441916..661fb761809 100644 --- a/spec/features/admin/admin_uses_repository_checks_spec.rb +++ b/spec/features/admin/admin_uses_repository_checks_spec.rb @@ -1,9 +1,7 @@ require 'rails_helper' feature 'Admin uses repository checks', feature: true do - before do - login_as :admin - end + before { login_as :admin } scenario 'to trigger a single check' do project = create(:empty_project) @@ -17,7 +15,12 @@ feature 'Admin uses repository checks', feature: true do end scenario 'to see a single failed repository check' do - visit_admin_project_page(broken_project) + project = create(:empty_project) + project.update_columns( + last_repository_check_failed: true, + last_repository_check_at: Time.now, + ) + visit_admin_project_page(project) page.within('.alert') do expect(page.text).to match(/Last repository check \(.* ago\) failed/) @@ -25,24 +28,16 @@ feature 'Admin uses repository checks', feature: true do end scenario 'to clear all repository checks', js: true do - project = broken_project visit admin_application_settings_path + + expect(RepositoryCheck::ClearWorker).to receive(:perform_async) - click_link 'Clear all repository checks' # pop-up should be auto confirmed + click_link 'Clear all repository checks' - expect(project.reload.last_repository_check_failed).to eq(false) + expect(page).to have_content('Started asynchronous removal of all repository check states.') end def visit_admin_project_page(project) visit admin_namespace_project_path(project.namespace, project) end - - def broken_project - project = create(:empty_project) - project.update_columns( - last_repository_check_failed: true, - last_repository_check_at: Time.now, - ) - project - end end diff --git a/spec/workers/repository_check_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb similarity index 94% rename from spec/workers/repository_check_worker_spec.rb rename to spec/workers/repository_check/batch_worker_spec.rb index 7a757658e97..51caa645f47 100644 --- a/spec/workers/repository_check_worker_spec.rb +++ b/spec/workers/repository_check/batch_worker_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe RepositoryCheckWorker do - subject { RepositoryCheckWorker.new } +describe RepositoryCheck::BatchWorker do + subject { described_class.new } it 'prefers projects that have never been checked' do projects = create_list(:project, 3) diff --git a/spec/workers/repository_check/clear_worker_spec.rb b/spec/workers/repository_check/clear_worker_spec.rb new file mode 100644 index 00000000000..a3b70c74787 --- /dev/null +++ b/spec/workers/repository_check/clear_worker_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe RepositoryCheck::ClearWorker do + it 'clears repository check columns' do + project = create(:empty_project) + project.update_columns( + last_repository_check_failed: true, + last_repository_check_at: Time.now, + ) + + described_class.new.perform + project.reload + + expect(project.last_repository_check_failed).to be_nil + expect(project.last_repository_check_at).to be_nil + end +end From acf911eeae0e952aec52d0e491efb69c99fb31f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 12 Apr 2016 18:09:52 +0200 Subject: [PATCH 128/187] Fix a bug with trailing slash in bamboo_url MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also, improve specs for BambooService Similar to https://gitlab.com/gitlab-org/gitlab-ce/issues/3515 Signed-off-by: Rémy Coutable --- CHANGELOG | 1 + app/models/project_services/bamboo_service.rb | 20 +- .../project_services/bamboo_service_spec.rb | 266 ++++++++++++++---- 3 files changed, 223 insertions(+), 64 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 382318a203c..8646d99b64d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,7 @@ v 8.7.0 (unreleased) - API: Expose `subscribed` for issues and merge requests (Robert Schilling) - Allow SAML to handle external users based on user's information !3530 - Add endpoints to archive or unarchive a project !3372 + - Fix a bug whith trailing slash in bamboo_url - Add links to CI setup documentation from project settings and builds pages - Handle nil descriptions in Slack issue messages (Stan Hu) - API: Expose open_issues_count, closed_issues_count, open_merge_requests_count for labels (Robert Schilling) diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 9e7f642180e..060062aaf7a 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -82,17 +82,17 @@ class BambooService < CiService end def build_info(sha) - url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}") + url = URI.join(bamboo_url, "/rest/api/latest/result?label=#{sha}").to_s if username.blank? && password.blank? - @response = HTTParty.get(parsed_url.to_s, verify: false) + @response = HTTParty.get(url, verify: false) else - get_url = "#{url}&os_authType=basic" + url << '&os_authType=basic' auth = { - username: username, - password: password, + username: username, + password: password } - @response = HTTParty.get(get_url, verify: false, basic_auth: auth) + @response = HTTParty.get(url, verify: false, basic_auth: auth) end end @@ -101,11 +101,11 @@ class BambooService < CiService if @response.code != 200 || @response['results']['results']['size'] == '0' # If actual build link can't be determined, send user to build summary page. - "#{bamboo_url}/browse/#{build_key}" + URI.join(bamboo_url, "/browse/#{build_key}").to_s else # If actual build link is available, go to build result page. result_key = @response['results']['results']['result']['planResultKey']['key'] - "#{bamboo_url}/browse/#{result_key}" + URI.join(bamboo_url, "/browse/#{result_key}").to_s end end @@ -134,7 +134,7 @@ class BambooService < CiService return unless supported_events.include?(data[:object_kind]) # Bamboo requires a GET and does not take any data. - self.class.get("#{bamboo_url}/updateAndBuild.action?buildKey=#{build_key}", - verify: false) + url = URI.join(bamboo_url, "/updateAndBuild.action?buildKey=#{build_key}").to_s + self.class.get(url, verify: false) end end diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index c34b2487ecf..31b2c90122d 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -21,74 +21,232 @@ require 'spec_helper' describe BambooService, models: true do - describe "Associations" do + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } end - describe "Execute" do - let(:user) { create(:user) } - let(:project) { create(:project) } + describe 'Validations' do + describe '#bamboo_url' do + it 'does not validate the presence of bamboo_url if service is not active' do + bamboo_service = service + bamboo_service.active = false - context "when a password was previously set" do - before do - @bamboo_service = BambooService.create( - project: create(:project), - properties: { - bamboo_url: 'http://gitlab.com', - username: 'mic', - password: "password" - } - ) - end - - it "reset password if url changed" do - @bamboo_service.bamboo_url = 'http://gitlab1.com' - @bamboo_service.save - expect(@bamboo_service.password).to be_nil - end - - it "does not reset password if username changed" do - @bamboo_service.username = "some_name" - @bamboo_service.save - expect(@bamboo_service.password).to eq("password") + expect(bamboo_service).not_to validate_presence_of(:bamboo_url) end - it "does not reset password if new url is set together with password, even if it's the same password" do - @bamboo_service.bamboo_url = 'http://gitlab_edited.com' - @bamboo_service.password = 'password' - @bamboo_service.save - expect(@bamboo_service.password).to eq("password") - expect(@bamboo_service.bamboo_url).to eq("http://gitlab_edited.com") - end + it 'validates the presence of bamboo_url if service is active' do + bamboo_service = service + bamboo_service.active = true - it "should reset password if url changed, even if setter called multiple times" do - @bamboo_service.bamboo_url = 'http://gitlab1.com' - @bamboo_service.bamboo_url = 'http://gitlab1.com' - @bamboo_service.save - expect(@bamboo_service.password).to be_nil + expect(bamboo_service).to validate_presence_of(:bamboo_url) end end - - context "when no password was previously set" do - before do - @bamboo_service = BambooService.create( - project: create(:project), - properties: { - bamboo_url: 'http://gitlab.com', - username: 'mic' - } - ) + + describe '#build_key' do + it 'does not validate the presence of build_key if service is not active' do + bamboo_service = service + bamboo_service.active = false + + expect(bamboo_service).not_to validate_presence_of(:build_key) end - it "saves password if new url is set together with password" do - @bamboo_service.bamboo_url = 'http://gitlab_edited.com' - @bamboo_service.password = 'password' - @bamboo_service.save - expect(@bamboo_service.password).to eq("password") - expect(@bamboo_service.bamboo_url).to eq("http://gitlab_edited.com") + it 'validates the presence of build_key if service is active' do + bamboo_service = service + bamboo_service.active = true + + expect(bamboo_service).to validate_presence_of(:build_key) + end + end + + describe '#username' do + it 'does not validate the presence of username if service is not active' do + bamboo_service = service + bamboo_service.active = false + + expect(bamboo_service).not_to validate_presence_of(:username) end + it 'does not validate the presence of username if username is nil' do + bamboo_service = service + bamboo_service.active = true + bamboo_service.password = nil + + expect(bamboo_service).not_to validate_presence_of(:username) + end + + it 'validates the presence of username if service is active and username is present' do + bamboo_service = service + bamboo_service.active = true + bamboo_service.password = 'secret' + + expect(bamboo_service).to validate_presence_of(:username) + end + end + + describe '#password' do + it 'does not validate the presence of password if service is not active' do + bamboo_service = service + bamboo_service.active = false + + expect(bamboo_service).not_to validate_presence_of(:password) + end + + it 'does not validate the presence of password if username is nil' do + bamboo_service = service + bamboo_service.active = true + bamboo_service.username = nil + + expect(bamboo_service).not_to validate_presence_of(:password) + end + + it 'validates the presence of password if service is active and username is present' do + bamboo_service = service + bamboo_service.active = true + bamboo_service.username = 'john' + + expect(bamboo_service).to validate_presence_of(:password) + end end end + + describe 'Callbacks' do + describe 'before_update :reset_password' do + context 'when a password was previously set' do + it 'resets password if url changed' do + bamboo_service = service + + bamboo_service.bamboo_url = 'http://gitlab1.com' + bamboo_service.save + + expect(bamboo_service.password).to be_nil + end + + it 'does not reset password if username changed' do + bamboo_service = service + + bamboo_service.username = 'some_name' + bamboo_service.save + + expect(bamboo_service.password).to eq('password') + end + + it "does not reset password if new url is set together with password, even if it's the same password" do + bamboo_service = service + + bamboo_service.bamboo_url = 'http://gitlab_edited.com' + bamboo_service.password = 'password' + bamboo_service.save + + expect(bamboo_service.password).to eq('password') + expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com') + end + end + + it 'saves password if new url is set together with password when no password was previously set' do + bamboo_service = service + bamboo_service.password = nil + + bamboo_service.bamboo_url = 'http://gitlab_edited.com' + bamboo_service.password = 'password' + bamboo_service.save + + expect(bamboo_service.password).to eq('password') + expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com') + end + end + end + + describe '#build_page' do + it 'returns a specific URL when status is 500' do + stub_request(status: 500) + + expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo') + end + + it 'returns a specific URL when response has no results' do + stub_request(body: %Q({"results":{"results":{"size":"0"}}})) + + expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo') + end + + it 'returns a build URL when bamboo_url has no trailing slash' do + stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}})) + + expect(service(bamboo_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42') + end + + it 'returns a build URL when bamboo_url has a trailing slash' do + stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}})) + + expect(service(bamboo_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42') + end + end + + describe '#commit_status' do + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) + + expect(service.commit_status('123', 'unused')).to eq(:error) + end + + it 'sets commit status to "pending" when status is 404' do + stub_request(status: 404) + + expect(service.commit_status('123', 'unused')).to eq('pending') + end + + it 'sets commit status to "pending" when response has no results' do + stub_request(body: %Q({"results":{"results":{"size":"0"}}})) + + expect(service.commit_status('123', 'unused')).to eq('pending') + end + + it 'sets commit status to "success" when build state contains Success' do + stub_request(build_state: 'YAY Success!') + + expect(service.commit_status('123', 'unused')).to eq('success') + end + + it 'sets commit status to "failed" when build state contains Failed' do + stub_request(build_state: 'NO Failed!') + + expect(service.commit_status('123', 'unused')).to eq('failed') + end + + it 'sets commit status to "pending" when build state contains Pending' do + stub_request(build_state: 'NO Pending!') + + expect(service.commit_status('123', 'unused')).to eq('pending') + end + + it 'sets commit status to :error when build state is unknown' do + stub_request(build_state: 'FOO BAR!') + + expect(service.commit_status('123', 'unused')).to eq(:error) + end + end + + def service(bamboo_url: 'http://gitlab.com') + described_class.create( + project: build_stubbed(:empty_project), + properties: { + bamboo_url: bamboo_url, + username: 'mic', + password: 'password', + build_key: 'foo' + } + ) + end + + def stub_request(status: 200, body: nil, build_state: 'success') + bamboo_full_url = 'http://mic:password@gitlab.com/rest/api/latest/result?label=123&os_authType=basic' + body ||= %Q({"results":{"results":{"result":{"buildState":"#{build_state}"}}}}) + + WebMock.stub_request(:get, bamboo_full_url).to_return( + status: status, + headers: { 'Content-Type' => 'application/json' }, + body: body + ) + end end From 02cfbf0db5dda8ca86f4811e5d5cb055a8cc5cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 13 Apr 2016 11:25:42 +0200 Subject: [PATCH 129/187] Refactor and expose only Gitlab::UrlBuilder.build(record) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/models/commit.rb | 6 +- app/services/issues/base_service.rb | 2 +- app/services/merge_requests/base_service.rb | 3 +- app/views/search/results/_note.html.haml | 2 +- lib/gitlab/note_data_builder.rb | 3 +- lib/gitlab/url_builder.rb | 74 +++++----- spec/factories/commits.rb | 12 ++ spec/lib/gitlab/note_data_builder_spec.rb | 3 +- spec/lib/gitlab/url_builder_spec.rb | 143 ++++++++++++-------- 9 files changed, 147 insertions(+), 101 deletions(-) create mode 100644 spec/factories/commits.rb diff --git a/app/models/commit.rb b/app/models/commit.rb index 11ecfcace14..d1f07ccd55c 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -154,7 +154,7 @@ class Commit id: id, message: safe_message, timestamp: committed_date.xmlschema, - url: commit_url, + url: Gitlab::UrlBuilder.build(self), author: { name: author_name, email: author_email @@ -168,10 +168,6 @@ class Commit data end - def commit_url - project.present? ? "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/#{id}" : "" - end - # Discover issues should be closed when this commit is pushed to a project's # default branch. def closes_issues(current_user = self.committer) diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 770f32de944..772f5c5fffa 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -3,7 +3,7 @@ module Issues def hook_data(issue, action) issue_data = issue.to_hook_data(current_user) - issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id) + issue_url = Gitlab::UrlBuilder.build(issue) issue_data[:object_attributes].merge!(url: issue_url, action: action) issue_data end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index ac5b58db862..e6837a18696 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -20,8 +20,7 @@ module MergeRequests def hook_data(merge_request, action) hook_data = merge_request.to_hook_data(current_user) - merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) - hook_data[:object_attributes][:url] = merge_request_url + hook_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(merge_request) hook_data[:object_attributes][:action] = action hook_data end diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index 9544e3d3e17..d9400b1d9fa 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -1,5 +1,5 @@ - project = note.project -- note_url = Gitlab::UrlBuilder.new(:note).build(note.id) +- note_url = Gitlab::UrlBuilder.build(note) - noteable_identifier = note.noteable.try(:iid) || note.noteable.id .search-result-row %h5.note-search-caption.str-truncated diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb index 18523e0aefe..8bdc89a7751 100644 --- a/lib/gitlab/note_data_builder.rb +++ b/lib/gitlab/note_data_builder.rb @@ -59,8 +59,7 @@ module Gitlab repository: project.hook_attrs.slice(:name, :url, :description, :homepage) } - base_data[:object_attributes][:url] = - Gitlab::UrlBuilder.new(:note).build(note.id) + base_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(note) base_data end diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index f301d42939d..f1943222edf 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -4,50 +4,58 @@ module Gitlab include GitlabRoutingHelper include ActionView::RecordIdentifier - def initialize(type) - @type = type + attr_reader :object + + def self.build(object) + new(object).url end - def build(id) - case @type - when :issue - build_issue_url(id) - when :merge_request - build_merge_request_url(id) - when :note - build_note_url(id) - + def url + case object + when Commit + commit_url + when Issue + issue_url(object) + when MergeRequest + merge_request_url(object) + when Note + note_url + else + raise NotImplementedError.new("No URL builder defined for #{object.class}") end end private - def build_issue_url(id) - issue = Issue.find(id) - issue_url(issue) + def initialize(object) + @object = object end - def build_merge_request_url(id) - merge_request = MergeRequest.find(id) - merge_request_url(merge_request) + def commit_url(opts = {}) + return '' if object.project.nil? + + namespace_project_commit_url({ + namespace_id: object.project.namespace, + project_id: object.project, + id: object.id + }.merge!(opts)) end - def build_note_url(id) - note = Note.find(id) - if note.for_commit? - namespace_project_commit_url(namespace_id: note.project.namespace, - id: note.commit_id, - project_id: note.project, - anchor: dom_id(note)) - elsif note.for_issue? - issue = Issue.find(note.noteable_id) - issue_url(issue, anchor: dom_id(note)) - elsif note.for_merge_request? - merge_request = MergeRequest.find(note.noteable_id) - merge_request_url(merge_request, anchor: dom_id(note)) - elsif note.for_snippet? - snippet = Snippet.find(note.noteable_id) - project_snippet_url(snippet, anchor: dom_id(note)) + def note_url + if object.for_commit? + commit_url(id: object.commit_id, anchor: dom_id(object)) + + elsif object.for_issue? + issue = Issue.find(object.noteable_id) + issue_url(issue, anchor: dom_id(object)) + + elsif object.for_merge_request? + merge_request = MergeRequest.find(object.noteable_id) + merge_request_url(merge_request, anchor: dom_id(object)) + + elsif object.for_snippet? + snippet = Snippet.find(object.noteable_id) + project_snippet_url(snippet, anchor: dom_id(object)) end end end diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb new file mode 100644 index 00000000000..ac6eb0a7897 --- /dev/null +++ b/spec/factories/commits.rb @@ -0,0 +1,12 @@ +require_relative '../support/repo_helpers' + +FactoryGirl.define do + factory :commit do + git_commit RepoHelpers.sample_commit + project factory: :empty_project + + initialize_with do + new(git_commit, project) + end + end +end diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb index da652677443..f093d0a0d8b 100644 --- a/spec/lib/gitlab/note_data_builder_spec.rb +++ b/spec/lib/gitlab/note_data_builder_spec.rb @@ -4,13 +4,12 @@ describe 'Gitlab::NoteDataBuilder', lib: true do let(:project) { create(:project) } let(:user) { create(:user) } let(:data) { Gitlab::NoteDataBuilder.build(note, user) } - let(:note_url) { Gitlab::UrlBuilder.new(:note).build(note.id) } let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors before(:each) do expect(data).to have_key(:object_attributes) expect(data[:object_attributes]).to have_key(:url) - expect(data[:object_attributes][:url]).to eq(note_url) + expect(data[:object_attributes][:url]).to eq(Gitlab::UrlBuilder.build(note)) expect(data[:object_kind]).to eq('note') expect(data[:user]).to eq(user.hook_attrs) end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index f023be6ae45..6ffc0d6e658 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -1,77 +1,110 @@ require 'spec_helper' describe Gitlab::UrlBuilder, lib: true do - describe 'When asking for an issue' do - it 'returns the issue url' do - issue = create(:issue) - url = Gitlab::UrlBuilder.new(:issue).build(issue.id) - expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}" + describe '.build' do + context 'when passing a Commit' do + it 'returns a proper URL' do + commit = build_stubbed(:commit) + + url = described_class.build(commit) + + expect(url).to eq "#{Settings.gitlab['url']}/#{commit.project.path_with_namespace}/commit/#{commit.id}" + end end - end - describe 'When asking for an merge request' do - it 'returns the merge request url' do - merge_request = create(:merge_request) - url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}" + context 'when passing an Issue' do + it 'returns a proper URL' do + issue = build_stubbed(:issue, iid: 42) + + url = described_class.build(issue) + + expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}" + end end - end - describe 'When asking for a note on commit' do - let(:note) { create(:note_on_commit) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + context 'when passing a MergeRequest' do + it 'returns a proper URL' do + merge_request = build_stubbed(:merge_request, iid: 42) - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + url = described_class.build(merge_request) + + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}" + end end - end - describe 'When asking for a note on commit diff' do - let(:note) { create(:note_on_commit_diff) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + context 'when passing a Note' do + context 'on a Commit' do + it 'returns a proper URL' do + note = build_stubbed(:note_on_commit) - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" - end - end + url = described_class.build(note) - describe 'When asking for a note on issue' do - let(:issue) { create(:issue) } - let(:note) { create(:note_on_issue, noteable_id: issue.id) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + end + end - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}" - end - end + context 'on a CommitDiff' do + it 'returns a proper URL' do + note = build_stubbed(:note_on_commit_diff) - describe 'When asking for a note on merge request' do - let(:merge_request) { create(:merge_request) } - let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + url = described_class.build(note) - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" - end - end + expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + end + end - describe 'When asking for a note on merge request diff' do - let(:merge_request) { create(:merge_request) } - let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + context 'on an Issue' do + it 'returns a proper URL' do + issue = create(:issue, iid: 42) + note = build_stubbed(:note_on_issue, noteable: issue) - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" - end - end + url = described_class.build(note) - describe 'When asking for a note on project snippet' do - let(:snippet) { create(:project_snippet) } - let(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}" + end + end - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}" + context 'on a MergeRequest' do + it 'returns a proper URL' do + merge_request = create(:merge_request, iid: 42) + note = build_stubbed(:note_on_merge_request, noteable: merge_request) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" + end + end + + context 'on a MergeRequestDiff' do + it 'returns a proper URL' do + merge_request = create(:merge_request, iid: 42) + note = build_stubbed(:note_on_merge_request_diff, noteable: merge_request) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" + end + end + + context 'on a ProjectSnippet' do + it 'returns a proper URL' do + project_snippet = create(:project_snippet) + note = build_stubbed(:note_on_project_snippet, noteable: project_snippet) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{project_snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}" + end + end + + context 'on another object' do + it 'returns a proper URL' do + project = build_stubbed(:project) + + expect { described_class.build(project) }. + to raise_error(NotImplementedError, 'No URL builder defined for Project') + end + end end end end From 54231aa4e036179d035ddd3641bc15a5b31883f2 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Wed, 13 Apr 2016 12:50:00 +0200 Subject: [PATCH 130/187] Styling changes to code and docs --- doc/api/projects.md | 1 - lib/api/helpers.rb | 2 +- lib/api/projects.rb | 4 +++- spec/requests/api/projects_spec.rb | 10 ++++------ 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/doc/api/projects.md b/doc/api/projects.md index b25c9080080..de1faadebf5 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -617,7 +617,6 @@ Example response: } ``` - ### Archive a project Archives the project if the user is either admin or the project owner of this project. This action is diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index aa0597564ed..54452f763a6 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -242,7 +242,7 @@ module API end def not_modified! - render_api_error!('304 Not modified', 304) + render_api_error!('304 Not Modified', 304) end def render_validation_error!(model) diff --git a/lib/api/projects.rb b/lib/api/projects.rb index c7fdfbfe57b..cc2c7a0c503 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -284,6 +284,7 @@ module API else current_user.toggle_star(user_project) user_project.reload + present user_project, with: Entities::Project end end @@ -293,11 +294,12 @@ module API # Parameters: # id (required) - The ID of a project # Example Request: - # DELETE /projects/:id/unstar + # DELETE /projects/:id/star delete ':id/star' do if current_user.starred?(user_project) current_user.toggle_star(user_project) user_project.reload + present user_project, with: Entities::Project else not_modified! diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 2a7c55fe65e..fccd08bd6da 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1023,7 +1023,7 @@ describe API::API, api: true do describe 'POST /projects/:id/star' do context 'on an unstarred project' do it 'stars the project' do - post api("/projects/#{project.id}/star", user) + expect { post api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1) expect(response.status).to eq(201) expect(json_response['star_count']).to eq(1) @@ -1037,10 +1037,9 @@ describe API::API, api: true do end it 'does not modify the star count' do - post api("/projects/#{project.id}/star", user) + expect { post api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } expect(response.status).to eq(304) - expect(project.star_count).to eq(1) end end end @@ -1053,7 +1052,7 @@ describe API::API, api: true do end it 'unstars the project' do - delete api("/projects/#{project.id}/star", user) + expect { delete api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1) expect(response.status).to eq(200) expect(json_response['star_count']).to eq(0) @@ -1062,10 +1061,9 @@ describe API::API, api: true do context 'on an unstarred project' do it 'does not modify the star count' do - delete api("/projects/#{project.id}/star", user) + expect { delete api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } expect(response.status).to eq(304) - expect(project.star_count).to eq(0) end end end From bf31b4495e020ef5fa45b204d2494d8d3b4d1494 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 13 Apr 2016 17:07:12 +0200 Subject: [PATCH 131/187] Schema improvements suggested by Yorick --- db/migrate/20160315135439_project_add_repository_check.rb | 4 +++- db/schema.rb | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/db/migrate/20160315135439_project_add_repository_check.rb b/db/migrate/20160315135439_project_add_repository_check.rb index 5a0859a30b2..8687d5d6296 100644 --- a/db/migrate/20160315135439_project_add_repository_check.rb +++ b/db/migrate/20160315135439_project_add_repository_check.rb @@ -1,6 +1,8 @@ class ProjectAddRepositoryCheck < ActiveRecord::Migration def change - add_column :projects, :last_repository_check_failed, :boolean, default: false + add_column :projects, :last_repository_check_failed, :boolean + add_index :projects, :last_repository_check_failed + add_column :projects, :last_repository_check_at, :datetime end end diff --git a/db/schema.rb b/db/schema.rb index db18d56f9cd..863e1f3f075 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -732,7 +732,7 @@ ActiveRecord::Schema.define(version: 20160412140240) do t.boolean "public_builds", default: true, null: false t.string "main_language" t.integer "pushes_since_gc", default: 0 - t.boolean "last_repository_check_failed", default: false + t.boolean "last_repository_check_failed" t.datetime "last_repository_check_at" end @@ -743,6 +743,7 @@ ActiveRecord::Schema.define(version: 20160412140240) do add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree + add_index "projects", ["last_repository_check_failed"], name: "index_projects_on_last_repository_check_failed", using: :btree add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree add_index "projects", ["path"], name: "index_projects_on_path", using: :btree From a9200d93d3e3d586302887fcaa0cf8f5fbd9a613 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Wed, 13 Apr 2016 16:30:20 +0200 Subject: [PATCH 132/187] Ensure that issues and merge requests are found --- doc/api/merge_requests.md | 1 + lib/api/issues.rb | 8 ++++---- spec/requests/api/issues_spec.rb | 6 +++--- spec/requests/api/merge_requests_spec.rb | 12 ++++++++++++ 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 3c18ebfa31e..2057f9d77aa 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -680,6 +680,7 @@ Example response: "subscribed": true } ``` + ## Unsubscribe from a merge request Unsubscribes the authenticated user from a merge request to not receive diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 37d25073074..4cdecadfe0f 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -239,8 +239,8 @@ module API # issue_id (required) - The ID of a project issue # Example Request: # POST /projects/:id/issues/:issue_id/subscription - post ":id/issues/:issue_id/subscription" do - issue = user_project.issues.find_by(id: params[:issue_id]) + post ':id/issues/:issue_id/subscription' do + issue = user_project.issues.find(params[:issue_id]) if issue.subscribed?(current_user) not_modified! @@ -257,8 +257,8 @@ module API # issue_id (required) - The ID of a project issue # Example Request: # DELETE /projects/:id/issues/:issue_id/subscription - delete ":id/issues/:issue_id/subscription" do - issue = user_project.issues.find_by(id: params[:issue_id]) + delete ':id/issues/:issue_id/subscription' do + issue = user_project.issues.find(params[:issue_id]) if issue.subscribed?(current_user) issue.unsubscribe(current_user) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 8361a1649e0..86ea223f206 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -594,20 +594,20 @@ describe API::API, api: true do describe 'DELETE :id/issues/:issue_id/subscription' do it 'unsubscribes from an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) + delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) expect(response.status).to eq(200) expect(json_response['subscribed']).to eq(false) end it 'returns 304 if not subscribed' do - post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) + delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) expect(response.status).to eq(304) end it 'returns 404 if the issue is not found' do - post api("/projects/#{project.id}/issues/123/subscription", user) + delete api("/projects/#{project.id}/issues/123/subscription", user) expect(response.status).to eq(404) end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index c247fcf9c96..1fa7e76894f 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -529,6 +529,12 @@ describe API::API, api: true do expect(response.status).to eq(304) end + + it 'returns 404 if the merge request is not found' do + post api("/projects/#{project.id}/merge_requests/123/subscription", user) + + expect(response.status).to eq(404) + end end describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do @@ -544,6 +550,12 @@ describe API::API, api: true do expect(response.status).to eq(304) end + + it 'returns 404 if the merge request is not found' do + post api("/projects/#{project.id}/merge_requests/123/subscription", user) + + expect(response.status).to eq(404) + end end def mr_with_later_created_and_updated_at_time From 31e28ebcebc054eaeef2eddba64ff2ff7ca3104f Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 12 Apr 2016 15:55:54 +0200 Subject: [PATCH 133/187] Load related MRs/branches asynchronously Currently this works by loading the HAML partials via XHR. While this is not the nicest setup it _is_ the easiest setup using the tools we currently have. Loading this data asynchronously doesn't make loading the related MRs/branches itself faster, it merely ensures that loading the issue itself is not slowed down. Fixes gitlab-org/gitlab-ce#14949 --- CHANGELOG | 1 + app/assets/javascripts/issue.js.coffee | 23 +++++++++++ app/controllers/projects/issues_controller.rb | 40 ++++++++++++++----- app/views/projects/issues/show.html.haml | 8 ++-- config/routes.rb | 2 + features/steps/shared/issuable.rb | 7 +++- 6 files changed, 66 insertions(+), 15 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ed59bc1b252..adbcd8f8bcf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) - All service classes (those residing in app/services) are now instrumented (Yorick Peterse) - Developers can now add custom tags to transactions (Yorick Peterse) + - Loading of an issue's referenced merge requests and related branches is now done asynchronously (Yorick Peterse) - Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea) - Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea) - All images in discussions and wikis now link to their source files !3464 (Connor Shea). diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index 946d83b7bdd..c7d74a12f99 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -10,6 +10,9 @@ class @Issue @initTaskList() @initIssueBtnEventListeners() + @initMergeRequests() + @initRelatedBranches() + initTaskList: -> $('.detail-page-description .js-task-list-container').taskList('enable') $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList @@ -69,3 +72,23 @@ class @Issue type: 'PATCH' url: $('form.js-issuable-update').attr('action') data: patchData + + initMergeRequests: -> + $container = $('#merge-requests') + + $.getJSON($container.data('url')) + .error -> + new Flash('Failed to load referenced merge requests', 'alert') + .success (data) -> + if 'html' of data + $container.html(data.html) + + initRelatedBranches: -> + $container = $('#related-branches') + + $.getJSON($container.data('url')) + .error -> + new Flash('Failed to load related branches', 'alert') + .success (data) -> + if 'html' of data + $container.html(data.html) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 6d649e72f84..c26cfeccf1d 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -3,7 +3,8 @@ class Projects::IssuesController < Projects::ApplicationController include IssuableActions before_action :module_enabled - before_action :issue, only: [:edit, :update, :show] + before_action :issue, + only: [:edit, :update, :show, :referenced_merge_requests, :related_branches] # Allow read any issue before_action :authorize_read_issue!, only: [:show] @@ -17,9 +18,6 @@ class Projects::IssuesController < Projects::ApplicationController # Allow issues bulk update before_action :authorize_admin_issues!, only: [:bulk_update] - # Cross-reference merge requests - before_action :closed_by_merge_requests, only: [:show] - respond_to :html def index @@ -65,8 +63,6 @@ class Projects::IssuesController < Projects::ApplicationController @note = @project.notes.new(noteable: @issue) @notes = @issue.notes.nonawards.with_associations.fresh @noteable = @issue - @merge_requests = @issue.referenced_merge_requests(current_user) - @related_branches = @issue.related_branches - @merge_requests.map(&:source_branch) respond_to do |format| format.html @@ -118,15 +114,39 @@ class Projects::IssuesController < Projects::ApplicationController end end + def referenced_merge_requests + @merge_requests = @issue.referenced_merge_requests(current_user) + @closed_by_merge_requests = @issue.closed_by_merge_requests(current_user) + + respond_to do |format| + format.json do + render json: { + html: view_to_html_string('projects/issues/_merge_requests') + } + end + end + end + + def related_branches + merge_requests = @issue.referenced_merge_requests(current_user) + + @related_branches = @issue.related_branches - + merge_requests.map(&:source_branch) + + respond_to do |format| + format.json do + render json: { + html: view_to_html_string('projects/issues/_related_branches') + } + end + end + end + def bulk_update result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" }) end - def closed_by_merge_requests - @closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user) - end - protected def issue diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 6fa059cbe68..5fe5ddc0819 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -64,9 +64,11 @@ = @issue.description = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') - .merge-requests - = render 'merge_requests' - = render 'related_branches' + #merge-requests{'data-url' => referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue)} + // This element is filled in using JavaScript. + + #related-branches{'data-url' => related_branches_namespace_project_issue_url(@project.namespace, @project, @issue)} + // This element is filled in using JavaScript. .content-block.content-block-small = render 'new_branch' diff --git a/config/routes.rb b/config/routes.rb index 48601b7567b..688b83d2c95 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -701,6 +701,8 @@ Rails.application.routes.draw do resources :issues, constraints: { id: /\d+/ } do member do post :toggle_subscription + get :referenced_merge_requests + get :related_branches end collection do post :bulk_update diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index b6d70a26c21..24b3fb6eacb 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -71,13 +71,16 @@ module SharedIssuable step 'I should not see any related merge requests' do page.within '.issue-details' do - expect(page).not_to have_content('.merge-requests') + expect(page).not_to have_content('#merge-requests .merge-requests-title') end end step 'I should see the "Enterprise fix" related merge request' do - page.within '.merge-requests' do + page.within '#merge-requests .merge-requests-title' do expect(page).to have_content('1 Related Merge Request') + end + + page.within '#merge-requests ul' do expect(page).to have_content('Enterprise fix') end end From 0a37976a4dcc88c7cbb746c89de122d3d10a453d Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 13 Apr 2016 17:29:04 +0200 Subject: [PATCH 134/187] Updated InfluxDB/Grafana setup/import docs The grafana-dashboards repository now contains _all_ GitLab.com dashboards and thus requires some extra continuous queries to be set up. The repository now also provided a way to automatically import/export dashboards. [ci skip] --- .../performance/grafana_configuration.md | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/doc/monitoring/performance/grafana_configuration.md b/doc/monitoring/performance/grafana_configuration.md index 10ef1009818..a79c8d48d3b 100644 --- a/doc/monitoring/performance/grafana_configuration.md +++ b/doc/monitoring/performance/grafana_configuration.md @@ -61,24 +61,32 @@ contents below and paste it in to the interactive session: ``` CREATE RETENTION POLICY gitlab_30d ON gitlab DURATION 30d REPLICATION 1 DEFAULT CREATE RETENTION POLICY seven_days ON gitlab DURATION 7d REPLICATION 1 -CREATE CONTINUOUS QUERY rails_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.rails_transaction_timings FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.sidekiq_transaction_timings FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.rails_transaction_counts FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.sidekiq_transaction_counts FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.rails_method_call_timings FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.sidekiq_method_call_timings FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.rails_method_call_timings_per_method FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m), method END -CREATE CONTINUOUS QUERY sidekiq_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.sidekiq_method_call_timings_per_method FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m), method END -CREATE CONTINUOUS QUERY rails_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.rails_memory_usage_per_minute FROM gitlab.gitlab_30d.rails_memory_usage GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.sidekiq_memory_usage_per_minute FROM gitlab.gitlab_30d.sidekiq_memory_usage GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.sidekiq_file_descriptors_per_minute FROM gitlab.gitlab_30d.sidekiq_file_descriptors GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.rails_file_descriptors_per_minute FROM gitlab.gitlab_30d.rails_file_descriptors GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.rails_gc_counts_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.sidekiq_gc_counts_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.rails_gc_timings_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.sidekiq_gc_timings_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.rails_gc_major_minor_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.sidekiq_gc_major_minor_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END +CREATE CONTINUOUS QUERY rails_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.rails_transaction_counts FROM rails_transactions GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.sidekiq_transaction_counts FROM sidekiq_transactions GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.rails_method_call_timings FROM rails_method_calls GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.sidekiq_method_call_timings FROM sidekiq_method_calls GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.rails_method_call_timings_per_method FROM rails_method_calls GROUP BY time(1m), method END; +CREATE CONTINUOUS QUERY sidekiq_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.sidekiq_method_call_timings_per_method FROM sidekiq_method_calls GROUP BY time(1m), method END; +CREATE CONTINUOUS QUERY rails_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.rails_memory_usage_per_minute FROM rails_memory_usage GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.sidekiq_memory_usage_per_minute FROM sidekiq_memory_usage GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.sidekiq_file_descriptors_per_minute FROM sidekiq_file_descriptors GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.rails_file_descriptors_per_minute FROM rails_file_descriptors GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.rails_gc_counts_per_minute FROM rails_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.sidekiq_gc_counts_per_minute FROM sidekiq_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.rails_gc_timings_per_minute FROM rails_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.sidekiq_gc_timings_per_minute FROM sidekiq_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.rails_gc_major_minor_per_minute FROM rails_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.sidekiq_gc_major_minor_per_minute FROM sidekiq_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_allowed_request_counts_per_minute ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.seven_days.grape_internal_allowed_request_counts_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/allowed' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_allowed_request_timings_per_minute ON gitlab BEGIN SELECT percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.grape_internal_allowed_request_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/allowed' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_allowed_sql_timings_per_minute ON gitlab BEGIN SELECT percentile(sql_duration, 95) AS duration_95th, percentile(sql_duration, 99) AS duration_99th, mean(sql_duration) AS duration_mean INTO gitlab.seven_days.grape_internal_allowed_sql_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/allowed' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_authorized_keys_request_counts_per_minute ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.seven_days.grape_internal_authorized_keys_request_counts_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/authorized_keys' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_authorized_keys_request_timings_per_minute ON gitlab BEGIN SELECT percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.grape_internal_authorized_keys_request_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/authorized_keys' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_authorized_keys_sql_timings_per_minute ON gitlab BEGIN SELECT percentile(sql_duration, 95) AS duration_95th, percentile(sql_duration, 99) AS duration_99th, mean(sql_duration) AS duration_mean INTO gitlab.seven_days.grape_internal_authorized_keys_sql_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/authorized_keys' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean, percentile(sql_duration, 95.000) AS sql_duration_95th, percentile(sql_duration, 99.000) AS sql_duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(view_duration, 95.000) AS view_duration_95th, percentile(view_duration, 99.000) AS view_duration_99th, mean(view_duration) AS view_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(cache_duration) AS cache_duration_mean INTO gitlab.seven_days.rails_transaction_timings FROM rails_transactions GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean, percentile(sql_duration, 95.000) AS sql_duration_95th, percentile(sql_duration, 99.000) AS sql_duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(view_duration, 95.000) AS view_duration_95th, percentile(view_duration, 99.000) AS view_duration_99th, mean(view_duration) AS view_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(cache_duration) AS cache_duration_mean INTO gitlab.seven_days.sidekiq_transaction_timings FROM sidekiq_transactions GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.seven_days.grape_transaction_counts FROM rails_transactions WHERE action !~ /.+/ GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean, percentile(sql_duration, 95.000) AS sql_duration_95th, percentile(sql_duration, 99.000) AS sql_duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(cache_duration) AS cache_duration_mean INTO gitlab.seven_days.grape_transaction_timings FROM rails_transactions WHERE action !~ /.+/ GROUP BY time(1m) END; ``` ## Import Dashboards @@ -106,6 +114,10 @@ navigate away. Repeat this process for each dashboard you wish to import. +Alternatively you can automatically import all the dashboards into your Grafana +instance. See the README of the [Grafana dashboards][grafana-dashboards] +repository for more information on this process. + [grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards --- From 6f6d2d0ad5039fa8ace7e09c4f5867bb680b0f79 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 13 Apr 2016 17:33:25 +0200 Subject: [PATCH 135/187] Use more conservative limits --- app/workers/repository_check/batch_worker.rb | 2 +- app/workers/repository_check/clear_worker.rb | 4 ++-- spec/workers/repository_check/batch_worker_spec.rb | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb index 16cd77a9bf0..44b3145d50f 100644 --- a/app/workers/repository_check/batch_worker.rb +++ b/app/workers/repository_check/batch_worker.rb @@ -35,7 +35,7 @@ module RepositoryCheck limit = 10_000 never_checked_projects = Project.where('last_repository_check_at IS NULL').limit(limit). pluck(:id) - old_check_projects = Project.where('last_repository_check_at < ?', 1.week.ago). + old_check_projects = Project.where('last_repository_check_at < ?', 1.month.ago). reorder('last_repository_check_at ASC').limit(limit).pluck(:id) never_checked_projects + old_check_projects end diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb index fe0cce9aab7..9c3347a7040 100644 --- a/app/workers/repository_check/clear_worker.rb +++ b/app/workers/repository_check/clear_worker.rb @@ -5,8 +5,8 @@ module RepositoryCheck sidekiq_options retry: false def perform - # Do batched updates because these updates will be slow and locking - Project.select(:id).find_in_batches(batch_size: 1000) do |batch| + # Do small batched updates because these updates will be slow and locking + Project.select(:id).find_in_batches(batch_size: 100) do |batch| Project.where(id: batch.map(&:id)).update_all( last_repository_check_failed: nil, last_repository_check_at: nil, diff --git a/spec/workers/repository_check/batch_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb index 51caa645f47..f486e45ddad 100644 --- a/spec/workers/repository_check/batch_worker_spec.rb +++ b/spec/workers/repository_check/batch_worker_spec.rb @@ -5,17 +5,17 @@ describe RepositoryCheck::BatchWorker do it 'prefers projects that have never been checked' do projects = create_list(:project, 3) - projects[0].update_column(:last_repository_check_at, 1.month.ago) - projects[2].update_column(:last_repository_check_at, 3.weeks.ago) + projects[0].update_column(:last_repository_check_at, 4.months.ago) + projects[2].update_column(:last_repository_check_at, 3.months.ago) expect(subject.perform).to eq(projects.values_at(1, 0, 2).map(&:id)) end it 'sorts projects by last_repository_check_at' do projects = create_list(:project, 3) - projects[0].update_column(:last_repository_check_at, 2.weeks.ago) - projects[1].update_column(:last_repository_check_at, 1.month.ago) - projects[2].update_column(:last_repository_check_at, 3.weeks.ago) + projects[0].update_column(:last_repository_check_at, 2.months.ago) + projects[1].update_column(:last_repository_check_at, 4.months.ago) + projects[2].update_column(:last_repository_check_at, 3.months.ago) expect(subject.perform).to eq(projects.values_at(1, 2, 0).map(&:id)) end @@ -23,7 +23,7 @@ describe RepositoryCheck::BatchWorker do it 'excludes projects that were checked recently' do projects = create_list(:project, 3) projects[0].update_column(:last_repository_check_at, 2.days.ago) - projects[1].update_column(:last_repository_check_at, 1.month.ago) + projects[1].update_column(:last_repository_check_at, 2.months.ago) projects[2].update_column(:last_repository_check_at, 3.days.ago) expect(subject.perform).to eq([projects[1].id]) From 28a7fe25fdf28042630282ace35e37310c8f0a12 Mon Sep 17 00:00:00 2001 From: Michael Greene Date: Tue, 5 Apr 2016 12:05:55 -0500 Subject: [PATCH 136/187] Allow back dating issues on update --- CHANGELOG | 2 +- doc/api/issues.md | 1 + lib/api/issues.rb | 7 +++++-- spec/requests/api/issues_spec.rb | 20 ++++++++++++++++---- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 21f24b5b61a..e5b8b9d7ce6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,11 +18,11 @@ v 8.7.0 (unreleased) - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) - Expose label description in API (Mariusz Jachimowicz) - - Allow back dating on issues when created through the API - API: Ability to update a group (Robert Schilling) - API: Ability to move issues (Robert Schilling) - Fix Error 500 after renaming a project path (Stan Hu) - Fix a bug whith trailing slash in teamcity_url (Charles May) + - Allow back dating on issues when created or updated through the API - Fix avatar stretching by providing a cropping feature - API: Expose `subscribed` for issues and merge requests (Robert Schilling) - Allow SAML to handle external users based on user's information !3530 diff --git a/doc/api/issues.md b/doc/api/issues.md index 42024becc36..3e78149f442 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -298,6 +298,7 @@ PUT /projects/:id/issues/:issue_id | `milestone_id` | integer | no | The ID of a milestone to assign the issue to | | `labels` | string | no | Comma-separated label names for an issue | | `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it | +| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` | ```bash curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 4cdecadfe0f..8aa08fd5acc 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -117,7 +117,7 @@ module API # assignee_id (optional) - The ID of a user to assign issue # milestone_id (optional) - The ID of a milestone to assign issue # labels (optional) - The labels of an issue - # created_at (optional) - The date + # created_at (optional) - Date time string, ISO 8601 formatted # Example Request: # POST /projects/:id/issues post ":id/issues" do @@ -166,12 +166,15 @@ module API # milestone_id (optional) - The ID of a milestone to assign issue # labels (optional) - The labels of an issue # state_event (optional) - The state event of an issue (close|reopen) + # updated_at (optional) - Date time string, ISO 8601 formatted # Example Request: # PUT /projects/:id/issues/:issue_id put ":id/issues/:issue_id" do issue = user_project.issues.find(params[:issue_id]) authorize! :update_issue, issue - attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id, :state_event] + keys = [:title, :description, :assignee_id, :milestone_id, :state_event] + keys << :updated_at if current_user.admin? || user_project.owner == current_user + attrs = attributes_for_keys(keys) # Validate label names in advance if (errors = validate_label_params(params)).any? diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 86ea223f206..f88e39cad9e 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -321,13 +321,13 @@ describe API::API, api: true do end context 'when an admin or owner makes the request' do - it "accepts the creation date to be set" do + it 'accepts the creation date to be set' do + creation_time = 2.weeks.ago post api("/projects/#{project.id}/issues", user), - title: 'new issue', labels: 'label, label2', created_at: 2.weeks.ago + title: 'new issue', labels: 'label, label2', created_at: creation_time expect(response.status).to eq(201) - # this take about a second, so probably not equal - expect(Time.parse(json_response['created_at'])).to be <= 2.weeks.ago + expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time) end end end @@ -478,6 +478,18 @@ describe API::API, api: true do expect(json_response['labels']).to include 'label2' expect(json_response['state']).to eq "closed" end + + context 'when an admin or owner makes the request' do + it 'accepts the update date to be set' do + update_time = 2.weeks.ago + put api("/projects/#{project.id}/issues/#{issue.id}", user), + labels: 'label3', state_event: 'close', updated_at: update_time + expect(response.status).to eq(200) + + expect(json_response['labels']).to include 'label3' + expect(Time.parse(json_response['updated_at'])).to be_within(1.second).of(update_time) + end + end end describe "DELETE /projects/:id/issues/:issue_id" do From c1467f5d97c04c22e2119ace084bb016f8f53d48 Mon Sep 17 00:00:00 2001 From: Michael Greene Date: Tue, 5 Apr 2016 13:04:11 -0500 Subject: [PATCH 137/187] Allow back dating notes on creation --- CHANGELOG | 1 + doc/api/notes.md | 1 + lib/api/notes.rb | 5 +++++ spec/requests/api/notes_spec.rb | 13 +++++++++++++ 4 files changed, 20 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index e5b8b9d7ce6..9baf6516ef6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -23,6 +23,7 @@ v 8.7.0 (unreleased) - Fix Error 500 after renaming a project path (Stan Hu) - Fix a bug whith trailing slash in teamcity_url (Charles May) - Allow back dating on issues when created or updated through the API + - Allow back dating on issue notes when created through the API - Fix avatar stretching by providing a cropping feature - API: Expose `subscribed` for issues and merge requests (Robert Schilling) - Allow SAML to handle external users based on user's information !3530 diff --git a/doc/api/notes.md b/doc/api/notes.md index 2e0936f11b5..7aa1c2155bf 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -89,6 +89,7 @@ Parameters: - `id` (required) - The ID of a project - `issue_id` (required) - The ID of an issue - `body` (required) - The content of a note +- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z ### Modify existing issue note diff --git a/lib/api/notes.rb b/lib/api/notes.rb index a1c98f5e8ff..71a53e6f0d6 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -61,6 +61,7 @@ module API # id (required) - The ID of a project # noteable_id (required) - The ID of an issue or snippet # body (required) - The content of a note + # created_at (optional) - The date # Example Request: # POST /projects/:id/issues/:noteable_id/notes # POST /projects/:id/snippets/:noteable_id/notes @@ -73,6 +74,10 @@ module API noteable_id: params[noteable_id_str] } + if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user) + opts[:created_at] = params[:created_at] + end + @note = ::Notes::CreateService.new(user_project, current_user, opts).execute if @note.valid? diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index a467bc935af..ec9eda0a2ed 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -158,6 +158,19 @@ describe API::API, api: true do post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' expect(response.status).to eq(401) end + + context 'when an admin or owner makes the request' do + it 'accepts the creation date to be set' do + creation_time = 2.weeks.ago + post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), + body: 'hi!', created_at: creation_time + expect(response.status).to eq(201) + expect(json_response['body']).to eq('hi!') + expect(json_response['author']['username']).to eq(user.username) + expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time) + end + end + end context "when noteable is a Snippet" do From 2244aaf98f6c9b68e30febf6b80f2c0d965e541a Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 13 Apr 2016 13:20:57 +0300 Subject: [PATCH 138/187] Redis configuration consistency --- config/initializers/session_store.rb | 2 +- config/initializers/sidekiq.rb | 6 ++---- lib/gitlab/redis.rb | 2 ++ 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 70285255877..88cb859871c 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -14,7 +14,7 @@ if Rails.env.test? Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session" else redis_config = Gitlab::Redis.redis_store_options - redis_config[:namespace] = 'session:gitlab' + redis_config[:namespace] = Gitlab::Redis::SESSION_NAMESPACE Gitlab::Application.config.session_store( :redis_store, # Using the cookie_store would enable session replay attacks. diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 9182d929809..f1eec674888 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -1,9 +1,7 @@ -SIDEKIQ_REDIS_NAMESPACE = 'resque:gitlab' - Sidekiq.configure_server do |config| config.redis = { url: Gitlab::Redis.url, - namespace: SIDEKIQ_REDIS_NAMESPACE + namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE } config.server_middleware do |chain| @@ -30,6 +28,6 @@ end Sidekiq.configure_client do |config| config.redis = { url: Gitlab::Redis.url, - namespace: SIDEKIQ_REDIS_NAMESPACE + namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE } end diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 319447669dc..5c352c96de5 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -1,6 +1,8 @@ module Gitlab class Redis CACHE_NAMESPACE = 'cache:gitlab' + SESSION_NAMESPACE = 'session:gitlab' + SIDEKIQ_NAMESPACE = 'resque:gitlab' attr_reader :url From 11f46b459e9d97900cb71f7f6bd313b5e18fbaa6 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Wed, 13 Apr 2016 15:28:10 -0300 Subject: [PATCH 139/187] Setup visibility level for project when transfering for a group --- app/services/projects/transfer_service.rb | 21 +++++++++++++---- app/views/projects/edit.html.haml | 1 + .../projects/transfer_service_spec.rb | 23 +++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 2e734654466..0d8f8c6fbee 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -34,9 +34,12 @@ module Projects raise TransferError.new("Project with same path in target namespace already exists") end - # Apply new namespace id - project.namespace = new_namespace - project.save! + # Apply new namespace id and visibility level + project.tap do |p| + p.namespace = new_namespace + setup_visibility_level(p, new_namespace) + p.save! + end # Notifications project.send_move_instructions(old_path) @@ -56,7 +59,7 @@ module Projects 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 @@ -68,5 +71,15 @@ module Projects namespace.id != project.namespace_id && current_user.can?(:create_projects, namespace) end + + private + + def setup_visibility_level(project, new_namespace) + return unless new_namespace.is_a?(Group) + + if project.visibility_level > new_namespace.visibility_level + project.visibility_level = new_namespace.visibility_level + end + end end end diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 6d872cd0b21..76a4f41193c 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -210,6 +210,7 @@ %li Be careful. Changing the project's namespace can have unintended side effects. %li You can only transfer the project to namespaces you manage. %li You will need to update your local repositories to point to the new location. + %li Project visibility level will be changed to match namespace rules when transfering to a group. .form-actions = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } - else diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index c46259431aa..06017317339 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -38,4 +38,27 @@ describe Projects::TransferService, services: true do def transfer_project(project, user, new_namespace) Projects::TransferService.new(project, user).execute(new_namespace) end + + context 'visibility level' do + let(:internal_group) { create(:group, :internal) } + + before { internal_group.add_owner(user) } + + context 'when namespace visibility level < project visibility level' do + let(:public_project) { create(:project, :public, namespace: user.namespace) } + + before { transfer_project(public_project, user, internal_group) } + + it { expect(public_project.visibility_level).to eq(internal_group.visibility_level) } + end + + context 'when namespace visibility level > project visibility level' do + let(:private_project) { create(:project, :private, namespace: user.namespace) } + + before { transfer_project(private_project, user, internal_group) } + + it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) } + end + end + end From 6b3a53848c68c8fd0931de9b7c6ab15b53b9475b Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Wed, 13 Apr 2016 15:30:50 -0300 Subject: [PATCH 140/187] Add changelog entry --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 5399e3e5b8b..416fffece63 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ v 8.7.0 (unreleased) - All images in discussions and wikis now link to their source files !3464 (Connor Shea). - Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu) - Add setting for customizing the list of trusted proxies !3524 + - Allow projects to be transfered to a lower visibility level group - Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524 - Improved Markdown rendering performance !3389 (Yorick Peterse) - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu) From a31c1dccb69e6c5999af31c2be4e6ee71043a5e3 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 13 Apr 2016 16:43:07 -0500 Subject: [PATCH 141/187] Add test to check if top right search form is not present --- spec/features/search_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 3e6289a46b1..029a11ea43c 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -10,6 +10,10 @@ describe "Search", feature: true do visit search_path end + it 'top right search form is not present' do + expect(page).not_to have_selector('.search') + end + describe 'searching for Projects' do it 'finds a project' do page.within '.search-holder' do From 897892132334f4004719d2489530898491f4fff6 Mon Sep 17 00:00:00 2001 From: Tom Downes Date: Thu, 14 Apr 2016 01:03:50 +0000 Subject: [PATCH 142/187] Update shibboleth configuration for GitLab 8.6 and Apache 2.4 --- doc/integration/shibboleth.md | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md index a0be3dd4e5c..b6b2d4e5e88 100644 --- a/doc/integration/shibboleth.md +++ b/doc/integration/shibboleth.md @@ -76,3 +76,50 @@ sudo gitlab-ctl reconfigure ``` On the sign in page there should now be a "Sign in with: Shibboleth" icon below the regular sign in form. Click the icon to begin the authentication process. You will be redirected to IdP server (Depends on your Shibboleth module configuration). If everything goes well the user will be returned to GitLab and will be signed in. + +## Apache 2.4 / GitLab 8.6 update +The order of the first 2 Location directives is important. If they are reversed, +you will not get a shibboleth session! + +``` + + Require all granted + ProxyPassReverse http://127.0.0.1:8181 + ProxyPassReverse http://YOUR_SERVER_FQDN/ + + + + AuthType shibboleth + ShibRequestSetting requireSession 1 + ShibUseHeaders On + Require shib-session + + + Alias /shibboleth-sp /usr/share/shibboleth + + + Require all granted + + + + SetHandler shib + + + RewriteEngine on + + #Don't escape encoded characters in api requests + RewriteCond %{REQUEST_URI} ^/api/v3/.* + RewriteCond %{REQUEST_URI} !/Shibboleth.sso + RewriteCond %{REQUEST_URI} !/shibboleth-sp + RewriteRule .* http://127.0.0.1:8181%{REQUEST_URI} [P,QSA,NE] + + #Forward all requests to gitlab-workhorse except existing files + RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f [OR] + RewriteCond %{REQUEST_URI} ^/uploads/.* + RewriteCond %{REQUEST_URI} !/Shibboleth.sso + RewriteCond %{REQUEST_URI} !/shibboleth-sp + RewriteRule .* http://127.0.0.1:8181%{REQUEST_URI} [P,QSA] + + RequestHeader set X_FORWARDED_PROTO 'https' + RequestHeader set X-Forwarded-Ssl on +``` \ No newline at end of file From f5e8667fc5fd7f650704e9507edad48c7fbdef79 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 14 Apr 2016 09:13:32 +0100 Subject: [PATCH 143/187] Fixed haml style issues --- app/views/projects/labels/_label.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index c7b4bb1f6e6..8bf544b8371 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -14,7 +14,7 @@ .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}} .subscription-status{data: {status: label_subscription_status(label)}} - %button.js-subscribe-button.label-subscribe-button.btn.action-buttons{ type: 'button', data: {toggle: "tooltip"} } + %button.js-subscribe-button.label-subscribe-button.btn.action-buttons{ type: "button", data: { toggle: "tooltip" } } %span= label_subscription_toggle_button_text(label) - if can? current_user, :admin_label, @project From 2fd05aed463684203cf21923b54e28888fcad0ea Mon Sep 17 00:00:00 2001 From: Frank Groeneveld Date: Thu, 14 Apr 2016 10:24:09 +0200 Subject: [PATCH 144/187] Allow empty recipient list when pusher is added Closes #13574 --- CHANGELOG | 1 + .../project_services/builds_email_service.rb | 10 +++-- .../builds_email_service_spec.rb | 37 ++++++++++++++++++- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9baf6516ef6..ba98b3b100c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -60,6 +60,7 @@ v 8.7.0 (unreleased) - API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling) - API: User can leave a project through the API when not master or owner. !3613 - Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu) + - Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld) v 8.6.6 - Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk) diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index f9f04838766..6ab6d7417b7 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -23,7 +23,7 @@ class BuildsEmailService < Service prop_accessor :recipients boolean_accessor :add_pusher boolean_accessor :notify_only_broken_builds - validates :recipients, presence: true, if: :activated? + validates :recipients, presence: true, if: ->(s) { s.activated? && !s.add_pusher? } def initialize_properties if properties.nil? @@ -87,10 +87,14 @@ class BuildsEmailService < Service end def all_recipients(data) - all_recipients = recipients.split(',').compact.reject(&:blank?) + all_recipients = [] + + unless recipients.blank? + all_recipients += recipients.split(',').compact.reject(&:blank?) + end if add_pusher? && data[:user][:email] - all_recipients << "#{data[:user][:email]}" + all_recipients << data[:user][:email] end all_recipients diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb index 2ccbff553f0..7c23c2efccd 100644 --- a/spec/models/project_services/builds_email_service_spec.rb +++ b/spec/models/project_services/builds_email_service_spec.rb @@ -3,9 +3,10 @@ require 'spec_helper' describe BuildsEmailService do let(:build) { create(:ci_build) } let(:data) { Gitlab::BuildDataBuilder.build(build) } - let(:service) { BuildsEmailService.new } + let!(:project) { create(:project, :public, ci_id: 1) } + let(:service) { described_class.new(project: project, active: true) } - describe :execute do + describe '#execute' do it 'sends email' do service.recipients = 'test@gitlab.com' data[:build_status] = 'failed' @@ -40,4 +41,36 @@ describe BuildsEmailService do service.execute(data) end end + + describe 'validations' do + + context 'when pusher is not added' do + before { service.add_pusher = false } + + it 'does not allow empty recipient input' do + service.recipients = '' + expect(service.valid?).to be false + end + + it 'does allow non-empty recipient input' do + service.recipients = 'test@example.com' + expect(service.valid?).to be true + end + + end + + context 'when pusher is added' do + before { service.add_pusher = true } + + it 'does allow empty recipient input' do + service.recipients = '' + expect(service.valid?).to be true + end + + it 'does allow non-empty recipient input' do + service.recipients = 'test@example.com' + expect(service.valid?).to be true + end + end + end end From ec3f4f5bf06aa662b86079d815f9a68b556190f3 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 14 Apr 2016 10:57:13 +0200 Subject: [PATCH 145/187] Projects on group page should be sorted by last activity instead of id/created_at Signed-off-by: Dmitriy Zaporozhets --- app/controllers/groups_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index c1adc999567..ee4fcc4e360 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -40,6 +40,7 @@ class GroupsController < Groups::ApplicationController @last_push = current_user.recent_push if current_user @projects = @projects.includes(:namespace) + @projects = @projects.sorted_by_activity @projects = filter_projects(@projects) @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.page(params[:page]) if params[:filter_projects].blank? From 82164a9f777f7eee37707b19ae6ec514a588c1ec Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 5 Apr 2016 10:17:00 +0100 Subject: [PATCH 146/187] Notes form JS update Updated the JS to have a standard class with standard actions for each form Created ability to have toolbar buttons that insert different prefixes dependant upon the data-prefix attribute --- app/assets/javascripts/gl_form.js.coffee | 40 +++++++++++++ .../javascripts/gl_form_actions.js.coffee | 43 ++++++++++++++ app/assets/javascripts/notes.js.coffee | 58 +++---------------- app/views/projects/notes/_form.html.haml | 2 +- app/views/projects/notes/_hints.html.haml | 3 + 5 files changed, 95 insertions(+), 51 deletions(-) create mode 100644 app/assets/javascripts/gl_form.js.coffee create mode 100644 app/assets/javascripts/gl_form_actions.js.coffee diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee new file mode 100644 index 00000000000..efa9284ed09 --- /dev/null +++ b/app/assets/javascripts/gl_form.js.coffee @@ -0,0 +1,40 @@ +class @GLForm + constructor: (@form) -> + @textarea = @form.find(".js-note-text") + + @setupForm() + + setupForm: -> + isNewForm = @form.is(':not(.gfm-form)') + + @form.removeClass "js-new-note-form" + + if isNewForm + @form.find('.div-dropzone').remove() + @form.addClass('gfm-form') + disableButtonIfEmptyField @form.find(".js-note-text"), @form.find(".js-comment-button") + + # remove notify commit author checkbox for non-commit notes + GitLab.GfmAutoComplete.setup() + new DropzoneInput(@form) + + autosize(@textarea) + + # Setup action buttons + actions = new GLFormActions @form, @textarea + @form.data 'form-actions', actions + + # form and textarea event listeners + @addEventListeners() + + # hide discard button + @form.find('.js-note-discard').hide() + + @form.show() + + addEventListeners: -> + @textarea.on 'focus', -> + $(@).closest('.md-area').addClass 'is-focused' + + @textarea.on 'blur', -> + $(@).closest('.md-area').removeClass 'is-focused' diff --git a/app/assets/javascripts/gl_form_actions.js.coffee b/app/assets/javascripts/gl_form_actions.js.coffee new file mode 100644 index 00000000000..d8de63a2be9 --- /dev/null +++ b/app/assets/javascripts/gl_form_actions.js.coffee @@ -0,0 +1,43 @@ +class @GLFormActions + constructor: (@form, @textarea) -> + @clearEventListeners() + @addEventListeners() + + clearEventListeners: -> + @form.off 'click', '.js-toolbar-button' + + addEventListeners: -> + @form.on 'click', '.js-toolbar-button', @toolbarButtonClick + + toolbarButtonClick: (e) => + $btn = $(e.currentTarget) + + # Get the prefix from the button + prefix = $btn.data('prefix') + @addPrefixToTextarea(prefix) + + addPrefixToTextarea: (prefix) -> + caretStart = @textarea.get(0).selectionStart + caretEnd = @textarea.get(0).selectionEnd + textEnd = @textarea.val().length + + beforeSelection = @textarea.val().substring 0, caretStart + afterSelection = @textarea.val().substring caretEnd, textEnd + + beforeSelectionSplit = beforeSelection.split '' + beforeSelectionLength = beforeSelection.length + + # Get the last character in the before selection + beforeSelectionLastChar = beforeSelectionSplit[beforeSelectionLength - 1] + + if beforeSelectionLastChar? and beforeSelectionLastChar isnt '' + # Append a white space char to the prefix if the previous char isn't a space + prefix = " #{prefix}" + + # Update the textarea + @textarea.val beforeSelection + prefix + afterSelection + @textarea.get(0).setSelectionRange caretStart + prefix.length, caretEnd + prefix.length + + # Focus the textarea + @textarea.focus() + @textarea.trigger('keyup') diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index a67890200dd..0581774424f 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -283,32 +283,10 @@ class @Notes show the form ### setupNoteForm: (form) -> - disableButtonIfEmptyField form.find(".js-note-text"), form.find(".js-comment-button") - form.removeClass "js-new-note-form" - form.find('.div-dropzone').remove() - - # hide discard button - form.find('.js-note-discard').hide() - - # setup preview buttons - previewButton = form.find(".js-md-preview-button") + new GLForm form textarea = form.find(".js-note-text") - textarea.on "input", -> - if $(this).val().trim() isnt "" - previewButton.removeClass("turn-off").addClass "turn-on" - else - previewButton.removeClass("turn-on").addClass "turn-off" - - textarea.on 'focus', -> - $(this).closest('.md-area').addClass 'is-focused' - - textarea.on 'blur', -> - $(this).closest('.md-area').removeClass 'is-focused' - - autosize(textarea) - new Autosave textarea, [ "Note" form.find("#note_commit_id").val() @@ -317,11 +295,6 @@ class @Notes form.find("#note_noteable_id").val() ] - # remove notify commit author checkbox for non-commit notes - GitLab.GfmAutoComplete.setup() - new DropzoneInput(form) - form.show() - ### Called in response to the new note form being submitted @@ -375,34 +348,15 @@ class @Notes note = $(this).closest(".note") note.addClass "is-editting" form = note.find(".note-edit-form") - isNewForm = form.is(':not(.gfm-form)') - if isNewForm - form.addClass('gfm-form') + form.addClass('current-note-edit-form') # Show the attachment delete link note.find(".js-note-attachment-delete").show() - # Setup markdown form - if isNewForm - GitLab.GfmAutoComplete.setup() - new DropzoneInput(form) + new GLForm form - textarea = form.find("textarea") - textarea.focus() - - if isNewForm - autosize(textarea) - - # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). - # The textarea has the correct value, Chrome just won't show it unless we - # modify it, so let's clear it and re-set it! - value = textarea.val() - textarea.val "" - textarea.val value - - if isNewForm - disableButtonIfEmptyField textarea, form.find(".js-comment-button") + form.find(".js-note-text").focus() ### Called in response to clicking the edit note link @@ -570,6 +524,10 @@ class @Notes # only remove the form form.remove() + # Remove the note actions + actions = form.data('form-actions') + actions.clearEventListeners() + form.data('form-actions', null) cancelDiscussionForm: (e) => e.preventDefault() diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index c446ecec2c3..261eef4df4f 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f| += form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form" }, authenticity_token: true do |f| = hidden_field_tag :view, diff_view = hidden_field_tag :line_type = note_target_fields(@note) diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml index 0b002043408..0c6758210b1 100644 --- a/app/views/projects/notes/_hints.html.haml +++ b/app/views/projects/notes/_hints.html.haml @@ -1,4 +1,7 @@ .comment-toolbar.clearfix + %button.toolbar-button.js-toolbar-button{ type: 'button', data: { prefix: ':' }, tabindex: '-1' } + = icon('smile-o', class: 'toolbar-button-icon') + Emoji .toolbar-text Styling with = link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1 From 5dd01f572c9a7504f34c5a3b30792197c856bd6f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 5 Apr 2016 10:28:10 +0100 Subject: [PATCH 147/187] Destroy discussion form --- app/assets/javascripts/gl_form.js.coffee | 22 +++++++++++++++++++--- app/assets/javascripts/notes.js.coffee | 8 +++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee index efa9284ed09..087a46cbac2 100644 --- a/app/assets/javascripts/gl_form.js.coffee +++ b/app/assets/javascripts/gl_form.js.coffee @@ -1,18 +1,30 @@ class @GLForm constructor: (@form) -> - @textarea = @form.find(".js-note-text") + @textarea = @form.find('.js-note-text') @setupForm() + @form.data 'gl-form', @ + + destroy: -> + # Destroy actions + actions = @form.data 'form-actions' + actions.clearEventListeners() + @form.data 'form-actions', null + + # Clean form listeners + @clearEventListeners() + @form.data 'gl-form', null + setupForm: -> isNewForm = @form.is(':not(.gfm-form)') - @form.removeClass "js-new-note-form" + @form.removeClass 'js-new-note-form' if isNewForm @form.find('.div-dropzone').remove() @form.addClass('gfm-form') - disableButtonIfEmptyField @form.find(".js-note-text"), @form.find(".js-comment-button") + disableButtonIfEmptyField @form.find('.js-note-text'), @form.find('.js-comment-button') # remove notify commit author checkbox for non-commit notes GitLab.GfmAutoComplete.setup() @@ -32,6 +44,10 @@ class @GLForm @form.show() + clearEventListeners: -> + @textarea.off 'focus' + @textarea.off 'blur' + addEventListeners: -> @textarea.on 'focus', -> $(@).closest('.md-area').addClass 'is-focused' diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 0581774424f..fa91baa07c0 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -513,6 +513,9 @@ class @Notes removeDiscussionNoteForm: (form)-> row = form.closest("tr") + glForm = form.data 'gl-form' + glForm.destroy() + form.find(".js-note-text").data("autosave").reset() # show the reply button (will only work for replies) @@ -524,11 +527,6 @@ class @Notes # only remove the form form.remove() - # Remove the note actions - actions = form.data('form-actions') - actions.clearEventListeners() - form.data('form-actions', null) - cancelDiscussionForm: (e) => e.preventDefault() form = $(e.target).closest(".js-discussion-note-form") From ff7fb09ea80036045e6fd17624b894f389de38b9 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 5 Apr 2016 10:34:02 +0100 Subject: [PATCH 148/187] Updated issue form to use new GLForm --- app/assets/javascripts/dispatcher.js.coffee | 2 +- app/assets/javascripts/gl_form.js.coffee | 12 +++++++++--- app/views/projects/issues/_form.html.haml | 2 +- app/views/shared/issuable/_form.html.haml | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 70fd6f50e9c..6efbe214aad 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -35,7 +35,7 @@ class Dispatcher new Diff() when 'projects:issues:new','projects:issues:edit' shortcut_handler = new ShortcutsNavigation() - new DropzoneInput($('.issue-form')) + new GLForm($('.issue-form')) new IssuableForm($('.issue-form')) when 'projects:merge_requests:new', 'projects:merge_requests:edit' new Diff() diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee index 087a46cbac2..aff3e909ce1 100644 --- a/app/assets/javascripts/gl_form.js.coffee +++ b/app/assets/javascripts/gl_form.js.coffee @@ -1,7 +1,11 @@ class @GLForm constructor: (@form) -> - @textarea = @form.find('.js-note-text') + @textarea = @form.find('textarea.js-gfm-input') + # Before we start, we should clean up any previous data for this form + @destroy() + + # Setup the form @setupForm() @form.data 'gl-form', @ @@ -9,8 +13,10 @@ class @GLForm destroy: -> # Destroy actions actions = @form.data 'form-actions' - actions.clearEventListeners() - @form.data 'form-actions', null + + if actions? + actions.clearEventListeners() + @form.data 'form-actions', null # Clean form listeners @clearEventListeners() diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 33c48199ba5..7076f5db015 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-quick-submit js-requires-input' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f| = render 'shared/issuable/form', f: f, issuable: @issue :javascript diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 6de07a92cd7..d77c74e5d7a 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -29,7 +29,7 @@ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :description, - classes: 'description form-control' + classes: 'note-textarea' = render 'projects/notes/hints' .clearfix .error-alert From c45ca936c77eceff367e27e1bf8fd5f11c6a7c39 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 5 Apr 2016 10:39:16 +0100 Subject: [PATCH 149/187] merge request form uses new GLForm --- app/assets/javascripts/dispatcher.js.coffee | 2 +- app/views/projects/merge_requests/_form.html.haml | 2 +- app/views/projects/merge_requests/_new_submit.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 6efbe214aad..d02e1130704 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -40,7 +40,7 @@ class Dispatcher when 'projects:merge_requests:new', 'projects:merge_requests:edit' new Diff() shortcut_handler = new ShortcutsNavigation() - new DropzoneInput($('.merge-request-form')) + new GLForm($('.merge-request-form')) new IssuableForm($('.merge-request-form')) when 'projects:tags:new' new ZenMode() diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 1e6724fc92b..88525f4036a 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input js-quick-submit' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f| = render 'shared/issuable/form', f: f, issuable: @merge_request :javascript diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 9e59f7df71b..2f14a91e64f 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -10,7 +10,7 @@ %span.pull-right = link_to 'Change branches', mr_change_branches_path(@merge_request) %hr -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input' } do |f| = render 'shared/issuable/form', f: f, issuable: @merge_request = f.hidden_field :source_project_id = f.hidden_field :source_branch From d67c3e35c98cff2372db803f93a97a60aa8b4104 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 5 Apr 2016 10:42:38 +0100 Subject: [PATCH 150/187] Milestones use new GLForm class --- app/assets/javascripts/dispatcher.js.coffee | 2 +- app/views/groups/milestones/new.html.haml | 4 ++-- app/views/projects/milestones/_form.html.haml | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index d02e1130704..a56019cd886 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -28,7 +28,7 @@ class Dispatcher new Todos() when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() - new DropzoneInput($('.milestone-form')) + new GLForm($('.milestone-form')) when 'groups:milestones:new' new ZenMode() when 'projects:compare:show' diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index 4290e0bf72e..caa8c4bc0ec 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -8,7 +8,7 @@ This will create milestone in every selected project %hr -= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input' } do |f| += form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f| .row - if @milestone.errors.any? #error_explanation @@ -27,7 +27,7 @@ = f.label :description, "Description", class: "control-label" .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea' .clearfix .error-alert .form-group diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index b2dae1c70ee..06b21754797 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -1,6 +1,5 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input'} do |f| += form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input'} do |f| = form_errors(@milestone) - .row .col-md-6 .form-group @@ -11,7 +10,7 @@ = f.label :description, "Description", class: "control-label" .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea' = render 'projects/notes/hints' .clearfix .error-alert From 3de09d1475db3d23f839991e57d16e00bff07cb4 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 5 Apr 2016 12:33:30 +0100 Subject: [PATCH 151/187] Updated all GFM forms to use new GLForm class --- app/assets/javascripts/dispatcher.js.coffee | 6 +++--- app/assets/stylesheets/framework/gfm.scss | 18 ------------------ app/views/projects/_zen.html.haml | 4 ++-- app/views/projects/notes/_edit_form.html.haml | 2 +- app/views/projects/notes/_form.html.haml | 2 +- app/views/projects/releases/edit.html.haml | 12 ++++++------ app/views/projects/tags/new.html.haml | 6 +++--- app/views/projects/wikis/_form.html.haml | 4 ++-- app/views/shared/issuable/_form.html.haml | 3 ++- 9 files changed, 20 insertions(+), 37 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index a56019cd886..0b9110d35fa 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -44,10 +44,10 @@ class Dispatcher new IssuableForm($('.merge-request-form')) when 'projects:tags:new' new ZenMode() - new DropzoneInput($('.tag-form')) + new GLForm($('.tag-form')) when 'projects:releases:edit' new ZenMode() - new DropzoneInput($('.release-form')) + new GLForm($('.release-form')) when 'projects:merge_requests:show' new Diff() shortcut_handler = new ShortcutsIssuable(true) @@ -137,7 +137,7 @@ class Dispatcher new Wikis() shortcut_handler = new ShortcutsNavigation() new ZenMode() - new DropzoneInput($('.wiki-form')) + new GLForm($('.wiki-form')) when 'snippets' shortcut_handler = new ShortcutsNavigation() new ZenMode() if path[2] == 'show' diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss index 5ae0520fd7b..f4d35c4b4b1 100644 --- a/app/assets/stylesheets/framework/gfm.scss +++ b/app/assets/stylesheets/framework/gfm.scss @@ -1,24 +1,6 @@ /** * Styles that apply to all GFM related forms. */ -.issue-form, .merge-request-form, .wiki-form { - .description { - height: 16em; - border-top-left-radius: 0; - } -} - -.wiki-form { - .description { - height: 26em; - } -} - -.milestone-form { - .description { - height: 14em; - } -} .gfm-commit, .gfm-commit_range { font-family: $monospace_font; diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index bddff5cdcbc..e1e35013968 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -1,8 +1,8 @@ .zen-backdrop - classes << ' js-gfm-input js-autosize markdown-area' - if defined?(f) && f - = f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..." + = f.text_area attr, class: classes, placeholder: placeholder - else - = text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..." + = text_area_tag attr, nil, class: classes, placeholder: placeholder %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } = icon('compress') diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index 23e4f93eab5..c87a3fadf72 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -2,7 +2,7 @@ = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f| = note_target_fields(note) = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do - = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field' + = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' .note-form-actions.clearfix diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 261eef4df4f..d0ac380f216 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -8,7 +8,7 @@ = f.hidden_field :noteable_type = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text' + = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' .error-alert diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index c4a3f06ee06..6f0b32aa165 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -9,11 +9,11 @@ %strong #{@tag.name} .prepend-top-default - = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form js-quick-submit' }) do |f| + = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f| = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/notes/hints' - .error-alert - .form-actions.prepend-top-default - = f.submit 'Save changes', class: 'btn btn-save' - = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" + .error-alert + .form-actions.prepend-top-default + = f.submit 'Save changes', class: 'btn btn-save' + = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 77c7c4d23de..b40a6e5cb2d 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -10,7 +10,7 @@ New Tag %hr -= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-quick-submit js-requires-input" do += form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal common-note-form tag-form js-quick-submit js-requires-input" do .form-group = label_tag :tag_name, nil, class: 'control-label' .col-sm-10 @@ -30,9 +30,9 @@ = label_tag :release_description, 'Release notes', class: 'control-label' .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', attr: :release_description, classes: 'description form-control' + = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/notes/hints' - .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. + .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. .form-actions = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 812876e2835..797a1a59e9f 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default js-quick-submit' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f| = form_errors(@page) = f.hidden_field :title, value: @page.title @@ -11,7 +11,7 @@ = f.label :content, class: 'control-label' .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :content, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...' = render 'projects/notes/hints' .clearfix diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index d77c74e5d7a..aed2622a6da 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -29,7 +29,8 @@ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :description, - classes: 'note-textarea' + classes: 'note-textarea', + placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' .clearfix .error-alert From 6e93bd56f0147929006daba6410aa5640403b2c7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 5 Apr 2016 13:23:43 +0100 Subject: [PATCH 152/187] Placeholder on milestone form Updated JS spec to include gl_form --- app/views/projects/milestones/_form.html.haml | 2 +- spec/javascripts/notes_spec.js.coffee | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 06b21754797..687222fa92f 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -10,7 +10,7 @@ = f.label :description, "Description", class: "control-label" .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea' + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' = render 'projects/notes/hints' .clearfix .error-alert diff --git a/spec/javascripts/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee index 050b6e362c6..dd160e821b3 100644 --- a/spec/javascripts/notes_spec.js.coffee +++ b/spec/javascripts/notes_spec.js.coffee @@ -1,4 +1,5 @@ #= require notes +#= require gl_form window.gon = {} window.disableButtonIfEmptyField = -> null From 7a1800fec87133e9971a83b457c969b5adaf5919 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 6 Apr 2016 09:49:29 +0100 Subject: [PATCH 153/187] Fixed group milestones placeholder bug --- app/views/groups/milestones/new.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index caa8c4bc0ec..7d9d27ae1fc 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -27,7 +27,7 @@ = f.label :description, "Description", class: "control-label" .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea' + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' .clearfix .error-alert .form-group From 094cafcaa7c135a839a7329ed06fdd5d19a05f4b Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 13 Apr 2016 08:46:30 +0100 Subject: [PATCH 154/187] Removed GL Actions class --- app/assets/javascripts/gl_form.js.coffee | 11 ----- .../javascripts/gl_form_actions.js.coffee | 43 ------------------- app/views/projects/notes/_hints.html.haml | 3 -- 3 files changed, 57 deletions(-) delete mode 100644 app/assets/javascripts/gl_form_actions.js.coffee diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee index aff3e909ce1..d540cc4dc46 100644 --- a/app/assets/javascripts/gl_form.js.coffee +++ b/app/assets/javascripts/gl_form.js.coffee @@ -11,13 +11,6 @@ class @GLForm @form.data 'gl-form', @ destroy: -> - # Destroy actions - actions = @form.data 'form-actions' - - if actions? - actions.clearEventListeners() - @form.data 'form-actions', null - # Clean form listeners @clearEventListeners() @form.data 'gl-form', null @@ -38,10 +31,6 @@ class @GLForm autosize(@textarea) - # Setup action buttons - actions = new GLFormActions @form, @textarea - @form.data 'form-actions', actions - # form and textarea event listeners @addEventListeners() diff --git a/app/assets/javascripts/gl_form_actions.js.coffee b/app/assets/javascripts/gl_form_actions.js.coffee deleted file mode 100644 index d8de63a2be9..00000000000 --- a/app/assets/javascripts/gl_form_actions.js.coffee +++ /dev/null @@ -1,43 +0,0 @@ -class @GLFormActions - constructor: (@form, @textarea) -> - @clearEventListeners() - @addEventListeners() - - clearEventListeners: -> - @form.off 'click', '.js-toolbar-button' - - addEventListeners: -> - @form.on 'click', '.js-toolbar-button', @toolbarButtonClick - - toolbarButtonClick: (e) => - $btn = $(e.currentTarget) - - # Get the prefix from the button - prefix = $btn.data('prefix') - @addPrefixToTextarea(prefix) - - addPrefixToTextarea: (prefix) -> - caretStart = @textarea.get(0).selectionStart - caretEnd = @textarea.get(0).selectionEnd - textEnd = @textarea.val().length - - beforeSelection = @textarea.val().substring 0, caretStart - afterSelection = @textarea.val().substring caretEnd, textEnd - - beforeSelectionSplit = beforeSelection.split '' - beforeSelectionLength = beforeSelection.length - - # Get the last character in the before selection - beforeSelectionLastChar = beforeSelectionSplit[beforeSelectionLength - 1] - - if beforeSelectionLastChar? and beforeSelectionLastChar isnt '' - # Append a white space char to the prefix if the previous char isn't a space - prefix = " #{prefix}" - - # Update the textarea - @textarea.val beforeSelection + prefix + afterSelection - @textarea.get(0).setSelectionRange caretStart + prefix.length, caretEnd + prefix.length - - # Focus the textarea - @textarea.focus() - @textarea.trigger('keyup') diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml index 0c6758210b1..0b002043408 100644 --- a/app/views/projects/notes/_hints.html.haml +++ b/app/views/projects/notes/_hints.html.haml @@ -1,7 +1,4 @@ .comment-toolbar.clearfix - %button.toolbar-button.js-toolbar-button{ type: 'button', data: { prefix: ':' }, tabindex: '-1' } - = icon('smile-o', class: 'toolbar-button-icon') - Emoji .toolbar-text Styling with = link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1 From c5d7b467ab3c67a71419e314c128d0ab5f371cb5 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 13 Apr 2016 09:08:34 +0100 Subject: [PATCH 155/187] Updated dropzone hover styling --- app/assets/javascripts/dropzone_input.js.coffee | 9 +++++---- app/assets/stylesheets/framework/markdown_area.scss | 13 +++---------- app/assets/stylesheets/framework/typography.scss | 8 -------- app/assets/stylesheets/pages/note_form.scss | 12 ++++++++++++ 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index b502131a99d..6eb8d27ee2b 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -15,11 +15,13 @@ class @DropzoneInput project_uploads_path = window.project_uploads_path or null max_file_size = gon.max_file_size or 10 - form_textarea = $(form).find("textarea.markdown-area") + form_textarea = $(form).find(".js-gfm-input") form_textarea.wrap "
" form_textarea.on 'paste', (event) => handlePaste(event) + $mdArea = $(form_textarea).closest('.md-area') + $(form).setupMarkdownPreview() form_dropzone = $(form).find('.div-dropzone') @@ -49,17 +51,16 @@ class @DropzoneInput $(".div-dropzone-alert").alert "close" dragover: -> - form_textarea.addClass "div-dropzone-focus" + $mdArea.addClass 'is-dropzone-hover' form.find(".div-dropzone-hover").css "opacity", 0.7 return dragleave: -> - form_textarea.removeClass "div-dropzone-focus" + $mdArea.removeClass 'is-dropzone-hover' form.find(".div-dropzone-hover").css "opacity", 0 return drop: -> - form_textarea.removeClass "div-dropzone-focus" form.find(".div-dropzone-hover").css "opacity", 0 form_textarea.focus() return diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index c8f86d60e3b..0f32d36d59c 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -1,22 +1,15 @@ .div-dropzone-wrapper { .div-dropzone { position: relative; - margin-bottom: -5px; - - .div-dropzone-focus { - border-color: #66afe9 !important; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6) !important; - outline: 0 !important; - } .div-dropzone-hover { position: absolute; top: 50%; left: 50%; - margin-top: -0.5em; - margin-left: -0.6em; + margin-top: -11.5px; + margin-left: -15px; opacity: 0; - font-size: 50px; + font-size: 30px; transition: opacity 200ms ease-in-out; pointer-events: none; } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 7b2aada5a0d..0a5b4b8834c 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -250,14 +250,6 @@ a > code { * Textareas intended for GFM * */ -.js-gfm-input { - font-family: $monospace_font; - color: $gl-text-color; -} - -.md-preview { -} - .strikethrough { text-decoration: line-through; } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index f4da17fadaa..0d92ebcc8e9 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -40,6 +40,7 @@ } .note-textarea { + display: block; padding: 10px 0; font-family: $regular_font; border: 0; @@ -72,6 +73,17 @@ } } + &.is-dropzone-hover { + border-color: $gl-success; + box-shadow: 0 0 2px rgba(#000, .2), + 0 0 4px rgba($gl-success, .4); + + .comment-toolbar, + .nav-links { + border-color: $gl-success; + } + } + p { code { white-space: normal; From 8ffb02d9f812c001514ff4c67e2eac53e1d9dd57 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 13 Apr 2016 09:09:10 +0100 Subject: [PATCH 156/187] Added CHANGELOG item --- CHANGELOG | 1 + app/assets/stylesheets/pages/note_form.scss | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ba98b3b100c..e5c8620bebf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -61,6 +61,7 @@ v 8.7.0 (unreleased) - API: User can leave a project through the API when not master or owner. !3613 - Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu) - Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld) + - Improved markdown forms v 8.6.6 - Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk) diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 0d92ebcc8e9..2395de2e17f 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -64,7 +64,7 @@ &.is-focused { border-color: $focus-border-color; - box-shadow: 0 0 2px rgba(#000, .2), + box-shadow: 0 0 2px $black-transparent, 0 0 4px rgba($focus-border-color, .4); .comment-toolbar, @@ -75,7 +75,7 @@ &.is-dropzone-hover { border-color: $gl-success; - box-shadow: 0 0 2px rgba(#000, .2), + box-shadow: 0 0 2px $black-transparent, 0 0 4px rgba($gl-success, .4); .comment-toolbar, From ca01103dedd1a44937068c6834865881ade3d301 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 13 Apr 2016 18:14:01 +0100 Subject: [PATCH 157/187] Focus variable for dropzone --- app/assets/stylesheets/framework/variables.scss | 1 + app/assets/stylesheets/pages/note_form.scss | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 0b6be86ce6a..fcf1f2193b1 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -150,6 +150,7 @@ $light-grey-header: #faf9f9; */ $gl-primary: $blue-normal; $gl-success: $green-normal; +$gl-success-focus: rgba($gl-success, .4) $gl-info: $blue-normal; $gl-warning: $orange-normal; $gl-danger: $red-normal; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 2395de2e17f..07c707e7b77 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -76,7 +76,7 @@ &.is-dropzone-hover { border-color: $gl-success; box-shadow: 0 0 2px $black-transparent, - 0 0 4px rgba($gl-success, .4); + 0 0 4px $gl-success-focus; .comment-toolbar, .nav-links { From 0d8462b897847ae87b131d5111b8772848ba45ab Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 14 Apr 2016 09:11:38 +0100 Subject: [PATCH 158/187] Fixed scss syntax issue --- app/assets/stylesheets/framework/variables.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index fcf1f2193b1..f910cf61817 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -150,7 +150,7 @@ $light-grey-header: #faf9f9; */ $gl-primary: $blue-normal; $gl-success: $green-normal; -$gl-success-focus: rgba($gl-success, .4) +$gl-success-focus: rgba($gl-success, .4); $gl-info: $blue-normal; $gl-warning: $orange-normal; $gl-danger: $red-normal; From 8a8d4c9bf27d17939bfce4b53287210d8108da2c Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 11 Apr 2016 13:19:55 +0100 Subject: [PATCH 159/187] Diff viewer links to correct part of the file Added highlight colours to diff rows Closes #13852 --- .../javascripts/merge_request_tabs.js.coffee | 28 ++++++++++++++++--- app/assets/stylesheets/highlight/dark.scss | 6 ++++ app/assets/stylesheets/highlight/monokai.scss | 6 ++++ .../stylesheets/highlight/solarized_dark.scss | 6 ++++ .../highlight/solarized_light.scss | 6 ++++ app/assets/stylesheets/highlight/white.scss | 6 ++++ .../projects/diffs/_parallel_view.html.haml | 8 +++--- 7 files changed, 58 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index ef0b534a709..d45c772b8de 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -85,8 +85,11 @@ class @MergeRequestTabs scrollToElement: (container) -> if window.location.hash - $el = $("div#{container} #{window.location.hash}") - $('body').scrollTo($el.offset().top) if $el.length + navBarHeight = $('.navbar-gitlab').outerHeight() + + $el = $("#{container} #{window.location.hash}") + $('body').scrollTo 0 + $.scrollTo("#{container} #{window.location.hash}", offset: -navBarHeight) if $el.length # Activate a tab based on the current action activateTab: (action) -> @@ -152,12 +155,29 @@ class @MergeRequestTabs @_get url: "#{source}.json" + @_location.search success: (data) => - document.querySelector("div#diffs").innerHTML = data.html + $('#diffs').html data.html gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')) - $('div#diffs .js-syntax-highlight').syntaxHighlight() + $('#diffs .js-syntax-highlight').syntaxHighlight() @expandViewContainer() if @diffViewType() is 'parallel' @diffsLoaded = true @scrollToElement("#diffs") + @highlighSelectedLine() + + highlighSelectedLine: -> + locationHash = location.hash + + if locationHash isnt '' + hashClassString = ".#{locationHash.replace('#', '')}" + $diffLine = $(locationHash) + + if $diffLine.is ':not(tr)' + $diffLine = $("td#{locationHash}, td#{hashClassString}") + else + $diffLine = $('td', $diffLine) + + $diffLine.addClass 'hll' + diffLineTop = $diffLine.offset().top + navBarHeight = $('.navbar-gitlab').outerHeight() loadBuilds: (source) -> return if @buildsLoaded diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 47673944896..77a73dc379b 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #557; + border-color: darken(#557, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080); } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 806401c21ae..28253d4ccb4 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #49483e; + border-color: darken(#49483e, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080); } diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 6a809d4dfd2..c62bd021aef 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #174652; + border-color: darken(#174652, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46); } diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index c482a1258f7..524cfaf90c3 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #ddd8c5; + border-color: darken(#ddd8c5, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4); } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 28331f59754..1ff6ad75e07 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #f8eec7; + border-color: darken(#f8eec7, 15%); + } + .diff-line-num { &.old { background-color: $line-number-old; diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index d7c49068745..81948513e43 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -14,11 +14,11 @@ %td.new_line.diff-line-num %td.line_content.parallel.match= left[:text] - else - %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]}"} + %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]} #{'empty-cell' if !left[:number]}"} = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(left[:line_code], 'old') - %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) + %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) - if right[:type] == 'new' - new_line_class = 'new' @@ -27,11 +27,11 @@ - new_line_class = nil - new_line_code = left[:line_code] - %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class}", data: { linenumber: right[:number] }} + %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }} = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(right[:line_code], 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) + %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) - if @reply_allowed - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code]) From f3ed4763f54ee46a50d2e7b3b894f895b4067b61 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 12 Apr 2016 09:07:20 +0100 Subject: [PATCH 160/187] Correctly scrolls to the line when clicking --- app/assets/javascripts/merge_request_tabs.js.coffee | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index d45c772b8de..316d1fefa5a 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -88,7 +88,6 @@ class @MergeRequestTabs navBarHeight = $('.navbar-gitlab').outerHeight() $el = $("#{container} #{window.location.hash}") - $('body').scrollTo 0 $.scrollTo("#{container} #{window.location.hash}", offset: -navBarHeight) if $el.length # Activate a tab based on the current action @@ -163,8 +162,17 @@ class @MergeRequestTabs @scrollToElement("#diffs") @highlighSelectedLine() + $(document) + .off 'click', '.diff-content a' + .on 'click', '.diff-content a', (e) => + e.preventDefault() + window.location.hash = $(e.currentTarget).attr 'href' + @highlighSelectedLine() + @scrollToElement("#diffs") + highlighSelectedLine: -> - locationHash = location.hash + $('.hll').removeClass 'hll' + locationHash = window.location.hash if locationHash isnt '' hashClassString = ".#{locationHash.replace('#', '')}" From fd9c3b7c7446d9b8513793606aa2ae0d980ad4b7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 12 Apr 2016 10:06:35 +0100 Subject: [PATCH 161/187] Fixed issue with other links being clicked in diffs --- app/assets/javascripts/merge_request_tabs.js.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 316d1fefa5a..1ab6e5114bc 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -163,8 +163,8 @@ class @MergeRequestTabs @highlighSelectedLine() $(document) - .off 'click', '.diff-content a' - .on 'click', '.diff-content a', (e) => + .off 'click', '.diff-line-num a' + .on 'click', '.diff-line-num a', (e) => e.preventDefault() window.location.hash = $(e.currentTarget).attr 'href' @highlighSelectedLine() From badb35335086b90e2522ea2da767830f2f3ba9a7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 13 Apr 2016 09:23:28 +0100 Subject: [PATCH 162/187] Added CHANGELOG item --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index e5c8620bebf..511889b3765 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -62,6 +62,8 @@ v 8.7.0 (unreleased) - Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu) - Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld) - Improved markdown forms + - Diffs load at the correct point when linking from from number + - Selected diff rows highlight v 8.6.6 - Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk) From 072b9c2c8fe466d43834c16bfbb565043a033fdf Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 14 Apr 2016 15:40:05 +0300 Subject: [PATCH 163/187] Add TOC to yaml README and an intro section [ci skip] --- doc/ci/yaml/README.md | 73 +++++++++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 7da9b31e30d..abb6e97e5e6 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1,5 +1,47 @@ # Configuration of your builds with .gitlab-ci.yml +This document describes the usage of `.gitlab-ci.yml`, the file that is used by +GitLab Runner to manage your project's builds. + +If you want a quick introduction to GitLab CI, follow our +[quick start guide](../quick_start/README.md). + +--- + + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [.gitlab-ci.yml](#gitlab-ci-yml) + - [image and services](#image-and-services) + - [before_script](#before_script) + - [stages](#stages) + - [types](#types) + - [variables](#variables) + - [cache](#cache) + - [cache:key](#cache-key) +- [Jobs](#jobs) + - [script](#script) + - [stage](#stage) + - [only and except](#only-and-except) + - [tags](#tags) + - [when](#when) + - [artifacts](#artifacts) + - [artifacts:name](#artifacts-name) + - [dependencies](#dependencies) +- [Hidden jobs](#hidden-jobs) +- [Special YAML features](#special-yaml-features) + - [Anchors](#anchors) +- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml) +- [Skipping builds](#skipping-builds) +- [Examples](#examples) + + + +--- + +## .gitlab-ci.yml + From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML) file (`.gitlab-ci.yml`) for the project configuration. It is placed in the root of your repository and contains definitions of how your project should be built. @@ -23,12 +65,10 @@ Of course a command can execute code directly (`./configure;make;make install`) or run a script (`test.sh`) in the repository. Jobs are used to create builds, which are then picked up by -[runners](../runners/README.md) and executed within the environment of the -runner. What is important, is that each job is run independently from each +[Runners](../runners/README.md) and executed within the environment of the +Runner. What is important, is that each job is run independently from each other. -## .gitlab-ci.yml - The YAML syntax allows for using more complex job specifications than in the above example: @@ -71,7 +111,7 @@ There are a few reserved `keywords` that **cannot** be used as job names: This allows to specify a custom Docker image and a list of services that can be used for time of the build. The configuration of this feature is covered in -separate document: [Use Docker](../docker/README.md). +[a separate document](../docker/README.md). ### before_script @@ -86,7 +126,8 @@ The specification of `stages` allows for having flexible multi stage pipelines. The ordering of elements in `stages` defines the ordering of builds' execution: 1. Builds of the same stage are run in parallel. -1. Builds of next stage are run after success. +1. Builds of the next stage are run after the jobs from the previous stage + complete successfully. Let's consider the following example, which defines 3 stages: @@ -98,9 +139,9 @@ stages: ``` 1. First all jobs of `build` are executed in parallel. -1. If all jobs of `build` succeeds, the `test` jobs are executed in parallel. -1. If all jobs of `test` succeeds, the `deploy` jobs are executed in parallel. -1. If all jobs of `deploy` succeeds, the commit is marked as `success`. +1. If all jobs of `build` succeed, the `test` jobs are executed in parallel. +1. If all jobs of `test` succeed, the `deploy` jobs are executed in parallel. +1. If all jobs of `deploy` succeed, the commit is marked as `success`. 1. If any of the previous jobs fails, the commit is marked as `failed` and no jobs of further stage are executed. @@ -278,14 +319,14 @@ job_name: | Keyword | Required | Description | |---------------|----------|-------------| -| script | yes | Defines a shell script which is executed by runner | +| script | yes | Defines a shell script which is executed by Runner | | image | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | | services | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | | stage | no | Defines a build stage (default: `test`) | | type | no | Alias for `stage` | | only | no | Defines a list of git refs for which build is created | | except | no | Defines a list of git refs for which build is not created | -| tags | no | Defines a list of tags which are used to select runner | +| tags | no | Defines a list of tags which are used to select Runner | | allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status | | when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` | | dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them| @@ -294,7 +335,7 @@ job_name: ### script -`script` is a shell script which is executed by the runner. For example: +`script` is a shell script which is executed by the Runner. For example: ```yaml job: @@ -375,13 +416,13 @@ except master. ### tags -`tags` is used to select specific runners from the list of all runners that are +`tags` is used to select specific Runners from the list of all Runners that are allowed to run this project. -During the registration of a runner, you can specify the runner's tags, for +During the registration of a Runner, you can specify the Runner's tags, for example `ruby`, `postgres`, `development`. -`tags` allow you to run builds with runners that have the specified tags +`tags` allow you to run builds with Runners that have the specified tags assigned to them: ```yaml @@ -391,7 +432,7 @@ job: - postgres ``` -The specification above, will make sure that `job` is built by a runner that +The specification above, will make sure that `job` is built by a Runner that has both `ruby` AND `postgres` tags defined. ### when From 0385cd5a585572be4d3b72797c14cad23efc48f5 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 13 Apr 2016 21:20:03 +0200 Subject: [PATCH 164/187] Start with iid on branch creation --- app/models/issue.rb | 4 ++-- app/services/merge_requests/build_service.rb | 2 +- app/services/system_note_service.rb | 2 +- doc/workflow/web_editor.md | 2 +- spec/models/issue_spec.rb | 17 ++++++++++++----- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/models/issue.rb b/app/models/issue.rb index e064b0f8b95..3f188e04770 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -106,7 +106,7 @@ class Issue < ActiveRecord::Base def related_branches project.repository.branch_names.select do |branch| - branch.end_with?("-#{iid}") + branch =~ /\A#{iid}-(?!\d+-stable)/i end end @@ -151,7 +151,7 @@ class Issue < ActiveRecord::Base end def to_branch_name - "#{title.parameterize}-#{iid}" + "#{iid}-#{title.parameterize}" end def can_be_worked_on?(current_user) diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 6e9152e444e..fa34753c4fd 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -51,7 +51,7 @@ module MergeRequests # be interpreted as the use wants to close that issue on this project # Pattern example: 112-fix-mep-mep # Will lead to appending `Closes #112` to the description - if match = merge_request.source_branch.match(/-(\d+)\z/) + if match = merge_request.source_branch.match(/\A(\d+)-/) iid = match[1] closes_issue = "Closes ##{iid}" diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 658b086496f..82a0e2fd1f5 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -222,7 +222,7 @@ class SystemNoteService # Called when a branch is created from the 'new branch' button on a issue # Example note text: # - # "Started branch `issue-branch-button-201`" + # "Started branch `201-issue-branch-button`" def self.new_issue_branch(issue, project, author, branch) h = Gitlab::Routing.url_helpers link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md index 5685a9d89dd..1832567a34c 100644 --- a/doc/workflow/web_editor.md +++ b/doc/workflow/web_editor.md @@ -85,7 +85,7 @@ Once you click it, a new branch will be created that diverges from the default branch of your project, by default `master`. The branch name will be based on the title of the issue and as suffix it will have its ID. Thus, the example screenshot above will yield a branch named -`et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum-2`. +`2-et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum`. After the branch is created, you can edit files in the repository to fix the issue. When a merge request is created based on the newly created branch, diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 15052aaca28..fac516f9568 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -191,12 +191,19 @@ describe Issue, models: true do end describe '#related_branches' do - it "selects the right branches" do + it 'selects the right branches' do allow(subject.project.repository).to receive(:branch_names). - and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name]) + and_return(['mpempe', "#{subject.iid}mepmep", subject.to_branch_name]) expect(subject.related_branches).to eq([subject.to_branch_name]) end + + it 'excludes stable branches from the related branches' do + allow(subject.project.repository).to receive(:branch_names). + and_return(["#{subject.iid}-0-stable"]) + + expect(subject.related_branches).to eq [] + end end it_behaves_like 'an editable mentionable' do @@ -210,11 +217,11 @@ describe Issue, models: true do let(:subject) { create :issue } end - describe "#to_branch_name" do + describe '#to_branch_name' do let(:issue) { create(:issue, title: 'a' * 30) } - it "starts with the issue iid" do - expect(issue.to_branch_name).to match /-#{issue.iid}\z/ + it 'starts with the issue iid' do + expect(issue.to_branch_name).to match /\A#{issue.iid}-a+\z/ end end end From a2931a392a4a96ad6df00c98578c193b91e4027f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 14 Apr 2016 14:06:33 +0100 Subject: [PATCH 165/187] Fixed style issues --- app/views/shared/issuable/_sidebar.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index fe6e4128003..56c8eaa0597 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -128,7 +128,7 @@ .title.hide-collapsed Notifications - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' - %button.btn.btn-block.btn-gray.js-subscribe-button.issuable-subscribe-button.hide-collapsed{:type => 'button'} + %button.btn.btn-block.btn-gray.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } %span= subscribed ? 'Unsubscribe' : 'Subscribe' .subscription-status.hide-collapsed{data: {status: subscribtion_status}} .unsubscribed{class: ( 'hidden' if subscribed )} From a54af831bae023770bf9b2633cc45ec0d5f5a66a Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 14 Apr 2016 15:53:54 +0200 Subject: [PATCH 166/187] Use rake db:reset instead of db:setup Using db:reset ensures existing tables are first dropped. This in turn ensures that we can drop tables regardless of any foreign key constraints. While CE currently doesn't have any foreign keys EE defines the following relation: remote_mirrors.project_id -> projects.id MySQL will complain whenever you try to drop the "projects" table first even when using "DROP TABLE ... CASCADE". --- bin/setup | 2 +- doc/development/rake_tasks.md | 2 +- lib/tasks/gitlab/setup.rake | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/setup b/bin/setup index acdb2c1389c..6cb2d7f1e3a 100755 --- a/bin/setup +++ b/bin/setup @@ -18,7 +18,7 @@ Dir.chdir APP_ROOT do # end puts "\n== Preparing database ==" - system "bin/rake db:setup" + system "bin/rake db:reset" puts "\n== Removing old logs and tempfiles ==" system "rm -f log/*" diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index 9f3fd69fc4e..6d04b9590e6 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -9,7 +9,7 @@ bundle exec rake setup ``` The `setup` task is a alias for `gitlab:setup`. -This tasks calls `db:setup` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database. +This tasks calls `db:reset` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database. Note: `db:setup` calls `db:seed` but this does nothing. ## Run tests diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake index 4cbccf2ca89..48baecfd2a2 100644 --- a/lib/tasks/gitlab/setup.rake +++ b/lib/tasks/gitlab/setup.rake @@ -14,7 +14,7 @@ namespace :gitlab do puts "" end - Rake::Task["db:setup"].invoke + Rake::Task["db:reset"].invoke Rake::Task["add_limits_mysql"].invoke Rake::Task["setup_postgresql"].invoke Rake::Task["db:seed_fu"].invoke From 7030ffb0e09dbcc0d03d377846a04fe6cf7d20b6 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 8 Apr 2016 14:43:21 -0700 Subject: [PATCH 167/187] Copying and pasting doesn't grab line numbers or +/- --- app/assets/stylesheets/pages/diff.scss | 20 ++++++++++++++++++++ app/helpers/diff_helper.rb | 1 + app/views/projects/blob/diff.html.haml | 6 +++--- app/views/projects/diffs/_line.html.haml | 6 +++--- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index d0855f66911..ca7fa2094b6 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -67,6 +67,14 @@ line-height: $code_line_height; font-size: $code_font_size; + &.noteable_line.old:before { + content: '-'; + } + + &.noteable_line.new:before { + content: '+'; + } + span { white-space: pre; } @@ -391,3 +399,15 @@ margin-bottom: 0; } } + +.diff-line-num:not(.js-unfold-bottom) { + a { + &:before { + content: attr(data-linenumber); + } + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + } +} diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index ff32e834499..f1e213b34e8 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -44,6 +44,7 @@ module DiffHelper if line.blank? "  ".html_safe else + line[0] = '' line end end diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index abcfca4cd11..ea6d4df7255 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -9,9 +9,9 @@ - line_old = line_new - @form.offset %tr.line_holder %td.old_line.diff-line-num{data: {linenumber: line_old}} - = link_to raw(line_old), "#" - %td.new_line.diff-line-num - = link_to raw(line_new) , "#" + / = link_to raw(line_old), "#" + %td.new_line.diff-line-num{data: {linenumber: line_old}} + / = link_to raw(line_new) , "#" %td.line_content.noteable_line==#{' ' * @form.indent}#{line} - if @form.unfold? && @form.bottom? && @form.to < @blob.loc diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 9464c8dc996..2dc6f548437 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -9,12 +9,12 @@ %td.new_line.diff-line-num %td.line_content.match= line.text - else - %td.old_line.diff-line-num{class: type} + %td.old_line.diff-line-num{class: type, data: {linenumber: line.new_pos}} - link_text = raw(type == "new" ? " " : line.old_pos) - if defined?(plain) && plain = link_text - else - = link_to link_text, "##{line_code}", id: line_code + = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code) %td.new_line.diff-line-num{class: type, data: {linenumber: line.new_pos}} @@ -22,5 +22,5 @@ - if defined?(plain) && plain = link_text - else - = link_to link_text, "##{line_code}", id: line_code + = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } %td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text) From d176f873e1fdf23ebeeebf6d2d25927941016390 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 13 Apr 2016 12:54:21 -0500 Subject: [PATCH 168/187] Add content attributes to discussion diffs; change styling of add-note icon to prevent white spaces in paste --- app/assets/stylesheets/pages/diff.scss | 26 +++++++++++++------ app/assets/stylesheets/pages/notes.scss | 10 ++----- app/views/projects/blob/diff.html.haml | 4 +-- .../notes/discussions/_diff.html.haml | 6 ++--- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index ca7fa2094b6..bd7640db3b7 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -69,10 +69,12 @@ &.noteable_line.old:before { content: '-'; + position: absolute; } &.noteable_line.new:before { content: '+'; + position: absolute; } span { @@ -400,14 +402,22 @@ } } -.diff-line-num:not(.js-unfold-bottom) { - a { - &:before { - content: attr(data-linenumber); +.file-holder { + .diff-line-num:not(.js-unfold-bottom) { + a { + &:before { + content: attr(data-linenumber); + } + } + } +} + +.discussion { + .diff-content { + .diff-line-num { + &:before { + content: attr(data-linenumber); + } } - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index e421a31549a..ce44f5aa13b 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -276,8 +276,7 @@ ul.notes { .diff-file tr.line_holder { @mixin show-add-diff-note { - filter: alpha(opacity=100); - opacity: 1.0; + display: inline-block; } .add-diff-note { @@ -291,13 +290,8 @@ ul.notes { position: absolute; z-index: 10; width: 32px; - - transition: all 0.2s ease; - // "hide" it by default - opacity: 0.0; - filter: alpha(opacity=0); - + display: none; &:hover { background: $gl-info; color: #fff; diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index ea6d4df7255..c6ed78aadf6 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -9,9 +9,9 @@ - line_old = line_new - @form.offset %tr.line_holder %td.old_line.diff-line-num{data: {linenumber: line_old}} - / = link_to raw(line_old), "#" + = link_to raw(line_old), "#" %td.new_line.diff-line-num{data: {linenumber: line_old}} - / = link_to raw(line_new) , "#" + = link_to raw(line_new) , "#" %td.line_content.noteable_line==#{' ' * @form.indent}#{line} - if @form.unfold? && @form.bottom? && @form.to < @blob.loc diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml index 820e31ccd61..6abfb3abc3b 100644 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ b/app/views/projects/notes/discussions/_diff.html.haml @@ -20,10 +20,8 @@ %td.new_line.diff-line-num= "..." %td.line_content.match= line.text - else - %td.old_line.diff-line-num - = raw(type == "new" ? " " : line.old_pos) - %td.new_line.diff-line-num - = raw(type == "old" ? " " : line.new_pos) + %td.old_line.diff-line-num{data: {linenumber: raw(type == "new" ? " " : line.old_pos)}} + %td.new_line.diff-line-num{data: {linenumber: raw(type == "old" ? " " : line.new_pos)}} %td.line_content{class: "noteable_line #{type} #{line_code}", line_code: line_code}= diff_line_content(line.text) - if line_code == note.line_code From 82d0221b632fbb2e7711678b11e9ff26214d9d69 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 13 Apr 2016 15:33:44 -0500 Subject: [PATCH 169/187] Add line type conditional to diff line helper --- app/assets/stylesheets/pages/diff.scss | 18 +++++++++++------- app/helpers/diff_helper.rb | 6 ++++-- app/views/projects/diffs/_line.html.haml | 2 +- .../projects/notes/discussions/_diff.html.haml | 2 +- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index bd7640db3b7..77d7a3024d5 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -67,14 +67,18 @@ line-height: $code_line_height; font-size: $code_font_size; - &.noteable_line.old:before { - content: '-'; - position: absolute; + &.noteable_line.old { + &:before { + content: '-'; + position: absolute; + } } - &.noteable_line.new:before { - content: '+'; - position: absolute; + &.noteable_line.new { + &:before { + content: '+'; + position: absolute; + } } span { @@ -406,7 +410,7 @@ .diff-line-num:not(.js-unfold-bottom) { a { &:before { - content: attr(data-linenumber); + content: attr(data-linenumber); } } } diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index f1e213b34e8..0504cfb7591 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -40,11 +40,13 @@ module DiffHelper (unfold) ? 'unfold js-unfold' : '' end - def diff_line_content(line) + def diff_line_content(line, line_type = nil) if line.blank? "  ".html_safe else - line[0] = '' + if line_type == 'new' || line_type == 'old' + line[0] = " " + end line end end diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 2dc6f548437..6c5602acd43 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -23,4 +23,4 @@ = link_text - else = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - %td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text) + %td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text, type) diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml index 6abfb3abc3b..9fd9d5bb2aa 100644 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ b/app/views/projects/notes/discussions/_diff.html.haml @@ -22,7 +22,7 @@ - else %td.old_line.diff-line-num{data: {linenumber: raw(type == "new" ? " " : line.old_pos)}} %td.new_line.diff-line-num{data: {linenumber: raw(type == "old" ? " " : line.new_pos)}} - %td.line_content{class: "noteable_line #{type} #{line_code}", line_code: line_code}= diff_line_content(line.text) + %td.line_content{class: "noteable_line #{type} #{line_code}", line_code: line_code}= diff_line_content(line.text, type) - if line_code == note.line_code = render "projects/notes/diff_notes_with_reply", notes: discussion_notes From 2f81baf4767df7ce0144a63b4e451b7d8e09f86a Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 13 Apr 2016 18:40:14 -0500 Subject: [PATCH 170/187] Update click_diff_line --- features/steps/shared/diff_note.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 1448c3f44cc..e846c52d474 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -227,7 +227,7 @@ module SharedDiffNote end def click_diff_line(code) - find("button[data-line-code='#{code}']").click + find("button[data-line-code='#{code}']").trigger('click') end def click_parallel_diff_line(code, line_type) From 0c082d5e3a34d787f8b2fea0c22fa4256cf82be3 Mon Sep 17 00:00:00 2001 From: connorshea Date: Tue, 12 Apr 2016 20:38:14 -0600 Subject: [PATCH 171/187] Fix the improper delete form being rendered for an oauth_authorized_application This fixes the authorized applications not being revoked properly at `/profile/applications`. Fixes #14370. --- app/views/doorkeeper/applications/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index 55f4a6f287d..0aff79749ef 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -68,7 +68,7 @@ %td= app.name %td= token.created_at %td= token.scopes - %td= render 'delete_form', application: app + %td= render 'doorkeeper/authorized_applications/delete_form', application: app - @authorized_anonymous_tokens.each do |token| %tr %td From c7e384aab23301ad0ee3559252324fa957d15db3 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 12 Apr 2016 22:39:18 -0700 Subject: [PATCH 172/187] Add spec for deletion of authorized OAuth2 application Closes #14370 Move gon function into its own helper --- CHANGELOG | 1 + app/controllers/application_controller.rb | 14 ------- .../oauth/applications_controller.rb | 1 + app/models/oauth_access_token.rb | 19 +++++++++ lib/gitlab/current_settings.rb | 2 + lib/gitlab/gon_helper.rb | 17 ++++++++ spec/factories/oauth_access_tokens.rb | 23 +++++++++++ spec/factories/oauth_applications.rb | 9 +++++ spec/factories/users.rb | 2 +- .../profiles/oauth_applications_spec.rb | 39 +++++++++++++++++++ 10 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 app/models/oauth_access_token.rb create mode 100644 lib/gitlab/gon_helper.rb create mode 100644 spec/factories/oauth_access_tokens.rb create mode 100644 spec/factories/oauth_applications.rb create mode 100644 spec/features/profiles/oauth_applications_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 21f24b5b61a..4db4f01bcbe 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.7.0 (unreleased) - The Projects::HousekeepingService class has extra instrumentation (Yorick Peterse) + - Fix revoking of authorized OAuth applications (Connor Shea) - All service classes (those residing in app/services) are now instrumented (Yorick Peterse) - Developers can now add custom tags to transactions (Yorick Peterse) - Loading of an issue's referenced merge requests and related branches is now done asynchronously (Yorick Peterse) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 97d53acde94..bdf2dd68531 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -158,20 +158,6 @@ class ApplicationController < ActionController::Base end end - def add_gon_variables - gon.api_version = API::API.version - gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s - gon.default_issues_tracker = Project.new.default_issue_tracker.to_param - gon.max_file_size = current_application_settings.max_attachment_size - gon.relative_url_root = Gitlab.config.gitlab.relative_url_root - gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class - - if current_user - gon.current_user_id = current_user.id - gon.api_token = current_user.private_token - end - end - def validate_user_service_ticket! return unless signed_in? && session[:service_tickets] diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index d1e4ac10f6c..377fef65a92 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -4,6 +4,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController before_action :verify_user_oauth_applications_enabled before_action :authenticate_user! + before_action :add_gon_variables layout 'profile' diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb new file mode 100644 index 00000000000..c78c7f4aa0e --- /dev/null +++ b/app/models/oauth_access_token.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: oauth_access_tokens +# +# id :integer not null, primary key +# resource_owner_id :integer +# application_id :integer +# token :string not null +# refresh_token :string +# expires_in :integer +# revoked_at :datetime +# created_at :datetime not null +# scopes :string +# + +class OauthAccessToken < ActiveRecord::Base + belongs_to :resource_owner, class_name: 'User' + belongs_to :application, class_name: 'Doorkeeper::Application' +end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 1acc22fe5bf..71b53136ed2 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -1,5 +1,7 @@ module Gitlab module CurrentSettings + include ::Gitlab::GonHelper + def current_application_settings key = :current_application_settings diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb new file mode 100644 index 00000000000..5ebaad6ca6e --- /dev/null +++ b/lib/gitlab/gon_helper.rb @@ -0,0 +1,17 @@ +module Gitlab + module GonHelper + def add_gon_variables + gon.api_version = API::API.version + gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s + gon.default_issues_tracker = Project.new.default_issue_tracker.to_param + gon.max_file_size = current_application_settings.max_attachment_size + gon.relative_url_root = Gitlab.config.gitlab.relative_url_root + gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class + + if current_user + gon.current_user_id = current_user.id + gon.api_token = current_user.private_token + end + end + end +end diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb new file mode 100644 index 00000000000..4bbc7d3a554 --- /dev/null +++ b/spec/factories/oauth_access_tokens.rb @@ -0,0 +1,23 @@ +# == Schema Information +# +# Table name: oauth_access_tokens +# +# id :integer not null, primary key +# resource_owner_id :integer +# application_id :integer +# token :string not null +# refresh_token :string +# expires_in :integer +# revoked_at :datetime +# created_at :datetime not null +# scopes :string +# + +FactoryGirl.define do + factory :oauth_access_token do + resource_owner + application + token '123456' + created_at :datetime + end +end diff --git a/spec/factories/oauth_applications.rb b/spec/factories/oauth_applications.rb new file mode 100644 index 00000000000..d116a573830 --- /dev/null +++ b/spec/factories/oauth_applications.rb @@ -0,0 +1,9 @@ +FactoryGirl.define do + factory :oauth_application, class: 'Doorkeeper::Application', aliases: [:application] do + name { FFaker::Name.name } + uid { FFaker::Name.name } + redirect_uri { FFaker::Internet.uri('http') } + owner + owner_type 'User' + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index a5c60c51c5b..a9b2148bd2a 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,7 +1,7 @@ FactoryGirl.define do sequence(:name) { FFaker::Name.name } - factory :user, aliases: [:author, :assignee, :recipient, :owner, :creator] do + factory :user, aliases: [:author, :assignee, :recipient, :owner, :creator, :resource_owner] do email { FFaker::Internet.email } name sequence(:username) { |n| "#{FFaker::Internet.user_name}#{n}" } diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb new file mode 100644 index 00000000000..1a5a9059dbd --- /dev/null +++ b/spec/features/profiles/oauth_applications_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe 'Profile > Applications', feature: true do + let(:user) { create(:user) } + + before do + login_as(user) + end + + describe 'User manages applications', js: true do + it 'deletes an application' do + create(:oauth_application, owner: user) + visit oauth_applications_path + + page.within('.oauth-applications') do + expect(page).to have_content('Your applications (1)') + click_button 'Destroy' + end + + expect(page).to have_content('The application was deleted successfully') + expect(page).to have_content('Your applications (0)') + expect(page).to have_content('Authorized applications (0)') + end + + it 'deletes an authorized application' do + create(:oauth_access_token, resource_owner: user) + visit oauth_applications_path + + page.within('.oauth-authorized-applications') do + expect(page).to have_content('Authorized applications (1)') + click_button 'Revoke' + end + + expect(page).to have_content('The application was revoked access.') + expect(page).to have_content('Your applications (0)') + expect(page).to have_content('Authorized applications (0)') + end + end +end From e450892f5688529b8a49e3ae7598f00dbdda7161 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 14 Apr 2016 05:28:46 -0700 Subject: [PATCH 173/187] Include GonHelper separately and remove created_at in factory --- app/controllers/application_controller.rb | 1 + app/controllers/oauth/applications_controller.rb | 1 + lib/gitlab/current_settings.rb | 2 -- spec/factories/oauth_access_tokens.rb | 1 - 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bdf2dd68531..ce5c84ee9bc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -3,6 +3,7 @@ require 'fogbugz' class ApplicationController < ActionController::Base include Gitlab::CurrentSettings + include Gitlab::GonHelper include GitlabRoutingHelper include PageLayoutHelper diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index 377fef65a92..c6bdd0602c1 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -1,5 +1,6 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController include Gitlab::CurrentSettings + include Gitlab::GonHelper include PageLayoutHelper before_action :verify_user_oauth_applications_enabled diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 71b53136ed2..1acc22fe5bf 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -1,7 +1,5 @@ module Gitlab module CurrentSettings - include ::Gitlab::GonHelper - def current_application_settings key = :current_application_settings diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb index 4bbc7d3a554..7700b15d538 100644 --- a/spec/factories/oauth_access_tokens.rb +++ b/spec/factories/oauth_access_tokens.rb @@ -18,6 +18,5 @@ FactoryGirl.define do resource_owner application token '123456' - created_at :datetime end end From a8ea2c18959c700913132d8a2d7ce3a211303c10 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 14 Apr 2016 11:34:42 -0300 Subject: [PATCH 174/187] Change transfer service to use existing methods --- app/services/projects/transfer_service.rb | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 0d8f8c6fbee..79a27f4af7e 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -35,11 +35,9 @@ module Projects end # Apply new namespace id and visibility level - project.tap do |p| - p.namespace = new_namespace - setup_visibility_level(p, new_namespace) - p.save! - end + project.namespace = new_namespace + project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group? + project.save! # Notifications project.send_move_instructions(old_path) @@ -71,15 +69,5 @@ module Projects namespace.id != project.namespace_id && current_user.can?(:create_projects, namespace) end - - private - - def setup_visibility_level(project, new_namespace) - return unless new_namespace.is_a?(Group) - - if project.visibility_level > new_namespace.visibility_level - project.visibility_level = new_namespace.visibility_level - end - end end end From 91cebb7289df872ae4887f7103f68a6f948ee61c Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 14 Apr 2016 17:04:18 +0200 Subject: [PATCH 175/187] Documentation / help improvements --- app/views/admin/projects/show.html.haml | 4 +++- doc/README.md | 1 + doc/administration/repository_checks.md | 7 +++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 1e44172f066..73986d21bcf 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -10,7 +10,7 @@ .col-md-12 .panel .panel-heading.alert.alert-danger - Last repository check + Last repository check = "(#{time_ago_in_words(@project.last_repository_check_at)} ago)" failed. See = link_to 'repocheck.log', admin_logs_path @@ -126,6 +126,8 @@ - else passed. + = link_to icon('question-circle'), help_page_path('administration', 'repository_checks') + .form-group = f.submit 'Trigger repository check', class: 'btn btn-primary' diff --git a/doc/README.md b/doc/README.md index d2660930653..e6ac4794827 100644 --- a/doc/README.md +++ b/doc/README.md @@ -31,6 +31,7 @@ - [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 webhook setup and the importing of projects. +- [Repository checks](administration/repository_checks.md) Periodic Git repository checks - [Security](security/README.md) Learn what you can do to further secure your GitLab instance. - [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed. - [Update](update/README.md) Update guides to upgrade your installation. diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md index 16f4627a596..61bf8ce6161 100644 --- a/doc/administration/repository_checks.md +++ b/doc/administration/repository_checks.md @@ -1,10 +1,9 @@ # Repository checks -_**Note:** This feature was [introduced][ce-3232] in GitLab 8.7_ +>**Note:** +This feature was [introduced][ce-3232] in GitLab 8.7. ---- - -Git has a built-in mechanism, \[git fsck\]\[git-fsck\], to verify the +Git has a built-in mechanism, [git fsck][git-fsck], to verify the integrity of all data commited to a repository. GitLab administrators can trigger such a check for a project via the project page under the admin panel. The checks run asynchronously so it may take a few minutes From 77daebe660eb3f4189ca6339e0fcd634abffe3fa Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 14 Apr 2016 17:05:41 +0200 Subject: [PATCH 176/187] More create_list --- spec/mailers/repository_check_mailer_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/mailers/repository_check_mailer_spec.rb b/spec/mailers/repository_check_mailer_spec.rb index 6ae9a93aaac..583bf15176f 100644 --- a/spec/mailers/repository_check_mailer_spec.rb +++ b/spec/mailers/repository_check_mailer_spec.rb @@ -5,7 +5,7 @@ describe RepositoryCheckMailer do describe '.notify' do it 'emails all admins' do - admins = 3.times.map { create(:admin) } + admins = create_list(:admin, 3) mail = described_class.notify(1) From b49069b72cbfa73b6f7bb7195659ba2958f02f7e Mon Sep 17 00:00:00 2001 From: Jamie Neubert Pedersen Date: Thu, 14 Apr 2016 16:04:18 +0000 Subject: [PATCH 177/187] change: "very demand" changed to "specific demand" This must be an error as the sentence doesn't make sense otherwise --- doc/ci/runners/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index 295d953db11..c7df0713a3d 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -19,7 +19,7 @@ many projects, you can have a single or a small number of runners that handle multiple projects. This makes it easier to maintain and update runners. **Specific runners** are useful for jobs that have special requirements or for -projects with a very demand. If a job has certain requirements, you can set +projects with a specific demand. If a job has certain requirements, you can set up the specific runner with this in mind, while not having to do this for all runners. For example, if you want to deploy a certain project, you can setup a specific runner to have the right credentials for this. From 7ef35b9c61b61b6cf03f20ce4ab34272404dca40 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 14 Apr 2016 12:28:48 -0400 Subject: [PATCH 178/187] Shut up, Rubocop --- app/workers/repository_check/clear_worker.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb index 9c3347a7040..b7202ddff34 100644 --- a/app/workers/repository_check/clear_worker.rb +++ b/app/workers/repository_check/clear_worker.rb @@ -1,9 +1,9 @@ module RepositoryCheck class ClearWorker include Sidekiq::Worker - + sidekiq_options retry: false - + def perform # Do small batched updates because these updates will be slow and locking Project.select(:id).find_in_batches(batch_size: 100) do |batch| @@ -14,4 +14,4 @@ module RepositoryCheck end end end -end \ No newline at end of file +end From cce21e7490d8684fb8fbaa6c9d5a8ce76c36d975 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 14 Apr 2016 12:32:33 -0400 Subject: [PATCH 179/187] Update CHANGELOG for "Auto git fsck" --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 41b6e2c67fd..1f94a574d5b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ v 8.7.0 (unreleased) - Add endpoints to archive or unarchive a project !3372 - Add links to CI setup documentation from project settings and builds pages - Handle nil descriptions in Slack issue messages (Stan Hu) + - Add automated repository integrity checks - API: Expose open_issues_count, closed_issues_count, open_merge_requests_count for labels (Robert Schilling) - API: Ability to star and unstar a project (Robert Schilling) - Add default scope to projects to exclude projects pending deletion From 2165bbc7853016ea68f36b44ad0590623add7bcf Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 14 Apr 2016 20:31:19 +0300 Subject: [PATCH 180/187] Remove deprecated NGINX CI config --- lib/support/nginx/gitlab_ci | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 lib/support/nginx/gitlab_ci diff --git a/lib/support/nginx/gitlab_ci b/lib/support/nginx/gitlab_ci deleted file mode 100644 index bf05edfd780..00000000000 --- a/lib/support/nginx/gitlab_ci +++ /dev/null @@ -1,29 +0,0 @@ -# GITLAB CI -server { - listen 80 default_server; # e.g., listen 192.168.1.1:80; - server_name YOUR_CI_SERVER_FQDN; # e.g., server_name source.example.com; - - access_log /var/log/nginx/gitlab_ci_access.log; - error_log /var/log/nginx/gitlab_ci_error.log; - - # expose API to fix runners - location /api { - proxy_read_timeout 300; - proxy_connect_timeout 300; - proxy_redirect off; - proxy_set_header X-Real-IP $remote_addr; - - # You need to specify your DNS servers that are able to resolve YOUR_GITLAB_SERVER_FQDN - resolver 8.8.8.8 8.8.4.4; - proxy_pass $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri; - } - - # redirect all other CI requests - location / { - return 301 $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri; - } - - # adjust this to match the largest build log your runners might submit, - # set to 0 to disable limit - client_max_body_size 10m; -} \ No newline at end of file From 80d8f8b87609ffb9b0fcc1f74fbeb4520a193e07 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 14 Apr 2016 09:10:36 -0500 Subject: [PATCH 181/187] Syntax & style updates --- app/assets/stylesheets/pages/diff.scss | 24 +++++++++++-------- app/helpers/diff_helper.rb | 4 +--- app/views/projects/blob/diff.html.haml | 12 +++++----- app/views/projects/diffs/_line.html.haml | 16 ++++++------- .../notes/discussions/_diff.html.haml | 6 ++--- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 77d7a3024d5..183f22a1b24 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -67,17 +67,21 @@ line-height: $code_line_height; font-size: $code_font_size; - &.noteable_line.old { - &:before { - content: '-'; - position: absolute; - } - } + &.noteable_line { + position: relative; - &.noteable_line.new { - &:before { - content: '+'; - position: absolute; + &.old { + &:before { + content: '-'; + position: absolute; + } + } + + &.new { + &:before { + content: '+'; + position: absolute; + } } } diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 0504cfb7591..6a3ec83b8c0 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -44,9 +44,7 @@ module DiffHelper if line.blank? "  ".html_safe else - if line_type == 'new' || line_type == 'old' - line[0] = " " - end + line[0] = ' ' if %w[new old].include?(line_type) line end end diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index c6ed78aadf6..38e62c81fed 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -1,20 +1,20 @@ - if @lines.present? - if @form.unfold? && @form.since != 1 && !@form.bottom? %tr.line_holder{ id: @form.since } - = render "projects/diffs/match_line", {line: @match_line, - line_old: @form.since, line_new: @form.since, bottom: false, new_file: false} + = render "projects/diffs/match_line", { line: @match_line, + line_old: @form.since, line_new: @form.since, bottom: false, new_file: false } - @lines.each_with_index do |line, index| - line_new = index + @form.since - line_old = line_new - @form.offset %tr.line_holder - %td.old_line.diff-line-num{data: {linenumber: line_old}} + %td.old_line.diff-line-num{ data: { linenumber: line_old } } = link_to raw(line_old), "#" - %td.new_line.diff-line-num{data: {linenumber: line_old}} + %td.new_line.diff-line-num{ data: { linenumber: line_old } } = link_to raw(line_new) , "#" %td.line_content.noteable_line==#{' ' * @form.indent}#{line} - if @form.unfold? && @form.bottom? && @form.to < @blob.loc %tr.line_holder{ id: @form.to } - = render "projects/diffs/match_line", {line: @match_line, - line_old: @form.to, line_new: @form.to, bottom: true, new_file: false} + = render "projects/diffs/match_line", { line: @match_line, + line_old: @form.to, line_new: @form.to, bottom: true, new_file: false } diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 6c5602acd43..107097ad963 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -1,26 +1,26 @@ - type = line.type -%tr.line_holder{id: line_code, class: type} +%tr.line_holder{ id: line_code, class: type } - case type - when 'match' - = render "projects/diffs/match_line", {line: line.text, - line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file} + = render "projects/diffs/match_line", { line: line.text, + line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file } - when 'nonewline' %td.old_line.diff-line-num %td.new_line.diff-line-num %td.line_content.match= line.text - else - %td.old_line.diff-line-num{class: type, data: {linenumber: line.new_pos}} - - link_text = raw(type == "new" ? " " : line.old_pos) + %td.old_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } + - link_text = type == "new" ? " ".html_safe : line.old_pos - if defined?(plain) && plain = link_text - else = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code) - %td.new_line.diff-line-num{class: type, data: {linenumber: line.new_pos}} - - link_text = raw(type == "old" ? " " : line.new_pos) + %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } + - link_text = type == "old" ? " ".html_safe : line.new_pos - if defined?(plain) && plain = link_text - else = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - %td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text, type) + %td.line_content{ class: ['noteable_line', type, line_code], data: { line_code: line_code } }= diff_line_content(line.text, type) diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml index 9fd9d5bb2aa..d46aab000c3 100644 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ b/app/views/projects/notes/discussions/_diff.html.haml @@ -20,9 +20,9 @@ %td.new_line.diff-line-num= "..." %td.line_content.match= line.text - else - %td.old_line.diff-line-num{data: {linenumber: raw(type == "new" ? " " : line.old_pos)}} - %td.new_line.diff-line-num{data: {linenumber: raw(type == "old" ? " " : line.new_pos)}} - %td.line_content{class: "noteable_line #{type} #{line_code}", line_code: line_code}= diff_line_content(line.text, type) + %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? " ".html_safe : line.old_pos } } + %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? " ".html_safe : line.new_pos } } + %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type) - if line_code == note.line_code = render "projects/notes/diff_notes_with_reply", notes: discussion_notes From 6ae2680ce7159eedd37e89c7aa99688949139f11 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 14 Apr 2016 19:38:17 +0300 Subject: [PATCH 182/187] Emoji categories fix --- CHANGELOG | 1 + lib/award_emoji.rb | 16 +++++++++++++--- spec/lib/award_emoji_spec.rb | 7 +++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9aa40fae18e..4a9a5c1bc36 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -66,6 +66,7 @@ v 8.7.0 (unreleased) - Improved markdown forms - Diffs load at the correct point when linking from from number - Selected diff rows highlight + - Fix emoji catgories in the emoji picker v 8.6.6 - Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk) diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb index 4fc3443ac68..5f8ff01b0a9 100644 --- a/lib/award_emoji.rb +++ b/lib/award_emoji.rb @@ -14,19 +14,29 @@ class AwardEmoji food_drink: "Food" }.with_indifferent_access + CATEGORY_ALIASES = { + symbols: "objects_symbols", + foods: "food_drink", + travel: "travel_places" + }.with_indifferent_access + def self.normilize_emoji_name(name) aliases[name] || name end def self.emoji_by_category unless @emoji_by_category - @emoji_by_category = {} + @emoji_by_category = Hash.new { |h, key| h[key] = [] } emojis.each do |emoji_name, data| data["name"] = emoji_name - @emoji_by_category[data["category"]] ||= [] - @emoji_by_category[data["category"]] << data + # Skip Fitzpatrick(tone) modifiers + next if data["category"] == "modifier" + + category = CATEGORY_ALIASES[data["category"]] || data["category"] + + @emoji_by_category[category] << data end @emoji_by_category = @emoji_by_category.sort.to_h diff --git a/spec/lib/award_emoji_spec.rb b/spec/lib/award_emoji_spec.rb index 330678f7f16..88c22912950 100644 --- a/spec/lib/award_emoji_spec.rb +++ b/spec/lib/award_emoji_spec.rb @@ -16,4 +16,11 @@ describe AwardEmoji do end end end + + describe '.emoji_by_category' do + it "only contains known categories" do + undefined_categories = AwardEmoji.emoji_by_category.keys - AwardEmoji::CATEGORIES.keys + expect(undefined_categories).to be_empty + end + end end From f58312976733fadb2b0cbf4b734f8d94220cb501 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 14 Apr 2016 13:56:30 -0400 Subject: [PATCH 183/187] Add Sentry program context even without a current user --- app/controllers/application_controller.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ce5c84ee9bc..1c53b0b21a3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -14,7 +14,7 @@ class ApplicationController < ActionController::Base before_action :check_password_expiration before_action :check_2fa_requirement before_action :ldap_security_check - before_action :sentry_user_context + before_action :sentry_context before_action :default_headers before_action :add_gon_variables before_action :configure_permitted_parameters, if: :devise_controller? @@ -41,13 +41,15 @@ class ApplicationController < ActionController::Base protected - def sentry_user_context - if Rails.env.production? && current_application_settings.sentry_enabled && current_user - Raven.user_context( - id: current_user.id, - email: current_user.email, - username: current_user.username, - ) + def sentry_context + if Rails.env.production? && current_application_settings.sentry_enabled + if current_user + Raven.user_context( + id: current_user.id, + email: current_user.email, + username: current_user.username, + ) + end Raven.tags_context(program: sentry_program_context) end From 5bf47f14b962b0e676e399dddc238d7bf764304d Mon Sep 17 00:00:00 2001 From: theoretick Date: Thu, 7 Apr 2016 15:49:35 -0700 Subject: [PATCH 184/187] Do not include award emojis in issue view comment_count Fixes Issue #14431 --- CHANGELOG | 1 + app/views/projects/issues/_issue.html.haml | 2 +- .../merge_requests/_merge_request.html.haml | 2 +- .../projects/merge_requests/_show.html.haml | 2 +- spec/features/issues_spec.rb | 16 ++++++++++-- spec/features/notes_on_merge_requests_spec.rb | 25 +++++++++++++++++++ 6 files changed, 43 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ec210867e7f..b67a12eaee3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ v 8.7.0 (unreleased) - Loading of an issue's referenced merge requests and related branches is now done asynchronously (Yorick Peterse) - Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea) - Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea) + - Do not include award_emojis in issue and merge_request comment_count !3610 (Lucas Charles) - All images in discussions and wikis now link to their source files !3464 (Connor Shea). - Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu) - Add setting for customizing the list of trusted proxies !3524 diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 4aa92d0b39e..7a8009f6da4 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -27,7 +27,7 @@ = icon('thumbs-down') = downvotes - - note_count = issue.notes.user.count + - note_count = issue.notes.user.nonawards.count - if note_count > 0 %li = link_to issue_path(issue) + "#notes" do diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 391193eed6c..e740fe8c84d 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -35,7 +35,7 @@ = icon('thumbs-down') = downvotes - - note_count = merge_request.mr_and_commit_notes.user.count + - note_count = merge_request.mr_and_commit_notes.user.nonawards.count - if note_count > 0 %li = link_to merge_request_path(merge_request) + "#notes" do diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 1dd8f721f7e..07037a14f51 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -51,7 +51,7 @@ %li.notes-tab = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.count + %span.badge= @merge_request.mr_and_commit_notes.user.nonawards.count %li.commits-tab = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 79000666ccc..1ce0024e93c 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -45,7 +45,7 @@ describe 'Issues', feature: true do project: project) end - it 'allows user to select unasigned', js: true do + it 'allows user to select unassigned', js: true do visit edit_namespace_project_issue_path(project.namespace, project, issue) expect(page).to have_content "Assignee #{@user.name}" @@ -64,6 +64,18 @@ describe 'Issues', feature: true do end end + describe 'Issue info' do + it 'excludes award_emoji from comment count' do + issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar') + create(:upvote_note, noteable: issue) + + visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id) + + expect(page).to have_content 'foobar' + expect(page.all('.issue-no-comments').first.text).to eq "0" + end + end + describe 'Filter issue' do before do ['foobar', 'barbaz', 'gitlab'].each do |title| @@ -187,7 +199,7 @@ describe 'Issues', feature: true do describe 'update assignee from issue#show' do let(:issue) { create(:issue, project: project, author: @user, assignee: @user) } - context 'by autorized user' do + context 'by authorized user' do it 'allows user to select unassigned', js: true do visit namespace_project_issue_path(project.namespace, project, issue) diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 5f855ccc701..389812ff7e1 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -4,6 +4,20 @@ describe 'Comments', feature: true do include RepoHelpers include WaitForAjax + describe 'On merge requests page', feature: true do + it 'excludes award_emoji from comment count' do + merge_request = create(:merge_request) + project = merge_request.source_project + create(:upvote_note, noteable: merge_request, project: project) + + login_as :admin + visit namespace_project_merge_requests_path(project.namespace, project) + + expect(merge_request.mr_and_commit_notes.count).to eq 1 + expect(page.all('.merge-request-no-comments').first.text).to eq "0" + end + end + describe 'On a merge request', js: true, feature: true do let!(:merge_request) { create(:merge_request) } let!(:project) { merge_request.source_project } @@ -129,6 +143,17 @@ describe 'Comments', feature: true do end end end + + describe 'comment info' do + it 'excludes award_emoji from comment count' do + create(:upvote_note, noteable: merge_request, project: project) + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + + expect(merge_request.mr_and_commit_notes.count).to eq 2 + expect(find('.notes-tab span.badge').text).to eq "1" + end + end end describe 'On a merge request diff', js: true, feature: true do From 70c9ceb7b1bd815f4a3e79a809a9279f841cee63 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 14 Apr 2016 18:46:18 -0300 Subject: [PATCH 185/187] Fix datetime format when migrating new notification settings on MySQL --- db/migrate/20160328115649_migrate_new_notification_setting.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20160328115649_migrate_new_notification_setting.rb b/db/migrate/20160328115649_migrate_new_notification_setting.rb index 0a110869027..3c81b2c37bf 100644 --- a/db/migrate/20160328115649_migrate_new_notification_setting.rb +++ b/db/migrate/20160328115649_migrate_new_notification_setting.rb @@ -7,7 +7,7 @@ # class MigrateNewNotificationSetting < ActiveRecord::Migration def up - timestamp = Time.now + timestamp = Time.now.strftime('%F %T') execute "INSERT INTO notification_settings ( user_id, source_id, source_type, level, created_at, updated_at ) SELECT user_id, source_id, source_type, notification_level, '#{timestamp}', '#{timestamp}' FROM members WHERE user_id IS NOT NULL" end From d2f490c8f3601915cae0e125a0d88871c7c2b7e8 Mon Sep 17 00:00:00 2001 From: lurdan Date: Fri, 15 Apr 2016 12:41:26 +0900 Subject: [PATCH 186/187] fix required gitlab-shell version. Under the procedure, I've warned about gitlab-shell version, which should be 2.6.12 (may be some minor revisions introduces that). --- doc/update/8.5-to-8.6.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md index b9abcbd2c12..6267f14eba4 100644 --- a/doc/update/8.5-to-8.6.md +++ b/doc/update/8.5-to-8.6.md @@ -46,7 +46,7 @@ sudo -u git -H git checkout 8-6-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all -sudo -u git -H git checkout v2.6.11 +sudo -u git -H git checkout v2.6.12 ``` ### 5. Update gitlab-workhorse From a434ffd3b3a895bd75daed76000def23f4002f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 14 Apr 2016 12:20:16 +0200 Subject: [PATCH 187/187] Make /profile/keys/new redirects to /profile/keys for back-compat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Report: https://github.com/gitlabhq/gitlabhq/issues/10138 Signed-off-by: Rémy Coutable --- CHANGELOG | 1 + app/controllers/profiles/keys_controller.rb | 5 +++++ config/routes.rb | 2 +- spec/controllers/profiles/keys_controller_spec.rb | 12 +++++++++++- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e5c8620bebf..da4f9b0fb65 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 8.7.0 (unreleased) - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu) - API: Ability to subscribe and unsubscribe from issues and merge requests (Robert Schilling) - Expose project badges in project settings + - Make /profile/keys/new redirect to /profile/keys for back-compat. !3717 - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) - Expose label description in API (Mariusz Jachimowicz) diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index b88c080352b..a12549d6bcb 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -10,6 +10,11 @@ class Profiles::KeysController < Profiles::ApplicationController @key = current_user.keys.find(params[:id]) end + # Back-compat: We need to support this URL since git-annex webapp points to it + def new + redirect_to profile_keys_path + end + def create @key = current_user.keys.new(key_params) diff --git a/config/routes.rb b/config/routes.rb index 688b83d2c95..f32f7ea8557 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -326,7 +326,7 @@ Rails.application.routes.draw do end end resource :preferences, only: [:show, :update] - resources :keys, except: [:new] + resources :keys resources :emails, only: [:index, :create, :destroy] resource :avatar, only: [:destroy] resource :two_factor_auth, only: [:new, :create, :destroy] do diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb index b6573f105dc..3a82083717f 100644 --- a/spec/controllers/profiles/keys_controller_spec.rb +++ b/spec/controllers/profiles/keys_controller_spec.rb @@ -1,7 +1,17 @@ require 'spec_helper' describe Profiles::KeysController do - let(:user) { create(:user) } + let(:user) { create(:user) } + + describe '#new' do + before { sign_in(user) } + + it 'redirect to #index' do + get :new + + expect(response).to redirect_to(profile_keys_path) + end + end describe "#get_keys" do describe "non existant user" do