Commit 29fe5d4d authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-to-ee-2017-07-28' into 'master'

CE upstream - Friday

Closes gitaly#408, gitaly#411, and #1827

See merge request !2554
parents 51fe933c 2ede81d2
......@@ -102,6 +102,7 @@ stages:
- export KNAPSACK_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- scripts/gitaly-test-spawn
- knapsack rspec "--color --format documentation"
artifacts:
expire_in: 31d
......@@ -228,6 +229,7 @@ setup-test-env:
- bundle exec rake gettext:po_to_json
- bundle exec rake gitlab:assets:compile
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
- scripts/gitaly-test-build # Do not use 'bundle exec' here
artifacts:
expire_in: 7d
paths:
......@@ -473,6 +475,7 @@ karma:
BABEL_ENV: "coverage"
CHROME_LOG_FILE: "chrome_debug.log"
script:
- scripts/gitaly-test-spawn
- bundle exec rake gettext:po_to_json
- bundle exec rake karma
coverage: '/^Statements *: (\d+\.\d+%)/'
......
7.5
\ No newline at end of file
......@@ -15,7 +15,7 @@ gem 'default_value_for', '~> 3.0.0'
gem 'mysql2', '~> 0.4.5', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.25.1.1'
gem 'rugged', '~> 0.26.0'
gem 'grape-route-helpers', '~> 2.0.0'
gem 'faraday', '~> 0.12'
......@@ -61,6 +61,9 @@ gem 'validates_hostname', '~> 1.0.6'
# Browser detection
gem 'browser', '~> 2.2'
# GPG
gem 'gpgme'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master
......@@ -298,7 +301,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~>0.7.0.beta9'
gem 'prometheus-client-mmap', '~>0.7.0.beta11'
gem 'raindrops', '~> 0.18'
end
......@@ -403,7 +406,7 @@ gem 'sys-filesystem', '~> 1.1.6'
gem 'net-ntp'
# Gitaly GRPC client
gem 'gitaly', '~> 0.19.0'
gem 'gitaly', '~> 0.21.0'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -293,7 +293,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly (0.19.0)
gitaly (0.21.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -357,6 +357,8 @@ GEM
multi_json (~> 1.11)
os (~> 0.9)
signet (~> 0.7)
gpgme (2.0.13)
mini_portile2 (~> 2.1)
grape (0.19.2)
activesupport
builder
......@@ -624,7 +626,7 @@ GEM
premailer-rails (1.9.7)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
prometheus-client-mmap (0.7.0.beta10)
prometheus-client-mmap (0.7.0.beta11)
mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4)
coderay (~> 1.1.0)
......@@ -778,7 +780,7 @@ GEM
rubyzip (1.2.1)
rufus-scheduler (3.4.0)
et-orbi (~> 1.0)
rugged (0.25.1.1)
rugged (0.26.0)
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
......@@ -1008,7 +1010,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.19.0)
gitaly (~> 0.21.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
......@@ -1018,6 +1020,7 @@ DEPENDENCIES
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.8.6)
gpgme
grape (~> 0.19.2)
grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.0.0)
......@@ -1084,7 +1087,7 @@ DEPENDENCIES
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta9)
prometheus-client-mmap (~> 0.7.0.beta11)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
......@@ -1118,7 +1121,7 @@ DEPENDENCIES
ruby-prof (~> 0.16.2)
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.25.1.1)
rugged (~> 0.26.0)
sanitize (~> 2.0)
sass-rails (~> 5.0.6)
scss_lint (~> 0.54.0)
......
......@@ -8,6 +8,7 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
// custom jQuery functions
$.fn.extend({
......
......@@ -23,6 +23,8 @@
/* global NamespaceSelects */
/* global Project */
/* global ProjectAvatar */
/* global MergeRequest */
/* global Compare */
/* global CompareAutocomplete */
/* global PathLocks */
/* global ProjectFindFile */
......@@ -72,6 +74,7 @@ import initSettingsPanels from './settings_panels';
import initExperimentalFlags from './experimental_flags';
import OAuthRememberMe from './oauth_remember_me';
import PerformanceBar from './performance_bar';
import GpgBadges from './gpg_badges';
import initNotes from './init_notes';
import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar';
......@@ -255,6 +258,19 @@ import AuditLogs from './audit_logs';
new gl.IssuableTemplateSelectors();
break;
case 'projects:merge_requests:creations:new':
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
if (mrNewCompareNode) {
new Compare({
targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl,
sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl,
targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl,
});
} else {
const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
new MergeRequest({
action: mrNewSubmitNode.dataset.mrSubmitAction,
});
}
case 'projects:merge_requests:creations:diffs':
case 'projects:merge_requests:edit':
new gl.Diff();
......@@ -294,6 +310,10 @@ import AuditLogs from './audit_logs';
new gl.Diff();
shortcut_handler = new ShortcutsIssuable(true);
new ZenMode();
const mrShowNode = document.querySelector('.merge-request');
window.mergeRequest = new MergeRequest({
action: mrShowNode.dataset.mrAction,
});
initIssuableSidebar();
initNotes();
break;
......@@ -325,6 +345,7 @@ import AuditLogs from './audit_logs';
CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit);
new gl.Activities();
shortcut_handler = new ShortcutsNavigation();
GpgBadges.fetch();
break;
case 'projects:edit':
new UsersSelect();
......@@ -616,6 +637,13 @@ import AuditLogs from './audit_logs';
case 'repository':
shortcut_handler = new ShortcutsNavigation();
}
break;
case 'users':
const action = path[1];
import(/* webpackChunkName: 'user_profile' */ './users')
.then(user => user.default(action))
.catch(() => {});
break;
}
// If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {
......
export default class GpgBadges {
static fetch() {
const form = $('.commits-search-form');
$.get({
url: form.data('signatures-path'),
data: form.serialize(),
}).done((response) => {
const badges = $('.js-loading-gpg-badge');
response.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
});
});
}
}
......@@ -3,10 +3,10 @@ document.addEventListener('DOMContentLoaded', () => {
modal: true,
show: false,
});
$('.how_to_merge_link').bind('click', () => {
$('.how_to_merge_link').on('click', () => {
modal.show();
});
$('.modal-header .close').bind('click', () => {
$('.modal-header .close').on('click', () => {
modal.hide();
});
});
......@@ -102,7 +102,7 @@ export default class IntegrationSettingsForm {
})
.done((res) => {
if (res.error) {
new Flash(res.message, null, null, {
new Flash(`${res.message} ${res.service_response}`, null, null, {
title: 'Save anyway',
clickHandler: (e) => {
e.preventDefault();
......
......@@ -166,6 +166,8 @@ document.addEventListener('beforeunload', function () {
$(document).off('scroll');
// Close any open tooltips
$('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
// Close any open popover
$('[data-toggle="popover"]').popover('destroy');
});
window.addEventListener('hashchange', gl.utils.handleLocationHash);
......@@ -254,6 +256,11 @@ $(function () {
return $(el).data('placement') || 'bottom';
}
});
// Initialize popovers
$body.popover({
selector: '[data-toggle="popover"]',
trigger: 'focus'
});
$('.trigger-submit').on('change', function () {
return $(this).parents('form').submit();
// Form submitter
......
import ActivityCalendar from './activity_calendar';
import User from './user';
import Cookies from 'js-cookie';
import UserTabs from './user_tabs';
// use legacy exports until embedded javascript is refactored
window.Calendar = ActivityCalendar;
window.gl = window.gl || {};
window.gl.User = User;
export default function initUserProfile(action) {
// place profile avatars to top
$('.profile-groups-avatars').tooltip({
placement: 'top',
});
// eslint-disable-next-line no-new
new UserTabs({ parentEl: '.user-profile', action });
// hide project limit message
$('.hide-project-limit-message').on('click', (e) => {
e.preventDefault();
Cookies.set('hide_project_limit_message', 'false');
$(this).parents('.project-limit-message').remove();
});
}
/* eslint-disable class-methods-use-this */
import Cookies from 'js-cookie';
import UserTabs from './user_tabs';
export default class User {
constructor({ action }) {
this.action = action;
this.placeProfileAvatarsToTop();
this.initTabs();
this.hideProjectLimitMessage();
}
placeProfileAvatarsToTop() {
$('.profile-groups-avatars').tooltip({
placement: 'top',
});
}
initTabs() {
return new UserTabs({
parentEl: '.user-profile',
action: this.action,
});
}
hideProjectLimitMessage() {
$('.hide-project-limit-message').on('click', (e) => {
e.preventDefault();
Cookies.set('hide_project_limit_message', 'false');
$(this).parents('.project-limit-message').remove();
});
}
}
/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, no-param-reassign, class-methods-use-this */
/*
UserTabs
Handles persisting and restoring the current tab selection and lazily-loading
content on the Users#show page.
### Example Markup
<ul class="nav-links">
<li class="activity-tab active">
<a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
Activity
</a>
</li>
<li class="groups-tab">
<a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
Groups
</a>
</li>
<li class="contributed-tab">
<a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
Contributed projects
</a>
</li>
<li class="projects-tab">
<a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
Personal projects
</a>
</li>
<li class="snippets-tab">
<a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane" id="activity">
Activity Content
</div>
<div class="tab-pane" id="groups">
Groups Content
</div>
<div class="tab-pane" id="contributed">
Contributed projects content
</div>
<div class="tab-pane" id="projects">
Projects content
</div>
<div class="tab-pane" id="snippets">
Snippets content
</div>
import ActivityCalendar from './activity_calendar';
/**
* UserTabs
*
* Handles persisting and restoring the current tab selection and lazily-loading
* content on the Users#show page.
*
* ### Example Markup
*
* <ul class="nav-links">
* <li class="activity-tab active">
* <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
* Activity
* </a>
* </li>
* <li class="groups-tab">
* <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
* Groups
* </a>
* </li>
* <li class="contributed-tab">
* ...
* </li>
* <li class="projects-tab">
* ...
* </li>
* <li class="snippets-tab">
* ...
* </li>
* </ul>
*
* <div class="tab-content">
* <div class="tab-pane" id="activity">
* Activity Content
* </div>
* <div class="tab-pane" id="groups">
* Groups Content
* </div>
* <div class="tab-pane" id="contributed">
* Contributed projects content
* </div>
* <div class="tab-pane" id="projects">
* Projects content
* </div>
* <div class="tab-pane" id="snippets">
* Snippets content
* </div>
* </div>
*
* <div class="loading-status">
* <div class="loading">
* Loading Animation
* </div>
* </div>
*/
const CALENDAR_TEMPLATE = `
<div class="clearfix calendar">
<div class="js-contrib-calendar"></div>
<div class="calendar-hint">
Summary of issues, merge requests, push events, and comments
</div>
</div>
<div class="loading-status">
<div class="loading">
Loading Animation
</div>
</div>
*/
`;
export default class UserTabs {
constructor ({ defaultAction, action, parentEl }) {
constructor({ defaultAction, action, parentEl }) {
this.loaded = {};
this.defaultAction = defaultAction || 'activity';
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
this._location = window.location;
this.windowLocation = window.location;
this.$parentEl.find('.nav-links a')
.each((i, navLink) => {
this.loaded[$(navLink).attr('data-action')] = false;
......@@ -82,12 +86,10 @@ export default class UserTabs {
}
bindEvents() {
this.changeProjectsPageWrapper = this.changeProjectsPage.bind(this);
this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
this.$parentEl.on('click', '.gl-pagination a', this.changeProjectsPageWrapper);
this.$parentEl
.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event))
.on('click', '.gl-pagination a', event => this.changeProjectsPage(event));
}
changeProjectsPage(e) {
......@@ -122,7 +124,7 @@ export default class UserTabs {
const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
if (loadableActions.indexOf(action) > -1) {
return this.loadTab(action, endpoint);
this.loadTab(action, endpoint);
}
}
......@@ -131,25 +133,38 @@ export default class UserTabs {
beforeSend: () => this.toggleLoading(true),
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
url: endpoint,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
}
gl.utils.localTimeAgo($('.js-timeago', tabSelector));
},
});
}
loadActivities() {
if (this.loaded['activity']) {
if (this.loaded.activity) {
return;
}
const $calendarWrap = this.$parentEl.find('.user-calendar');
$calendarWrap.load($calendarWrap.data('href'));
const calendarPath = $calendarWrap.data('calendarPath');
const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath');
$.ajax({
dataType: 'json',
url: calendarPath,
success: (activityData) => {
$calendarWrap.html(CALENDAR_TEMPLATE);
// eslint-disable-next-line no-new
new ActivityCalendar('.js-contrib-calendar', activityData, calendarActivitiesPath);
},
});
// eslint-disable-next-line no-new
new gl.Activities();
return this.loaded['activity'] = true;
this.loaded.activity = true;
}
toggleLoading(status) {
......@@ -158,13 +173,13 @@ export default class UserTabs {
}
setCurrentAction(source) {
let new_state = source;
new_state = new_state.replace(/\/+$/, '');
new_state += this._location.search + this._location.hash;
let newState = source;
newState = newState.replace(/\/+$/, '');
newState += this.windowLocation.search + this.windowLocation.hash;
history.replaceState({
url: new_state
}, document.title, new_state);
return new_state;
url: newState,
}, document.title, newState);
return newState;
}
getCurrentAction() {
......
......@@ -148,7 +148,6 @@
padding: 5px 8px;
color: $gl-text-color;
line-height: initial;
text-overflow: ellipsis;
border-radius: 2px;
white-space: nowrap;
overflow: hidden;
......@@ -203,11 +202,6 @@
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
@media (max-width: $screen-sm-min) {
width: 100%;
min-width: 180px;
}
&.dropdown-open-left {
right: 0;
left: auto;
......@@ -289,6 +283,11 @@
padding: 5px 8px;
color: $gl-text-color-secondary;
}
.badge + span:not(.badge) {
// Expects up to 3 digits on the badge
margin-right: 40px;
}
}
.droplab-dropdown {
......@@ -373,7 +372,6 @@
.dropdown-menu,
.dropdown-menu-nav {
max-width: 280px;
width: auto;
}
}
......
......@@ -393,7 +393,8 @@
@media (max-width: $screen-xs) {
.filter-dropdown-container {
.dropdown-toggle,
.dropdown {
.dropdown,
.dropdown-menu {
width: 100%;
}
......
......@@ -118,3 +118,29 @@
@content;
}
}
/*
* Mixin for status badges, as used for pipelines and commit signatures
*/
@mixin status-color($color-light, $color-main, $color-dark) {
color: $color-main;
border-color: $color-main;
&:not(span):hover {
background-color: $color-light;
color: $color-dark;
border-color: $color-dark;
svg {
fill: $color-dark;
}
}
svg {
fill: $color-main;
}
}
@mixin green-status-color {
@include status-color($green-50, $green-500, $green-700);
}
......@@ -287,3 +287,63 @@
color: $gl-text-color;
}
}
.gpg-status-box {
&.valid {
@include green-status-color;
}
&.invalid {
@include status-color($gray-dark, $gray, $common-gray-dark);
border-color: $common-gray-light;
}
}
.gpg-popover-status {
display: flex;
align-items: center;
font-weight: normal;
line-height: 1.5;
}
.gpg-popover-icon {
// same margin as .s32.avatar
margin-right: $btn-side-margin;
&.valid {
svg {
border: 1px solid $brand-success;
fill: $brand-success;
}
}
&.invalid {
svg {
border: 1px solid $common-gray-light;
fill: $common-gray-light;
}
}
svg {
width: 32px;
height: 32px;
border-radius: 50%;
vertical-align: middle;
}
}
.gpg-popover-user-link {
display: flex;
align-items: center;
margin-bottom: $gl-padding / 2;
text-decoration: none;
color: $gl-text-color;
}
.commit .gpg-popover-help-link {
display: block;
color: $link-color;
}
......@@ -211,6 +211,10 @@
-webkit-overflow-scrolling: touch;
}
&.affix-top .issuable-sidebar {
height: 100%;
}
&.right-sidebar-expanded {
width: $gutter_width;
......
......@@ -391,3 +391,26 @@ table.u2f-registrations {
margin-bottom: 0;
}
}
.gpg-email-badge {
display: inline;
margin-right: $gl-padding / 2;
.gpg-email-badge-email {
display: inline;
margin-right: $gl-padding / 4;
}
.label-verification-status {
border-width: 1px;
border-style: solid;
&.verified {
@include green-status-color;
}
&.unverified {
@include status-color($gray-dark, $gray, $common-gray-dark);
}
}
}
......@@ -727,6 +727,7 @@ a.allowed-to-push {
background-color: transparent;
border: 0;
text-align: left;
text-overflow: ellipsis;
}
.protected-branches-list,
......
@mixin status-color($color-light, $color-main, $color-dark) {
color: $color-main;
border-color: $color-main;
&:not(span):hover {
background-color: $color-light;
color: $color-dark;
border-color: $color-dark;
svg {
fill: $color-dark;
}
}
svg {
fill: $color-main;
}
}
.ci-status {
padding: 2px 7px 4px;
border: 1px solid $gray-darker;
......@@ -41,7 +22,7 @@
}
&.ci-success {
@include status-color($green-50, $green-500, $green-700);
@include green-status-color;
}
&.ci-canceled,
......
......@@ -78,12 +78,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
params.require(:application_setting).permit(
permitted_application_setting_attributes
visible_application_setting_attributes
)
end
def permitted_application_setting_attributes
def visible_application_setting_attributes
ApplicationSettingsHelper.visible_attributes + [
:domain_blacklist_file,
disabled_oauth_sign_in_sources: [],
import_sources: [],
repository_storages: [],
......
......@@ -70,6 +70,16 @@ class ApplicationController < ActionController::Base
protected
def append_info_to_payload(payload)
super
payload[:remote_ip] = request.remote_ip
if current_user.present?
payload[:user_id] = current_user.id
payload[:username] = current_user.username
end
end
# This filter handles both private tokens and personal access tokens
def authenticate_user_from_private_token!
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
......
module EE
module Admin
module ApplicationSettingsController
def permitted_application_setting_attributes
def visible_application_setting_attributes
attrs = super
if License.feature_available?(:repository_mirrors)
......
class Profiles::GpgKeysController < Profiles::ApplicationController
before_action :set_gpg_key, only: [:destroy, :revoke]
def index
@gpg_keys = current_user.gpg_keys
@gpg_key = GpgKey.new
end
def create
@gpg_key = current_user.gpg_keys.new(gpg_key_params)
if @gpg_key.save
redirect_to profile_gpg_keys_path
else
@gpg_keys = current_user.gpg_keys.select(&:persisted?)
render :index
end
end
def destroy
@gpg_key.destroy
respond_to do |format|
format.html { redirect_to profile_gpg_keys_url, status: 302 }
format.js { head :ok }
end
end
def revoke
@gpg_key.revoke
respond_to do |format|
format.html { redirect_to profile_gpg_keys_url, status: 302 }
format.js { head :ok }
end
end
private
def gpg_key_params
params.require(:gpg_key).permit(:key)
end
def set_gpg_key
@gpg_key = current_user.gpg_keys.find(params[:id])
end
end
......@@ -6,18 +6,9 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_download_code!
before_action :set_commits
def show
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
search = params[:search]
@commits =
if search.present?
@repository.find_commits_by_message(search, @ref, @path, @limit, @offset)
else
@repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
end
@note_counts = project.notes.where(commit_id: @commits.map(&:id))
.group(:commit_id).count
......@@ -37,4 +28,33 @@ class Projects::CommitsController < Projects::ApplicationController
end
end
end
def signatures
respond_to do |format|
format.json do
render json: {
signatures: @commits.select(&:has_signature?).map do |commit|
{
commit_sha: commit.sha,
html: view_to_html_string('projects/commit/_signature', signature: commit.signature)
}
end
}
end
end
end
private
def set_commits
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
search = params[:search]
@commits =
if search.present?
@repository.find_commits_by_message(search, @ref, @path, @limit, @offset)
else
@repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
end
end
end
......@@ -43,23 +43,7 @@ class Projects::GraphsController < Projects::ApplicationController
end
def get_languages
@languages = Linguist::Repository.new(@repository.rugged, @repository.rugged.head.target_id).languages
total = @languages.map(&:last).sum
@languages = @languages.map do |language|
name, share = language
color = Linguist::Language[name].color || "##{Digest::SHA256.hexdigest(name)[0...6]}"
{
value: (share.to_f * 100 / total).round(2),
label: name,
color: color,
highlight: color
}
end
@languages.sort! do |x, y|
y[:value] <=> x[:value]
end
@languages = @project.repository.languages
end
def fetch_graph
......
......@@ -58,6 +58,9 @@ class Projects::WikisController < Projects::ApplicationController
else
render 'edit'
end
rescue WikiPage::PageChangedError
@conflict = true
render 'edit'
end
def create
......@@ -125,6 +128,6 @@ class Projects::WikisController < Projects::ApplicationController
end
def wiki_params
params.require(:wiki).permit(:title, :content, :format, :message)
params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha)
end
end
......@@ -73,10 +73,7 @@ class UsersController < ApplicationController
end
def calendar
calendar = contributions_calendar
@activity_dates = calendar.activity_dates
render 'calendar', layout: false
render json: contributions_calendar.activity_dates
end
def calendar_activities
......
......@@ -11,6 +11,7 @@
# group_id: integer
# project_id: integer
# milestone_title: string
# author_id: integer
# assignee_id: integer
# search: string
# label_name: string
......
......@@ -10,6 +10,7 @@
# group_id: integer
# project_id: integer
# milestone_title: string
# author_id: integer
# assignee_id: integer
# search: string
# label_name: string
......
......@@ -113,6 +113,10 @@ module CommitsHelper
commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
end
def commit_signature_badge_classes(additional_classes)
%w(btn status-box gpg-status-box) + Array(additional_classes)
end
protected
# Private: Returns a link to a person. If the person has a matching user and
......
......@@ -14,7 +14,7 @@ module Emails
end
def new_ssh_key_email(key_id)
@key = Key.find_by_id(key_id)
@key = Key.find_by(id: key_id)
return unless @key
......@@ -22,5 +22,15 @@ module Emails
@target_url = user_url(@user)
mail(to: @user.notification_email, subject: subject("SSH key was added to your account"))
end
def new_gpg_key_email(gpg_key_id)
@gpg_key = GpgKey.find_by(id: gpg_key_id)
return unless @gpg_key
@current_user = @user = @gpg_key.user
@target_url = user_url(@user)
mail(to: @user.notification_email, subject: subject("GPG key was added to your account"))
end
end
end
......@@ -362,7 +362,9 @@ class ApplicationSetting < ActiveRecord::Base
Array(read_attribute(:repository_storages))
end
# DEPRECATED
# repository_storage is still required in the API. Remove in 9.0
# Still used in API v3
def repository_storage
repository_storages.first
end
......
......@@ -3,4 +3,13 @@ class ChatTeam < ActiveRecord::Base
validates :namespace, uniqueness: true
belongs_to :namespace
def remove_mattermost_team(current_user)
Mattermost::Team.new(current_user).destroy(team_id: team_id)
rescue Mattermost::ClientError => e
# Either the group is not found, or the user doesn't have the proper
# access on the mattermost instance. In the first case, we're done either way
# in the latter case, we can't recover by retrying, so we just log what happened
Rails.logger.error("Mattermost team deletion failed: #{e}")
end
end
......@@ -234,6 +234,14 @@ class Commit
@statuses[ref] = pipelines.latest_status(ref)
end
def signature
return @signature if defined?(@signature)
@signature = gpg_commit.signature
end
delegate :has_signature?, to: :gpg_commit
def revert_branch_name
"revert-#{short_id}"
end
......@@ -382,4 +390,8 @@ class Commit
def merged_merge_request_no_cache(user)
MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit?
end
def gpg_commit
@gpg_commit ||= Gitlab::Gpg::Commit.new(self)
end
end
class GpgKey < ActiveRecord::Base
KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----'.freeze
KEY_SUFFIX = '-----END PGP PUBLIC KEY BLOCK-----'.freeze
include ShaAttribute
sha_attribute :primary_keyid
sha_attribute :fingerprint
belongs_to :user
has_many :gpg_signatures
validates :user, presence: true
validates :key,
presence: true,
uniqueness: true,
format: {
with: /\A#{KEY_PREFIX}((?!#{KEY_PREFIX})(?!#{KEY_SUFFIX}).)+#{KEY_SUFFIX}\Z/m,
message: "is invalid. A valid public GPG key begins with '#{KEY_PREFIX}' and ends with '#{KEY_SUFFIX}'"
}
validates :fingerprint,
presence: true,
uniqueness: true,
# only validate when the `key` is valid, as we don't want the user to show
# the error about the fingerprint
unless: -> { errors.has_key?(:key) }
validates :primary_keyid,
presence: true,
uniqueness: true,
# only validate when the `key` is valid, as we don't want the user to show
# the error about the fingerprint
unless: -> { errors.has_key?(:key) }
before_validation :extract_fingerprint, :extract_primary_keyid
after_commit :update_invalid_gpg_signatures, on: :create
after_commit :notify_user, on: :create
def primary_keyid
super&.upcase
end
def fingerprint
super&.upcase
end
def key=(value)
super(value&.strip)
end
def user_infos
@user_infos ||= Gitlab::Gpg.user_infos_from_key(key)
end
def verified_user_infos
user_infos.select do |user_info|
user_info[:email] == user.email
end
end
def emails_with_verified_status
user_infos.map do |user_info|
[
user_info[:email],
user_info[:email] == user.email
]
end.to_h
end
def verified?
emails_with_verified_status.any? { |_email, verified| verified }
end
def update_invalid_gpg_signatures
InvalidGpgSignatureUpdateWorker.perform_async(self.id)
end
def revoke
GpgSignature.where(gpg_key: self, valid_signature: true).update_all(
gpg_key_id: nil,
valid_signature: false,
updated_at: Time.zone.now
)
destroy
end
private
def extract_fingerprint
# we can assume that the result only contains one item as the validation
# only allows one key
self.fingerprint = Gitlab::Gpg.fingerprints_from_key(key).first
end
def extract_primary_keyid
# we can assume that the result only contains one item as the validation
# only allows one key
self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first
end
def notify_user
NotificationService.new.new_gpg_key(self)
end
end
class GpgSignature < ActiveRecord::Base
include ShaAttribute
sha_attribute :commit_sha
sha_attribute :gpg_key_primary_keyid
belongs_to :project
belongs_to :gpg_key
validates :commit_sha, presence: true
validates :project_id, presence: true
validates :gpg_key_primary_keyid, presence: true
def gpg_key_primary_keyid
super&.upcase
end
def commit
project.commit(commit_sha)
end
end
......@@ -236,10 +236,21 @@ class MergeRequestDiff < ActiveRecord::Base
def create_merge_request_diff_files(diffs)
rows = diffs.map.with_index do |diff, index|
diff.to_hash.merge(
diff_hash = diff.to_hash.merge(
binary: false,
merge_request_diff_id: self.id,
relative_order: index
)
# Compatibility with old diffs created with Psych.
diff_hash.tap do |hash|
diff_text = hash[:diff]
if diff_text.encoding == Encoding::BINARY && !diff_text.ascii_only?
hash[:binary] = true
hash[:diff] = [diff_text].pack('m0')
end
end
end
Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
......@@ -268,9 +279,7 @@ class MergeRequestDiff < ActiveRecord::Base
st_diffs
end
elsif merge_request_diff_files.present?
merge_request_diff_files
.as_json(only: Gitlab::Git::Diff::SERIALIZE_KEYS)
.map(&:with_indifferent_access)
merge_request_diff_files.map(&:to_hash)
end
end
......
......@@ -8,4 +8,14 @@ class MergeRequestDiffFile < ActiveRecord::Base
encode_utf8(diff) if diff.respond_to?(:encoding)
end
def diff
binary? ? super.unpack('m0').first : super
end
def to_hash
keys = Gitlab::Git::Diff::SERIALIZE_KEYS - [:diff]
as_json(only: keys).merge(diff: diff).with_indifferent_access
end
end
......@@ -160,7 +160,10 @@ class JiraService < IssueTrackerService
def test(_)
result = test_settings
{ success: result.present?, result: result }
success = result.present?
result = @error if @error && !success
{ success: success, result: result }
end
# JIRA does not need test data.
......@@ -288,7 +291,8 @@ class JiraService < IssueTrackerService
yield
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => e
Rails.logger.info "#{self.class.name} Send message ERROR: #{client_url} - #{e.message}"
@error = e.message
Rails.logger.info "#{self.class.name} Send message ERROR: #{client_url} - #{@error}"
nil
end
......
......@@ -465,10 +465,6 @@ class Repository
nil
end
def blob_by_oid(oid)
Gitlab::Git::Blob.raw(self, oid)
end
def root_ref
if raw_repository
raw_repository.root_ref
......
......@@ -79,6 +79,7 @@ class User < ActiveRecord::Base
where(type.not_eq('DeployKey').or(type.eq(nil)))
end, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :gpg_keys
has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :personal_access_tokens, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......@@ -160,6 +161,7 @@ class User < ActiveRecord::Base
before_save :ensure_authentication_token, :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: :external_changed?
after_save :ensure_namespace_correct
after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
after_initialize :set_projects_limit
after_destroy :post_destroy_hook
......@@ -531,6 +533,10 @@ class User < ActiveRecord::Base
end
end
def update_invalid_gpg_signatures
gpg_keys.each(&:update_invalid_gpg_signatures)
end
# Returns the groups a user has access to
def authorized_groups
union = Gitlab::SQL::Union
......
class WikiPage
PageChangedError = Class.new(StandardError)
include ActiveModel::Validations
include ActiveModel::Conversion
include StaticModel
......@@ -136,6 +138,10 @@ class WikiPage
versions.first
end
def last_commit_sha
commit&.sha
end
# Returns the Date that this latest version was
# created on.
def created_at
......@@ -182,17 +188,22 @@ class WikiPage
# Updates an existing Wiki Page, creating a new version.
#
# new_content - The raw markup content to replace the existing.
# format - Optional symbol representing the content format.
# See ProjectWiki::MARKUPS Hash for available formats.
# message - Optional commit message to set on the new version.
# new_content - The raw markup content to replace the existing.
# format - Optional symbol representing the content format.
# See ProjectWiki::MARKUPS Hash for available formats.
# message - Optional commit message to set on the new version.
# last_commit_sha - Optional last commit sha to validate the page unchanged.
#
# Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful.
def update(new_content = "", format = :markdown, message = nil)
def update(new_content, format: :markdown, message: nil, last_commit_sha: nil)
@attributes[:content] = new_content
@attributes[:format] = format
if last_commit_sha && last_commit_sha != self.last_commit_sha
raise PageChangedError.new("You are attempting to update a page that has changed since you started editing it.")
end
save :update_page, @page, content, format, message
end
......
......@@ -61,6 +61,8 @@ class GitPushService < BaseService
update_remote_mirrors
update_caches
update_signatures
end
def update_gitattributes
......@@ -85,6 +87,12 @@ class GitPushService < BaseService
ProjectCacheWorker.perform_async(@project.id, types, [:commit_count, :repository_size])
end
def update_signatures
@push_commits.each do |commit|
CreateGpgSignatureWorker.perform_async(commit.sha, @project.id)
end
end
# Schedules processing of commit messages.
def process_commit_messages
default = is_default_branch?
......
......@@ -21,6 +21,8 @@ module Groups
DestroyService.new(group, current_user).execute
end
group.chat_team&.remove_mattermost_team(current_user)
group.really_destroy!
end
end
......
......@@ -19,6 +19,16 @@ class NotificationService
end
end
# Always notify the user about gpg key added
#
# This is a security email so it will be sent even if the user user disabled
# notifications
def new_gpg_key(gpg_key)
if gpg_key.user
mailer.new_gpg_key_email(gpg_key.id).deliver_later
end
end
# Always notify user about email added to profile
def new_email(email)
if email.user
......
......@@ -138,7 +138,11 @@ module Projects
end
def max_size
current_application_settings.max_pages_size.megabytes || MAX_SIZE
max_pages_size = current_application_settings.max_pages_size.megabytes
return MAX_SIZE if max_pages_size.zero?
[max_pages_size, MAX_SIZE].min
end
def tmp_path
......
module WikiPages
class UpdateService < WikiPages::BaseService
def execute(page)
if page.update(@params[:content], @params[:format], @params[:message])
if page.update(@params[:content], format: @params[:format], message: @params[:message], last_commit_sha: @params[:last_commit_sha])
execute_hooks(page, 'update')
end
......
......@@ -175,7 +175,7 @@
.well-segment.well-centered
= link_to admin_groups_path do
%h3.text-center
Groups
Groups:
= number_with_delimiter(Group.count)
%hr
= link_to 'New group', new_admin_group_path, class: "btn btn-new"
......
= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: false
......@@ -65,6 +65,10 @@
= 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
%span
GPG Keys
= nav_link(controller: :preferences) do
= link_to profile_preferences_path, title: 'Preferences' do
.nav-icon-container
......
......@@ -43,6 +43,10 @@
= link_to profile_keys_path, title: 'SSH Keys' do
%span
SSH Keys
= nav_link(controller: :gpg_keys) do
= link_to profile_gpg_keys_path, title: 'GPG Keys' do
%span
GPG Keys
= nav_link(controller: :preferences) do
= link_to profile_preferences_path, title: 'Preferences' do
%span
......
%p
Hi #{@user.name}!
%p
A new GPG key was added to your account:
%p
Fingerprint:
%code= @gpg_key.fingerprint
%p
If this key was added in error, you can remove it under
= link_to "GPG Keys", profile_gpg_keys_url
Hi <%= @user.name %>!
A new GPG key was added to your account:
Fingerprint: <%= @gpg_key.fingerprint %>
If this key was added in error, you can remove it at <%= profile_gpg_keys_url %>
- css_classes = %w(label label-verification-status)
- css_classes << (verified ? 'verified': 'unverified')
- text = verified ? 'Verified' : 'Unverified'
.gpg-email-badge
.gpg-email-badge-email= email
%div{ class: css_classes }
= text
%div
= form_for [:profile, @gpg_key], html: { class: 'js-requires-input' } do |f|
= form_errors(@gpg_key)
.form-group
= f.label :key, class: 'label-light'
= f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: "Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'."
.prepend-top-default
= f.submit 'Add key', class: "btn btn-create"
%li.key-list-item
.pull-left.append-right-10
= icon 'key', class: "settings-list-icon hidden-xs"
.key-list-item-info
- key.emails_with_verified_status.map do |email, verified|
= render partial: 'email_with_badge', locals: { email: email, verified: verified }
.description
%code= key.fingerprint
.pull-right
%span.key-created-at
created #{time_ago_with_tooltip(key.created_at)}
= link_to profile_gpg_key_path(key), data: { confirm: 'Are you sure? Removing this GPG key does not affect already signed commits.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
%span.sr-only Remove
= icon('trash')
= link_to revoke_profile_gpg_key_path(key), data: { confirm: 'Are you sure? All commits that were signed with this GPG key will be unverified.' }, method: :put, class: "btn btn-danger prepend-left-10" do
%span.sr-only Revoke
Revoke
- is_admin = local_assigns.fetch(:admin, false)
- if @gpg_keys.any?
%ul.well-list
= render partial: 'profiles/gpg_keys/key', collection: @gpg_keys, locals: { is_admin: is_admin }
- else
%p.settings-message.text-center
- if is_admin
There are no GPG keys associated with this account.
- else
There are no GPG keys with access to your account.
- page_title "GPG Keys"
= render 'profiles/head'
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
= page_title
%p
GPG keys allow you to verify signed commits.
.col-lg-9
%h5.prepend-top-0
Add a GPG key
%p.profile-settings-content
Before you can add a GPG key you need to
= link_to 'generate it.', help_page_path('workflow/gpg_signed_commits/index.md')
= render 'form'
%hr
%h5
Your GPG keys (#{@gpg_keys.count})
.append-bottom-default
= render 'key_table'
.file-content.image_file
%img{ 'data-src': blob_raw_url, alt: viewer.blob.name }
= image_tag(blob_raw_url, alt: viewer.blob.name)
- if commit.has_signature?
%button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
%i.fa.fa-spinner.fa-spin
.page-content-header.js-commit-box{ 'data-commit-path' => branches_project_commit_path(@project, @commit.id) }
.header-main-content
= render partial: 'signature', object: @commit.signature
%strong
#{ s_('CommitBoxTitle|Commit') }
%span.commit-sha= @commit.short_id
......
- title = capture do
.gpg-popover-icon.invalid
= render 'shared/icons/icon_status_notfound_borderless.svg'
%div
This commit was signed with an <strong>unverified</strong> signature.
- locals = { signature: signature, title: title, label: 'Unverified', css_classes: ['invalid'] }
= render partial: 'projects/commit/signature_badge', locals: locals
- if signature
- if signature.valid_signature?
= render partial: 'projects/commit/valid_signature_badge', locals: { signature: signature }
- else
= render partial: 'projects/commit/invalid_signature_badge', locals: { signature: signature }
- css_classes = commit_signature_badge_classes(css_classes)
- title = capture do
.gpg-popover-status
= title
- content = capture do
.clearfix
= content
GPG Key ID:
%span.monospace= signature.gpg_key_primary_keyid
= link_to('Learn more about signing commits', help_page_path('workflow/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
%button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } }
= label
- title = capture do
.gpg-popover-icon.valid
= render 'shared/icons/icon_status_success_borderless.svg'
%div
This commit was signed with a <strong>verified</strong> signature.
- content = capture do
- gpg_key = signature.gpg_key
- user = gpg_key&.user
- user_name = signature.gpg_key_user_name
- user_email = signature.gpg_key_user_email
- if user
= link_to user_path(user), class: 'gpg-popover-user-link' do
%div
= user_avatar_without_link(user: user, size: 32)
%div
%strong= gpg_key.user.name
%div @#{gpg_key.user.username}
- else
= mail_to user_email do
%div
= user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
%div
%strong= user_name
%div= user_email
- locals = { signature: signature, title: title, content: content, label: 'Verified', css_classes: ['valid'] }
= render partial: 'projects/commit/signature_badge', locals: locals
......@@ -10,7 +10,7 @@
- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
%li.commit.flex-list.js-toggle-container{ id: "commit-#{commit.short_id}" }
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
.avatar-cell.hidden-xs
= author_avatar(commit, size: 36)
......@@ -40,9 +40,15 @@
= project.name_with_namespace
.commit-actions.flex-row.hidden-xs
.commit-actions.hidden-xs
- if commit.status(ref)
= render_commit_status(commit, ref: ref)
- if request.xhr?
= render partial: 'projects/commit/signature', object: commit.signature
- else
= render partial: 'projects/commit/ajax_signature', locals: { commit: commit }
= link_to commit.short_id, project_commit_path(project, commit), class: "commit-sha btn btn-transparent"
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"))
= link_to_browse_code(project, commit)
......@@ -7,7 +7,7 @@
%span.commits-count= n_("%d commit", "%d commits", commits.count) % commits.count
%li.commits-row{ data: { day: day } }
%ul.content-list.commit-list
%ul.content-list.commit-list.flex-list
= render partial: 'projects/commits/commit', collection: commits, locals: { project: project, ref: ref }
- if hidden > 0
......
......@@ -8,7 +8,7 @@
.image
%span.wrap
.frame{ class: (diff_file.deleted_file? ? 'deleted' : 'added') }
%img{ 'data-src': blob_raw_path, alt: diff_file.file_path }
= image_tag(blob_raw_path, alt: diff_file.file_path)
%p.image-info= number_to_human_size(blob.size)
- else
.image
......@@ -16,7 +16,7 @@
%span.wrap
.frame.deleted
%a{ href: project_blob_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) }
%img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
= image_tag(old_blob_raw_path, alt: diff_file.old_path)
%p.image-info.hide
%span.meta-filesize= number_to_human_size(old_blob.size)
|
......@@ -28,7 +28,7 @@
%span.wrap
.frame.added
%a{ href: project_blob_path(@project, tree_join(diff_file.content_sha, diff_file.new_path)) }
%img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
= image_tag(blob_raw_path, alt: diff_file.new_path)
%p.image-info.hide
%span.meta-filesize= number_to_human_size(blob.size)
|
......@@ -41,10 +41,10 @@
.swipe.view.hide
.swipe-frame
.frame.deleted
%img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
= image_tag(old_blob_raw_path, alt: diff_file.old_path)
.swipe-wrap
.frame.added
%img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
= image_tag(blob_raw_path, alt: diff_file.new_path)
%span.swipe-bar
%span.top-handle
%span.bottom-handle
......@@ -52,9 +52,9 @@
.onion-skin.view.hide
.onion-skin-frame
.frame.deleted
%img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
= image_tag(old_blob_raw_path, alt: diff_file.old_path)
.frame.added
%img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
= image_tag(blob_raw_path, alt: diff_file.new_path)
.controls
.transparent
.drag-track
......
- @no_container = true
- page_title "Labels"
- hide_class = ''
- can_admin_label = can?(current_user, :admin_label, @project)
- if show_new_nav? && can?(current_user, :admin_label, @project)
- content_for :breadcrumbs_extra do
......@@ -12,15 +13,17 @@
%div{ class: container_class }
.top-area.adjust
.nav-text
Labels can be applied to issues and merge requests. Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
Labels can be applied to issues and merge requests.
- if can_admin_label
Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
.nav-controls{ class: ("visible-xs" if show_new_nav?) }
- if can?(current_user, :admin_label, @project)
- if can_admin_label
.nav-controls{ class: ("visible-xs" if show_new_nav?) }
= link_to new_project_label_path(@project), class: "btn btn-new" do
New label
.labels
- if can?(current_user, :admin_label, @project)
- if can_admin_label
-# Only show it in the first page
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
.prioritized-labels{ class: ('hide' if hide) }
......@@ -33,7 +36,7 @@
- if @labels.present?
.other-labels
- if can?(current_user, :admin_label, @project)
- if can_admin_label
%h5{ class: ('hide' if hide) } Other Labels
%ul.content-list.manage-labels-list.js-other-labels
= render partial: 'shared/label', subject: @project, collection: @labels, as: :label
......
......@@ -3,7 +3,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f|
.hide.alert.alert-danger.mr-compare-errors
.merge-request-branches.row
.merge-request-branches.js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
.col-md-6
.panel.panel-default.panel-new-merge-request
.panel-heading
......@@ -66,10 +66,3 @@
- if @merge_request.errors.any?
= form_errors(@merge_request)
= f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn"
:javascript
new Compare({
targetProjectUrl: "#{project_new_merge_request_update_branches_path(@source_project)}",
sourceBranchUrl: "#{project_new_merge_request_branch_from_path(@source_project)}",
targetBranchUrl: "#{project_new_merge_request_branch_to_path(@source_project)}"
});
......@@ -17,7 +17,7 @@
= f.hidden_field :target_project_id
= f.hidden_field :target_branch
.mr-compare.merge-request
.mr-compare.merge-request.js-merge-request-new-submit{ 'data-mr-submit-action': "#{j params[:tab].presence || 'new'}" }
- if @commits.empty?
.commits-empty
%h4
......@@ -50,8 +50,3 @@
.mr-loading-status
= spinner
:javascript
var merge_request = new MergeRequest({
action: "#{j params[:tab].presence || 'new'}",
});
......@@ -3,11 +3,11 @@
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('diff_notes')
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('diff_notes')
= webpack_bundle_tag('issuable')
.merge-request{ 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) }
.merge-request{ 'data-mr-action': "#{j params[:tab].presence || 'show'}", 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) }
= render "projects/merge_requests/mr_title"
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
......@@ -16,6 +16,7 @@
- if @merge_request.source_branch_exists?
= render "projects/merge_requests/how_to_merge"
-# haml-lint:disable InlineJavaScript
:javascript
window.gl.mrWidgetData = #{serialize_issuable(@merge_request)}
......@@ -29,7 +30,6 @@
#js-vue-mr-widget.mr-widget
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'vue_merge_request_widget'
.content-block.content-block-small.emoji-list-container
......@@ -96,10 +96,3 @@
= render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title
- if @merge_request.can_be_cherry_picked?
= render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title
:javascript
$(function () {
window.mergeRequest = new MergeRequest({
action: "#{j params[:tab].presence || 'show'}",
});
});
......@@ -74,7 +74,7 @@
%div
- if fogbugz_import_enabled?
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
= icon('bug', text: 'Fogbugz')
= icon('bug', text: 'FogBugz')
%div
- if gitea_import_enabled?
= link_to new_import_gitea_url, class: 'btn import_gitea' do
......
......@@ -4,6 +4,8 @@
= form_errors(@page)
= f.hidden_field :title, value: @page.title
- if @page.persisted?
= f.hidden_field :last_commit_sha, value: @page.last_commit_sha
.form-group
.col-sm-12= f.label :format, class: 'control-label-full-width'
.col-sm-12
......
- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
- page_title "Edit", @page.title.capitalize, "Wiki"
- if @conflict
.alert.alert-danger
Someone edited the page the same time you did. Please check out
= link_to "the page", project_wiki_path(@project, @page), target: "_blank"
and make sure your changes will not unintentionally remove theirs.
.wiki-page-header.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
......
<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0V11.78a5.9 5.9 0 0 0 .827-.492z" fill-rule="nonzero"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></svg>
......@@ -4,5 +4,5 @@
= link_to user, title: user.name, class: "darken" do
= image_tag avatar_icon(user, 32), class: "avatar s32"
%strong= truncate(user.name, length: 40)
%br
%small.cgray= user.username
%div
%small.cgray= user.username
.clearfix.calendar
.js-contrib-calendar
.calendar-hint
Summary of issues, merge requests, push events, and comments
:javascript
new Calendar(
#{@activity_dates.to_json},
'#{user_calendar_activities_path}'
);
......@@ -2,9 +2,6 @@
- @hide_breadcrumbs = true
- page_title @user.name
- page_description @user.bio
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('users')
- header_title @user.name, user_path(@user)
- @no_container = true
......@@ -107,7 +104,7 @@
.tab-content
#activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs
.user-calendar{ data: { href: user_calendar_path } }
.user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path } }
%h4.center.light
%i.fa.fa-spinner.fa-spin
.user-calendar-activities
......@@ -131,10 +128,3 @@
.loading-status
= spinner
:javascript
var userProfile;
userProfile = new gl.User({
action: "#{controller.action_name}"
});
class CreateGpgSignatureWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
def perform(commit_sha, project_id)
project = Project.find_by(id: project_id)
return unless project
commit = project.commit(commit_sha)
return unless commit
commit.signature
end
end
class InvalidGpgSignatureUpdateWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
def perform(gpg_key_id)
gpg_key = GpgKey.find_by(id: gpg_key_id)
return unless gpg_key
Gitlab::Gpg::InvalidGpgSignatureUpdater.new(gpg_key).run
end
end
---
title: Alert the user if a Wiki page changed while they were editing it in order to prevent overwriting changes.
merge_request: 9707
author: Hiroyuki Sato
---
title: Display specific error message when JIRA test fails
merge_request:
author:
---
title: Add CSRF token verification to API
merge_request: 12154
author: Vitaliy @blackst0ne Klachkov
---
title: Improve CSS for global nav dropdown UI
merge_request: 12772
author: Takuya Noguchi
---
title: Remove help message about prioritized labels for non-members
merge_request: 12912
author: Takuya Noguchi
---
title: Fix creating merge request diffs when diff contains bytes that are invalid
in UTF-8
merge_request:
author:
---
title: Support custom directory in gitlab:backup:create task
merge_request: 12984
author: Markus Koller
---
title: GPG signed commits integration
merge_request: 9546
author: Alexis Reigel
---
title: Handle maximum pages artifacts size correctly
merge_request: 13072
author:
---
title: Add LDAP SSL certificate verification option
merge_request:
author:
---
title: Improve redirect route query performance
merge_request: 13062
author:
---
title: Ensure filesystem metrics test files are deleted
merge_request:
author:
---
title: Fix Prometheus client PID reuse bug
merge_request: 13130
author:
---
title: Enable gitaly_post_upload_pack by default
merge_request: 13078
author:
---
title: Fix the /projects/:id/repository/branches endpoint to handle dots in the branch
name when the project full patch contains a `/`
merge_request: 13115
author:
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.
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.
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