Commit 79e1ed24 authored by Zeger-Jan van de Weg's avatar Zeger-Jan van de Weg

Merge branch 'master' into 'zj-format-chat-messages'

# Conflicts:
#   spec/lib/gitlab/chat_commands/command_spec.rb
parents dbda72a7 f27721e8
......@@ -15,6 +15,7 @@
"filenames"
],
"rules": {
"filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"]
"filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"],
"no-multiple-empty-lines": ["error", { "max": 1 }]
}
}
......@@ -46,7 +46,7 @@ linters:
max: 80
MultilinePipe:
enabled: false
enabled: true
MultilineScript:
enabled: true
......@@ -77,7 +77,7 @@ linters:
- Style/WhileUntilModifier
RubyComments:
enabled: false
enabled: true
SpaceBeforeScript:
enabled: true
......@@ -97,7 +97,7 @@ linters:
enabled: true
UnnecessaryInterpolation:
enabled: false
enabled: true
UnnecessaryStringOutput:
enabled: false
enabled: true
......@@ -2,6 +2,22 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 8.16.2 (2017-01-25)
- allow issue filter bar to be operated with mouse only. !8681
- Fix CI requests concurrency for newer runners that prevents from picking pending builds (from 1.9.0-rc5). !8760
- Add some basic fixes for IE11/Edge.
- Remove blue border from comment box hover.
- Fixed bug where links in merge dropdown wouldn't work.
## 8.16.1 (2017-01-23)
- Ensure export files are removed after a namespace is deleted.
- Don't allow project guests to subscribe to merge requests through the API. (Robert Schilling)
- Prevent users from creating notes on resources they can't access.
- Prevent users from deleting system deploy keys via the project deploy key API.
- Upgrade omniauth gem to 1.3.2.
## 8.16.0 (2017-02-22)
- Add LDAP Rake task to rename a provider. !2181
......@@ -395,6 +411,14 @@ entry.
- Whitelist next project names: help, ci, admin, search. !8227
- Adds back CSS for progress-bars. !8237
## 8.14.8 (2017-01-25)
- Accept environment variables from the `pre-receive` script. !7967
- Milestoneish SQL performance partially improved and memoized. !8146
- Fix N+1 queries on milestone show pages. !8185
- Speed up group milestone index by passing group_id to IssuesFinder. !8363
- Ensure issuable state changes only fire webhooks once.
## 8.14.6 (2017-01-10)
- Update the gitlab-markup gem to the version 1.5.1. !8509
......
......@@ -80,11 +80,9 @@ the remaining issues on the GitHub issue tracker.
## I want to contribute!
If you want to contribute to GitLab, but are not sure where to start,
look for [issues with the label `up-for-grabs`][up-for-grabs]. These issues
will be of reasonable size and challenge, for anyone to start contributing to
GitLab.
This was inspired by [an article by Kent C. Dodds][medium-up-for-grabs].
look for [issues with the label `Accepting Merge Requests` and weight < 5][accepting-mrs-weight].
These issues will be of reasonable size and challenge, for anyone to start
contributing to GitLab.
## Implement design & UI elements
......@@ -214,16 +212,19 @@ associated with in the description of the issue.
## Merge requests
We welcome merge requests with fixes and improvements to GitLab code, tests,
and/or documentation. The features we would really like a merge request for are
listed with the label [`Accepting Merge Requests` on our issue tracker for CE][accepting-mrs-ce]
and [EE][accepting-mrs-ee] but other improvements are also welcome. Please note
that if an issue is marked for the current milestone either before or while you
are working on it, a team member may take over the merge request in order to
ensure the work is finished before the release date.
and/or documentation. The issues that are specifically suitable for
community contributions are listed with the label
[`Accepting Merge Requests` on our issue tracker for CE][accepting-mrs-ce]
and [EE][accepting-mrs-ee], but you are free to contribute to any other issue
you want.
Please note that if an issue is marked for the current milestone either before
or while you are working on it, a team member may take over the merge request
in order to ensure the work is finished before the release date.
If you want to add a new feature that is not labeled it is best to first create
a feedback issue (if there isn't one already) and leave a comment asking for it
to be marked as `Accepting merge requests`. Please include screenshots or
to be marked as `Accepting Merge Requests`. Please include screenshots or
wireframes if the feature will also change the UI.
Merge requests should be opened at [GitLab.com][gitlab-mr-tracker].
......@@ -285,14 +286,6 @@ request is as follows:
1. For tests that use Capybara or PhantomJS, see this [article on how
to write reliable asynchronous tests](https://robots.thoughtbot.com/write-reliable-asynchronous-integration-tests-with-capybara).
The **official merge window** is in the beginning of the month from the 1st to
the 7th day of the month. This is the best time to submit an MR and get
feedback fast. Before this time the GitLab Inc. team is still dealing with work
that is created by the monthly release such as regressions requiring patch
releases. After the 7th it is already getting closer to the release date of the
next version. This means there is less time to fix the issues created by
merging large new features.
Please keep the change in a single MR **as small as possible**. If you want to
contribute a large feature think very hard what the minimum viable change is.
Can you split the functionality? Can you only submit the backend/API code? Can
......@@ -450,8 +443,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[core team]: https://about.gitlab.com/core-team/
[getting-help]: https://about.gitlab.com/getting-help/
[codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up-for-grabs
[medium-up-for-grabs]: https://medium.com/@kentcdodds/first-timers-only-78281ea47455
[accepting-mrs-weight]: https://gitlab.com/gitlab-org/gitlab-ce/issues?assignee_id=0&label_name[]=Accepting%20Merge%20Requests&sort=weight_asc
[ce-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues
[ee-tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues
[google-group]: https://groups.google.com/forum/#!forum/gitlabhq
......
......@@ -322,7 +322,7 @@ group :test do
gem 'email_spec', '~> 1.6.0'
gem 'json-schema', '~> 2.6.2'
gem 'webmock', '~> 1.21.0'
gem 'test_after_commit', '~> 0.4.2'
gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0'
end
......
......@@ -760,7 +760,7 @@ GEM
teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0)
temple (0.7.7)
test_after_commit (0.4.2)
test_after_commit (1.1.0)
activerecord (>= 3.2)
thin (1.7.0)
daemons (~> 1.0, >= 1.0.9)
......@@ -997,7 +997,7 @@ DEPENDENCIES
sys-filesystem (~> 1.1.6)
teaspoon (~> 1.1.0)
teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 0.4.2)
test_after_commit (~> 1.1)
thin (~> 1.7.0)
timecop (~> 0.8.0)
truncato (~> 0.7.8)
......
This diff is collapsed.
......@@ -261,6 +261,9 @@
case 'projects:artifacts:browse':
new BuildArtifacts();
break;
case 'help:index':
gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
break;
case 'search:show':
new Search();
break;
......
......@@ -58,6 +58,7 @@ var CustomEvent = require('./custom_event_polyfill');
var utils = require('./utils');
var DropDown = function(list) {
this.currentIndex = 0;
this.hidden = true;
this.list = list;
this.items = [];
......@@ -164,15 +165,21 @@ Object.assign(DropDown.prototype, {
},
show: function() {
if (this.hidden) {
// debugger
this.list.style.display = 'block';
this.currentIndex = 0;
this.hidden = false;
}
},
hide: function() {
if (!this.hidden) {
// debugger
this.list.style.display = 'none';
this.currentIndex = 0;
this.hidden = true;
}
},
destroy: function() {
......@@ -478,6 +485,8 @@ Object.assign(HookInput.prototype, {
this.input = function input(e) {
if(self.hasRemovedEvents) return;
self.list.show();
var inputEvent = new CustomEvent('input.dl', {
detail: {
hook: self,
......@@ -487,7 +496,6 @@ Object.assign(HookInput.prototype, {
cancelable: true
});
e.target.dispatchEvent(inputEvent);
self.list.show();
}
this.keyup = function keyup(e) {
......@@ -503,6 +511,8 @@ Object.assign(HookInput.prototype, {
}
function keyEvent(e, keyEventName){
self.list.show();
var keyEvent = new CustomEvent(keyEventName, {
detail: {
hook: self,
......@@ -514,7 +524,6 @@ Object.assign(HookInput.prototype, {
cancelable: true
});
e.target.dispatchEvent(keyEvent);
self.list.show();
}
this.events = this.events || {};
......@@ -572,24 +581,43 @@ require('./window')(function(w){
module.exports = function(){
var currentKey;
var currentFocus;
var currentIndex = 0;
var isUpArrow = false;
var isDownArrow = false;
var removeHighlight = function removeHighlight(list) {
var listItems = list.list.querySelectorAll('li');
var listItems = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0);
var listItemsTmp = [];
for(var i = 0; i < listItems.length; i++) {
listItems[i].classList.remove('dropdown-active');
var listItem = listItems[i];
listItem.classList.remove('dropdown-active');
if (listItem.style.display !== 'none') {
listItemsTmp.push(listItem);
}
return listItems;
}
return listItemsTmp;
};
var setMenuForArrows = function setMenuForArrows(list) {
var listItems = removeHighlight(list);
if(currentIndex>0){
if(!listItems[currentIndex-1]){
currentIndex = currentIndex-1;
if(list.currentIndex>0){
if(!listItems[list.currentIndex-1]){
list.currentIndex = list.currentIndex-1;
}
if (listItems[list.currentIndex-1]) {
var el = listItems[list.currentIndex-1];
var filterDropdownEl = el.closest('.filter-dropdown');
el.classList.add('dropdown-active');
if (filterDropdownEl) {
var filterDropdownBottom = filterDropdownEl.offsetHeight;
var elOffsetTop = el.offsetTop - 30;
if (elOffsetTop > filterDropdownBottom) {
filterDropdownEl.scrollTop = elOffsetTop - filterDropdownBottom;
}
}
}
listItems[currentIndex-1].classList.add('dropdown-active');
}
};
......@@ -597,13 +625,13 @@ require('./window')(function(w){
var list = e.detail.hook.list;
removeHighlight(list);
list.show();
currentIndex = 0;
list.currentIndex = 0;
isUpArrow = false;
isDownArrow = false;
};
var selectItem = function selectItem(list) {
var listItems = removeHighlight(list);
var currentItem = listItems[currentIndex-1];
var currentItem = listItems[list.currentIndex-1];
var listEvent = new CustomEvent('click.dl', {
detail: {
list: list,
......@@ -617,6 +645,8 @@ require('./window')(function(w){
var keydown = function keydown(e){
var typedOn = e.target;
var list = e.detail.hook.list;
var currentIndex = list.currentIndex;
isUpArrow = false;
isDownArrow = false;
......@@ -648,6 +678,7 @@ require('./window')(function(w){
if(isUpArrow){ currentIndex--; }
if(isDownArrow){ currentIndex++; }
if(currentIndex < 0){ currentIndex = 0; }
list.currentIndex = currentIndex;
setMenuForArrows(e.detail.hook.list);
};
......
......@@ -29,6 +29,7 @@ require('../window')(function(w){
init: function init(hook) {
var self = this;
var config = hook.config.droplabAjax;
this.hook = hook;
if (!config || !config.endpoint || !config.method) {
return;
......@@ -52,19 +53,26 @@ require('../window')(function(w){
this._loadUrlData(config.endpoint)
.then(function(d) {
if (config.loadingTemplate) {
var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]');
var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) {
dataLoadingTemplate.outerHTML = self.listTemplate;
}
}
hook.list[config.method].call(hook.list, d);
if (!self.hook.list.hidden) {
self.hook.list[config.method].call(self.hook.list, d);
}
}).catch(function(e) {
throw new droplabAjaxException(e.message || e);
});
},
destroy: function() {
if (this.listTemplate) {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
dynamicList.outerHTML = this.listTemplate;
}
}
};
});
......
......@@ -93,6 +93,7 @@ require('../window')(function(w){
self.hook.list.setData.call(self.hook.list, data);
}
self.notLoading();
self.hook.list.currentIndex = 0;
});
},
......
......@@ -6,6 +6,8 @@ require('../window')(function(w){
w.droplabFilter = {
keydownWrapper: function(e){
var hiddenCount = 0;
var dataHiddenCount = 0;
var list = e.detail.hook.list;
var data = list.data;
var value = e.detail.hook.trigger.value.toLowerCase();
......@@ -27,10 +29,22 @@ require('../window')(function(w){
};
}
dataHiddenCount = data.filter(function(o) {
return !o.droplab_hidden;
}).length;
matches = data.map(function(o) {
return filterFunction(o, value);
});
hiddenCount = matches.filter(function(o) {
return !o.droplab_hidden;
}).length;
if (dataHiddenCount !== hiddenCount) {
list.render(matches);
list.currentIndex = 0;
}
},
init: function init(hookInput) {
......
......@@ -3,7 +3,6 @@
//= require ./components/environment
//= require ./vue_resource_interceptor
$(() => {
window.gl = window.gl || {};
......
......@@ -84,7 +84,7 @@
let inputValue = input.value;
// Replace all spaces inside quote marks with underscores
// This helps with matching the beginning & end of a token:key
inputValue = inputValue.replace(/"(.*?)"/g, str => str.replace(/\s/g, '_'));
inputValue = inputValue.replace(/("(.*?)"|:\s+)/g, str => str.replace(/\s/g, '_'));
// Get the right position for the word selected
// Regex matches first space
......
......@@ -64,15 +64,28 @@
}
checkForEnter(e) {
if (e.keyCode === 38 || e.keyCode === 40) {
const selectionStart = this.filteredSearchInput.selectionStart;
e.preventDefault();
this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
}
if (e.keyCode === 13) {
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
const dropdownEl = dropdown.element;
const activeElements = dropdownEl.querySelectorAll('.dropdown-active');
e.preventDefault();
if (!activeElements.length) {
// Prevent droplab from opening dropdown
this.dropdownManager.destroyDroplab();
this.search();
}
}
}
toggleClearSearchButton(e) {
if (e.target.value) {
......
......@@ -367,9 +367,14 @@
return $input.trigger('keyup');
},
isLoading(data) {
if (!data || !data.length) return false;
if (Array.isArray(data)) data = data[0];
return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0];
var dataToInspect = data;
if (data && data.length > 0) {
dataToInspect = data[0];
}
var loadingState = this.defaultLoadingData[0];
return dataToInspect &&
(dataToInspect === loadingState || dataToInspect.name === loadingState);
}
};
}).call(this);
......@@ -651,18 +651,14 @@
isMarking = false;
el.removeClass(ACTIVE_CLASS);
if (field && field.length) {
if (isInput) {
field.val('');
} else {
field.remove();
}
this.clearField(field, isInput);
}
} else if (el.hasClass(INDETERMINATE_CLASS)) {
isMarking = true;
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
if (field && field.length && value == null) {
field.remove();
this.clearField(field, isInput);
}
if ((!field || !field.length) && fieldName) {
this.addInput(fieldName, value, selectedObject);
......@@ -676,7 +672,7 @@
}
}
if (field && field.length && value == null) {
field.remove();
this.clearField(field, isInput);
}
// Toggle active class for the tick mark
el.addClass(ACTIVE_CLASS);
......@@ -826,6 +822,10 @@
return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance));
};
GitLabDropdown.prototype.clearField = function(field, isInput) {
return isInput ? field.val('') : field.remove();
};
return GitLabDropdown;
})();
......
......@@ -61,7 +61,6 @@
return labels;
}
/**
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
......@@ -80,7 +79,6 @@
return result;
}
/**
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
......
......@@ -336,7 +336,11 @@
.removeClass('is-active');
}
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
if ($dropdown.hasClass('js-issuable-form-dropdown')) {
return;
}
if ($dropdown.hasClass('js-filter-bulk-update')) {
_this.enableBulkLabelDropdown();
_this.setDropdownData($dropdown, isMarking, this.id(label));
return;
......
......@@ -159,5 +159,75 @@
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
w.gl.utils.getSelectedFragment = () => {
const selection = window.getSelection();
const documentFragment = selection.getRangeAt(0).cloneContents();
if (documentFragment.textContent.length === 0) return null;
return documentFragment;
};
w.gl.utils.insertText = (target, text) => {
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
const selectionStart = target.selectionStart;
const selectionEnd = target.selectionEnd;
const value = target.value;
const textBefore = value.substring(0, selectionStart);
const textAfter = value.substring(selectionEnd, value.length);
const newText = textBefore + text + textAfter;
target.value = newText;
target.selectionStart = target.selectionEnd = selectionStart + text.length;
// Trigger autosave
$(target).trigger('input');
// Trigger autosize
var event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
target.dispatchEvent(event);
};
w.gl.utils.nodeMatchesSelector = (node, selector) => {
const matches = Element.prototype.matches ||
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector;
if (matches) {
return matches.call(node, selector);
}
// IE11 doesn't support `node.matches(selector)`
let parentNode = node.parentNode;
if (!parentNode) {
parentNode = document.createElement('div');
node = node.cloneNode(true);
parentNode.appendChild(node);
}
const matchingNodes = parentNode.querySelectorAll(selector);
return Array.prototype.indexOf.call(matchingNodes, node) !== -1;
};
/**
this will take in the headers from an API response and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
w.gl.utils.normalizeHeaders = (headers) => {
const upperCaseHeaders = {};
Object.keys(headers).forEach((e) => {
upperCaseHeaders[e.toUpperCase()] = headers[e];
});
return upperCaseHeaders;
};
})(window);
}).call(this);
......@@ -220,7 +220,6 @@
})(this));
};
/*
Increase @pollingInterval up to 120 seconds on every function call,
if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
......@@ -244,7 +243,6 @@
return this.initRefresh();
};
Notes.prototype.handleCreateChanges = function(note) {
if (typeof note === 'undefined') {
return;
......@@ -294,7 +292,6 @@
}
};
/*
Check if note does not exists on page
*/
......@@ -307,7 +304,6 @@
return this.view === 'parallel';
};
/*
Render note in discussion area.
......@@ -358,7 +354,6 @@
return this.updateNotesCount(1);
};
/*
Called in response the main target form has been successfully submitted.
......@@ -390,7 +385,6 @@
return form.find(".js-note-text").trigger("input");
};
/*
Shows the main form and does some setup on it.
......@@ -415,7 +409,6 @@
return this.parentTimeline = form.parents('.timeline');
};
/*
General note form setup.
......@@ -432,7 +425,6 @@
return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]);
};
/*
Called in response to the new note form being submitted
......@@ -448,7 +440,6 @@
return new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', this.parentTimeline);
};
/*
Called in response to the new note form being submitted
......@@ -473,7 +464,6 @@
this.removeDiscussionNoteForm($form);
};
/*
Called in response to the edit note form being submitted
......@@ -498,7 +488,6 @@
}
};
Notes.prototype.checkContentToAllowEditing = function($el) {
var initialContent = $el.find('.original-note-content').text().trim();
var currentContent = $el.find('.note-textarea').val();
......@@ -522,7 +511,6 @@
return isAllowed;
};
/*
Called in response to clicking the edit note link
......@@ -551,7 +539,6 @@
this.putEditFormInPlace($target);
};
/*
Called in response to clicking the edit note link
......@@ -596,7 +583,6 @@
return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note'));
};
/*
Called in response to deleting a note of any kind.
......@@ -636,7 +622,6 @@
return this.updateNotesCount(-1);
};
/*
Called in response to clicking the delete attachment link
......@@ -653,7 +638,6 @@
return note.find(".current-note-edit-form").remove();
};
/*
Called when clicking on the "reply" button for a diff line.
......@@ -673,7 +657,6 @@
return this.setupDiscussionNoteForm(replyLink, form);
};
/*
Shows the diff or discussion form and does some setup on it.
......@@ -715,7 +698,6 @@
.addClass("discussion-form js-discussion-note-form");
};
/*
Called when clicking on the "add a comment" button on the side of a diff line.
......@@ -772,7 +754,6 @@
}
};
/*
Called in response to "cancel" on a diff note form.
......@@ -806,7 +787,6 @@
return this.removeDiscussionNoteForm(form);
};
/*
Called after an attachment file has been selected.
......@@ -821,7 +801,6 @@
return form.find(".js-attachment-filename").text(filename);
};
/*
Called when the tab visibility changes
*/
......
......@@ -69,12 +69,17 @@
search: {
fields: ['text']
},
id: this.getSearchText,
data: this.getData.bind(this),
selectable: true,
clicked: this.onClick.bind(this)
});
}
getSearchText(selectedObject, el) {
return selectedObject.id ? selectedObject.text : '';
}
getData(term, callback) {
var _this, contents, jqXHR;
_this = this;
......@@ -364,7 +369,7 @@
onClick(item, $el, e) {
if (location.pathname.indexOf(item.url) !== -1) {
e.preventDefault();
if (!e.metaKey) e.preventDefault();
if (!this.badgePresent) {
if (item.category === 'Projects') {
this.projectInputEl.val(item.id);
......
......@@ -39,29 +39,39 @@
}
ShortcutsIssuable.prototype.replyWithSelectedText = function() {
var quote, replyField, selected, separator;
if (window.getSelection) {
selected = window.getSelection().toString();
var quote, replyField, documentFragment, selected, separator;
documentFragment = window.gl.utils.getSelectedFragment();
if (!documentFragment) return;
// If the documentFragment contains more than just Markdown, don't copy as GFM.
if (documentFragment.querySelector('.md, .wiki')) return;
selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment);
replyField = $('.js-main-target-form #note_note');
if (selected.trim() === "") {
return;
}
// Put a '>' character before each non-empty line in the selection
quote = _.map(selected.split("\n"), function(val) {
if (val.trim() !== '') {
return "> " + val + "\n";
}
return ("> " + val).trim() + "\n";
});
// If replyField already has some content, add a newline before our quote
separator = replyField.val().trim() !== "" && "\n" || '';
separator = replyField.val().trim() !== "" && "\n\n" || '';
replyField.val(function(_, current) {
return current + separator + quote.join('') + "\n";
});
// Trigger autosave for the added text
// Trigger autosave
replyField.trigger('input');
// Trigger autosize
var event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
replyField.get(0).dispatchEvent(event);
// Focus the input field
return replyField.focus();
}
};
ShortcutsIssuable.prototype.editIssue = function() {
......
(() => {
class VersionCheckImage {
static bindErrorEvent(imageElement) {
imageElement.off('error').on('error', () => imageElement.hide());
}
}
window.gl = window.gl || {};
gl.VersionCheckImage = VersionCheckImage;
})();
......@@ -22,47 +22,51 @@
<div class="controls pull-right">
<div class="btn-group inline">
<div class="btn-group">
<a
<button
v-if='actions'
class="dropdown-toggle btn btn-default js-pipeline-dropdown-manual-actions"
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
data-toggle="dropdown"
title="Manual build"
alt="Manual Build"
data-placement="top"
data-toggle="dropdown"
aria-label="Manual build"
>
<span v-html='svgs.iconPlay'></span>
<i class="fa fa-caret-down"></i>
</a>
<span v-html='svgs.iconPlay' aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='action in pipeline.details.manual_actions'>
<a
rel="nofollow"
data-method="post"
:href='action.path'
title="Manual build"
>
<span v-html='svgs.iconPlay'></span>
<span title="Manual build">{{action.name}}</span>
<span v-html='svgs.iconPlay' aria-hidden="true"></span>
<span>{{action.name}}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group">
<a
<button
v-if='artifacts'
class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
data-toggle="dropdown"
type="button"
title="Artifacts"
data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts"
>
<i class="fa fa-download"></i>
<i class="fa fa-caret-down"></i>
</a>
<i class="fa fa-download" aria-hidden="true"></i>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='artifact in pipeline.details.artifacts'>
<a
rel="nofollow"
:href='artifact.path'
>
<i class="fa fa-download"></i>
<i class="fa fa-download" aria-hidden="true"></i>
<span>{{download(artifact.name)}}</span>
</a>
</li>
......@@ -76,9 +80,12 @@
title="Retry"
rel="nofollow"
data-method="post"
data-placement="top"
data-toggle="dropdown"
:href='pipeline.retry_path'
aria-label="Retry"
>
<i class="fa fa-repeat"></i>
<i class="fa fa-repeat" aria-hidden="true"></i>
</a>
<a
v-if='pipeline.flags.cancelable'
......@@ -86,10 +93,12 @@
title="Cancel"
rel="nofollow"
data-method="post"
data-placement="top"
data-toggle="dropdown"
:href='pipeline.cancel_path'
data-original-title="Cancel"
aria-label="Cancel"
>
<i class="fa fa-remove"></i>
<i class="fa fa-remove" aria-hidden="true"></i>
</a>
</div>
</div>
......
......@@ -82,12 +82,13 @@
data-placement="top"
data-toggle="dropdown"
type="button"
:aria-label='stage.title'
>
<span v-html="svg"></span>
<i class="fa fa-caret-down "></i>
<span v-html="svg" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up"></div>
<div class="arrow-up" aria-hidden="true"></div>
<div
@click='keepGraph($event)'
:class="dropdownClass"
......
......@@ -4,19 +4,15 @@
((gl) => {
const pageValues = (headers) => {
const normalizedHeaders = {};
Object.keys(headers).forEach((e) => {
normalizedHeaders[e.toUpperCase()] = headers[e];
});
const normalized = gl.utils.normalizeHeaders(headers);
const paginationInfo = {
perPage: +normalizedHeaders['X-PER-PAGE'],
page: +normalizedHeaders['X-PAGE'],
total: +normalizedHeaders['X-TOTAL'],
totalPages: +normalizedHeaders['X-TOTAL-PAGES'],
nextPage: +normalizedHeaders['X-NEXT-PAGE'],
previousPage: +normalizedHeaders['X-PREV-PAGE'],
perPage: +normalized['X-PER-PAGE'],
page: +normalized['X-PAGE'],
total: +normalized['X-TOTAL'],
totalPages: +normalized['X-TOTAL-PAGES'],
nextPage: +normalized['X-NEXT-PAGE'],
previousPage: +normalized['X-PREV-PAGE'],
};
return paginationInfo;
......
......@@ -82,7 +82,12 @@
}
.block-controls {
float: right;
display: -webkit-flex;
display: flex;
-webkit-justify-content: flex-end;
justify-content: flex-end;
-webkit-flex: 1;
flex: 1;
.control {
float: left;
......@@ -282,3 +287,8 @@
}
}
}
.flex-container-block {
display: -webkit-flex;
display: flex;
}
......@@ -79,6 +79,16 @@
overflow: auto;
}
%filter-dropdown-item-btn-hover {
background-color: $dropdown-hover-color;
color: $white-light;
text-decoration: none;
.avatar {
border-color: $white-light;
}
}
.filter-dropdown-item {
.btn {
border: none;
......@@ -103,13 +113,7 @@
&:hover,
&:focus {
background-color: $dropdown-hover-color;
color: $white-light;
text-decoration: none;
.avatar {
border-color: $white-light;
}
@extend %filter-dropdown-item-btn-hover;
}
}
......@@ -131,6 +135,12 @@
}
}
.filter-dropdown-item.dropdown-active {
.btn {
@extend %filter-dropdown-item-btn-hover;
}
}
.hint-dropdown {
width: 250px;
}
......
......@@ -155,7 +155,8 @@ ul.content-list {
}
> .btn,
> .btn-group {
> .btn-group,
> .dropdown.inline {
margin-right: $gl-padding-top;
display: inline-block;
margin-top: 3px;
......
......@@ -13,6 +13,8 @@ $dark-main-bg: #1d1f21;
$dark-main-color: #1d1f21;
$dark-line-color: #c5c8c6;
$dark-line-num-color: rgba(255, 255, 255, 0.3);
$dark-line-num-color-new: #627165;
$dark-line-num-color-old: #806565;
$dark-diff-not-empty-bg: #557;
$dark-highlight-bg: #ffe792;
$dark-highlight-color: $black;
......@@ -89,7 +91,6 @@ $dark-il: #de935f;
.diff-line-num,
.diff-line-num a {
color: $dark-main-color;
color: $dark-line-num-color;
}
......@@ -121,11 +122,21 @@ $dark-il: #de935f;
.diff-line-num.new,
.line_content.new {
@include diff_background($dark-new-bg, $dark-new-idiff, $dark-border);
&::before,
a {
color: $dark-line-num-color-new;
}
}
.diff-line-num.old,
.line_content.old {
@include diff_background($dark-old-bg, $dark-old-idiff, $dark-border);
&::before,
a {
color: $dark-line-num-color-old;
}
}
.line_content.match {
......
......@@ -7,6 +7,8 @@ $monokai-bg: #272822;
$monokai-border: #555;
$monokai-text-color: #f8f8f2;
$monokai-line-num-color: rgba(255, 255, 255, 0.3);
$monokai-line-num-color-new: #707565;
$monokai-line-num-color-old: #7e736f;
$monokai-line-empty-bg: #49483e;
$monokai-line-empty-border: darken($monokai-line-empty-bg, 15%);
$monokai-diff-border: #808080;
......@@ -120,11 +122,21 @@ $monokai-gi: #a6e22e;
.diff-line-num.new,
.line_content.new {
@include diff_background($monokai-new-bg, $monokai-new-idiff, $monokai-diff-border);
&::before,
a {
color: $monokai-line-num-color-new;
}
}
.diff-line-num.old,
.line_content.old {
@include diff_background($monokai-old-bg, $monokai-old-idiff, $monokai-diff-border);
&::before,
a {
color: $monokai-line-num-color-old;
}
}
.line_content.match {
......
......@@ -13,6 +13,8 @@ $solarized-dark-pre-color: #93a1a1;
$solarized-dark-pre-border: #113b46;
$solarized-dark-line-bg: #002b36;
$solarized-dark-line-color: rgba(255, 255, 255, 0.3);
$solarized-dark-line-color-new: #5a766c;
$solarized-dark-line-color-old: #7a6c71;
$solarized-dark-highlight: #094554;
$solarized-dark-hll-bg: #174652;
$solarized-dark-c: #586e75;
......@@ -124,11 +126,21 @@ $solarized-dark-il: #2aa198;
.diff-line-num.new,
.line_content.new {
@include diff_background($solarized-dark-new-bg, $solarized-dark-new-idiff, $solarized-dark-border);
&::before,
a {
color: $solarized-dark-line-color-new;
}
}
.diff-line-num.old,
.line_content.old {
@include diff_background($solarized-dark-old-bg, $solarized-dark-old-idiff, $solarized-dark-border);
&::before,
a {
color: $solarized-dark-line-color-old;
}
}
.line_content.match {
......
......@@ -13,6 +13,9 @@ $solarized-light-pre-bg: #002b36;
$solarized-light-pre-bg: #fdf6e3;
$solarized-light-pre-color: #586e75;
$solarized-light-line-bg: #fdf6e3;
$solarized-light-line-color: rgba(0, 0, 0, 0.3);
$solarized-light-line-color-new: #a1a080;
$solarized-light-line-color-old: #ad9186;
$solarized-light-highlight: #eee8d5;
$solarized-light-hll-bg: #ddd8c5;
$solarized-light-c: #93a1a1;
......@@ -98,7 +101,7 @@ $solarized-light-il: #2aa198;
.diff-line-num,
.diff-line-num a {
color: $black-transparent;
color: $solarized-light-line-color;
}
// Code itself
......@@ -130,11 +133,21 @@ $solarized-light-il: #2aa198;
.line_content.new {
@include diff_background($solarized-light-new-bg,
$solarized-light-new-idiff, $solarized-light-border);
&::before,
a {
color: $solarized-light-line-color-new;
}
}
.diff-line-num.old,
.line_content.old {
@include diff_background($solarized-light-old-bg, $solarized-light-old-idiff, $solarized-light-border);
&::before,
a {
color: $solarized-light-line-color-old;
}
}
.line_content.match {
......
......@@ -108,11 +108,19 @@ $white-gc-bg: #eaf2f5;
&.old {
background-color: $line-number-old;
border-color: $line-removed-dark;
a {
color: scale-color($line-number-old,$red: -30%, $green: -30%, $blue: -30%);
}
}
&.new {
background-color: $line-number-new;
border-color: $line-added-dark;
a {
color: scale-color($line-number-new,$red: -30%, $green: -30%, $blue: -30%);
}
}
&.hll:not(.empty-cell) {
......@@ -125,6 +133,10 @@ $white-gc-bg: #eaf2f5;
&.old {
background-color: $line-removed;
&::before {
color: scale-color($line-number-old,$red: -30%, $green: -30%, $blue: -30%);
}
span.idiff {
background-color: $line-removed-dark;
}
......@@ -133,6 +145,10 @@ $white-gc-bg: #eaf2f5;
&.new {
background-color: $line-added;
&::before {
color: scale-color($line-number-new,$red: -30%, $green: -30%, $blue: -30%);
}
span.idiff {
background-color: $line-added-dark;
}
......
......@@ -203,6 +203,10 @@
position: relative;
margin-right: 6px;
.tooltip {
white-space: nowrap;
}
.tooltip-inner {
padding: 3px 4px;
}
......@@ -288,6 +292,10 @@
}
}
}
.tooltip {
white-space: nowrap;
}
}
.build-link {
......
......@@ -929,8 +929,32 @@ pre.light-well {
.variables-table {
table-layout: fixed;
&.table-responsive {
border: none;
}
.variable-key {
width: 30%;
width: 300px;
max-width: 300px;
overflow: hidden;
word-wrap: break-word;
// override bootstrap
white-space: normal!important;
@media (max-width: $screen-sm-max) {
width: 150px;
max-width: 150px;
}
}
.variable-value {
@media(max-width: $screen-xs-max) {
width: 150px;
max-width: 150px;
overflow: hidden;
word-wrap: break-word;
}
}
}
......
......@@ -32,6 +32,10 @@
.last-commit {
@include str-truncated(506px);
.fa-angle-right {
margin-left: 5px;
}
@media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
@include str-truncated(450px);
}
......
......@@ -5,7 +5,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def update
if @application_setting.update_attributes(application_setting_params)
successful = ApplicationSettings::UpdateService
.new(@application_setting, current_user, application_setting_params)
.execute
if successful
redirect_to admin_application_settings_path,
notice: 'Application settings saved successfully'
else
......
......@@ -14,12 +14,8 @@ class ConfirmationsController < Devise::ConfirmationsController
if signed_in?(resource_name)
after_sign_in_path_for(resource)
else
sign_in(resource)
if signed_in?(resource_name)
after_sign_in_path_for(resource)
else
flash[:notice] += " Please sign in."
new_session_path(resource_name)
end
end
end
end
......@@ -109,12 +109,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController
end
def repository
_, suffix = project_id_with_suffix
if suffix == '.wiki.git'
project.wiki.repository
else
project.repository
wiki? ? project.wiki.repository : project.repository
end
def wiki?
return @wiki if defined?(@wiki)
_, suffix = project_id_with_suffix
@wiki = suffix == '.wiki.git'
end
def render_not_found
......
......@@ -84,7 +84,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end
def access
@access ||= Gitlab::GitAccess.new(user, project, 'http', authentication_abilities: authentication_abilities)
@access ||= access_klass.new(user, project, 'http', authentication_abilities: authentication_abilities)
end
def access_check
......@@ -102,4 +102,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController
access_check.allowed?
end
def access_klass
@access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
end
end
......@@ -45,6 +45,8 @@ class SearchController < ApplicationController
end
@search_objects = @search_results.objects(@scope, params[:page])
check_single_commit_result
end
def autocomplete
......@@ -59,4 +61,16 @@ class SearchController < ApplicationController
render json: search_autocomplete_opts(term).to_json
end
private
def check_single_commit_result
if @search_results.single_commit_result?
only_commit = @search_results.objects('commits').first
query = params[:search].strip.downcase
found_by_commit_sha = Commit.valid_hash?(query) && only_commit.sha.start_with?(query)
redirect_to namespace_project_commit_path(@project.namespace, @project, only_commit) if found_by_commit_sha
end
end
end
......@@ -3,7 +3,7 @@ module CompareHelper
from.present? &&
to.present? &&
from != to &&
project.feature_available?(:merge_requests, current_user) &&
can?(current_user, :create_merge_request, project) &&
project.repository.branch_names.include?(from) &&
project.repository.branch_names.include?(to)
end
......
......@@ -14,7 +14,7 @@ module GroupsHelper
def group_title(group, name = nil, url = nil)
full_title = ''
group.parents.each do |parent|
group.ancestors.each do |parent|
full_title += link_to(simple_sanitize(parent.name), group_path(parent))
full_title += ' / '.html_safe
end
......
module VersionCheckHelper
def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled
image_tag VersionCheck.new.url
image_url = VersionCheck.new.url
image_tag image_url, class: 'js-version-status-badge'
end
end
end
......@@ -38,6 +38,14 @@ module Emails
mail_answer_thread(@snippet, note_thread_options(recipient_id))
end
def note_personal_snippet_email(recipient_id, note_id)
setup_note_mail(note_id, recipient_id)
@snippet = @note.noteable
@target_url = snippet_url(@note.noteable)
mail_answer_thread(@snippet, note_thread_options(recipient_id))
end
private
def note_target_url_options
......
......@@ -22,6 +22,17 @@ class Ability
end
end
# Given a list of users and a snippet this method returns the users that can
# read the given snippet.
def users_that_can_read_personal_snippet(users, snippet)
case snippet.visibility_level
when Snippet::INTERNAL, Snippet::PUBLIC
users
when Snippet::PRIVATE
users.include?(snippet.author) ? [snippet.author] : []
end
end
# Returns an Array of Issues that can be read by the given user.
#
# issues - The issues to reduce down to those readable by the user.
......
......@@ -13,6 +13,49 @@ class ApplicationSetting < ActiveRecord::Base
[\r\n] # any number of newline characters
}x
DEFAULTS_CE = {
after_sign_up_text: nil,
akismet_enabled: false,
container_registry_token_expire_delay: 5,
default_branch_protection: Settings.gitlab['default_branch_protection'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_projects_limit: Settings.gitlab['default_projects_limit'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
disabled_oauth_sign_in_sources: [],
domain_whitelist: Settings.gitlab['domain_whitelist'],
gravatar_enabled: Settings.gravatar['enabled'],
help_page_text: nil,
housekeeping_bitmaps_enabled: true,
housekeeping_enabled: true,
housekeeping_full_repack_period: 50,
housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10,
import_sources: Gitlab::ImportSources.values,
koding_enabled: false,
koding_url: nil,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
plantuml_enabled: false,
plantuml_url: nil,
recaptcha_enabled: false,
repository_checks_enabled: true,
repository_storages: ['default'],
require_two_factor_authentication: false,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
session_expire_delay: Settings.gitlab['session_expire_delay'],
send_user_confirmation_email: false,
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
shared_runners_text: nil,
sidekiq_throttling_enabled: false,
sign_in_text: nil,
signin_enabled: Settings.gitlab['signin_enabled'],
signup_enabled: Settings.gitlab['signup_enabled'],
two_factor_grace_period: 48,
user_default_external: false
}
DEFAULTS = DEFAULTS_CE
serialize :restricted_visibility_levels
serialize :import_sources
serialize :disabled_oauth_sign_in_sources, Array
......@@ -163,46 +206,7 @@ class ApplicationSetting < ActiveRecord::Base
end
def self.create_from_defaults
create(
default_projects_limit: Settings.gitlab['default_projects_limit'],
default_branch_protection: Settings.gitlab['default_branch_protection'],
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
sign_in_text: nil,
after_sign_up_text: nil,
help_page_text: nil,
shared_runners_text: nil,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
session_expire_delay: Settings.gitlab['session_expire_delay'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
domain_whitelist: Settings.gitlab['domain_whitelist'],
import_sources: Gitlab::ImportSources.values,
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
two_factor_grace_period: 48,
recaptcha_enabled: false,
akismet_enabled: false,
koding_enabled: false,
koding_url: nil,
plantuml_enabled: false,
plantuml_url: nil,
repository_checks_enabled: true,
disabled_oauth_sign_in_sources: [],
send_user_confirmation_email: false,
container_registry_token_expire_delay: 5,
repository_storages: ['default'],
user_default_external: false,
sidekiq_throttling_enabled: false,
housekeeping_enabled: true,
housekeeping_bitmaps_enabled: true,
housekeeping_incremental_repack_period: 10,
housekeeping_full_repack_period: 50,
housekeeping_gc_period: 200,
)
create(DEFAULTS)
end
def home_page_url_column_exist
......
......@@ -21,6 +21,9 @@ class Commit
DIFF_HARD_LIMIT_FILES = 1000
DIFF_HARD_LIMIT_LINES = 50000
# The SHA can be between 7 and 40 hex characters.
COMMIT_SHA_PATTERN = '\h{7,40}'
class << self
def decorate(commits, project)
commits.map do |commit|
......@@ -52,6 +55,10 @@ class Commit
def from_hash(hash, project)
new(Gitlab::Git::Commit.new(hash), project)
end
def valid_hash?(key)
!!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key)
end
end
attr_accessor :raw
......@@ -77,8 +84,6 @@ class Commit
# Pattern used to extract commit references from text
#
# The SHA can be between 7 and 40 hex characters.
#
# This pattern supports cross-project references.
def self.reference_pattern
@reference_pattern ||= %r{
......@@ -88,7 +93,7 @@ class Commit
end
def self.link_reference_pattern
@link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/)
@link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/)
end
def to_reference(from_project = nil, full: false)
......
......@@ -51,6 +51,10 @@ module CacheMarkdownField
CACHING_CLASSES.map(&:constantize)
end
def skip_project_check?
false
end
extend ActiveSupport::Concern
included do
......@@ -112,7 +116,8 @@ module CacheMarkdownField
invalidation_method = "#{html_field}_invalidated?".to_sym
define_method(cache_method) do
html = Banzai::Renderer.cacheless_render_field(self, markdown_field)
options = { skip_project_check: skip_project_check? }
html = Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
__send__("#{html_field}=", html)
true
end
......
......@@ -49,7 +49,11 @@ module Mentionable
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
options = options.merge(cache_key: [self, attr], author: author)
options = options.merge(
cache_key: [self, attr],
author: author,
skip_project_check: skip_project_check?
)
extractor.analyze(text, options)
end
......@@ -121,4 +125,8 @@ module Mentionable
def cross_reference_exists?(target)
SystemNoteService.cross_reference_exists?(target, local_reference)
end
def skip_project_check?
false
end
end
......@@ -96,6 +96,11 @@ module Participable
participants.merge(ext.users)
case self
when PersonalSnippet
Ability.users_that_can_read_personal_snippet(participants.to_a, self)
else
Ability.users_that_can_read_project(participants.to_a, project)
end
end
end
......@@ -60,6 +60,21 @@ module Routable
joins(:route).where(wheres.join(' OR '))
end
end
# Builds a relation to find multiple objects that are nested under user membership
#
# Usage:
#
# Klass.member_descendants(1)
#
# Returns an ActiveRecord::Relation.
def member_descendants(user_id)
joins(:route).
joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%')
INNER JOIN members ON members.source_id = r2.source_id
AND members.source_type = r2.source_type").
where('members.user_id = ?', user_id)
end
end
private
......
......@@ -11,8 +11,8 @@ module Taskable
INCOMPLETE = 'incomplete'.freeze
ITEM_PATTERN = /
^
(?:\s*[-+*]|(?:\d+\.))? # optional list prefix
\s* # optional whitespace prefix
\s*(?:[-+*]|(?:\d+\.)) # list prefix required - task item has to be always in a list
\s+ # whitespace prefix has to be always presented for a list item
(\[\s\]|\[[xX]\]) # checkbox
(\s.+) # followed by whitespace and some text.
/x
......
......@@ -201,7 +201,7 @@ class Group < Namespace
end
def members_with_parents
GroupMember.where(requested_at: nil, source_id: parents.map(&:id).push(id))
GroupMember.where(requested_at: nil, source_id: ancestors.map(&:id).push(id))
end
def users_with_parents
......
......@@ -68,9 +68,9 @@ class Member < ActiveRecord::Base
after_create :send_request, if: :request?, unless: :importing?
after_create :create_notification_setting, unless: [:pending?, :importing?]
after_create :post_create_hook, unless: [:pending?, :importing?]
after_create :refresh_member_authorized_projects, if: :importing?
after_update :post_update_hook, unless: [:pending?, :importing?]
after_destroy :post_destroy_hook, unless: :pending?
after_commit :refresh_member_authorized_projects
delegate :name, :username, :email, to: :user, prefix: true
......@@ -147,8 +147,6 @@ class Member < ActiveRecord::Base
member.save
end
UserProjectAccessChangedService.new(user.id).execute if user.is_a?(User)
member
end
......@@ -275,23 +273,27 @@ class Member < ActiveRecord::Base
end
def post_create_hook
UserProjectAccessChangedService.new(user.id).execute
system_hook_service.execute_hooks_for(self, :create)
end
def post_update_hook
UserProjectAccessChangedService.new(user.id).execute if access_level_changed?
# override in sub class
end
def post_destroy_hook
refresh_member_authorized_projects
system_hook_service.execute_hooks_for(self, :destroy)
end
# Refreshes authorizations of the current member.
#
# This method schedules a job using Sidekiq and as such **must not** be called
# in a transaction. Doing so can lead to the job running before the
# transaction has been committed, resulting in the job either throwing an
# error or not doing any meaningful work.
def refresh_member_authorized_projects
# If user/source is being destroyed, project access are gonna be destroyed eventually
# because of DB foreign keys, so we shouldn't bother with refreshing after each
# member is destroyed through association
# If user/source is being destroyed, project access are going to be
# destroyed eventually because of DB foreign keys, so we shouldn't bother
# with refreshing after each member is destroyed through association
return if destroyed_by_association.present?
UserProjectAccessChangedService.new(user_id).execute
......
......@@ -865,11 +865,13 @@ class MergeRequest < ActiveRecord::Base
paths: paths
)
transaction do
active_diff_notes.each do |note|
service.execute(note)
Gitlab::Timeless.timeless(note, &:save)
end
end
end
def keep_around_commit
project.repository.keep_around(self.merge_commit_sha)
......
......@@ -4,6 +4,7 @@ class Namespace < ActiveRecord::Base
include CacheMarkdownField
include Sortable
include Gitlab::ShellAdapter
include Gitlab::CurrentSettings
include Routable
cache_markdown_field :description, pipeline: :description
......@@ -176,6 +177,10 @@ class Namespace < ActiveRecord::Base
end
end
def shared_runners_enabled?
projects.with_shared_runners.any?
end
def full_name
@full_name ||=
if parent
......@@ -185,8 +190,26 @@ class Namespace < ActiveRecord::Base
end
end
def parents
@parents ||= parent ? parent.parents + [parent] : []
# Scopes the model on ancestors of the record
def ancestors
if parent_id
path = route.path
paths = []
until path.blank?
path = path.rpartition('/').first
paths << path
end
self.class.joins(:route).where('routes.path IN (?)', paths).reorder('routes.path ASC')
else
self.class.none
end
end
# Scopes the model on direct and indirect children of the record
def descendants
self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC')
end
private
......
......@@ -43,7 +43,8 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true
delegate :title, to: :noteable, allow_nil: true
validates :note, :project, presence: true
validates :note, presence: true
validates :project, presence: true, unless: :for_personal_snippet?
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
......@@ -53,7 +54,7 @@ class Note < ActiveRecord::Base
validates :commit_id, presence: true, if: :for_commit?
validates :author, presence: true
validate unless: [:for_commit?, :importing?] do |note|
validate unless: [:for_commit?, :importing?, :for_personal_snippet?] do |note|
unless note.noteable.try(:project) == note.project
errors.add(:invalid_project, 'Note and noteable project mismatch')
end
......@@ -83,7 +84,7 @@ class Note < ActiveRecord::Base
after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code
before_validation :set_discussion_id
after_save :keep_around_commit
after_save :keep_around_commit, unless: :for_personal_snippet?
class << self
def model_name
......@@ -165,6 +166,14 @@ class Note < ActiveRecord::Base
noteable_type == "Snippet"
end
def for_personal_snippet?
noteable.is_a?(PersonalSnippet)
end
def skip_project_check?
for_personal_snippet?
end
# override to return commits, which are not active record
def noteable
if for_commit?
......@@ -220,6 +229,10 @@ class Note < ActiveRecord::Base
note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
end
def to_ability_name
for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore
end
private
def keep_around_commit
......
......@@ -121,8 +121,6 @@ class Project < ActiveRecord::Base
# Merge Requests for target project should be removed with it
has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id'
# Merge requests from source project should be kept when source project was removed
has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
has_many :issues, dependent: :destroy
has_many :labels, dependent: :destroy, class_name: 'ProjectLabel'
has_many :services, dependent: :destroy
......@@ -226,6 +224,7 @@ class Project < ActiveRecord::Base
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_statistics, -> { includes(:statistics) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
# "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) {
......@@ -1098,12 +1097,20 @@ class Project < ActiveRecord::Base
project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end
def shared_runners_available?
shared_runners_enabled?
end
def shared_runners
shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
end
def any_runners?(&block)
if runners.active.any?(&block)
return true
end
shared_runners_enabled? && Ci::Runner.shared.active.any?(&block)
shared_runners.active.any?(&block)
end
def valid_runners_token?(token)
......
......@@ -16,8 +16,7 @@ class ProjectGroupLink < ActiveRecord::Base
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
validate :different_group
after_create :refresh_group_members_authorized_projects
after_destroy :refresh_group_members_authorized_projects
after_commit :refresh_group_members_authorized_projects
def self.access_options
Gitlab::Access.options
......
......@@ -8,15 +8,16 @@ class Route < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
after_update :rename_children, if: :path_changed?
after_update :rename_descendants, if: :path_changed?
def rename_children
def rename_descendants
# We update each row separately because MySQL does not have regexp_replace.
# rubocop:disable Rails/FindEach
Route.where('path LIKE ?', "#{path_was}/%").each do |route|
# Note that update column skips validation and callbacks.
# We need this to avoid recursive call of rename_children method
# We need this to avoid recursive call of rename_descendants method
route.update_column(:path, route.path.sub(path_was, path))
end
# rubocop:enable Rails/FindEach
end
end
......@@ -179,8 +179,8 @@ class User < ActiveRecord::Base
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) }
scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'ASC')) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
......@@ -439,6 +439,15 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})")
end
def nested_groups
Group.member_descendants(id)
end
def nested_projects
Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL').
member_descendants(id)
end
def refresh_authorized_projects
Users::RefreshAuthorizedProjectsService.new(self).execute
end
......
module ApplicationSettings
class BaseService < ::BaseService
def initialize(application_setting, user, params = {})
@application_setting, @current_user, @params = application_setting, user, params.dup
end
end
end
module ApplicationSettings
class UpdateService < ApplicationSettings::BaseService
def execute
@application_setting.update(@params)
end
end
end
......@@ -2,48 +2,72 @@ module Ci
# This class responsible for assigning
# proper pending build to runner on runner API request
class RegisterBuildService
def execute(current_runner)
builds = Ci::Build.pending.unstarted
include Gitlab::CurrentSettings
builds =
if current_runner.shared?
builds.
# don't run projects which have not enabled shared runners and builds
joins(:project).where(projects: { shared_runners_enabled: true }).
joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
attr_reader :runner
# this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
Result = Struct.new(:build, :valid?)
def initialize(runner)
@runner = runner
end
def execute
builds =
if runner.shared?
builds_for_shared_runner
else
# do run projects which are only assigned to this runner (FIFO)
builds.where(project: current_runner.projects.with_builds_enabled).order('created_at ASC')
builds_for_specific_runner
end
build = builds.find do |build|
current_runner.can_pick?(build)
runner.can_pick?(build)
end
if build
# In case when 2 runners try to assign the same build, second runner will be declined
# with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
build.runner_id = current_runner.id
build.runner_id = runner.id
build.run!
end
build
Result.new(build, true)
rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
nil
Result.new(build, false)
end
private
def builds_for_shared_runner
new_builds.
# don't run projects which have not enabled shared runners and builds
joins(:project).where(projects: { shared_runners_enabled: true }).
joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
# Implement fair scheduling
# this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
end
def builds_for_specific_runner
new_builds.where(project: runner.projects.with_builds_enabled).order('created_at ASC')
end
def running_builds_for_shared_runners
Ci::Build.running.where(runner: Ci::Runner.shared).
group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds')
end
def new_builds
Ci::Build.pending.unstarted
end
def shared_runner_build_limits_feature_enabled?
ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true'
end
end
end
......@@ -38,15 +38,13 @@ module MergeRequests
private
def merge_requests_for(branch)
origin_merge_requests = @project.origin_merge_requests
.opened.where(source_branch: branch).to_a
fork_merge_requests = @project.fork_merge_requests
.opened.where(source_branch: branch).to_a
(origin_merge_requests + fork_merge_requests)
.uniq.select(&:source_project)
# Returns all origin and fork merge requests from `@project` satisfying passed arguments.
def merge_requests_for(source_branch, mr_states: [:opened])
MergeRequest
.with_state(mr_states)
.where(source_branch: source_branch, source_project_id: @project.id)
.preload(:source_project) # we don't need a #includes since we're just preloading for the #select
.select(&:source_project)
end
def pipeline_merge_requests(pipeline)
......
......@@ -42,7 +42,7 @@ module MergeRequests
commit_ids.include?(merge_request.diff_head_sha)
end
merge_requests.uniq.select(&:source_project).each do |merge_request|
filter_merge_requests(merge_requests).each do |merge_request|
MergeRequests::PostMergeService.
new(merge_request.target_project, @current_user).
execute(merge_request)
......@@ -58,10 +58,13 @@ module MergeRequests
def reload_merge_requests
merge_requests = @project.merge_requests.opened.
by_source_or_target_branch(@branch_name).to_a
merge_requests += fork_merge_requests
merge_requests = filter_merge_requests(merge_requests)
merge_requests.each do |merge_request|
# Fork merge requests
merge_requests += MergeRequest.opened
.where(source_branch: @branch_name, source_project: @project)
.where.not(target_project: @project).to_a
filter_merge_requests(merge_requests).each do |merge_request|
if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_diff
else
......@@ -175,16 +178,7 @@ module MergeRequests
end
def merge_requests_for_source_branch
@source_merge_requests ||= begin
merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a
merge_requests += fork_merge_requests
filter_merge_requests(merge_requests)
end
end
def fork_merge_requests
@fork_merge_requests ||= @project.fork_merge_requests.opened.
where(source_branch: @branch_name).to_a
@source_merge_requests ||= merge_requests_for(@branch_name)
end
def branch_added?
......
......@@ -3,7 +3,8 @@ module Notes
def execute
merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
note = project.notes.new(params)
note = Note.new(params)
note.project = project
note.author = current_user
note.system = false
......
......@@ -10,6 +10,9 @@ module Notes
# Skip system notes, like status changes and cross-references and awards
unless @note.system?
EventCreateService.new.leave_note(@note, @note.author)
return if @note.for_personal_snippet?
@note.create_cross_references!
execute_note_hooks
end
......
......@@ -12,7 +12,7 @@ module Notes
def self.supported?(note, current_user)
noteable_update_service(note) &&
current_user &&
current_user.can?(:"update_#{note.noteable_type.underscore}", note.noteable)
current_user.can?(:"update_#{note.to_ability_name}", note.noteable)
end
def supported?(note)
......
......@@ -178,8 +178,15 @@ class NotificationService
recipients = []
mentioned_users = note.mentioned_users
ability, subject = if note.for_personal_snippet?
[:read_personal_snippet, note.noteable]
else
[:read_project, note.project]
end
mentioned_users.select! do |user|
user.can?(:read_project, note.project)
user.can?(ability, subject)
end
# Add all users participating in the thread (author, assignee, comment authors)
......@@ -192,11 +199,13 @@ class NotificationService
recipients = recipients.concat(participants)
unless note.for_personal_snippet?
# Merge project watchers
recipients = add_project_watchers(recipients, note.project)
# Merge project with custom notification
recipients = add_custom_notifications(recipients, note.project, :new_note)
end
# Reject users with Mention notification level, except those mentioned in _this_ note.
recipients = reject_mention_users(recipients - mentioned_users, note.project)
......@@ -211,8 +220,7 @@ class NotificationService
recipients.delete(note.author)
recipients = recipients.uniq
# build notify method like 'note_commit_email'
notify_method = "note_#{note.noteable_type.underscore}_email".to_sym
notify_method = "note_#{note.to_ability_name}_email".to_sym
recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later
......
......@@ -22,17 +22,7 @@ module Projects
return @project
end
# Set project name from path
if @project.name.present? && @project.path.present?
# if both name and path set - everything is ok
elsif @project.path.present?
# Set project name from path
@project.name = @project.path.dup
elsif @project.name.present?
# For compatibility - set path from name
# TODO: remove this in 8.0
@project.path = @project.name.dup.parameterize
end
set_project_name_from_path
# get namespace id
namespace_id = params[:namespace_id]
......@@ -144,5 +134,19 @@ module Projects
service.save!
end
end
def set_project_name_from_path
# Set project name from path
if @project.name.present? && @project.path.present?
# if both name and path set - everything is ok
elsif @project.path.present?
# Set project name from path
@project.name = @project.path.dup
elsif @project.name.present?
# For compatibility - set path from name
# TODO: remove this in 8.0
@project.path = @project.name.dup.parameterize
end
end
end
end
......@@ -22,6 +22,8 @@ module Projects
if project.update_attributes(params.except(:default_branch))
if project.previous_changes.include?('path')
project.rename_repo
else
system_hook_service.execute_hooks_for(project, :update)
end
success
......
......@@ -4,6 +4,6 @@ class UserProjectAccessChangedService
end
def execute
AuthorizedProjectsWorker.bulk_perform_async(@user_ids.map { |id| [id] })
AuthorizedProjectsWorker.bulk_perform_and_wait(@user_ids.map { |id| [id] })
end
end
......@@ -118,7 +118,8 @@ module Users
user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
user.groups_projects.select_for_project_authorization,
user.projects.select_for_project_authorization,
user.groups.joins(:shared_projects).select_for_project_authorization
user.groups.joins(:shared_projects).select_for_project_authorization,
user.nested_projects.select_for_project_authorization
]
Gitlab::SQL::Union.new(relations)
......
......@@ -4,7 +4,7 @@
- if @broadcast_message.message.present?
= render_broadcast_message(@broadcast_message)
- else
= "Your message here"
Your message here
= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
= form_errors(@broadcast_message)
......
%tr
%td
= "#{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})"
#{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})
%td
= identity.extern_uid
%td
......
......@@ -11,7 +11,7 @@
that for future communication.
%br
Registration token is
%code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
%code#runners-token= current_application_settings.runners_registration_token
.bs-callout.clearfix
.pull-left
......
......@@ -100,7 +100,7 @@
%td.build-link
- if project
= link_to ci_status_path(build.pipeline) do
%strong #{build.pipeline.short_sha}
%strong= build.pipeline.short_sha
%td.timestamp
- if build.finished_at
......
......@@ -10,7 +10,7 @@
%h4 CPU
.data
- if @cpus
%h1= "#{@cpus.length} cores"
%h1 #{@cpus.length} cores
- else
= icon('warning', class: 'text-warning')
Unable to collect CPU info
......@@ -19,7 +19,7 @@
%h4 Memory
.data
- if @memory
%h1= "#{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}"
%h1 #{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}
- else
= icon('warning', class: 'text-warning')
Unable to collect memory info
......@@ -28,6 +28,6 @@
%h4 Disks
.data
- @disks.each do |disk|
%h1= "#{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}"
%p= "#{disk[:disk_name]}"
%p= "#{disk[:mount_path]}"
%h1 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}
%p= disk[:disk_name]
%p= disk[:mount_path]
......@@ -186,6 +186,6 @@
- if @user.solo_owned_groups.present?
%p
This user is currently an owner in these groups:
%strong #{@user.solo_owned_groups.map(&:name).join(', ')}
%strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
You must transfer ownership or delete these groups before you can delete this user.
......@@ -13,7 +13,7 @@
.file-holder
.file-title.clearfix
Content of .gitlab-ci.yml
#ci-editor.ci-editor #{@content}
#ci-editor.ci-editor= @content
= text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
.col-sm-12
.pull-left.prepend-top-10
......
- expanded = discussion.expanded?
%li.note.note-discussion.timeline-entry
.timeline-entry-inner
.timeline-icon
= link_to user_path(discussion.author) do
= image_tag avatar_icon(discussion.author), class: "avatar s40"
.timeline-content
.discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } }
.discussion-header
......
......@@ -6,7 +6,7 @@
.content{ class: ('hide' unless discussion_left.expanded?) }
= render "discussions/notes", discussion: discussion_left, line_type: 'old'
- else
%td.notes_line.old= ""
%td.notes_line.old= ("")
%td.notes_content.parallel.old
.content
......@@ -16,6 +16,6 @@
.content{ class: ('hide' unless discussion_right.expanded?) }
= render "discussions/notes", discussion: discussion_right, line_type: 'new'
- else
%td.notes_line.new= ""
%td.notes_line.new= ("")
%td.notes_content.parallel.new
.content
......@@ -10,7 +10,7 @@
%p
= icon("exclamation-triangle fw")
You are an admin, which means granting access to
%strong #{@pre_auth.client.name}
%strong= @pre_auth.client.name
will allow them to interact with GitLab as an admin as well. Proceed with caution.
- if @pre_auth.scopes
......
......@@ -25,7 +25,7 @@
.panel.panel-default
.panel-heading
Users with access to
%strong #{@group.name}
%strong= @group.name
%span.badge= @members.total_count
%ul.content-list
= render partial: 'shared/members/member', collection: @members, as: :member
......
......@@ -18,7 +18,7 @@
.row-content-block.second-block
Only issues from the
%strong #{@group.name}
%strong= @group.name
group are listed here.
- if current_user
To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
......
......@@ -10,7 +10,7 @@
.row-content-block.second-block
Only merge requests from
%strong #{@group.name}
%strong= @group.name
group are listed here.
- if current_user
To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
......
......@@ -10,7 +10,7 @@
.row-content-block
Only milestones from
%strong #{@group.name}
%strong= @group.name
group are listed here.
.milestones
......
......@@ -16,7 +16,7 @@
%colgroup.import-jobs-status-col
%thead
%tr
%th= "From #{provider_title}"
%th From #{provider_title}
%th To GitLab
%th Status
%tbody
......
......@@ -37,7 +37,7 @@
%tbody
- @user_map.each do |id, user|
%tr
%td= id
%td= (id)
%td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control'
%td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control'
%td
......
......@@ -50,7 +50,7 @@
%td
= repo.name
%td.import-target
= "#{current_user.username}/#{repo.name}"
#{current_user.username}/#{repo.name}
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
......
......@@ -55,7 +55,7 @@
%td
= link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
%td.import-target
= "#{current_user.username}/#{repo.name}"
#{current_user.username}/#{repo.name}
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
......
......@@ -2,9 +2,9 @@
Assignee changed
- if @previous_assignee
from
%strong #{@previous_assignee.name}
%strong= @previous_assignee.name
to
- if issuable.assignee_id
%strong #{issuable.assignee_name}
%strong= issuable.assignee_name
- else
%strong Unassigned
%p
= "Issue was closed by #{@updated_by.name}"
Issue was closed by #{@updated_by.name}
= "Issue was closed by #{@updated_by.name}"
Issue was closed by #{@updated_by.name}
Issue ##{@issue.iid}: #{namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)}
%p
= "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}"
Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}
= "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}"
Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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