Commit 58455601 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into 'remove-redundant-has_internal_id'

 Conflicts:
   app/models/ci/pipeline.rb
parents 4dc7cf2b f3c7ab45
Please view this file on the master branch, on stable branches it's out of date.
## 10.8.4 (2018-06-06)
### Fixed (4 changes)
- Render a 403 when showing an access denied message. !5964
- Validate classification label on create & update. !5976
- Fix breadcrumbs being covered by System Header message.
- Treat external authorization service response status 403 as failure.
## 10.8.3 (2018-05-30)
- No changes.
......
......@@ -2,6 +2,10 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.8.4 (2018-06-06)
- No changes.
## 10.8.3 (2018-05-30)
### Fixed (4 changes)
......
......@@ -183,7 +183,7 @@ Assigning a team label makes sure issues get the attention of the appropriate
people.
The current team labels are ~Distribution, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX".
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products", ~"Configuration", and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team.
......@@ -350,7 +350,7 @@ on those issues. Please select someone with relevant experience from the
[GitLab team][team]. If there is nobody mentioned with that expertise look in
the commit history for the affected files to find someone.
[described in our handbook]: https://about.gitlab.com/handbook/engineering/issues/issue-triage-policies/
[described in our handbook]: https://about.gitlab.com/handbook/engineering/issue-triage/
[issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815
### Feature proposals
......@@ -513,7 +513,7 @@ request is as follows:
1. Write [tests](https://docs.gitlab.com/ee/development/rake_tasks.html#run-tests) and code
1. [Generate a changelog entry with `bin/changelog`][changelog]
1. If you are writing documentation, make sure to follow the
[documentation styleguide][doc-styleguide]
[documentation guidelines][doc-guidelines]
1. If you have multiple commits please combine them into a few logically
organized commits by [squashing them][git-squash]
1. Push the commit(s) to your fork
......@@ -746,7 +746,7 @@ When your code contains more than 500 changes, any major breaking changes, or an
[rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
[changelog]: doc/development/changelog.md "Generate a changelog entry"
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
[doc-guidelines]: doc/development/documentation/index.md "Documentation guidelines"
[js-styleguide]: doc/development/fe_guide/style_guide_js.md "JavaScript styleguide"
[scss-styleguide]: doc/development/fe_guide/style_guide_scss.md "SCSS styleguide"
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
......
......@@ -96,6 +96,10 @@ gem 'grape', '~> 1.0'
gem 'grape-entity', '~> 0.7.1'
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# GraphQL API
gem 'graphql', '~> 1.8.0'
gem 'graphiql-rails', '~> 1.4.10'
# Disable strong_params so that Mash does not respond to :permitted?
gem 'hashie-forbidden_attributes'
......@@ -386,7 +390,7 @@ end
group :test do
gem 'shoulda-matchers', '~> 3.1.2', require: false
gem 'email_spec', '~> 1.6.0'
gem 'email_spec', '~> 2.2.0'
gem 'json-schema', '~> 2.8.0'
gem 'webmock', '~> 2.3.2'
gem 'rails-controller-testing' if rails5? # Rails5 only gem.
......
......@@ -180,7 +180,7 @@ GEM
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.3.2)
railties (>= 4.2)
doorkeeper-openid_connect (1.3.0)
doorkeeper-openid_connect (1.4.0)
doorkeeper (~> 4.3)
json-jwt (~> 1.6)
dropzonejs-rails (0.7.2)
......@@ -199,9 +199,10 @@ GEM
faraday
multi_json
email_reply_trimmer (0.1.6)
email_spec (1.6.0)
email_spec (2.2.0)
htmlentities (~> 4.3.3)
launchy (~> 2.1)
mail (~> 2.2)
mail (~> 2.7)
encryptor (3.0.0)
equalizer (0.0.11)
erubis (2.7.0)
......@@ -389,6 +390,10 @@ GEM
rake (~> 12)
grape_logging (1.7.0)
grape
graphiql-rails (1.4.10)
railties
sprockets-rails
graphql (1.8.1)
grpc (1.11.0)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
......@@ -1045,7 +1050,7 @@ DEPENDENCIES
elasticsearch-model (~> 0.1.9)
elasticsearch-rails (~> 0.1.9)
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
email_spec (~> 2.2.0)
factory_bot_rails (~> 4.8.2)
faraday (~> 0.12)
faraday_middleware-aws-signers-v4
......@@ -1087,6 +1092,8 @@ DEPENDENCIES
grape-entity (~> 0.7.1)
grape-path-helpers (~> 1.0)
grape_logging (~> 1.7)
graphiql-rails (~> 1.4.10)
graphql (~> 1.8.0)
grpc (~> 1.11.0)
gssapi
haml_lint (~> 0.26.0)
......
......@@ -201,9 +201,10 @@ GEM
faraday
multi_json
email_reply_trimmer (0.1.10)
email_spec (1.6.0)
email_spec (2.2.0)
htmlentities (~> 4.3.3)
launchy (~> 2.1)
mail (~> 2.2)
mail (~> 2.7)
encryptor (3.0.0)
equalizer (0.0.11)
erubis (2.7.0)
......@@ -1050,7 +1051,7 @@ DEPENDENCIES
elasticsearch-model (~> 0.1.9)
elasticsearch-rails (~> 0.1.9)
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
email_spec (~> 2.2.0)
factory_bot_rails (~> 4.8.2)
faraday (~> 0.12)
faraday_middleware-aws-signers-v4
......
......@@ -176,6 +176,7 @@ the stable branch are:
* Fixes for [regressions](#regressions)
* Fixes for security issues
* Fixes or improvements to automated QA scenarios
* New or updated translations (as long as they do not touch application code)
During the feature freeze all merge requests that are meant to go into the
......
......@@ -156,5 +156,5 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on
## Is it awesome?
Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua.
[These people](https://twitter.com/gitlab/likes) seem to like it.
......@@ -187,7 +187,7 @@
role="row"
>
<div
class="alert alert-danger alert-block append-bottom-0"
class="alert alert-danger alert-block append-bottom-0 clusters-error-alert"
role="gridcell"
>
<div>
......
......@@ -107,6 +107,7 @@ export default {
:deploy-board-data="model.deployBoardData"
:is-loading="model.isLoadingDeployBoard"
:is-empty="model.isEmptyDeployBoard"
:logs-path="model.logs_path"
/>
</div>
</div>
......
<script>
import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
......@@ -20,6 +21,13 @@ export default {
},
methods: {
...mapActions(['updateActivityBarView']),
changedActivityView(e, view) {
e.currentTarget.blur();
this.updateActivityBarView(view);
$(e.currentTarget).tooltip('hide');
},
},
activityBarViews,
};
......@@ -54,7 +62,7 @@ export default {
:class="{
active: currentActivityView === $options.activityBarViews.edit
}"
@click.prevent="updateActivityBarView($options.activityBarViews.edit)"
@click.prevent="changedActivityView($event, $options.activityBarViews.edit)"
:title="s__('IDE|Edit')"
:aria-label="s__('IDE|Edit')"
>
......@@ -73,7 +81,7 @@ export default {
:class="{
active: currentActivityView === $options.activityBarViews.review
}"
@click.prevent="updateActivityBarView($options.activityBarViews.review)"
@click.prevent="changedActivityView($event, $options.activityBarViews.review)"
:title="s__('IDE|Review')"
:aria-label="s__('IDE|Review')"
>
......@@ -92,7 +100,7 @@ export default {
:class="{
active: currentActivityView === $options.activityBarViews.commit
}"
@click.prevent="updateActivityBarView($options.activityBarViews.commit)"
@click.prevent="changedActivityView($event, $options.activityBarViews.commit)"
:title="s__('IDE|Commit')"
:aria-label="s__('IDE|Commit')"
>
......
......@@ -6,9 +6,12 @@ import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
import { numberToHumanSize } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils';
import { isScrolledToBottom, scrollDown } from './lib/utils/scroll_utils';
import LogOutputBehaviours from './lib/utils/logoutput_behaviours';
export default class Job {
export default class Job extends LogOutputBehaviours {
constructor(options) {
super();
this.timeout = null;
this.state = null;
this.fetchingStatusFavicon = false;
......@@ -29,10 +32,6 @@ export default class Job {
this.$buildTraceOutput = $('.js-build-output');
this.$topBar = $('.js-top-bar');
// Scroll controllers
this.$scrollTopBtn = $('.js-scroll-up');
this.$scrollBottomBtn = $('.js-scroll-down');
clearTimeout(this.timeout);
this.initSidebar();
......@@ -48,23 +47,14 @@ export default class Job {
.off('click', '.stage-item')
.on('click', '.stage-item', this.updateDropdown);
// add event listeners to the scroll buttons
this.$scrollTopBtn
.off('click')
.on('click', this.scrollToTop.bind(this));
this.$scrollBottomBtn
.off('click')
.on('click', this.scrollToBottom.bind(this));
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$window
.off('scroll')
.on('scroll', () => {
if (!this.isScrolledToBottom()) {
if (!isScrolledToBottom()) {
this.toggleScrollAnimation(false);
} else if (this.isScrolledToBottom() && !this.isLogComplete) {
} else if (isScrolledToBottom() && !this.isLogComplete) {
this.toggleScrollAnimation(true);
}
this.scrollThrottled();
......@@ -90,60 +80,8 @@ export default class Job {
StickyFill.add(this.$topBar);
}
// eslint-disable-next-line class-methods-use-this
canScroll() {
return $(document).height() > $(window).height();
}
toggleScroll() {
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = $(window).height();
if (this.canScroll()) {
if (currentPosition > 0 &&
(scrollHeight - currentPosition !== windowHeight)) {
// User is in the middle of the log
this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (currentPosition === 0) {
// User is at Top of Log
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (this.isScrolledToBottom()) {
// User is at the bottom of the build log.
this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, true);
}
} else {
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, true);
}
}
// eslint-disable-next-line class-methods-use-this
isScrolledToBottom() {
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = $(window).height();
return scrollHeight - currentPosition === windowHeight;
}
// eslint-disable-next-line class-methods-use-this
scrollDown() {
const $document = $(document);
$document.scrollTop($document.height());
}
scrollToBottom() {
this.scrollDown();
scrollDown();
this.hasBeenScrolled = true;
this.toggleScroll();
}
......@@ -154,12 +92,6 @@ export default class Job {
this.toggleScroll();
}
// eslint-disable-next-line class-methods-use-this
toggleDisableButton($button, disable) {
if (disable && $button.prop('disabled')) return;
$button.prop('disabled', disable);
}
toggleScrollAnimation(toggle) {
this.$scrollBottomBtn.toggleClass('animate', toggle);
}
......@@ -191,7 +123,7 @@ export default class Job {
this.state = log.state;
}
this.isScrollInBottom = this.isScrolledToBottom();
this.isScrollInBottom = isScrolledToBottom();
if (log.append) {
this.$buildTraceOutput.append(log.html);
......@@ -231,7 +163,7 @@ export default class Job {
})
.then(() => {
if (this.isScrollInBottom) {
this.scrollDown();
scrollDown();
}
})
.then(() => this.toggleScroll());
......
......@@ -426,7 +426,7 @@ export default class LabelsSelect {
const tpl = _.template([
'<% _.each(labels, function(label){ %>',
'<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
'<span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
'<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
'<%- label.title %>',
'</span>',
'</a>',
......
import $ from 'jquery';
import { canScroll, isScrolledToBottom, toggleDisableButton } from './scroll_utils';
export default class LogOutputBehaviours {
constructor() {
// Scroll buttons
this.$scrollTopBtn = $('.js-scroll-up');
this.$scrollBottomBtn = $('.js-scroll-down');
this.$scrollTopBtn.off('click').on('click', this.scrollToTop.bind(this));
this.$scrollBottomBtn.off('click').on('click', this.scrollToBottom.bind(this));
}
toggleScroll() {
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = $(window).height();
if (canScroll()) {
if (currentPosition > 0 && scrollHeight - currentPosition !== windowHeight) {
// User is in the middle of the log
toggleDisableButton(this.$scrollTopBtn, false);
toggleDisableButton(this.$scrollBottomBtn, false);
} else if (currentPosition === 0) {
// User is at Top of Log
toggleDisableButton(this.$scrollTopBtn, true);
toggleDisableButton(this.$scrollBottomBtn, false);
} else if (isScrolledToBottom()) {
// User is at the bottom of the build log.
toggleDisableButton(this.$scrollTopBtn, false);
toggleDisableButton(this.$scrollBottomBtn, true);
}
} else {
toggleDisableButton(this.$scrollTopBtn, true);
toggleDisableButton(this.$scrollBottomBtn, true);
}
}
toggleScrollAnimation(toggle) {
this.$scrollBottomBtn.toggleClass('animate', toggle);
}
}
import $ from 'jquery';
export const canScroll = () => $(document).height() > $(window).height();
/**
* Checks if the entire page is scrolled down all the way to the bottom
*/
export const isScrolledToBottom = () => {
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = $(window).height();
return scrollHeight - currentPosition === windowHeight;
};
export const scrollDown = () => {
const $document = $(document);
$document.scrollTop($document.height());
};
export const toggleDisableButton = ($button, disable) => {
if (disable && $button.prop('disabled')) return;
$button.prop('disabled', disable);
};
export default {};
......@@ -14,6 +14,7 @@ export const EPIC_NOTEABLE_TYPE = 'epic';
export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post';
export const DESCRIPTION_TYPE = 'changed the description';
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
......
import { n__, s__, sprintf } from '~/locale';
import { DESCRIPTION_TYPE } from '../constants';
/**
* Changes the description from a note, returns 'changed the description n number of times'
*/
export const changeDescriptionNote = (note, descriptionChangedTimes, timeDifferenceMinutes) => {
const descriptionNote = Object.assign({}, note);
descriptionNote.note_html = sprintf(
s__(`MergeRequest|
%{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}`),
{
paragraphStart: '<p dir="auto">',
paragraphEnd: '</p>',
descriptionChangedTimes,
timeDifferenceMinutes: n__('within %d minute ', 'within %d minutes ', timeDifferenceMinutes),
},
false,
);
descriptionNote.times_updated = descriptionChangedTimes;
return descriptionNote;
};
/**
* Checks the time difference between two notes from their 'created_at' dates
* returns an integer
*/
export const getTimeDifferenceMinutes = (noteBeggining, noteEnd) => {
const descriptionNoteBegin = new Date(noteBeggining.created_at);
const descriptionNoteEnd = new Date(noteEnd.created_at);
const timeDifferenceMinutes = (descriptionNoteEnd - descriptionNoteBegin) / 1000 / 60;
return Math.ceil(timeDifferenceMinutes);
};
/**
* Checks if a note is a system note and if the content is description
*
* @param {Object} note
* @returns {Boolean}
*/
export const isDescriptionSystemNote = note => note.system && note.note === DESCRIPTION_TYPE;
/**
* Collapses the system notes of a description type, e.g. Changed the description, n minutes ago
* the notes will collapse as long as they happen no more than 10 minutes away from each away
* in between the notes can be anything, another type of system note
* (such as 'changed the weight') or a comment.
*
* @param {Array} notes
* @returns {Array}
*/
export const collapseSystemNotes = notes => {
let lastDescriptionSystemNote = null;
let lastDescriptionSystemNoteIndex = -1;
let descriptionChangedTimes = 1;
return notes.slice(0).reduce((acc, currentNote) => {
const note = currentNote.notes[0];
if (isDescriptionSystemNote(note)) {
// is it the first one?
if (!lastDescriptionSystemNote) {
lastDescriptionSystemNote = note;
lastDescriptionSystemNoteIndex = acc.length;
} else if (lastDescriptionSystemNote) {
const timeDifferenceMinutes = getTimeDifferenceMinutes(
lastDescriptionSystemNote,
note,
);
// are they less than 10 minutes appart?
if (timeDifferenceMinutes > 10) {
// reset counter
descriptionChangedTimes = 1;
// update the previous system note
lastDescriptionSystemNote = note;
lastDescriptionSystemNoteIndex = acc.length;
} else {
// increase counter
descriptionChangedTimes += 1;
// delete the previous one
acc.splice(lastDescriptionSystemNoteIndex, 1);
// replace the text of the current system note with the collapsed note.
currentNote.notes.splice(
0,
1,
changeDescriptionNote(note, descriptionChangedTimes, timeDifferenceMinutes),
);
// update the previous system note index
lastDescriptionSystemNoteIndex = acc.length;
}
}
}
acc.push(currentNote);
return acc;
}, []);
};
// for babel-rewire
export default {};
import _ from 'underscore';
import { collapseSystemNotes } from './collapse_utils';
export const notes = state => collapseSystemNotes(state.notes);
export const notes = state => state.notes;
export const targetNoteHash = state => state.targetNoteHash;
export const getNotesData = state => state.notesData;
......
......@@ -124,15 +124,18 @@
break;
}
},
hideOnSmallScreen(item) {
return !item.first && !item.last && !item.next && !item.prev && !item.active;
},
},
};
</script>
<template>
<div
v-if="showPagination"
class="gl-pagination"
class="gl-pagination prepend-top-default"
>
<ul class="pagination clearfix">
<ul class="pagination justify-content-center">
<li
v-for="(item, index) in getItems"
:key="index"
......@@ -142,12 +145,17 @@
'js-next-button': item.next,
'js-last-button': item.last,
'js-first-button': item.first,
'd-none d-md-block': hideOnSmallScreen(item),
separator: item.separator,
active: item.active,
disabled: item.disabled
disabled: item.disabled || item.separator
}"
class="page-item"
>
<a
@click.prevent="changePage(item.title, item.disabled)"
class="page-link"
>
<a @click.prevent="changePage(item.title, item.disabled)">
{{ item.title }}
</a>
</li>
......
......@@ -24,16 +24,54 @@ html {
font-size: 14px;
}
legend {
border-bottom: 1px solid $border-color;
margin-bottom: 20px;
}
button,
html [type="button"],
[type="reset"],
[type="submit"] {
[type="submit"],
[role="button"] {
// Override bootstrap reboot
-webkit-appearance: inherit;
cursor: pointer;
}
[role="button"] {
cursor: pointer;
h1,
h2,
h3,
h4,
h5,
h6 {
color: $gl-text-color;
font-weight: 600;
}
h1,
.h1,
h2,
.h2,
h3,
.h3 {
margin-top: 20px;
margin-bottom: 10px;
}
h4,
.h4,
h5,
.h5,
h6,
.h6 {
margin-top: 10px;
margin-bottom: 10px;
}
h5,
.h5 {
font-size: $gl-font-size;
}
input[type="file"] {
......@@ -59,6 +97,10 @@ a {
}
}
kbd {
display: inline-block;
}
code {
padding: 2px 4px;
color: $red-600;
......@@ -69,6 +111,11 @@ code {
background-color: inherit;
padding: unset;
}
.build-trace & {
background-color: inherit;
padding: inherit;
}
}
.code {
......@@ -177,8 +224,13 @@ table {
}
.nav-tabs {
// Override bootstrap's default border
border-bottom: 0;
.nav-link {
border: 0;
border-top: 0;
border-left: 0;
border-right: 0;
}
.nav-item {
......
......@@ -35,6 +35,12 @@
@include media-breakpoint-down(xs) {
width: 100%;
}
&.projects-dropdown-menu {
padding: 0;
overflow-y: initial;
max-height: initial;
}
}
.dropdown-toggle,
......
......@@ -35,7 +35,7 @@
}
&.active > a,
&.dropdown.open > a {
&.dropdown.show > a {
color: $color-900;
background-color: $color-alternate;
}
......@@ -74,7 +74,7 @@
}
&.active > a,
&.dropdown.open > a {
&.dropdown.show > a {
color: $color-900;
background-color: $color-alternate;
......
......@@ -297,12 +297,6 @@
display: flex;
margin: 0 0 0 6px;
.projects-dropdown-menu {
padding: 0;
overflow-y: initial;
max-height: initial;
}
.dropdown-chevron {
position: relative;
top: -1px;
......
......@@ -115,9 +115,3 @@ body {
.with-performance-bar .layout-page {
margin-top: $header-height + $performance-bar-height;
}
.vertical-center {
min-height: 100vh;
display: flex;
align-items: center;
}
.gl-pagination {
text-align: center;
border-top: 1px solid $border-color;
margin: 0;
margin-top: 0;
.pagination {
padding: 0;
margin: 20px 0;
a {
cursor: pointer;
}
.separator,
.separator:hover {
a {
cursor: default;
background-color: $gray-light;
padding: $gl-vert-padding;
}
}
}
.gap,
.gap:hover {
background-color: $gray-light;
padding: $gl-vert-padding;
cursor: default;
}
}
.card > .gl-pagination {
margin: 0;
}
/**
* Extra-small screen pagination.
*/
@media (max-width: 320px) {
.gl-pagination {
.first,
.last {
display: none;
}
.page-item {
display: none;
&.active {
display: inline;
}
}
}
}
/**
* Small screen pagination
*/
@include media-breakpoint-down(xs) {
.gl-pagination {
.pagination li a {
padding: 6px 10px;
}
.page-item {
display: none;
&.active {
display: inline;
}
}
}
}
/**
* Medium screen pagination
*/
@media (min-width: map-get($grid-breakpoints, xs)) and (max-width: map-get($grid-breakpoints, sm)) {
.gl-pagination {
.page-item {
display: none;
&.active,
&.sibling {
display: inline;
}
}
color: inherit;
text-decoration: none;
}
}
......@@ -11,15 +11,15 @@
padding-top: $gl-padding;
}
.panel {
.panel-heading {
.card {
.card-header {
display: -webkit-flex;
display: flex;
align-items: center;
justify-content: space-between;
line-height: $line-height-base;
.title {
.card-title {
display: flex;
align-items: center;
......@@ -34,6 +34,8 @@
.navbar-collapse {
padding-right: 0;
flex-grow: 0;
flex-basis: auto;
.navbar-nav {
margin: 0;
......
......@@ -114,20 +114,20 @@
font-size: 0.95em;
}
blockquote,
.blockquote {
color: $gl-grayish-blue;
font-size: inherit;
padding: 8px 24px;
margin: 16px 0;
border-left: 3px solid $white-dark;
}
.blockquote:dir(rtl) {
&:dir(rtl) {
border-left: 0;
border-right: 3px solid $white-dark;
}
.blockquote p {
p {
color: $gl-grayish-blue !important;
font-size: inherit;
line-height: 1.5;
......@@ -136,6 +136,7 @@
margin: 0;
}
}
}
p {
color: $gl-text-color;
......
......@@ -806,6 +806,7 @@ $border-color-settings: #e1e1e1;
Modals
*/
$modal-body-height: 134px;
$modal-border-color: #e9ecef;
/*
Prometheus
......
......@@ -138,6 +138,7 @@ pre {
margin: 0;
}
blockquote,
.blockquote {
color: $gl-grayish-blue;
padding: 0 0 0 15px;
......
......@@ -125,6 +125,7 @@
align-items: center;
svg {
width: 15px;
height: 15px;
display: block;
fill: $gl-text-color;
......@@ -159,7 +160,12 @@
}
}
.btn-scroll:disabled {
.btn-refresh {
border-radius: 4px;
}
.btn-scroll:disabled,
.btn-refresh:disabled {
opacity: 0.35;
cursor: not-allowed;
}
......@@ -447,3 +453,14 @@
right: 0;
margin-top: -17px;
}
@include media-breakpoint-down(sm) {
.top-bar {
.truncated-info {
white-space: nowrap;
overflow: hidden;
max-width: 220px;
text-overflow: ellipsis;
}
}
}
......@@ -13,6 +13,10 @@
max-width: 100%;
}
.clusters-error-alert {
width: 100%;
}
.clusters-container {
.nav-bar-right {
padding: $gl-padding-top $gl-padding;
......
......@@ -232,6 +232,13 @@
&-running {
background-color: $green-100;
border-color: $green-400;
// EE-specific start
&:hover {
background-color: $green-300;
border-color: $green-500;
}
// EE-specific end
}
&-succeeded {
......
......@@ -489,6 +489,15 @@
.sidebar-collapsed-user {
padding-bottom: 0;
margin-bottom: 10px;
.author_link {
padding-left: 0;
.avatar {
position: static;
margin: 0;
}
}
}
.issuable-header-btn {
......
......@@ -161,6 +161,35 @@ ul.related-merge-requests > li {
}
.issues-export-modal {
.modal-header {
justify-content: flex-start;
.export-svg-container {
flex-grow: 1;
> svg {
float: right;
}
}
}
.modal-body {
padding: 0;
.export-modal-subheader {
justify-content: flex-start;
align-items: center;
border-bottom: 1px solid $modal-border-color;
padding: 14px;
}
.export-modal-text {
padding: 24px 16px;
line-height: 1.5;
min-height: $modal-body-height;
}
}
.export-svg-container {
height: 56px;
padding: 10px 10px 0;
......
......@@ -196,6 +196,10 @@
.prioritized-labels {
margin-bottom: 30px;
h5 {
font-size: $gl-font-size;
}
.add-priority {
display: none;
color: $gray-light;
......@@ -210,6 +214,10 @@
}
.other-labels {
h5 {
font-size: $gl-font-size;
}
.remove-priority {
display: none;
}
......
......@@ -183,7 +183,7 @@
svg {
position: relative;
top: -1px;
top: -2px;
}
.ide-file-changed-icon {
......@@ -458,6 +458,10 @@
width: auto;
margin-right: 0;
a {
height: 60px;
}
a:hover,
a:focus {
text-decoration: none;
......@@ -718,9 +722,17 @@
}
.ide-new-btn {
.btn {
padding-top: 3px;
padding-bottom: 3px;
}
.dropdown {
display: flex;
}
.dropdown-toggle svg {
margin-top: -2px;
margin-bottom: 2px;
top: 0;
}
.dropdown-menu {
......@@ -877,6 +889,7 @@
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
outline: 0;
cursor: pointer;
svg {
margin: 0 auto;
......
......@@ -174,7 +174,7 @@
.option-description,
.option-disabled-reason {
margin-left: 45px;
margin-left: 30px;
color: $project-option-descr-color;
}
......
......@@ -22,9 +22,9 @@
header,
nav,
nav.main-nav,
nav.navbar-collapse,
nav.navbar-collapse.collapse,
.nav-sidebar,
.profiler-results,
.tree-ref-holder,
.tree-holder .breadcrumb,
......@@ -38,7 +38,8 @@ ul.notes-form,
.edit-link,
.note-action-button,
.right-sidebar,
.flash-container {
.flash-container,
#js-peek {
display: none !important;
}
......
class GraphqlController < ApplicationController
# Unauthenticated users have access to the API for public data
skip_before_action :authenticate_user!
before_action :check_graphql_feature_flag!
def execute
variables = Gitlab::Graphql::Variables.new(params[:variables]).to_h
query = params[:query]
operation_name = params[:operationName]
context = {
current_user: current_user
}
result = GitlabSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
end
rescue_from StandardError do |exception|
log_exception(exception)
render_error("Internal server error")
end
rescue_from Gitlab::Graphql::Variables::Invalid do |exception|
render_error(exception.message, status: :unprocessable_entity)
end
private
# Overridden from the ApplicationController to make the response look like
# a GraphQL response. That is nicely picked up in Graphiql.
def render_404
render_error("Not found!", status: :not_found)
end
def render_error(message, status: 500)
error = { errors: [message: message] }
render json: error, status: status
end
def check_graphql_feature_flag!
render_404 unless Feature.enabled?(:graphql)
end
end
......@@ -5,13 +5,12 @@ class Groups::GroupMembersController < Groups::ApplicationController
include MembersPresentation
include SortingHelper
# Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access, :update, :override]
before_action :authorize_update_group_member!, only: [:update, :override]
def self.admin_not_required_endpoints
%i[index leave request_access]
end
skip_cross_project_access_check :index, :create, :update, :destroy, :request_access,
:approve_access_request, :leave, :resend_invite,
:override
# Authorize
before_action :authorize_admin_group_member!, except: admin_not_required_endpoints
skip_cross_project_access_check :index, :create, :update, :destroy, :request_access,
:approve_access_request, :leave, :resend_invite,
......
class Groups::MilestonesController < Groups::ApplicationController
prepend EE::Groups::MilestonesController
include MilestoneActions
before_action :group_projects
......@@ -77,17 +79,14 @@ class Groups::MilestonesController < Groups::ApplicationController
def milestones
milestones = MilestonesFinder.new(search_params).execute
legacy_milestones =
if params[:only_group_milestones]
[]
else
GroupMilestone.build_collection(group, group_projects, params)
end
@sort = params[:sort] || 'due_date_asc'
MilestoneArray.sort(milestones + legacy_milestones, @sort)
end
def legacy_milestones
GroupMilestone.build_collection(group, group_projects, params)
end
def milestone
@milestone =
if params[:title]
......
......@@ -24,7 +24,9 @@ module Groups
# Make the `search` param consistent for the frontend,
# which will be using `filter`.
params[:search] ||= params[:filter] if params[:filter]
params.permit(:sort, :search)
# Don't show archived projects
params[:non_archived] = true
params.permit(:sort, :search, :non_archived)
end
end
end
......
......@@ -9,6 +9,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index]
prepend ::EE::Projects::EnvironmentsController
def index
@environments = project.environments
.with_state(params[:scope] || :available)
......
......@@ -18,7 +18,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
def upload_authorize
set_workhorse_internal_api_content_type
authorized = LfsObjectUploader.workhorse_authorize
authorized = LfsObjectUploader.workhorse_authorize(has_length: true)
authorized.merge!(LfsOid: oid, LfsSize: size)
render json: authorized
......
class Projects::MilestonesController < Projects::ApplicationController
include Gitlab::Utils::StrongMemoize
include MilestoneActions
before_action :check_issuables_available!
......@@ -103,7 +104,7 @@ class Projects::MilestonesController < Projects::ApplicationController
protected
def milestones
@milestones ||= begin
strong_memoize(:milestones) do
MilestonesFinder.new(search_params).execute
end
end
......@@ -121,10 +122,10 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def search_params
if @project.group && can?(current_user, :read_group, @project.group)
group = @project.group
if request.format.json? && @project.group && can?(current_user, :read_group, @project.group)
groups = @project.group.self_and_ancestors
end
params.permit(:state).merge(project_ids: @project.id, group_ids: group&.id)
params.permit(:state).merge(project_ids: @project.id, group_ids: groups&.select(:id))
end
end
......@@ -25,8 +25,6 @@ class Projects::PipelinesController < Projects::ApplicationController
@finished_count = limited_pipelines_count(project, 'finished')
@pipelines_count = limited_pipelines_count(project)
Gitlab::Ci::Pipeline::Preloader.preload(@pipelines)
respond_to do |format|
format.html
format.json do
......@@ -36,7 +34,7 @@ class Projects::PipelinesController < Projects::ApplicationController
pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user)
.with_pagination(request, response)
.represent(@pipelines, disable_coverage: true),
.represent(@pipelines, disable_coverage: true, preload: true),
count: {
all: @pipelines_count,
running: @running_count,
......
......@@ -13,6 +13,10 @@ module Users
def index
@redirect = redirect_path
if @term.accepted_by_user?(current_user)
flash.now[:notice] = "You have already accepted the Terms of Service as #{current_user.to_reference}"
end
end
def accept
......
module Functions
class BaseFunction < GraphQL::Function
end
end
module Functions
class Echo < BaseFunction
argument :text, GraphQL::STRING_TYPE
description "Testing endpoint to validate the API with"
def call(obj, args, ctx)
username = ctx[:current_user]&.username
"#{username.inspect} says: #{args[:text]}"
end
end
end
class GitlabSchema < GraphQL::Schema
use BatchLoader::GraphQL
use Gitlab::Graphql::Authorize
use Gitlab::Graphql::Present
query(Types::QueryType)
# mutation(Types::MutationType)
end
module Resolvers
class BaseResolver < GraphQL::Schema::Resolver
end
end
module Resolvers
module FullPathResolver
extend ActiveSupport::Concern
prepended do
argument :full_path, GraphQL::ID_TYPE,
required: true,
description: 'The full path of the project or namespace, e.g., "gitlab-org/gitlab-ce"'
end
def model_by_full_path(model, full_path)
BatchLoader.for(full_path).batch(key: "#{model.model_name.param_key}:full_path") do |full_paths, loader|
# `with_route` avoids an N+1 calculating full_path
results = model.where_full_path_in(full_paths).with_route
results.each { |project| loader.call(project.full_path, project) }
end
end
end
end
module Resolvers
class MergeRequestResolver < BaseResolver
prepend FullPathResolver
type Types::ProjectType, null: true
argument :iid, GraphQL::ID_TYPE,
required: true,
description: 'The IID of the merge request, e.g., "1"'
def resolve(full_path:, iid:)
project = model_by_full_path(Project, full_path)
return unless project.present?
BatchLoader.for(iid.to_s).batch(key: project.id) do |iids, loader|
results = project.merge_requests.where(iid: iids)
results.each { |mr| loader.call(mr.iid.to_s, mr) }
end
end
end
end
module Resolvers
class ProjectResolver < BaseResolver
prepend FullPathResolver
type Types::ProjectType, null: true
def resolve(full_path:)
model_by_full_path(Project, full_path)
end
end
end
module Types
class BaseEnum < GraphQL::Schema::Enum
end
end
module Types
class BaseField < GraphQL::Schema::Field
prepend Gitlab::Graphql::Authorize
end
end
module Types
class BaseInputObject < GraphQL::Schema::InputObject
end
end
module Types
module BaseInterface
include GraphQL::Schema::Interface
end
end
module Types
class BaseObject < GraphQL::Schema::Object
prepend Gitlab::Graphql::Present
field_class Types::BaseField
end
end
module Types
class BaseScalar < GraphQL::Schema::Scalar
end
end
module Types
class BaseUnion < GraphQL::Schema::Union
end
end
module Types
class MergeRequestType < BaseObject
present_using MergeRequestPresenter
graphql_name 'MergeRequest'
field :id, GraphQL::ID_TYPE, null: false
field :iid, GraphQL::ID_TYPE, null: false
field :title, GraphQL::STRING_TYPE, null: false
field :description, GraphQL::STRING_TYPE, null: true
field :state, GraphQL::STRING_TYPE, null: true
field :created_at, Types::TimeType, null: false
field :updated_at, Types::TimeType, null: false
field :source_project, Types::ProjectType, null: true
field :target_project, Types::ProjectType, null: false
# Alias for target_project
field :project, Types::ProjectType, null: false
field :project_id, GraphQL::INT_TYPE, null: false, method: :target_project_id
field :source_project_id, GraphQL::INT_TYPE, null: true
field :target_project_id, GraphQL::INT_TYPE, null: false
field :source_branch, GraphQL::STRING_TYPE, null: false
field :target_branch, GraphQL::STRING_TYPE, null: false
field :work_in_progress, GraphQL::BOOLEAN_TYPE, method: :work_in_progress?, null: false
field :merge_when_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true
field :diff_head_sha, GraphQL::STRING_TYPE, null: true
field :merge_commit_sha, GraphQL::STRING_TYPE, null: true
field :user_notes_count, GraphQL::INT_TYPE, null: true
field :should_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :should_remove_source_branch?, null: true
field :force_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :force_remove_source_branch?, null: true
field :merge_status, GraphQL::STRING_TYPE, null: true
field :in_progress_merge_commit_sha, GraphQL::STRING_TYPE, null: true
field :merge_error, GraphQL::STRING_TYPE, null: true
field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true
field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false
field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true
field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false
field :diff_head_sha, GraphQL::STRING_TYPE, null: true
field :merge_commit_message, GraphQL::STRING_TYPE, null: true
field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false
field :source_branch_exists, GraphQL::BOOLEAN_TYPE, method: :source_branch_exists?, null: false
field :mergeable_discussions_state, GraphQL::BOOLEAN_TYPE, null: true
field :web_url, GraphQL::STRING_TYPE, null: true
field :upvotes, GraphQL::INT_TYPE, null: false
field :downvotes, GraphQL::INT_TYPE, null: false
field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false
end
end
module Types
class MutationType < BaseObject
graphql_name "Mutation"
# TODO: Add Mutations as fields
end
end
module Types
class ProjectType < BaseObject
graphql_name 'Project'
field :id, GraphQL::ID_TYPE, null: false
field :full_path, GraphQL::ID_TYPE, null: false
field :path, GraphQL::STRING_TYPE, null: false
field :name_with_namespace, GraphQL::STRING_TYPE, null: false
field :name, GraphQL::STRING_TYPE, null: false
field :description, GraphQL::STRING_TYPE, null: true
field :default_branch, GraphQL::STRING_TYPE, null: true
field :tag_list, GraphQL::STRING_TYPE, null: true
field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true
field :http_url_to_repo, GraphQL::STRING_TYPE, null: true
field :web_url, GraphQL::STRING_TYPE, null: true
field :star_count, GraphQL::INT_TYPE, null: false
field :forks_count, GraphQL::INT_TYPE, null: false
field :created_at, Types::TimeType, null: true
field :last_activity_at, Types::TimeType, null: true
field :archived, GraphQL::BOOLEAN_TYPE, null: true
field :visibility, GraphQL::STRING_TYPE, null: true
field :container_registry_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :shared_runners_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :merge_requests_ff_only_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (project, args, ctx) do
project.avatar_url(only_path: false)
end
%i[issues merge_requests wiki snippets].each do |feature|
field "#{feature}_enabled", GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do
project.feature_available?(feature, ctx[:current_user])
end
end
field :jobs_enabled, GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do
project.feature_available?(:builds, ctx[:current_user])
end
field :public_jobs, GraphQL::BOOLEAN_TYPE, method: :public_builds, null: true
field :open_issues_count, GraphQL::INT_TYPE, null: true, resolve: -> (project, args, ctx) do
project.open_issues_count if project.feature_available?(:issues, ctx[:current_user])
end
field :import_status, GraphQL::STRING_TYPE, null: true
field :ci_config_path, GraphQL::STRING_TYPE, null: true
field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true
field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true
field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true
end
end
module Types
class QueryType < BaseObject
graphql_name 'Query'
field :project, Types::ProjectType,
null: true,
resolver: Resolvers::ProjectResolver,
description: "Find a project" do
authorize :read_project
end
field :merge_request, Types::MergeRequestType,
null: true,
resolver: Resolvers::MergeRequestResolver,
description: "Find a merge request" do
authorize :read_merge_request
end
field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new
end
end
module Types
class TimeType < BaseScalar
graphql_name 'Time'
description 'Time represented in ISO 8601'
def self.coerce_input(value, ctx)
Time.parse(value)
end
def self.coerce_result(value, ctx)
value.iso8601
end
end
end
......@@ -37,7 +37,7 @@ module ApplicationSettingsHelper
# Return a group of checkboxes that use Bootstrap's button plugin for a
# toggle button effect.
def restricted_level_checkboxes(help_block_id, checkbox_name)
def restricted_level_checkboxes(help_block_id, checkbox_name, options = {})
Gitlab::VisibilityLevel.values.map do |level|
checked = restricted_visibility_levels(true).include?(level)
css_class = checked ? 'active' : ''
......@@ -47,6 +47,7 @@ module ApplicationSettingsHelper
check_box_tag(checkbox_name, level, checked,
autocomplete: 'off',
'aria-describedby' => help_block_id,
'class' => options[:class],
id: tag_name) + visibility_level_icon(level) + visibility_level_label(level)
end
end
......@@ -54,7 +55,7 @@ module ApplicationSettingsHelper
# Return a group of checkboxes that use Bootstrap's button plugin for a
# toggle button effect.
def import_sources_checkboxes(help_block_id)
def import_sources_checkboxes(help_block_id, options = {})
Gitlab::ImportSources.options.map do |name, source|
checked = Gitlab::CurrentSettings.import_sources.include?(source)
css_class = checked ? 'active' : ''
......@@ -64,6 +65,7 @@ module ApplicationSettingsHelper
check_box_tag(checkbox_name, source, checked,
autocomplete: 'off',
'aria-describedby' => help_block_id,
'class' => options[:class],
id: name.tr(' ', '_')) + name
end
end
......
......@@ -240,6 +240,14 @@ module ProjectsHelper
"git push --set-upstream #{repository_url}/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)"
end
def show_xcode_link?(project = @project)
browser.platform.mac? && project.repository.xcode_project?
end
def xcode_uri_to_repo(project = @project)
"xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}"
end
private
def get_project_nav_tabs(project, current_user)
......@@ -455,7 +463,10 @@ module ProjectsHelper
exports_path = File.join(Settings.shared['path'], 'tmp/project_exports')
filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]")
disk_path = Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path
disk_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path
end
filtered_message.gsub(disk_path.chomp('/'), "[REPOS PATH]")
end
......
class ApplicationSetting
class Term < ActiveRecord::Base
include CacheMarkdownField
has_many :term_agreements
validates :terms, presence: true
......@@ -9,5 +10,10 @@ class ApplicationSetting
def self.latest
order(:id).last
end
def accepted_by_user?(user)
user.accepted_term_id == id ||
term_agreements.accepted.where(user: user).exists?
end
end
end
......@@ -59,6 +59,11 @@ module Ci
where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
end
scope :without_archived_trace, ->() do
where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
end
scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
......@@ -148,6 +153,7 @@ module Ci
after_transition any => [:success] do |build|
build.run_after_commit do
BuildSuccessWorker.perform_async(id)
PagesWorker.perform_async(:deploy, id) if build.pages_generator?
end
end
......@@ -187,6 +193,11 @@ module Ci
pipeline.manual_actions.where.not(name: name)
end
def pages_generator?
Gitlab.config.pages.enabled &&
self.name == 'pages'
end
def playable?
action? && (manual? || retryable?)
end
......@@ -406,8 +417,6 @@ module Ci
build_data = Gitlab::DataBuilder::Build.build(self)
project.execute_hooks(build_data.dup, :job_hooks)
project.execute_services(build_data.dup, :job_hooks)
PagesService.new(build_data).execute
project.running_or_pending_build_count(force: true)
end
def browsable_artifacts?
......
......@@ -31,6 +31,14 @@ module Ci
end
end
def self.fabricate(stage)
stage.statuses.ordered.latest
.sort_by(&:sortable_name).group_by(&:group_name)
.map do |group_name, grouped_statuses|
self.new(stage, name: group_name, jobs: grouped_statuses)
end
end
private
def commit_statuses
......
......@@ -16,11 +16,7 @@ module Ci
end
def groups
@groups ||= statuses.ordered.latest
.sort_by(&:sortable_name).group_by(&:group_name)
.map do |group_name, grouped_statuses|
Ci::Group.new(self, name: group_name, jobs: grouped_statuses)
end
@groups ||= Ci::Group.fabricate(self)
end
def to_param
......
......@@ -29,7 +29,7 @@ module Ci
has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
has_many :stages
has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
......@@ -267,6 +267,20 @@ module Ci
stage unless stage.statuses_count.zero?
end
##
# TODO We do not completely switch to persisted stages because of
# race conditions with setting statuses gitlab-ce#23257.
#
def ordered_stages
return legacy_stages unless complete?
if Feature.enabled?('ci_pipeline_persisted_stages')
stages
else
legacy_stages
end
end
def legacy_stages
# TODO, this needs refactoring, see gitlab-ce#26481.
......@@ -429,7 +443,7 @@ module Ci
def number_of_warnings
BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader|
Build.where(commit_id: pipeline_ids)
::Ci::Build.where(commit_id: pipeline_ids)
.latest
.failed_but_allowed
.group(:commit_id)
......@@ -525,7 +539,8 @@ module Ci
def update_status
retry_optimistic_lock(self) do
case latest_builds_status
case latest_builds_status.to_s
when 'created' then nil
when 'pending' then enqueue
when 'running' then run
when 'success' then succeed
......@@ -533,6 +548,9 @@ module Ci
when 'canceled' then cancel
when 'skipped' then skip
when 'manual' then block
else
raise HasStatus::UnknownStatusError,
"Unknown status `#{latest_builds_status}`"
end
end
end
......
......@@ -220,10 +220,8 @@ module Ci
cache_attributes(values)
if persist_cached_data?
self.assign_attributes(values)
self.save if self.changed?
end
# We save data without validation, it will always change due to `contacted_at`
self.update_columns(values) if persist_cached_data?
end
def pick_build!(build)
......
......@@ -68,16 +68,44 @@ module Ci
def update_status
retry_optimistic_lock(self) do
case statuses.latest.status
when 'created' then nil
when 'pending' then enqueue
when 'running' then run
when 'success' then succeed
when 'failed' then drop
when 'canceled' then cancel
when 'manual' then block
when 'skipped' then skip
else skip
when 'skipped', nil then skip
else
raise HasStatus::UnknownStatusError,
"Unknown status `#{statuses.latest.status}`"
end
end
end
def groups
@groups ||= Ci::Group.fabricate(self)
end
def has_warnings?
number_of_warnings.positive?
end
def number_of_warnings
BatchLoader.for(id).batch(default_value: 0) do |stage_ids, loader|
::Ci::Build.where(stage_id: stage_ids)
.latest
.failed_but_allowed
.group(:stage_id)
.count
.each { |id, amount| loader.call(id, amount) }
end
end
def detailed_status(current_user)
Gitlab::Ci::Status::Stage::Factory
.new(self, current_user)
.fabricate!
end
end
end
......@@ -11,6 +11,8 @@ module HasStatus
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze
UnknownStatusError = Class.new(StandardError)
class_methods do
def status_sql
scope_relevant = respond_to?(:exclude_ignored) ? exclude_ignored : all
......
......@@ -9,8 +9,8 @@ module ProtectedRefAccess
].freeze
HUMAN_ACCESS_LEVELS = {
Gitlab::Access::MASTER => "Masters".freeze,
Gitlab::Access::DEVELOPER => "Developers + Masters".freeze,
Gitlab::Access::MASTER => "Maintainers".freeze,
Gitlab::Access::DEVELOPER => "Developers + Maintainers".freeze,
Gitlab::Access::NO_ACCESS => "No one".freeze
}.freeze
......
......@@ -235,6 +235,7 @@ class Project < ActiveRecord::Base
has_many :commit_statuses
has_many :pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
# Ci::Build objects store data on the file system such as artifact files and
# build traces. Currently there's no efficient way of removing this data in
......@@ -1443,8 +1444,14 @@ class Project < ActiveRecord::Base
Ci::Runner.from("(#{union.to_sql}) ci_runners")
end
def active_runners
strong_memoize(:active_runners) do
all_runners.active
end
end
def any_runners?(&block)
all_runners.active.any?(&block)
active_runners.any?(&block)
end
def valid_runners_token?(token)
......@@ -1667,12 +1674,6 @@ class Project < ActiveRecord::Base
import_state.update_column(:jid, nil)
end
def running_or_pending_build_count(force: false)
Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
builds.running_or_pending.count(:all)
end
end
# Lazy loading of the `pipeline_status` attribute
def pipeline_status
@pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
......
......@@ -7,7 +7,7 @@ class ProtectedBranch < ActiveRecord::Base
protected_ref_access_levels :merge, :push
def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
# Masters, owners and admins are allowed to create the default branch
# Maintainers, owners and admins are allowed to create the default branch
if default_branch_protected? && project.empty_repo?
return true if user.admin? || project.team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
end
......
......@@ -277,6 +277,16 @@ class Repository
end
end
def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:)
raw_repository.archive_metadata(
ref,
storage_path,
project.path,
format,
append_sha: append_sha
)
end
def expire_tags_cache
expire_method_caches(%i(tag_names tag_count))
@tags = nil
......
......@@ -2,5 +2,7 @@ class TermAgreement < ActiveRecord::Base
belongs_to :term, class_name: 'ApplicationSetting::Term'
belongs_to :user
scope :accepted, -> { where(accepted: true) }
validates :user, :term, presence: true
end
......@@ -46,7 +46,7 @@ class ProjectPolicy < BasePolicy
desc "User has developer access"
condition(:developer) { team_access_level >= Gitlab::Access::DEVELOPER }
desc "User has master access"
desc "User has maintainer access"
condition(:master) { team_access_level >= Gitlab::Access::MASTER }
desc "Project is public"
......
......@@ -181,6 +181,25 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
.can_push_to_branch?(source_branch)
end
def mergeable_discussions_state
# This avoids calling MergeRequest#mergeable_discussions_state without
# considering the state of the MR first. If a MR isn't mergeable, we can
# safely short-circuit it.
if merge_request.mergeable_state?(skip_ci_check: true, skip_discussions_check: true)
merge_request.mergeable_discussions_state?
else
false
end
end
def web_url
Gitlab::UrlBuilder.build(merge_request)
end
def subscribed?
merge_request.subscribed?(current_user, merge_request.target_project)
end
private
def cached_can_be_reverted?
......
class EnvironmentEntity < Grape::Entity
include RequestAwareEntity
prepend ::EE::EnvironmentEntity
expose :id
expose :name
expose :state
......
class PipelineDetailsEntity < PipelineEntity
expose :details do
expose :legacy_stages, as: :stages, using: StageEntity
expose :ordered_stages, as: :stages, using: StageEntity
expose :artifacts, using: BuildArtifactEntity
expose :manual_actions, using: BuildActionEntity
end
......
class PipelineSerializer < BaseSerializer
include WithPagination
InvalidResourceError = Class.new(StandardError)
entity PipelineDetailsEntity
def represent(resource, opts = {})
if resource.is_a?(ActiveRecord::Relation)
resource = resource.preload([
:stages,
:retryable_builds,
:cancelable_statuses,
:trigger_requests,
......@@ -22,10 +19,14 @@ class PipelineSerializer < BaseSerializer
end
if paginated?
super(@paginator.paginate(resource), opts)
else
super(resource, opts)
resource = paginator.paginate(resource)
end
if opts.delete(:preload)
resource = Gitlab::Ci::Pipeline::Preloader.preload!(resource)
end
super(resource, opts)
end
def represent_status(resource)
......@@ -38,7 +39,7 @@ class PipelineSerializer < BaseSerializer
def represent_stages(resource)
return {} unless resource.present?
data = represent(resource, { only: [{ details: [:stages] }] })
data = represent(resource, { only: [{ details: [:stages] }], preload: true })
data.dig(:details, :stages) || []
end
end
......@@ -26,7 +26,7 @@ module Lfs
success(lock: lock, http_status: :ok)
elsif forced
error(_('You must have master access to force delete a lock'), 403)
error(_('You must have maintainer access to force delete a lock'), 403)
else
error(_("%{lock_path} is locked by GitLab User %{lock_user_id}") % { lock_path: lock.path, lock_user_id: lock.user_id }, 403)
end
......
class PagesService
attr_reader :data
def initialize(data)
@data = data
end
def execute
return unless Settings.pages.enabled
return unless data[:build_name] == 'pages'
return unless data[:build_status] == 'success'
PagesWorker.perform_async(:deploy, data[:build_id])
end
end
......@@ -11,7 +11,7 @@ module Projects
order: { due_date: :asc, title: :asc }
}
finder_params[:group_ids] = [@project.group.id] if @project.group
finder_params[:group_ids] = @project.group.self_and_ancestors.select(:id) if @project.group
MilestonesFinder.new(finder_params).execute.select([:iid, :title])
end
......
......@@ -48,6 +48,9 @@ module Projects
yield(@project) if block_given?
# If the block added errors, don't try to save the project
return @project if @project.errors.any?
@project.creator = current_user
if forked_from_project_id
......
......@@ -21,6 +21,9 @@ module Projects
yield if block_given?
# If the block added errors, don't try to save the project
return validation_failed! if project.errors.any?
if project.update_attributes(params.except(:default_branch))
if project.previous_changes.include?('path')
project.rename_repo
......@@ -32,21 +35,25 @@ module Projects
success
else
model_errors = project.errors.full_messages.to_sentence
error_message = model_errors.presence || 'Project could not be updated!'
error(error_message)
validation_failed!
end
end
def run_auto_devops_pipeline?
return false if project.repository.gitlab_ci_yml || !project.auto_devops.previous_changes.include?('enabled')
return false if project.repository.gitlab_ci_yml || !project.auto_devops&.previous_changes&.include?('enabled')
project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled?)
end
private
def validation_failed!
model_errors = project.errors.full_messages.to_sentence
error_message = model_errors.presence || 'Project could not be updated!'
error(error_message)
end
def renaming_project_with_container_registry_tags?
new_path = params[:path]
......
......@@ -10,8 +10,6 @@ module ObjectStorage
UnknownStoreError = Class.new(StandardError)
ObjectStorageUnavailable = Class.new(StandardError)
DIRECT_UPLOAD_TIMEOUT = 4.hours
DIRECT_UPLOAD_EXPIRE_OFFSET = 15.minutes
TMP_UPLOAD_PATH = 'tmp/uploads'.freeze
module Store
......@@ -157,9 +155,9 @@ module ObjectStorage
model_class.uploader_options.dig(mount_point, :mount_on) || mount_point
end
def workhorse_authorize
def workhorse_authorize(has_length:, maximum_size: nil)
{
RemoteObject: workhorse_remote_upload_options,
RemoteObject: workhorse_remote_upload_options(has_length: has_length, maximum_size: maximum_size),
TempPath: workhorse_local_upload_path
}.compact
end
......@@ -168,23 +166,16 @@ module ObjectStorage
File.join(self.root, TMP_UPLOAD_PATH)
end
def workhorse_remote_upload_options
def workhorse_remote_upload_options(has_length:, maximum_size: nil)
return unless self.object_store_enabled?
return unless self.direct_upload_enabled?
id = [CarrierWave.generate_cache_id, SecureRandom.hex].join('-')
upload_path = File.join(TMP_UPLOAD_PATH, id)
connection = ::Fog::Storage.new(self.object_store_credentials)
expire_at = Time.now + DIRECT_UPLOAD_TIMEOUT + DIRECT_UPLOAD_EXPIRE_OFFSET
options = { 'Content-Type' => 'application/octet-stream' }
direct_upload = ObjectStorage::DirectUpload.new(self.object_store_credentials, remote_store_path, upload_path,
has_length: has_length, maximum_size: maximum_size)
{
ID: id,
Timeout: DIRECT_UPLOAD_TIMEOUT,
GetURL: connection.get_object_url(remote_store_path, upload_path, expire_at),
DeleteURL: connection.delete_object_url(remote_store_path, upload_path, expire_at),
StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options)
}
direct_upload.to_hash.merge(ID: id)
end
end
......
......@@ -24,7 +24,7 @@
.col-sm-10
- checkbox_name = 'application_setting[restricted_visibility_levels][]'
= hidden_field_tag(checkbox_name)
- restricted_level_checkboxes('restricted-visibility-help', checkbox_name).each do |level|
- restricted_level_checkboxes('restricted-visibility-help', checkbox_name, class: 'form-check-input').each do |level|
.form-check
= level
%span.form-text.text-muted#restricted-visibility-help
......@@ -34,7 +34,7 @@
= f.label :import_sources, class: 'col-form-label col-sm-2'
.col-sm-10
= hidden_field_tag 'application_setting[import_sources][]'
- import_sources_checkboxes('import-sources-help').each do |source|
- import_sources_checkboxes('import-sources-help', class: 'form-check-input').each do |source|
.form-check= source
%span.form-text.text-muted#import-sources-help
Enabled sources for code import during project creation. OmniAuth must be configured for GitHub
......
......@@ -118,8 +118,8 @@
.card-body
= form_for @project, url: transfer_admin_project_path(@project), method: :put do |f|
.form-group.row
= f.label :new_namespace_id, "Namespace", class: 'col-form-label col-sm-2'
.col-sm-10
= f.label :new_namespace_id, "Namespace", class: 'col-form-label col-sm-3'
.col-sm-9
.dropdown
= dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' })
.dropdown-menu.dropdown-select
......@@ -129,7 +129,7 @@
= dropdown_loading
.form-group.row
.offset-sm-2.col-sm-10
.offset-sm-3.col-sm-9
= f.submit 'Transfer', class: 'btn btn-primary'
.card.repository-check
......
.nav-block
.nav-block.activities
.controls
= link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do
%i.fa.fa-rss
......
......@@ -7,7 +7,7 @@
.settings-header
%h4
= _('Variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.btn-default.js-settings-toggle{ type: "button" }
= expanded ? _('Collapse') : _('Expand')
%p.append-bottom-0
......
This diff is collapsed.
......@@ -12,11 +12,11 @@
= form_tag personal_access_token_import_gitea_path do
.form-group.row
= label_tag :gitea_host_url, 'Gitea Host URL', class: 'col-form-label col-sm-8'
= label_tag :gitea_host_url, 'Gitea Host URL', class: 'col-form-label col-sm-2'
.col-sm-4
= text_field_tag :gitea_host_url, nil, placeholder: 'https://try.gitea.io', class: 'form-control'
.form-group.row
= label_tag :personal_access_token, 'Personal Access Token', class: 'col-form-label col-sm-8'
= label_tag :personal_access_token, 'Personal Access Token', class: 'col-form-label col-sm-2'
.col-sm-4
= text_field_tag :personal_access_token, nil, class: 'form-control'
.form-actions
......
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.
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