Commit 4a1c50a5 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'master' into issue_928_group_boards

parents d0cd4ae9 4dce42cc
...@@ -397,7 +397,6 @@ db:migrate:reset-mysql: ...@@ -397,7 +397,6 @@ db:migrate:reset-mysql:
.migration-paths: &migration-paths .migration-paths: &migration-paths
<<: *dedicated-runner <<: *dedicated-runner
<<: *only-canonical-masters
<<: *pull-cache <<: *pull-cache
stage: test stage: test
variables: variables:
......
...@@ -336,7 +336,7 @@ group :development, :test do ...@@ -336,7 +336,7 @@ group :development, :test do
# Generate Fake data # Generate Fake data
gem 'ffaker', '~> 2.4' gem 'ffaker', '~> 2.4'
gem 'capybara', '~> 2.6.2' gem 'capybara', '~> 2.15.0'
gem 'capybara-screenshot', '~> 1.0.0' gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0' gem 'poltergeist', '~> 1.9.0'
......
...@@ -108,9 +108,9 @@ GEM ...@@ -108,9 +108,9 @@ GEM
bundler (~> 1.2) bundler (~> 1.2)
thor (~> 0.18) thor (~> 0.18)
byebug (9.0.6) byebug (9.0.6)
capybara (2.6.2) capybara (2.15.1)
addressable addressable
mime-types (>= 1.16) mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3) nokogiri (>= 1.3.3)
rack (>= 1.0.0) rack (>= 1.0.0)
rack-test (>= 0.5.4) rack-test (>= 0.5.4)
...@@ -506,6 +506,7 @@ GEM ...@@ -506,6 +506,7 @@ GEM
method_source (0.8.2) method_source (0.8.2)
mime-types (2.99.3) mime-types (2.99.3)
mimemagic (0.3.0) mimemagic (0.3.0)
mini_mime (0.1.4)
mini_portile2 (2.2.0) mini_portile2 (2.2.0)
minitest (5.7.0) minitest (5.7.0)
mmap2 (2.2.7) mmap2 (2.2.7)
...@@ -976,7 +977,7 @@ GEM ...@@ -976,7 +977,7 @@ GEM
expression_parser expression_parser
rinku rinku
xml-simple (1.1.5) xml-simple (1.1.5)
xpath (2.0.0) xpath (2.1.0)
nokogiri (~> 1.3) nokogiri (~> 1.3)
PLATFORMS PLATFORMS
...@@ -1008,7 +1009,7 @@ DEPENDENCIES ...@@ -1008,7 +1009,7 @@ DEPENDENCIES
browser (~> 2.2) browser (~> 2.2)
bullet (~> 5.5.0) bullet (~> 5.5.0)
bundler-audit (~> 0.5.0) bundler-audit (~> 0.5.0)
capybara (~> 2.6.2) capybara (~> 2.15.0)
capybara-screenshot (~> 1.0.0) capybara-screenshot (~> 1.0.0)
carrierwave (~> 1.1) carrierwave (~> 1.1)
charlock_holmes (~> 0.7.5) charlock_holmes (~> 0.7.5)
......
...@@ -122,6 +122,7 @@ $(() => { ...@@ -122,6 +122,7 @@ $(() => {
this.state.lists = _.sortBy(this.state.lists, 'position'); this.state.lists = _.sortBy(this.state.lists, 'position');
Store.addBlankState(); Store.addBlankState();
Store.addPromotionState();
this.loading = false; this.loading = false;
}) })
.catch(() => new Flash('An error occurred. Please try again.')); .catch(() => new Flash('An error occurred. Please try again.'));
......
/* eslint-disable comma-dangle, space-before-function-paren, one-var */ /* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* global Sortable */ /* global Sortable */
import Vue from 'vue'; import Vue from 'vue';
import boardPromotionState from 'ee/boards/components/board_promotion_state';
import AccessorUtilities from '../../lib/utils/accessor'; import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list'; import boardList from './board_list';
import boardBlankState from './board_blank_state'; import boardBlankState from './board_blank_state';
...@@ -17,6 +18,7 @@ gl.issueBoards.Board = Vue.extend({ ...@@ -17,6 +18,7 @@ gl.issueBoards.Board = Vue.extend({
boardList, boardList,
'board-delete': gl.issueBoards.BoardDelete, 'board-delete': gl.issueBoards.BoardDelete,
boardBlankState, boardBlankState,
boardPromotionState,
}, },
props: { props: {
list: Object, list: Object,
......
...@@ -12,7 +12,7 @@ class List { ...@@ -12,7 +12,7 @@ class List {
this.position = obj.position; this.position = obj.position;
this.title = obj.title; this.title = obj.title;
this.type = obj.list_type; this.type = obj.list_type;
this.preset = ['backlog', 'closed', 'blank'].indexOf(this.type) > -1; this.preset = ['backlog', 'closed', 'blank', 'promotion'].indexOf(this.type) > -1;
this.isExpandable = ['backlog', 'closed'].indexOf(this.type) > -1; this.isExpandable = ['backlog', 'closed'].indexOf(this.type) > -1;
this.isExpanded = true; this.isExpanded = true;
this.page = 1; this.page = 1;
...@@ -26,7 +26,7 @@ class List { ...@@ -26,7 +26,7 @@ class List {
this.label = new ListLabel(obj.label); this.label = new ListLabel(obj.label);
} }
if (this.type !== 'blank' && this.id) { if (this.type !== 'blank' && this.type !== 'promotion' && this.id) {
this.getIssues().catch(() => { this.getIssues().catch(() => {
// TODO: handle request error // TODO: handle request error
}); });
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/* global List */ /* global List */
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import boardsStoreEE from 'ee/boards/stores/boards_store_ee';
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {}; window.gl.issueBoards = window.gl.issueBoards || {};
...@@ -140,3 +141,5 @@ gl.issueBoards.BoardsStore = { ...@@ -140,3 +141,5 @@ gl.issueBoards.BoardsStore = {
} }
}, },
}; };
boardsStoreEE.initEESpecific(gl.issueBoards.BoardsStore);
...@@ -237,12 +237,20 @@ ...@@ -237,12 +237,20 @@
} }
} }
.board-blank-state { .board-blank-state,
.board-promotion-state {
height: calc(100% - 49px); height: calc(100% - 49px);
padding: $gl-padding; padding: $gl-padding;
background-color: $white-light; background-color: $white-light;
} }
.board-promotion-state {
.btn.btn-primary {
display: block;
margin-bottom: 15px;
}
}
.board-blank-state-list { .board-blank-state-list {
list-style: none; list-style: none;
......
...@@ -514,7 +514,7 @@ ul.notes { ...@@ -514,7 +514,7 @@ ul.notes {
} }
.note-actions-item { .note-actions-item {
margin-left: 15px; margin-left: 12px;
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -618,15 +618,25 @@ ul.notes { ...@@ -618,15 +618,25 @@ ul.notes {
.note-role { .note-role {
position: relative; position: relative;
padding: 0 7px; display: inline-block;
color: $notes-role-color; color: $notes-role-color;
font-size: 12px; font-size: 12px;
line-height: 20px; line-height: 20px;
border: 1px solid $border-color; margin: 0 3px;
border-radius: $label-border-radius;
&.note-role-access {
padding: 0 7px;
border: 1px solid $border-color;
border-radius: $label-border-radius;
}
&.note-role-special {
text-shadow: 0 0 15px $gl-text-color-inverted;
}
} }
/** /**
* Line note button on the side of diffs * Line note button on the side of diffs
*/ */
......
...@@ -76,3 +76,31 @@ ...@@ -76,3 +76,31 @@
max-width: 700px; max-width: 700px;
} }
} }
.promotion-info-weight-message {
padding: $gl-padding-top;
.dropdown-title {
margin: 0 0 10px;
}
.btn {
padding: $gl-vert-padding $gl-btn-padding;
border-radius: $border-radius-default;
display: block;
line-height: $line-height-base;
}
.btn-link {
display: inline;
color: $gl-link-color;
background-color: transparent;
padding: 0;
&:hover {
background-color: transparent;
color: $gl-link-hover-color;
}
}
}
class Admin::LogsController < Admin::ApplicationController class Admin::LogsController < Admin::ApplicationController
prepend EE::Admin::LogsController
before_action :loggers
def show def show
@loggers = [ end
private
def loggers
@loggers ||= [
Gitlab::AppLogger, Gitlab::AppLogger,
Gitlab::GitLogger, Gitlab::GitLogger,
Gitlab::EnvironmentLogger, Gitlab::EnvironmentLogger,
......
module RendersNotes module RendersNotes
def prepare_notes_for_rendering(notes) def prepare_notes_for_rendering(notes, noteable = nil)
preload_noteable_for_regular_notes(notes) preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project) preload_max_access_for_authors(notes, @project)
preload_first_time_contribution_for_authors(noteable, notes)
Banzai::NoteRenderer.render(notes, @project, current_user) Banzai::NoteRenderer.render(notes, @project, current_user)
notes notes
...@@ -19,4 +20,10 @@ module RendersNotes ...@@ -19,4 +20,10 @@ module RendersNotes
def preload_noteable_for_regular_notes(notes) def preload_noteable_for_regular_notes(notes)
ActiveRecord::Associations::Preloader.new.preload(notes.reject(&:for_commit?), :noteable) ActiveRecord::Associations::Preloader.new.preload(notes.reject(&:for_commit?), :noteable)
end end
def preload_first_time_contribution_for_authors(noteable, notes)
return unless noteable.is_a?(Issuable) && noteable.first_contribution?
notes.each {|n| n.specialize_for_first_contribution!(noteable)}
end
end end
...@@ -127,7 +127,7 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -127,7 +127,7 @@ class Projects::CommitController < Projects::ApplicationController
@discussions = commit.discussions @discussions = commit.discussions
@notes = (@grouped_diff_discussions.values.flatten + @discussions).flat_map(&:notes) @notes = (@grouped_diff_discussions.values.flatten + @discussions).flat_map(&:notes)
@notes = prepare_notes_for_rendering(@notes) @notes = prepare_notes_for_rendering(@notes, @commit)
end end
def assign_change_commit_vars def assign_change_commit_vars
......
...@@ -89,7 +89,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -89,7 +89,7 @@ class Projects::IssuesController < Projects::ApplicationController
@note = @project.notes.new(noteable: @issue) @note = @project.notes.new(noteable: @issue)
@discussions = @issue.discussions @discussions = @issue.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable)
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -61,6 +61,6 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -61,6 +61,6 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@use_legacy_diff_notes = !@merge_request.has_complete_diff_refs? @use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
@grouped_diff_discussions = @merge_request.grouped_diff_discussions(@compare.diff_refs) @grouped_diff_discussions = @merge_request.grouped_diff_discussions(@compare.diff_refs)
@notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes)) @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes), @merge_request)
end end
end end
...@@ -63,12 +63,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -63,12 +63,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
# Build a note object for comment form # Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request) @note = @project.notes.new(noteable: @merge_request)
@discussions = @merge_request.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
@noteable = @merge_request @noteable = @merge_request
@commits_count = @merge_request.commits_count @commits_count = @merge_request.commits_count
@discussions = @merge_request.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable)
labels labels
set_pipeline_variables set_pipeline_variables
......
...@@ -64,7 +64,7 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -64,7 +64,7 @@ class Projects::SnippetsController < Projects::ApplicationController
@noteable = @snippet @noteable = @snippet
@discussions = @snippet.discussions @discussions = @snippet.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable)
render 'show' render 'show'
end end
......
...@@ -66,7 +66,7 @@ class SnippetsController < ApplicationController ...@@ -66,7 +66,7 @@ class SnippetsController < ApplicationController
@noteable = @snippet @noteable = @snippet
@discussions = @snippet.discussions @discussions = @snippet.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable)
respond_to do |format| respond_to do |format|
format.html do format.html do
......
...@@ -134,6 +134,8 @@ module IssuablesHelper ...@@ -134,6 +134,8 @@ module IssuablesHelper
end end
output << "&ensp;".html_safe output << "&ensp;".html_safe
output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!'))
output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm") output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm")
output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg") output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg")
...@@ -176,6 +178,13 @@ module IssuablesHelper ...@@ -176,6 +178,13 @@ module IssuablesHelper
end end
end end
def issuable_first_contribution_icon
content_tag(:span, class: 'fa-stack') do
concat(icon('certificate', class: "fa-stack-2x"))
concat(content_tag(:strong, '1', class: 'fa-inverse fa-stack-1x'))
end
end
def assigned_issuables_count(issuable_type) def assigned_issuables_count(issuable_type)
case issuable_type case issuable_type
when :issues when :issues
......
...@@ -73,7 +73,7 @@ module NotesHelper ...@@ -73,7 +73,7 @@ module NotesHelper
end end
def note_max_access_for_user(note) def note_max_access_for_user(note)
note.project.team.human_max_access(note.author_id) note.project.team.max_member_access(note.author_id)
end end
def discussion_path(discussion) def discussion_path(discussion)
......
...@@ -338,4 +338,11 @@ module Issuable ...@@ -338,4 +338,11 @@ module Issuable
metrics = self.metrics || create_metrics metrics = self.metrics || create_metrics
metrics.record! metrics.record!
end end
##
# Override in issuable specialization
#
def first_contribution?
false
end
end end
...@@ -2,6 +2,10 @@ module Geo ...@@ -2,6 +2,10 @@ module Geo
class EventLog < ActiveRecord::Base class EventLog < ActiveRecord::Base
include Geo::Model include Geo::Model
belongs_to :repository_created_event,
class_name: 'Geo::RepositoryCreatedEvent',
foreign_key: :repository_created_event_id
belongs_to :repository_updated_event, belongs_to :repository_updated_event,
class_name: 'Geo::RepositoryUpdatedEvent', class_name: 'Geo::RepositoryUpdatedEvent',
foreign_key: :repository_updated_event_id foreign_key: :repository_updated_event_id
...@@ -19,7 +23,8 @@ module Geo ...@@ -19,7 +23,8 @@ module Geo
foreign_key: :repositories_changed_event_id foreign_key: :repositories_changed_event_id
def event def event
repository_updated_event || repository_created_event ||
repository_updated_event ||
repository_deleted_event || repository_deleted_event ||
repository_renamed_event || repository_renamed_event ||
repositories_changed_event repositories_changed_event
......
module Geo
class RepositoryCreatedEvent < ActiveRecord::Base
include Geo::Model
belongs_to :project
validates :project, :project_name, :repo_path, :repository_storage_name,
:repository_storage_path, presence: true
end
end
...@@ -983,6 +983,12 @@ class MergeRequest < ActiveRecord::Base ...@@ -983,6 +983,12 @@ class MergeRequest < ActiveRecord::Base
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end end
def first_contribution?
return false if project.team.max_member_access(author_id) > Gitlab::Access::GUEST
project.merge_requests.merged.where(author_id: author_id).empty?
end
private private
def write_ref def write_ref
......
...@@ -16,6 +16,16 @@ class Note < ActiveRecord::Base ...@@ -16,6 +16,16 @@ class Note < ActiveRecord::Base
include IgnorableColumn include IgnorableColumn
include Editable include Editable
module SpecialRole
FIRST_TIME_CONTRIBUTOR = :first_time_contributor
class << self
def values
constants.map {|const| self.const_get(const)}
end
end
end
ignore_column :original_discussion_id ignore_column :original_discussion_id
cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
...@@ -33,9 +43,12 @@ class Note < ActiveRecord::Base ...@@ -33,9 +43,12 @@ class Note < ActiveRecord::Base
# Banzai::ObjectRenderer # Banzai::ObjectRenderer
attr_accessor :user_visible_reference_count attr_accessor :user_visible_reference_count
# Attribute used to store the attributes that have ben changed by quick actions. # Attribute used to store the attributes that have been changed by quick actions.
attr_accessor :commands_changes attr_accessor :commands_changes
# A special role that may be displayed on issuable's discussions
attr_accessor :special_role
default_value_for :system, false default_value_for :system, false
attr_mentionable :note, pipeline: :note attr_mentionable :note, pipeline: :note
...@@ -143,6 +156,10 @@ class Note < ActiveRecord::Base ...@@ -143,6 +156,10 @@ class Note < ActiveRecord::Base
.group(:noteable_id) .group(:noteable_id)
.where(noteable_type: type, noteable_id: ids) .where(noteable_type: type, noteable_id: ids)
end end
def has_special_role?(role, note)
note.special_role == role
end
end end
def searchable? def searchable?
...@@ -212,6 +229,22 @@ class Note < ActiveRecord::Base ...@@ -212,6 +229,22 @@ class Note < ActiveRecord::Base
super(noteable_type.to_s.classify.constantize.base_class.to_s) super(noteable_type.to_s.classify.constantize.base_class.to_s)
end end
def special_role=(role)
raise "Role is undefined, #{role} not found in #{SpecialRole.values}" unless SpecialRole.values.include?(role)
@special_role = role
end
def has_special_role?(role)
self.class.has_special_role?(role, self)
end
def specialize_for_first_contribution!(noteable)
return unless noteable.author_id == self.author_id
self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR
end
def editable? def editable?
!system? !system?
end end
......
...@@ -152,7 +152,7 @@ class ProjectTeam ...@@ -152,7 +152,7 @@ class ProjectTeam
end end
def human_max_access(user_id) def human_max_access(user_id)
Gitlab::Access.options_with_owner.key(max_member_access(user_id)) Gitlab::Access.human_access(max_member_access(user_id))
end end
# Determine the maximum access level for a group of users in bulk. # Determine the maximum access level for a group of users in bulk.
......
module Geo
class RepositoryCreatedEventStore < EventStore
self.event_type = :repository_created_event
private
def build_event
Geo::RepositoryCreatedEvent.new(
project: project,
repository_storage_name: project.repository.storage,
repository_storage_path: project.repository_storage_path,
repo_path: project.disk_path,
wiki_path: "#{project.disk_path}.wiki",
project_name: project.name
)
end
end
end
- access = note_max_access_for_user(note) - if note.has_special_role?(Note::SpecialRole::FIRST_TIME_CONTRIBUTOR)
- if access %span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project. Handle with care.") }
%span.note-role= access = issuable_first_contribution_icon
- if access = note_max_access_for_user(note)
%span.note-role.note-role-access= Gitlab::Access.human_access(access)
- if note.resolvable? - if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note) - can_resolve = can?(current_user, :resolve_note, note)
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
%script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board" %script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
%script#js-board-promotion{ type: "text/x-template" }= render "shared/promotions/promote_issue_board"
- if @group - if @group
= render "groups/head_issues" = render "groups/head_issues"
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
"v-if" => "!list.preset && list.id" } "v-if" => "!list.preset && list.id" }
%button.board-delete.has-tooltip.append-right-10{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" } %button.board-delete.has-tooltip.append-right-10{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash") = icon("trash")
.issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank"' } .issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"' }
%span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' } %span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }} {{ list.issuesSize }}
- if can?(current_user, :admin_list, current_board_parent) - if can?(current_user, :admin_list, current_board_parent)
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
"title" => "New issue", "title" => "New issue",
data: { placement: "top", container: "body" } } data: { placement: "top", container: "body" } }
= icon("plus", class: "js-no-trigger-collapse") = icon("plus", class: "js-no-trigger-collapse")
%board-list{ "v-if" => 'list.type !== "blank"', %board-list{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"',
":list" => "list", ":list" => "list",
":issues" => "list.issues", ":issues" => "list.issues",
":loading" => "list.loading", ":loading" => "list.loading",
...@@ -45,3 +45,4 @@ ...@@ -45,3 +45,4 @@
"ref" => "board-list" } "ref" => "board-list" }
- if can?(current_user, :admin_list, current_board_parent) - if can?(current_user, :admin_list, current_board_parent)
%board-blank-state{ "v-if" => 'list.id == "blank"' } %board-blank-state{ "v-if" => 'list.id == "blank"' }
%board-promotion-state{ "v-if" => 'list.id == "promotion"' }
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><g transform="translate(12 25)"><path fill="#E1DBF2" d="M3 0h10a3 3 0 0 1 3 3v22a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm1 4v20h8V4H4zm2 2h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/><rect width="6" height="4" x="5" y="12" fill="#6B4FBB" rx="1"/></g><g transform="translate(50 25)"><rect width="6" height="4" x="5" y="6" fill="#6B4FBB" rx="1"/><path fill="#E1DBF2" fill-rule="nonzero" d="M3 0h10a3 3 0 0 1 3 3v22a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm1 4v20h8V4H4zm2 8h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></g><path fill="#E1DBF2" d="M34 25h10a3 3 0 0 1 3 3v28a3 3 0 0 1-3 3H34a3 3 0 0 1-3-3V28a3 3 0 0 1 3-3zm1 4v26h8V29h-8zm2 8h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/><path fill="#6B4FBB" d="M37 43h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0-12h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></g></svg>
\ No newline at end of file
...@@ -141,6 +141,7 @@ ...@@ -141,6 +141,7 @@
%li %li
%a{ href: "#", data: { id: weight, none: weight == Issue::WEIGHT_NONE }, class: ("is-active" if params[:weight] == weight.to_s) } %a{ href: "#", data: { id: weight, none: weight == Issue::WEIGHT_NONE }, class: ("is-active" if params[:weight] == weight.to_s) }
= weight = weight
= render 'shared/promotions/promote_issue_weights'
- if issuable.has_attribute?(:confidential) - if issuable.has_attribute?(:confidential)
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe
......
---
title: Geo - Log a repository created event when a project is created
merge_request: 2807
author:
type: added
---
title: Don't put the password in the SSH remote if using public-key authentication
merge_request: 2837
author:
---
title: Show geo.log in the Admin area
merge_request: 2845
author:
type: added
---
title: "First-time contributor badge"
merge_request: 13143
author: Micaël Bergeron <micaelbergeron@gmail.com>
...@@ -18,6 +18,7 @@ class MigrateIssuesToGhostUser < ActiveRecord::Migration ...@@ -18,6 +18,7 @@ class MigrateIssuesToGhostUser < ActiveRecord::Migration
ActiveRecord::Base.clear_cache! ActiveRecord::Base.clear_cache!
::User.reset_column_information ::User.reset_column_information
::Namespace.reset_column_information
end end
def up def up
......
class CreateGeoRepositoryCreatedEvents < ActiveRecord::Migration
DOWNTIME = false
def change
create_table :geo_repository_created_events, id: :bigserial do |t|
t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: false
t.text :repository_storage_name, null: false
t.text :repository_storage_path, null: false
t.text :repo_path, null: false
t.text :wiki_path
t.text :project_name, null: false
end
add_column :geo_event_log, :repository_created_event_id, :integer, limit: 8
end
end
class AddIndexToGeoEventLogRepositoryCreatedEventId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :geo_event_log, :repository_created_event_id
end
def down
if index_exists? :geo_event_log, :repository_created_event_id
remove_concurrent_index :geo_event_log, :repository_created_event_id
end
end
end
class AddGeoRepositoryCreatedEventsFkOnGeoEventLog < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_foreign_key :geo_event_log, :geo_repository_created_events,
column: :repository_created_event_id, on_delete: :cascade
end
def down
remove_foreign_key :geo_event_log, column: :repository_created_event_id
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170905202320) do ActiveRecord::Schema.define(version: 20170906160132) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -671,9 +671,11 @@ ActiveRecord::Schema.define(version: 20170905202320) do ...@@ -671,9 +671,11 @@ ActiveRecord::Schema.define(version: 20170905202320) do
t.integer "repository_deleted_event_id", limit: 8 t.integer "repository_deleted_event_id", limit: 8
t.integer "repository_renamed_event_id", limit: 8 t.integer "repository_renamed_event_id", limit: 8
t.integer "repositories_changed_event_id", limit: 8 t.integer "repositories_changed_event_id", limit: 8
t.integer "repository_created_event_id", limit: 8
end end
add_index "geo_event_log", ["repositories_changed_event_id"], name: "index_geo_event_log_on_repositories_changed_event_id", using: :btree add_index "geo_event_log", ["repositories_changed_event_id"], name: "index_geo_event_log_on_repositories_changed_event_id", using: :btree
add_index "geo_event_log", ["repository_created_event_id"], name: "index_geo_event_log_on_repository_created_event_id", using: :btree
add_index "geo_event_log", ["repository_deleted_event_id"], name: "index_geo_event_log_on_repository_deleted_event_id", using: :btree add_index "geo_event_log", ["repository_deleted_event_id"], name: "index_geo_event_log_on_repository_deleted_event_id", using: :btree
add_index "geo_event_log", ["repository_renamed_event_id"], name: "index_geo_event_log_on_repository_renamed_event_id", using: :btree add_index "geo_event_log", ["repository_renamed_event_id"], name: "index_geo_event_log_on_repository_renamed_event_id", using: :btree
add_index "geo_event_log", ["repository_updated_event_id"], name: "index_geo_event_log_on_repository_updated_event_id", using: :btree add_index "geo_event_log", ["repository_updated_event_id"], name: "index_geo_event_log_on_repository_updated_event_id", using: :btree
...@@ -713,6 +715,17 @@ ActiveRecord::Schema.define(version: 20170905202320) do ...@@ -713,6 +715,17 @@ ActiveRecord::Schema.define(version: 20170905202320) do
add_index "geo_repositories_changed_events", ["geo_node_id"], name: "index_geo_repositories_changed_events_on_geo_node_id", using: :btree add_index "geo_repositories_changed_events", ["geo_node_id"], name: "index_geo_repositories_changed_events_on_geo_node_id", using: :btree
create_table "geo_repository_created_events", id: :bigserial, force: :cascade do |t|
t.integer "project_id", null: false
t.text "repository_storage_name", null: false
t.text "repository_storage_path", null: false
t.text "repo_path", null: false
t.text "wiki_path"
t.text "project_name", null: false
end
add_index "geo_repository_created_events", ["project_id"], name: "index_geo_repository_created_events_on_project_id", using: :btree
create_table "geo_repository_deleted_events", id: :bigserial, force: :cascade do |t| create_table "geo_repository_deleted_events", id: :bigserial, force: :cascade do |t|
t.integer "project_id", null: false t.integer "project_id", null: false
t.text "repository_storage_name", null: false t.text "repository_storage_name", null: false
...@@ -2060,12 +2073,14 @@ ActiveRecord::Schema.define(version: 20170905202320) do ...@@ -2060,12 +2073,14 @@ ActiveRecord::Schema.define(version: 20170905202320) do
add_foreign_key "events_for_migration", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade add_foreign_key "events_for_migration", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade
add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_repositories_changed_events", column: "repositories_changed_event_id", name: "fk_4a99ebfd60", on_delete: :cascade add_foreign_key "geo_event_log", "geo_repositories_changed_events", column: "repositories_changed_event_id", name: "fk_4a99ebfd60", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_repository_created_events", column: "repository_created_event_id", name: "fk_9b9afb1916", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_repository_deleted_events", column: "repository_deleted_event_id", name: "fk_c4b1c1f66e", on_delete: :cascade add_foreign_key "geo_event_log", "geo_repository_deleted_events", column: "repository_deleted_event_id", name: "fk_c4b1c1f66e", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_repository_renamed_events", column: "repository_renamed_event_id", name: "fk_86c84214ec", on_delete: :cascade add_foreign_key "geo_event_log", "geo_repository_renamed_events", column: "repository_renamed_event_id", name: "fk_86c84214ec", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_repository_updated_events", column: "repository_updated_event_id", on_delete: :cascade add_foreign_key "geo_event_log", "geo_repository_updated_events", column: "repository_updated_event_id", on_delete: :cascade
add_foreign_key "geo_node_namespace_links", "geo_nodes", on_delete: :cascade add_foreign_key "geo_node_namespace_links", "geo_nodes", on_delete: :cascade
add_foreign_key "geo_node_namespace_links", "namespaces", on_delete: :cascade add_foreign_key "geo_node_namespace_links", "namespaces", on_delete: :cascade
add_foreign_key "geo_repositories_changed_events", "geo_nodes", on_delete: :cascade add_foreign_key "geo_repositories_changed_events", "geo_nodes", on_delete: :cascade
add_foreign_key "geo_repository_created_events", "projects", on_delete: :cascade
add_foreign_key "geo_repository_renamed_events", "projects", on_delete: :cascade add_foreign_key "geo_repository_renamed_events", "projects", on_delete: :cascade
add_foreign_key "geo_repository_updated_events", "projects", on_delete: :cascade add_foreign_key "geo_repository_updated_events", "projects", on_delete: :cascade
add_foreign_key "index_statuses", "projects", name: "fk_74b2492545", on_delete: :cascade add_foreign_key "index_statuses", "projects", name: "fk_74b2492545", on_delete: :cascade
......
const Store = gl.issueBoards.BoardsStore;
export default {
template: '#js-board-promotion',
methods: {
clearPromotionState: Store.removePromotionState.bind(Store),
},
};
/* eslint-disable class-methods-use-this */
import Cookies from 'js-cookie';
class BoardsStoreEE {
initEESpecific(boardsStore) {
this.$boardApp = document.getElementById('board-app');
this.store = boardsStore;
this.store.addPromotionState = () => {
this.addPromotion();
};
this.store.removePromotionState = () => {
this.removePromotion();
};
}
shouldAddPromotionState() {
// Decide whether to add the promotion state
return this.$boardApp.dataset.showPromotion === 'true';
}
addPromotion() {
if (!this.shouldAddPromotionState() || this.promotionIsHidden() || this.store.disabled) return;
this.store.addList({
id: 'promotion',
list_type: 'promotion',
title: 'Improve Issue boards',
position: 0,
});
this.store.state.lists = _.sortBy(this.store.state.lists, 'position');
}
removePromotion() {
this.store.removeList('promotion', 'promotion');
Cookies.set('promotion_issue_board_hidden', 'true', {
expires: 365 * 10,
path: '',
});
}
promotionIsHidden() {
return Cookies.get('promotion_issue_board_hidden') === 'true';
}
}
export default new BoardsStoreEE();
module EE::Admin::LogsController
def loggers
raise NotImplementedError unless defined?(super)
@loggers ||= super + [
Gitlab::GeoLogger
]
end
end
...@@ -48,7 +48,9 @@ module EE ...@@ -48,7 +48,9 @@ module EE
end end
def board_sidebar_user_data def board_sidebar_user_data
super.merge(group_id: @group&.id) super.merge(group_id: @group&.id,
focus_mode_available: @project.feature_available?(:issue_board_focus_mode).to_s,
show_promotion: (show_promotions? && (!@project.feature_available?(:multiple_issue_boards) || !@project.feature_available?(:issue_board_milestone) || !@project.feature_available?(:issue_board_focus_mode))).to_s)
end end
end end
end end
...@@ -156,7 +156,15 @@ module EE ...@@ -156,7 +156,15 @@ module EE
def fetch_mirror def fetch_mirror
return unless mirror? return unless mirror?
repository.fetch_upstream(self.import_url) # Only send the password if it's needed
url =
if import_data&.password_auth?
import_url
else
username_only_import_url
end
repository.fetch_upstream(url)
end end
def can_override_approvers? def can_override_approvers?
......
...@@ -32,6 +32,10 @@ module EE ...@@ -32,6 +32,10 @@ module EE
ssh_import? && auth_method == 'ssh_public_key' ssh_import? && auth_method == 'ssh_public_key'
end end
def password_auth?
auth_method == 'password'
end
def ssh_import? def ssh_import?
project&.import_url&.start_with?('ssh://') project&.import_url&.start_with?('ssh://')
end end
......
...@@ -9,7 +9,7 @@ module EE ...@@ -9,7 +9,7 @@ module EE
mirror_user_id = params.delete(:mirror_user_id) mirror_user_id = params.delete(:mirror_user_id)
mirror_trigger_builds = params.delete(:mirror_trigger_builds) mirror_trigger_builds = params.delete(:mirror_trigger_builds)
super do |project| project = super do |project|
# Repository size limit comes as MB from the view # Repository size limit comes as MB from the view
project.repository_size_limit = ::Gitlab::Utils.try_megabytes_to_bytes(limit) if limit project.repository_size_limit = ::Gitlab::Utils.try_megabytes_to_bytes(limit) if limit
...@@ -19,10 +19,18 @@ module EE ...@@ -19,10 +19,18 @@ module EE
project.mirror_user_id = mirror_user_id project.mirror_user_id = mirror_user_id
end end
end end
log_geo_event(project) if project&.persisted?
project
end end
private private
def log_geo_event(project)
::Geo::RepositoryCreatedEventStore.new(project).create
end
def after_create_actions def after_create_actions
raise NotImplementedError unless defined?(super) raise NotImplementedError unless defined?(super)
......
.board-promotion-state
.svg-container.center
= custom_icon('icon_issue_board')
%p
- if current_application_settings.should_check_namespace_plan?
= _('Upgrade your plan to improve Issue boards.')
- else
= _('Improve Issue boards with GitLab Enterprise Edition.')
%ul
- unless @project.feature_available?(:multiple_issue_boards)
%li
= link_to _('Multiple issue boards'), help_page_path('user/project/issue_board.html', anchor:'use-cases-for-multiple-issue-boards'), target: '_blank'
- unless @project.feature_available?(:issue_board_milestone)
%li
= link_to _('Issue boards with milestones'), help_page_path('user/project/issue_board.html', anchor:'board-with-a-milestone'), target: '_blank'
- unless @project.feature_available?(:issue_board_focus_mode)
%li
= link_to _('Issue board focus mode'), help_page_path('user/project/issue_board.html', anchor:'focus-mode'), target: '_blank'
= render 'shared/promotions/promotion_link_project'
.top-space
%button.btn.btn-default.btn-block#hide-btn{ :href => "#", "@click.stop" => "clearPromotionState" }
= _("Thanks! Don't show me this again")
- if show_promotions? && !@project.feature_available?(:issue_weights)
.block.weight
.sidebar-collapsed-icon{ data: { toggle: "dropdown", target: ".weight" } }
= icon('balance-scale')
%span
No
.title.hide-collapsed
= _('Weight')
= link_to _('Edit'), '#', class: 'edit-link promote-weight-link pull-right', data: { toggle: "dropdown", target: ".weight" }
.promotion-info-weight.dropdown
.dropdown-menu.promotion-info-weight-message
.dropdown-title
%span
= _('Change Weight')
%button.dropdown-title-button.dropdown-menu-close{ "aria-label" => _('Close'), :type => "button" }
%i.fa.fa-times.dropdown-menu-close-icon{ "aria-hidden" => "true", "data-hidden" => "true" }
%div
%p
- if current_application_settings.should_check_namespace_plan?
= _('Upgrade your plan to activate Issue weight.')
- else
= _('Improve issues management with Issue weight and GitLab Enterprise Edition.')
= link_to _('Read more'), help_page_path('workflow/issue_weight.html'), class: 'btn-link', target: '_blank'
%div
= render 'shared/promotions/promotion_link_project', short_form: true
.hide-collapsed
%span.no-value
= _('None')
- short_form = local_assigns.fetch :short_form, false
- if current_application_settings.should_check_namespace_plan? - if current_application_settings.should_check_namespace_plan?
- if can?(current_user, :admin_namespace, @project.namespace) - if can?(current_user, :admin_namespace, @project.namespace)
= link_to 'Upgrade your plan', upgrade_plan_url, class: 'btn btn-primary' = link_to 'Upgrade your plan', upgrade_plan_url, class: 'btn btn-primary'
...@@ -8,9 +9,9 @@ ...@@ -8,9 +9,9 @@
%p Contact owner #{ link_to(owner.name, user_path(owner)) } to upgrade the plan. %p Contact owner #{ link_to(owner.name, user_path(owner)) } to upgrade the plan.
- elsif current_user&.admin? - elsif current_user&.admin?
- if License.current&.expired? - if License.current&.expired?
= link_to 'Buy GitLab Enterprise Edition', Gitlab::SUBSCRIPTIONS_PLANS_URL, class: 'btn btn-primary' = link_to (!short_form ? 'Buy GitLab Enterprise Edition' : 'Buy EE'), Gitlab::SUBSCRIPTIONS_PLANS_URL, class: 'btn btn-primary'
- else - else
= link_to 'Start GitLab Enterprise Edition trial', new_trial_url, class: 'btn btn-primary' = link_to (!short_form ? 'Start GitLab Enterprise Edition trial' : 'Start GitLab EE trial'), new_trial_url, class: 'btn btn-primary'
- else - else
%p %p
Contact your Administrator to upgrade your license. Contact your Administrator to upgrade your license.
module Gitlab
class GeoLogger < Gitlab::Logger
def self.file_name_noext
'geo'
end
end
end
...@@ -67,10 +67,14 @@ module Gitlab ...@@ -67,10 +67,14 @@ module Gitlab
def protection_values def protection_values
protection_options.values protection_options.values
end end
def human_access(access)
options_with_owner.key(access)
end
end end
def human_access def human_access
Gitlab::Access.options_with_owner.key(access_field) Gitlab::Access.human_access(access_field)
end end
def owner? def owner?
......
...@@ -66,6 +66,8 @@ module Gitlab ...@@ -66,6 +66,8 @@ module Gitlab
if event_log.repository_updated_event if event_log.repository_updated_event
handle_repository_update(event_log) handle_repository_update(event_log)
elsif event_log.repository_created_event
handle_repository_created(event_log)
elsif event_log.repository_deleted_event elsif event_log.repository_deleted_event
handle_repository_delete(event_log) handle_repository_delete(event_log)
elsif event_log.repositories_changed_event elsif event_log.repositories_changed_event
...@@ -104,6 +106,24 @@ module Gitlab ...@@ -104,6 +106,24 @@ module Gitlab
Gitlab::Geo.current_node&.projects_include?(event_log.project_id) Gitlab::Geo.current_node&.projects_include?(event_log.project_id)
end end
def handle_repository_created(event_log)
created_event = event_log.repository_created_event
registry = ::Geo::ProjectRegistry.find_or_initialize_by(project_id: created_event.project_id)
registry.resync_repository = true
registry.resync_wiki = created_event.wiki_path.present?
log_event_info(
event_log.created_at,
message: 'Repository created',
project_id: created_event.project_id,
repo_path: created_event.repo_path,
wiki_path: created_event.wiki_path,
resync_repository: registry.resync_repository,
resync_wiki: registry.resync_wiki)
registry.save!
end
def handle_repository_update(event) def handle_repository_update(event)
updated_event = event.repository_updated_event updated_event = event.repository_updated_event
registry = ::Geo::ProjectRegistry.find_or_initialize_by(project_id: updated_event.project_id) registry = ::Geo::ProjectRegistry.find_or_initialize_by(project_id: updated_event.project_id)
......
...@@ -56,6 +56,28 @@ describe Projects::MergeRequestsController do ...@@ -56,6 +56,28 @@ describe Projects::MergeRequestsController do
expect(response).to be_success expect(response).to be_success
end end
context "loads notes" do
let(:first_contributor) { create(:user) }
let(:contributor) { create(:user) }
let(:merge_request) { create(:merge_request, author: first_contributor, target_project: project, source_project: project) }
let(:contributor_merge_request) { create(:merge_request, :merged, author: contributor, target_project: project, source_project: project) }
# the order here is important
# as the controller reloads these from DB, references doesn't correspond after
let!(:first_contributor_note) { create(:note, author: first_contributor, noteable: merge_request, project: project) }
let!(:contributor_note) { create(:note, author: contributor, noteable: merge_request, project: project) }
let!(:owner_note) { create(:note, author: user, noteable: merge_request, project: project) }
it "with special_role FIRST_TIME_CONTRIBUTOR" do
go(format: :html)
notes = assigns(:notes)
expect(notes).to match(a_collection_containing_exactly(an_object_having_attributes(special_role: Note::SpecialRole::FIRST_TIME_CONTRIBUTOR),
an_object_having_attributes(special_role: nil),
an_object_having_attributes(special_role: nil)
))
end
end
end end
describe 'as json' do describe 'as json' do
......
require 'spec_helper'
describe 'Admin browses logs' do
before do
sign_in(create(:admin))
end
it 'shows available log files' do
visit admin_logs_path
expect(page).to have_link 'geo.log'
end
end
...@@ -198,6 +198,24 @@ describe Project do ...@@ -198,6 +198,24 @@ describe Project do
end end
end end
describe '#fetch_mirror' do
where(:import_url, :auth_method, :expected) do
'http://foo:bar@example.com' | 'password' | 'http://foo:bar@example.com'
'ssh://foo:bar@example.com' | 'password' | 'ssh://foo:bar@example.com'
'ssh://foo:bar@example.com' | 'ssh_public_key' | 'ssh://foo@example.com'
end
with_them do
let(:project) { build(:project, :mirror, import_url: import_url, import_data_attributes: { auth_method: auth_method } ) }
it do
expect(project.repository).to receive(:fetch_upstream).with(expected)
project.fetch_mirror
end
end
end
describe '#mirror_waiting_duration' do describe '#mirror_waiting_duration' do
it 'returns in seconds the time spent in the queue' do it 'returns in seconds the time spent in the queue' do
project = create(:project, :mirror, :import_scheduled) project = create(:project, :mirror, :import_scheduled)
......
...@@ -134,7 +134,24 @@ describe Projects::CreateService, '#execute' do ...@@ -134,7 +134,24 @@ describe Projects::CreateService, '#execute' do
end end
end end
context 'when running on a primary node' do
let!(:geo_node) { create(:geo_node, :primary, :current) }
it 'logs an event to the Geo event log' do
expect { create_project(user, opts) }.to change(Geo::RepositoryCreatedEvent, :count).by(1)
end
it 'does not log event to the Geo log if project creation fails' do
failing_opts = {
name: nil,
namespace: user.namespace
}
expect { create_project(user, failing_opts) }.not_to change(Geo::RepositoryCreatedEvent, :count)
end
end
def create_project(user, opts) def create_project(user, opts)
Projects::CreateService.new(user, opts).execute described_class.new(user, opts).execute
end end
end end
FactoryGirl.define do FactoryGirl.define do
factory :geo_event_log, class: Geo::EventLog do factory :geo_event_log, class: Geo::EventLog do
trait :created_event do
repository_created_event factory: :geo_repository_created_event
end
trait :updated_event do trait :updated_event do
repository_updated_event factory: :geo_repository_updated_event repository_updated_event factory: :geo_repository_updated_event
end end
...@@ -13,11 +17,22 @@ FactoryGirl.define do ...@@ -13,11 +17,22 @@ FactoryGirl.define do
end end
end end
factory :geo_repository_created_event, class: Geo::RepositoryCreatedEvent do
project
repository_storage_name { project.repository_storage }
repository_storage_path { project.repository_storage_path }
add_attribute(:repo_path) { project.disk_path }
project_name { project.name }
wiki_path { "{project.disk_path}.wiki" }
end
factory :geo_repository_updated_event, class: Geo::RepositoryUpdatedEvent do factory :geo_repository_updated_event, class: Geo::RepositoryUpdatedEvent do
project
source 0 source 0
branches_affected 0 branches_affected 0
tags_affected 0 tags_affected 0
project
end end
factory :geo_repository_deleted_event, class: Geo::RepositoryDeletedEvent do factory :geo_repository_deleted_event, class: Geo::RepositoryDeletedEvent do
......
...@@ -8,8 +8,10 @@ describe 'Admin browses logs' do ...@@ -8,8 +8,10 @@ describe 'Admin browses logs' do
it 'shows available log files' do it 'shows available log files' do
visit admin_logs_path visit admin_logs_path
expect(page).to have_content 'test.log' expect(page).to have_link 'application.log'
expect(page).to have_content 'githost.log' expect(page).to have_link 'githost.log'
expect(page).to have_content 'application.log' expect(page).to have_link 'test.log'
expect(page).to have_link 'sidekiq.log'
expect(page).to have_link 'repocheck.log'
end end
end end
...@@ -115,9 +115,9 @@ feature 'Dashboard Projects' do ...@@ -115,9 +115,9 @@ feature 'Dashboard Projects' do
expect(page).to have_selector('.merge-request-form') expect(page).to have_selector('.merge-request-form')
expect(current_path).to eq project_new_merge_request_path(project) expect(current_path).to eq project_new_merge_request_path(project)
expect(find('#merge_request_target_project_id').value).to eq project.id.to_s expect(find('#merge_request_target_project_id', visible: false).value).to eq project.id.to_s
expect(find('input#merge_request_source_branch').value).to eq 'feature' expect(find('input#merge_request_source_branch', visible: false).value).to eq 'feature'
expect(find('input#merge_request_target_branch').value).to eq 'master' expect(find('input#merge_request_target_branch', visible: false).value).to eq 'master'
end end
end end
end end
...@@ -139,7 +139,7 @@ feature 'Diff note avatars', js: true do ...@@ -139,7 +139,7 @@ feature 'Diff note avatars', js: true do
end end
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find("[id='#{position.line_code(project.repository)}']") do
find('.diff-notes-collapse').click find('.diff-notes-collapse').trigger('click')
expect(page).to have_selector('img.js-diff-comment-avatar', count: 2) expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
end end
...@@ -152,7 +152,7 @@ feature 'Diff note avatars', js: true do ...@@ -152,7 +152,7 @@ feature 'Diff note avatars', js: true do
page.within '.js-discussion-note-form' do page.within '.js-discussion-note-form' do
find('.js-note-text').native.send_keys('Test') find('.js-note-text').native.send_keys('Test')
find('.js-comment-button').trigger 'click' find('.js-comment-button').trigger('click')
wait_for_requests wait_for_requests
end end
......
...@@ -196,10 +196,11 @@ feature 'Diff notes resolve', js: true do ...@@ -196,10 +196,11 @@ feature 'Diff notes resolve', js: true do
end end
it 'does not mark discussion as resolved when resolving single note' do it 'does not mark discussion as resolved when resolving single note' do
page.first '.diff-content .note' do page.within("#note_#{note.id}") do
first('.line-resolve-btn').click first('.line-resolve-btn').click
expect(page).to have_selector('.note-action-button .loading') wait_for_requests
expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}") expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
end end
......
...@@ -14,7 +14,7 @@ feature 'User wants to create a file' do ...@@ -14,7 +14,7 @@ feature 'User wants to create a file' do
file_name = find('#file_name') file_name = find('#file_name')
file_name.set options[:file_name] || 'README.md' file_name.set options[:file_name] || 'README.md'
file_content = find('#file-content') file_content = find('#file-content', visible: false)
file_content.set options[:file_content] || 'Some content' file_content.set options[:file_content] || 'Some content'
click_button 'Commit changes' click_button 'Commit changes'
......
...@@ -204,6 +204,33 @@ describe 'Promotions', js: true do ...@@ -204,6 +204,33 @@ describe 'Promotions', js: true do
end end
end end
describe 'for issue boards ', js: true do
before do
stub_application_setting(check_namespace_plan: true)
allow(Gitlab).to receive(:com?) { true }
project.team << [user, :master]
sign_in(user)
end
it 'should appear in milestone page' do
visit project_boards_path(project)
expect(find('.board-promotion-state')).to have_content "Upgrade your plan to improve Issue boards"
end
it 'does not show when cookie is set' do
visit project_boards_path(project)
within('.board-promotion-state') do
find('#hide-btn').trigger('click')
end
visit project_boards_path(project, milestone)
expect(page).not_to have_selector('.board-promotion-state')
end
end
describe 'for issue export', js: true do describe 'for issue export', js: true do
before do before do
allow(License).to receive(:current).and_return(nil) allow(License).to receive(:current).and_return(nil)
...@@ -220,6 +247,23 @@ describe 'Promotions', js: true do ...@@ -220,6 +247,23 @@ describe 'Promotions', js: true do
end end
end end
describe 'for issue weight', js: true do
before do
allow(License).to receive(:current).and_return(nil)
stub_application_setting(check_namespace_plan: false)
project.team << [user, :master]
sign_in(user)
end
it 'should appear on the page', js: true do
visit project_issue_path(project, issue)
wait_for_requests
find('.promote-weight-link').click
expect(find('.promotion-info-weight-message')).to have_content 'Improve issues management with Issue weight and GitLab Enterprise Edition'
end
end
describe 'for project audit events', js: true do describe 'for project audit events', js: true do
before do before do
allow(License).to receive(:current).and_return(nil) allow(License).to receive(:current).and_return(nil)
......
...@@ -295,7 +295,7 @@ describe "Search" do ...@@ -295,7 +295,7 @@ describe "Search" do
fill_in 'search', with: 'foo' fill_in 'search', with: 'foo'
click_button 'Search' click_button 'Search'
expect(find('#group_id').value).to eq(project.namespace.id.to_s) expect(find('#group_id', visible: false).value).to eq(project.namespace.id.to_s)
end end
it 'preserves the project being searched in' do it 'preserves the project being searched in' do
...@@ -304,7 +304,7 @@ describe "Search" do ...@@ -304,7 +304,7 @@ describe "Search" do
fill_in 'search', with: 'foo' fill_in 'search', with: 'foo'
click_button 'Search' click_button 'Search'
expect(find('#project_id').value).to eq(project.id.to_s) expect(find('#project_id', visible: false).value).to eq(project.id.to_s)
end end
end end
end end
...@@ -23,10 +23,10 @@ describe NotesHelper do ...@@ -23,10 +23,10 @@ describe NotesHelper do
end end
describe "#notes_max_access_for_users" do describe "#notes_max_access_for_users" do
it 'returns human access levels' do it 'returns access levels' do
expect(helper.note_max_access_for_user(owner_note)).to eq('Owner') expect(helper.note_max_access_for_user(owner_note)).to eq(Gitlab::Access::OWNER)
expect(helper.note_max_access_for_user(master_note)).to eq('Master') expect(helper.note_max_access_for_user(master_note)).to eq(Gitlab::Access::MASTER)
expect(helper.note_max_access_for_user(reporter_note)).to eq('Reporter') expect(helper.note_max_access_for_user(reporter_note)).to eq(Gitlab::Access::REPORTER)
end end
it 'handles access in different projects' do it 'handles access in different projects' do
...@@ -34,8 +34,8 @@ describe NotesHelper do ...@@ -34,8 +34,8 @@ describe NotesHelper do
second_project.team << [master, :reporter] second_project.team << [master, :reporter]
other_note = create(:note, author: master, project: second_project) other_note = create(:note, author: master, project: second_project)
expect(helper.note_max_access_for_user(master_note)).to eq('Master') expect(helper.note_max_access_for_user(master_note)).to eq(Gitlab::Access::MASTER)
expect(helper.note_max_access_for_user(other_note)).to eq('Reporter') expect(helper.note_max_access_for_user(other_note)).to eq(Gitlab::Access::REPORTER)
end end
end end
......
...@@ -23,6 +23,38 @@ describe Gitlab::Geo::LogCursor::Daemon, :postgresql do ...@@ -23,6 +23,38 @@ describe Gitlab::Geo::LogCursor::Daemon, :postgresql do
end end
end end
context 'when replaying a repository created event' do
let(:repository_created_event) { create(:geo_repository_created_event) }
let(:event_log) { create(:geo_event_log, repository_created_event: repository_created_event) }
let!(:event_log_state) { create(:geo_event_log_state, event_id: event_log.id - 1) }
before do
allow(subject).to receive(:exit?).and_return(false, true)
end
it 'creates a new project registry' do
expect { subject.run! }.to change(Geo::ProjectRegistry, :count).by(1)
end
it 'sets resync attributes to true' do
subject.run!
registry = Geo::ProjectRegistry.last
expect(registry).to have_attributes(resync_repository: true, resync_wiki: true)
end
it 'sets resync_wiki to false if wiki_path is nil' do
repository_created_event.update_attribute(:wiki_path, nil)
subject.run!
registry = Geo::ProjectRegistry.last
expect(registry).to have_attributes(resync_repository: true, resync_wiki: false)
end
end
context 'when replaying a repository updated event' do context 'when replaying a repository updated event' do
let(:event_log) { create(:geo_event_log, :updated_event) } let(:event_log) { create(:geo_event_log, :updated_event) }
let!(:event_log_state) { create(:geo_event_log_state, event_id: event_log.id - 1) } let!(:event_log_state) { create(:geo_event_log_state, event_id: event_log.id - 1) }
......
...@@ -487,4 +487,71 @@ describe Issuable do ...@@ -487,4 +487,71 @@ describe Issuable do
end end
end end
end end
describe '#first_contribution?' do
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let(:other_project) { create(:project) }
let(:owner) { create(:owner) }
let(:master) { create(:user) }
let(:reporter) { create(:user) }
let(:guest) { create(:user) }
let(:contributor) { create(:user) }
let(:first_time_contributor) { create(:user) }
before do
group.add_owner(owner)
project.add_master(master)
project.add_reporter(reporter)
project.add_guest(guest)
project.add_guest(contributor)
project.add_guest(first_time_contributor)
end
let(:merged_mr) { create(:merge_request, :merged, author: contributor, target_project: project, source_project: project) }
let(:open_mr) { create(:merge_request, author: first_time_contributor, target_project: project, source_project: project) }
let(:merged_mr_other_project) { create(:merge_request, :merged, author: first_time_contributor, target_project: other_project, source_project: other_project) }
context "for merge requests" do
it "is false for MASTER" do
mr = create(:merge_request, author: master, target_project: project, source_project: project)
expect(mr).not_to be_first_contribution
end
it "is false for OWNER" do
mr = create(:merge_request, author: owner, target_project: project, source_project: project)
expect(mr).not_to be_first_contribution
end
it "is false for REPORTER" do
mr = create(:merge_request, author: reporter, target_project: project, source_project: project)
expect(mr).not_to be_first_contribution
end
it "is true when you don't have any merged MR" do
expect(open_mr).to be_first_contribution
expect(merged_mr).not_to be_first_contribution
end
it "handles multiple projects separately" do
expect(open_mr).to be_first_contribution
expect(merged_mr_other_project).not_to be_first_contribution
end
end
context "for issues" do
let(:contributor_issue) { create(:issue, author: contributor, project: project) }
let(:first_time_contributor_issue) { create(:issue, author: first_time_contributor, project: project) }
it "is false even without merged MR" do
expect(merged_mr).to be
expect(first_time_contributor_issue).not_to be_first_contribution
expect(contributor_issue).not_to be_first_contribution
end
end
end
end end
...@@ -2,10 +2,11 @@ require 'spec_helper' ...@@ -2,10 +2,11 @@ require 'spec_helper'
RSpec.describe Geo::EventLog, type: :model do RSpec.describe Geo::EventLog, type: :model do
describe 'relationships' do describe 'relationships' do
it { is_expected.to belong_to(:repository_updated_event).class_name('Geo::RepositoryUpdatedEvent').with_foreign_key('repository_updated_event_id') } it { is_expected.to belong_to(:repositories_changed_event).class_name('Geo::RepositoriesChangedEvent').with_foreign_key('repositories_changed_event_id') }
it { is_expected.to belong_to(:repository_created_event).class_name('Geo::RepositoryCreatedEvent').with_foreign_key('repository_created_event_id') }
it { is_expected.to belong_to(:repository_deleted_event).class_name('Geo::RepositoryDeletedEvent').with_foreign_key('repository_deleted_event_id') } it { is_expected.to belong_to(:repository_deleted_event).class_name('Geo::RepositoryDeletedEvent').with_foreign_key('repository_deleted_event_id') }
it { is_expected.to belong_to(:repository_renamed_event).class_name('Geo::RepositoryRenamedEvent').with_foreign_key('repository_renamed_event_id') } it { is_expected.to belong_to(:repository_renamed_event).class_name('Geo::RepositoryRenamedEvent').with_foreign_key('repository_renamed_event_id') }
it { is_expected.to belong_to(:repositories_changed_event).class_name('Geo::RepositoriesChangedEvent').with_foreign_key('repositories_changed_event_id') } it { is_expected.to belong_to(:repository_updated_event).class_name('Geo::RepositoryUpdatedEvent').with_foreign_key('repository_updated_event_id') }
end end
describe '#event' do describe '#event' do
...@@ -13,6 +14,13 @@ RSpec.describe Geo::EventLog, type: :model do ...@@ -13,6 +14,13 @@ RSpec.describe Geo::EventLog, type: :model do
expect(subject.event).to be_nil expect(subject.event).to be_nil
end end
it 'returns repository_created_event when set' do
repository_created_event = build(:geo_repository_created_event)
subject.repository_created_event = repository_created_event
expect(subject.event).to eq repository_created_event
end
it 'returns repository_updated_event when set' do it 'returns repository_updated_event when set' do
repository_updated_event = build(:geo_repository_updated_event) repository_updated_event = build(:geo_repository_updated_event)
subject.repository_updated_event = repository_updated_event subject.repository_updated_event = repository_updated_event
......
require 'spec_helper'
describe Geo::RepositoryCreatedEvent, type: :model do
describe 'relationships' do
it { is_expected.to belong_to(:project) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:project_name) }
it { is_expected.to validate_presence_of(:repo_path) }
it { is_expected.to validate_presence_of(:repository_storage_name) }
it { is_expected.to validate_presence_of(:repository_storage_path) }
end
end
require 'spec_helper'
describe Geo::RepositoryCreatedEventStore do
let(:project) { create(:project) }
subject(:event) { described_class.new(project) }
describe '#create' do
it 'does not create an event when not running on a primary node' do
allow(Gitlab::Geo).to receive(:primary?) { false }
expect { event.create }.not_to change(Geo::RepositoryCreatedEvent, :count)
end
context 'when running on a primary node' do
before do
allow(Gitlab::Geo).to receive(:primary?) { true }
end
it 'creates a created event' do
expect { event.create }.to change(Geo::RepositoryCreatedEvent, :count).by(1)
end
it 'tracks information for the created project' do
event.create
event = Geo::RepositoryCreatedEvent.last
expect(event).to have_attributes(
project_id: project.id,
repo_path: project.disk_path,
wiki_path: "#{project.disk_path}.wiki",
project_name: project.name,
repository_storage_name: project.repository_storage,
repository_storage_path: project.repository_storage_path
)
end
end
end
end
...@@ -33,11 +33,11 @@ describe 'shared/issuable/_approvals.html.haml' do ...@@ -33,11 +33,11 @@ describe 'shared/issuable/_approvals.html.haml' do
end end
it 'shows select approvers field' do it 'shows select approvers field' do
expect(rendered).to have_css('#merge_request_approver_ids') expect(rendered).to have_css('#merge_request_approver_ids', visible: false)
end end
it 'shows select approver groups field' do it 'shows select approver groups field' do
expect(rendered).to have_css('#merge_request_approver_group_ids') expect(rendered).to have_css('#merge_request_approver_group_ids', visible: false)
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment