Commit bd2822ce authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge remote-tracking branch 'origin/master' into sh-headless-chrome-support

parents b452b0c7 a7976905
...@@ -8,4 +8,4 @@ ...@@ -8,4 +8,4 @@
karma.config.js karma.config.js
webpack.config.js webpack.config.js
svg.config.js svg.config.js
/app/assets/javascripts/locale/**/*.js /app/assets/javascripts/locale/**/app.js
...@@ -2,6 +2,13 @@ ...@@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.0.2 (2017-09-27)
- [FIXED] Notes will not show an empty bubble when the author isn't a member. !14450
- [FIXED] Some checks in `rake gitlab:check` were failling with 'undefined method `run_command`'. !14469
- [FIXED] Make locked setting of Runner to not affect jobs scheduling. !14483
- [FIXED] Re-allow `name` attribute on user-provided anchor HTML.
## 10.0.1 (2017-09-23) ## 10.0.1 (2017-09-23)
- [FIXED] Fix duplicate key errors in PostDeployMigrateUserExternalMailData migration. - [FIXED] Fix duplicate key errors in PostDeployMigrateUserExternalMailData migration.
...@@ -78,6 +85,8 @@ entry. ...@@ -78,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)
......
...@@ -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,13 @@ know how difficult the issue is. Additionally: ...@@ -223,6 +222,13 @@ 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.
[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.38.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -276,7 +276,7 @@ GEM ...@@ -276,7 +276,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.38.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)
...@@ -1021,7 +1021,7 @@ DEPENDENCIES ...@@ -1021,7 +1021,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.38.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>
......
...@@ -77,9 +77,6 @@ $(() => { ...@@ -77,9 +77,6 @@ $(() => {
}); });
Store.rootPath = this.boardsEndpoint; Store.rootPath = this.boardsEndpoint;
this.filterManager = new FilteredSearchBoards(Store.filter, true);
this.filterManager.setup();
// Listen for updateTokens event // Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens); eventHub.$on('updateTokens', this.updateTokens);
}, },
...@@ -87,6 +84,9 @@ $(() => { ...@@ -87,6 +84,9 @@ $(() => {
eventHub.$off('updateTokens', this.updateTokens); eventHub.$off('updateTokens', this.updateTokens);
}, },
mounted () { mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true);
this.filterManager.setup();
Store.disabled = this.disabled; Store.disabled = this.disabled;
gl.boardService.all() gl.boardService.all()
.then(response => response.json()) .then(response => response.json())
......
...@@ -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`);
......
...@@ -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.'));
} }
}, },
}, },
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
* causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a * causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a
*/ */
import Cookies from 'js-cookie';
const LINE_NUMBER_CLASS = 'diff-line-num'; const LINE_NUMBER_CLASS = 'diff-line-num';
const UNFOLDABLE_LINE_CLASS = 'js-unfold'; const UNFOLDABLE_LINE_CLASS = 'js-unfold';
const NO_COMMENT_CLASS = 'no-comment-btn'; const NO_COMMENT_CLASS = 'no-comment-btn';
...@@ -27,9 +29,7 @@ export default { ...@@ -27,9 +29,7 @@ export default {
this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === ''; this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === '';
} }
if (typeof notes !== 'undefined' && !this.isParallelView) { this.isParallelView = Cookies.get('diff_view') === 'parallel';
this.isParallelView = notes.isParallelView && notes.isParallelView();
}
if (this.userCanCreateNote) { if (this.userCanCreateNote) {
$diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e)) $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
......
...@@ -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 [];
}) })
......
...@@ -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
......
...@@ -28,148 +28,149 @@ ...@@ -28,148 +28,149 @@
// </div> // </div>
// </div> // </div>
// //
(function() {
this.LineHighlighter = (function() {
// CSS class applied to highlighted lines
LineHighlighter.prototype.highlightClass = 'hll';
// Internal copy of location.hash so we're not dependent on `location` in tests
LineHighlighter.prototype._hash = '';
function LineHighlighter(hash) {
if (hash == null) {
// Initialize a LineHighlighter object
//
// hash - String URL hash for dependency injection in tests
hash = location.hash;
}
this.setHash = this.setHash.bind(this);
this.highlightLine = this.highlightLine.bind(this);
this.clickHandler = this.clickHandler.bind(this);
this.highlightHash = this.highlightHash.bind(this);
this._hash = hash;
this.bindEvents();
this.highlightHash();
}
LineHighlighter.prototype.bindEvents = function() { const LineHighlighter = function(options = {}) {
const $fileHolder = $('.file-holder'); options.highlightLineClass = options.highlightLineClass || 'hll';
$fileHolder.on('click', 'a[data-line-number]', this.clickHandler); options.fileHolderSelector = options.fileHolderSelector || '.file-holder';
$fileHolder.on('highlight:line', this.highlightHash); options.scrollFileHolder = options.scrollFileHolder || false;
}; options.hash = options.hash || location.hash;
LineHighlighter.prototype.highlightHash = function() {
var range;
if (this._hash !== '') {
range = this.hashToRange(this._hash);
if (range[0]) {
this.highlightRange(range);
$.scrollTo("#L" + range[0], {
// Scroll to the first highlighted line on initial load
// Offset -50 for the sticky top bar, and another -100 for some context
offset: -150
});
}
}
};
LineHighlighter.prototype.clickHandler = function(event) {
var current, lineNumber, range;
event.preventDefault();
this.clearHighlight();
lineNumber = $(event.target).closest('a').data('line-number');
current = this.hashToRange(this._hash);
if (!(current[0] && event.shiftKey)) {
// If there's no current selection, or there is but Shift wasn't held,
// treat this like a single-line selection.
this.setHash(lineNumber);
return this.highlightLine(lineNumber);
} else if (event.shiftKey) {
if (lineNumber < current[0]) {
range = [lineNumber, current[0]];
} else {
range = [current[0], lineNumber];
}
this.setHash(range[0], range[1]);
return this.highlightRange(range);
}
};
LineHighlighter.prototype.clearHighlight = function() {
return $("." + this.highlightClass).removeClass(this.highlightClass);
// Unhighlight previously highlighted lines
};
// Convert a URL hash String into line numbers
//
// hash - Hash String
//
// Examples:
//
// hashToRange('#L5') # => [5, null]
// hashToRange('#L5-15') # => [5, 15]
// hashToRange('#foo') # => [null, null]
//
// Returns an Array
LineHighlighter.prototype.hashToRange = function(hash) {
var first, last, matches;
// ?L(\d+)(?:-(\d+))?$/)
matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
if (matches && matches.length) {
first = parseInt(matches[1], 10);
last = matches[2] ? parseInt(matches[2], 10) : null;
return [first, last];
} else {
return [null, null];
}
};
// Highlight a single line
//
// lineNumber - Line number to highlight
LineHighlighter.prototype.highlightLine = function(lineNumber) {
return $("#LC" + lineNumber).addClass(this.highlightClass);
};
// Highlight all lines within a range
//
// range - Array containing the starting and ending line numbers
LineHighlighter.prototype.highlightRange = function(range) {
var i, lineNumber, ref, ref1, results;
if (range[1]) {
results = [];
for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
results.push(this.highlightLine(lineNumber));
}
return results;
} else {
return this.highlightLine(range[0]);
}
};
// Set the URL hash string this.options = options;
LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) { this._hash = options.hash;
var hash; this.highlightLineClass = options.highlightLineClass;
if (lastLineNumber) { this.setHash = this.setHash.bind(this);
hash = "#L" + firstLineNumber + "-" + lastLineNumber; this.highlightLine = this.highlightLine.bind(this);
this.clickHandler = this.clickHandler.bind(this);
this.highlightHash = this.highlightHash.bind(this);
this.bindEvents();
this.highlightHash();
};
LineHighlighter.prototype.bindEvents = function() {
const $fileHolder = $(this.options.fileHolderSelector);
$fileHolder.on('click', 'a[data-line-number]', this.clickHandler);
$fileHolder.on('highlight:line', this.highlightHash);
};
LineHighlighter.prototype.highlightHash = function() {
var range;
if (this._hash !== '') {
range = this.hashToRange(this._hash);
if (range[0]) {
this.highlightRange(range);
const lineSelector = `#L${range[0]}`;
const scrollOptions = {
// Scroll to the first highlighted line on initial load
// Offset -50 for the sticky top bar, and another -100 for some context
offset: -150
};
if (this.options.scrollFileHolder) {
$(this.options.fileHolderSelector).scrollTo(lineSelector, scrollOptions);
} else { } else {
hash = "#L" + firstLineNumber; $.scrollTo(lineSelector, scrollOptions);
} }
this._hash = hash; }
return this.__setLocationHash__(hash); }
}; };
// Make the actual hash change in the browser LineHighlighter.prototype.clickHandler = function(event) {
// var current, lineNumber, range;
// This method is stubbed in tests. event.preventDefault();
LineHighlighter.prototype.__setLocationHash__ = function(value) { this.clearHighlight();
return history.pushState({ lineNumber = $(event.target).closest('a').data('line-number');
url: value current = this.hashToRange(this._hash);
// We're using pushState instead of assigning location.hash directly to if (!(current[0] && event.shiftKey)) {
// prevent the page from scrolling on the hashchange event // If there's no current selection, or there is but Shift wasn't held,
}, document.title, value); // treat this like a single-line selection.
}; this.setHash(lineNumber);
return this.highlightLine(lineNumber);
return LineHighlighter; } else if (event.shiftKey) {
})(); if (lineNumber < current[0]) {
}).call(window); range = [lineNumber, current[0]];
} else {
range = [current[0], lineNumber];
}
this.setHash(range[0], range[1]);
return this.highlightRange(range);
}
};
LineHighlighter.prototype.clearHighlight = function() {
return $("." + this.highlightLineClass).removeClass(this.highlightLineClass);
};
// Convert a URL hash String into line numbers
//
// hash - Hash String
//
// Examples:
//
// hashToRange('#L5') # => [5, null]
// hashToRange('#L5-15') # => [5, 15]
// hashToRange('#foo') # => [null, null]
//
// Returns an Array
LineHighlighter.prototype.hashToRange = function(hash) {
var first, last, matches;
// ?L(\d+)(?:-(\d+))?$/)
matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
if (matches && matches.length) {
first = parseInt(matches[1], 10);
last = matches[2] ? parseInt(matches[2], 10) : null;
return [first, last];
} else {
return [null, null];
}
};
// Highlight a single line
//
// lineNumber - Line number to highlight
LineHighlighter.prototype.highlightLine = function(lineNumber) {
return $("#LC" + lineNumber).addClass(this.highlightLineClass);
};
// Highlight all lines within a range
//
// range - Array containing the starting and ending line numbers
LineHighlighter.prototype.highlightRange = function(range) {
var i, lineNumber, ref, ref1, results;
if (range[1]) {
results = [];
for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
results.push(this.highlightLine(lineNumber));
}
return results;
} else {
return this.highlightLine(range[0]);
}
};
// Set the URL hash string
LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
var hash;
if (lastLineNumber) {
hash = "#L" + firstLineNumber + "-" + lastLineNumber;
} else {
hash = "#L" + firstLineNumber;
}
this._hash = hash;
return this.__setLocationHash__(hash);
};
// Make the actual hash change in the browser
//
// This method is stubbed in tests.
LineHighlighter.prototype.__setLocationHash__ = function(value) {
return history.pushState({
url: value
// We're using pushState instead of assigning location.hash directly to
// prevent the page from scrolling on the hashchange event
}, document.title, value);
};
window.LineHighlighter = LineHighlighter;
...@@ -16,9 +16,8 @@ const locales = allLocales.reduce((d, obj) => { ...@@ -16,9 +16,8 @@ const locales = allLocales.reduce((d, obj) => {
return data; return data;
}, {}); }, {});
let lang = document.querySelector('html').getAttribute('lang') || 'en'; const langAttribute = document.querySelector('html').getAttribute('lang');
lang = lang.replace(/-/g, '_'); const lang = (langAttribute || 'en').replace(/-/g, '_');
const locale = new Jed(locales[lang]); const locale = new Jed(locales[lang]);
/** /**
......
...@@ -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';
...@@ -302,7 +301,10 @@ $(function () { ...@@ -302,7 +301,10 @@ $(function () {
return $container.remove(); return $container.remove();
// Commit show suppressed diff // Commit show suppressed diff
}); });
$('.navbar-toggle').on('click', () => $('.header-content').toggleClass('menu-expanded')); $('.navbar-toggle').on('click', () => {
$('.header-content').toggleClass('menu-expanded');
gl.lazyLoader.loadCheck();
});
// Show/hide comments on diff // Show/hide comments on diff
$body.on('click', '.js-toggle-diff-comments', function (e) { $body.on('click', '.js-toggle-diff-comments', function (e) {
var $this = $(this); var $this = $(this);
......
...@@ -352,7 +352,7 @@ import { ...@@ -352,7 +352,7 @@ import {
} }
expandViewContainer() { expandViewContainer() {
const $wrapper = $('.content-wrapper .container-fluid'); const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs');
if (this.fixedLayoutPref === null) { if (this.fixedLayoutPref === null) {
this.fixedLayoutPref = $wrapper.hasClass('container-limited'); this.fixedLayoutPref = $wrapper.hasClass('container-limited');
} }
......
...@@ -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.'));
}, },
}, },
}; };
...@@ -73,7 +73,8 @@ import _ from 'underscore'; ...@@ -73,7 +73,8 @@ import _ from 'underscore';
aspectRatio: 1, aspectRatio: 1,
modal: true, modal: true,
scalable: false, scalable: false,
rotatable: false, rotatable: true,
checkOrientation: true,
zoomable: true, zoomable: true,
dragMode: 'move', dragMode: 'move',
guides: false, guides: false,
......
/* 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); });
};
...@@ -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>
......
<script> <script>
/* global LineHighlighter */
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
export default { export default {
data: () => Store, data: () => Store,
mounted() {
this.highlightFile();
},
computed: { computed: {
html() { html() {
return this.activeFile.html; return this.activeFile.html;
}, },
}, },
methods: { methods: {
highlightFile() { highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight(); $(this.$el).find('.file-content').syntaxHighlight();
}, },
}, },
mounted() {
this.highlightFile();
this.lineHighlighter = new LineHighlighter({
fileHolderSelector: '.blob-viewer-container',
scrollFileHolder: true,
});
},
watch: { watch: {
html() { html() {
this.$nextTick(() => { this.$nextTick(() => {
...@@ -45,7 +49,7 @@ export default { ...@@ -45,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);
}
} }
}, },
......
...@@ -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;
}, },
......
...@@ -43,6 +43,8 @@ import Cookies from 'js-cookie'; ...@@ -43,6 +43,8 @@ import Cookies from 'js-cookie';
$allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
$('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
$('.page-with-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) { if (!triggered) {
return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed')); return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
......
...@@ -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) {
......
...@@ -126,7 +126,7 @@ ...@@ -126,7 +126,7 @@
.search-input-wrap { .search-input-wrap {
.search-icon, .search-icon,
.clear-icon { .clear-icon {
color: rgba($color-200, .8); fill: rgba($color-200, .8);
} }
} }
...@@ -141,7 +141,7 @@ ...@@ -141,7 +141,7 @@
.search-input-wrap { .search-input-wrap {
.search-icon { .search-icon {
color: rgba($color-200, .8); fill: rgba($color-200, .8);
} }
} }
} }
...@@ -252,7 +252,7 @@ body { ...@@ -252,7 +252,7 @@ body {
.search-input-wrap { .search-input-wrap {
.search-icon { .search-icon {
color: $theme-gray-200; fill: $theme-gray-200;
} }
.search-input { .search-input {
......
...@@ -109,8 +109,7 @@ header { ...@@ -109,8 +109,7 @@ header {
.user-counter { .user-counter {
svg { svg {
height: 16px; margin-right: 3px;
width: 23px;
} }
} }
...@@ -133,16 +132,16 @@ header { ...@@ -133,16 +132,16 @@ header {
} }
&.navbar-gitlab-new { &.navbar-gitlab-new {
.fa-times { .close-icon {
display: none; display: none;
} }
.menu-expanded { .menu-expanded {
.fa-ellipsis-v { .more-icon {
display: none; display: none;
} }
.fa-times { .close-icon {
display: block; display: block;
} }
} }
......
...@@ -27,7 +27,9 @@ ...@@ -27,7 +27,9 @@
} }
svg { svg {
&.s8 { @include svg-size(8px); }
&.s16 { @include svg-size(16px); } &.s16 { @include svg-size(16px); }
&.s18 { @include svg-size(18px); }
&.s24 { @include svg-size(24px); } &.s24 { @include svg-size(24px); }
&.s32 { @include svg-size(32px); } &.s32 { @include svg-size(32px); }
&.s48 { @include svg-size(48px); } &.s48 { @include svg-size(48px); }
......
...@@ -120,17 +120,24 @@ header.navbar-gitlab-new { ...@@ -120,17 +120,24 @@ header.navbar-gitlab-new {
.container-fluid { .container-fluid {
.navbar-toggle { .navbar-toggle {
min-width: 45px; min-width: 45px;
padding: 4px $gl-padding; padding: 0 $gl-padding;
margin-right: -7px; margin-right: -7px;
font-size: 14px;
text-align: center; text-align: center;
color: currentColor; color: currentColor;
svg {
fill: currentColor;
}
&:hover, &:hover,
&:focus, &:focus,
&.active { &.active {
color: currentColor; color: currentColor;
background-color: transparent; background-color: transparent;
svg {
fill: currentColor;
}
} }
} }
...@@ -279,10 +286,6 @@ header.navbar-gitlab-new { ...@@ -279,10 +286,6 @@ header.navbar-gitlab-new {
} }
} }
.admin-icon i {
font-size: 18px;
}
.caret-down { .caret-down {
height: 11px; height: 11px;
width: 11px; width: 11px;
...@@ -306,6 +309,8 @@ header.navbar-gitlab-new { ...@@ -306,6 +309,8 @@ header.navbar-gitlab-new {
display: flex; display: flex;
width: 100%; width: 100%;
position: relative; position: relative;
padding-top: $gl-padding / 2;
padding-bottom: $gl-padding / 2;
align-items: center; align-items: center;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
...@@ -346,6 +351,7 @@ header.navbar-gitlab-new { ...@@ -346,6 +351,7 @@ header.navbar-gitlab-new {
display: flex; display: flex;
align-items: center; align-items: center;
position: relative; position: relative;
padding: 2px 0;
&:not(:last-child) { &:not(:last-child) {
margin-right: 20px; margin-right: 20px;
...@@ -381,7 +387,7 @@ header.navbar-gitlab-new { ...@@ -381,7 +387,7 @@ header.navbar-gitlab-new {
margin: 0; margin: 0;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
line-height: 1; line-height: 16px;
a { a {
color: $gl-text-color; color: $gl-text-color;
......
...@@ -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;
} }
} }
} }
......
...@@ -7,6 +7,7 @@ $gutter_inner_width: 250px; ...@@ -7,6 +7,7 @@ $gutter_inner_width: 250px;
$sidebar-transition-duration: .15s; $sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1024px; $sidebar-breakpoint: 1024px;
$default-transition-duration: .15s; $default-transition-duration: .15s;
$right-sidebar-transition-duration: .3s;
/* /*
* Color schema * Color schema
......
...@@ -55,6 +55,15 @@ ...@@ -55,6 +55,15 @@
.boards-app { .boards-app {
position: relative; position: relative;
@media (min-width: $screen-sm-min) {
transition: width $right-sidebar-transition-duration;
width: 100%;
&.is-compact {
width: calc(100% - #{$gutter_width});
}
}
} }
.boards-app-loading { .boards-app-loading {
...@@ -78,11 +87,6 @@ ...@@ -78,11 +87,6 @@
height: calc(100vh - 222px); height: calc(100vh - 222px);
// scss-lint:enable DuplicateProperty // scss-lint:enable DuplicateProperty
min-height: 475px; min-height: 475px;
transition: width .2s;
&.is-compact {
width: calc(100% - 290px);
}
} }
} }
...@@ -412,14 +416,6 @@ ...@@ -412,14 +416,6 @@
.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar, .page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar,
.page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar { .page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar {
position: absolute;
&.right-sidebar {
top: 0;
bottom: 0;
height: 100%;
}
.issuable-sidebar-header { .issuable-sidebar-header {
position: relative; position: relative;
} }
...@@ -457,8 +453,8 @@ ...@@ -457,8 +453,8 @@
.right-sidebar.right-sidebar-expanded { .right-sidebar.right-sidebar-expanded {
&.boards-sidebar-slide-enter-active, &.boards-sidebar-slide-enter-active,
&.boards-sidebar-slide-leave-active { &.boards-sidebar-slide-leave-active {
transition: width .2s, transition: width $right-sidebar-transition-duration,
padding .2s; padding $right-sidebar-transition-duration;
} }
&.boards-sidebar-slide-enter, &.boards-sidebar-slide-enter,
......
...@@ -535,7 +535,6 @@ ...@@ -535,7 +535,6 @@
} }
.diff-notes-collapse { .diff-notes-collapse {
position: relative;
width: 19px; width: 19px;
height: 19px; height: 19px;
padding: 0; padding: 0;
...@@ -543,11 +542,7 @@ ...@@ -543,11 +542,7 @@
z-index: 100; z-index: 100;
svg { svg {
position: absolute; vertical-align: text-top;
left: 50%;
top: 50%;
margin-left: -5.5px;
margin-top: -5.5px;
} }
path { path {
......
...@@ -223,14 +223,14 @@ ...@@ -223,14 +223,14 @@
top: $new-navbar-height; top: $new-navbar-height;
bottom: 0; bottom: 0;
right: 0; right: 0;
transition: width .3s; transition: width $right-sidebar-transition-duration;
background: $gray-light; background: $gray-light;
z-index: 200; z-index: 200;
overflow: hidden; overflow: hidden;
.issuable-sidebar { .issuable-sidebar {
width: calc(100% + 100px); width: calc(100% + 100px);
height: calc(100% - #{$new-navbar-height}); height: 100%;
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
......
...@@ -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;
......
...@@ -54,6 +54,10 @@ ...@@ -54,6 +54,10 @@
border-radius: $border-radius-default; border-radius: $border-radius-default;
color: $almost-black; color: $almost-black;
.code.white pre .hll {
background-color: $well-light-border !important;
}
.tree-content-holder { .tree-content-holder {
display: flex; display: flex;
min-height: 300px; min-height: 300px;
......
...@@ -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;
} }
......
...@@ -22,8 +22,7 @@ class Admin::ApplicationsController < Admin::ApplicationController ...@@ -22,8 +22,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
@application = Doorkeeper::Application.new(application_params) @application = Doorkeeper::Application.new(application_params)
if @application.save if @application.save
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) redirect_to_admin_page
redirect_to admin_application_url(@application)
else else
render :new render :new
end end
...@@ -42,6 +41,13 @@ class Admin::ApplicationsController < Admin::ApplicationController ...@@ -42,6 +41,13 @@ class Admin::ApplicationsController < Admin::ApplicationController
redirect_to admin_applications_url, status: 302, notice: 'Application was successfully destroyed.' redirect_to admin_applications_url, status: 302, notice: 'Application was successfully destroyed.'
end end
protected
def redirect_to_admin_page
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
redirect_to admin_application_url(@application)
end
private private
def set_application def set_application
......
...@@ -128,7 +128,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -128,7 +128,7 @@ class Admin::UsersController < Admin::ApplicationController
end end
respond_to do |format| respond_to do |format|
result = Users::UpdateService.new(user, user_params_with_pass).execute do |user| result = Users::UpdateService.new(current_user, user_params_with_pass.merge(user: user)).execute do |user|
user.skip_reconfirmation! user.skip_reconfirmation!
end end
...@@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController
def remove_email def remove_email
email = user.emails.find(params[:email_id]) email = user.emails.find(params[:email_id])
success = Emails::DestroyService.new(user, email: email.email).execute success = Emails::DestroyService.new(current_user, user: user, email: email.email).execute
respond_to do |format| respond_to do |format|
if success if success
...@@ -219,7 +219,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -219,7 +219,7 @@ class Admin::UsersController < Admin::ApplicationController
end end
def update_user(&block) def update_user(&block)
result = Users::UpdateService.new(user).execute(&block) result = Users::UpdateService.new(current_user, user: user).execute(&block)
result[:status] == :success result[:status] == :success
end end
......
...@@ -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
...@@ -106,7 +106,7 @@ module IssuableCollections ...@@ -106,7 +106,7 @@ module IssuableCollections
# @filter_params[:authorized_only] = true # @filter_params[:authorized_only] = true
end end
@filter_params @filter_params.permit(IssuableFinder::VALID_PARAMS)
end end
def set_default_state def set_default_state
......
...@@ -12,10 +12,14 @@ class ConfirmationsController < Devise::ConfirmationsController ...@@ -12,10 +12,14 @@ class ConfirmationsController < Devise::ConfirmationsController
def after_confirmation_path_for(resource_name, resource) def after_confirmation_path_for(resource_name, resource)
if signed_in?(resource_name) if signed_in?(resource_name)
after_sign_in_path_for(resource) after_sign_in(resource)
else else
flash[:notice] += " Please sign in." flash[:notice] += " Please sign in."
new_session_path(resource_name) new_session_path(resource_name)
end end
end end
def after_sign_in(resource)
after_sign_in_path_for(resource)
end
end end
...@@ -21,14 +21,20 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController ...@@ -21,14 +21,20 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
@application.owner = current_user @application.owner = current_user
if @application.save if @application.save
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) redirect_to_oauth_application_page
redirect_to oauth_application_url(@application)
else else
set_index_vars set_index_vars
render :index render :index
end end
end end
protected
def redirect_to_oauth_application_page
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
redirect_to oauth_application_url(@application)
end
private private
def verify_user_oauth_applications_enabled def verify_user_oauth_applications_enabled
......
...@@ -2,7 +2,7 @@ class Profiles::AvatarsController < Profiles::ApplicationController ...@@ -2,7 +2,7 @@ class Profiles::AvatarsController < Profiles::ApplicationController
def destroy def destroy
@user = current_user @user = current_user
Users::UpdateService.new(@user).execute { |user| user.remove_avatar! } Users::UpdateService.new(current_user, user: @user).execute { |user| user.remove_avatar! }
redirect_to profile_path, status: 302 redirect_to profile_path, status: 302
end end
......
...@@ -5,7 +5,7 @@ class Profiles::EmailsController < Profiles::ApplicationController ...@@ -5,7 +5,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
end end
def create def create
@email = Emails::CreateService.new(current_user, email_params).execute @email = Emails::CreateService.new(current_user, email_params.merge(user: current_user)).execute
if @email.errors.blank? if @email.errors.blank?
NotificationService.new.new_email(@email) NotificationService.new.new_email(@email)
...@@ -19,7 +19,7 @@ class Profiles::EmailsController < Profiles::ApplicationController ...@@ -19,7 +19,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
def destroy def destroy
@email = current_user.emails.find(params[:id]) @email = current_user.emails.find(params[:id])
Emails::DestroyService.new(current_user, email: @email.email).execute Emails::DestroyService.new(current_user, user: current_user, email: @email.email).execute
respond_to do |format| respond_to do |format|
format.html { redirect_to profile_emails_url, status: 302 } format.html { redirect_to profile_emails_url, status: 302 }
......
...@@ -14,7 +14,7 @@ class Profiles::KeysController < Profiles::ApplicationController ...@@ -14,7 +14,7 @@ class Profiles::KeysController < Profiles::ApplicationController
@key = Keys::CreateService.new(current_user, key_params).execute @key = Keys::CreateService.new(current_user, key_params).execute
if @key.persisted? if @key.persisted?
redirect_to profile_key_path(@key) redirect_to_profile_key_path
else else
@keys = current_user.keys.select(&:persisted?) @keys = current_user.keys.select(&:persisted?)
render :index render :index
...@@ -50,6 +50,12 @@ class Profiles::KeysController < Profiles::ApplicationController ...@@ -50,6 +50,12 @@ class Profiles::KeysController < Profiles::ApplicationController
end end
end end
protected
def redirect_to_profile_key_path
redirect_to profile_key_path(@key)
end
private private
def key_params def key_params
......
...@@ -7,7 +7,7 @@ class Profiles::NotificationsController < Profiles::ApplicationController ...@@ -7,7 +7,7 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end end
def update def update
result = Users::UpdateService.new(current_user, user_params).execute result = Users::UpdateService.new(current_user, user_params.merge(user: current_user)).execute
if result[:status] == :success if result[:status] == :success
flash[:notice] = "Notification settings saved" flash[:notice] = "Notification settings saved"
......
...@@ -21,10 +21,10 @@ class Profiles::PasswordsController < Profiles::ApplicationController ...@@ -21,10 +21,10 @@ class Profiles::PasswordsController < Profiles::ApplicationController
password_automatically_set: false password_automatically_set: false
} }
result = Users::UpdateService.new(@user, password_attributes).execute result = Users::UpdateService.new(current_user, password_attributes.merge(user: @user)).execute
if result[:status] == :success if result[:status] == :success
Users::UpdateService.new(@user, password_expires_at: nil).execute Users::UpdateService.new(current_user, user: @user, password_expires_at: nil).execute
redirect_to root_path, notice: 'Password successfully changed' redirect_to root_path, notice: 'Password successfully changed'
else else
...@@ -46,7 +46,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController ...@@ -46,7 +46,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
return return
end end
result = Users::UpdateService.new(@user, password_attributes).execute result = Users::UpdateService.new(current_user, password_attributes.merge(user: @user)).execute
if result[:status] == :success if result[:status] == :success
flash[:notice] = "Password was successfully updated. Please login with it" flash[:notice] = "Password was successfully updated. Please login with it"
......
...@@ -6,7 +6,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController ...@@ -6,7 +6,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
def update def update
begin begin
result = Users::UpdateService.new(user, preferences_params).execute result = Users::UpdateService.new(current_user, preferences_params.merge(user: user)).execute
if result[:status] == :success if result[:status] == :success
flash[:notice] = 'Preferences saved.' flash[:notice] = 'Preferences saved.'
......
...@@ -10,7 +10,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -10,7 +10,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.otp_grace_period_started_at = Time.current current_user.otp_grace_period_started_at = Time.current
end end
Users::UpdateService.new(current_user).execute! Users::UpdateService.new(current_user, user: current_user).execute!
if two_factor_authentication_required? && !current_user.two_factor_enabled? if two_factor_authentication_required? && !current_user.two_factor_enabled?
two_factor_authentication_reason( two_factor_authentication_reason(
...@@ -41,7 +41,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -41,7 +41,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def create def create
if current_user.validate_and_consume_otp!(params[:pin_code]) if current_user.validate_and_consume_otp!(params[:pin_code])
Users::UpdateService.new(current_user, otp_required_for_login: true).execute! do |user| Users::UpdateService.new(current_user, user: current_user, otp_required_for_login: true).execute! do |user|
@codes = user.generate_otp_backup_codes! @codes = user.generate_otp_backup_codes!
end end
...@@ -70,7 +70,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -70,7 +70,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end end
def codes def codes
Users::UpdateService.new(current_user).execute! do |user| Users::UpdateService.new(current_user, user: current_user).execute! do |user|
@codes = user.generate_otp_backup_codes! @codes = user.generate_otp_backup_codes!
end end
end end
......
...@@ -10,7 +10,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -10,7 +10,7 @@ class ProfilesController < Profiles::ApplicationController
def update def update
respond_to do |format| respond_to do |format|
result = Users::UpdateService.new(@user, user_params).execute result = Users::UpdateService.new(current_user, user_params.merge(user: @user)).execute
if result[:status] == :success if result[:status] == :success
message = "Profile was successfully updated" message = "Profile was successfully updated"
...@@ -25,7 +25,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -25,7 +25,7 @@ class ProfilesController < Profiles::ApplicationController
end end
def reset_private_token def reset_private_token
Users::UpdateService.new(@user).execute! do |user| Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_authentication_token! user.reset_authentication_token!
end end
...@@ -35,7 +35,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -35,7 +35,7 @@ class ProfilesController < Profiles::ApplicationController
end end
def reset_incoming_email_token def reset_incoming_email_token
Users::UpdateService.new(@user).execute! do |user| Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_incoming_email_token! user.reset_incoming_email_token!
end end
...@@ -45,7 +45,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -45,7 +45,7 @@ class ProfilesController < Profiles::ApplicationController
end end
def reset_rss_token def reset_rss_token
Users::UpdateService.new(@user).execute! do |user| Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_rss_token! user.reset_rss_token!
end end
...@@ -61,7 +61,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -61,7 +61,7 @@ class ProfilesController < Profiles::ApplicationController
end end
def update_username def update_username
result = Users::UpdateService.new(@user, username: user_params[:username]).execute result = Users::UpdateService.new(current_user, user: @user, username: user_params[:username]).execute
options = if result[:status] == :success options = if result[:status] == :success
{ notice: "Username successfully changed" } { notice: "Username successfully changed" }
......
...@@ -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
......
...@@ -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)
......
...@@ -55,7 +55,7 @@ class SessionsController < Devise::SessionsController ...@@ -55,7 +55,7 @@ class SessionsController < Devise::SessionsController
return unless user && user.require_password_creation? return unless user && user.require_password_creation?
Users::UpdateService.new(user).execute do |user| Users::UpdateService.new(current_user, user: user).execute do |user|
@token = user.generate_reset_token @token = user.generate_reset_token
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
...@@ -25,6 +25,28 @@ class IssuableFinder ...@@ -25,6 +25,28 @@ class IssuableFinder
NONE = '0'.freeze NONE = '0'.freeze
SCALAR_PARAMS = %i[
assignee_id
assignee_username
author_id
author_username
authorized_only
due_date
group_id
iids
label_name
milestone_title
non_archived
project_id
scope
search
sort
state
].freeze
ARRAY_PARAMS = { label_name: [], iids: [], assignee_username: [] }.freeze
VALID_PARAMS = (SCALAR_PARAMS + [ARRAY_PARAMS]).freeze
attr_accessor :current_user, :params attr_accessor :current_user, :params
def initialize(current_user, params = {}) def initialize(current_user, params = {})
......
...@@ -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
......
...@@ -79,6 +79,6 @@ module BoardsHelper ...@@ -79,6 +79,6 @@ module BoardsHelper
end end
def boards_link_text def boards_link_text
_("Board") s_("IssueBoards|Board")
end end
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
......
...@@ -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
......
...@@ -434,7 +434,7 @@ module Ci ...@@ -434,7 +434,7 @@ module Ci
def update_duration def update_duration
return unless started_at return unless started_at
self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self) self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self)
end end
def execute_hooks def execute_hooks
......
...@@ -245,6 +245,9 @@ class Project < ActiveRecord::Base ...@@ -245,6 +245,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') }
...@@ -1032,7 +1035,7 @@ class Project < ActiveRecord::Base ...@@ -1032,7 +1035,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 +1553,44 @@ class Project < ActiveRecord::Base ...@@ -1550,18 +1553,44 @@ 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 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 +1603,14 @@ class Project < ActiveRecord::Base ...@@ -1574,6 +1603,14 @@ 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
# 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)
......
...@@ -91,12 +91,6 @@ class Repository ...@@ -91,12 +91,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
...@@ -489,13 +483,7 @@ class Repository ...@@ -489,13 +483,7 @@ class Repository
def exists? def exists?
return false unless full_path return false unless full_path
Gitlab::GitalyClient.migrate(:repository_exists) do |enabled| raw_repository.exists?
if enabled
raw_repository.exists?
else
refs_directory_exists?
end
end
end end
cache_method :exists? cache_method :exists?
...@@ -529,9 +517,10 @@ class Repository ...@@ -529,9 +517,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
...@@ -1063,12 +1052,6 @@ class Repository ...@@ -1063,12 +1052,6 @@ class Repository
blob.data blob.data
end end
def refs_directory_exists?
circuit_breaker.perform do
File.exist?(File.join(path_to_repo, 'refs'))
end
end
def cache def cache
# TODO: should we use UUIDs here? We could move repositories without clearing this cache # TODO: should we use UUIDs here? We could move repositories without clearing this cache
@cache ||= RepositoryCache.new(full_path, @project.id) @cache ||= RepositoryCache.new(full_path, @project.id)
...@@ -1120,10 +1103,6 @@ class Repository ...@@ -1120,10 +1103,6 @@ class Repository
Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false)) Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false))
end end
def circuit_breaker
@circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(project.repository_storage)
end
def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset) def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
ref ||= root_ref ref ||= root_ref
......
...@@ -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
......
...@@ -60,7 +60,7 @@ class User < ActiveRecord::Base ...@@ -60,7 +60,7 @@ class User < ActiveRecord::Base
lease = Gitlab::ExclusiveLease.new("user_update_tracked_fields:#{id}", timeout: 1.hour.to_i) lease = Gitlab::ExclusiveLease.new("user_update_tracked_fields:#{id}", timeout: 1.hour.to_i)
return unless lease.try_obtain return unless lease.try_obtain
Users::UpdateService.new(self).execute(validate: false) Users::UpdateService.new(self, user: self).execute(validate: false)
end end
attr_accessor :force_random_password attr_accessor :force_random_password
...@@ -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
# #
...@@ -526,8 +528,8 @@ class User < ActiveRecord::Base ...@@ -526,8 +528,8 @@ class User < ActiveRecord::Base
def update_emails_with_primary_email def update_emails_with_primary_email
primary_email_record = emails.find_by(email: email) primary_email_record = emails.find_by(email: email)
if primary_email_record if primary_email_record
Emails::DestroyService.new(self, email: email).execute Emails::DestroyService.new(self, user: self, email: email).execute
Emails::CreateService.new(self, email: email_was).execute Emails::CreateService.new(self, user: self, email: email_was).execute
end end
end end
...@@ -1000,7 +1002,7 @@ class User < ActiveRecord::Base ...@@ -1000,7 +1002,7 @@ class User < ActiveRecord::Base
if attempts_exceeded? if attempts_exceeded?
lock_access! unless access_locked? lock_access! unless access_locked?
else else
Users::UpdateService.new(self).execute(validate: false) Users::UpdateService.new(self, user: self).execute(validate: false)
end end
end end
...@@ -1186,7 +1188,7 @@ class User < ActiveRecord::Base ...@@ -1186,7 +1188,7 @@ class User < ActiveRecord::Base
&creation_block &creation_block
) )
Users::UpdateService.new(user).execute(validate: false) Users::UpdateService.new(user, user: user).execute(validate: false)
user user
ensure ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid) Gitlab::ExclusiveLease.cancel(lease_key, uuid)
......
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
...@@ -2,110 +2,55 @@ module Ci ...@@ -2,110 +2,55 @@ module Ci
class CreatePipelineService < BaseService class CreatePipelineService < BaseService
attr_reader :pipeline attr_reader :pipeline
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil) SEQUENCE = [Gitlab::Ci::Pipeline::Chain::Validate::Abilities,
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
Gitlab::Ci::Pipeline::Chain::Validate::Config,
Gitlab::Ci::Pipeline::Chain::Skip,
Gitlab::Ci::Pipeline::Chain::Create].freeze
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block)
@pipeline = Ci::Pipeline.new( @pipeline = Ci::Pipeline.new(
source: source, source: source,
project: project, project: project,
ref: ref, ref: ref,
sha: sha, sha: sha,
before_sha: before_sha, before_sha: before_sha,
tag: tag?, tag: tag_exists?,
trigger_requests: Array(trigger_request), trigger_requests: Array(trigger_request),
user: current_user, user: current_user,
pipeline_schedule: schedule, pipeline_schedule: schedule,
protected: project.protected_for?(ref) protected: project.protected_for?(ref)
) )
result = validate_project_and_git_items || command = OpenStruct.new(ignore_skip_ci: ignore_skip_ci,
validate_pipeline(ignore_skip_ci: ignore_skip_ci, save_incompleted: save_on_errors,
save_on_errors: save_on_errors) seeds_block: block,
project: project,
current_user: current_user)
return result if result sequence = Gitlab::Ci::Pipeline::Chain::Sequence
.new(pipeline, command, SEQUENCE)
begin sequence.build! do |pipeline, sequence|
Ci::Pipeline.transaction do update_merge_requests_head_pipeline if pipeline.persisted?
pipeline.save!
yield(pipeline) if block_given? if sequence.complete?
cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
pipeline_created_counter.increment(source: source)
Ci::CreatePipelineStagesService pipeline.process!
.new(project, current_user)
.execute(pipeline)
end end
rescue ActiveRecord::RecordInvalid => e
return error("Failed to persist the pipeline: #{e}")
end end
update_merge_requests_head_pipeline
cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
pipeline_created_counter.increment(source: source)
pipeline.tap(&:process!)
end end
private private
def validate_project_and_git_items def commit
unless project.builds_enabled? @commit ||= project.commit(origin_sha || origin_ref)
return error('Pipeline is disabled')
end
unless allowed_to_trigger_pipeline?
if can?(current_user, :create_pipeline, project)
return error("Insufficient permissions for protected ref '#{ref}'")
else
return error('Insufficient permissions to create a new pipeline')
end
end
unless branch? || tag?
return error('Reference not found')
end
unless commit
return error('Commit not found')
end
end
def validate_pipeline(ignore_skip_ci:, save_on_errors:)
unless pipeline.config_processor
unless pipeline.ci_yaml_file
return error("Missing #{pipeline.ci_yaml_file_path} file")
end
return error(pipeline.yaml_errors, save: save_on_errors)
end
if !ignore_skip_ci && skip_ci?
pipeline.skip if save_on_errors
return pipeline
end
unless pipeline.has_stage_seeds?
return error('No stages / jobs for this pipeline.')
end
end
def allowed_to_trigger_pipeline?
if current_user
allowed_to_create?
else # legacy triggers don't have a corresponding user
!project.protected_for?(ref)
end
end end
def allowed_to_create? def sha
return unless can?(current_user, :create_pipeline, project) commit.try(:id)
access = Gitlab::UserAccess.new(current_user, project: project)
if branch?
access.can_update_branch?(ref)
elsif tag?
access.can_create_tag?(ref)
else
true # Allow it for now and we'll reject when we check ref existence
end
end end
def update_merge_requests_head_pipeline def update_merge_requests_head_pipeline
...@@ -115,11 +60,6 @@ module Ci ...@@ -115,11 +60,6 @@ module Ci
.update_all(head_pipeline_id: @pipeline.id) .update_all(head_pipeline_id: @pipeline.id)
end end
def skip_ci?
return false unless pipeline.git_commit_message
pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
end
def cancel_pending_pipelines def cancel_pending_pipelines
Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables| Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables|
cancelables.find_each do |cancelable| cancelables.find_each do |cancelable|
...@@ -136,14 +76,6 @@ module Ci ...@@ -136,14 +76,6 @@ module Ci
.created_or_pending .created_or_pending
end end
def commit
@commit ||= project.commit(origin_sha || origin_ref)
end
def sha
commit.try(:id)
end
def before_sha def before_sha
params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA
end end
...@@ -156,41 +88,17 @@ module Ci ...@@ -156,41 +88,17 @@ module Ci
params[:ref] params[:ref]
end end
def branch? def tag_exists?
return @is_branch if defined?(@is_branch) project.repository.tag_exists?(ref)
@is_branch =
project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref)
end
def tag?
return @is_tag if defined?(@is_tag)
@is_tag =
project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref)
end end
def ref def ref
@ref ||= Gitlab::Git.ref_name(origin_ref) @ref ||= Gitlab::Git.ref_name(origin_ref)
end end
def valid_sha?
origin_sha && origin_sha != Gitlab::Git::BLANK_SHA
end
def error(message, save: false)
pipeline.tap do
pipeline.errors.add(:base, message)
if save
pipeline.drop
update_merge_requests_head_pipeline
end
end
end
def pipeline_created_counter def pipeline_created_counter
@pipeline_created_counter ||= Gitlab::Metrics.counter(:pipelines_created_total, "Counter of pipelines created") @pipeline_created_counter ||= Gitlab::Metrics
.counter(:pipelines_created_total, "Counter of pipelines created")
end end
end end
end end
module Emails module Emails
class BaseService class BaseService
def initialize(user, opts) def initialize(current_user, opts)
@user = user @current_user = current_user
@user = opts.delete(:user)
@email = opts[:email] @email = opts[:email]
end end
end end
......
module Emails module Emails
class DestroyService < ::Emails::BaseService class DestroyService < ::Emails::BaseService
def execute def execute
Email.find_by_email!(@email).destroy && update_secondary_emails! update_secondary_emails! if Email.find_by_email!(@email).destroy
end end
private private
def update_secondary_emails! def update_secondary_emails!
result = ::Users::UpdateService.new(@user).execute do |user| result = ::Users::UpdateService.new(@current_user, user: @user).execute do |user|
user.update_secondary_emails! user.update_secondary_emails!
end end
......
...@@ -14,13 +14,13 @@ module MergeRequests ...@@ -14,13 +14,13 @@ module MergeRequests
@merge_request = merge_request @merge_request = merge_request
unless @merge_request.mergeable? unless @merge_request.mergeable?
return log_merge_error('Merge request is not mergeable', save_message_on_model: true) return handle_merge_error(log_message: 'Merge request is not mergeable', save_message_on_model: true)
end end
@source = find_merge_source @source = find_merge_source
unless @source unless @source
return log_merge_error('No source for merge', save_message_on_model: true) return handle_merge_error(log_message: 'No source for merge', save_message_on_model: true)
end end
merge_request.in_locked_state do merge_request.in_locked_state do
...@@ -31,8 +31,7 @@ module MergeRequests ...@@ -31,8 +31,7 @@ module MergeRequests
end end
end end
rescue MergeError => e rescue MergeError => e
clean_merge_jid handle_merge_error(log_message: e.message, save_message_on_model: true)
log_merge_error(e.message, save_message_on_model: true)
end end
private private
...@@ -74,10 +73,16 @@ module MergeRequests ...@@ -74,10 +73,16 @@ module MergeRequests
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user @merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end end
def log_merge_error(message, save_message_on_model: false) # Logs merge error message and cleans `MergeRequest#merge_jid`.
Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}") #
def handle_merge_error(log_message:, save_message_on_model: false)
Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{log_message}")
@merge_request.update(merge_error: message) if save_message_on_model if save_message_on_model
@merge_request.update(merge_error: log_message, merge_jid: nil)
else
clean_merge_jid
end
end end
def merge_request_info def merge_request_info
......
...@@ -14,6 +14,7 @@ module MergeRequests ...@@ -14,6 +14,7 @@ module MergeRequests
notification_service.merge_mr(merge_request, current_user) notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge') execute_hooks(merge_request, 'merge')
invalidate_cache_counts(merge_request, users: merge_request.assignees) invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
end end
private private
......
module Projects
class HashedStorageMigrationService < BaseService
include Gitlab::ShellAdapter
attr_reader :old_disk_path, :new_disk_path
def initialize(project, logger = nil)
@project = project
@logger ||= Rails.logger
end
def execute
return if project.hashed_storage?
@old_disk_path = project.disk_path
has_wiki = project.wiki.repository_exists?
project.storage_version = Storage::HashedProject::STORAGE_VERSION
project.ensure_storage_path_exists
@new_disk_path = project.disk_path
result = move_repository(@old_disk_path, @new_disk_path)
if has_wiki
result &&= move_repository("#{@old_disk_path}.wiki", "#{@new_disk_path}.wiki")
end
unless result
rollback_folder_move
return
end
project.repository_read_only = false
project.save!
block_given? ? yield : result
end
private
def move_repository(from_name, to_name)
from_exists = gitlab_shell.exists?(project.repository_storage_path, "#{from_name}.git")
to_exists = gitlab_shell.exists?(project.repository_storage_path, "#{to_name}.git")
# If we don't find the repository on either original or target we should log that as it could be an issue if the
# project was not originally empty.
if !from_exists && !to_exists
logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..."
return false
elsif !from_exists
# Repository have been moved already.
return true
end
gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
end
def rollback_folder_move
move_repository(@new_disk_path, @old_disk_path)
move_repository("#{@new_disk_path}.wiki", "#{@old_disk_path}.wiki")
end
def logger
@logger
end
end
end
...@@ -11,7 +11,7 @@ module Tags ...@@ -11,7 +11,7 @@ module Tags
begin begin
new_tag = repository.add_tag(current_user, tag_name, target, message) new_tag = repository.add_tag(current_user, tag_name, target, message)
rescue Rugged::TagError rescue Gitlab::Git::Repository::TagExistsError
return error("Tag #{tag_name} already exists") return error("Tag #{tag_name} already exists")
rescue Gitlab::Git::HooksService::PreReceiveError => ex rescue Gitlab::Git::HooksService::PreReceiveError => ex
return error(ex.message) return error(ex.message)
......
...@@ -2,22 +2,21 @@ module Users ...@@ -2,22 +2,21 @@ module Users
class UpdateService < BaseService class UpdateService < BaseService
include NewUserNotifier include NewUserNotifier
def initialize(user, params = {}) def initialize(current_user, params = {})
@user = user @current_user = current_user
@user = params.delete(:user)
@params = params.dup @params = params.dup
end end
def execute(validate: true, &block) def execute(validate: true, &block)
yield(@user) if block_given? yield(@user) if block_given?
assign_attributes(&block)
user_exists = @user.persisted? user_exists = @user.persisted?
if @user.save(validate: validate) assign_attributes(&block)
notify_new_user(@user, nil) unless user_exists
success if @user.save(validate: validate)
notify_success(user_exists)
else else
error(@user.errors.full_messages.uniq.join('. ')) error(@user.errors.full_messages.uniq.join('. '))
end end
...@@ -33,6 +32,12 @@ module Users ...@@ -33,6 +32,12 @@ module Users
private private
def notify_success(user_exists)
notify_new_user(@user, nil) unless user_exists
success
end
def assign_attributes(&block) def assign_attributes(&block)
if @user.user_synced_attributes_metadata if @user.user_synced_attributes_metadata
params.except!(*@user.user_synced_attributes_metadata.read_only_attributes) params.except!(*@user.user_synced_attributes_metadata.read_only_attributes)
......
- confirmation_link = confirmation_url(@resource, confirmation_token: @token)
- if @resource.unconfirmed_email.present? - if @resource.unconfirmed_email.present?
#content #content
= email_default_heading(@resource.unconfirmed_email) = email_default_heading(@resource.unconfirmed_email)
%p Click the link below to confirm your email address. %p Click the link below to confirm your email address.
#cta #cta
= link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token) = link_to confirmation_link, confirmation_link
- else - else
#content #content
- if Gitlab.com? - if Gitlab.com?
...@@ -12,4 +13,4 @@ ...@@ -12,4 +13,4 @@
= email_default_heading("Welcome, #{@resource.name}!") = email_default_heading("Welcome, #{@resource.name}!")
%p To get started, click the link below to confirm your account. %p To get started, click the link below to confirm your account.
#cta #cta
= link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) = link_to confirmation_link, confirmation_link
...@@ -21,8 +21,8 @@ ...@@ -21,8 +21,8 @@
%a %a
Loading... Loading...
= dropdown_loading = dropdown_loading
%i.search-icon = sprite_icon('search', size: 16, css_class: 'search-icon')
%i.clear-icon.js-clear-input = sprite_icon('close', size: 16, css_class: 'clear-icon js-clear-input')
= hidden_field_tag :group_id, @group.try(:id), class: 'js-search-group-options', data: group_data_attrs = hidden_field_tag :group_id, @group.try(:id), class: 'js-search-group-options', data: group_data_attrs
......
...@@ -22,29 +22,29 @@ ...@@ -22,29 +22,29 @@
= render 'layouts/search' unless current_controller?(:search) = render 'layouts/search' unless current_controller?(:search)
%li.visible-sm-inline-block.visible-xs-inline-block %li.visible-sm-inline-block.visible-xs-inline-block
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search') = sprite_icon('search', size: 16)
- if current_user - if current_user
%li.user-counter = nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('issues') = sprite_icon('issues', size: 16)
- issues_count = assigned_issuables_count(:issues) - issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count) = number_with_delimiter(issues_count)
%li.user-counter = nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold') = sprite_icon('git-merge', size: 16)
- merge_requests_count = assigned_issuables_count(:merge_requests) - merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count) = number_with_delimiter(merge_requests_count)
%li.user-counter = nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('todo_done') = sprite_icon('todo-done', size: 16)
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) } %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count) = todos_count_format(todos_pending_count)
%li.header-user.dropdown %li.header-user.dropdown
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar" = image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar"
= custom_icon('caret_down') = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu-nav.dropdown-menu-align-right .dropdown-menu-nav.dropdown-menu-align-right
%ul %ul
%li.current-user %li.current-user
...@@ -73,7 +73,7 @@ ...@@ -73,7 +73,7 @@
%button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' } %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
%span.sr-only Toggle navigation %span.sr-only Toggle navigation
= icon('ellipsis-v', class: 'js-navbar-toggle-right') = sprite_icon('more', size: 16, css_class: 'more-icon js-navbar-toggle-right')
= icon('times', class: 'js-navbar-toggle-left') = sprite_icon('close', size: 16, css_class: 'close-icon js-navbar-toggle-left')
= render 'shared/outdated_browser' = render 'shared/outdated_browser'
%li.header-new.dropdown %li.header-new.dropdown
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do
= custom_icon('plus_square') = sprite_icon('plus-square', size: 16)
= custom_icon('caret_down') = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu-nav.dropdown-menu-align-right .dropdown-menu-nav.dropdown-menu-align-right
%ul %ul
- if @group&.persisted? - if @group&.persisted?
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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