Commit f6dcfd0d authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge remote-tracking branch 'origin-ee/master' into 3328-one-request-deploy-boards

parents a90478c6 65e847ac
...@@ -411,6 +411,7 @@ docs lint: ...@@ -411,6 +411,7 @@ docs lint:
before_script: [] before_script: []
script: script:
- scripts/lint-doc.sh - scripts/lint-doc.sh
- scripts/lint-changelog-yaml
- mv doc/ /nanoc/content/ - mv doc/ /nanoc/content/
- cd /nanoc - cd /nanoc
# Build HTML from Markdown # Build HTML from Markdown
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 10.0.3 (2017-10-05)
- [FIXED] Rewrite Geo database rake tasks so they operate on the correct database. !3052
- [FIXED] Show group tab if member lock is enabled.
- [FIXED] File uploaders do not perform hard check, only soft check.
- [FIXED] Only show Turn on Service Desk button when user has permissions.
- [FIXED] Fix EE delta size check handling with annotated tags.
## 10.0.2 (2017-09-27) ## 10.0.2 (2017-09-27)
- [FIXED] Send valid project path as name for Jira dev panel. - [FIXED] Send valid project path as name for Jira dev panel.
...@@ -53,6 +61,19 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -53,6 +61,19 @@ Please view this file on the master branch, on stable branches it's out of date.
- Add group issue boards. - Add group issue boards.
- Ports style changes fixed in a conflict in ce to ee upstream to master for new projects page. - Ports style changes fixed in a conflict in ce to ee upstream to master for new projects page.
## 9.5.8 (2017-10-04)
- [FIXED] Fix EE delta size check handling with annotated tags.
- [FIXED] Fix delta size check to handle commit or nil objects.
## 9.5.7 (2017-10-03)
- No changes.
## 9.5.6 (2017-09-29)
- [FIXED] Show group tab if member lock is enabled.
## 9.5.5 (2017-09-18) ## 9.5.5 (2017-09-18)
- [FIXED] Fixes activation of project mirror when new project is created. !2756 - [FIXED] Fixes activation of project mirror when new project is created. !2756
......
...@@ -2,6 +2,23 @@ ...@@ -2,6 +2,23 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.0.3 (2017-10-05)
- [FIXED] find_user Users helper method no longer overrides find_user API helper method. !14418
- [FIXED] Fix CSRF validation issue when closing/opening merge requests from the UI. !14555
- [FIXED] Kubernetes integration: ensure v1.8.0 compatibility. !14635
- [FIXED] Fixes data parameter not being sent in ajax request for jobs log.
- [FIXED] Improves UX of autodevops popover to match gpg one.
- [FIXED] Fixed commenting on side-by-side commit diff.
- [FIXED] Make sure API responds with 401 when invalid authentication info is provided.
- [FIXED] Fix merge request counter updates after merge.
- [FIXED] Fix gitlab-rake gitlab:import:repos task failing.
- [FIXED] Fix pushes to an empty repository not invalidating has_visible_content? cache.
- [FIXED] Ensure all refs are restored on a restore from backup.
- [FIXED] Gitaly RepositoryExists remains opt-in for all method calls.
- [FIXED] Fix 500 error on merged merge requests when GitLab is restored from a backup.
- [FIXED] Adjust MRs being stuck on "process of being merged" for more than 2 hours.
## 10.0.2 (2017-09-27) ## 10.0.2 (2017-09-27)
- [FIXED] Notes will not show an empty bubble when the author isn't a member. !14450 - [FIXED] Notes will not show an empty bubble when the author isn't a member. !14450
...@@ -195,6 +212,21 @@ entry. ...@@ -195,6 +212,21 @@ entry.
- Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi) - Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi)
- [BUGIFX] Improves subgroup creation permissions. !13418 - [BUGIFX] Improves subgroup creation permissions. !13418
## 9.5.8 (2017-10-04)
- [FIXED] Fixed fork button being disabled for users who can fork to a group.
## 9.5.7 (2017-10-03)
- Fix gitlab rake:import:repos task.
## 9.5.6 (2017-09-29)
- [FIXED] Fix MR ready to merge buttons/controls at mobile breakpoint. !14242
- [FIXED] Fix errors thrown in merge request widget with external CI service/integration.
- [FIXED] Update x/x discussions resolved checkmark icon to be green when all discussions resolved.
- [FIXED] Fix 500 error on merged merge requests when GitLab is restored from a backup.
## 9.5.5 (2017-09-18) ## 9.5.5 (2017-09-18)
- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller) - [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)
......
...@@ -50,7 +50,7 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ ...@@ -50,7 +50,7 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
Thank you for your interest in contributing to GitLab. This guide details how Thank you for your interest in contributing to GitLab. This guide details how
to contribute to GitLab in a way that is efficient for everyone. to contribute to GitLab in a way that is efficient for everyone.
Looking for something to work on? Look for the label [Accepting Merge Requests](#i-want-to-contribute). Looking for something to work on? Look for issues with the label [Accepting Merge Requests](#i-want-to-contribute).
GitLab comes into two flavors, GitLab Community Edition (CE) our free and open GitLab comes into two flavors, GitLab Community Edition (CE) our free and open
source edition, and GitLab Enterprise Edition (EE) which is our commercial source edition, and GitLab Enterprise Edition (EE) which is our commercial
...@@ -102,7 +102,7 @@ the remaining issues on the GitHub issue tracker. ...@@ -102,7 +102,7 @@ the remaining issues on the GitHub issue tracker.
## I want to contribute! ## I want to contribute!
If you want to contribute to GitLab, but are not sure where to start, If you want to contribute to GitLab, but are not sure where to start,
look for [issues with the label `Accepting Merge Requests` and weight < 5][accepting-mrs-weight]. look for [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight].
These issues will be of reasonable size and challenge, for anyone to start These issues will be of reasonable size and challenge, for anyone to start
contributing to GitLab. contributing to GitLab.
...@@ -210,8 +210,7 @@ We add the ~"Accepting Merge Requests" label to: ...@@ -210,8 +210,7 @@ We add the ~"Accepting Merge Requests" label to:
- Low priority ~bug issues (i.e. we do not add it to the bugs that we want to - Low priority ~bug issues (i.e. we do not add it to the bugs that we want to
solve in the ~"Next Patch Release") solve in the ~"Next Patch Release")
- Small ~"feature proposal" that do not need ~UX / ~"Product work", or for which - Small ~"feature proposal"
the ~UX / ~"Product work" is already done
- Small ~"technical debt" issues - Small ~"technical debt" issues
After adding the ~"Accepting Merge Requests" label, we try to estimate the After adding the ~"Accepting Merge Requests" label, we try to estimate the
...@@ -224,6 +223,17 @@ know how difficult the issue is. Additionally: ...@@ -224,6 +223,17 @@ know how difficult the issue is. Additionally:
- We encourage people that have never contributed to any open source project to - We encourage people that have never contributed to any open source project to
look for ["Accepting Merge Requests" issues with a weight of 1][firt-timers] look for ["Accepting Merge Requests" issues with a weight of 1][firt-timers]
If you've decided that you would like to work on an issue, please @-mention
the [appropriate product manager](https://about.gitlab.com/handbook/product/#who-to-talk-to-for-what)
as soon as possible. The product manager will then pull in appropriate GitLab team
members to further discuss scope, design, and technical considerations. This will
ensure that that your contribution is aligned with the GitLab product and minimize
any rework and delay in getting it merged into master.
GitLab team members who apply the ~"Accepting Merge Requests" label to an issue
should update the issue description with a responsible product manager, inviting
any potential community contributor to @-mention per above.
[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests&scope=all&sort=weight_asc&state=opened [up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests&scope=all&sort=weight_asc&state=opened
[firt-timers]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=Accepting+Merge+Requests&scope=all&sort=upvotes_desc&state=opened&weight=1 [firt-timers]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=Accepting+Merge+Requests&scope=all&sort=upvotes_desc&state=opened&weight=1
......
{"iconCount":134,"icons":["abuse","account","admin","angle-double-left","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","calendar","cancel","chevron-down","chevron-left","chevron-right","chevron-up","clock","code","comment-dots","comment-next","comment","comments","commit","credit-card","disk","doc_code","doc_image","doc_text","download","duplicate","earth","eye-slash","eye","file-additions","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","merge-request-close-m","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","star-o","star","stop","talic","task-done","template","thump-down","thump-up","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} {"iconCount":135,"spriteSize":58718,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","calendar","cancel","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","comment-dots","comment-next","comment","comments","commit","credit-card","disk","doc_code","doc_image","doc_text","download","duplicate","earth","eye-slash","eye","file-additions","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","star-o","star","stop","talic","task-done","template","thump-down","thump-up","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}
\ No newline at end of file \ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */
import { visitUrl } from './lib/utils/url_utility';
import { convertPermissionToBoolean } from './lib/utils/common_utils';
window.BuildArtifacts = (function() { window.BuildArtifacts = (function() {
function BuildArtifacts() { function BuildArtifacts() {
this.disablePropagation(); this.disablePropagation();
this.setupEntryClick(); this.setupEntryClick();
this.setupTooltips();
} }
BuildArtifacts.prototype.disablePropagation = function() { BuildArtifacts.prototype.disablePropagation = function() {
...@@ -17,9 +20,28 @@ window.BuildArtifacts = (function() { ...@@ -17,9 +20,28 @@ window.BuildArtifacts = (function() {
BuildArtifacts.prototype.setupEntryClick = function() { BuildArtifacts.prototype.setupEntryClick = function() {
return $('.tree-holder').on('click', 'tr[data-link]', function(e) { return $('.tree-holder').on('click', 'tr[data-link]', function(e) {
return window.location = this.dataset.link; visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink));
}); });
}; };
BuildArtifacts.prototype.setupTooltips = function() {
$('.js-artifact-tree-tooltip').tooltip({
placement: 'bottom',
// Stop the tooltip from hiding when we stop hovering the element directly
// We handle all the showing/hiding below
trigger: 'manual',
});
// We want the tooltip to show if you hover anywhere on the row
// But be placed below and in the middle of the file name
$('.js-artifact-tree-row')
.on('mouseenter', (e) => {
$(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('show');
})
.on('mouseleave', (e) => {
$(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide');
});
};
return BuildArtifacts; return BuildArtifacts;
})(); })();
<script> <script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
export default { export default {
props: { props: {
...@@ -8,6 +10,8 @@ ...@@ -8,6 +10,8 @@
}, },
components: { components: {
userAvatarImage, userAvatarImage,
limitWarning,
totalTime,
}, },
}; };
</script> </script>
......
<script> <script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
export default { export default {
props: { props: {
...@@ -8,6 +10,8 @@ ...@@ -8,6 +10,8 @@
}, },
components: { components: {
userAvatarImage, userAvatarImage,
limitWarning,
totalTime,
}, },
}; };
</script> </script>
......
<script> <script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconCommit from '../svg/icon_commit.svg'; import iconCommit from '../svg/icon_commit.svg';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
export default { export default {
props: { props: {
items: Array, items: Array,
stage: Object, stage: Object,
},
components: {
userAvatarImage,
},
computed: {
iconCommit() {
return iconCommit;
}, },
}, components: {
}; userAvatarImage,
totalTime,
limitWarning,
},
computed: {
iconCommit() {
return iconCommit;
},
},
};
</script> </script>
<template> <template>
<div> <div>
......
<script> <script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
export default { export default {
props: { props: {
...@@ -8,6 +10,8 @@ ...@@ -8,6 +10,8 @@
}, },
components: { components: {
userAvatarImage, userAvatarImage,
totalTime,
limitWarning,
}, },
}; };
</script> </script>
......
<script> <script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg'; import iconBranch from '../svg/icon_branch.svg';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
export default { export default {
props: { props: {
...@@ -9,6 +11,8 @@ ...@@ -9,6 +11,8 @@
}, },
components: { components: {
userAvatarImage, userAvatarImage,
totalTime,
limitWarning,
}, },
computed: { computed: {
iconBranch() { iconBranch() {
......
<script> <script>
import iconBuildStatus from '../svg/icon_build_status.svg'; import iconBuildStatus from '../svg/icon_build_status.svg';
import iconBranch from '../svg/icon_branch.svg'; import iconBranch from '../svg/icon_branch.svg';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
export default { export default {
props: { props: {
items: Array, items: Array,
stage: Object, stage: Object,
},
computed: {
iconBuildStatus() {
return iconBuildStatus;
}, },
iconBranch() { components: {
return iconBranch; totalTime,
limitWarning,
}, },
}, computed: {
}; iconBuildStatus() {
return iconBuildStatus;
},
iconBranch() {
return iconBranch;
},
},
};
</script> </script>
<template> <template>
<div> <div>
......
...@@ -3,14 +3,12 @@ ...@@ -3,14 +3,12 @@
import Vue from 'vue'; import Vue from 'vue';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
import limitWarningComponent from './components/limit_warning_component.vue';
import stageCodeComponent from './components/stage_code_component.vue'; import stageCodeComponent from './components/stage_code_component.vue';
import stagePlanComponent from './components/stage_plan_component.vue'; import stagePlanComponent from './components/stage_plan_component.vue';
import stageComponent from './components/stage_component.vue'; import stageComponent from './components/stage_component.vue';
import stageReviewComponent from './components/stage_review_component.vue'; import stageReviewComponent from './components/stage_review_component.vue';
import stageStagingComponent from './components/stage_staging_component.vue'; import stageStagingComponent from './components/stage_staging_component.vue';
import stageTestComponent from './components/stage_test_component.vue'; import stageTestComponent from './components/stage_test_component.vue';
import totalTime from './components/total_time_component.vue';
import CycleAnalyticsService from './cycle_analytics_service'; import CycleAnalyticsService from './cycle_analytics_service';
import CycleAnalyticsStore from './cycle_analytics_store'; import CycleAnalyticsStore from './cycle_analytics_store';
...@@ -133,8 +131,4 @@ $(() => { ...@@ -133,8 +131,4 @@ $(() => {
}, },
}, },
}); });
// Register global components
Vue.component('limit-warning', limitWarningComponent);
Vue.component('total-time', totalTime);
}); });
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
/* global NotificationsDropdown */ /* global NotificationsDropdown */
/* global GroupAvatar */ /* global GroupAvatar */
/* global LineHighlighter */ /* global LineHighlighter */
/* global ProjectFork */
/* global BuildArtifacts */ /* global BuildArtifacts */
/* global GroupsSelect */ /* global GroupsSelect */
/* global Search */ /* global Search */
...@@ -524,7 +523,9 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -524,7 +523,9 @@ import initGroupAnalytics from './init_group_analytics';
shortcut_handler = true; shortcut_handler = true;
break; break;
case 'projects:forks:new': case 'projects:forks:new':
new ProjectFork(); import(/* webpackChunkName: 'project_fork' */ './project_fork')
.then(fork => fork.default())
.catch(() => {});
break; break;
case 'projects:artifacts:browse': case 'projects:artifacts:browse':
new ShortcutsNavigation(); new ShortcutsNavigation();
......
...@@ -67,10 +67,13 @@ const PARTICIPANTS_ROW_COUNT = 7; ...@@ -67,10 +67,13 @@ const PARTICIPANTS_ROW_COUNT = 7;
originalText = $(this).data("original-text"); originalText = $(this).data("original-text");
if (currentText === originalText) { if (currentText === originalText) {
$(this).text(lessText); $(this).text(lessText);
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
} else { } else {
$(this).text(originalText); $(this).text(originalText);
} }
return $(".js-participants-hidden").toggle();
$(".js-participants-hidden").toggle();
}; };
return IssuableContext; return IssuableContext;
......
...@@ -71,6 +71,7 @@ export const handleLocationHash = () => { ...@@ -71,6 +71,7 @@ export const handleLocationHash = () => {
// This is required to handle non-unicode characters in hash // This is required to handle non-unicode characters in hash
hash = decodeURIComponent(hash); hash = decodeURIComponent(hash);
const target = document.getElementById(hash) || document.getElementById(`user-content-${hash}`);
const fixedTabs = document.querySelector('.js-tabs-affix'); const fixedTabs = document.querySelector('.js-tabs-affix');
const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck'); const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck');
const fixedNav = document.querySelector('.navbar-gitlab'); const fixedNav = document.querySelector('.navbar-gitlab');
...@@ -78,25 +79,19 @@ export const handleLocationHash = () => { ...@@ -78,25 +79,19 @@ export const handleLocationHash = () => {
let adjustment = 0; let adjustment = 0;
if (fixedNav) adjustment -= fixedNav.offsetHeight; if (fixedNav) adjustment -= fixedNav.offsetHeight;
// scroll to user-generated markdown anchor if we cannot find a match if (target && target.scrollIntoView) {
if (document.getElementById(hash) === null) { target.scrollIntoView(true);
const target = document.getElementById(`user-content-${hash}`); }
if (target && target.scrollIntoView) {
target.scrollIntoView(true);
window.scrollBy(0, adjustment);
}
} else {
// only adjust for fixedTabs when not targeting user-generated content
if (fixedTabs) {
adjustment -= fixedTabs.offsetHeight;
}
if (fixedDiffStats) { if (fixedTabs) {
adjustment -= fixedDiffStats.offsetHeight; adjustment -= fixedTabs.offsetHeight;
} }
window.scrollBy(0, adjustment); if (fixedDiffStats) {
adjustment -= fixedDiffStats.offsetHeight;
} }
window.scrollBy(0, adjustment);
}; };
// Check if element scrolled into viewport from above or below // Check if element scrolled into viewport from above or below
......
...@@ -55,7 +55,7 @@ window.dateFormat = dateFormat; ...@@ -55,7 +55,7 @@ window.dateFormat = dateFormat;
if (!timeagoInstance) { if (!timeagoInstance) {
const localeRemaining = function(number, index) { const localeRemaining = function(number, index) {
return [ return [
[s__('Timeago|less than a minute ago'), s__('Timeago|a while')], [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
[s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')], [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')],
[s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')], [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')],
[s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
...@@ -73,7 +73,7 @@ window.dateFormat = dateFormat; ...@@ -73,7 +73,7 @@ window.dateFormat = dateFormat;
}; };
locale = function(number, index) { locale = function(number, index) {
return [ return [
[s__('Timeago|less than a minute ago'), s__('Timeago|a while')], [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
[s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')], [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')],
[s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')], [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')],
[s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
var base; var base;
var w = window; var w = window;
if (w.gl == null) { if (w.gl == null) {
...@@ -86,6 +87,21 @@ w.gl.utils.getLocationHash = function(url) { ...@@ -86,6 +87,21 @@ w.gl.utils.getLocationHash = function(url) {
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href); w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
w.gl.utils.visitUrl = (url) => { // eslint-disable-next-line import/prefer-default-export
document.location.href = url; export function visitUrl(url, external = false) {
if (external) {
// Simulate `target="blank" rel="noopener noreferrer"`
// See https://mathiasbynens.github.io/rel-noopener/
const otherWindow = window.open();
otherWindow.opener = null;
otherWindow.location = url;
} else {
document.location.href = url;
}
}
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
visitUrl,
}; };
...@@ -124,7 +124,6 @@ import './preview_markdown'; ...@@ -124,7 +124,6 @@ import './preview_markdown';
import './project'; import './project';
import './project_avatar'; import './project_avatar';
import './project_find_file'; import './project_find_file';
import './project_fork';
import './project_import'; import './project_import';
import './project_label_subscription'; import './project_label_subscription';
import './project_new'; import './project_new';
......
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
}, },
deleteHandler() { deleteHandler() {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (confirm('Are you sure you want to delete this list?')) { if (confirm('Are you sure you want to delete this comment?')) {
this.isDeleting = true; this.isDeleting = true;
this.deleteNote(this.note) this.deleteNote(this.note)
......
...@@ -28,8 +28,7 @@ ...@@ -28,8 +28,7 @@
popoverOptions() { popoverOptions() {
return { return {
html: true, html: true,
delay: { hide: 600 }, trigger: 'focus',
trigger: 'hover',
placement: 'top', placement: 'top',
title: '<div class="autodevops-title">This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b></div>', title: '<div class="autodevops-title">This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b></div>',
content: `<a class="autodevops-link" href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow">Learn more about Auto DevOps</a>`, content: `<a class="autodevops-link" href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow">Learn more about Auto DevOps</a>`,
...@@ -73,8 +72,16 @@ ...@@ -73,8 +72,16 @@
:title="pipeline.yaml_errors"> :title="pipeline.yaml_errors">
yaml invalid yaml invalid
</span> </span>
<span
v-if="pipeline.flags.failure_reason"
v-tooltip
class="js-pipeline-url-failure label label-danger"
:title="pipeline.failure_reason">
error
</span>
<a <a
v-if="pipeline.flags.auto_devops" v-if="pipeline.flags.auto_devops"
tabindex="0"
class="js-pipeline-url-autodevops label label-info autodevops-badge" class="js-pipeline-url-autodevops label label-info autodevops-badge"
v-popover="popoverOptions" v-popover="popoverOptions"
role="button"> role="button">
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */ export default () => {
(function() { $('.fork-thumbnail a').on('click', function forkThumbnailClicked() {
this.ProjectFork = (function() { if ($(this).hasClass('disabled')) return false;
function ProjectFork() {
$('.fork-thumbnail a').on('click', function() {
$('.fork-namespaces').hide();
return $('.save-project-loader').show();
});
}
return ProjectFork; $('.fork-namespaces').hide();
})(); return $('.save-project-loader').show();
}).call(window); });
};
...@@ -19,7 +19,7 @@ export default class ProjectsService { ...@@ -19,7 +19,7 @@ export default class ProjectsService {
getSearchedProjects(searchQuery) { getSearchedProjects(searchQuery) {
return this.projectsPath.get({ return this.projectsPath.get({
simple: false, simple: true,
per_page: 20, per_page: 20,
membership: !!gon.current_user_id, membership: !!gon.current_user_id,
order_by: 'last_activity_at', order_by: 'last_activity_at',
......
...@@ -95,7 +95,7 @@ export default RepoFile; ...@@ -95,7 +95,7 @@ export default RepoFile;
</div> </div>
</td> </td>
<td class="hidden-xs"> <td class="hidden-xs text-right">
<span <span
class="commit-update" class="commit-update"
:title="tooltipTitle(file.lastCommitUpdate)"> :title="tooltipTitle(file.lastCommitUpdate)">
......
...@@ -75,7 +75,7 @@ export default { ...@@ -75,7 +75,7 @@ export default {
<tr> <tr>
<th class="name">Name</th> <th class="name">Name</th>
<th class="hidden-sm hidden-xs last-commit">Last Commit</th> <th class="hidden-sm hidden-xs last-commit">Last Commit</th>
<th class="hidden-xs last-update">Last Update</th> <th class="hidden-xs last-update text-right">Last Update</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
......
...@@ -58,13 +58,13 @@ const RepoHelper = { ...@@ -58,13 +58,13 @@ const RepoHelper = {
return langs.find(lang => lang.extensions && lang.extensions.indexOf(`.${ext}`) > -1); return langs.find(lang => lang.extensions && lang.extensions.indexOf(`.${ext}`) > -1);
}, },
setDirectoryOpen(tree) { setDirectoryOpen(tree, title) {
const file = tree; const file = tree;
if (!file) return undefined; if (!file) return undefined;
file.opened = true; file.opened = true;
file.icon = 'fa-folder-open'; file.icon = 'fa-folder-open';
RepoHelper.updateHistoryEntry(file.url, file.name); RepoHelper.updateHistoryEntry(file.url, title);
return file; return file;
}, },
...@@ -135,6 +135,8 @@ const RepoHelper = { ...@@ -135,6 +135,8 @@ const RepoHelper = {
return Service.getContent() return Service.getContent()
.then((response) => { .then((response) => {
const data = response.data; const data = response.data;
if (response.headers && response.headers['page-title']) data.pageTitle = response.headers['page-title'];
Store.isTree = RepoHelper.isTree(data); Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) { if (!Store.isTree) {
if (!file) file = data; if (!file) file = data;
...@@ -168,7 +170,7 @@ const RepoHelper = { ...@@ -168,7 +170,7 @@ const RepoHelper = {
} else { } else {
// it's a tree // it's a tree
if (!file) Store.isRoot = RepoHelper.isRoot(Service.url); if (!file) Store.isRoot = RepoHelper.isRoot(Service.url);
file = RepoHelper.setDirectoryOpen(file); file = RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name);
const newDirectory = RepoHelper.dataToListOfFiles(data); const newDirectory = RepoHelper.dataToListOfFiles(data);
Store.addFilesToDirectory(file, Store.files, newDirectory); Store.addFilesToDirectory(file, Store.files, newDirectory);
Store.prevURL = Service.blobURLtoParentTree(Service.url); Store.prevURL = Service.blobURLtoParentTree(Service.url);
...@@ -255,7 +257,7 @@ const RepoHelper = { ...@@ -255,7 +257,7 @@ const RepoHelper = {
history.pushState({ key: RepoHelper.key }, '', url); history.pushState({ key: RepoHelper.key }, '', url);
if (title) { if (title) {
document.title = `${title} · GitLab`; document.title = title;
} }
}, },
......
...@@ -83,7 +83,7 @@ const RepoStore = { ...@@ -83,7 +83,7 @@ const RepoStore = {
}).catch(Helper.loadingError); }).catch(Helper.loadingError);
} }
if (!file.loading) Helper.updateHistoryEntry(file.url, file.name); if (!file.loading) Helper.updateHistoryEntry(file.url, file.pageTitle || file.name);
RepoStore.binary = file.binary; RepoStore.binary = file.binary;
}, },
......
...@@ -29,30 +29,32 @@ import Cookies from 'js-cookie'; ...@@ -29,30 +29,32 @@ import Cookies from 'js-cookie';
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$document.on('click', '.js-sidebar-toggle', function(e, triggered) { $document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault();
$this = $(this);
$thisIcon = $this.find('i');
$allGutterToggleIcons = $('.js-sidebar-toggle i');
if ($thisIcon.hasClass('fa-angle-double-right')) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
$('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
$('.page-with-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
} else {
$allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
$('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
$('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
}
if (!triggered) {
return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
}
});
return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo); return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo);
}; };
Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault();
$this = $(this);
$thisIcon = $this.find('i');
$allGutterToggleIcons = $('.js-sidebar-toggle i');
if ($thisIcon.hasClass('fa-angle-double-right')) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
$('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
$('.page-with-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
} else {
$allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
$('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
$('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
}
if (!triggered) {
Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
}
};
Sidebar.prototype.toggleTodo = function(e) { Sidebar.prototype.toggleTodo = function(e) {
var $btnText, $this, $todoLoading, ajaxType, url; var $btnText, $this, $todoLoading, ajaxType, url;
$this = $(e.currentTarget); $this = $(e.currentTarget);
......
...@@ -287,6 +287,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. ...@@ -287,6 +287,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
onClearInputClick(e) { onClearInputClick(e) {
e.preventDefault(); e.preventDefault();
this.wrap.toggleClass('has-value', !!e.target.value);
return this.searchInput.val('').focus(); return this.searchInput.val('').focus();
} }
......
...@@ -27,7 +27,7 @@ export default { ...@@ -27,7 +27,7 @@ export default {
<button <button
v-if="showDisabledButton" v-if="showDisabledButton"
type="button" type="button"
class="btn btn-success btn-sm" class="js-disabled-merge-button btn btn-success btn-sm"
disabled="true"> disabled="true">
Merge Merge
</button> </button>
......
...@@ -10,27 +10,37 @@ export default { ...@@ -10,27 +10,37 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="failed" showDisabledButton /> <status-icon
status="failed"
showDisabledButton />
<div class="media-body space-children"> <div class="media-body space-children">
<span class="bold"> <span
There are merge conflicts<span v-if="!mr.canMerge">.</span> v-if="mr.shouldBeRebased"
<span v-if="!mr.canMerge"> class="bold">
Resolve these conflicts or ask someone with write access to this repository to merge it locally Fast-forward merge is not possible.
</span> To merge this request, first rebase locally.
</span> </span>
<a <template v-else>
v-if="mr.canMerge && mr.conflictResolutionPath" <span class="bold">
:href="mr.conflictResolutionPath" There are merge conflicts<span v-if="!mr.canMerge">.</span>
class="btn btn-default btn-xs js-resolve-conflicts-button"> <span v-if="!mr.canMerge">
Resolve conflicts Resolve these conflicts or ask someone with write access to this repository to merge it locally
</a> </span>
<a </span>
v-if="mr.canMerge" <a
class="btn btn-default btn-xs js-merge-locally-button" v-if="mr.canMerge && mr.conflictResolutionPath"
data-toggle="modal" :href="mr.conflictResolutionPath"
href="#modal_merge_info"> class="js-resolve-conflicts-button btn btn-default btn-xs">
Merge locally Resolve conflicts
</a> </a>
<a
v-if="mr.canMerge"
class="js-merge-locally-button btn btn-default btn-xs"
data-toggle="modal"
href="#modal_merge_info">
Merge locally
</a>
</template>
</div> </div>
</div> </div>
`, `,
......
...@@ -288,14 +288,16 @@ export default { ...@@ -288,14 +288,16 @@ export default {
:mr="mr" :mr="mr"
:is-merge-button-disabled="isMergeButtonDisabled" /> :is-merge-button-disabled="isMergeButtonDisabled" />
<span v-if="mr.ffOnlyEnabled"> <span
v-if="mr.ffOnlyEnabled"
class="js-fast-forward-message">
Fast-forward merge without a merge commit Fast-forward merge without a merge commit
</span> </span>
<button <button
v-else v-else
@click="toggleCommitMessageEditor" @click="toggleCommitMessageEditor"
:disabled="isMergeButtonDisabled" :disabled="isMergeButtonDisabled"
class="btn btn-default btn-xs" class="js-modify-commit-message-button btn btn-default btn-xs"
type="button"> type="button">
Modify commit message Modify commit message
</button> </button>
......
...@@ -59,6 +59,8 @@ export default class MergeRequestStore { ...@@ -59,6 +59,8 @@ export default class MergeRequestStore {
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false; this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false; this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false;
this.mergePath = data.merge_path; this.mergePath = data.merge_path;
this.ffOnlyEnabled = data.ff_only_enabled;
this.shouldBeRebased = !!data.should_be_rebased;
this.statusPath = data.status_path; this.statusPath = data.status_path;
this.emailPatchesPath = data.email_patches_path; this.emailPatchesPath = data.email_patches_path;
this.plainDiffPath = data.plain_diff_path; this.plainDiffPath = data.plain_diff_path;
......
...@@ -126,7 +126,7 @@ ...@@ -126,7 +126,7 @@
.search-input-wrap { .search-input-wrap {
.search-icon, .search-icon,
.clear-icon { .clear-icon {
color: rgba($color-200, .8); fill: rgba($color-200, .8);
} }
} }
...@@ -141,7 +141,7 @@ ...@@ -141,7 +141,7 @@
.search-input-wrap { .search-input-wrap {
.search-icon { .search-icon {
color: rgba($color-200, .8); fill: rgba($color-200, .8);
} }
} }
} }
...@@ -252,7 +252,7 @@ body { ...@@ -252,7 +252,7 @@ body {
.search-input-wrap { .search-input-wrap {
.search-icon { .search-icon {
color: $theme-gray-200; fill: $theme-gray-200;
} }
.search-input { .search-input {
......
...@@ -109,8 +109,7 @@ header { ...@@ -109,8 +109,7 @@ header {
.user-counter { .user-counter {
svg { svg {
height: 16px; margin-right: 3px;
width: 23px;
} }
} }
...@@ -133,16 +132,16 @@ header { ...@@ -133,16 +132,16 @@ header {
} }
&.navbar-gitlab-new { &.navbar-gitlab-new {
.fa-times { .close-icon {
display: none; display: none;
} }
.menu-expanded { .menu-expanded {
.fa-ellipsis-v { .more-icon {
display: none; display: none;
} }
.fa-times { .close-icon {
display: block; display: block;
} }
} }
......
...@@ -27,7 +27,9 @@ ...@@ -27,7 +27,9 @@
} }
svg { svg {
&.s8 { @include svg-size(8px); }
&.s16 { @include svg-size(16px); } &.s16 { @include svg-size(16px); }
&.s18 { @include svg-size(18px); }
&.s24 { @include svg-size(24px); } &.s24 { @include svg-size(24px); }
&.s32 { @include svg-size(32px); } &.s32 { @include svg-size(32px); }
&.s48 { @include svg-size(48px); } &.s48 { @include svg-size(48px); }
......
...@@ -120,17 +120,24 @@ header.navbar-gitlab-new { ...@@ -120,17 +120,24 @@ header.navbar-gitlab-new {
.container-fluid { .container-fluid {
.navbar-toggle { .navbar-toggle {
min-width: 45px; min-width: 45px;
padding: 4px $gl-padding; padding: 0 $gl-padding;
margin-right: -7px; margin-right: -7px;
font-size: 14px;
text-align: center; text-align: center;
color: currentColor; color: currentColor;
svg {
fill: currentColor;
}
&:hover, &:hover,
&:focus, &:focus,
&.active { &.active {
color: currentColor; color: currentColor;
background-color: transparent; background-color: transparent;
svg {
fill: currentColor;
}
} }
} }
...@@ -279,10 +286,6 @@ header.navbar-gitlab-new { ...@@ -279,10 +286,6 @@ header.navbar-gitlab-new {
} }
} }
.admin-icon i {
font-size: 18px;
}
.caret-down { .caret-down {
height: 11px; height: 11px;
width: 11px; width: 11px;
......
...@@ -56,8 +56,8 @@ $new-sidebar-collapsed-width: 50px; ...@@ -56,8 +56,8 @@ $new-sidebar-collapsed-width: 50px;
color: $hover-color; color: $hover-color;
.settings-avatar { .settings-avatar {
i { svg {
color: $hover-color; fill: $hover-color;
} }
} }
} }
...@@ -76,12 +76,9 @@ $new-sidebar-collapsed-width: 50px; ...@@ -76,12 +76,9 @@ $new-sidebar-collapsed-width: 50px;
.settings-avatar { .settings-avatar {
background-color: $white-light; background-color: $white-light;
i { svg {
font-size: 20px; fill: $gl-text-color-secondary;
width: 100%; margin: auto;
color: $gl-text-color-secondary;
text-align: center;
align-self: center;
} }
} }
...@@ -177,16 +174,16 @@ $new-sidebar-collapsed-width: 50px; ...@@ -177,16 +174,16 @@ $new-sidebar-collapsed-width: 50px;
.nav-icon-container { .nav-icon-container {
display: flex; display: flex;
margin-right: 8px; margin-right: 8px;
svg {
height: 16px;
width: 16px;
}
} }
.fly-out-top-item { .fly-out-top-item {
display: none; display: none;
} }
svg {
height: 16px;
width: 16px;
}
} }
.nav-sidebar-inner-scroll { .nav-sidebar-inner-scroll {
...@@ -354,18 +351,22 @@ $new-sidebar-collapsed-width: 50px; ...@@ -354,18 +351,22 @@ $new-sidebar-collapsed-width: 50px;
display: flex; display: flex;
align-items: center; align-items: center;
i { svg {
font-size: 20px; fill: $gl-text-color-secondary;
margin-right: 8px; margin-right: 8px;
} }
.fa-angle-double-right { .icon-angle-double-right {
display: none; display: none;
} }
&:hover { &:hover {
background-color: $border-color; background-color: $border-color;
color: $gl-text-color; color: $gl-text-color;
svg {
fill: $gl-text-color;
}
} }
} }
...@@ -407,15 +408,16 @@ $new-sidebar-collapsed-width: 50px; ...@@ -407,15 +408,16 @@ $new-sidebar-collapsed-width: 50px;
.toggle-sidebar-button { .toggle-sidebar-button {
width: $new-sidebar-collapsed-width - 2px; width: $new-sidebar-collapsed-width - 2px;
padding: 16px 18px; padding: 16px;
.collapse-text, .collapse-text,
.fa-angle-double-left { .icon-angle-double-left {
display: none; display: none;
} }
.fa-angle-double-right { .icon-angle-double-right {
display: block; display: block;
margin: 0;
} }
} }
} }
......
...@@ -524,7 +524,7 @@ a.deploy-project-label { ...@@ -524,7 +524,7 @@ a.deploy-project-label {
text-align: center; text-align: center;
width: 169px; width: 169px;
&:hover, &:hover:not(.disabled),
&.forked { &.forked {
background-color: $row-hover; background-color: $row-hover;
border-color: $row-hover-border; border-color: $row-hover-border;
...@@ -551,6 +551,15 @@ a.deploy-project-label { ...@@ -551,6 +551,15 @@ a.deploy-project-label {
padding-top: $gl-padding; padding-top: $gl-padding;
color: $gl-text-color; color: $gl-text-color;
&.disabled {
opacity: .3;
cursor: not-allowed;
&:hover {
text-decoration: none;
}
}
.caption { .caption {
min-height: 30px; min-height: 30px;
padding: $gl-padding 0; padding: $gl-padding 0;
......
...@@ -81,17 +81,10 @@ input[type="checkbox"]:hover { ...@@ -81,17 +81,10 @@ input[type="checkbox"]:hover {
.clear-icon { .clear-icon {
position: absolute; position: absolute;
right: 5px; right: 5px;
top: 0; top: 4px;
&::before {
font-family: FontAwesome;
font-weight: $gl-font-weight-normal;
font-style: normal;
}
} }
.search-icon { .search-icon {
@extend .fa-search;
transition: color $default-transition-duration; transition: color $default-transition-duration;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
...@@ -99,7 +92,6 @@ input[type="checkbox"]:hover { ...@@ -99,7 +92,6 @@ input[type="checkbox"]:hover {
} }
.clear-icon { .clear-icon {
@extend .fa-times;
display: none; display: none;
} }
......
...@@ -169,6 +169,14 @@ ...@@ -169,6 +169,14 @@
} }
} }
.tree-item-file-external-link {
margin-right: 4px;
span {
text-decoration: inherit;
}
}
.tree_commit { .tree_commit {
max-width: 320px; max-width: 320px;
......
...@@ -26,7 +26,7 @@ class Admin::PushRulesController < Admin::ApplicationController ...@@ -26,7 +26,7 @@ class Admin::PushRulesController < Admin::ApplicationController
def push_rule_params def push_rule_params
params.require(:push_rule).permit(:deny_delete_tag, :delete_branch_regex, params.require(:push_rule).permit(:deny_delete_tag, :delete_branch_regex,
:commit_message_regex, :branch_name_regex, :force_push_regex, :author_email_regex, :member_check, :commit_message_regex, :branch_name_regex, :force_push_regex, :author_email_regex, :member_check,
:file_name_regex, :max_file_size, :prevent_secrets) :file_name_regex, :max_file_size, :prevent_secrets, :reject_unsigned_commits)
end end
def push_rule def push_rule
......
...@@ -25,6 +25,8 @@ class ApplicationController < ActionController::Base ...@@ -25,6 +25,8 @@ class ApplicationController < ActionController::Base
around_action :set_locale around_action :set_locale
after_action :set_page_title_header, if: -> { request.format == :json }
protect_from_forgery with: :exception protect_from_forgery with: :exception
helper_method :can?, :current_application_settings helper_method :can?, :current_application_settings
...@@ -347,4 +349,9 @@ class ApplicationController < ActionController::Base ...@@ -347,4 +349,9 @@ class ApplicationController < ActionController::Base
sign_in user, store: false sign_in user, store: false
end end
end end
def set_page_title_header
# Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8
response.headers['Page-Title'] = page_title('GitLab').encode('ISO-8859-1')
end
end end
...@@ -59,6 +59,7 @@ module AuthenticatesWithTwoFactor ...@@ -59,6 +59,7 @@ module AuthenticatesWithTwoFactor
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts! user.increment_failed_attempts!
Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=OTP")
flash.now[:alert] = 'Invalid two-factor code.' flash.now[:alert] = 'Invalid two-factor code.'
prompt_for_two_factor(user) prompt_for_two_factor(user)
end end
...@@ -75,6 +76,7 @@ module AuthenticatesWithTwoFactor ...@@ -75,6 +76,7 @@ module AuthenticatesWithTwoFactor
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts! user.increment_failed_attempts!
Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=U2F")
flash.now[:alert] = 'Authentication via U2F device failed.' flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user) prompt_for_two_factor(user)
end end
......
...@@ -16,6 +16,7 @@ class ConfirmationsController < Devise::ConfirmationsController ...@@ -16,6 +16,7 @@ class ConfirmationsController < Devise::ConfirmationsController
if signed_in?(resource_name) if signed_in?(resource_name)
after_sign_in(resource) after_sign_in(resource)
else else
Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}")
flash[:notice] += " Please sign in." flash[:notice] += " Please sign in."
new_session_path(resource_name) new_session_path(resource_name)
end end
......
...@@ -6,7 +6,7 @@ class Groups::BillingsController < Groups::ApplicationController ...@@ -6,7 +6,7 @@ class Groups::BillingsController < Groups::ApplicationController
def index def index
@top_most_group = @group.root_ancestor if @group.has_parent? @top_most_group = @group.root_ancestor if @group.has_parent?
current_plan = (@top_most_group || @group).actual_plan current_plan = (@top_most_group || @group).actual_plan_name
@plans_data = FetchSubscriptionPlansService.new(plan: current_plan).execute @plans_data = FetchSubscriptionPlansService.new(plan: current_plan).execute
end end
end end
...@@ -2,6 +2,8 @@ class Profiles::BillingsController < Profiles::ApplicationController ...@@ -2,6 +2,8 @@ class Profiles::BillingsController < Profiles::ApplicationController
before_action :verify_namespace_plan_check_enabled before_action :verify_namespace_plan_check_enabled
def index def index
@plans_data = FetchSubscriptionPlansService.new(plan: current_user.namespace.actual_plan).execute @plans_data = FetchSubscriptionPlansService
.new(plan: current_user.namespace.actual_plan_name)
.execute
end end
end end
...@@ -26,13 +26,17 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -26,13 +26,17 @@ class Projects::ArtifactsController < Projects::ApplicationController
blob = @entry.blob blob = @entry.blob
conditionally_expand_blob(blob) conditionally_expand_blob(blob)
respond_to do |format| if blob.external_link?(build)
format.html do redirect_to blob.external_url(@project, build)
render 'file' else
end respond_to do |format|
format.html do
format.json do render 'file'
render_blob_json(blob) end
format.json do
render_blob_json(blob)
end
end end
end end
end end
......
...@@ -41,6 +41,8 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -41,6 +41,8 @@ class Projects::BlobController < Projects::ApplicationController
end end
format.json do format.json do
page_title @blob.path, @ref, @project.name_with_namespace
show_json show_json
end end
end end
......
...@@ -9,7 +9,7 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -9,7 +9,7 @@ class Projects::BranchesController < Projects::ApplicationController
def index def index
@sort = params[:sort].presence || sort_value_recently_updated @sort = params[:sort].presence || sort_value_recently_updated
@branches = BranchesFinder.new(@repository, params).execute @branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute
@branches = Kaminari.paginate_array(@branches).page(params[:page]) @branches = Kaminari.paginate_array(@branches).page(params[:page])
respond_to do |format| respond_to do |format|
......
...@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue # Allow modify issue
before_action :authorize_update_issue!, only: [:edit, :update, :move] before_action :authorize_update_issue!, only: [:update, :move]
# Allow create a new branch and empty WIP merge request from current issue # Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request] before_action :authorize_create_merge_request!, only: [:create_merge_request]
...@@ -65,10 +65,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -65,10 +65,6 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue) respond_with(@issue)
end end
def edit
respond_with(@issue)
end
def show def show
@noteable = @issue @noteable = @issue
@note = @project.notes.new(noteable: @issue) @note = @project.notes.new(noteable: @issue)
...@@ -128,10 +124,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -128,10 +124,6 @@ class Projects::IssuesController < Projects::ApplicationController
@issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue) @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
respond_to do |format| respond_to do |format|
format.html do
recaptcha_check_with_fallback { render :edit }
end
format.json do format.json do
render_issue_json render_issue_json
end end
......
...@@ -25,7 +25,14 @@ class Projects::PushRulesController < Projects::ApplicationController ...@@ -25,7 +25,14 @@ class Projects::PushRulesController < Projects::ApplicationController
# Only allow a trusted parameter "white list" through. # Only allow a trusted parameter "white list" through.
def push_rule_params def push_rule_params
params.require(:push_rule).permit(:deny_delete_tag, :delete_branch_regex, allowed_fields = %i[deny_delete_tag delete_branch_regex commit_message_regex
:commit_message_regex, :branch_name_regex, :force_push_regex, :author_email_regex, :member_check, :file_name_regex, :max_file_size, :prevent_secrets) branch_name_regex force_push_regex author_email_regex
member_check file_name_regex max_file_size prevent_secrets]
if can?(current_user, :change_reject_unsigned_commits, project)
allowed_fields << :reject_unsigned_commits
end
params.require(:push_rule).permit(allowed_fields)
end end
end end
...@@ -35,6 +35,8 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -35,6 +35,8 @@ class Projects::TreeController < Projects::ApplicationController
end end
format.json do format.json do
page_title @path.presence || _("Files"), @ref, @project.name_with_namespace
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261 # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261
Gitlab::GitalyClient.allow_n_plus_1_calls do Gitlab::GitalyClient.allow_n_plus_1_calls do
render json: TreeSerializer.new(project: @project, repository: @repository, ref: @ref).represent(@tree) render json: TreeSerializer.new(project: @project, repository: @repository, ref: @ref).represent(@tree)
......
...@@ -347,6 +347,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -347,6 +347,7 @@ class ProjectsController < Projects::ApplicationController
:tag_list, :tag_list,
:visibility_level, :visibility_level,
:template_name, :template_name,
:merge_method,
project_feature_attributes: %i[ project_feature_attributes: %i[
builds_access_level builds_access_level
......
...@@ -43,10 +43,12 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -43,10 +43,12 @@ class RegistrationsController < Devise::RegistrationsController
end end
def after_sign_up_path_for(user) def after_sign_up_path_for(user)
Gitlab::AppLogger.info("User Created: username=#{user.username} email=#{user.email} ip=#{request.remote_ip} confirmed:#{user.confirmed?}")
user.confirmed? ? dashboard_projects_path : users_almost_there_path user.confirmed? ? dashboard_projects_path : users_almost_there_path
end end
def after_inactive_sign_up_path_for(_resource) def after_inactive_sign_up_path_for(resource)
Gitlab::AppLogger.info("User Created: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip} confirmed:false")
users_almost_there_path users_almost_there_path
end end
......
...@@ -15,6 +15,8 @@ class SessionsController < Devise::SessionsController ...@@ -15,6 +15,8 @@ class SessionsController < Devise::SessionsController
before_action :auto_sign_in_with_provider, only: [:new] before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha before_action :load_recaptcha
after_action :log_failed_login, only: [:new], if: :failed_login?
def new def new
set_minimum_password_length set_minimum_password_length
@ldap_servers = Gitlab::LDAP::Config.available_servers @ldap_servers = Gitlab::LDAP::Config.available_servers
...@@ -31,12 +33,13 @@ class SessionsController < Devise::SessionsController ...@@ -31,12 +33,13 @@ class SessionsController < Devise::SessionsController
end end
# hide the signed-in notification # hide the signed-in notification
flash[:notice] = nil flash[:notice] = nil
log_audit_event(current_user, with: authentication_method) log_audit_event(current_user, resource, with: authentication_method)
log_user_activity(current_user) log_user_activity(current_user)
end end
end end
def destroy def destroy
Gitlab::AppLogger.info("User Logout: username=#{current_user.username} ip=#{request.remote_ip}")
super super
# hide the signed_out notice # hide the signed_out notice
flash[:notice] = nil flash[:notice] = nil
...@@ -44,6 +47,14 @@ class SessionsController < Devise::SessionsController ...@@ -44,6 +47,14 @@ class SessionsController < Devise::SessionsController
private private
def log_failed_login
Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}")
end
def failed_login?
(options = env["warden.options"]) && options[:action] == "unauthenticated"
end
def login_counter def login_counter
@login_counter ||= Gitlab::Metrics.counter(:user_session_logins_total, 'User sign in count') @login_counter ||= Gitlab::Metrics.counter(:user_session_logins_total, 'User sign in count')
end end
...@@ -147,7 +158,8 @@ class SessionsController < Devise::SessionsController ...@@ -147,7 +158,8 @@ class SessionsController < Devise::SessionsController
user.invalidate_otp_backup_code!(user_params[:otp_attempt]) user.invalidate_otp_backup_code!(user_params[:otp_attempt])
end end
def log_audit_event(user, options = {}) def log_audit_event(user, resource, options = {})
Gitlab::AppLogger.info("Successful Login: username=#{resource.username} ip=#{request.remote_ip} method=#{options[:with]} admin=#{resource.admin?}")
AuditEventService.new(user, user, options) AuditEventService.new(user, user, options)
.for_authentication.security_event .for_authentication.security_event
end end
......
...@@ -21,7 +21,7 @@ module BreadcrumbsHelper ...@@ -21,7 +21,7 @@ module BreadcrumbsHelper
def breadcrumb_list_item(link) def breadcrumb_list_item(link)
content_tag "li" do content_tag "li" do
link + icon("angle-right", class: "breadcrumbs-list-angle") link + sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle")
end end
end end
......
...@@ -24,9 +24,9 @@ module IconsHelper ...@@ -24,9 +24,9 @@ module IconsHelper
end end
def sprite_icon(icon_name, size: nil, css_class: nil) def sprite_icon(icon_name, size: nil, css_class: nil)
css_classes = size ? "s#{size}" : nil css_classes = size ? "s#{size}" : ""
css_classes << " #{css_class}" unless css_class.blank? css_classes << " #{css_class}" unless css_class.blank?
content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{image_path('icons.svg')}##{icon_name}" } ), class: css_classes) content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{image_path('icons.svg')}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes)
end end
def audit_icon(names, options = {}) def audit_icon(names, options = {})
......
...@@ -9,7 +9,7 @@ module PageLayoutHelper ...@@ -9,7 +9,7 @@ module PageLayoutHelper
end end
# Segments are seperated by middot # Segments are seperated by middot
@page_title.join(" \u00b7 ") @page_title.join(" · ")
end end
# Define or get a description for the current page # Define or get a description for the current page
......
module ProjectsHelper module ProjectsHelper
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
prepend ::EE::ProjectsHelper
def link_to_project(project) def link_to_project(project)
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name') title = content_tag(:span, project.name, class: 'project-name')
...@@ -21,11 +23,14 @@ module ProjectsHelper ...@@ -21,11 +23,14 @@ module ProjectsHelper
classes = %W[avatar avatar-inline s#{opts[:size]}] classes = %W[avatar avatar-inline s#{opts[:size]}]
classes << opts[:avatar_class] if opts[:avatar_class] classes << opts[:avatar_class] if opts[:avatar_class]
image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: classes, alt: '') avatar = avatar_icon(author, opts[:size])
src = opts[:lazy_load] ? nil : avatar
image_tag(src, width: opts[:size], class: classes, alt: '', "data-src" => avatar)
end end
def link_to_member(project, author, opts = {}, &block) def link_to_member(project, author, opts = {}, &block)
default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name", tooltip: false } default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name", tooltip: false, lazy_load: false }
opts = default_opts.merge(opts) opts = default_opts.merge(opts)
return "(deleted)" unless author return "(deleted)" unless author
......
module PushRulesHelper
def reject_unsigned_commits_description(push_rule)
message = [s_("ProjectSettings|Only signed commits can be pushed to this repository.")]
if push_rule.global?
message << s_("ProjectSettings|This setting will be applied to all projects unless overridden by an admin.")
else
if PushRule.global&.reject_unsigned_commits
message << if push_rule.reject_unsigned_commits
s_("ProjectSettings|This setting is applied on the server level and can be overridden by an admin.")
else
s_("ProjectSettings|This setting is applied on the server level but has been overridden for this project.")
end
message << s_("ProjectSettings|Contact an admin to change this setting.") unless current_user.admin?
end
end
message.join(' ')
end
end
...@@ -2,6 +2,8 @@ module Ci ...@@ -2,6 +2,8 @@ module Ci
class ArtifactBlob class ArtifactBlob
include BlobLike include BlobLike
EXTENTIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json].freeze
attr_reader :entry attr_reader :entry
def initialize(entry) def initialize(entry)
...@@ -17,6 +19,7 @@ module Ci ...@@ -17,6 +19,7 @@ module Ci
def size def size
entry.metadata[:size] entry.metadata[:size]
end end
alias_method :external_size, :size
def data def data
"Build artifact #{path}" "Build artifact #{path}"
...@@ -30,6 +33,27 @@ module Ci ...@@ -30,6 +33,27 @@ module Ci
:build_artifact :build_artifact
end end
alias_method :external_size, :size def external_url(project, job)
return unless external_link?(job)
components = project.full_path_components
components << "-/jobs/#{job.id}/artifacts/file/#{path}"
artifact_path = components[1..-1].join('/')
"#{pages_config.protocol}://#{components[0]}.#{pages_config.host}/#{artifact_path}"
end
def external_link?(job)
pages_config.enabled &&
pages_config.artifacts_server &&
EXTENTIONS_SERVED_BY_PAGES.include?(File.extname(name)) &&
job.project.public?
end
private
def pages_config
Gitlab.config.pages
end
end end
end end
...@@ -5,6 +5,7 @@ module Ci ...@@ -5,6 +5,7 @@ module Ci
include Importable include Importable
include AfterCommitQueue include AfterCommitQueue
include Presentable include Presentable
include Gitlab::OptimisticLocking
prepend ::EE::Ci::Pipeline prepend ::EE::Ci::Pipeline
...@@ -71,6 +72,11 @@ module Ci ...@@ -71,6 +72,11 @@ module Ci
auto_devops_source: 2 auto_devops_source: 2
} }
enum failure_reason: {
unknown_failure: 0,
config_error: 1
}.merge(EE_FAILURE_REASONS)
state_machine :status, initial: :created do state_machine :status, initial: :created do
event :enqueue do event :enqueue do
transition created: :pending transition created: :pending
...@@ -122,6 +128,12 @@ module Ci ...@@ -122,6 +128,12 @@ module Ci
pipeline.auto_canceled_by = nil pipeline.auto_canceled_by = nil
end end
before_transition any => :failed do |pipeline, transition|
transition.args.first.try do |reason|
pipeline.failure_reason = reason
end
end
after_transition [:created, :pending] => :running do |pipeline| after_transition [:created, :pending] => :running do |pipeline|
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) } pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end end
...@@ -276,7 +288,7 @@ module Ci ...@@ -276,7 +288,7 @@ module Ci
end end
def cancel_running def cancel_running
Gitlab::OptimisticLocking.retry_lock(cancelable_statuses) do |cancelable| retry_optimistic_lock(cancelable_statuses) do |cancelable|
cancelable.find_each do |job| cancelable.find_each do |job|
yield(job) if block_given? yield(job) if block_given?
job.cancel job.cancel
...@@ -325,6 +337,10 @@ module Ci ...@@ -325,6 +337,10 @@ module Ci
@stage_seeds ||= config_processor.stage_seeds(self) @stage_seeds ||= config_processor.stage_seeds(self)
end end
def seeds_size
@seeds_size ||= stage_seeds.sum(&:size)
end
def has_kubernetes_active? def has_kubernetes_active?
project.kubernetes_service&.active? project.kubernetes_service&.active?
end end
...@@ -416,7 +432,7 @@ module Ci ...@@ -416,7 +432,7 @@ module Ci
end end
def update_status def update_status
Gitlab::OptimisticLocking.retry_lock(self) do retry_optimistic_lock(self) do
case latest_builds_status case latest_builds_status
when 'pending' then enqueue when 'pending' then enqueue
when 'running' then run when 'running' then run
......
...@@ -81,6 +81,7 @@ module HasStatus ...@@ -81,6 +81,7 @@ module HasStatus
scope :canceled, -> { where(status: 'canceled') } scope :canceled, -> { where(status: 'canceled') }
scope :skipped, -> { where(status: 'skipped') } scope :skipped, -> { where(status: 'skipped') }
scope :manual, -> { where(status: 'manual') } scope :manual, -> { where(status: 'manual') }
scope :alive, -> { where(status: [:created, :pending, :running]) }
scope :created_or_pending, -> { where(status: [:created, :pending]) } scope :created_or_pending, -> { where(status: [:created, :pending]) }
scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :running_or_pending, -> { where(status: [:running, :pending]) }
scope :finished, -> { where(status: [:success, :failed, :canceled]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) }
......
...@@ -106,6 +106,10 @@ module Routable ...@@ -106,6 +106,10 @@ module Routable
RequestStore[full_path_key] ||= uncached_full_path RequestStore[full_path_key] ||= uncached_full_path
end end
def full_path_components
full_path.split('/')
end
def expires_full_path_cache def expires_full_path_cache
RequestStore.delete(full_path_key) if RequestStore.active? RequestStore.delete(full_path_key) if RequestStore.active?
@full_path = nil @full_path = nil
......
...@@ -23,6 +23,7 @@ class GeoNode < ActiveRecord::Base ...@@ -23,6 +23,7 @@ class GeoNode < ActiveRecord::Base
validates :encrypted_secret_access_key, presence: true validates :encrypted_secret_access_key, presence: true
validates :geo_node_key, presence: true, if: :secondary? validates :geo_node_key, presence: true, if: :secondary?
validate :check_not_adding_primary_as_secondary, if: :secondary?
after_initialize :build_dependents after_initialize :build_dependents
after_save :expire_cache! after_save :expire_cache!
...@@ -216,12 +217,12 @@ class GeoNode < ActiveRecord::Base ...@@ -216,12 +217,12 @@ class GeoNode < ActiveRecord::Base
end end
end end
def validate(record) # Prevent locking yourself out
# Prevent locking yourself out def check_not_adding_primary_as_secondary
if record.host == Gitlab.config.gitlab.host && if host == Gitlab.config.gitlab.host &&
record.port == Gitlab.config.gitlab.port && port == Gitlab.config.gitlab.port &&
record.relative_url_root == Gitlab.config.gitlab.relative_url_root && !record.primary relative_url_root == Gitlab.config.gitlab.relative_url_root
record.errors[:base] << 'Current node must be the primary node or you will be locking yourself out' errors.add(:base, 'Current node must be the primary node or you will be locking yourself out')
end end
end end
......
...@@ -73,7 +73,7 @@ class GpgKey < ActiveRecord::Base ...@@ -73,7 +73,7 @@ class GpgKey < ActiveRecord::Base
end end
def verified_and_belongs_to_email?(email) def verified_and_belongs_to_email?(email)
emails_with_verified_status.fetch(email, false) emails_with_verified_status.fetch(email.downcase, false)
end end
def update_invalid_gpg_signatures def update_invalid_gpg_signatures
......
...@@ -12,7 +12,6 @@ class License < ActiveRecord::Base ...@@ -12,7 +12,6 @@ class License < ActiveRecord::Base
contribution_analytics contribution_analytics
elastic_search elastic_search
export_issues export_issues
fast_forward_merge
group_webhooks group_webhooks
issuable_default_templates issuable_default_templates
issue_board_focus_mode issue_board_focus_mode
...@@ -63,7 +62,6 @@ class License < ActiveRecord::Base ...@@ -63,7 +62,6 @@ class License < ActiveRecord::Base
cross_project_pipelines cross_project_pipelines
deploy_board deploy_board
export_issues export_issues
fast_forward_merge
file_locks file_locks
group_webhooks group_webhooks
issuable_default_templates issuable_default_templates
......
...@@ -549,6 +549,14 @@ class MergeRequest < ActiveRecord::Base ...@@ -549,6 +549,14 @@ class MergeRequest < ActiveRecord::Base
true true
end end
def ff_merge_possible?
project.repository.ancestor?(target_branch_sha, diff_head_sha)
end
def should_be_rebased?
project.ff_merge_must_be_possible? && !ff_merge_possible?
end
def can_cancel_merge_when_pipeline_succeeds?(current_user) def can_cancel_merge_when_pipeline_succeeds?(current_user)
can_be_merged_by?(current_user) || self.author == current_user can_be_merged_by?(current_user) || self.author == current_user
end end
......
...@@ -75,6 +75,7 @@ class Project < ActiveRecord::Base ...@@ -75,6 +75,7 @@ class Project < ActiveRecord::Base
attr_accessor :old_path_with_namespace attr_accessor :old_path_with_namespace
attr_accessor :template_name attr_accessor :template_name
attr_writer :pipeline_status attr_writer :pipeline_status
attr_accessor :skip_disk_validation
alias_attribute :title, :name alias_attribute :title, :name
...@@ -231,7 +232,7 @@ class Project < ActiveRecord::Base ...@@ -231,7 +232,7 @@ class Project < ActiveRecord::Base
validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?] validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 } validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create validate :check_limit, on: :create
validate :can_create_repository?, on: [:create, :update], if: ->(project) { !project.persisted? || project.renamed? } validate :check_repository_path_availability, on: [:create, :update], if: ->(project) { !project.persisted? || project.renamed? }
validate :avatar_type, validate :avatar_type,
if: ->(project) { project.avatar.present? && project.avatar_changed? } if: ->(project) { project.avatar.present? && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
...@@ -1018,7 +1019,8 @@ class Project < ActiveRecord::Base ...@@ -1018,7 +1019,8 @@ class Project < ActiveRecord::Base
end end
# Check if repository already exists on disk # Check if repository already exists on disk
def can_create_repository? def check_repository_path_availability
return true if skip_disk_validation
return false unless repository_storage_path return false unless repository_storage_path
expires_full_path_cache # we need to clear cache to validate renames correctly expires_full_path_cache # we need to clear cache to validate renames correctly
...@@ -1586,6 +1588,34 @@ class Project < ActiveRecord::Base ...@@ -1586,6 +1588,34 @@ class Project < ActiveRecord::Base
Gitlab::GlRepository.gl_repository(self, is_wiki) Gitlab::GlRepository.gl_repository(self, is_wiki)
end end
def merge_method
if self.merge_requests_ff_only_enabled
:ff
elsif self.merge_requests_rebase_enabled
:rebase_merge
else
:merge
end
end
def merge_method=(method)
case method.to_s
when "ff"
self.merge_requests_ff_only_enabled = true
self.merge_requests_rebase_enabled = true
when "rebase_merge"
self.merge_requests_ff_only_enabled = false
self.merge_requests_rebase_enabled = true
when "merge"
self.merge_requests_ff_only_enabled = false
self.merge_requests_rebase_enabled = false
end
end
def ff_merge_must_be_possible?
self.merge_requests_ff_only_enabled || self.merge_requests_rebase_enabled
end
private private
def storage def storage
......
...@@ -6,16 +6,27 @@ class PushRule < ActiveRecord::Base ...@@ -6,16 +6,27 @@ class PushRule < ActiveRecord::Base
FILES_BLACKLIST = YAML.load_file(Rails.root.join('lib/gitlab/checks/files_blacklist.yml')) FILES_BLACKLIST = YAML.load_file(Rails.root.join('lib/gitlab/checks/files_blacklist.yml'))
def self.global
find_by(is_sample: true)
end
def commit_validation? def commit_validation?
commit_message_regex.present? || commit_message_regex.present? ||
branch_name_regex.present? || branch_name_regex.present? ||
author_email_regex.present? || author_email_regex.present? ||
reject_unsigned_commits ||
member_check || member_check ||
file_name_regex.present? || file_name_regex.present? ||
max_file_size > 0 || max_file_size > 0 ||
prevent_secrets prevent_secrets
end end
def commit_signature_allowed?(commit)
return true unless reject_unsigned_commits
commit.has_signature?
end
def commit_message_allowed?(message) def commit_message_allowed?(message)
data_match?(message, commit_message_regex) data_match?(message, commit_message_regex)
end end
...@@ -36,6 +47,33 @@ class PushRule < ActiveRecord::Base ...@@ -36,6 +47,33 @@ class PushRule < ActiveRecord::Base
regex_list.find { |regex| data_match?(file_path, regex) } regex_list.find { |regex| data_match?(file_path, regex) }
end end
def reject_unsigned_commits=(value)
enabled_globally = PushRule.global&.reject_unsigned_commits
is_disabled = !Gitlab::Utils.to_boolean(value)
# If setting is globally disabled and user disable it at project level,
# reset the attr so we can use the default global if required later.
if !enabled_globally && is_disabled
super(nil)
else
super(value)
end
end
def reject_unsigned_commits
value = super
# return if value is true/false or if current object is the global setting
return value if global? || !value.nil?
PushRule.global&.reject_unsigned_commits
end
alias_method :reject_unsigned_commits?, :reject_unsigned_commits
def global?
is_sample?
end
private private
def data_match?(data, regex) def data_match?(data, regex)
......
...@@ -41,7 +41,10 @@ class Repository ...@@ -41,7 +41,10 @@ class Repository
CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide
changelog license_blob license_key gitignore koding_yml changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? empty? root_ref).freeze tag_count avatar exists? empty? root_ref has_visible_content?).freeze
# Methods that use cache_method but only memoize the value
MEMOIZED_CACHED_METHODS = %i(license empty_repo?).freeze
# Certain method caches should be refreshed when certain types of files are # Certain method caches should be refreshed when certain types of files are
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
...@@ -98,12 +101,6 @@ class Repository ...@@ -98,12 +101,6 @@ class Repository
) )
end end
# we need to have this method here because it is not cached in ::Git and
# the method is called multiple times for every request
def has_visible_content?
branch_count > 0
end
def inspect def inspect
"#<#{self.class.name}:#{@disk_path}>" "#<#{self.class.name}:#{@disk_path}>"
end end
...@@ -282,7 +279,7 @@ class Repository ...@@ -282,7 +279,7 @@ class Repository
end end
def expire_branches_cache def expire_branches_cache
expire_method_caches(%i(branch_names branch_count)) expire_method_caches(%i(branch_names branch_count has_visible_content?))
@local_branches = nil @local_branches = nil
@branch_exists_memo = nil @branch_exists_memo = nil
end end
...@@ -353,7 +350,7 @@ class Repository ...@@ -353,7 +350,7 @@ class Repository
def expire_emptiness_caches def expire_emptiness_caches
return unless empty? return unless empty?
expire_method_caches(%i(empty?)) expire_method_caches(%i(empty? has_visible_content?))
end end
def lookup_cache def lookup_cache
...@@ -530,9 +527,10 @@ class Repository ...@@ -530,9 +527,10 @@ class Repository
delegate :tag_names, to: :raw_repository delegate :tag_names, to: :raw_repository
cache_method :tag_names, fallback: [] cache_method :tag_names, fallback: []
delegate :branch_count, :tag_count, to: :raw_repository delegate :branch_count, :tag_count, :has_visible_content?, to: :raw_repository
cache_method :branch_count, fallback: 0 cache_method :branch_count, fallback: 0
cache_method :tag_count, fallback: 0 cache_method :tag_count, fallback: 0
cache_method :has_visible_content?, fallback: false
def avatar def avatar
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38327 # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38327
......
...@@ -711,7 +711,11 @@ class User < ActiveRecord::Base ...@@ -711,7 +711,11 @@ class User < ActiveRecord::Base
end end
def ldap_user? def ldap_user?
identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"]) if identities.loaded?
identities.find { |identity| identity.provider.start_with?('ldap') && !identity.extern_uid.nil? }
else
identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
end
end end
def ldap_identity def ldap_identity
...@@ -1086,6 +1090,12 @@ class User < ActiveRecord::Base ...@@ -1086,6 +1090,12 @@ class User < ActiveRecord::Base
user_synced_attributes_metadata&.read_only?(attribute) user_synced_attributes_metadata&.read_only?(attribute)
end end
# override, from Devise
def lock_access!
Gitlab::AppLogger.info("Account Locked: username=#{username}")
super
end
protected protected
# override, from Devise::Validatable # override, from Devise::Validatable
......
...@@ -11,6 +11,8 @@ class GlobalPolicy < BasePolicy ...@@ -11,6 +11,8 @@ class GlobalPolicy < BasePolicy
with_options scope: :user, score: 0 with_options scope: :user, score: 0
condition(:access_locked) { @user.access_locked? } condition(:access_locked) { @user.access_locked? }
condition(:can_create_fork, scope: :user) { @user.manageable_namespaces.any? { |namespace| @user.can?(:create_projects, namespace) } }
rule { anonymous }.policy do rule { anonymous }.policy do
prevent :log_in prevent :log_in
prevent :access_api prevent :access_api
...@@ -40,6 +42,10 @@ class GlobalPolicy < BasePolicy ...@@ -40,6 +42,10 @@ class GlobalPolicy < BasePolicy
enable :create_group enable :create_group
end end
rule { can_create_fork }.policy do
enable :create_fork
end
rule { access_locked }.policy do rule { access_locked }.policy do
prevent :log_in prevent :log_in
end end
......
class NamespacePolicy < BasePolicy class NamespacePolicy < BasePolicy
rule { anonymous }.prevent_all rule { anonymous }.prevent_all
condition(:personal_project, scope: :subject) { @subject.kind == 'user' }
condition(:can_create_personal_project, scope: :user) { @user.can_create_project? }
condition(:owner) { @subject.owner == @user } condition(:owner) { @subject.owner == @user }
rule { owner | admin }.policy do rule { owner | admin }.policy do
enable :create_projects enable :create_projects
enable :admin_namespace enable :admin_namespace
end end
rule { personal_project & ~can_create_personal_project }.prevent :create_projects
end end
module Ci module Ci
class PipelinePresenter < Gitlab::View::Presenter::Delegated class PipelinePresenter < Gitlab::View::Presenter::Delegated
prepend ::EE::Ci::PipelinePresenter
FAILURE_REASONS = {
config_error: 'CI/CD YAML configuration error!'
}.merge(EE_FAILURE_REASONS)
presents :pipeline presents :pipeline
def failure_reason
return unless pipeline.failure_reason?
FAILURE_REASONS[pipeline.failure_reason.to_sym] ||
pipeline.failure_reason
end
def status_title def status_title
if auto_canceled? if auto_canceled?
"Pipeline is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}" "Pipeline is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
......
...@@ -20,6 +20,7 @@ class PipelineEntity < Grape::Entity ...@@ -20,6 +20,7 @@ class PipelineEntity < Grape::Entity
expose :has_yaml_errors?, as: :yaml_errors expose :has_yaml_errors?, as: :yaml_errors
expose :can_retry?, as: :retryable expose :can_retry?, as: :retryable
expose :can_cancel?, as: :cancelable expose :can_cancel?, as: :cancelable
expose :failure_reason?, as: :failure_reason
end end
expose :details do expose :details do
...@@ -44,6 +45,11 @@ class PipelineEntity < Grape::Entity ...@@ -44,6 +45,11 @@ class PipelineEntity < Grape::Entity
end end
expose :commit, using: CommitEntity expose :commit, using: CommitEntity
expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? }
expose :failure_reason, if: -> (pipeline, _) { pipeline.failure_reason? } do |pipeline|
pipeline.present.failure_reason
end
expose :retry_path, if: -> (*) { can_retry? } do |pipeline| expose :retry_path, if: -> (*) { can_retry? } do |pipeline|
retry_project_pipeline_path(pipeline.project, pipeline) retry_project_pipeline_path(pipeline.project, pipeline)
...@@ -53,8 +59,6 @@ class PipelineEntity < Grape::Entity ...@@ -53,8 +59,6 @@ class PipelineEntity < Grape::Entity
cancel_project_pipeline_path(pipeline.project, pipeline) cancel_project_pipeline_path(pipeline.project, pipeline)
end end
expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? }
private private
alias_method :pipeline, :object alias_method :pipeline, :object
......
...@@ -6,7 +6,9 @@ module Ci ...@@ -6,7 +6,9 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Validate::Repository, Gitlab::Ci::Pipeline::Chain::Validate::Repository,
Gitlab::Ci::Pipeline::Chain::Validate::Config, Gitlab::Ci::Pipeline::Chain::Validate::Config,
Gitlab::Ci::Pipeline::Chain::Skip, Gitlab::Ci::Pipeline::Chain::Skip,
Gitlab::Ci::Pipeline::Chain::Create].freeze EE::Gitlab::Ci::Pipeline::Chain::Limit::Size,
Gitlab::Ci::Pipeline::Chain::Create,
EE::Gitlab::Ci::Pipeline::Chain::Limit::Activity].freeze
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, mirror_update: false, &block) def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, mirror_update: false, &block)
@pipeline = Ci::Pipeline.new( @pipeline = Ci::Pipeline.new(
......
...@@ -34,7 +34,7 @@ module Issues ...@@ -34,7 +34,7 @@ module Issues
if issue.assignees != old_assignees if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees) create_assignee_note(issue, old_assignees)
notification_service.reassigned_issue(issue, current_user, old_assignees) notification_service.reassigned_issue(issue, current_user, old_assignees)
todo_service.reassigned_issue(issue, current_user) todo_service.reassigned_issue(issue, current_user, old_assignees)
end end
if issue.previous_changes.include?('confidential') if issue.previous_changes.include?('confidential')
......
...@@ -12,7 +12,7 @@ module MergeRequests ...@@ -12,7 +12,7 @@ module MergeRequests
def rebase def rebase
if merge_request.rebase_in_progress? if merge_request.rebase_in_progress?
log_error('Rebase task canceled: Another rebase is already in progress') log_error('Rebase task canceled: Another rebase is already in progress', save_message_on_model: true)
return false return false
end end
...@@ -52,7 +52,7 @@ module MergeRequests ...@@ -52,7 +52,7 @@ module MergeRequests
false false
rescue => e rescue => e
log_error('Failed to rebase branch:') log_error('Failed to rebase branch:')
log_error(e) log_error(e.message, save_message_on_model: true)
false false
ensure ensure
clean_dir clean_dir
......
...@@ -24,7 +24,7 @@ module MergeRequests ...@@ -24,7 +24,7 @@ module MergeRequests
log_error("`#{command.join(' ')}` failed:") log_error("`#{command.join(' ')}` failed:")
end end
log_error(output) log_error(output, save_message_on_model: true)
raise GitCommandError raise GitCommandError
end end
...@@ -40,8 +40,10 @@ module MergeRequests ...@@ -40,8 +40,10 @@ module MergeRequests
@target_project ||= merge_request.target_project @target_project ||= merge_request.target_project
end end
def log_error(message) def log_error(message, save_message_on_model: false)
Gitlab::GitLogger.error("#{self.class.name} error (#{merge_request.to_reference(full: true)}): #{message}") Gitlab::GitLogger.error("#{self.class.name} error (#{merge_request.to_reference(full: true)}): #{message}")
merge_request.update(merge_error: message) if save_message_on_model
end end
def clean_dir def clean_dir
......
...@@ -43,8 +43,8 @@ class TodoService ...@@ -43,8 +43,8 @@ class TodoService
# #
# * create a pending todo for new assignee if issue is assigned # * create a pending todo for new assignee if issue is assigned
# #
def reassigned_issue(issue, current_user) def reassigned_issue(issue, current_user, old_assignees = [])
create_assignment_todo(issue, current_user) create_assignment_todo(issue, current_user, old_assignees)
end end
# When create a merge request we should: # When create a merge request we should:
...@@ -267,10 +267,11 @@ class TodoService ...@@ -267,10 +267,11 @@ class TodoService
create_mention_todos(project, target, author, note, skip_users) create_mention_todos(project, target, author, note, skip_users)
end end
def create_assignment_todo(issuable, author) def create_assignment_todo(issuable, author, old_assignees = [])
if issuable.assignees.any? if issuable.assignees.any?
assignees = issuable.assignees - old_assignees
attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED) attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED)
create_todos(issuable.assignees, attributes) create_todos(assignees, attributes)
end end
end end
......
- page_title "Billing" - page_title "Billing"
- if @top_most_group - if @top_most_group
- top_most_group_plan = subscription_plan_info(@plans_data, @top_most_group.actual_plan) - top_most_group_plan = subscription_plan_info(@plans_data, @top_most_group.actual_plan_name)
= render 'shared/billings/billing_plan_header', namespace: @group, plan: top_most_group_plan, parent_group: @top_most_group = render 'shared/billings/billing_plan_header', namespace: @group, plan: top_most_group_plan, parent_group: @top_most_group
- else - else
= render 'shared/billings/billing_plans', plans_data: @plans_data, namespace: @group = render 'shared/billings/billing_plans', plans_data: @plans_data, namespace: @group
...@@ -21,8 +21,8 @@ ...@@ -21,8 +21,8 @@
%a %a
Loading... Loading...
= dropdown_loading = dropdown_loading
%i.search-icon = sprite_icon('search', size: 16, css_class: 'search-icon')
%i.clear-icon.js-clear-input = sprite_icon('close', size: 16, css_class: 'clear-icon js-clear-input')
= hidden_field_tag :group_id, @group.try(:id), class: 'js-search-group-options', data: group_data_attrs = hidden_field_tag :group_id, @group.try(:id), class: 'js-search-group-options', data: group_data_attrs
......
...@@ -24,29 +24,29 @@ ...@@ -24,29 +24,29 @@
= render 'layouts/search' unless current_controller?(:search) = render 'layouts/search' unless current_controller?(:search)
%li.visible-sm-inline-block.visible-xs-inline-block %li.visible-sm-inline-block.visible-xs-inline-block
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search') = sprite_icon('search', size: 16)
- if current_user - if current_user
= nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do = nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('issues') = sprite_icon('issues', size: 16)
- issues_count = assigned_issuables_count(:issues) - issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count) = number_with_delimiter(issues_count)
= nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do = nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold') = sprite_icon('git-merge', size: 16)
- merge_requests_count = assigned_issuables_count(:merge_requests) - merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count) = number_with_delimiter(merge_requests_count)
= nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do = nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('todo_done') = sprite_icon('todo-done', size: 16)
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) } %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count) = todos_count_format(todos_pending_count)
%li.header-user.dropdown %li.header-user.dropdown
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar" = image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar"
= custom_icon('caret_down') = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu-nav.dropdown-menu-align-right .dropdown-menu-nav.dropdown-menu-align-right
%ul %ul
%li.current-user %li.current-user
...@@ -75,7 +75,7 @@ ...@@ -75,7 +75,7 @@
%button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' } %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
%span.sr-only Toggle navigation %span.sr-only Toggle navigation
= icon('ellipsis-v', class: 'js-navbar-toggle-right') = sprite_icon('more', size: 16, css_class: 'more-icon js-navbar-toggle-right')
= icon('times', class: 'js-navbar-toggle-left') = sprite_icon('close', size: 16, css_class: 'close-icon js-navbar-toggle-left')
= render 'shared/outdated_browser' = render 'shared/outdated_browser'
%li.header-new.dropdown %li.header-new.dropdown
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do
= custom_icon('plus_square') = sprite_icon('plus-square', size: 16)
= custom_icon('caret_down') = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu-nav.dropdown-menu-align-right .dropdown-menu-nav.dropdown-menu-align-right
%ul %ul
- if @group&.persisted? - if @group&.persisted?
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects" }) do = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects" }) do
%a{ href: "#", data: { toggle: "dropdown" } } %a{ href: "#", data: { toggle: "dropdown" } }
Projects Projects
= custom_icon('caret_down') = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu.projects-dropdown-menu .dropdown-menu.projects-dropdown-menu
= render "layouts/nav/projects_dropdown/show" = render "layouts/nav/projects_dropdown/show"
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
%li.header-more.dropdown.hidden-lg %li.header-more.dropdown.hidden-lg
%a{ href: "#", data: { toggle: "dropdown" } } %a{ href: "#", data: { toggle: "dropdown" } }
More More
= custom_icon('caret_down') = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu .dropdown-menu
%ul %ul
= nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
- if current_user.admin? - if current_user.admin?
= nav_link(controller: 'admin/dashboard') do = nav_link(controller: 'admin/dashboard') do
= link_to admin_root_path, class: 'admin-icon', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to admin_root_path, class: 'admin-icon', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('wrench fw') = sprite_icon('admin', size: 18)
- if Gitlab::Sherlock.enabled? - if Gitlab::Sherlock.enabled?
%li %li
= link_to sherlock_transactions_path, class: 'admin-icon', title: 'Sherlock Transactions', = link_to sherlock_transactions_path, class: 'admin-icon', title: 'Sherlock Transactions',
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
%li.dropdown %li.dropdown
%button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { toggle: "dropdown", container: "body" }, "aria-label": button_tooltip, title: button_tooltip } %button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { toggle: "dropdown", container: "body" }, "aria-label": button_tooltip, title: button_tooltip }
= icon("ellipsis-h") = icon("ellipsis-h")
= icon("angle-right", class: "breadcrumbs-list-angle") = sprite_icon("angle-right", css_class: "breadcrumbs-list-angle")
.dropdown-menu .dropdown-menu
%ul %ul
- @breadcrumb_dropdown_links[dropdown_location].each_with_index do |link, index| - @breadcrumb_dropdown_links[dropdown_location].each_with_index do |link, index|
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.context-header .context-header
= link_to admin_root_path, title: 'Admin Overview' do = link_to admin_root_path, title: 'Admin Overview' do
.avatar-container.s40.settings-avatar .avatar-container.s40.settings-avatar
= icon('wrench') = sprite_icon('admin', size: 24)
.sidebar-context-title Admin Area .sidebar-context-title Admin Area
%ul.sidebar-top-level-items %ul.sidebar-top-level-items
= nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: {class: 'home'}) do = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: {class: 'home'}) do
...@@ -12,7 +12,6 @@ ...@@ -12,7 +12,6 @@
= sprite_icon('overview') = sprite_icon('overview')
%span.nav-item-name %span.nav-item-name
Overview Overview
%ul.sidebar-sub-level-items %ul.sidebar-sub-level-items
= nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: { class: "fly-out-top-item" } ) do = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: { class: "fly-out-top-item" } ) do
= link_to admin_root_path do = link_to admin_root_path do
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index'] - issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index']
- if @group.feature_available?(:group_issue_boards) - if @group.feature_available?(:group_issue_boards)
- issues_sub_menu_items.push('boards#index') - issues_sub_menu_items.push('boards#index', 'boards#show')
.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll .nav-sidebar-inner-scroll
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
List List
- if @group.feature_available?(:group_issue_boards) - if @group.feature_available?(:group_issue_boards)
= nav_link(path: 'boards#index') do = nav_link(path: ['boards#index', 'boards#show']) do
= link_to group_boards_path(@group), title: 'Boards' do = link_to group_boards_path(@group), title: 'Boards' do
%span %span
Boards Boards
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.context-header .context-header
= link_to profile_path, title: 'Profile Settings' do = link_to profile_path, title: 'Profile Settings' do
.avatar-container.s40.settings-avatar .avatar-container.s40.settings-avatar
= icon('user') = sprite_icon('user', size: 24)
.sidebar-context-title User Settings .sidebar-context-title User Settings
%ul.sidebar-top-level-items %ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
......
- form = local_assigns.fetch(:form)
- project = local_assigns.fetch(:project)
.radio
= label_tag :project_merge_method_ff do
= form.radio_button :merge_method, :ff, class: "js-merge-method-radio"
%strong Fast-forward merge
%br
%span.descr
No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.
%br
%span.descr
When fast-forward merge is not possible, the user must first rebase locally.
- form = local_assigns.fetch(:form)
.radio
= label_tag :project_merge_method_rebase_merge do
= form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio"
%strong Merge commit with semi-linear history
%br
%span.descr
A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible.
This way you could make sure that if this merge request would build, after merging to target branch it would also build.
%br
%span.descr
When fast-forward merge is not possible, the user must first rebase locally.
- form = local_assigns.fetch(:form) - form = local_assigns.fetch(:form)
= render 'projects/ee/merge_request_settings', form: form, project: @project .form-group
= label_tag :merge_method_merge, class: 'label-light' do
Merge method
.radio
= label_tag :project_merge_method_merge do
= form.radio_button :merge_method, :merge, class: "js-merge-method-radio"
%strong Merge commit
%br
%span.descr
A merge commit is created for every merge, and merging is allowed as long as there are no conflicts.
= render 'merge_request_rebase_settings', form: form
= render 'merge_request_fast_forward_settings', project: @project, form: form
= render 'projects/merge_request_merge_settings', form: form = render 'projects/merge_request_merge_settings', form: form
- blob = file.blob
- path_to_file = file_project_job_artifacts_path(@project, @build, path: file.path) - path_to_file = file_project_job_artifacts_path(@project, @build, path: file.path)
- external_link = blob.external_link?(@build)
%tr.tree-item{ 'data-link' => path_to_file } %tr.tree-item.js-artifact-tree-row{ data: { link: path_to_file, external_link: "#{external_link}" } }
- blob = file.blob
%td.tree-item-file-name %td.tree-item-file-name
= tree_icon('file', blob.mode, blob.name) = tree_icon('file', blob.mode, blob.name)
%span.str-truncated - if external_link
= link_to file.name, path_to_file = link_to path_to_file, class: 'tree-item-file-external-link js-artifact-tree-tooltip',
target: '_blank', rel: 'noopener noreferrer', title: _('Opens in a new window') do
%span.str-truncated>= blob.name
= icon('external-link', class: 'js-artifact-tree-external-icon')
- else
= link_to path_to_file do
%span.str-truncated= blob.name
%td %td
= number_to_human_size(blob.size, precision: 2) = number_to_human_size(blob.size, precision: 2)
...@@ -4,12 +4,11 @@ ...@@ -4,12 +4,11 @@
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn has-tooltip' do = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn has-tooltip' do
= custom_icon('icon_fork') = custom_icon('icon_fork')
%span= s_('GoToYourFork|Fork') %span= s_('GoToYourFork|Fork')
- elsif !current_user.can_create_project?
= link_to new_project_fork_path(@project), title: _('You have reached your project limit'), class: 'btn has-tooltip disabled' do
= custom_icon('icon_fork')
%span= s_('CreateNewFork|Fork')
- else - else
= link_to new_project_fork_path(@project), class: 'btn' do - can_create_fork = current_user.can?(:create_fork)
= link_to new_project_fork_path(@project),
class: "btn btn-default #{'has-tooltip disabled' unless can_create_fork}",
title: (_('You have reached your project limit') unless can_create_fork) do
= custom_icon('icon_fork') = custom_icon('icon_fork')
%span= s_('CreateNewFork|Fork') %span= s_('CreateNewFork|Fork')
.count-with-arrow .count-with-arrow
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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