Commit 9e42df3c authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into...

Merge remote-tracking branch 'upstream/master' into gem/sm/bump-google-api-client-gem-from-0-8-6-to-0-13-6

* upstream/master: (186 commits)
  Resolve "Precompiled assets with digest strings are ignored in CI"
  Fix navigation dropdown close animation on mobile screens
  Add (partial) index on Labels.template
  Add grpc.log for Gitaly
  Fix gitaly-proto version in Gemfile.lock
  refactor issues_controller_spec `update` action
  Remove edit action for issues
  Add documentation to summarise project archiving
  Fix for Gitaly nil encoding issue
  Fix broken certificate-authority-data with kubectl >= 1.8.0
  Improve performance of filtering notes in NotesController
  Bump gitaly-proto version to v0.39.0
  Fix specs for project creation and update services
  Add CHANGELOG
  Doesn't check if path exists on disk when renaming a hashed project
  [ci skip] Fix archive spec descrptions
  Only copy old/new code when selecting left/right side of parallel diff
  Fix gitlab-rake gitlab:import:repos task
  Send extra Gitaly params for `send_git_archive` if needed
  Update GitLab Pages to v0.6.0
  ...
parents e9ffa339 2828b768
...@@ -63,4 +63,5 @@ eslint-report.html ...@@ -63,4 +63,5 @@ eslint-report.html
/.gitlab_workhorse_secret /.gitlab_workhorse_secret
/webpack-report/ /webpack-report/
/locale/**/LC_MESSAGES /locale/**/LC_MESSAGES
/locale/**/*.time_stamp
/.rspec /.rspec
...@@ -404,6 +404,7 @@ docs lint: ...@@ -404,6 +404,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
......
...@@ -85,6 +85,8 @@ entry. ...@@ -85,6 +85,8 @@ entry.
- [FIXED] Fixed merge request changes bar jumping. - [FIXED] Fixed merge request changes bar jumping.
- [FIXED] Improve migrations using triggers. - [FIXED] Improve migrations using triggers.
- [FIXED] Fix ConvDev Index nav item and Monitoring submenu regression. - [FIXED] Fix ConvDev Index nav item and Monitoring submenu regression.
- [FIXED] disabling notifications globally now properly turns off group/project added
emails !13325
- [DEPRECATED] Deprecate custom SSH client configuration for the git user. !13930 - [DEPRECATED] Deprecate custom SSH client configuration for the git user. !13930
- [CHANGED] allow all users to delete their account. !13636 (Jacopo Beschi @jacopo-beschi) - [CHANGED] allow all users to delete their account. !13636 (Jacopo Beschi @jacopo-beschi)
- [CHANGED] Use full path of project's avatar in webhooks. !13649 (Vitaliy @blackst0ne Klachkov) - [CHANGED] Use full path of project's avatar in webhooks. !13649 (Vitaliy @blackst0ne Klachkov)
...@@ -193,6 +195,13 @@ entry. ...@@ -193,6 +195,13 @@ 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.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)
......
...@@ -49,7 +49,7 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ ...@@ -49,7 +49,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
...@@ -101,7 +101,7 @@ the remaining issues on the GitHub issue tracker. ...@@ -101,7 +101,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.
...@@ -209,8 +209,7 @@ We add the ~"Accepting Merge Requests" label to: ...@@ -209,8 +209,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
...@@ -223,6 +222,17 @@ know how difficult the issue is. Additionally: ...@@ -223,6 +222,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
......
...@@ -398,7 +398,7 @@ group :ed25519 do ...@@ -398,7 +398,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.33.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.39.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -273,7 +273,7 @@ GEM ...@@ -273,7 +273,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.33.0) gitaly-proto (0.39.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -1027,7 +1027,7 @@ DEPENDENCIES ...@@ -1027,7 +1027,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.33.0) gitaly-proto (~> 0.39.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
......
{"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.
...@@ -40,10 +40,10 @@ export default () => { ...@@ -40,10 +40,10 @@ export default () => {
class="text-center" class="text-center"
v-if="error"> v-if="error">
<span v-if="loadError"> <span v-if="loadError">
An error occured whilst loading the file. Please try again later. An error occurred whilst loading the file. Please try again later.
</span> </span>
<span v-else> <span v-else>
An error occured whilst parsing the file. An error occurred whilst parsing the file.
</span> </span>
</p> </p>
</div> </div>
......
...@@ -48,10 +48,10 @@ export default () => { ...@@ -48,10 +48,10 @@ export default () => {
class="text-center" class="text-center"
v-if="error"> v-if="error">
<span v-if="loadError"> <span v-if="loadError">
An error occured whilst loading the file. Please try again later. An error occurred whilst loading the file. Please try again later.
</span> </span>
<span v-else> <span v-else>
An error occured whilst decoding the file. An error occurred whilst decoding the file.
</span> </span>
</p> </p>
</div> </div>
......
...@@ -68,7 +68,7 @@ export default { ...@@ -68,7 +68,7 @@ export default {
<div class="flash-container" <div class="flash-container"
v-if="error"> v-if="error">
<div class="flash-alert"> <div class="flash-alert">
An error occured. Please try again. An error occurred. Please try again.
</div> </div>
</div> </div>
<label class="label-light" <label class="label-light"
......
...@@ -167,7 +167,7 @@ window.Build = (function () { ...@@ -167,7 +167,7 @@ window.Build = (function () {
Build.prototype.getBuildTrace = function () { Build.prototype.getBuildTrace = function () {
return $.ajax({ return $.ajax({
url: `${this.pageUrl}/trace.json`, url: `${this.pageUrl}/trace.json`,
data: this.state, data: { state: this.state },
}) })
.done((log) => { .done((log) => {
setCiStatusFavicon(`${this.pageUrl}/status.json`); setCiStatusFavicon(`${this.pageUrl}/status.json`);
......
...@@ -298,7 +298,7 @@ class CopyAsGFM { ...@@ -298,7 +298,7 @@ class CopyAsGFM {
const documentFragment = getSelectedFragment(); const documentFragment = getSelectedFragment();
if (!documentFragment) return; if (!documentFragment) return;
const el = transformer(documentFragment.cloneNode(true)); const el = transformer(documentFragment.cloneNode(true), e.currentTarget);
if (!el) return; if (!el) return;
e.preventDefault(); e.preventDefault();
...@@ -338,55 +338,64 @@ class CopyAsGFM { ...@@ -338,55 +338,64 @@ class CopyAsGFM {
} }
static transformGFMSelection(documentFragment) { static transformGFMSelection(documentFragment) {
const gfmEls = documentFragment.querySelectorAll('.md, .wiki'); const gfmElements = documentFragment.querySelectorAll('.md, .wiki');
switch (gfmEls.length) { switch (gfmElements.length) {
case 0: { case 0: {
return documentFragment; return documentFragment;
} }
case 1: { case 1: {
return gfmEls[0]; return gfmElements[0];
} }
default: { default: {
const allGfmEl = document.createElement('div'); const allGfmElement = document.createElement('div');
for (let i = 0; i < gfmEls.length; i += 1) { for (let i = 0; i < gfmElements.length; i += 1) {
const lineEl = gfmEls[i]; const gfmElement = gfmElements[i];
allGfmEl.appendChild(lineEl); allGfmElement.appendChild(gfmElement);
allGfmEl.appendChild(document.createTextNode('\n\n')); allGfmElement.appendChild(document.createTextNode('\n\n'));
} }
return allGfmEl; return allGfmElement;
} }
} }
} }
static transformCodeSelection(documentFragment) { static transformCodeSelection(documentFragment, target) {
const lineEls = documentFragment.querySelectorAll('.line'); let lineSelector = '.line';
let codeEl; if (target) {
if (lineEls.length > 1) { const lineClass = ['left-side', 'right-side'].filter(name => target.classList.contains(name))[0];
codeEl = document.createElement('pre'); if (lineClass) {
codeEl.className = 'code highlight'; lineSelector = `.line_content.${lineClass} ${lineSelector}`;
}
}
const lineElements = documentFragment.querySelectorAll(lineSelector);
let codeElement;
if (lineElements.length > 1) {
codeElement = document.createElement('pre');
codeElement.className = 'code highlight';
const lang = lineEls[0].getAttribute('lang'); const lang = lineElements[0].getAttribute('lang');
if (lang) { if (lang) {
codeEl.setAttribute('lang', lang); codeElement.setAttribute('lang', lang);
} }
} else { } else {
codeEl = document.createElement('code'); codeElement = document.createElement('code');
} }
if (lineEls.length > 0) { if (lineElements.length > 0) {
for (let i = 0; i < lineEls.length; i += 1) { for (let i = 0; i < lineElements.length; i += 1) {
const lineEl = lineEls[i]; const lineElement = lineElements[i];
codeEl.appendChild(lineEl); codeElement.appendChild(lineElement);
codeEl.appendChild(document.createTextNode('\n')); codeElement.appendChild(document.createTextNode('\n'));
} }
} else { } else {
codeEl.appendChild(documentFragment); codeElement.appendChild(documentFragment);
} }
return codeEl; return codeElement;
} }
static nodeToGFM(node, respectWhitespaceParam = false) { static nodeToGFM(node, respectWhitespaceParam = false) {
......
<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);
}); });
...@@ -24,7 +24,8 @@ class Diff { ...@@ -24,7 +24,8 @@ class Diff {
if (!isBound) { if (!isBound) {
$(document) $(document)
.on('click', '.js-unfold', this.handleClickUnfold.bind(this)) .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
.on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this))
.on('mousedown', 'td.line_content.parallel', this.handleParallelLineDown.bind(this));
isBound = true; isBound = true;
} }
...@@ -100,6 +101,18 @@ class Diff { ...@@ -100,6 +101,18 @@ class Diff {
this.highlightSelectedLine(); this.highlightSelectedLine();
} }
handleParallelLineDown(e) {
const line = $(e.currentTarget);
const table = line.closest('table');
table.removeClass('left-side-selected right-side-selected');
const lineClass = ['left-side', 'right-side'].filter(name => line.hasClass(name))[0];
if (lineClass) {
table.addClass(`${lineClass}-selected`);
}
}
diffViewType() { diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type'); return $('.inline-parallel-buttons a.active').data('view-type');
} }
......
...@@ -14,7 +14,6 @@ ...@@ -14,7 +14,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 */
...@@ -476,7 +475,9 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; ...@@ -476,7 +475,9 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
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();
......
...@@ -163,7 +163,7 @@ export default { ...@@ -163,7 +163,7 @@ export default {
this.service.postAction(endpoint) this.service.postAction(endpoint)
.then(() => this.fetchEnvironments()) .then(() => this.fetchEnvironments())
.catch(() => new Flash('An error occured while making the request.')); .catch(() => new Flash('An error occurred while making the request.'));
} }
}, },
......
...@@ -158,7 +158,7 @@ export default { ...@@ -158,7 +158,7 @@ export default {
this.service.postAction(endpoint) this.service.postAction(endpoint)
.then(() => this.fetchEnvironments()) .then(() => this.fetchEnvironments())
.catch(() => new Flash('An error occured while making the request.')); .catch(() => new Flash('An error occurred while making the request.'));
} }
}, },
}, },
......
...@@ -14,7 +14,7 @@ class DropdownEmoji extends gl.FilteredSearchDropdown { ...@@ -14,7 +14,7 @@ class DropdownEmoji extends gl.FilteredSearchDropdown {
loadingTemplate: this.loadingTemplate, loadingTemplate: this.loadingTemplate,
onError() { onError() {
/* eslint-disable no-new */ /* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.'); new Flash('An error occurred fetching the dropdown data.');
/* eslint-enable no-new */ /* eslint-enable no-new */
}, },
}, },
......
...@@ -17,7 +17,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown { ...@@ -17,7 +17,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown {
preprocessing, preprocessing,
onError() { onError() {
/* eslint-disable no-new */ /* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.'); new Flash('An error occurred fetching the dropdown data.');
/* eslint-enable no-new */ /* eslint-enable no-new */
}, },
}, },
......
...@@ -26,7 +26,7 @@ class DropdownUser extends gl.FilteredSearchDropdown { ...@@ -26,7 +26,7 @@ class DropdownUser extends gl.FilteredSearchDropdown {
}, },
onError() { onError() {
/* eslint-disable no-new */ /* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.'); new Flash('An error occurred fetching the dropdown data.');
/* eslint-enable no-new */ /* eslint-enable no-new */
}, },
}, },
......
...@@ -36,7 +36,7 @@ class FilteredSearchManager { ...@@ -36,7 +36,7 @@ class FilteredSearchManager {
.catch((error) => { .catch((error) => {
if (error.name === 'RecentSearchesServiceError') return undefined; if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new window.Flash('An error occured while parsing recent searches'); new window.Flash('An error occurred while parsing recent searches');
// Gracefully fail to empty array // Gracefully fail to empty array
return []; return [];
}) })
......
...@@ -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')],
......
...@@ -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>`,
...@@ -75,6 +74,7 @@ ...@@ -75,6 +74,7 @@
</span> </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">
......
...@@ -97,7 +97,7 @@ export default { ...@@ -97,7 +97,7 @@ export default {
postAction(endpoint) { postAction(endpoint) {
this.service.postAction(endpoint) this.service.postAction(endpoint)
.then(() => eventHub.$emit('refreshPipelines')) .then(() => eventHub.$emit('refreshPipelines'))
.catch(() => new Flash('An error occured while making the request.')); .catch(() => new Flash('An error occurred while making the request.'));
}, },
}, },
}; };
/* 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',
......
...@@ -37,14 +37,14 @@ export default { ...@@ -37,14 +37,14 @@ export default {
content: f.newContent, content: f.newContent,
})); }));
const payload = { const payload = {
branch: Store.targetBranch, branch: Store.currentBranch,
commit_message: commitMessage, commit_message: commitMessage,
actions, actions,
}; };
Store.submitCommitsLoading = true; Store.submitCommitsLoading = true;
Service.commitFiles(payload) Service.commitFiles(payload)
.then(this.resetCommitState) .then(this.resetCommitState)
.catch(() => Flash('An error occured while committing your changes')); .catch(() => Flash('An error occurred while committing your changes'));
}, },
resetCommitState() { resetCommitState() {
...@@ -105,7 +105,7 @@ export default { ...@@ -105,7 +105,7 @@ export default {
</label> </label>
<div class="col-md-6"> <div class="col-md-6">
<span class="help-block"> <span class="help-block">
{{targetBranch}} {{currentBranch}}
</span> </span>
</div> </div>
</div> </div>
......
...@@ -26,16 +26,6 @@ export default { ...@@ -26,16 +26,6 @@ export default {
this.editMode = !this.editMode; this.editMode = !this.editMode;
Store.toggleBlobView(); Store.toggleBlobView();
}, },
toggleProjectRefsForm() {
$('.project-refs-form').toggleClass('disabled', this.editMode);
$('.js-tree-ref-target-holder').toggle(this.editMode);
},
},
watch: {
editMode() {
this.toggleProjectRefsForm();
},
}, },
}; };
</script> </script>
......
...@@ -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)">
......
...@@ -49,7 +49,7 @@ export default { ...@@ -49,7 +49,7 @@ export default {
v-else v-else
class="vertical-center render-error"> class="vertical-center render-error">
<p class="text-center"> <p class="text-center">
The source could not be displayed because a rendering error occured. You can <a :href="activeFile.raw_path">download</a> it instead. The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.raw_path">download</a> it instead.
</p> </p>
</div> </div>
</div> </div>
......
...@@ -37,17 +37,24 @@ export default { ...@@ -37,17 +37,24 @@ export default {
let file = clickedFile; let file = clickedFile;
if (file.loading) return; if (file.loading) return;
file.loading = true; file.loading = true;
if (file.type === 'tree' && file.opened) { if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file); file = Store.removeChildFilesOfTree(file);
file.loading = false; file.loading = false;
} else { } else {
Service.url = file.url; const openFile = Helper.getFileFromPath(file.url);
Helper.getContent(file) if (openFile) {
.then(() => { file.loading = false;
file.loading = false; Store.setActiveFiles(openFile);
Helper.scrollTabsRight(); } else {
}) Service.url = file.url;
.catch(Helper.loadingError); Helper.getContent(file)
.then(() => {
file.loading = false;
Helper.scrollTabsRight();
})
.catch(Helper.loadingError);
}
} }
}, },
...@@ -68,7 +75,7 @@ export default { ...@@ -68,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;
} }
}, },
...@@ -263,6 +265,10 @@ const RepoHelper = { ...@@ -263,6 +265,10 @@ const RepoHelper = {
return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url); return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url);
}, },
getFileFromPath(path) {
return Store.openedFiles.find(file => file.url === path);
},
loadingError() { loadingError() {
Flash('Unable to load this content at this time.'); Flash('Unable to load this content at this time.');
}, },
......
...@@ -11,10 +11,6 @@ function initDropdowns() { ...@@ -11,10 +11,6 @@ function initDropdowns() {
} }
function addEventsForNonVueEls() { function addEventsForNonVueEls() {
$(document).on('change', '.dropdown', () => {
Store.targetBranch = $('.project-refs-target-form input[name="ref"]').val();
});
window.onbeforeunload = function confirmUnload(e) { window.onbeforeunload = function confirmUnload(e) {
const hasChanged = Store.openedFiles const hasChanged = Store.openedFiles
.some(file => file.changed); .some(file => file.changed);
......
...@@ -32,7 +32,6 @@ const RepoStore = { ...@@ -32,7 +32,6 @@ const RepoStore = {
isCommitable: false, isCommitable: false,
binary: false, binary: false,
currentBranch: '', currentBranch: '',
targetBranch: 'new-branch',
commitMessage: '', commitMessage: '',
binaryTypes: { binaryTypes: {
png: false, png: false,
...@@ -84,7 +83,7 @@ const RepoStore = { ...@@ -84,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();
} }
......
...@@ -38,7 +38,7 @@ class SidebarMoveIssue { ...@@ -38,7 +38,7 @@ class SidebarMoveIssue {
data: (searchTerm, callback) => { data: (searchTerm, callback) => {
this.mediator.fetchAutocompleteProjects(searchTerm) this.mediator.fetchAutocompleteProjects(searchTerm)
.then(callback) .then(callback)
.catch(() => new Flash('An error occured while fetching projects autocomplete.')); .catch(() => new Flash('An error occurred while fetching projects autocomplete.'));
}, },
renderRow: project => ` renderRow: project => `
<li> <li>
...@@ -73,7 +73,7 @@ class SidebarMoveIssue { ...@@ -73,7 +73,7 @@ class SidebarMoveIssue {
this.mediator.moveIssue() this.mediator.moveIssue()
.catch(() => { .catch(() => {
Flash('An error occured while moving the issue.'); Flash('An error occurred while moving the issue.');
this.$confirmButton this.$confirmButton
.enable() .enable()
.removeClass('is-loading'); .removeClass('is-loading');
......
...@@ -41,7 +41,7 @@ export default class SidebarMediator { ...@@ -41,7 +41,7 @@ export default class SidebarMediator {
this.store.setAssigneeData(data); this.store.setAssigneeData(data);
this.store.setTimeTrackingData(data); this.store.setTimeTrackingData(data);
}) })
.catch(() => new Flash('Error occured when fetching sidebar data')); .catch(() => new Flash('Error occurred when fetching sidebar data'));
} }
fetchAutocompleteProjects(searchTerm) { fetchAutocompleteProjects(searchTerm) {
......
...@@ -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>
......
...@@ -16,9 +16,9 @@ export default { ...@@ -16,9 +16,9 @@ export default {
<div class="media-body"> <div class="media-body">
<mr-widget-author-and-time <mr-widget-author-and-time
actionText="Closed by" actionText="Closed by"
:author="mr.closedBy" :author="mr.closedEvent.author"
:dateTitle="mr.updatedAt" :dateTitle="mr.closedEvent.updatedAt"
:dateReadable="mr.closedAt" :dateReadable="mr.closedEvent.formattedUpdatedAt"
/> />
<section class="mr-info-list"> <section class="mr-info-list">
<p> <p>
......
...@@ -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>
`, `,
......
...@@ -69,9 +69,9 @@ export default { ...@@ -69,9 +69,9 @@ export default {
<div class="space-children"> <div class="space-children">
<mr-widget-author-and-time <mr-widget-author-and-time
actionText="Merged by" actionText="Merged by"
:author="mr.mergedBy" :author="mr.mergedEvent.author"
:dateTitle="mr.updatedAt" :date-title="mr.mergedEvent.updatedAt"
:dateReadable="mr.mergedAt" /> :date-readable="mr.mergedEvent.formattedUpdatedAt" />
<a <a
v-if="mr.canRevertInCurrentMR" v-if="mr.canRevertInCurrentMR"
v-tooltip v-tooltip
......
...@@ -284,10 +284,16 @@ export default { ...@@ -284,10 +284,16 @@ export default {
:mr="mr" :mr="mr"
:is-merge-button-disabled="isMergeButtonDisabled" /> :is-merge-button-disabled="isMergeButtonDisabled" />
<span
v-if="mr.ffOnlyEnabled"
class="js-fast-forward-message">
Fast-forward merge without a merge commit
</span>
<button <button
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>
......
...@@ -37,10 +37,8 @@ export default class MergeRequestStore { ...@@ -37,10 +37,8 @@ export default class MergeRequestStore {
} }
this.updatedAt = data.updated_at; this.updatedAt = data.updated_at;
this.mergedAt = MergeRequestStore.getEventDate(data.merge_event); this.mergedEvent = MergeRequestStore.getEventObject(data.merge_event);
this.closedAt = MergeRequestStore.getEventDate(data.closed_event); this.closedEvent = MergeRequestStore.getEventObject(data.closed_event);
this.mergedBy = MergeRequestStore.getAuthorObject(data.merge_event);
this.closedBy = MergeRequestStore.getAuthorObject(data.closed_event);
this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} }); this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} });
this.mergeUserId = data.merge_user_id; this.mergeUserId = data.merge_user_id;
this.currentUserId = gon.current_user_id; this.currentUserId = gon.current_user_id;
...@@ -57,6 +55,8 @@ export default class MergeRequestStore { ...@@ -57,6 +55,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;
...@@ -118,6 +118,14 @@ export default class MergeRequestStore { ...@@ -118,6 +118,14 @@ export default class MergeRequestStore {
} }
} }
static getEventObject(event) {
return {
author: MergeRequestStore.getAuthorObject(event),
updatedAt: gl.utils.formatDate(MergeRequestStore.getEventUpdatedAtDate(event)),
formattedUpdatedAt: MergeRequestStore.getEventDate(event),
};
}
static getAuthorObject(event) { static getAuthorObject(event) {
if (!event) { if (!event) {
return {}; return {};
...@@ -131,6 +139,14 @@ export default class MergeRequestStore { ...@@ -131,6 +139,14 @@ export default class MergeRequestStore {
}; };
} }
static getEventUpdatedAtDate(event) {
if (!event) {
return '';
}
return event.updated_at;
}
static getEventDate(event) { static getEventDate(event) {
const timeagoInstance = new Timeago(); const timeagoInstance = new Timeago();
...@@ -138,7 +154,7 @@ export default class MergeRequestStore { ...@@ -138,7 +154,7 @@ export default class MergeRequestStore {
return ''; return '';
} }
return timeagoInstance.format(event.updated_at); return timeagoInstance.format(MergeRequestStore.getEventUpdatedAtDate(event));
} }
} }
...@@ -873,6 +873,13 @@ ...@@ -873,6 +873,13 @@
min-width: 100%; min-width: 100%;
} }
} }
header.navbar-gitlab-new .header-content .dropdown {
.dropdown-menu {
left: 0;
min-width: 100%;
}
}
} }
@include new-style-dropdown('.breadcrumbs-list .dropdown '); @include new-style-dropdown('.breadcrumbs-list .dropdown ');
......
...@@ -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); }
......
...@@ -229,6 +229,10 @@ ul.content-list { ...@@ -229,6 +229,10 @@ ul.content-list {
.label-default { .label-default {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
.avatar-cell {
align-self: flex-start;
}
} }
.panel > .content-list > li { .panel > .content-list > li {
......
...@@ -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;
} }
} }
} }
......
...@@ -77,6 +77,18 @@ ...@@ -77,6 +77,18 @@
word-wrap: break-word; word-wrap: break-word;
} }
} }
&.left-side-selected {
td.line_content.parallel.right-side {
@include user-select(none);
}
}
&.right-side-selected {
td.line_content.parallel.left-side {
@include user-select(none);
}
}
} }
tr.line_holder.parallel { tr.line_holder.parallel {
......
...@@ -516,7 +516,7 @@ a.deploy-project-label { ...@@ -516,7 +516,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;
...@@ -543,6 +543,15 @@ a.deploy-project-label { ...@@ -543,6 +543,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;
} }
......
...@@ -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
...@@ -335,4 +337,9 @@ class ApplicationController < ActionController::Base ...@@ -335,4 +337,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
......
...@@ -15,9 +15,9 @@ module NotesActions ...@@ -15,9 +15,9 @@ module NotesActions
notes = notes_finder.execute notes = notes_finder.execute
.inc_relations_for_view .inc_relations_for_view
.reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes = prepare_notes_for_rendering(notes) notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes_json[:notes] = notes_json[:notes] =
if noteable.discussions_rendered_on_frontend? if noteable.discussions_rendered_on_frontend?
......
...@@ -14,6 +14,7 @@ class ConfirmationsController < Devise::ConfirmationsController ...@@ -14,6 +14,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
......
...@@ -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
......
...@@ -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]
...@@ -63,10 +63,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -63,10 +63,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)
...@@ -126,10 +122,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -126,10 +122,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
......
...@@ -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)
......
...@@ -344,6 +344,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -344,6 +344,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
......
...@@ -42,10 +42,12 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -42,10 +42,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
......
...@@ -13,6 +13,8 @@ class SessionsController < Devise::SessionsController ...@@ -13,6 +13,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]
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
...@@ -29,12 +31,13 @@ class SessionsController < Devise::SessionsController ...@@ -29,12 +31,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
...@@ -42,6 +45,16 @@ class SessionsController < Devise::SessionsController ...@@ -42,6 +45,16 @@ class SessionsController < Devise::SessionsController
private private
def log_failed_login
return unless 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
...@@ -123,7 +136,8 @@ class SessionsController < Devise::SessionsController ...@@ -123,7 +136,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
......
module CustomAttributesFilter
def by_custom_attributes(items)
return items unless params[:custom_attributes].is_a?(Hash)
return items unless Ability.allowed?(current_user, :read_custom_attribute)
association = items.reflect_on_association(:custom_attributes)
attributes_table = association.klass.arel_table
attributable_table = items.model.arel_table
custom_attributes = association.klass.select('true').where(
attributes_table[association.foreign_key]
.eq(attributable_table[association.association_primary_key])
)
# perform a subquery for each attribute to be filtered
params[:custom_attributes].inject(items) do |scope, (key, value)|
scope.where('EXISTS (?)', custom_attributes.where(key: key, value: value))
end
end
end
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
# #
class UsersFinder class UsersFinder
include CreatedAtFilter include CreatedAtFilter
include CustomAttributesFilter
attr_accessor :current_user, :params attr_accessor :current_user, :params
...@@ -32,6 +33,7 @@ class UsersFinder ...@@ -32,6 +33,7 @@ class UsersFinder
users = by_external_identity(users) users = by_external_identity(users)
users = by_external(users) users = by_external(users)
users = by_created_at(users) users = by_created_at(users)
users = by_custom_attributes(users)
users users
end end
......
...@@ -10,11 +10,7 @@ module BreadcrumbsHelper ...@@ -10,11 +10,7 @@ module BreadcrumbsHelper
def breadcrumb_title_link def breadcrumb_title_link
return @breadcrumb_link if @breadcrumb_link return @breadcrumb_link if @breadcrumb_link
if controller.available_action?(:index) request.path
url_for(action: "index")
else
request.path
end
end end
def breadcrumb_title(title) def breadcrumb_title(title)
...@@ -25,7 +21,7 @@ module BreadcrumbsHelper ...@@ -25,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
......
...@@ -33,19 +33,21 @@ module DiffHelper ...@@ -33,19 +33,21 @@ module DiffHelper
end end
def diff_match_line(old_pos, new_pos, text: '', view: :inline, bottom: false) def diff_match_line(old_pos, new_pos, text: '', view: :inline, bottom: false)
content = content_tag :td, text, class: "line_content match #{view == :inline ? '' : view}" content_line_class = %w[line_content match]
cls = ['diff-line-num', 'unfold', 'js-unfold'] content_line_class << 'parallel' if view == :parallel
cls << 'js-unfold-bottom' if bottom
line_num_class = %w[diff-line-num unfold js-unfold]
line_num_class << 'js-unfold-bottom' if bottom
html = '' html = ''
if old_pos if old_pos
html << content_tag(:td, '...', class: cls + ['old_line'], data: { linenumber: old_pos }) html << content_tag(:td, '...', class: [*line_num_class, 'old_line'], data: { linenumber: old_pos })
html << content unless view == :inline html << content_tag(:td, text, class: [*content_line_class, 'left-side']) if view == :parallel
end end
if new_pos if new_pos
html << content_tag(:td, '...', class: cls + ['new_line'], data: { linenumber: new_pos }) html << content_tag(:td, '...', class: [*line_num_class, 'new_line'], data: { linenumber: new_pos })
html << content html << content_tag(:td, text, class: [*content_line_class, ('right-side' if view == :parallel)])
end end
html.html_safe html.html_safe
......
...@@ -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 = {})
......
...@@ -248,16 +248,25 @@ module IssuablesHelper ...@@ -248,16 +248,25 @@ module IssuablesHelper
Gitlab::IssuablesCountForState.new(finder)[state] Gitlab::IssuablesCountForState.new(finder)[state]
end end
def close_issuable_url(issuable) def close_issuable_path(issuable)
issuable_url(issuable, close_reopen_params(issuable, :close)) issuable_path(issuable, close_reopen_params(issuable, :close))
end end
def reopen_issuable_url(issuable) def reopen_issuable_path(issuable)
issuable_url(issuable, close_reopen_params(issuable, :reopen)) issuable_path(issuable, close_reopen_params(issuable, :reopen))
end end
def close_reopen_issuable_url(issuable, should_inverse = false) def close_reopen_issuable_path(issuable, should_inverse = false)
issuable.closed? ^ should_inverse ? reopen_issuable_url(issuable) : close_issuable_url(issuable) issuable.closed? ^ should_inverse ? reopen_issuable_path(issuable) : close_issuable_path(issuable)
end
def issuable_path(issuable, *options)
case issuable
when Issue
issue_path(issuable, *options)
when MergeRequest
merge_request_path(issuable, *options)
end
end end
def issuable_url(issuable, *options) def issuable_url(issuable, *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
......
...@@ -21,11 +21,14 @@ module ProjectsHelper ...@@ -21,11 +21,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
......
...@@ -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
......
...@@ -524,6 +524,14 @@ class MergeRequest < ActiveRecord::Base ...@@ -524,6 +524,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
......
...@@ -64,6 +64,7 @@ class Project < ActiveRecord::Base ...@@ -64,6 +64,7 @@ class Project < ActiveRecord::Base
# Storage specific hooks # Storage specific hooks
after_initialize :use_hashed_storage after_initialize :use_hashed_storage
after_create :check_repository_absence!
after_create :ensure_storage_path_exists after_create :ensure_storage_path_exists
after_save :ensure_storage_path_exists, if: :namespace_id_changed? after_save :ensure_storage_path_exists, if: :namespace_id_changed?
...@@ -72,6 +73,7 @@ class Project < ActiveRecord::Base ...@@ -72,6 +73,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
...@@ -227,7 +229,7 @@ class Project < ActiveRecord::Base ...@@ -227,7 +229,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: :update, if: ->(project) { 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 }
...@@ -245,6 +247,9 @@ class Project < ActiveRecord::Base ...@@ -245,6 +247,9 @@ class Project < ActiveRecord::Base
scope :pending_delete, -> { where(pending_delete: true) } scope :pending_delete, -> { where(pending_delete: true) }
scope :without_deleted, -> { where(pending_delete: false) } scope :without_deleted, -> { where(pending_delete: false) }
scope :with_hashed_storage, -> { where('storage_version >= 1') }
scope :with_legacy_storage, -> { where(storage_version: [nil, 0]) }
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
...@@ -1015,12 +1020,15 @@ class Project < ActiveRecord::Base ...@@ -1015,12 +1020,15 @@ 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
if gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git") # Check if repository with same path already exists on disk we can
# skip this for the hashed storage because the path does not change
if legacy_storage? && repository_with_same_path_already_exists?
errors.add(:base, 'There is already a repository with that name on disk') errors.add(:base, 'There is already a repository with that name on disk')
return false return false
end end
...@@ -1032,7 +1040,7 @@ class Project < ActiveRecord::Base ...@@ -1032,7 +1040,7 @@ class Project < ActiveRecord::Base
# Forked import is handled asynchronously # Forked import is handled asynchronously
return if forked? && !force return if forked? && !force
if gitlab_shell.add_repository(repository_storage_path, disk_path) if gitlab_shell.add_repository(repository_storage, disk_path)
repository.after_create repository.after_create
true true
else else
...@@ -1550,18 +1558,72 @@ class Project < ActiveRecord::Base ...@@ -1550,18 +1558,72 @@ class Project < ActiveRecord::Base
end end
def legacy_storage? def legacy_storage?
self.storage_version.nil? [nil, 0].include?(self.storage_version)
end
def hashed_storage?
self.storage_version && self.storage_version >= 1
end end
def renamed? def renamed?
persisted? && path_changed? persisted? && path_changed?
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
def migrate_to_hashed_storage!
return if hashed_storage?
update!(repository_read_only: true)
if repo_reference_count > 0 || wiki_reference_count > 0
ProjectMigrateHashedStorageWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
else
ProjectMigrateHashedStorageWorker.perform_async(id)
end
end
def storage_version=(value)
super
@storage = nil if storage_version_changed?
end
def gl_repository(is_wiki:)
Gitlab::GlRepository.gl_repository(self, is_wiki)
end
private private
def storage def storage
@storage ||= @storage ||=
if self.storage_version && self.storage_version >= 1 if hashed_storage?
Storage::HashedProject.new(self) Storage::HashedProject.new(self)
else else
Storage::LegacyProject.new(self) Storage::LegacyProject.new(self)
...@@ -1574,6 +1636,27 @@ class Project < ActiveRecord::Base ...@@ -1574,6 +1636,27 @@ class Project < ActiveRecord::Base
end end
end end
def repo_reference_count
Gitlab::ReferenceCounter.new(gl_repository(is_wiki: false)).value
end
def wiki_reference_count
Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value
end
def check_repository_absence!
return if skip_disk_validation
if repository_storage_path.blank? || repository_with_same_path_already_exists?
errors.add(:base, 'There is already a repository with that name on disk')
throw :abort
end
end
def repository_with_same_path_already_exists?
gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git")
end
# set last_activity_at to the same as created_at # set last_activity_at to the same as created_at
def set_last_activity_at def set_last_activity_at
update_column(:last_activity_at, self.created_at) update_column(:last_activity_at, self.created_at)
......
...@@ -174,7 +174,7 @@ class ProjectWiki ...@@ -174,7 +174,7 @@ class ProjectWiki
private private
def init_repo(disk_path) def init_repo(disk_path)
gitlab_shell.add_repository(project.repository_storage_path, disk_path) gitlab_shell.add_repository(project.repository_storage, disk_path)
end end
def commit_details(action, message = nil, title = nil) def commit_details(action, message = nil, title = nil)
......
...@@ -34,7 +34,10 @@ class Repository ...@@ -34,7 +34,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
...@@ -91,12 +94,6 @@ class Repository ...@@ -91,12 +94,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
...@@ -275,7 +272,7 @@ class Repository ...@@ -275,7 +272,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
...@@ -346,7 +343,7 @@ class Repository ...@@ -346,7 +343,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
...@@ -523,9 +520,10 @@ class Repository ...@@ -523,9 +520,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
...@@ -852,6 +850,25 @@ class Repository ...@@ -852,6 +850,25 @@ class Repository
end end
end end
def ff_merge(user, source, target_branch, merge_request: nil)
our_commit = rugged.branches[target_branch].target
their_commit =
if source.is_a?(Gitlab::Git::Commit)
source.raw_commit
else
rugged.lookup(source)
end
raise 'Invalid merge target' if our_commit.nil?
raise 'Invalid merge source' if their_commit.nil?
with_branch(user, target_branch) do |start_commit|
merge_request&.update(in_progress_merge_commit_sha: their_commit.oid)
their_commit.oid
end
end
def revert( def revert(
user, commit, branch_name, message, user, commit, branch_name, message,
start_branch_name: nil, start_project: project) start_branch_name: nil, start_project: project)
......
...@@ -4,6 +4,7 @@ module Storage ...@@ -4,6 +4,7 @@ module Storage
delegate :gitlab_shell, :repository_storage_path, to: :project delegate :gitlab_shell, :repository_storage_path, to: :project
ROOT_PATH_PREFIX = '@hashed'.freeze ROOT_PATH_PREFIX = '@hashed'.freeze
STORAGE_VERSION = 1
def initialize(project) def initialize(project)
@project = project @project = project
......
...@@ -130,6 +130,8 @@ class User < ActiveRecord::Base ...@@ -130,6 +130,8 @@ class User < ActiveRecord::Base
has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
has_many :custom_attributes, class_name: 'UserCustomAttribute'
# #
# Validations # Validations
# #
...@@ -690,7 +692,11 @@ class User < ActiveRecord::Base ...@@ -690,7 +692,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
...@@ -1061,6 +1067,12 @@ class User < ActiveRecord::Base ...@@ -1061,6 +1067,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
......
class UserCustomAttribute < ActiveRecord::Base
belongs_to :user
validates :user_id, :key, :value, presence: true
validates :key, uniqueness: { scope: [:user_id] }
end
...@@ -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
...@@ -47,4 +53,9 @@ class GlobalPolicy < BasePolicy ...@@ -47,4 +53,9 @@ class GlobalPolicy < BasePolicy
rule { ~(anonymous & restricted_public_level) }.policy do rule { ~(anonymous & restricted_public_level) }.policy do
enable :read_users_list enable :read_users_list
end end
rule { admin }.policy do
enable :read_custom_attribute
enable :update_custom_attribute
end
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
...@@ -13,6 +13,11 @@ class MergeRequestEntity < IssuableEntity ...@@ -13,6 +13,11 @@ class MergeRequestEntity < IssuableEntity
expose :target_branch expose :target_branch
expose :target_project_id expose :target_project_id
expose :should_be_rebased?, as: :should_be_rebased
expose :ff_only_enabled do |merge_request|
merge_request.project.merge_requests_ff_only_enabled
end
# Events # Events
expose :merge_event, using: EventEntity expose :merge_event, using: EventEntity
expose :closed_event, using: EventEntity expose :closed_event, using: EventEntity
......
module MergeRequests
# MergeService class
#
# Do git fast-forward merge and in case of success
# mark merge request as merged and execute all hooks and notifications
# Executed when you do fast-forward merge via GitLab UI
#
class FfMergeService < MergeRequests::MergeService
private
def commit
repository.ff_merge(current_user,
source,
merge_request.target_branch,
merge_request: merge_request)
rescue Gitlab::Git::HooksService::PreReceiveError => e
raise MergeError, e.message
rescue StandardError => e
raise MergeError, "Something went wrong during merge: #{e.message}"
ensure
merge_request.update(in_progress_merge_commit_sha: nil)
end
end
end
...@@ -11,6 +11,11 @@ module MergeRequests ...@@ -11,6 +11,11 @@ module MergeRequests
attr_reader :merge_request, :source attr_reader :merge_request, :source
def execute(merge_request) def execute(merge_request)
if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
FfMergeService.new(project, current_user, params).execute(merge_request)
return
end
@merge_request = merge_request @merge_request = merge_request
unless @merge_request.mergeable? unless @merge_request.mergeable?
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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