Commit 31dd0ee2 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'ee/master' into ce-to-ee-2017-08-30

* ee/master: (89 commits)
  Merge branch '36807-gc-unwanted-refs-after-import' into tmp
  EE port of remove-sidebar-title
  Make namespace dropdown on new project page consistent
  Adds CE to EE compatibility with 2756
  Fixed Test
  Back to simple headlines without specific branching
  Add my-reaction filter (EE)
  Let's do JS later
  Add a spec when ressource is not modified
  Add changelog and doc
  Add tests for the unmodified header
  Update remaining endpoints
  Use commit date for branches and tags
  Conditionally destroy a ressource
  API: Respect the 'If-Unmodified-Since' for delete endpoints
  Fixes activation of project mirror when project is created.
  Kick off the pipelines again
  Fixed the default callout_id state
  Adjustments to calling logic
  Mapped correct setup
  ...
parents 388e07e1 1a8ee80e
......@@ -204,6 +204,7 @@ import initGroupAnalytics from './init_group_analytics';
new ProjectSelect();
break;
case 'projects:milestones:show':
new UserCallout();
case 'groups:milestones:show':
case 'dashboard:milestones:show':
new Milestone();
......@@ -290,6 +291,7 @@ import initGroupAnalytics from './init_group_analytics';
action: mrNewSubmitNode.dataset.mrSubmitAction,
});
}
new UserCallout();
case 'projects:merge_requests:creations:diffs':
case 'projects:merge_requests:edit':
new gl.Diff();
......@@ -391,7 +393,8 @@ import initGroupAnalytics from './init_group_analytics';
setupProjectEdit();
// Initialize expandable settings panels
initSettingsPanels();
new UsersSelect();
new UserCallout('js-service-desk-callout');
new UserCallout('js-mr-approval-callout');
break;
case 'projects:imports:show':
new ProjectImport();
......@@ -541,6 +544,7 @@ import initGroupAnalytics from './init_group_analytics';
break;
case 'projects:settings:repository:show':
new UsersSelect();
new UserCallout();
// Initialize expandable settings panels
initSettingsPanels();
break;
......
......@@ -85,6 +85,13 @@ class DropDown {
const renderableList = this.list.querySelector('ul[data-dynamic]') || this.list;
renderableList.innerHTML = children.join('');
const listEvent = new CustomEvent('render.dl', {
detail: {
list: this,
},
});
this.list.dispatchEvent(listEvent);
}
renderChildren(data) {
......
/* global Flash */
import Ajax from '~/droplab/plugins/ajax';
import Filter from '~/droplab/plugins/filter';
import './filtered_search_dropdown';
class DropdownEmoji extends gl.FilteredSearchDropdown {
constructor(options = {}) {
super(options);
this.config = {
Ajax: {
endpoint: `${gon.relative_url_root || ''}/autocomplete/award_emojis`,
method: 'setData',
loadingTemplate: this.loadingTemplate,
onError() {
/* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.');
/* eslint-enable no-new */
},
},
Filter: {
template: 'name',
},
};
import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(({ glEmojiTag }) => { this.glEmojiTag = glEmojiTag; })
.catch(() => { /* ignore error and leave emoji name in the search bar */ });
this.unbindEvents();
this.bindEvents();
}
bindEvents() {
super.bindEvents();
this.listRenderedWrapper = this.listRendered.bind(this);
this.dropdown.addEventListener('render.dl', this.listRenderedWrapper);
}
unbindEvents() {
this.dropdown.removeEventListener('render.dl', this.listRenderedWrapper);
super.unbindEvents();
}
listRendered() {
this.replaceEmojiElement();
}
itemClicked(e) {
super.itemClicked(e, (selected) => {
const name = selected.querySelector('.js-data-value').innerText.trim();
return gl.DropdownUtils.getEscapedText(name);
});
}
renderContent(forceShowList = false) {
this.droplab.changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config);
super.renderContent(forceShowList);
}
replaceEmojiElement() {
if (!this.glEmojiTag) return;
// Replace empty gl-emoji tag to real content
const dropdownItems = [...this.dropdown.querySelectorAll('.filter-dropdown-item')];
dropdownItems.forEach((dropdownItem) => {
const name = dropdownItem.querySelector('.js-data-value').innerText;
const emojiTag = this.glEmojiTag(name);
const emojiElement = dropdownItem.querySelector('gl-emoji');
emojiElement.outerHTML = emojiTag;
});
}
init() {
this.droplab
.addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
}
}
window.gl = window.gl || {};
gl.DropdownEmoji = DropdownEmoji;
......@@ -61,7 +61,7 @@ class DropdownHint extends gl.FilteredSearchDropdown {
.map(tokenKey => ({
icon: `fa-${tokenKey.icon}`,
hint: tokenKey.key,
tag: `<${tokenKey.symbol}${tokenKey.key}>`,
tag: `<${tokenKey.tag}>`,
type: tokenKey.type,
}));
......
import './dropdown_emoji';
import './dropdown_hint';
import './dropdown_non_user';
import './dropdown_user';
......
......@@ -62,6 +62,11 @@ class FilteredSearchDropdownManager {
},
element: this.container.querySelector('#js-dropdown-label'),
},
'my-reaction': {
reference: null,
gl: 'DropdownEmoji',
element: this.container.querySelector('#js-dropdown-my-reaction'),
},
hint: {
reference: null,
gl: 'DropdownHint',
......
......@@ -447,8 +447,13 @@ class FilteredSearchManager {
const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
if (match) {
const indexOf = keyParam.indexOf('_');
const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
// Use lastIndexOf because the token key is allowed to contain underscore
// e.g. 'my_reaction' is the token key of 'my_reaction_emoji'
const lastIndexOf = keyParam.lastIndexOf('_');
let sanitizedKey = lastIndexOf !== -1 ? keyParam.slice(0, lastIndexOf) : keyParam;
// Replace underscore with hyphen in the sanitizedkey.
// e.g. 'my_reaction' => 'my-reaction'
sanitizedKey = sanitizedKey.replace('_', '-');
const symbol = match.symbol;
let quotationsToUse = '';
......@@ -523,7 +528,10 @@ class FilteredSearchManager {
const condition = this.filteredSearchTokenKeys
.searchByConditionKeyValue(token.key, token.value.toLowerCase());
const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
const keyParam = param ? `${token.key}_${param}` : token.key;
// Replace hyphen with underscore to use as request parameter
// e.g. 'my-reaction' => 'my_reaction'
const underscoredKey = token.key.replace('-', '_');
const keyParam = param ? `${underscoredKey}_${param}` : underscoredKey;
let tokenPath = '';
if (condition) {
......
......@@ -4,26 +4,42 @@ const tokenKeys = [{
param: 'username',
symbol: '@',
icon: 'pencil',
tag: '@author',
}, {
key: 'assignee',
type: 'string',
param: 'username',
symbol: '@',
icon: 'user',
tag: '@assignee',
}, {
key: 'milestone',
type: 'string',
param: 'title',
symbol: '%',
icon: 'clock-o',
tag: '%milestone',
}, {
key: 'label',
type: 'array',
param: 'name[]',
symbol: '~',
icon: 'tag',
tag: '~label',
}];
if (gon.current_user_id) {
// Appending tokenkeys only logged-in
tokenKeys.push({
key: 'my-reaction',
type: 'string',
param: 'emoji',
symbol: '',
icon: 'thumbs-up',
tag: 'emoji',
});
}
const alternativeTokenKeys = [{
key: 'label',
type: 'string',
......@@ -84,6 +100,10 @@ class FilteredSearchTokenKeys {
return tokenKeysWithAlternative.find((tokenKey) => {
let tokenKeyParam = tokenKey.key;
// Replace hyphen with underscore to compare keyParam with tokenKeyParam
// e.g. 'my-reaction' => 'my_reaction'
tokenKeyParam = tokenKeyParam.replace('-', '_');
if (tokenKey.param) {
tokenKeyParam += `_${tokenKey.param}`;
}
......
......@@ -6,6 +6,7 @@ const weightTokenKey = {
param: '',
symbol: '',
icon: 'balance-scale',
tag: 'weight',
};
const weightConditions = [{
......@@ -76,6 +77,10 @@ class FilteredSearchTokenKeysIssuesEE extends gl.FilteredSearchTokenKeys {
return tokenKeysWithAlternative.find((tokenKey) => {
let tokenKeyParam = tokenKey.key;
// Replace hyphen with underscore to compare keyParam with tokenKeyParam
// e.g. 'my-reaction' => 'my_reaction'
tokenKeyParam = tokenKeyParam.replace('-', '_');
if (tokenKey.param) {
tokenKeyParam += `_${tokenKey.param}`;
}
......
......@@ -132,6 +132,23 @@ class FilteredSearchVisualTokens {
.catch(() => { });
}
static updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) {
const container = tokenValueContainer;
const element = tokenValueElement;
return import(/* webpackChunkName: 'emoji' */ '../emoji')
.then((Emoji) => {
if (!Emoji.isEmojiNameValid(tokenValue)) {
return;
}
container.dataset.originalValue = tokenValue;
element.innerHTML = Emoji.glEmojiTag(tokenValue);
})
// ignore error and leave emoji name in the search bar
.catch(() => { });
}
static renderVisualTokenValue(parentElement, tokenName, tokenValue) {
const tokenValueContainer = parentElement.querySelector('.value-container');
const tokenValueElement = tokenValueContainer.querySelector('.value');
......@@ -144,6 +161,10 @@ class FilteredSearchVisualTokens {
FilteredSearchVisualTokens.updateUserTokenAppearance(
tokenValueContainer, tokenValueElement, tokenValue,
);
} else if (tokenType === 'my-reaction') {
FilteredSearchVisualTokens.updateEmojiTokenAppearance(
tokenValueContainer, tokenValueElement, tokenValue,
);
}
}
......
/* global Chart */
export default () => {
const dataEl = document.getElementById('js-analytics-data');
const data = JSON.parse(dataEl.innerHTML);
const labels = data.labels;
const outputElIds = ['push', 'issues_closed', 'merge_requests_created'];
if (dataEl) {
const data = JSON.parse(dataEl.innerHTML);
const labels = data.labels;
const outputElIds = ['push', 'issues_closed', 'merge_requests_created'];
outputElIds.forEach((id) => {
const el = document.getElementById(id);
const ctx = el.getContext('2d');
const chart = new Chart(ctx);
outputElIds.forEach((id) => {
const el = document.getElementById(id);
const ctx = el.getContext('2d');
const chart = new Chart(ctx);
chart.Bar(
{
chart.Bar({
labels,
datasets: [{
fillColor: 'rgba(220,220,220,0.5)',
......@@ -22,13 +22,14 @@ export default () => {
data: data[id].data,
}],
},
{
scaleOverlay: true,
responsive: true,
maintainAspectRatio: false,
},
);
});
{
scaleOverlay: true,
responsive: true,
maintainAspectRatio: false,
},
);
});
$('#event-stats').tablesorter();
$('#event-stats').tablesorter();
}
};
......@@ -15,6 +15,7 @@ export default class NewNavSidebar {
this.$openSidebar = $('.toggle-mobile-nav');
this.$closeSidebar = $('.close-nav-button');
this.$sidebarToggle = $('.js-toggle-sidebar');
this.$topLevelLinks = $('.sidebar-top-level-items > li > a');
}
bindEvents() {
......@@ -50,6 +51,10 @@ export default class NewNavSidebar {
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
}
NewNavSidebar.setCollapsedCookie(collapsed);
this.$topLevelLinks.attr('title', function updateTopLevelTitle() {
return collapsed ? this.getAttribute('aria-label') : '';
});
}
render() {
......
......@@ -10,7 +10,7 @@ export default class UserCallout {
init() {
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
$('.js-close-callout').on('click', e => this.dismissCallout(e));
this.userCalloutBody.find('.js-close-callout').on('click', e => this.dismissCallout(e));
}
}
......
......@@ -225,6 +225,18 @@
color: $common-gray-dark;
}
gl-emoji {
display: inline-block;
font-family: inherit;
font-size: inherit;
vertical-align: inherit;
img {
height: 18px;
width: 18px;
}
}
.form-control {
position: relative;
min-width: 200px;
......@@ -277,7 +289,7 @@
}
.filtered-search-input-dropdown-menu {
max-height: 225px;
max-height: 260px;
max-width: 280px;
overflow: auto;
......
......@@ -279,6 +279,7 @@
// TODO: change global style
.ajax-project-dropdown,
body[data-page="projects:new"] #select2-drop,
body[data-page="projects:blob:new"] #select2-drop,
body[data-page="projects:blob:edit"] #select2-drop {
&.select2-drop {
......
......@@ -17,6 +17,7 @@ $darken-border-factor: 5%;
$white-light: #fff;
$white-normal: #f0f0f0;
$white-dark: #eaeaea;
$white-transparent: rgba(255, 255, 255, 0.8);
$gray-lightest: #fdfdfd;
$gray-light: #fafafa;
......
......@@ -289,6 +289,7 @@ table.u2f-registrations {
margin: 20px -5px 0;
.bordered-box {
padding: 32px;
border: 1px solid $blue-300;
border-radius: $border-radius-default;
background-color: $blue-25;
......@@ -296,10 +297,6 @@ table.u2f-registrations {
display: flex;
justify-content: center;
align-items: center;
}
.landing {
padding: 32px;
.close {
position: absolute;
......
.user-callout.promotion-callout {
margin: 20px 0 0;
&.prepend-top-10 {
margin-top: 10px;
}
&.append-bottom-20 {
margin-bottom: 20px;
}
.bordered-box {
padding: 20px;
border-color: $border-color;
background-color: $white-light;
align-items: flex-start;
.user-callout-copy {
max-width: 700px;
}
.close {
.dismiss-icon {
color: $gray-darkest;
}
&:hover {
.dismiss-icon {
color: $text-color;
}
}
}
.svg-container {
margin-right: 15px;
}
}
}
.promotion-modal {
.modal-dialog {
width: 540px;
}
.modal-header {
border-bottom: none;
}
.modal-body {
margin-top: -20px;
padding: 16px 16px 32px;
}
.modal-footer {
border-top: none;
}
}
.promotion-backdrop {
background-color: $white-transparent;
position: absolute;
padding-top: 72px;
.user-callout-copy {
max-width: 700px;
}
}
class AutocompleteController < ApplicationController
skip_before_action :authenticate_user!, only: [:users]
AWARD_EMOJI_MAX = 100
skip_before_action :authenticate_user!, only: [:users, :award_emojis]
before_action :load_project, only: [:users, :project_groups]
before_action :find_users, only: [:users]
......@@ -52,6 +54,20 @@ class AutocompleteController < ApplicationController
render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace)
end
def award_emojis
emoji_with_count = AwardEmoji
.limit(AWARD_EMOJI_MAX)
.where(user: current_user)
.group(:name)
.order(count: :desc, name: :asc)
.count
# Transform from hash to array to guarantee json order
# e.g. { 'thumbsup' => 2, 'thumbsdown' = 1 }
# => [{ name: 'thumbsup' }, { name: 'thumbsdown' }]
render json: emoji_with_count.map { |k, v| { name: k } }
end
private
def load_users_by_ability
......
......@@ -30,4 +30,8 @@ class Groups::AnalyticsController < Groups::ApplicationController
def user_ids
@user_ids ||= @users.map(&:id)
end
def check_contribution_analytics_available!
render_404 unless @group.feature_available?(:contribution_analytics) || LicenseHelper.show_promotions?(current_user)
end
end
class Projects::AuditEventsController < Projects::ApplicationController
include LicenseHelper
before_action :authorize_admin_project!
before_action :check_audit_events_available!
......@@ -7,4 +9,8 @@ class Projects::AuditEventsController < Projects::ApplicationController
def index
@events = project.audit_events.page(params[:page])
end
def check_audit_events_available!
render_404 unless @project.feature_available?(:audit_events) || LicenseHelper.show_promotions?(current_user)
end
end
......@@ -18,6 +18,7 @@
# sort: string
# non_archived: boolean
# iids: integer[]
# my_reaction_emoji: string
#
class IssuableFinder
include CreatedAtFilter
......@@ -51,6 +52,7 @@ class IssuableFinder
items = by_iids(items)
items = by_milestone(items)
items = by_label(items)
items = by_my_reaction_emoji(items)
# Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far
items = by_project(items)
......@@ -389,6 +391,14 @@ class IssuableFinder
params[:weight] == Issue::WEIGHT_ANY
end
def by_my_reaction_emoji(items)
if params[:my_reaction_emoji].present? && current_user
items = items.awarded(current_user, params[:my_reaction_emoji])
end
items
end
def by_due_date(items)
if due_date?
if filter_by_no_due_date?
......
......@@ -67,5 +67,30 @@ module LicenseHelper
uri.to_s
end
def upgrade_plan_url
if @project.group
group_billings_path(@project.group)
else
profile_billings_path
end
end
def show_promotions?(selected_user = current_user)
return false unless selected_user
return @show_promotions if defined?(@show_promotions)
@show_promotions =
if current_application_settings.should_check_namespace_plan?
true
else
license = License.current
license.nil? || license.expired?
end
end
def show_project_feature_promotion?(project_feature, callout_id = nil)
!@project.feature_available?(project_feature) && show_promotions? && (callout_id.nil? || show_callout?(callout_id))
end
extend self
end
......@@ -119,4 +119,8 @@ module TabHelper
'active' if current_controller?('oauth/applications')
end
def sidebar_link(href, title: nil, css: nil, &block)
link_to capture(&block), href, title: (title if collapsed_sidebar?), class: css, aria: { label: title }
end
end
......@@ -11,6 +11,21 @@ module Awardable
end
module ClassMethods
def awarded(user, name)
sql = <<~EOL
EXISTS (
SELECT TRUE
FROM award_emoji
WHERE user_id = :user_id AND
name = :name AND
awardable_type = :awardable_type AND
awardable_id = #{self.arel_table.name}.id
)
EOL
where(sql, user_id: user.id, name: name, awardable_type: self.name)
end
def order_upvotes_desc
order_votes_desc(AwardEmoji::UPVOTE_NAME)
end
......
......@@ -114,7 +114,7 @@ class Environment < ActiveRecord::Base
end
def ref_path
"refs/environments/#{Shellwords.shellescape(name)}"
"refs/#{Repository::REF_ENVIRONMENTS}/#{Shellwords.shellescape(name)}"
end
def formatted_external_url
......
......@@ -828,7 +828,7 @@ class MergeRequest < ActiveRecord::Base
end
def ref_path
"refs/merge-requests/#{iid}/head"
"refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
end
def ref_fetched?
......
......@@ -371,11 +371,7 @@ class Project < ActiveRecord::Base
if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
project.run_after_commit do
begin
Projects::HousekeepingService.new(project).execute
rescue Projects::HousekeepingService::LeaseTaken => e
Rails.logger.info("Could not perform housekeeping for project #{project.full_path} (#{project.id}): #{e}")
end
Projects::AfterImportService.new(project).execute
end
end
end
......
......@@ -3,6 +3,18 @@ require 'securerandom'
require 'forwardable'
class Repository
REF_MERGE_REQUEST = 'merge-requests'.freeze
REF_KEEP_AROUND = 'keep-around'.freeze
REF_ENVIRONMENTS = 'environments'.freeze
RESERVED_REFS_NAMES = %W[
heads
tags
#{REF_ENVIRONMENTS}
#{REF_KEEP_AROUND}
#{REF_ENVIRONMENTS}
].freeze
include Gitlab::ShellAdapter
include Elastic::RepositoriesSearch
include RepositoryMirroring
......@@ -241,10 +253,10 @@ class Repository
begin
write_ref(keep_around_ref_name(sha), sha)
rescue Rugged::ReferenceError => ex
Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
rescue Rugged::OSError => ex
raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/
Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
end
end
......@@ -1239,7 +1251,7 @@ class Repository
end
def keep_around_ref_name(sha)
"refs/keep-around/#{sha}"
"refs/#{REF_KEEP_AROUND}/#{sha}"
end
def repository_event(event, tags = {})
......@@ -1299,4 +1311,16 @@ class Repository
.commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
.map { |c| commit(c) }
end
def with_repo_tmp_commit(start_repository, start_branch_name, sha)
tmp_ref = fetch_ref(
start_repository.path_to_repo,
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
"refs/tmp/#{SecureRandom.hex}/head"
)
yield commit(sha)
ensure
delete_refs(tmp_ref) if tmp_ref
end
end
module Projects
class AfterImportService
RESERVED_REFS_REGEXP =
%r{\Arefs/(?:#{Regexp.union(*Repository::RESERVED_REFS_NAMES)})/}
def initialize(project)
@project = project
end
def execute
Projects::HousekeepingService.new(@project).execute do
repository.delete_refs(*garbage_refs)
end
rescue Projects::HousekeepingService::LeaseTaken => e
Rails.logger.info(
"Could not perform housekeeping for project #{@project.full_path} (#{@project.id}): #{e}")
end
private
def garbage_refs
@garbage_refs ||= repository.all_ref_names_except(RESERVED_REFS_REGEXP)
end
def repository
@repository ||= @project.repository
end
end
end
module Projects
class CreateService < BaseService
prepend ::EE::Projects::CreateService
def initialize(user, params)
@current_user, @params = user, params.dup
end
......@@ -47,6 +49,8 @@ module Projects
@project.namespace_id = current_user.namespace_id
end
yield(@project) if block_given?
@project.creator = current_user
if forked_from_project_id
......
......@@ -26,6 +26,8 @@ module Projects
lease_uuid = try_obtain_lease
raise LeaseTaken unless lease_uuid.present?
yield if block_given?
execute_gitlab_shell_gc(lease_uuid)
end
......
module Projects
class UpdateService < BaseService
prepend ::EE::Projects::UpdateService
def execute
# Repository size limit comes as MB from the view
limit = params.delete(:repository_size_limit)
......
- page_title "Contribution Analytics"
- header_title group_title(@group, "Contribution Analytics", group_analytics_path(@group))
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('graphs')
.sub-header-block
.pull-right
.dropdown.inline
%button.dropdown-toggle.btn{ type: 'button', 'data-toggle' => 'dropdown' }
= icon('calendar-o')
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right
- if @group.feature_available?(:contribution_analytics)
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('graphs')
.sub-header-block
.pull-right
.dropdown.inline
%button.dropdown-toggle.btn{ type: 'button', 'data-toggle' => 'dropdown' }
= icon('calendar-o')
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to group_analytics_path(@group, start_date: Date.today - 1.week) do
Last week
%li
= link_to group_analytics_path(@group, start_date: Date.today - 1.month) do
Last month
%li
= link_to group_analytics_path(@group, start_date: Date.today - 3.months) do
Last 3 months
.oneline
Contribution analytics for issues, merge requests and push events since #{@start_date}
%h3 Push
.row
.col-md-4
%ul
%li
= link_to group_analytics_path(@group, start_date: Date.today - 1.week) do
Last week
= @events.code_push.count
times
%li
= link_to group_analytics_path(@group, start_date: Date.today - 1.month) do
Last month
more than
= @events.code_push.map(&:commits_count).sum
commits
%li
= link_to group_analytics_path(@group, start_date: Date.today - 3.months) do
Last 3 months
.oneline
Contribution analytics for issues, merge requests and push events since #{@start_date}
%h3 Push
.row
.col-md-4
%ul
%li
= @events.code_push.count
times
%li
more than
= @events.code_push.map(&:commits_count).sum
commits
%li
by
= pluralize @events.code_push.pluck(:author_id).uniq.count, 'person'
by
= pluralize @events.code_push.pluck(:author_id).uniq.count, 'person'
.col-md-8
%div
%p.light Push events per group member
%canvas#push{ height: 250 }
.col-md-8
%div
%p.light Push events per group member
%canvas#push{ height: 250 }
%h3 Merge Requests
%h3 Merge Requests
.row
.col-md-4
%ul
%li
= @events.merge_requests.created.count
created
%li
= @events.merge_requests.merged.count
accepted
.row
.col-md-4
%ul
%li
= @events.merge_requests.created.count
created
%li
= @events.merge_requests.merged.count
accepted
.col-md-8
%div
%p.light Merge requests created per group member
%canvas#merge_requests_created{ height: 250 }
.col-md-8
%div
%p.light Merge requests created per group member
%canvas#merge_requests_created{ height: 250 }
%h3 Issues
%h3 Issues
.row
.col-md-4
%ul
%li
= @events.issues.created.count
created
%li
= @events.issues.closed.pluck(:target_id).uniq.count
closed
.row
.col-md-4
%ul
%li
= @events.issues.created.count
created
%li
= @events.issues.closed.pluck(:target_id).uniq.count
closed
.col-md-8
%div
%p.light Issues closed per group member
%canvas#issues_closed{ height: 250 }
.col-md-8
%div
%p.light Issues closed per group member
%canvas#issues_closed{ height: 250 }
.gray-content-block
.oneline
Contributions per group member
.gray-content-block
.oneline
Contributions per group member
.table-holder
%table.table.sortable-table#event-stats
%thead
%tr
%th.sortable
Name
= icon('sort')
%th.sortable
Pushed
= icon('sort')
%th.sortable
Opened issues
= icon('sort')
%th.sortable
Closed issues
= icon('sort')
%th.sortable
Opened MR
= icon('sort')
%th.sortable
Accepted MR
= icon('sort')
%th.sortable
Total Contributions
= icon('sort')
%tbody
- @users.each_with_index do |user, index|
.table-holder
%table.table.sortable-table#event-stats
%thead
%tr
%td
%strong
= link_to user.name, user
%td= @stats[:push][index]
%td= @stats[:issues_created][index]
%td= @stats[:issues_closed][index]
%td= @stats[:merge_requests_created][index]
%td= @stats[:merge_requests_merged][index]
%td= @stats[:total_events][index]
%th.sortable
Name
= icon('sort')
%th.sortable
Pushed
= icon('sort')
%th.sortable
Opened issues
= icon('sort')
%th.sortable
Closed issues
= icon('sort')
%th.sortable
Opened MR
= icon('sort')
%th.sortable
Accepted MR
= icon('sort')
%th.sortable
Total Contributions
= icon('sort')
%tbody
- @users.each_with_index do |user, index|
%tr
%td
%strong
= link_to user.name, user
%td= @stats[:push][index]
%td= @stats[:issues_created][index]
%td= @stats[:issues_closed][index]
%td= @stats[:merge_requests_created][index]
%td= @stats[:merge_requests_merged][index]
%td= @stats[:total_events][index]
%script#js-analytics-data{ type: "application/json" }
- data = {}
- data[:labels] = @users.map(&:name)
- [:push, :issues_closed, :merge_requests_created].each do |scope|
- data[scope] = {}
- data[scope][:data] = @stats[scope]
= data.to_json.html_safe
%script#js-analytics-data{ type: "application/json" }
- data = {}
- data[:labels] = @users.map(&:name)
- [:push, :issues_closed, :merge_requests_created].each do |scope|
- data[scope] = {}
- data[scope][:data] = @stats[scope]
= data.to_json.html_safe
- elsif show_promotions?
= render 'shared/promotions/promote_contribution_analytics'
......@@ -24,7 +24,7 @@
= link_to group_group_members_path(@group), title: 'Members' do
%span
Members
- if @group.feature_available?(:contribution_analytics)
- if @group.feature_available?(:contribution_analytics) || show_promotions?
= nav_link(path: 'analytics#show') do
= link_to group_analytics_path(@group), title: 'Contribution Analytics', data: {placement: 'right'} do
%span
......
......@@ -7,7 +7,7 @@
.sidebar-context-title Admin Area
%ul.sidebar-top-level-items
= nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
= sidebar_link admin_root_path, title: _('Overview'), css: 'shortcuts-tree' do
.nav-icon-container
= custom_icon('overview')
%span.nav-item-name
......@@ -48,7 +48,7 @@
ConvDev Index
= nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles audit_logs)) do
= link_to admin_conversational_development_index_path, title: 'Monitoring' do
= sidebar_link admin_conversational_development_index_path, title: _('Monitoring') do
.nav-icon-container
= custom_icon('monitoring')
%span.nav-item-name
......@@ -78,28 +78,28 @@
= render 'layouts/nav/ee/new_admin_monitoring_sidebar'
= nav_link(controller: :broadcast_messages) do
= link_to admin_broadcast_messages_path, title: 'Messages' do
= sidebar_link admin_broadcast_messages_path, title: _('Messages') do
.nav-icon-container
= custom_icon('messages')
%span.nav-item-name
Messages
= nav_link(controller: [:hooks, :hook_logs]) do
= link_to admin_hooks_path, title: 'Hooks' do
= sidebar_link admin_hooks_path, title: _('Hooks') do
.nav-icon-container
= custom_icon('system_hooks')
%span.nav-item-name
System Hooks
= nav_link(controller: :applications) do
= link_to admin_applications_path, title: 'Applications' do
= sidebar_link admin_applications_path, title: _('Applications') do
.nav-icon-container
= custom_icon('applications')
%span.nav-item-name
Applications
= nav_link(controller: :abuse_reports) do
= link_to admin_abuse_reports_path, title: "Abuse Reports" do
= sidebar_link admin_abuse_reports_path, title: _("Abuse Reports") do
.nav-icon-container
= custom_icon('abuse_reports')
%span.nav-item-name
......@@ -107,7 +107,7 @@
%span.badge.count= number_with_delimiter(AbuseReport.count(:all))
= nav_link(controller: :licenses) do
= link_to admin_license_path, title: 'License' do
= sidebar_link admin_license_path, title: _('License') do
.nav-icon-container
= custom_icon('license')
%span.nav-item-name
......@@ -115,56 +115,56 @@
- if akismet_enabled?
= nav_link(controller: :spam_logs) do
= link_to admin_spam_logs_path, title: "Spam Logs" do
= sidebar_link admin_spam_logs_path, title: _("Spam Logs") do
.nav-icon-container
= custom_icon('spam_logs')
%span.nav-item-name
Spam Logs
= nav_link(controller: :push_rules) do
= link_to admin_push_rule_path, title: 'Push Rules' do
= sidebar_link admin_push_rule_path, title: _('Push Rules') do
.nav-icon-container
= custom_icon('push_rules')
%span.nav-item-name
Push Rules
= nav_link(controller: :geo_nodes) do
= link_to admin_geo_nodes_path, title: 'Geo Nodes' do
= sidebar_link admin_geo_nodes_path, title: _('Geo Nodes') do
.nav-icon-container
= custom_icon('geo_nodes')
%span.nav-item-name
Geo Nodes
= nav_link(controller: :deploy_keys) do
= link_to admin_deploy_keys_path, title: 'Deploy Keys' do
= sidebar_link admin_deploy_keys_path, title: _('Deploy Keys') do
.nav-icon-container
= custom_icon('key')
%span.nav-item-name
Deploy Keys
= nav_link(controller: :services) do
= link_to admin_application_settings_services_path, title: 'Service Templates' do
= sidebar_link admin_application_settings_services_path, title: _('Service Templates') do
.nav-icon-container
= custom_icon('service_templates')
%span.nav-item-name
Service Templates
= nav_link(controller: :labels) do
= link_to admin_labels_path, title: 'Labels' do
= sidebar_link admin_labels_path, title: _('Labels') do
.nav-icon-container
= custom_icon('labels')
%span.nav-item-name
Labels
= nav_link(controller: :appearances) do
= link_to admin_appearances_path, title: 'Appearances' do
= sidebar_link admin_appearances_path, title: _('Appearances') do
.nav-icon-container
= custom_icon('appearance')
%span.nav-item-name
Appearance
= nav_link(controller: :application_settings) do
= link_to admin_application_settings_path, title: 'Settings' do
= sidebar_link admin_application_settings_path, title: _('Settings') do
.nav-icon-container
= custom_icon('settings')
%span.nav-item-name
......
......@@ -8,7 +8,7 @@
= @group.name
%ul.sidebar-top-level-items
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups', 'analytics#show'], html_options: { class: 'home' }) do
= link_to group_path(@group), title: 'Group overview' do
= sidebar_link group_path(@group), title: _('Group overview') do
.nav-icon-container
= custom_icon('project')
%span.nav-item-name
......@@ -25,14 +25,14 @@
%span
Activity
- if @group.feature_available?(:contribution_analytics)
- if @group.feature_available?(:contribution_analytics) || show_promotions?
= nav_link(path: 'analytics#show') do
= link_to group_analytics_path(@group), title: 'Contribution Analytics', data: {placement: 'right'} do
%span
Contribution Analytics
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
= link_to issues_group_path(@group), title: 'Issues' do
= sidebar_link issues_group_path(@group), title: _('Issues') do
.nav-icon-container
= custom_icon('issues')
%span.nav-item-name
......@@ -57,7 +57,7 @@
Milestones
= nav_link(path: 'groups#merge_requests') do
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do
= sidebar_link merge_requests_group_path(@group), title: _('Merge Requests') do
.nav-icon-container
= custom_icon('mr_bold')
%span.nav-item-name
......@@ -65,14 +65,14 @@
Merge Requests
%span.badge.count= number_with_delimiter(merge_requests.count)
= nav_link(path: 'group_members#index') do
= link_to group_group_members_path(@group), title: 'Members' do
= sidebar_link group_group_members_path(@group), title: _('Members') do
.nav-icon-container
= custom_icon('members')
%span.nav-item-name
Members
- if current_user && can?(current_user, :admin_group, @group)
= nav_link(path: %w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]) do
= link_to edit_group_path(@group), title: 'Settings' do
= sidebar_link edit_group_path(@group), title: _('Settings') do
.nav-icon-container
= custom_icon('settings')
%span.nav-item-name
......
......@@ -7,13 +7,13 @@
.sidebar-context-title User Settings
%ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path, title: 'Profile Settings' do
= sidebar_link profile_path, title: _('Profile Settings') do
.nav-icon-container
= custom_icon('profile')
%span.nav-item-name
Profile
= nav_link(controller: [:accounts, :two_factor_auths]) do
= link_to profile_account_path, title: 'Account' do
= sidebar_link profile_account_path, title: _('Account') do
.nav-icon-container
= custom_icon('account')
%span.nav-item-name
......@@ -25,68 +25,68 @@
Billing
- if current_application_settings.user_oauth_applications?
= nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path, title: 'Applications' do
= sidebar_link applications_profile_path, title: _('Applications') do
.nav-icon-container
= custom_icon('applications')
%span.nav-item-name
Applications
= nav_link(controller: :chat_names) do
= link_to profile_chat_names_path, title: 'Chat' do
= sidebar_link profile_chat_names_path, title: _('Chat') do
.nav-icon-container
= custom_icon('chat')
%span.nav-item-name
Chat
= nav_link(controller: :personal_access_tokens) do
= link_to profile_personal_access_tokens_path, title: 'Access Tokens' do
= sidebar_link profile_personal_access_tokens_path, title: _('Access Tokens') do
.nav-icon-container
= custom_icon('access_tokens')
%span.nav-item-name
Access Tokens
= nav_link(controller: :emails) do
= link_to profile_emails_path, title: 'Emails' do
= sidebar_link profile_emails_path, title: _('Emails') do
.nav-icon-container
= custom_icon('emails')
%span.nav-item-name
Emails
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
= link_to edit_profile_password_path, title: 'Password' do
= sidebar_link edit_profile_password_path, title: _('Password') do
.nav-icon-container
= custom_icon('lock')
%span.nav-item-name
Password
= nav_link(controller: :notifications) do
= link_to profile_notifications_path, title: 'Notifications' do
= sidebar_link profile_notifications_path, title: _('Notifications') do
.nav-icon-container
= custom_icon('notifications')
%span.nav-item-name
Notifications
= nav_link(controller: :keys) do
= link_to profile_keys_path, title: 'SSH Keys' do
= sidebar_link profile_keys_path, title: _('SSH Keys') do
.nav-icon-container
= custom_icon('key')
%span.nav-item-name
SSH Keys
= nav_link(controller: :gpg_keys) do
= link_to profile_gpg_keys_path, title: 'GPG Keys' do
= sidebar_link profile_gpg_keys_path, title: _('GPG Keys') do
.nav-icon-container
= custom_icon('key_2')
%span.nav-item-name
GPG Keys
= nav_link(controller: :preferences) do
= link_to profile_preferences_path, title: 'Preferences' do
= sidebar_link profile_preferences_path, title: _('Preferences') do
.nav-icon-container
= custom_icon('preferences')
%span.nav-item-name
Preferences
= nav_link(path: 'profiles#audit_log') do
= link_to audit_log_profile_path, title: 'Authentication log' do
= sidebar_link audit_log_profile_path, title: _('Authentication log') do
.nav-icon-container
= custom_icon('authentication_log')
%span.nav-item-name
Authentication log
= nav_link(path: 'profiles#pipeline_quota') do
= link_to profile_pipeline_quota_path, title: 'Pipeline quota' do
= sidebar_link profile_pipeline_quota_path, title: _('Pipeline quota') do
.nav-icon-container
= custom_icon('pipeline')
%span.nav-item-name
......
......@@ -9,7 +9,7 @@
= @project.name
%ul.sidebar-top-level-items
= nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
= link_to project_path(@project), title: 'Project overview', class: 'shortcuts-project' do
= sidebar_link project_path(@project), title: _('Project overview'), css: 'shortcuts-project' do
.nav-icon-container
= custom_icon('project')
%span.nav-item-name
......@@ -31,7 +31,7 @@
- if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do
= link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do
= sidebar_link project_tree_path(@project), title: _('Repository'), css: 'shortcuts-tree' do
.nav-icon-container
= custom_icon('doc_text')
%span.nav-item-name
......@@ -72,7 +72,7 @@
- if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
= sidebar_link project_container_registry_index_path(@project), title: _('Container Registry'), css: 'shortcuts-container-registry' do
.nav-icon-container
= custom_icon('container_registry')
%span.nav-item-name
......@@ -80,7 +80,7 @@
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
= link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do
= sidebar_link project_issues_path(@project), title: _('Issues'), css: 'shortcuts-issues' do
.nav-icon-container
= custom_icon('issues')
%span.nav-item-name
......@@ -112,7 +112,7 @@
- if project_nav_tab? :merge_requests
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
= link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
= sidebar_link project_merge_requests_path(@project), title: _('Merge Requests'), css: 'shortcuts-merge_requests' do
.nav-icon-container
= custom_icon('mr_bold')
%span.nav-item-name
......@@ -122,7 +122,7 @@
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do
= link_to project_pipelines_path(@project), title: 'CI / CD', class: 'shortcuts-pipelines' do
= sidebar_link project_pipelines_path(@project), title: _('CI / CD'), css: 'shortcuts-pipelines' do
.nav-icon-container
= custom_icon('pipeline')
%span.nav-item-name
......@@ -161,7 +161,7 @@
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
= link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do
= sidebar_link get_project_wiki_path(@project), title: _('Wiki'), css: 'shortcuts-wiki' do
.nav-icon-container
= custom_icon('wiki')
%span.nav-item-name
......@@ -169,7 +169,7 @@
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
= link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do
= sidebar_link project_snippets_path(@project), title: _('Snippets'), css: 'shortcuts-snippets' do
.nav-icon-container
= custom_icon('snippets')
%span.nav-item-name
......@@ -177,7 +177,7 @@
- if project_nav_tab? :settings
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do
= link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do
= sidebar_link edit_project_path(@project), title: _('Settings'), css: 'shortcuts-tree' do
.nav-icon-container
= custom_icon('settings')
%span.nav-item-name
......@@ -214,6 +214,8 @@
%span
Pages
= render 'projects/settings/ee/nav'
- else
= nav_link(path: %w[members#show]) do
= link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do
......
......@@ -2,7 +2,11 @@
= render "projects/settings/head"
- if show_project_feature_promotion?(:audit_events)
= render 'shared/promotions/promote_audit_events'
%h3.page-title Project Audit Events
%p.light Events in #{@project.full_path}
= render 'shared/audit_events/event_table', events: @events
- if @project.feature_available?(:audit_events)
= render 'shared/audit_events/event_table', events: @events
......@@ -163,24 +163,13 @@
%p
Customize your merge request restrictions.
.settings-content.no-animate{ class: ('expanded' if expanded) }
= render 'shared/promotions/promote_mr_features'
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f|
= render 'merge_request_settings', form: f
= f.submit 'Save changes', class: "btn btn-save"
- if EE::Gitlab::ServiceDesk.enabled?(project: @project)
%section.settings.js-service-desk-setting-wrapper
.settings-header
%h4
Service Desk
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Customize your service desk settings.
= link_to "Learn more about service desk.", help_page_path('user/project/service_desk')
.settings-content.no-animate{ class: ('expanded' if expanded) }
.js-service-desk-setting-root{ data: { endpoint: project_service_desk_path(@project),
enabled: "#{@project.service_desk_enabled}",
incoming_email: (@project.service_desk_address if @project.service_desk_enabled) } }
= render 'projects/ee/service_desk_settings'
= render 'export', project: @project
......
- if current_user && @project.feature_available?(:export_issues)
- if (current_user && @project.feature_available?(:export_issues)) || show_promotions?
%button.csv_download_link.btn.append-right-10.has-tooltip{ title: 'Export as CSV' }
= icon('download')
- return unless current_user && @project.feature_available?(:export_issues)
.issues-export-modal.modal
.modal-dialog
.modal-content
.modal-header
%a.close{ href: '#', 'data-dismiss' => 'modal' } ×
.export-svg-container.pull-right
= render 'projects/issues/export_issues/export_issues_list.svg'
%h3
Export issues
.modal-header
= icon('check', { class: 'export-checkmark' })
%strong
#{pluralize(issuables_count_for_state(:issues, params[:state]), 'issue')} selected
.modal-body
%div
The CSV export will be created in the background. Once finished, it will be sent to
%strong= @current_user.notification_email
in an attachment.
.modal-footer
= link_to 'Export issues', export_csv_project_issues_path(@project, request.query_parameters), method: :post, class: 'btn btn-success pull-left', title: 'Export issues'
- if current_user && @project.feature_available?(:export_issues)
.issues-export-modal.modal
.modal-dialog
.modal-content
.modal-header
%a.close{ href: '#', 'data-dismiss' => 'modal' } ×
.export-svg-container.pull-right
= render 'projects/issues/export_issues/export_issues_list.svg'
%h3
Export issues
.modal-header
= icon('check', { class: 'export-checkmark' })
%strong
#{pluralize(issuables_count_for_state(:issues, params[:state]), 'issue')} selected
.modal-body
%div
The CSV export will be created in the background. Once finished, it will be sent to
%strong= @current_user.notification_email
in an attachment.
.modal-footer
= link_to 'Export issues', export_csv_project_issues_path(@project, request.query_parameters), method: :post, class: 'btn btn-success pull-left', title: 'Export issues'
- elsif show_promotions?
= render 'shared/promotions/promote_csv_export'
......@@ -25,3 +25,5 @@
= render "projects/protected_tags/index"
= render @deploy_keys
= render 'shared/promotions/promote_repository_features'
......@@ -20,13 +20,4 @@
Once imported, repositories can be mirrored over SSH. Read more
= link_to 'here', help_page_path('/workflow/repository_mirroring.md', anchor: 'ssh-authentication')
.form-group
= f.label :mirror, class: 'label-light' do
= f.check_box :mirror, disabled: true
%strong
Mirror repository
.help-block
Automatically update this project's branches and tags from the upstream
repository every hour. The Git LFS objects will not be synced.
= f.hidden_field :mirror_user_id, value: current_user.id
= render 'shared/ee/import_form', f: f
<svg xmlns="http://www.w3.org/2000/svg" width="74" height="78" viewBox="0 0 74 78"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M.053 39A37.599 37.599 0 0 0 0 41c0 20.435 16.565 37 37 37s37-16.565 37-37c0-.671-.018-1.338-.053-2C72.907 58.505 56.764 74 37 74 17.236 74 1.092 58.505.053 39z"/><path fill="#EEE" fill-rule="nonzero" d="M37 70c18.225 0 33-14.775 33-33S55.225 4 37 4 4 18.775 4 37s14.775 33 33 33zm0 4C16.565 74 0 57.435 0 37S16.565 0 37 0s37 16.565 37 37-16.565 37-37 37z"/><g fill-rule="nonzero"><path fill="#E1DBF2" d="M37 49c-6.406 0-12.228-2.843-17.38-8.412a4 4 0 0 1-.267-5.113C24.53 28.559 30.434 25 37 25c6.566 0 12.47 3.56 17.647 10.475a4 4 0 0 1-.266 5.113C49.228 46.158 43.406 49 37 49zm0-4c5.225 0 10.012-2.337 14.445-7.128C46.966 31.89 42.173 29 37 29s-9.966 2.89-14.445 8.872C26.988 42.662 31.775 45 37 45z"/><path fill="#6B4FBB" d="M37 45a8 8 0 1 1 0-16 8 8 0 0 1 0 16zm0-4a4 4 0 1 0 0-8 4 4 0 0 0 0 8zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></g></g></svg>
\ No newline at end of file
<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"/><path fill="#E1DBF2" fill-rule="nonzero" d="M59.542 39.692l-12.93 15.484a2 2 0 0 1-3.36-.466L31.911 29.34l-9.073 10.532a4.007 4.007 0 0 0-2.9-2.762l10.998-12.767a2 2 0 0 1 3.341.49l11.318 25.318 9.511-11.39A3.989 3.989 0 0 0 58 40c.547 0 1.068-.11 1.542-.308z"/><circle cx="32" cy="26" r="4" fill="#6B4FBB"/><circle cx="45" cy="54" r="4" fill="#6B4FBB"/><path fill="#6B4FBB" fill-rule="nonzero" d="M19 47a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm39-1a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-4a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></g></svg>
\ No newline at end of file
<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.11986346,42 C2.04046544,42.9895269 2,43.9900378 2,45 C2,65.4345357 18.5654643,82 39,82 C59.4345357,82 76,65.4345357 76,45 C76,43.9900378 75.9595346,42.9895269 75.8801365,42 C74.3530766,61.0315425 58.4245736,76 39,76 C19.5754264,76 3.64692341,61.0315425 2.11986346,42 Z"/>
<path fill="#EEEEEE" fill-rule="nonzero" d="M39,78 C17.4608948,78 0,60.5391052 0,39 C0,17.4608948 17.4608948,0 39,0 C60.5391052,0 78,17.4608948 78,39 C78,60.5391052 60.5391052,78 39,78 Z M39,74 C58.3299662,74 74,58.3299662 74,39 C74,19.6700338 58.3299662,4 39,4 C19.6700338,4 4,19.6700338 4,39 C4,58.3299662 19.6700338,74 39,74 Z"/>
<path fill="#E1DBF2" fill-rule="nonzero" d="M39.670936,22.3541589 C38.0983891,22.716355 36.7037657,23.1741464 35.4859588,23.7326005 C34.4819249,24.193024 34.0412416,25.3802012 34.501665,26.3842351 C34.9620885,27.388269 36.1492657,27.8289523 37.1532996,27.3685289 C38.1094554,26.9300611 39.2483006,26.55623 40.5687283,26.252103 C41.6451158,26.0041845 42.3167227,24.9306224 42.0688042,23.8542348 C41.8208857,22.7778472 40.7473235,22.1062404 39.670936,22.3541589 Z M29.6758858,30.0170788 C29.4780579,30.7816803 29.3836179,31.5837814 29.3901446,32.4195311 C29.4045873,33.4859725 29.5976087,34.4314808 29.9757846,35.3060759 C30.4141738,36.3199243 31.5914451,36.7864262 32.6052935,36.3480369 C33.6191419,35.9096477 34.0856438,34.7323765 33.6472546,33.718528 C33.4838878,33.3407149 33.3972215,32.9161831 33.3898994,32.3767671 C33.3861622,31.8939624 33.4397509,31.438821 33.5483677,31.019019 C33.8250458,29.9496626 33.1824532,28.8584861 32.1130968,28.5818079 C31.0437405,28.3051298 29.952564,28.9477224 29.6758858,30.0170788 Z M36.6272376,40.4932998 C37.7133168,40.8851469 38.7297008,41.1918271 40.5894013,41.7131118 C41.6529776,42.0112383 42.7568557,41.3907187 43.0549822,40.3271425 C43.3531087,39.2635663 42.7325891,38.1596881 41.6690129,37.8615616 C39.899684,37.3656086 38.9517195,37.0795731 37.9847486,36.7306991 C36.9457351,36.3558328 35.7995591,36.8942304 35.4246928,37.9332439 C35.0498265,38.9722574 35.5882242,40.1184335 36.6272376,40.4932998 Z M46.7875829,43.8511503 C47.8524563,44.422726 48.5019906,45.0252727 48.8111047,45.6925961 C49.2753683,46.6948601 50.4642238,47.1309957 51.4664878,46.6667321 C52.4687518,46.2024686 52.9048873,45.0136131 52.4406238,44.0113491 C51.7210658,42.457949 50.4457724,41.2749108 48.6793167,40.3267577 C47.7060826,39.8043698 46.4936414,40.169853 45.9712535,41.1430871 C45.4488657,42.1163213 45.8143488,43.3287625 46.7875829,43.8511503 Z M48.230047,50.5614784 C47.7707449,51.2398807 47.0579604,51.7552716 45.9583537,52.1623784 C44.9224975,52.5458828 44.3936619,53.696502 44.7771664,54.7323582 C45.1606709,55.7682144 46.3112901,56.29705 47.3471463,55.9135455 C49.185476,55.2329417 50.5810001,54.2238844 51.5423128,52.8039983 C52.1615676,51.8893414 51.9220968,50.6458602 51.0074399,50.0266055 C50.092783,49.4073507 48.8493018,49.6468214 48.230047,50.5614784 Z M39.6938681,53.1746657 C38.5642199,53.2416635 37.289592,53.2865495 35.7764848,53.3153389 C34.6721152,53.3363513 33.7938807,54.2486538 33.8148931,55.3530234 C33.8359056,56.457393 34.748208,57.3356275 35.8525776,57.3146151 C37.4153285,57.2848811 38.7414211,57.2381828 39.9306863,57.1676492 C41.0333182,57.1022536 41.8741644,56.1553803 41.8087689,55.0527483 C41.7433734,53.9501164 40.7965,53.1092701 39.6938681,53.1746657 Z"/>
<path fill="#6B4FBB" fill-rule="nonzero" d="M26,62 C22.1340068,62 19,58.8659932 19,55 C19,51.1340068 22.1340068,48 26,48 C29.8659932,48 33,51.1340068 33,55 C33,58.8659932 29.8659932,62 26,62 Z M26,58 C27.6568542,58 29,56.6568542 29,55 C29,53.3431458 27.6568542,52 26,52 C24.3431458,52 23,53.3431458 23,55 C23,56.6568542 24.3431458,58 26,58 Z M52,30 C48.1340068,30 45,26.8659932 45,23 C45,19.1340068 48.1340068,16 52,16 C55.8659932,16 59,19.1340068 59,23 C59,26.8659932 55.8659932,30 52,30 Z M52,26 C53.6568542,26 55,24.6568542 55,23 C55,21.3431458 53.6568542,20 52,20 C50.3431458,20 49,21.3431458 49,23 C49,24.6568542 50.3431458,26 52,26 Z"/>
</g>
</svg>
<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"/><path fill="#E1DBF2" fill-rule="nonzero" d="M45 23c-1.312 0-2.593.23-3.795.673A10.982 10.982 0 0 0 33 20c-5.8 0-10.553 4.49-10.97 10.184A11.003 11.003 0 0 0 16 40c0 6.075 4.925 11 11 11h1.042a2 2 0 1 0 0-4H27a7 7 0 0 1-2.323-13.606 2 2 0 0 0 1.334-1.998 7 7 0 0 1 12.89-4.163 2 2 0 0 0 2.667.664A7.004 7.004 0 0 1 51.7 31.966a2 2 0 0 0 1.334 1.334A7 7 0 0 1 51 47h-.94a2 2 0 1 0 0 4H51c6.075 0 11-4.925 11-11 0-4.524-2.754-8.51-6.822-10.178A11.005 11.005 0 0 0 45 23z"/><path fill="#6B4FBB" d="M41 53V43a2 2 0 1 0-4 0v10h-2.796a1 1 0 0 0-.753 1.659l4.796 5.48a1 1 0 0 0 1.506 0l4.796-5.48A1 1 0 0 0 43.796 53H41z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82">
<g fill="none" fill-rule="evenodd" transform="translate(2 2)">
<path fill="#F9F9F9" d="M0.119863464,40 C0.0404654401,40.9895269 0,41.9900378 0,43 C0,63.4345357 16.5654643,80 37,80 C57.4345357,80 74,63.4345357 74,43 C74,41.9900378 73.9595346,40.9895269 73.8801365,40 C72.3530766,59.0315425 56.4245736,74 37,74 C17.5754264,74 1.64692341,59.0315425 0.119863464,40 Z"/>
<path fill="#EEEEEE" fill-rule="nonzero" d="M37,76 C15.4608948,76 -2,58.5391052 -2,37 C-2,15.4608948 15.4608948,-2 37,-2 C58.5391052,-2 76,15.4608948 76,37 C76,58.5391052 58.5391052,76 37,76 Z M37,72 C56.3299662,72 72,56.3299662 72,37 C72,17.6700338 56.3299662,2 37,2 C17.6700338,2 2,17.6700338 2,37 C2,56.3299662 17.6700338,72 37,72 Z"/>
<rect width="16" height="4" x="41" y="35" fill="#E1DBF1" rx="2"/>
<rect width="16" height="4" x="17" y="35" fill="#E1DBF1" rx="2"/>
<path fill="#6B4FBB" fill-rule="nonzero" d="M37,45 C32.581722,45 29,41.418278 29,37 C29,32.581722 32.581722,29 37,29 C41.418278,29 45,32.581722 45,37 C45,41.418278 41.418278,45 37,45 Z M37,41 C39.209139,41 41,39.209139 41,37 C41,34.790861 39.209139,33 37,33 C34.790861,33 33,34.790861 33,37 C33,39.209139 34.790861,41 37,41 Z"/>
</g>
</svg>
<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"/><rect width="7" height="1" x="59" y="38" fill="#E1DBF2" rx=".5"/><path fill="#6B4FBB" d="M60.5 42a3.5 3.5 0 0 0 0-7v7z"/><rect width="7" height="1" x="12" y="38" fill="#E1DBF2" transform="matrix(-1 0 0 1 31 0)" rx=".5"/><path fill="#6B4FBB" d="M17.5 42a3.5 3.5 0 0 1 0-7v7z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M39 58c10.493 0 19-8.507 19-19s-8.507-19-19-19-19 8.507-19 19 8.507 19 19 19zm0 4c-12.703 0-23-10.297-23-23s10.297-23 23-23 23 10.297 23 23-10.297 23-23 23z"/><path fill="#6B4FBB" d="M35 56a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M26.5 40c0 4.143 3.355 7.5 7.494 7.5h10.012A7.497 7.497 0 0 0 51.5 40c0-4.143-3.355-7.5-7.494-7.5H33.994A7.497 7.497 0 0 0 26.5 40zm-3 0c0-5.799 4.698-10.5 10.494-10.5h10.012C49.802 29.5 54.5 34.2 54.5 40c0 5.799-4.698 10.5-10.494 10.5H33.994C28.198 50.5 23.5 45.8 23.5 40z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M35.255 42.406a1 1 0 1 1 1.872-.703 2.001 2.001 0 0 0 3.76-.038 1 1 0 1 1 1.886.665 4 4 0 0 1-7.518.076zM31.5 40a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm15 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/><path fill="#6B4FBB" d="M38 22h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2zm0 3h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2z" style="mix-blend-mode:multiply"/></g></svg>
\ No newline at end of file
......@@ -97,6 +97,13 @@
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{title}}
#js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link
%gl-emoji
%span.js-data-value.prepend-left-10
{{name}}
- if type == :issues || type == :boards || type == :boards_modal
#js-dropdown-weight.filtered-search-input-dropdown-menu.dropdown-menu
......
......@@ -34,3 +34,4 @@
this milestone and the chart will appear here, always up-to-date.
= link_to "Add start and due date", edit_project_milestone_path(project, milestone), class: 'btn'
= render 'shared/promotions/promote_burndown_charts'
---
title: Fixes activation of project mirror when new project is created.
merge_request: 2756
author:
type: fixed
---
title: Remove unwanted refs after importing a project
merge_request: 13766
author:
type: other
---
title: 'API: Respect the "If-Unmodified-Since" header when delting a resource'
merge_request: 9621
author: Robert Schilling
type: added
......@@ -35,6 +35,7 @@ Rails.application.routes.draw do
get '/autocomplete/users' => 'autocomplete#users'
get '/autocomplete/users/:id' => 'autocomplete#user'
get '/autocomplete/projects' => 'autocomplete#projects'
get '/autocomplete/award_emojis' => 'autocomplete#award_emojis'
get '/autocomplete/project_groups' => 'autocomplete#project_groups'
# Search
......
......@@ -264,6 +264,7 @@ The following table shows the possible return codes for API requests.
| `404 Not Found` | A resource could not be accessed, e.g., an ID for a resource could not be found. |
| `405 Method Not Allowed` | The request is not supported. |
| `409 Conflict` | A conflicting resource already exists, e.g., creating a project with a name that already exists. |
| `412` | Indicates the request was denied. May happen if the `If-Unmodified-Since` header is provided when trying to delete a resource, which was modified in between. |
| `422 Unprocessable` | The entity could not be processed. |
| `500 Server Error` | While handling the request something went wrong server-side. |
......
module EE
module ProjectsController
def project_params_attributes
attrs = super + project_params_ee
attrs += repository_mirrors_params if project&.feature_available?(:repository_mirrors)
attrs
super + project_params_ee
end
private
......@@ -21,11 +18,6 @@ module EE
repository_size_limit
reset_approvals_on_push
service_desk_enabled
]
end
def repository_mirrors_params
%i[
mirror
mirror_trigger_builds
mirror_user_id
......
module EE
module Projects
module CreateService
def execute
raise NotImplementedError unless defined?(super)
mirror = params.delete(:mirror)
mirror_user_id = params.delete(:mirror_user_id)
mirror_trigger_builds = params.delete(:mirror_trigger_builds)
super do |project|
if mirror && project.feature_available?(:repository_mirrors)
project.mirror = mirror
project.mirror_user_id = mirror_user_id
project.mirror_trigger_builds = mirror_trigger_builds
end
end
end
end
end
end
module EE
module Projects
module UpdateService
def execute
raise NotImplementedError unless defined?(super)
unless project.feature_available?(:repository_mirrors)
params.delete(:mirror)
params.delete(:mirror_user_id)
params.delete(:mirror_trigger_builds)
end
super
end
end
end
end
- if EE::Gitlab::ServiceDesk.enabled?(project: @project) || (show_promotions? && show_callout?('promote_service_desk_dismissed'))
- expanded = Rails.env.test?
%section.settings.js-service-desk-setting-wrapper
.settings-header
%h4
Service Desk
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Customize your service desk settings.
= link_to "Learn more about service desk.", help_page_path('user/project/service_desk')
.settings-content.no-animate{ class: ('expanded' if expanded) }
- if EE::Gitlab::ServiceDesk.enabled?(project: @project)
.js-service-desk-setting-root{ data: { endpoint: project_service_desk_path(@project),
enabled: "#{@project.service_desk_enabled}",
incoming_email: (@project.service_desk_address if @project.service_desk_enabled) } }
- elsif show_promotions? && show_callout?('promote_service_desk_dismissed')
= render 'shared/promotions/promote_servicedesk'
- if @project.feature_available?(:audit_events)
- if @project.feature_available?(:audit_events) || show_promotions?
= nav_link(controller: :audit_events) do
= link_to project_audit_events_path(@project), title: "Audit Events" do
%span
......
- if License.feature_available?(:repository_mirrors)
.form-group
= f.label :mirror, class: 'label-light' do
= f.check_box :mirror, disabled: true
%strong
Mirror repository
.help-block
Automatically update this project's branches and tags from the upstream
repository every hour. The Git LFS objects will not be synced.
- if current_application_settings.should_check_namespace_plan?
.help-block
Mirroring will only be available if the feature is included in the plan of the selected group or user.
= f.hidden_field :mirror_user_id, value: current_user.id
- issuable = local_assigns.fetch(:issuable)
- return unless issuable.project.feature_available?(:merge_request_squash)
.form-group
.col-sm-10.col-sm-offset-2
.checkbox
= label_tag 'merge_request[squash]' do
= hidden_field_tag 'merge_request[squash]', '0', id: nil
= check_box_tag 'merge_request[squash]', '1', issuable.squash
Squash commits when merge request is accepted.
= link_to 'About this feature', help_page_path('user/project/merge_requests/squash_and_merge')
- if issuable.project.feature_available?(:merge_request_squash)
.form-group
.col-sm-10.col-sm-offset-2
.checkbox
= label_tag 'merge_request[squash]' do
= hidden_field_tag 'merge_request[squash]', '0', id: nil
= check_box_tag 'merge_request[squash]', '1', issuable.squash
Squash commits when merge request is accepted.
= link_to 'About this feature', help_page_path('user/project/merge_requests/squash_and_merge')
- elsif show_promotions? && show_callout?('promote_squash_commits_dismissed')
.form-group
.col-sm-10.col-sm-offset-2
= render 'shared/promotions/promote_squash_commits'
.center.promotion-backdrop.append-top-72
.svg-container
= custom_icon('icon_audit_events_purple')
.user-callout-copy
%h4
- if current_application_settings.should_check_namespace_plan?
Upgrade your plan to activate Audit Events.
- else
Track your project with Audit Events.
%p
Audit Events is a way to keep track of important events that happened in GitLab.
= link_to 'Read more', help_page_path('administration/audit_events.html'), target: '_blank'
= render 'shared/promotions/promotion_link_project'
- if show_promotions? && show_callout?('promote_burndown_charts_dismissed')
.user-callout.promotion-callout#promote_burndown_charts{ data: { uid: 'promote_burndown_charts_dismissed' } }
.bordered-box.content-block
%button.btn.btn-default.close.js-close-callout{ type: 'button', 'aria-label' => 'Dismiss burndown charts promotion' }
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
.svg-container
= custom_icon('icon_burndown_charts')
.user-callout-copy
%h4
- if current_application_settings.should_check_namespace_plan?
Upgrade your plan to improve milestones with Burndown Charts.
- else
Improve milestones with Burndown Charts.
%p
Burndown Charts are visual representations of the progress of completing a milestone. At a glance, you see the current state for the completion a given milestone. Without them, you would have to organize the data from the milestone and plot it yourself to have the same sense of progress.
= link_to 'Read more', help_page_path('user/project/milestones/burndown_charts.html'), target: '_blank'
= render 'shared/promotions/promotion_link_project'
.center.promotion-backdrop
.svg-container
= custom_icon('icon_contribution_analytics')
.user-callout-copy
%h4
- if current_application_settings.should_check_namespace_plan?
Upgrade your plan to activate Contribution Analytics.
- else
Track activity with Contribution Analytics.
%p
With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members.
= link_to 'Read more', help_page_path('user/analytics/contribution_analytics.html'), target: '_blank'
= render 'shared/promotions/promotion_link_project'
.issues-export-modal.modal.promotion-modal
.modal-dialog
.modal-content
.modal-header
%a.close{ href: '#', 'data-dismiss' => 'modal' } ×
.modal-body.center
%div
.svg-container
= custom_icon('icon_export_issues')
.user-callout-copy
%h4
- if current_application_settings.should_check_namespace_plan?
Upgrade your plan to export issues.
- else
Export issues with GitLab Enterprise Edition.
%p
Export Issues to CSV enables you and your team to export all the data collected from issues into a comma-separated values (CSV) file, which stores tabular data in plain text.
= link_to 'Read more', help_page_path('user/project/issues/csv_export.html'), target: '_blank'
= render 'shared/promotions/promotion_link_project'
- if show_promotions? && show_callout?('promote_mr_features_dismissed') && (!@project.feature_available?(:merge_request_approvers) || !@project.feature_available?(:fast_forward_merge))
.user-callout.promotion-callout.append-bottom-20.js-mr-approval-callout#promote_mr_features{ data: { uid: 'promote_mr_features_dismissed' } }
.bordered-box.content-block
%button.btn.btn-default.close.js-close-callout{ type: 'button', 'aria-label' => 'Dismiss Merge Request promotion' }
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
.user-callout-copy
%h4
- if current_application_settings.should_check_namespace_plan?
Upgrade your plan to improve Merge Requests.
- else
Improve Merge Requests and customer support with GitLab Enterprise Edition.
%ul
- unless @project.feature_available?(:merge_request_approvers)
%li
= link_to 'Merge Request Approvals', help_page_path('user/project/merge_requests/merge_request_approvals.html'), target: '_blank'
%p
Merge request approvals allow you to set the number of necessary approvals and predefine a list of approvers that will need to approve every merge request in a project.
- unless @project.feature_available?(:fast_forward_merge)
%li
= link_to 'Fast-forward Merge', help_page_path('user/project/merge_requests/fast_forward_merge.html'), target: '_blank'
%p
If you prefer a linear Git history and a way to accept merge requests without creating merge commits, you can configure this on a per-project basis.
= render 'shared/promotions/promotion_link_project'
- if show_promotions? && show_callout?('promote_repository_features_dismissed') && (!@project.feature_available?(:push_rules) || !@project.feature_available?(:repository_mirrors) || !@project.feature_available?(:protected_refs_for_users))
.user-callout.promotion-callout{ id: 'promote_repository_features', data: { uid: 'promote_repository_features_dismissed' } }
.bordered-box.content-block
%button.btn.btn-default.close.js-close-callout{ type: 'button', 'aria-label' => 'Dismiss repository features promotion' }
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
.svg-container
= custom_icon('icon_push_rules')
.user-callout-copy
%h4
- if current_application_settings.should_check_namespace_plan?
Upgrade your plan to improve repositories.
- else
Improve repositories with GitLab Enterprise Edition.
%ul
- unless @project.feature_available?(:push_rules)
%li
= link_to 'Push Rules', help_page_path('push_rules/push_rules.md#push-rules'), target: '_blank'
%p
Push Rules are defined per project so you can have different rules applied to different projects depends on your needs.
- unless @project.feature_available?(:repository_mirrors)
%li
= link_to 'Repository Mirroring', help_page_path('workflow/repository_mirroring.html'), target: '_blank'
%p
Repository Mirroring is a way to mirror repositories from external sources. It can be used to mirror all branches, tags, and commits that you have in your repository.
- unless @project.feature_available?(:protected_refs_for_users)
%li
= link_to 'Better Protected Branches', help_page_path('user/project/protected_branches.html'), target: '_blank'
%p
You can restrict access to protected branches by choosing a role (Masters, Developers) as well as certain users.
= render 'shared/promotions/promotion_link_project'
.user-callout.promotion-callout.js-service-desk-callout#promote_service_desk{ data: { uid: 'promote_service_desk_dismissed' } }
.bordered-box.content-block
%button.btn.btn-default.close.js-close-callout{ type: 'button', 'aria-label' => 'Dismiss Service Desk promotion' }
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
.svg-container
= custom_icon('icon_service_desk')
.user-callout-copy
%h4
- if current_application_settings.should_check_namespace_plan?
Upgrade your plan to activate Service Desk.
- else
Improve customer support with GitLab Service Desk.
%p
GitLab Service Desk is a simple way to allow people to create issues in your GitLab instance without needing their own user account. It provides a unique email address for end users to create issues in a project, and replies can be sent either through the GitLab interface or by email. End users will only see the thread through email.
= link_to 'Read more', help_page_path('user/project/service_desk.html'), target: '_blank'
= render 'shared/promotions/promotion_link_project'
.user-callout.promotion-callout.prepend-top-10#promote_squash_commits{ data: { uid: 'promote_squash_commits_dismissed' } }
.bordered-box.content-block
%button.btn.btn-default.close.js-close-callout{ type: 'button', 'aria-label' => 'Dismiss merge request promotion' }
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
.user-callout-copy
%h4
- if current_application_settings.should_check_namespace_plan?
Upgrade your plan to improve Merge Requests with Squash Commit.
- else
Improve Merge Requests with Squash Commit and GitLab Enterprise Edition.
%p
Squashing lets you tidy up the commit history of a branch when accepting a merge request. It applies all of the changes in the merge request as a single commit, and then merges that commit using the merge method set for the project.
= link_to 'Read more', help_page_path('user/project/merge_requests/squash_and_merge.html'), target: '_blank'
= render 'shared/promotions/promotion_link_project'
- if current_application_settings.should_check_namespace_plan?
- if can?(current_user, :admin_namespace, @project.namespace)
= link_to 'Upgrade your plan', upgrade_plan_url, class: 'btn btn-primary'
- elsif @project.group
%p Contact an owner of group #{ @project.group.name } to upgrade the plan.
- else
- owner = @project.namespace.owner
%p Contact owner #{ link_to(owner.name, user_path(owner)) } to upgrade the plan.
- elsif current_user&.admin?
- if License.current&.expired?
= link_to 'Buy GitLab Enterprise Edition', Gitlab::SUBSCRIPTIONS_PLANS_URL, class: 'btn btn-primary'
- else
= link_to 'Start GitLab Enterprise Edition trial', new_trial_url, class: 'btn btn-primary'
- else
%p
Contact your Administrator to upgrade your license.
......@@ -67,10 +67,12 @@ module API
end
delete ":id/access_requests/:user_id" do
source = find_source(source_type, params[:id])
member = source.requesters.find_by!(user_id: params[:user_id])
status 204
::Members::DestroyService.new(source, current_user, params)
.execute(:requesters)
destroy_conditionally!(member) do
::Members::DestroyService.new(source, current_user, params)
.execute(:requesters)
end
end
end
end
......
......@@ -88,8 +88,7 @@ module API
unauthorized! unless award.user == current_user || current_user.admin?
status 204
award.destroy
destroy_conditionally!(award)
end
end
end
......
......@@ -116,13 +116,13 @@ module API
end
delete "/lists/:list_id" do
authorize!(:admin_list, user_project)
list = board_lists.find(params[:list_id])
service = ::Boards::Lists::DestroyService.new(user_project, current_user)
unless service.execute(list)
render_api_error!({ error: 'List could not be deleted!' }, 400)
destroy_conditionally!(list) do |list|
service = ::Boards::Lists::DestroyService.new(user_project, current_user)
unless service.execute(list)
render_api_error!({ error: 'List could not be deleted!' }, 400)
end
end
end
end
......
......@@ -125,11 +125,18 @@ module API
delete ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
authorize_push_project
result = DeleteBranchService.new(user_project, current_user)
.execute(params[:branch])
branch = user_project.repository.find_branch(params[:branch])
not_found!('Branch') unless branch
commit = user_project.repository.commit(branch.dereferenced_target)
destroy_conditionally!(commit, last_update_field: :authored_date) do
result = DeleteBranchService.new(user_project, current_user)
.execute(params[:branch])
if result[:status] != :success
render_api_error!(result[:message], result[:return_code])
if result[:status] != :success
render_api_error!(result[:message], result[:return_code])
end
end
end
......
......@@ -91,8 +91,7 @@ module API
delete ':id' do
message = find_message
status 204
message.destroy
destroy_conditionally!(message)
end
end
end
......
......@@ -125,8 +125,7 @@ module API
key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
not_found!('Deploy Key') unless key
status 204
key.destroy
destroy_conditionally!(key)
end
end
end
......
......@@ -79,8 +79,7 @@ module API
environment = user_project.environments.find(params[:environment_id])
status 204
environment.destroy
destroy_conditionally!(environment)
end
desc 'Stops an existing environment' do
......
......@@ -88,8 +88,7 @@ module API
variable = user_group.variables.find_by(key: params[:key])
not_found!('GroupVariable') unless variable
status 204
variable.destroy
destroy_conditionally!(variable)
end
end
end
......
......@@ -150,8 +150,9 @@ module API
group = find_group!(params[:id])
authorize! :admin_group, group
status 204
::Groups::DestroyService.new(group, current_user).execute
destroy_conditionally!(group) do |group|
::Groups::DestroyService.new(group, current_user).execute
end
end
desc 'Get a list of projects in this group.' do
......
......@@ -13,6 +13,25 @@ module API
declared(params, options).to_h.symbolize_keys
end
def check_unmodified_since!(last_modified)
if_unmodified_since = Time.parse(headers['If-Unmodified-Since']) rescue nil
if if_unmodified_since && last_modified && last_modified > if_unmodified_since
render_api_error!('412 Precondition Failed', 412)
end
end
def destroy_conditionally!(resource, last_update_field: :updated_at)
check_unmodified_since!(resource.public_send(last_update_field))
status 204
if block_given?
yield resource
else
resource.destroy
end
end
def current_user
return @current_user if defined?(@current_user)
......
......@@ -236,8 +236,8 @@ module API
not_found!('Issue') unless issue
authorize!(:destroy_issue, issue)
status 204
issue.destroy
destroy_conditionally!(issue)
end
desc 'List merge requests closing issue' do
......
......@@ -56,8 +56,7 @@ module API
label = user_project.labels.find_by(title: params[:name])
not_found!('Label') unless label
status 204
label.destroy
destroy_conditionally!(label)
end
desc 'Update an existing label. At least one optional parameter is required.' do
......
......@@ -99,11 +99,11 @@ module API
end
delete ":id/members/:user_id" do
source = find_source(source_type, params[:id])
# Ensure the member exists
source.members.find_by!(user_id: params[:user_id])
member = source.members.find_by!(user_id: params[:user_id])
status 204
::Members::DestroyService.new(source, current_user, declared_params).execute
destroy_conditionally!(member) do
::Members::DestroyService.new(source, current_user, declared_params).execute
end
end
end
end
......
......@@ -193,8 +193,8 @@ module API
merge_request = find_project_merge_request(params[:merge_request_iid])
authorize!(:destroy_merge_request, merge_request)
status 204
merge_request.destroy
destroy_conditionally!(merge_request)
end
params do
......
......@@ -129,10 +129,12 @@ module API
end
delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
note = user_project.notes.find(params[:note_id])
authorize! :admin_note, note
status 204
::Notes::DestroyService.new(user_project, current_user).execute(note)
destroy_conditionally!(note) do |note|
::Notes::DestroyService.new(user_project, current_user).execute(note)
end
end
end
end
......
......@@ -117,8 +117,7 @@ module API
not_found!('PipelineSchedule') unless pipeline_schedule
authorize! :admin_pipeline_schedule, pipeline_schedule
status :accepted
present pipeline_schedule.destroy, with: Entities::PipelineScheduleDetails
destroy_conditionally!(pipeline_schedule)
end
end
......
......@@ -96,8 +96,7 @@ module API
delete ":id/hooks/:hook_id" do
hook = user_project.hooks.find(params.delete(:hook_id))
status 204
hook.destroy
destroy_conditionally!(hook)
end
end
end
......
......@@ -116,8 +116,8 @@ module API
not_found!('Snippet') unless snippet
authorize! :admin_project_snippet, snippet
status 204
snippet.destroy
destroy_conditionally!(snippet)
end
desc 'Get a raw project snippet'
......
require_dependency 'declarative_policy'
module API
# Projects API
class Projects < Grape::API
include PaginationParams
......@@ -346,7 +345,10 @@ module API
desc 'Remove a project'
delete ":id" do
authorize! :remove_project, user_project
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
destroy_conditionally!(user_project) do
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
end
accepted!
end
......@@ -375,8 +377,7 @@ module API
authorize! :remove_fork_project, user_project
if user_project.forked?
status 204
user_project.forked_project_link.destroy
destroy_conditionally!(user_project.forked_project_link)
else
not_modified!
end
......@@ -420,8 +421,7 @@ module API
link = user_project.project_group_links.find_by(group_id: params[:group_id])
not_found!('Group Link') unless link
status 204
link.destroy
destroy_conditionally!(link)
end
desc 'Upload a file'
......
......@@ -76,9 +76,7 @@ module API
delete ':id/protected_branches/:name', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
protected_branch = user_project.protected_branches.find_by!(name: params[:name])
protected_branch.destroy
status 204
destroy_conditionally!(protected_branch)
end
end
end
......
......@@ -45,8 +45,10 @@ module API
end
delete '/' do
authenticate_runner!
status 204
Ci::Runner.find_by_token(params[:token]).destroy
runner = Ci::Runner.find_by_token(params[:token])
destroy_conditionally!(runner)
end
desc 'Validates authentication credentials' do
......
......@@ -77,10 +77,10 @@ module API
end
delete ':id' do
runner = get_runner(params[:id])
authenticate_delete_runner!(runner)
status 204
runner.destroy!
destroy_conditionally!(runner)
end
end
......@@ -135,8 +135,7 @@ module API
runner = runner_project.runner
forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
status 204
runner_project.destroy
destroy_conditionally!(runner_project)
end
end
......
......@@ -704,12 +704,14 @@ module API
delete ":id/services/:service_slug" do
service = user_project.find_or_initialize_service(params[:service_slug].underscore)
attrs = service_attributes(service).inject({}) do |hash, key|
hash.merge!(key => nil)
end
destroy_conditionally!(service) do
attrs = service_attributes(service).inject({}) do |hash, key|
hash.merge!(key => nil)
end
unless service.update_attributes(attrs.merge(active: false))
render_api_error!('400 Bad Request', 400)
unless service.update_attributes(attrs.merge(active: false))
render_api_error!('400 Bad Request', 400)
end
end
end
......
......@@ -123,8 +123,7 @@ module API
authorize! :destroy_personal_snippet, snippet
status 204
snippet.destroy
destroy_conditionally!(snippet)
end
desc 'Get a raw snippet' do
......
......@@ -66,8 +66,7 @@ module API
hook = SystemHook.find_by(id: params[:id])
not_found!('System hook') unless hook
status 204
hook.destroy
destroy_conditionally!(hook)
end
end
end
......
......@@ -65,11 +65,18 @@ module API
delete ':id/repository/tags/:tag_name', requirements: TAG_ENDPOINT_REQUIREMENTS do
authorize_push_project
result = ::Tags::DestroyService.new(user_project, current_user)
.execute(params[:tag_name])
tag = user_project.repository.find_tag(params[:tag_name])
not_found!('Tag') unless tag
commit = user_project.repository.commit(tag.dereferenced_target)
destroy_conditionally!(commit, last_update_field: :authored_date) do
result = ::Tags::DestroyService.new(user_project, current_user)
.execute(params[:tag_name])
if result[:status] != :success
render_api_error!(result[:message], result[:return_code])
if result[:status] != :success
render_api_error!(result[:message], result[:return_code])
end
end
end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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