Commit c0b61bae authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2017-11-15

# Conflicts:
#	app/assets/javascripts/members.js
#	app/assets/javascripts/project.js
#	app/assets/stylesheets/framework/blank.scss
#	app/assets/stylesheets/framework/mixins.scss
#	app/models/merge_request.rb
#	app/views/dashboard/projects/_zero_authorized_projects.html.haml
#	app/views/shared/milestones/_sidebar.html.haml
#	config/prometheus/additional_metrics.yml
#	spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
#	spec/lib/gitlab/checks/change_access_spec.rb

[ci skip]
parents 88291fff 7b34d828
......@@ -441,12 +441,8 @@ ee_compat_check:
- /^[\d-]+-stable(-ee)?/
- branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
allow_failure: yes
allow_failure: no
retry: 0
cache:
key: "ee_compat_check_repo"
paths:
- ee_compat_check/ee-repo/
artifacts:
name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
when: on_failure
......
......@@ -414,7 +414,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.51.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.52.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -298,7 +298,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.51.0)
gitaly-proto (0.52.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -1061,7 +1061,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.51.0)
gitaly-proto (~> 0.52.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
......@@ -1224,4 +1224,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.15.4
1.16.0
......@@ -338,7 +338,8 @@ class GfmAutoComplete {
let resultantValue = value;
if (value && !this.setting.skipSpecialCharacterTest) {
const withoutAt = value.substring(1);
if (withoutAt && /[^\w\d]/.test(withoutAt)) {
const regex = value.charAt() === '~' ? /\W|^\d+$/ : /\W/;
if (withoutAt && regex.test(withoutAt)) {
resultantValue = `${value.charAt()}"${withoutAt}"`;
}
}
......
......@@ -309,6 +309,42 @@ export const setParamInURL = (param, value) => {
return search;
};
/**
* Given a string of query parameters creates an object.
*
* @example
* `scope=all&page=2` -> { scope: 'all', page: '2'}
* `scope=all` -> { scope: 'all' }
* ``-> {}
* @param {String} query
* @returns {Object}
*/
export const parseQueryStringIntoObject = (query = '') => {
if (query === '') return {};
return query
.split('&')
.reduce((acc, element) => {
const val = element.split('=');
Object.assign(acc, {
[val[0]]: decodeURIComponent(val[1]),
});
return acc;
}, {});
};
export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
/**
* Based on the current location and the string parameters provided
* creates a new entry in the history without reloading the page.
*
* @param {String} param
*/
export const historyPushState = (newUrl) => {
window.history.pushState({}, document.title, newUrl);
};
/**
* Converts permission provided as strings to booleans.
*
......
......@@ -52,3 +52,31 @@ export function bytesToKiB(number) {
export function bytesToMiB(number) {
return number / (BYTES_IN_KIB * BYTES_IN_KIB);
}
/**
* Utility function that calculates GiB of the given bytes.
* @param {Number} number
* @returns {Number}
*/
export function bytesToGiB(number) {
return number / (BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB);
}
/**
* Port of rails number_to_human_size
* Formats the bytes in number into a more understandable
* representation (e.g., giving it 1500 yields 1.5 KB).
*
* @param {Number} size
* @returns {String}
*/
export function numberToHumanSize(size) {
if (size < BYTES_IN_KIB) {
return `${size} bytes`;
} else if (size < BYTES_IN_KIB * BYTES_IN_KIB) {
return `${bytesToKiB(size).toFixed(2)} KiB`;
} else if (size < BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB) {
return `${bytesToMiB(size).toFixed(2)} MiB`;
}
return `${bytesToGiB(size).toFixed(2)} GiB`;
}
......@@ -60,7 +60,6 @@ export default class Poll {
checkConditions(response) {
const headers = normalizeHeaders(response.headers);
const pollInterval = parseInt(headers[this.intervalHeader], 10);
if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
this.timeoutID = setTimeout(() => {
this.makeRequest();
......@@ -102,7 +101,12 @@ export default class Poll {
/**
* Restarts polling after it has been stoped
*/
restart() {
restart(options) {
// update data
if (options && options.data) {
this.options.data = options.data;
}
this.canPoll = true;
this.makeRequest();
}
......
......@@ -18,7 +18,7 @@ export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3}
export const highCountTrim = count => (count > 99 ? '99+' : count);
/**
* Converst first char to uppercase and replaces undercores with spaces
* Converts first char to uppercase and replaces undercores with spaces
* @param {String} string
* @requires {String}
*/
......
......@@ -100,6 +100,10 @@ export function visitUrl(url, external = false) {
}
}
export function redirectTo(url) {
return window.location.assign(url);
}
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
......
......@@ -7,9 +7,12 @@ export default class Members {
}
addListeners() {
<<<<<<< HEAD
$('.js-ldap-permissions').off('click').on('click', this.showLDAPPermissionsWarning.bind(this));
$('.js-ldap-override').off('click').on('click', this.toggleMemberAccessToggle.bind(this));
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
=======
>>>>>>> upstream/master
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
......@@ -59,17 +62,6 @@ export default class Members {
});
});
}
// eslint-disable-next-line class-methods-use-this
removeRow(e) {
const $target = $(e.target);
if ($target.hasClass('btn-remove')) {
$target.closest('.member')
.fadeOut(function fadeOutMemberRow() {
$(this).remove();
});
}
}
formSubmit(e, $el = null) {
const $this = e ? $(e.currentTarget) : $el;
......
......@@ -2,16 +2,8 @@
export default {
name: 'PipelineNavigationTabs',
props: {
scope: {
type: String,
required: true,
},
count: {
type: Object,
required: true,
},
paths: {
type: Object,
tabs: {
type: Array,
required: true,
},
},
......@@ -23,68 +15,37 @@
// 0 is valid in a badge, but evaluates to false, we need to check for undefined
return count !== undefined;
},
onTabClick(tab) {
this.$emit('onChangeTab', tab.scope);
},
},
};
</script>
<template>
<ul class="nav-links scrolling-tabs">
<li
class="js-pipelines-tab-all"
:class="{ active: scope === 'all'}">
<a :href="paths.allPath">
All
<span
v-if="shouldRenderBadge(count.all)"
class="badge js-totalbuilds-count">
{{count.all}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-pending"
:class="{ active: scope === 'pending'}">
<a :href="paths.pendingPath">
Pending
<span
v-if="shouldRenderBadge(count.pending)"
class="badge">
{{count.pending}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-running"
:class="{ active: scope === 'running'}">
<a :href="paths.runningPath">
Running
<span
v-if="shouldRenderBadge(count.running)"
class="badge">
{{count.running}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-finished"
:class="{ active: scope === 'finished'}">
<a :href="paths.finishedPath">
Finished
v-for="(tab, i) in tabs"
:key="i"
:class="{
active: tab.isActive,
}"
>
<a
role="button"
@click="onTabClick(tab)"
:class="`js-pipelines-tab-${tab.scope}`"
>
{{ tab.name }}
<span
v-if="shouldRenderBadge(count.finished)"
class="badge">
{{count.finished}}
v-if="shouldRenderBadge(tab.count)"
class="badge"
>
{{tab.count}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-branches"
:class="{ active: scope === 'branches'}">
<a :href="paths.branchesPath">Branches</a>
</li>
<li
class="js-pipelines-tab-tags"
:class="{ active: scope === 'tags'}">
<a :href="paths.tagsPath">Tags</a>
</li>
</ul>
</template>
<script>
import _ from 'underscore';
import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import navigationTabs from './navigation_tabs.vue';
import navigationControls from './nav_controls.vue';
import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils';
import {
convertPermissionToBoolean,
getParameterByName,
historyPushState,
buildUrlWithCurrentLocation,
parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
export default {
props: {
......@@ -41,27 +48,18 @@
autoDevopsPath: pipelinesData.helpAutoDevopsPath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
allPath: pipelinesData.allPath,
pendingPath: pipelinesData.pendingPath,
runningPath: pipelinesData.runningPath,
finishedPath: pipelinesData.finishedPath,
branchesPath: pipelinesData.branchesPath,
tagsPath: pipelinesData.tagsPath,
hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath,
state: this.store.state,
apiScope: 'all',
pagenum: 1,
scope: getParameterByName('scope') || 'all',
page: getParameterByName('page') || '1',
requestData: {},
};
},
computed: {
canCreatePipelineParsed() {
return convertPermissionToBoolean(this.canCreatePipeline);
},
scope() {
const scope = getParameterByName('scope');
return scope === null ? 'all' : scope;
},
/**
* The empty state should only be rendered when the request is made to fetch all pipelines
......@@ -106,46 +104,112 @@
hasCiEnabled() {
return this.hasCi !== undefined;
},
paths() {
return {
allPath: this.allPath,
pendingPath: this.pendingPath,
finishedPath: this.finishedPath,
runningPath: this.runningPath,
branchesPath: this.branchesPath,
tagsPath: this.tagsPath,
};
},
pageParameter() {
return getParameterByName('page') || this.pagenum;
},
scopeParameter() {
return getParameterByName('scope') || this.apiScope;
tabs() {
const { count } = this.state;
return [
{
name: 'All',
scope: 'all',
count: count.all,
isActive: this.scope === 'all',
},
{
name: 'Pending',
scope: 'pending',
count: count.pending,
isActive: this.scope === 'pending',
},
{
name: 'Running',
scope: 'running',
count: count.running,
isActive: this.scope === 'running',
},
{
name: 'Finished',
scope: 'finished',
count: count.finished,
isActive: this.scope === 'finished',
},
{
name: 'Branches',
scope: 'branches',
isActive: this.scope === 'branches',
},
{
name: 'Tags',
scope: 'tags',
isActive: this.scope === 'tags',
},
];
},
},
created() {
this.service = new PipelinesService(this.endpoint);
this.requestData = { page: this.pageParameter, scope: this.scopeParameter };
this.requestData = { page: this.page, scope: this.scope };
},
methods: {
successCallback(resp) {
return resp.json().then((response) => {
// Because we are polling & the user is interacting verify if the response received
// matches the last request made
if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
this.store.storeCount(response.count);
this.store.storePagination(resp.headers);
this.setCommonData(response.pipelines);
}
});
},
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
* Handles URL and query parameter changes.
* When the user uses the pagination or the tabs,
* - update URL
* - Make API request to the server with new parameters
* - Update the polling function
* - Update the internal state
*/
change(pageNumber) {
const param = setParamInURL('page', pageNumber);
updateContent(parameters) {
// stop polling
this.poll.stop();
const queryString = Object.keys(parameters).map((parameter) => {
const value = parameters[parameter];
// update internal state for UI
this[parameter] = value;
return `${parameter}=${encodeURIComponent(value)}`;
}).join('&');
gl.utils.visitUrl(param);
return param;
// update polling parameters
this.requestData = parameters;
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
this.isLoading = true;
// fetch new data
return this.service.getPipelines(this.requestData)
.then((response) => {
this.isLoading = false;
this.successCallback(response);
// restart polling
this.poll.restart({ data: this.requestData });
})
.catch(() => {
this.isLoading = false;
this.errorCallback();
// restart polling
this.poll.restart();
});
},
successCallback(resp) {
return resp.json().then((response) => {
this.store.storeCount(response.count);
this.store.storePagination(resp.headers);
this.setCommonData(response.pipelines);
});
onChangeTab(scope) {
this.updateContent({ scope, page: '1' });
},
onChangePage(page) {
/* URLS parameters are strings, we need to parse to match types */
this.updateContent({ scope: this.scope, page: Number(page).toString() });
},
},
};
......@@ -154,7 +218,7 @@
<div class="pipelines-container">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState">
v-if="!shouldRenderEmptyState">
<div class="fade-left">
<i
class="fa fa-angle-left"
......@@ -167,17 +231,17 @@
aria-hidden="true">
</i>
</div>
<navigation-tabs
:scope="scope"
:count="state.count"
:paths="paths"
:tabs="tabs"
@onChangeTab="onChangeTab"
/>
<navigation-controls
:new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath"
:ciLintPath="ciLintPath"
:ci-lint-path="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed "
/>
</div>
......@@ -188,6 +252,7 @@
label="Loading Pipelines"
size="3"
v-if="isLoading"
class="prepend-top-20"
/>
<empty-state
......@@ -221,8 +286,8 @@
<table-pagination
v-if="shouldRenderPagination"
:change="change"
:pageInfo="state.pageInfo"
:change="onChangePage"
:page-info="state.pageInfo"
/>
</div>
</div>
......
......@@ -13,6 +13,7 @@ export default class Project {
if (selectedCloneOption.length > 0) {
$(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
}
<<<<<<< HEAD
$('a', $cloneOptions).on('click', (e) => {
const $this = $(e.currentTarget);
......@@ -30,6 +31,20 @@ export default class Project {
cloneUrlPrimary: $this.data('primaryUrl') || '',
});
=======
$('a', $cloneOptions).on('click', (e) => {
const $this = $(e.currentTarget);
const url = $this.attr('href');
e.preventDefault();
$('.is-active', $cloneOptions).not($this).removeClass('is-active');
$this.toggleClass('is-active');
$projectCloneField.val(url);
$cloneBtnText.text($this.text());
>>>>>>> upstream/master
return $('.clone').text(url);
});
// Ref switcher
......@@ -47,6 +62,7 @@ export default class Project {
$(this).parents('.no-password-message').remove();
return e.preventDefault();
});
<<<<<<< HEAD
$('.hide-shared-runner-limit-message').on('click', function(e) {
var $alert = $(this).parents('.shared-runner-quota-message');
var scope = $alert.data('scope');
......@@ -58,6 +74,12 @@ export default class Project {
}
static projectSelectDropdown() {
=======
Project.projectSelectDropdown();
}
static projectSelectDropdown () {
>>>>>>> upstream/master
new ProjectSelect();
$('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val()));
}
......
......@@ -8,6 +8,7 @@
import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { errorMessages, errorMessagesTypes } from '../constants';
import { numberToHumanSize } from '../../lib/utils/number_utils';
export default {
props: {
......@@ -41,6 +42,10 @@
return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
},
formatSize(size) {
return numberToHumanSize(size);
},
handleDeleteRegistry(registry) {
this.deleteRegistry(registry)
.then(() => this.fetchList({ repo: this.repo }))
......@@ -97,7 +102,7 @@
</span>
</td>
<td>
{{item.size}}
{{formatSize(item.size)}}
<template v-if="item.size && item.layers">
&middot;
</template>
......
......@@ -33,6 +33,7 @@
@import "framework/modal";
@import "framework/pagination";
@import "framework/panels";
@import "framework/popup";
@import "framework/secondary-navigation-elements";
@import "framework/selects";
@import "framework/sidebar";
......
......@@ -30,6 +30,7 @@
margin-bottom: 0;
}
}
<<<<<<< HEAD
.blank-state-link {
display: block;
......@@ -37,6 +38,15 @@
flex: 0 0 100%;
margin-bottom: 15px;
=======
.blank-state-link {
display: block;
color: $gl-text-color;
flex: 0 0 100%;
margin-bottom: 15px;
>>>>>>> upstream/master
@media (min-width: $screen-sm-min) {
flex: 0 0 49%;
......@@ -63,7 +73,10 @@
@media (min-width: $screen-sm-min) {
display: flex;
<<<<<<< HEAD
height: 100%;
=======
>>>>>>> upstream/master
align-items: center;
padding: 50px 30px;
}
......@@ -89,6 +102,7 @@
@media (min-width: $screen-sm-min) {
padding-left: 20px;
}
<<<<<<< HEAD
}
}
......@@ -118,5 +132,7 @@
@media (max-width: $screen-xs-max) {
.blank-state-icon svg {
width: 315px;
=======
>>>>>>> upstream/master
}
}
......@@ -129,7 +129,7 @@
margin: 5px 2px 5px -8px;
border-radius: $border-radius-default;
svg {
.tanuki-logo {
@media (min-width: $screen-sm-min) {
margin-right: 8px;
}
......
......@@ -181,6 +181,7 @@
}
}
<<<<<<< HEAD
@mixin fade($gradient-direction, $gradient-color) {
visibility: hidden;
opacity: 0;
......@@ -214,5 +215,32 @@
&::-webkit-scrollbar {
display: none;
=======
@mixin triangle($color, $border-color, $size, $border-size) {
&::before,
&::after {
bottom: 100%;
left: 50%;
border: solid transparent;
content: '';
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
&::before {
border-color: transparent;
border-bottom-color: $border-color;
border-width: ($size + $border-size);
margin-left: -($size + $border-size);
}
&::after {
border-color: transparent;
border-bottom-color: $color;
border-width: $size;
margin-left: -$size;
>>>>>>> upstream/master
}
}
.popup {
@include triangle(
$gray-lighter,
$gray-darker,
$popup-triangle-size,
$popup-triangle-border-size
);
padding: $gl-padding;
background-color: $gray-lighter;
border: 1px solid $gray-darker;
border-radius: $border-radius-default;
box-shadow: 0 5px 8px $popup-box-shadow-color;
position: relative;
}
......@@ -741,3 +741,10 @@ Image Commenting cursor
*/
$image-comment-cursor-left-offset: 12;
$image-comment-cursor-top-offset: 30;
/*
Popup
*/
$popup-triangle-size: 15px;
$popup-triangle-border-size: 1px;
$popup-box-shadow-color: rgba(90, 90, 90, 0.05);
......@@ -93,15 +93,7 @@ module LfsRequest
end
def storage_project
@storage_project ||= begin
result = project
# TODO: Make this go to the fork_network root immeadiatly
# dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
result = result.fork_source while result.forked?
result
end
@storage_project ||= project.lfs_storage_project
end
def objects
......
......@@ -4,7 +4,8 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_read_build!,
only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all]
except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase]
before_action :authorize_erase_build!, only: [:erase]
layout 'project'
......@@ -131,6 +132,10 @@ class Projects::JobsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :update_build, build)
end
def authorize_erase_build!
return access_denied! unless can?(current_user, :erase_build, build)
end
def build
@build ||= project.builds.find(params[:id])
.present(current_user: current_user)
......
......@@ -57,7 +57,7 @@ class AutocompleteUsersFinder
def find_users
return users_from_project if project
return group.users if group
return group.users_with_parents if group
return User.all if current_user
User.none
......
......@@ -36,6 +36,7 @@ class IssuableFinder
iids
label_name
milestone_title
my_reaction_emoji
non_archived
project_id
scope
......
......@@ -6,8 +6,11 @@ module NamespacesHelper
end
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
groups = current_user.owned_groups + current_user.masters_groups
users = [current_user.namespace]
groups = current_user.manageable_groups
.joins(:route)
.includes(:route)
.order('routes.path')
users = [current_user.namespace]
unless extra_group.nil? || extra_group.is_a?(Group)
extra_group = Group.find(extra_group) if Namespace.find(extra_group).kind == 'group'
......
......@@ -197,6 +197,10 @@ module Ci
project.build_timeout
end
def triggered_by?(current_user)
user == current_user
end
# A slugified version of the build ref, suitable for inclusion in URLs and
# domain names. Rules:
#
......
......@@ -79,8 +79,8 @@ module Ci
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
transition [:success, :failed, :canceled, :skipped] => :running
transition [:created, :skipped] => :pending
transition [:success, :failed, :canceled] => :running
end
event :run do
......
......@@ -86,6 +86,14 @@ module Milestoneish
false
end
def total_issue_time_spent
@total_issue_time_spent ||= issues.joins(:timelogs).sum(:time_spent)
end
def human_total_issue_time_spent
Gitlab::TimeTrackingFormatter.output(total_issue_time_spent)
end
private
def count_issues_by_state(user)
......
......@@ -23,7 +23,8 @@ class DiffNote < Note
validate :positions_complete
validate :verify_supported
before_validation :set_original_position, :update_position, on: :create
before_validation :set_original_position, on: :create
before_validation :update_position, on: :create, if: :on_text?
before_validation :set_line_code
after_save :keep_around_commits
......
......@@ -301,10 +301,6 @@ class Issue < ActiveRecord::Base
true
end
def update_project_counter_caches?
state_changed? || confidential_changed?
end
def update_project_counter_caches
Projects::OpenIssuesCountService.new(project).refresh_cache
end
......
......@@ -8,16 +8,8 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader
def storage_project(project)
if project && project.forked?
storage_project(project.forked_from_project)
else
project
end
end
def project_allowed_access?(project)
projects.exists?(storage_project(project).id)
projects.exists?(project.lfs_storage_project.id)
end
def self.destroy_unreferenced
......
......@@ -984,6 +984,7 @@ class MergeRequest < ActiveRecord::Base
true
end
<<<<<<< HEAD
def base_pipeline
@base_pipeline ||= project.pipelines.find_by(sha: merge_request_diff&.base_commit_sha)
end
......@@ -992,6 +993,8 @@ class MergeRequest < ActiveRecord::Base
state_changed?
end
=======
>>>>>>> upstream/master
def update_project_counter_caches
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end
......
......@@ -69,6 +69,10 @@ class PagesDomain < ActiveRecord::Base
current < x509.not_before || x509.not_after < current
end
def expiration
x509&.not_after
end
def subject
return unless x509
x509.subject.to_s
......
......@@ -703,10 +703,6 @@ class Project < ActiveRecord::Base
import_type == 'gitea'
end
def github_import?
import_type == 'github'
end
def check_limit
unless creator.can_create_project? || namespace.kind == 'group'
projects_limit = creator.projects_limit
......@@ -1046,6 +1042,18 @@ class Project < ActiveRecord::Base
forked_from_project || fork_network&.root_project
end
def lfs_storage_project
@lfs_storage_project ||= begin
result = self
# TODO: Make this go to the fork_network root immeadiatly
# dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
result = result.fork_source while result&.forked?
result || self
end
end
def personal?
!group
end
......
......@@ -25,7 +25,7 @@ class PrometheusService < MonitoringService
end
def description
'Prometheus monitoring'
s_('PrometheusService|Prometheus monitoring')
end
def self.to_param
......@@ -38,8 +38,8 @@ class PrometheusService < MonitoringService
type: 'text',
name: 'api_url',
title: 'API URL',
placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/',
help: 'By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.',
placeholder: s_('PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/'),
help: s_('PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.'),
required: true
}
]
......
......@@ -944,7 +944,16 @@ class User < ActiveRecord::Base
end
def manageable_namespaces
@manageable_namespaces ||= [namespace] + owned_groups + masters_groups
@manageable_namespaces ||= [namespace] + manageable_groups
end
def manageable_groups
union = Gitlab::SQL::Union.new([owned_groups.select(:id),
masters_groups.select(:id)])
arel_union = Arel::Nodes::SqlLiteral.new(union.to_sql)
owned_and_master_groups = Group.where(Group.arel_table[:id].in(arel_union))
Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants
end
def namespaces
......
......@@ -10,6 +10,15 @@ module Ci
end
end
rule { protected_ref }.prevent :update_build
condition(:owner_of_job) do
can?(:developer_access) && @subject.triggered_by?(@user)
end
rule { protected_ref }.policy do
prevent :update_build
prevent :erase_build
end
rule { can?(:master_access) | owner_of_job }.enable :erase_build
end
end
......@@ -6,7 +6,7 @@ class BuildDetailsEntity < JobEntity
expose :pipeline, using: PipelineEntity
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :update_build, project) } do |build|
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build)
end
......
module Ci
class PipelineTriggerService < BaseService
include Gitlab::Utils::StrongMemoize
def execute
if trigger_from_token
create_pipeline_from_trigger(trigger_from_token)
......@@ -52,9 +54,9 @@ module Ci
end
def trigger_from_token
return @trigger if defined?(@trigger)
@trigger = Ci::Trigger.find_by_token(params[:token].to_s)
strong_memoize(:trigger) do
Ci::Trigger.find_by_token(params[:token].to_s)
end
end
def job_from_token
......
......@@ -189,7 +189,7 @@ class IssuableBaseService < BaseService
# We have to perform this check before saving the issuable as Rails resets
# the changed fields upon calling #save.
update_project_counters = issuable.project && issuable.update_project_counter_caches?
update_project_counters = issuable.project && update_project_counter_caches?(issuable)
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
......@@ -290,4 +290,8 @@ class IssuableBaseService < BaseService
# override if needed
def execute_hooks(issuable, action = 'open', params = {})
end
def update_project_counter_caches?(issuable)
issuable.state_changed?
end
end
......@@ -46,5 +46,9 @@ module Issues
params.delete(:assignee_ids)
end
end
def update_project_counter_caches?(issue)
super || issue.confidential_changed?
end
end
end
......@@ -11,13 +11,11 @@ module Projects
# supported by an importer class (`Gitlab::GithubImport::ParallelImporter`
# for example).
def async?
return false unless has_importer?
!!importer_class.try(:async?)
has_importer? && !!importer_class.try(:async?)
end
def execute
add_repository_to_project unless project.gitlab_project_import?
add_repository_to_project
import_data
......@@ -29,6 +27,14 @@ module Projects
private
def add_repository_to_project
if project.external_import? && !unknown_url?
raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
end
# We should skip the repository for a GitHub import or GitLab project import,
# because these importers fetch the project repositories for us.
return if has_importer? && importer_class.try(:imports_repository?)
if unknown_url?
# In this case, we only want to import issues, not a repository.
create_repository
......@@ -44,12 +50,6 @@ module Projects
end
def import_repository
raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
# We should return early for a GitHub import because the new GitHub
# importer fetch the project repositories for us.
return if project.github_import?
begin
if project.gitea_import?
fetch_repository
......@@ -88,7 +88,7 @@ module Projects
end
def importer_class
Gitlab::ImportSources.importer(project.import_type)
@importer_class ||= Gitlab::ImportSources.importer(project.import_type)
end
def has_importer?
......
......@@ -64,7 +64,7 @@
%th Projects
%th Jobs
%th Tags
%th Last contact
%th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc'))
%th
- @runners.each do |runner|
......
<<<<<<< HEAD
- admin_without_ee_license = !current_license && current_user.admin?
.blank-state-parent-container{ class: ('has-start-trial-container' if admin_without_ee_license) }
.section-container.section-welcome{ class: ('col-md-6' if admin_without_ee_license) }
=======
.blank-state-parent-container
.section-container.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" }
>>>>>>> upstream/master
.container.section-body
.row
.blank-state-welcome
......@@ -9,6 +14,7 @@
Welcome to GitLab
%p.blank-state-text
Code, test, and deploy together
<<<<<<< HEAD
.blank-state-row
%div{ class: ('column-large' if admin_without_ee_license) }
- if current_user.admin?
......@@ -18,3 +24,9 @@
- if admin_without_ee_license
.column-small
= render "blank_state_ee_trial"
=======
- if current_user.admin?
= render "blank_state_admin_welcome"
- else
= render "blank_state_welcome"
>>>>>>> upstream/master
- auth_app_owner = @pre_auth.client.application.owner
%main{ :role => "main" }
.modal-no-backdrop.modal-doorkeepr-auth
.modal-content
......@@ -20,9 +18,14 @@
%p
An application called
= link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer'
is requesting access to your GitLab account. This application was created by
= succeed "." do
= link_to auth_app_owner.name, user_path(auth_app_owner)
is requesting access to your GitLab account.
- auth_app_owner = @pre_auth.client.application.owner
- if auth_app_owner
This application was created by
= succeed "." do
= link_to auth_app_owner.name, user_path(auth_app_owner)
Please note that this application is not provided by GitLab and you should verify its authenticity before
allowing access.
- if @pre_auth.scopes
......
- discussion = @note.discussion if @note.part_of_discussion?
- diff_discussion = discussion&.diff_discussion?
- on_image = discussion.on_image? if diff_discussion
- if discussion
- phrase_end_char = on_image ? "." : ":"
%p.details
= succeed ':' do
= succeed phrase_end_char do
= link_to @note.author_name, user_url(@note.author)
- if discussion.diff_discussion?
- if diff_discussion
- if discussion.new_discussion?
started a new discussion
- else
......@@ -21,7 +26,7 @@
%p.details
#{link_to @note.author_name, user_url(@note.author)} commented:
- if discussion&.diff_discussion?
- if diff_discussion && !on_image
= content_for :head do
= stylesheet_link_tag 'mailers/highlighted_diff_email'
......
......@@ -73,7 +73,7 @@
class: 'js-raw-link-controller has-tooltip controllers-buttons' do
= icon('file-text-o')
- if can?(current_user, :update_build, @project) && @build.erasable?
- if @build.erasable? && can?(current_user, :erase_build, @build)
= link_to erase_project_job_path(@project, @build),
method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
......
......@@ -12,12 +12,6 @@
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
"new-pipeline-path" => new_project_pipeline_path(@project),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
"all-path" => project_pipelines_path(@project),
"pending-path" => project_pipelines_path(@project, scope: :pending),
"running-path" => project_pipelines_path(@project, scope: :running),
"finished-path" => project_pipelines_path(@project, scope: :finished),
"branches-path" => project_pipelines_path(@project, scope: :branches),
"tags-path" => project_pipelines_path(@project, scope: :tags),
"has-ci" => @repository.gitlab_ci_yml,
"ci-lint-path" => ci_lint_path } }
......
......@@ -4,42 +4,39 @@
.row.prepend-top-default.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring
.col-lg-3
%h4.prepend-top-0
Metrics
= s_('PrometheusService|Metrics')
%p
Metrics are automatically configured and monitored
based on a library of metrics from popular exporters.
= link_to 'More information', help_page_path('user/project/integrations/prometheus')
= s_('PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters.')
= link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus')
.col-lg-9
.panel.panel-default.js-panel-monitored-metrics{ data: { "active-metrics" => "#{project_prometheus_active_metrics_path(@project, :json)}" } }
.panel-heading
%h3.panel-title
Monitored
= s_('PrometheusService|Monitored')
%span.badge.js-monitored-count 0
.panel-body
.loading-metrics.text-center.js-loading-metrics
= icon('spinner spin 3x', class: 'metrics-load-spinner')
%p Finding and configuring metrics...
%p
= s_('PrometheusService|Finding and configuring metrics...')
.empty-metrics.text-center.hidden.js-empty-metrics
= custom_icon('icon_empty_metrics')
%p No metrics are being monitored. To start monitoring, deploy to an environment.
= link_to project_environments_path(@project), title: 'View environments', class: 'btn btn-success' do
View environments
%p
= s_('PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment.')
= link_to s_('PrometheusService|View environments'), project_environments_path(@project), class: 'btn btn-success'
%ul.list-unstyled.metrics-list.hidden.js-metrics-list
.panel.panel-default.hidden.js-panel-missing-env-vars
.panel-heading
%h3.panel-title
= icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel')
Missing environment variable
= s_('PrometheusService|Missing environment variable')
%span.badge.js-env-var-count 0
.panel-body.hidden
.flash-container
.flash-notice
.flash-text
To set up automatic monitoring, add the environment variable
%code
$CI_ENVIRONMENT_SLUG
to exporter&rsquo;s queries.
= link_to 'More information', help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels')
= s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries." % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>" }).html_safe
= link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels')
%ul.list-unstyled.metrics-list.js-missing-var-metrics-list
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></svg>
......@@ -109,7 +109,6 @@
class: 'btn btn-remove prepend-left-10'
- else
= link_to member,
remote: true,
method: :delete,
data: { confirm: remove_member_message(member) },
class: 'btn btn-remove prepend-left-10',
......
......@@ -85,7 +85,25 @@
Closed:
= milestone.issues_visible_to_user(current_user).closed.count
<<<<<<< HEAD
= render 'shared/milestones/weight', milestone: milestone
=======
.block.time_spent
.sidebar-collapsed-icon
= custom_icon('icon_hourglass')
%span.collapsed-milestone-total-time-spent
- if milestone.human_total_issue_time_spent
= milestone.human_total_issue_time_spent
- else
= _("None")
.title.hide-collapsed
= _("Total issue time spent")
.value.hide-collapsed
- if milestone.human_total_issue_time_spent
%span.bold= milestone.human_total_issue_time_spent
- else
%span.no-value= _("No time spent")
>>>>>>> upstream/master
.block.merge-requests
.sidebar-collapsed-icon
......
......@@ -2,6 +2,8 @@ class UpdateMergeRequestsWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
LOG_TIME_THRESHOLD = 90 # seconds
def perform(project_id, user_id, oldrev, newrev, ref)
project = Project.find_by(id: project_id)
return unless project
......@@ -9,6 +11,20 @@ class UpdateMergeRequestsWorker
user = User.find_by(id: user_id)
return unless user
MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
# TODO: remove this benchmarking when we have rich logging
time = Benchmark.measure do
MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
end
args_log = [
"elapsed=#{time.real}",
"project_id=#{project_id}",
"user_id=#{user_id}",
"oldrev=#{oldrev}",
"newrev=#{newrev}",
"ref=#{ref}"
].join(',')
Rails.logger.info("UpdateMergeRequestsWorker#perform #{args_log}") if time.real > LOG_TIME_THRESHOLD
end
end
---
title: Stop reloading the page when using pagination and tabs - use API calls - in
Pipelines table
merge_request:
author:
type: other
---
title: Add internationalization support for the prometheus integration
merge_request: 33338
author:
type: other
---
title: Fix errors when selecting numeric-only labels in the labels autocomplete selector
merge_request: 14607
author: haseebeqx
type: fixed
---
title: Add total time spent to milestones
merge_request: 15116
author: George Andrinopoulos
type: added
---
title: Add administrative endpoint to list all pages domains
merge_request: 15160
author: Travis Miller
type: added
---
title: Move update_project_counter_caches? out of issue and merge request
merge_request: 15300
author: George Andrinopoulos
type: other
---
title: Fix pipeline status transition for single manual job. This would also fix pipeline
duration becuse it is depending on status transition
merge_request: 15251
author:
type: fixed
---
title: Revert a regression on runners sorting (!15134)
merge_request: 15341
author: Takuya Noguchi
type: fixed
---
title: Remove extra margin from wordmark in header
merge_request:
author:
type: fixed
---
title: Don't use JS to delete memberships from projects and groups
merge_request: 15344
author:
type: fixed
---
title: Make sure a user can add projects to subgroups they have access to
merge_request: 15294
author:
type: fixed
---
title: Enable UnnecessaryMantissa in scss-lint
merge_request: 15255
author: Takuya Noguchi
type: other
---
title: Fix filter by my reaction is not working
merge_request: 15345
author: Hiroyuki Sato
type: fixed
---
title: Only owner or master can erase jobs
merge_request: 15216
author:
type: changed
---
title: Fix user autocomplete in subgroups
merge_request:
author:
type: fixed
---
title: Fix image diff notification email from showing wrong content
merge_request:
author:
type: fixed
---
title: Add performance logging to UpdateMergeRequestsWorker.
merge_request: 15360
author:
type: performance
......@@ -160,8 +160,12 @@
queries:
- query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100'
label: Average
<<<<<<< HEAD
unit: "%"
- query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}-canary"}[2m])) without (job)) * 100'
label: Average
unit: "%"
track: canary
=======
unit: "%"
>>>>>>> upstream/master
......@@ -4,6 +4,31 @@ Endpoints for connecting custom domain(s) and TLS certificates in [GitLab Pages]
The GitLab Pages feature must be enabled to use these endpoints. Find out more about [administering](../administration/pages/index.md) and [using](../user/project/pages/index.md) the feature.
## List all pages domains
Get a list of all pages domains. The user must have admin permissions.
```http
GET /pages/domains
```
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/pages/domains
```
```json
[
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
"certificate": {
"expired": false,
"expiration": "2020-04-12T14:32:00.000Z"
}
}
]
```
## List pages domains
Get a list of project pages domains. The user must have permissions to view pages domains.
......
......@@ -4,11 +4,11 @@ GitLab CE is licensed under the terms of the MIT License. GitLab EE is licensed
## Automated Testing
In order to comply with the terms the libraries we use are licensed under, we have to make sure to check new gems for compatible licenses whenever they're added. To automate this process, we use the [license_finder][license_finder] gem by Pivotal. It runs every time a new commit is pushed and verifies that all gems in the bundle use a license that doesn't conflict with the licensing of either GitLab Community Edition or GitLab Enterprise Edition.
In order to comply with the terms the libraries we use are licensed under, we have to make sure to check new gems for compatible licenses whenever they're added. To automate this process, we use the [license_finder][license_finder] gem by Pivotal. It runs every time a new commit is pushed and verifies that all gems and node modules in the bundle use a license that doesn't conflict with the licensing of either GitLab Community Edition or GitLab Enterprise Edition.
There are some limitations with the automated testing, however. CSS and JavaScript libraries, as well as any Ruby libraries not included by way of Bundler, must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them.
There are some limitations with the automated testing, however. CSS, JavaScript, or Ruby libraries which are not included by way of Bundler, NPM, or Yarn (for instance those manually copied into our source tree in the `vendor` directory), must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them.
Some gems may not include their license information in their `gemspec` file. These won't be detected by License Finder, and will have to be verified manually.
Some gems may not include their license information in their `gemspec` file, and some node modules may not include their license information in their `package.json` file. These won't be detected by License Finder, and will have to be verified manually.
### License Finder commands
......
......@@ -346,6 +346,12 @@ Blocks of code that are EE-specific should be moved to partials as much as
possible to avoid conflicts with big chunks of HAML code that that are not fun
to resolve when you add the indentation in the equation.
### Assets
#### gitlab-svgs
Conflicts in `app/assets/images/icons.json` or `app/assets/images/icons.svg` can be resolved simply by regenerating those assets with [`yarn run svg`](https://gitlab.com/gitlab-org/gitlab-svgs).
---
[Return to Development documentation](README.md)
......@@ -321,7 +321,7 @@ Auto DevOps uses [Helm](https://helm.sh/) to deploy your application to Kubernet
You can override the Helm chart used by bundling up a chart into your project
repo or by specifying a project variable:
- **Bundled chart** - If your project has a `./charts` directory with a `Chart.yaml`
- **Bundled chart** - If your project has a `./chart` directory with a `Chart.yaml`
file in it, Auto DevOps will detect the chart and use it instead of the [default
one](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app).
This can be a great way to control exactly how your application is deployed.
......
......@@ -217,6 +217,7 @@ instance and project. In addition, all admins can use the admin interface under
|---------------------------------------|-----------------|-------------|----------|--------|
| See commits and jobs | ✓ | ✓ | ✓ | ✓ |
| Retry or cancel job | | ✓ | ✓ | ✓ |
| Erase job artifacts and trace | | ✓ [^7] | ✓ | ✓ |
| Remove project | | | ✓ | ✓ |
| Create project | | | ✓ | ✓ |
| Change project configuration | | | ✓ | ✓ |
......@@ -281,6 +282,7 @@ only.
[^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner
[^5]: Only if user is not external one.
[^6]: Only if user is a member of the project.
[^7]: Only if the build was triggered by the user
[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
[new-mod]: project/new_ci_build_permissions_model.md
[ee-998]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/998
......
......@@ -62,7 +62,7 @@ which is highly recommendable and much faster than hardcoding.
If you set up a GitLab Pages project on GitLab.com,
it will automatically be accessible under a
[subdomain of `namespace.pages.io`](introduction.md#gitlab-pages-on-gitlab-com).
[subdomain of `namespace.gitlab.io`](introduction.md#gitlab-pages-on-gitlab-com).
The `namespace` is defined by your username on GitLab.com,
or the group name you created this project under.
......
......@@ -62,7 +62,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I should see additional file lines' do
page.within @diff.parent do
page.within @diff.query_scope do
expect(first('.new_line').text).not_to have_content "..."
end
end
......
......@@ -44,6 +44,8 @@ module API
# Helper Methods for Grape Endpoint
module HelperMethods
include Gitlab::Utils::StrongMemoize
def find_current_user!
user = find_user_from_access_token || find_user_from_job_token || find_user_from_warden
return unless user
......@@ -54,9 +56,9 @@ module API
end
def access_token
return @access_token if defined?(@access_token)
@access_token = find_oauth_access_token || find_personal_access_token
strong_memoize(:access_token) do
find_oauth_access_token || find_personal_access_token
end
end
def validate_access_token!(scopes: [])
......
......@@ -1164,6 +1164,11 @@ module API
expose :value
end
class PagesDomainCertificateExpiration < Grape::Entity
expose :expired?, as: :expired
expose :expiration
end
class PagesDomainCertificate < Grape::Entity
expose :subject
expose :expired?, as: :expired
......@@ -1171,12 +1176,23 @@ module API
expose :certificate_text
end
class PagesDomainBasic < Grape::Entity
expose :domain
expose :url
expose :certificate,
as: :certificate_expiration,
if: ->(pages_domain, _) { pages_domain.certificate? },
using: PagesDomainCertificateExpiration do |pages_domain|
pages_domain
end
end
class PagesDomain < Grape::Entity
expose :domain
expose :url
expose :certificate,
if: ->(pages_domain, _) { pages_domain.certificate? },
using: PagesDomainCertificate do |pages_domain|
if: ->(pages_domain, _) { pages_domain.certificate? },
using: PagesDomainCertificate do |pages_domain|
pages_domain
end
end
......
......@@ -175,6 +175,11 @@ module API
end
end
def authenticated_with_full_private_access!
authenticate!
forbidden! unless current_user.full_private_access?
end
def authenticated_as_admin!
authenticate!
forbidden! unless current_user.admin?
......@@ -210,6 +215,10 @@ module API
not_found! unless user_project.pages_available?
end
def require_pages_config_enabled!
not_found! unless Gitlab.config.pages.enabled
end
def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject)
end
......
......@@ -136,7 +136,7 @@ module API
authorize_update_builds!
build = find_build!(params[:job_id])
authorize!(:update_build, build)
authorize!(:erase_build, build)
return forbidden!('Job is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
......
......@@ -4,7 +4,6 @@ module API
before do
authenticate!
require_pages_enabled!
end
after_validation do
......@@ -29,10 +28,31 @@ module API
end
end
resource :pages do
before do
require_pages_config_enabled!
authenticated_with_full_private_access!
end
desc "Get all pages domains" do
success Entities::PagesDomainBasic
end
params do
use :pagination
end
get "domains" do
present paginate(PagesDomain.all), with: Entities::PagesDomainBasic
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
before do
require_pages_enabled!
end
desc 'Get all pages domains' do
success Entities::PagesDomain
end
......
......@@ -169,7 +169,7 @@ module API
authorize_update_builds!
build = get_build!(params[:build_id])
authorize!(:update_build, build)
authorize!(:erase_build, build)
return forbidden!('Build is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
......
......@@ -51,10 +51,20 @@ module Gitlab
FROM projects
WHERE forked_project_links.forked_from_project_id = projects.id
)
AND NOT EXISTS (
SELECT true
FROM forked_project_links AS parent_links
WHERE parent_links.forked_to_project_id = forked_project_links.forked_from_project_id
AND NOT EXISTS (
SELECT true
FROM projects
WHERE parent_links.forked_from_project_id = projects.id
)
)
AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
MISSING_MEMBERS
ForkNetworkMember.count_by_sql(count_sql) > 0
ForkedProjectLink.count_by_sql(count_sql) > 0
end
def log(message)
......
......@@ -15,7 +15,10 @@ module Gitlab
return false unless new_lfs_pointers.present?
existing_count = @project.lfs_objects.where(oid: new_lfs_pointers.map(&:lfs_oid)).count
existing_count = @project.lfs_storage_project
.lfs_objects
.where(oid: new_lfs_pointers.map(&:lfs_oid))
.count
existing_count != new_lfs_pointers.count
end
......
......@@ -59,7 +59,13 @@ module Gitlab
end
def pages
gollum_wiki.pages.map { |gollum_page| new_page(gollum_page) }
@repository.gitaly_migrate(:wiki_get_all_pages) do |is_enabled|
if is_enabled
gitaly_get_all_pages
else
gollum_get_all_pages
end
end
end
def page(title:, version: nil, dir: nil)
......@@ -179,6 +185,10 @@ module Gitlab
Gitlab::Git::WikiFile.new(gollum_file)
end
def gollum_get_all_pages
gollum_wiki.pages.map { |gollum_page| new_page(gollum_page) }
end
def gitaly_write_page(name, format, content, commit_details)
gitaly_wiki_client.write_page(name, format, content, commit_details)
end
......@@ -204,6 +214,12 @@ module Gitlab
Gitlab::Git::WikiFile.new(wiki_file)
end
def gitaly_get_all_pages
gitaly_wiki_client.get_all_pages.map do |wiki_page, version|
Gitlab::Git::WikiPage.new(wiki_page, version)
end
end
end
end
end
......@@ -11,6 +11,10 @@ module Gitlab
FIELDS.each do |field|
instance_variable_set("@#{field}", params[field])
end
# All gRPC strings in a response are frozen, so we get an unfrozen
# version here so appending to `raw_data` doesn't blow up.
@raw_data = @raw_data.dup
end
def historical?
......
......@@ -81,28 +81,23 @@ module Gitlab
)
response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_page, request)
wiki_page = version = nil
response.each do |message|
page = message.page
next unless page
wiki_page_from_iterator(response)
end
if wiki_page
wiki_page.raw_data << page.raw_data
else
wiki_page = GitalyClient::WikiPage.new(page.to_h)
# All gRPC strings in a response are frozen, so we get
# an unfrozen version here so appending in the else clause below doesn't blow up.
wiki_page.raw_data = wiki_page.raw_data.dup
def get_all_pages
request = Gitaly::WikiGetAllPagesRequest.new(repository: @gitaly_repo)
response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_all_pages, request)
pages = []
version = Gitlab::Git::WikiPageVersion.new(
Gitlab::Git::Commit.decorate(@repository, page.version.commit),
page.version.format
)
end
loop do
page, version = wiki_page_from_iterator(response) { |message| message.end_of_page }
break unless page && version
pages << [page, version]
end
[wiki_page, version]
pages
end
def find_file(name, revision)
......@@ -133,6 +128,35 @@ module Gitlab
private
# If a block is given and the yielded value is true, iteration will be
# stopped early at that point; else the iterator is consumed entirely.
# The iterator is traversed with `next` to allow resuming the iteration.
def wiki_page_from_iterator(iterator)
wiki_page = version = nil
while message = iterator.next
break if block_given? && yield(message)
page = message.page
next unless page
if wiki_page
wiki_page.raw_data << page.raw_data
else
wiki_page = GitalyClient::WikiPage.new(page.to_h)
version = Gitlab::Git::WikiPageVersion.new(
Gitlab::Git::Commit.decorate(@repository, page.version.commit),
page.version.format
)
end
end
[wiki_page, version]
rescue StopIteration
[wiki_page, version]
end
def gitaly_commit_details(commit_details)
Gitaly::WikiCommitDetails.new(
name: GitalyClient.encode(commit_details.name),
......
......@@ -11,6 +11,10 @@ module Gitlab
true
end
def self.imports_repository?
true
end
def initialize(project)
@project = project
end
......
module Gitlab
module ImportExport
class Importer
def self.imports_repository?
true
end
def initialize(project)
@archive_file = project.import_source
@current_user = project.creator
......
module Gitlab
module Utils
module StrongMemoize
# Instead of writing patterns like this:
#
# def trigger_from_token
# return @trigger if defined?(@trigger)
#
# @trigger = Ci::Trigger.find_by_token(params[:token].to_s)
# end
#
# We could write it like:
#
# def trigger_from_token
# strong_memoize(:trigger) do
# Ci::Trigger.find_by_token(params[:token].to_s)
# end
# end
#
def strong_memoize(name)
ivar_name = "@#{name}"
if instance_variable_defined?(ivar_name)
instance_variable_get(ivar_name)
else
instance_variable_set(ivar_name, yield)
end
end
end
end
end
......@@ -50,7 +50,7 @@ module QA
Capybara.configure do |config|
config.default_driver = :chrome
config.javascript_driver = :chrome
config.default_max_wait_time = 4
config.default_max_wait_time = 10
# https://github.com/mattheworiordan/capybara-screenshot/issues/164
config.save_path = 'tmp'
......
......@@ -12,12 +12,14 @@ describe IssuableCollections do
controller = klass.new
allow(controller).to receive(:params).and_return(state: 'opened')
allow(controller).to receive(:params).and_return(ActionController::Parameters.new(params))
controller
end
describe '#page_count_for_relation' do
let(:params) { { state: 'opened' } }
it 'returns the number of pages' do
relation = double(:relation, limit_value: 20)
pages = controller.send(:page_count_for_relation, relation, 28)
......@@ -25,4 +27,55 @@ describe IssuableCollections do
expect(pages).to eq(2)
end
end
describe '#filter_params' do
let(:params) do
{
assignee_id: '1',
assignee_username: 'user1',
author_id: '2',
author_username: 'user2',
authorized_only: 'true',
due_date: '2017-01-01',
group_id: '3',
iids: '4',
label_name: 'foo',
milestone_title: 'bar',
my_reaction_emoji: 'thumbsup',
non_archived: 'true',
project_id: '5',
scope: 'all',
search: 'baz',
sort: 'priority',
state: 'opened',
invalid_param: 'invalid_param'
}
end
it 'filters params' do
allow(controller).to receive(:cookies).and_return({})
filtered_params = controller.send(:filter_params)
expect(filtered_params).to eq({
'assignee_id' => '1',
'assignee_username' => 'user1',
'author_id' => '2',
'author_username' => 'user2',
'authorized_only' => 'true',
'due_date' => '2017-01-01',
'group_id' => '3',
'iids' => '4',
'label_name' => 'foo',
'milestone_title' => 'bar',
'my_reaction_emoji' => 'thumbsup',
'non_archived' => 'true',
'project_id' => '5',
'scope' => 'all',
'search' => 'baz',
'sort' => 'priority',
'state' => 'opened'
})
end
end
end
......@@ -371,8 +371,10 @@ describe Projects::JobsController do
end
describe 'POST erase' do
let(:role) { :master }
before do
project.add_developer(user)
project.team << [user, role]
sign_in(user)
post_erase
......@@ -404,6 +406,27 @@ describe Projects::JobsController do
end
end
context 'when user is developer' do
let(:role) { :developer }
let(:job) { create(:ci_build, :erasable, :trace, pipeline: pipeline, user: triggered_by) }
context 'when triggered by same user' do
let(:triggered_by) { user }
it 'has successful status' do
expect(response).to have_gitlab_http_status(:found)
end
end
context 'when triggered by different user' do
let(:triggered_by) { create(:user) }
it 'does not have successful status' do
expect(response).not_to have_gitlab_http_status(:found)
end
end
end
def post_erase
post :erase, namespace_id: project.namespace,
project_id: project,
......
......@@ -44,7 +44,11 @@ feature 'Groups > Members > Manage members' do
visit group_group_members_path(group)
find(:css, '.project-members-page li', text: user2.name).find(:css, 'a.btn-remove').click
accept_confirm do
find(:css, '.project-members-page li', text: user2.name).find(:css, 'a.btn-remove').click
end
wait_for_requests
expect(page).not_to have_content(user2.name)
expect(group.users).not_to include(user2)
......
......@@ -218,18 +218,18 @@ feature 'GFM autocomplete', :js do
user_item = find('.atwho-view li', text: user.username)
expect(user_item).to have_content(user.username)
end
end
def expect_to_wrap(should_wrap, item, note, value)
expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"")
def expect_to_wrap(should_wrap, item, note, value)
expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"")
item.click
item.click
if should_wrap
expect(note.value).to include("\"#{value}\"")
else
expect(note.value).not_to include("\"#{value}\"")
end
if should_wrap
expect(note.value).to include("\"#{value}\"")
else
expect(note.value).not_to include("\"#{value}\"")
end
end
end
......@@ -65,4 +65,33 @@ feature 'Milestone' do
expect(find('.alert-danger')).to have_content('already being used for another group or project milestone.')
end
end
feature 'Open a milestone' do
scenario 'shows total issue time spent correctly when no time has been logged' do
milestone = create(:milestone, project: project, title: 8.7)
visit project_milestone_path(project, milestone)
page.within('.block.time_spent') do
expect(page).to have_content 'No time spent'
expect(page).to have_content 'None'
end
end
scenario 'shows total issue time spent' do
milestone = create(:milestone, project: project, title: 8.7)
issue1 = create(:issue, project: project, milestone: milestone)
issue2 = create(:issue, project: project, milestone: milestone)
issue1.spend_time(duration: 3600, user: user)
issue1.save!
issue2.spend_time(duration: 7200, user: user)
issue2.save!
visit project_milestone_path(project, milestone)
page.within('.block.time_spent') do
expect(page).to have_content '3h'
end
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.
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