Commit 513ff0ce authored by Valery Sizov's avatar Valery Sizov

Merge branch 'ce_upstream' into 'master'

CE upstream

```
Unmerged paths:
  (use "git add <file>..." to mark resolution)

	both modified:   app/views/layouts/nav/_group_settings.html.haml
	both modified:   app/views/projects/edit.html.haml
	both modified:   app/views/shared/issuable/_form.html.haml
```

Related CE changes https://gitlab.com/gitlab-org/gitlab-ce/compare/26bd854aa19e186f529681612164cbf1d5b386fc...fd1741b47970fc52d994367ba38b5d1353d94725

See merge request !709
parents 1bb331dd 011292f1
See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html.
## What does this MR do?
(briefly describe what this MR is about)
## Moving docs to a new location?
See the guidelines: http://docs.gitlab.com/ce/development/doc_styleguide.html#changing-document-location
- [ ] Make sure the old link is not removed and has its contents replaced with a link to the new location.
- [ ] Make sure internal links pointing to the document in question are not broken.
- [ ] Search and replace any links referring to old docs in GitLab Rails app, specifically under the `app/views/` directory.
- [ ] If working on CE, submit an MR to EE with the changes as well.
...@@ -5,8 +5,8 @@ require: ...@@ -5,8 +5,8 @@ require:
inherit_from: .rubocop_todo.yml inherit_from: .rubocop_todo.yml
AllCops: AllCops:
TargetRubyVersion: 2.1 TargetRubyVersion: 2.3
# Cop names are not displayed in offense messages by default. Change behavior # Cop names are not d§splayed in offense messages by default. Change behavior
# by overriding DisplayCopNames, or by giving the -D/--display-cop-names # by overriding DisplayCopNames, or by giving the -D/--display-cop-names
# option. # option.
DisplayCopNames: true DisplayCopNames: true
...@@ -192,6 +192,9 @@ Style/FlipFlop: ...@@ -192,6 +192,9 @@ Style/FlipFlop:
Style/For: Style/For:
Enabled: true Enabled: true
# Checks if there is a magic comment to enforce string literals
Style/FrozenStringLiteralComment:
Enabled: false
# Do not introduce global variables. # Do not introduce global variables.
Style/GlobalVars: Style/GlobalVars:
Enabled: true Enabled: true
......
...@@ -6,31 +6,57 @@ v 8.11.1 ...@@ -6,31 +6,57 @@ v 8.11.1
- Fix file links on project page when default view is Files !5933 - Fix file links on project page when default view is Files !5933
- Fixed enter key in search input not working !5888 - Fixed enter key in search input not working !5888
v 8.12.0 (unreleased) v 8.12.0 (unreleased)
- Filter tags by name !6121
- Make push events have equal vertical spacing. - Make push events have equal vertical spacing.
- Add two-factor recovery endpoint to internal API !5510 - Add two-factor recovery endpoint to internal API !5510
- Remove vendor prefixes for linear-gradient CSS (ClemMakesApps)
- Add font color contrast to external label in admin area (ClemMakesApps) - Add font color contrast to external label in admin area (ClemMakesApps)
- Change logo animation to CSS (ClemMakesApps) - Change logo animation to CSS (ClemMakesApps)
- Instructions for enabling Git packfile bitmaps !6104
- Change merge_error column from string to text type - Change merge_error column from string to text type
- Reduce contributions calendar data payload (ClemMakesApps) - Reduce contributions calendar data payload (ClemMakesApps)
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel) - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel)
- Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling) - Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
- Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps)
- Center build stage columns in pipeline overview (ClemMakesApps)
- Shorten task status phrase (ClemMakesApps)
- Add hover color to emoji icon (ClemMakesApps) - Add hover color to emoji icon (ClemMakesApps)
- Fix branches page dropdown sort alignment (ClemMakesApps) - Fix branches page dropdown sort alignment (ClemMakesApps)
- Add white background for no readme container (ClemMakesApps)
- API: Expose issue confidentiality flag. (Robert Schilling)
- Optimistic locking for Issues and Merge Requests (title and description overriding prevention) - Optimistic locking for Issues and Merge Requests (title and description overriding prevention)
- Add `wiki_page_events` to project hook APIs (Ben Boeckel) - Add `wiki_page_events` to project hook APIs (Ben Boeckel)
- Remove Gitorious import - Remove Gitorious import
- Fix inconsistent background color for filter input field (ClemMakesApps)
- Remove prefixes from transition CSS property (ClemMakesApps)
- Add Sentry logging to API calls - Add Sentry logging to API calls
- Add BroadcastMessage API
- Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling) - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
- Remove unused mixins (ClemMakesApps) - Remove unused mixins (ClemMakesApps)
- Add search to all issue board lists
- Fix groups sort dropdown alignment (ClemMakesApps) - Fix groups sort dropdown alignment (ClemMakesApps)
- Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps) - Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps)
- Use JavaScript tooltips for mentions !5301 (winniehell)
- Fix markdown help references (ClemMakesApps) - Fix markdown help references (ClemMakesApps)
- Add last commit time to repo view (ClemMakesApps)
- Fix accessibility and visibility of project list dropdown button !6140
- Added project specific enable/disable setting for LFS !5997
- Don't expose a user's token in the `/api/v3/user` API (!6047)
- Remove redundant js-timeago-pending from user activity log (ClemMakesApps)
- Ability to manage project issues, snippets, wiki, merge requests and builds access level
- Remove inconsistent font weight for sidebar's labels (ClemMakesApps)
- Added tests for diff notes - Added tests for diff notes
- Add a button to download latest successful artifacts for branches and tags !5142
- Remove redundant pipeline tooltips (ClemMakesApps)
- Expire commit info views after one day, instead of two weeks, to allow for user email updates
- Add delimiter to project stars and forks count (ClemMakesApps) - Add delimiter to project stars and forks count (ClemMakesApps)
- Fix badge count alignment (ClemMakesApps) - Fix badge count alignment (ClemMakesApps)
- Fix repo title alignment (ClemMakesApps)
- Fix branch title trailing space on hover (ClemMakesApps) - Fix branch title trailing space on hover (ClemMakesApps)
- Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison) - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
- Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison) - Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
- Order award emoji tooltips in order they were added (EspadaV8)
- Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps) - Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps)
- Update merge_requests.md with a simpler way to check out a merge request. !5944 - Update merge_requests.md with a simpler way to check out a merge request. !5944
- Fix button missing type (ClemMakesApps) - Fix button missing type (ClemMakesApps)
...@@ -39,21 +65,48 @@ v 8.12.0 (unreleased) ...@@ -39,21 +65,48 @@ v 8.12.0 (unreleased)
- Load branches asynchronously in Cherry Pick and Revert dialogs. - Load branches asynchronously in Cherry Pick and Revert dialogs.
- Add merge request versions !5467 - Add merge request versions !5467
- Change using size to use count and caching it for number of group members. !5935 - Change using size to use count and caching it for number of group members. !5935
- Replace play icon font with svg (ClemMakesApps)
- Added 'only_allow_merge_if_build_succeeds' project setting in the API. !5930 (Duck) - Added 'only_allow_merge_if_build_succeeds' project setting in the API. !5930 (Duck)
- Reduce number of database queries on builds tab - Reduce number of database queries on builds tab
- Capitalize mentioned issue timeline notes (ClemMakesApps) - Capitalize mentioned issue timeline notes (ClemMakesApps)
- Fix inconsistent checkbox alignment (ClemMakesApps)
- Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger) - Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger)
- Adds response mime type to transaction metric action when it's not HTML - Adds response mime type to transaction metric action when it's not HTML
- Fix hover leading space bug in pipeline graph !5980 - Fix hover leading space bug in pipeline graph !5980
- User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496
v 8.11.3 (unreleased) - Fixed invisible scroll controls on build page on iPhone
v 8.11.5 (unreleased)
- Optimize branch lookups and force a repository reload for Repository#find_branch
- Fix suggested colors options for new labels in the admin area. !6138
v 8.11.4
- Fix resolving conflicts on forks. !6082
- Fix diff commenting on merge requests created prior to 8.10. !6029
- Fix pipelines tab layout regression. !5952
- Fix "Wiki" link not appearing in navigation for projects with external wiki. !6057
- Do not enforce using hash with hidden key in CI configuration. !6079
- Fix hover leading space bug in pipeline graph !5980
- Fix sorting issues by "last updated" doesn't work after import from GitHub
- GitHub importer use default project visibility for non-private projects
- Creating an issue through our API now emails label subscribers !5720
- Block concurrent updates for Pipeline
- Don't create groups for unallowed users when importing projects
- Fix issue boards leak private label names and descriptions
- Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner)
- Remove gitorious. !5866
v 8.11.3
- Allow system info page to handle case where info is unavailable - Allow system info page to handle case where info is unavailable
- Label list shows all issues (opened or closed) with that label - Label list shows all issues (opened or closed) with that label
- Don't show resolve conflicts link before MR status is updated - Don't show resolve conflicts link before MR status is updated
- Fix "Wiki" link not appearing in navigation for projects with external wiki - Fix "Wiki" link not appearing in navigation for projects with external wiki
- Fix IE11 fork button bug !598 - Fix IE11 fork button bug !598
- Fix IE11 fork button bug !5982
- Don't prevent viewing the MR when git refs for conflicts can't be found on disk - Don't prevent viewing the MR when git refs for conflicts can't be found on disk
- Fix external issue tracker "Issues" link leading to 404s - Fix external issue tracker "Issues" link leading to 404s
- Don't try to show merge conflict resolution info if a merge conflict contains non-UTF-8 characters
- Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
v 8.11.2 v 8.11.2
- Show "Create Merge Request" widget for push events to fork projects on the source project. !5978 - Show "Create Merge Request" widget for push events to fork projects on the source project. !5978
......
...@@ -287,6 +287,8 @@ request is as follows: ...@@ -287,6 +287,8 @@ request is as follows:
migrations on a fresh database before the MR is reviewed. If the review leads migrations on a fresh database before the MR is reviewed. If the review leads
to large changes in the MR, do this again once the review is complete. to large changes in the MR, do this again once the review is complete.
1. For more complex migrations, write tests. 1. For more complex migrations, write tests.
1. Merge requests **must** adhere to the [merge request performance
guidelines](doc/development/merge_request_performance_guidelines.md).
The **official merge window** is in the beginning of the month from the 1st to 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 the 7th day of the month. This is the best time to submit an MR and get
......
...@@ -57,7 +57,7 @@ gem 'browser', '~> 2.2' ...@@ -57,7 +57,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository # Extracting information from a git repository
# Provide access to Gitlab::Git library # Provide access to Gitlab::Git library
gem 'gitlab_git', '~> 10.4.7' gem 'gitlab_git', '~> 10.5'
# LDAP Auth # LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes # GitLab fork with several improvements to original library. For full list of changes
......
...@@ -301,7 +301,7 @@ GEM ...@@ -301,7 +301,7 @@ GEM
mime-types (>= 1.16, < 3) mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab-license (1.0.0) gitlab-license (1.0.0)
gitlab_git (10.4.7) gitlab_git (10.5.0)
activesupport (~> 4.0) activesupport (~> 4.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
...@@ -889,7 +889,7 @@ DEPENDENCIES ...@@ -889,7 +889,7 @@ DEPENDENCIES
gitlab-elasticsearch-git (~> 0.0.17) gitlab-elasticsearch-git (~> 0.0.17)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
gitlab_git (~> 10.4.7) gitlab_git (~> 10.5)
gitlab_meta (= 7.0) gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1) gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
......
...@@ -50,7 +50,7 @@ etc.). ...@@ -50,7 +50,7 @@ etc.).
The most important thing is making sure valid issues receive feedback from the The most important thing is making sure valid issues receive feedback from the
development team. Therefore the priority is mentioning developers that can help development team. Therefore the priority is mentioning developers that can help
on those issue. Please select someone with relevant experience from on those issues. Please select someone with relevant experience from
[GitLab core team][core-team]. If there is nobody mentioned with that expertise [GitLab core team][core-team]. If there is nobody mentioned with that expertise
look in the commit history for the affected files to find someone. Avoid look in the commit history for the affected files to find someone. Avoid
mentioning the lead developer, this is the person that is least likely to give a mentioning the lead developer, this is the person that is least likely to give a
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
} }
Activities.prototype.updateTooltips = function() { Activities.prototype.updateTooltips = function() {
return gl.utils.localTimeAgo($('.js-timeago', '#activity')); return gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
}; };
Activities.prototype.reloadActivities = function() { Activities.prototype.reloadActivities = function() {
......
(function(w) { (function(w) {
$(function() { $(function() {
$('.js-toggle-button').on('click', function(e) { $('body').on('click', '.js-toggle-button', function(e) {
e.preventDefault(); e.preventDefault();
$(this) $(this)
.find('.fa') .find('.fa')
......
...@@ -54,4 +54,11 @@ $(() => { ...@@ -54,4 +54,11 @@ $(() => {
}); });
} }
}); });
gl.IssueBoardsSearch = new Vue({
el: '#js-boards-seach',
data: {
filters: Store.state.filters
}
});
}); });
...@@ -21,15 +21,10 @@ ...@@ -21,15 +21,10 @@
}, },
data () { data () {
return { return {
query: '',
filters: Store.state.filters filters: Store.state.filters
}; };
}, },
watch: { watch: {
query () {
this.list.filters = this.getFilterData();
this.list.getIssues(true);
},
filters: { filters: {
handler () { handler () {
this.list.page = 1; this.list.page = 1;
...@@ -38,16 +33,6 @@ ...@@ -38,16 +33,6 @@
deep: true deep: true
} }
}, },
methods: {
getFilterData () {
const filters = this.filters;
let queryData = { search: this.query };
Object.keys(filters).forEach((key) => { queryData[key] = filters[key]; });
return queryData;
}
},
ready () { ready () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({ const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled, disabled: this.disabled,
......
...@@ -20,7 +20,8 @@ ...@@ -20,7 +20,8 @@
data () { data () {
return { return {
scrollOffset: 250, scrollOffset: 250,
filters: Store.state.filters filters: Store.state.filters,
showCount: false
}; };
}, },
watch: { watch: {
...@@ -30,6 +31,15 @@ ...@@ -30,6 +31,15 @@
this.$els.list.scrollTop = 0; this.$els.list.scrollTop = 0;
}, },
deep: true deep: true
},
issues () {
this.$nextTick(() => {
if (this.scrollHeight() > this.listHeight()) {
this.showCount = true;
} else {
this.showCount = false;
}
});
} }
}, },
methods: { methods: {
...@@ -58,6 +68,7 @@ ...@@ -58,6 +68,7 @@
group: 'issues', group: 'issues',
sort: false, sort: false,
disabled: this.disabled, disabled: this.disabled,
filter: '.board-list-count',
onStart: (e) => { onStart: (e) => {
const card = this.$refs.issue[e.oldIndex]; const card = this.$refs.issue[e.oldIndex];
......
...@@ -11,6 +11,7 @@ class List { ...@@ -11,6 +11,7 @@ class List {
this.loading = true; this.loading = true;
this.loadingMore = false; this.loadingMore = false;
this.issues = []; this.issues = [];
this.issuesSize = 0;
if (obj.label) { if (obj.label) {
this.label = new ListLabel(obj.label); this.label = new ListLabel(obj.label);
...@@ -51,17 +52,13 @@ class List { ...@@ -51,17 +52,13 @@ class List {
} }
nextPage () { nextPage () {
if (Math.floor(this.issues.length / 20) === this.page) { if (this.issuesSize > this.issues.length) {
this.page++; this.page++;
return this.getIssues(false); return this.getIssues(false);
} }
} }
canSearch () {
return this.type === 'backlog';
}
getIssues (emptyIssues = true) { getIssues (emptyIssues = true) {
const filters = this.filters; const filters = this.filters;
let data = { page: this.page }; let data = { page: this.page };
...@@ -80,12 +77,13 @@ class List { ...@@ -80,12 +77,13 @@ class List {
.then((resp) => { .then((resp) => {
const data = resp.json(); const data = resp.json();
this.loading = false; this.loading = false;
this.issuesSize = data.size;
if (emptyIssues) { if (emptyIssues) {
this.issues = []; this.issues = [];
} }
this.createIssues(data); this.createIssues(data.issues);
}); });
} }
...@@ -96,14 +94,20 @@ class List { ...@@ -96,14 +94,20 @@ class List {
} }
addIssue (issue, listFrom) { addIssue (issue, listFrom) {
this.issues.push(issue); if (!this.findIssue(issue.id)) {
this.issues.push(issue);
if (this.label) { if (this.label) {
issue.addLabel(this.label); issue.addLabel(this.label);
} }
if (listFrom) { if (listFrom) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id); this.issuesSize++;
gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
.then(() => {
listFrom.getIssues(false);
});
}
} }
} }
...@@ -116,6 +120,7 @@ class List { ...@@ -116,6 +120,7 @@ class List {
const matchesRemove = removeIssue.id === issue.id; const matchesRemove = removeIssue.id === issue.id;
if (matchesRemove) { if (matchesRemove) {
this.issuesSize--;
issue.removeLabel(this.label); issue.removeLabel(this.label);
} }
......
...@@ -15,7 +15,8 @@ ...@@ -15,7 +15,8 @@
author_id: gl.utils.getParameterValues('author_id')[0], author_id: gl.utils.getParameterValues('author_id')[0],
assignee_id: gl.utils.getParameterValues('assignee_id')[0], assignee_id: gl.utils.getParameterValues('assignee_id')[0],
milestone_title: gl.utils.getParameterValues('milestone_title')[0], milestone_title: gl.utils.getParameterValues('milestone_title')[0],
label_name: gl.utils.getParameterValues('label_name[]') label_name: gl.utils.getParameterValues('label_name[]'),
search: ''
}; };
}, },
addList (listObj) { addList (listObj) {
......
...@@ -206,6 +206,7 @@ ...@@ -206,6 +206,7 @@
break; break;
case 'labels': case 'labels':
switch (path[2]) { switch (path[2]) {
case 'new':
case 'edit': case 'edit':
new Labels(); new Labels();
} }
......
...@@ -556,7 +556,7 @@ ...@@ -556,7 +556,7 @@
if (isInput) { if (isInput) {
field = $(this.el); field = $(this.el);
} else { } else {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + escape(value) + "']");
} }
if (el.hasClass(ACTIVE_CLASS)) { if (el.hasClass(ACTIVE_CLASS)) {
el.removeClass(ACTIVE_CLASS); el.removeClass(ACTIVE_CLASS);
......
...@@ -164,7 +164,7 @@ ...@@ -164,7 +164,7 @@
instance.addInput(this.fieldName, label.id); instance.addInput(this.fieldName, label.id);
} }
} }
if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + (this.id(label)) + "']").length) { if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + escape(this.id(label)) + "']").length) {
selectedClass.push('is-active'); selectedClass.push('is-active');
} }
if ($dropdown.hasClass('js-multiselect') && removesAll) { if ($dropdown.hasClass('js-multiselect') && removesAll) {
......
(function() { (function() {
Turbolinks.enableProgressBar(); Turbolinks.enableProgressBar();
start = function() { $(document).on('page:fetch', function() {
$('.tanuki-logo').addClass('animate'); $('.tanuki-logo').addClass('animate');
}; });
stop = function() { $(document).on('page:change', function() {
$('.tanuki-logo').removeClass('animate'); $('.tanuki-logo').removeClass('animate');
}; });
$(document).on('page:fetch', start);
$(document).on('page:change', stop);
}).call(this); }).call(this);
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
this.perPage = this.el.data('perPage'); this.perPage = this.el.data('perPage');
this.clearListeners(); this.clearListeners();
this.initBtnListeners(); this.initBtnListeners();
this.initFilters();
} }
Todos.prototype.clearListeners = function() { Todos.prototype.clearListeners = function() {
...@@ -27,6 +28,31 @@ ...@@ -27,6 +28,31 @@
return $('.todo').on('click', this.goToTodoUrl); return $('.todo').on('click', this.goToTodoUrl);
}; };
Todos.prototype.initFilters = function() {
new UsersSelect();
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
this.initFilterDropdown($('.js-type-search'), 'type');
this.initFilterDropdown($('.js-action-search'), 'action_id');
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
});
};
Todos.prototype.initFilterDropdown = function($dropdown, fieldName, searchFields) {
$dropdown.glDropdown({
selectable: true,
filterable: searchFields ? true : false,
fieldName: fieldName,
search: { fields: searchFields },
data: $dropdown.data('data'),
clicked: function() {
return $dropdown.closest('form.filter-form').submit();
}
})
};
Todos.prototype.doneClicked = function(e) { Todos.prototype.doneClicked = function(e) {
var $this; var $this;
e.preventDefault(); e.preventDefault();
...@@ -66,7 +92,7 @@ ...@@ -66,7 +92,7 @@
success: (function(_this) { success: (function(_this) {
return function(data) { return function(data) {
$this.remove(); $this.remove();
$('.js-todos-list').remove(); $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
return _this.updateBadges(data); return _this.updateBadges(data);
}; };
})(this) })(this)
......
(function() { (global => {
this.User = (function() { global.User = class {
function User(opts) { constructor(opts) {
this.opts = opts; this.opts = opts;
$('.profile-groups-avatars').tooltip({ this.placeProfileAvatarsToTop();
"placement": "top"
});
this.initTabs(); this.initTabs();
$('.hide-project-limit-message').on('click', function(e) { this.hideProjectLimitMessage();
$.cookie('hide_project_limit_message', 'false', { }
path: gon.relative_url_root || '/'
}); placeProfileAvatarsToTop() {
$(this).parents('.project-limit-message').remove(); $('.profile-groups-avatars').tooltip({
return e.preventDefault(); placement: 'top'
}); });
} }
User.prototype.initTabs = function() { initTabs() {
return new UserTabs({ return new UserTabs({
parentEl: '.user-profile', parentEl: '.user-profile',
action: this.opts.action action: this.opts.action
}); });
}; }
return User;
})();
}).call(this); hideProjectLimitMessage() {
$('.hide-project-limit-message').on('click', e => {
e.preventDefault();
const path = gon.relative_url_root || '/';
$.cookie('hide_project_limit_message', 'false', {
path: path
});
$(this).parents('.project-limit-message').remove();
});
}
}
})(window.gl || (window.gl = {}));
...@@ -183,6 +183,13 @@ ...@@ -183,6 +183,13 @@
&.dropdown-menu-user-link { &.dropdown-menu-user-link {
line-height: 16px; line-height: 16px;
} }
.icon-play {
fill: $table-text-gray;
margin-right: 6px;
height: 12px;
width: 11px;
}
} }
.dropdown-header { .dropdown-header {
...@@ -195,6 +202,12 @@ ...@@ -195,6 +202,12 @@
.separator + .dropdown-header { .separator + .dropdown-header {
padding-top: 2px; padding-top: 2px;
} }
.unclickable {
cursor: not-allowed;
padding: 5px 8px;
color: $dropdown-header-color;
}
} }
.dropdown-menu-large { .dropdown-menu-large {
......
...@@ -84,7 +84,7 @@ header { ...@@ -84,7 +84,7 @@ header {
.side-nav-toggle { .side-nav-toggle {
position: absolute; position: absolute;
left: -10px; left: -10px;
margin: 6px 0; margin: 7px 0;
font-size: 18px; font-size: 18px;
padding: 6px 10px; padding: 6px 10px;
border: none; border: none;
...@@ -136,6 +136,8 @@ header { ...@@ -136,6 +136,8 @@ header {
} }
.title { .title {
position: relative;
padding-right: 20px;
margin: 0; margin: 0;
font-size: 19px; font-size: 19px;
max-width: 400px; max-width: 400px;
...@@ -148,7 +150,11 @@ header { ...@@ -148,7 +150,11 @@ header {
vertical-align: top; vertical-align: top;
white-space: nowrap; white-space: nowrap;
@media (max-width: $screen-sm-max) { @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
max-width: 300px;
}
@media (max-width: $screen-xs-max) {
max-width: 190px; max-width: 190px;
} }
...@@ -160,11 +166,15 @@ header { ...@@ -160,11 +166,15 @@ header {
} }
.dropdown-toggle-caret { .dropdown-toggle-caret {
position: relative; color: $gl-text-color;
top: -2px; border: transparent;
background: transparent;
position: absolute;
right: 3px;
width: 12px; width: 12px;
line-height: 12px; line-height: 19px;
margin-left: 5px; margin-top: (($header-height - 19) / 2);
padding: 0;
font-size: 10px; font-size: 10px;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
......
...@@ -9,14 +9,6 @@ ...@@ -9,14 +9,6 @@
border-radius: $radius; border-radius: $radius;
} }
@mixin transition($transition) {
-webkit-transition: $transition;
-moz-transition: $transition;
-ms-transition: $transition;
-o-transition: $transition;
transition: $transition;
}
/** /**
* Prefilled mixins * Prefilled mixins
* Mixins with fixed values * Mixins with fixed values
......
...@@ -8,10 +8,7 @@ ...@@ -8,10 +8,7 @@
height: 30px; height: 30px;
transition-duration: .3s; transition-duration: .3s;
-webkit-transform: translateZ(0); -webkit-transform: translateZ(0);
background: -webkit-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%); background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4));
background: -o-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%);
background: -moz-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%);
background: linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%);
&.scrolling { &.scrolling {
visibility: visible; visibility: visible;
...@@ -211,12 +208,6 @@ ...@@ -211,12 +208,6 @@
} }
} }
.project-filter-form {
input {
background-color: $background-color;
}
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
padding-bottom: 0; padding-bottom: 0;
width: 100%; width: 100%;
......
...@@ -151,7 +151,7 @@ ...@@ -151,7 +151,7 @@
background-position: right 0 bottom 6px; background-position: right 0 bottom 6px;
border: 1px solid $input-border; border: 1px solid $input-border;
@include border-radius($border-radius-default); @include border-radius($border-radius-default);
@include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s); transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus { &:focus {
border-color: $input-border-focus; border-color: $input-border-focus;
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
.is-dragging { .is-dragging {
// Important because plugin sets inline CSS // Important because plugin sets inline CSS
opacity: 1!important; opacity: 1!important;
* { * {
// !important to make sure no style can override this when dragging // !important to make sure no style can override this when dragging
cursor: -webkit-grabbing!important; cursor: -webkit-grabbing!important;
...@@ -142,11 +142,6 @@ ...@@ -142,11 +142,6 @@
} }
} }
.board-header-loading-spinner {
margin-right: 10px;
color: $gray-darkest;
}
.board-inner-container { .board-inner-container {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
padding: $gl-padding; padding: $gl-padding;
...@@ -160,40 +155,6 @@ ...@@ -160,40 +155,6 @@
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
.board-search-container {
position: relative;
background-color: #fff;
.form-control {
padding-right: 30px;
}
}
.board-search-icon,
.board-search-clear-btn {
position: absolute;
right: $gl-padding + 10px;
top: 50%;
margin-top: -7px;
font-size: 14px;
}
.board-search-icon {
color: $gl-placeholder-color;
}
.board-search-clear-btn {
padding: 0;
line-height: 1;
background: transparent;
border: 0;
outline: 0;
&:hover {
color: $gl-link-color;
}
}
.board-delete { .board-delete {
margin-right: 10px; margin-right: 10px;
padding: 0; padding: 0;
...@@ -304,3 +265,22 @@ ...@@ -304,3 +265,22 @@
margin-right: 8px; margin-right: 8px;
font-weight: 500; font-weight: 500;
} }
.issue-boards-search {
width: 335px;
.form-control {
display: inline-block;
width: 210px;
}
}
.board-list-count {
padding: 10px 0;
color: $gl-placeholder-color;
font-size: 13px;
> .fa {
margin-right: 5px;
}
}
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
&.affix { &.affix {
right: 30px; right: 30px;
bottom: 15px; bottom: 15px;
z-index: 1;
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
right: 26%; right: 26%;
......
...@@ -4,8 +4,9 @@ ...@@ -4,8 +4,9 @@
margin: 0; margin: 0;
} }
.fa-play { .icon-play {
font-size: 14px; height: 13px;
width: 12px;
} }
.dropdown-new { .dropdown-new {
......
...@@ -12,6 +12,10 @@ ...@@ -12,6 +12,10 @@
padding-right: 8px; padding-right: 8px;
margin-bottom: 10px; margin-bottom: 10px;
min-width: 15px; min-width: 15px;
.selected_issue {
vertical-align: text-top;
}
} }
.issue-labels { .issue-labels {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
.stage { .stage {
max-width: 90px; max-width: 90px;
width: 90px; width: 90px;
text-align: center;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
...@@ -146,6 +147,7 @@ ...@@ -146,6 +147,7 @@
} }
.stage-cell { .stage-cell {
text-align: center;
svg { svg {
height: 18px; height: 18px;
...@@ -153,10 +155,6 @@ ...@@ -153,10 +155,6 @@
vertical-align: middle; vertical-align: middle;
overflow: visible; overflow: visible;
} }
.light {
width: 3px;
}
} }
.duration, .duration,
...@@ -215,6 +213,13 @@ ...@@ -215,6 +213,13 @@
border-color: $border-white-normal; border-color: $border-white-normal;
} }
} }
.btn {
.icon-play {
height: 13px;
width: 12px;
}
}
} }
} }
...@@ -469,11 +474,22 @@ ...@@ -469,11 +474,22 @@
.pipelines.tab-pane { .pipelines.tab-pane {
.content-list.pipelines { .content-list.pipelines {
overflow: scroll; overflow: auto;
} }
.stage { .stage {
max-width: 60px; max-width: 100px;
width: 60px; width: 100px;
}
.pipeline-actions {
min-width: initial;
}
}
.ci-status-icon-created {
svg {
fill: $table-text-gray;
} }
} }
...@@ -311,6 +311,14 @@ a.deploy-project-label { ...@@ -311,6 +311,14 @@ a.deploy-project-label {
color: $gl-success; color: $gl-success;
} }
.lfs-enabled {
color: $gl-success;
}
.lfs-disabled {
color: $gl-warning;
}
.breadcrumb.repo-breadcrumb { .breadcrumb.repo-breadcrumb {
padding: 0; padding: 0;
background: transparent; background: transparent;
...@@ -600,18 +608,25 @@ pre.light-well { ...@@ -600,18 +608,25 @@ pre.light-well {
} }
} }
.project-show-readme .readme-holder { .project-show-readme {
padding: $gl-padding 0; .row-content-block {
border-top: 0; background-color: inherit;
border: none;
.edit-project-readme {
z-index: 2;
position: relative;
} }
.wiki h1 { .readme-holder {
border-bottom: none; padding: $gl-padding 0;
padding: 0; border-top: 0;
.edit-project-readme {
z-index: 2;
position: relative;
}
.wiki h1 {
border-bottom: none;
padding: 0;
}
} }
} }
......
...@@ -80,7 +80,7 @@ ...@@ -80,7 +80,7 @@
.search-icon { .search-icon {
@extend .fa-search; @extend .fa-search;
@include transition(color .15s); transition: color 0.15s;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
...@@ -125,7 +125,7 @@ ...@@ -125,7 +125,7 @@
} }
.location-badge { .location-badge {
@include transition(all .15s); transition: all 0.15s;
background-color: $location-badge-active-bg; background-color: $location-badge-active-bg;
color: $white-light; color: $white-light;
} }
......
...@@ -43,6 +43,15 @@ ...@@ -43,6 +43,15 @@
border-color: $blue-normal; border-color: $blue-normal;
} }
&.ci-created {
color: $table-text-gray;
border-color: $table-text-gray;
svg {
fill: $table-text-gray;
}
}
svg { svg {
height: 13px; height: 13px;
width: 13px; width: 13px;
......
...@@ -11,6 +11,16 @@ ...@@ -11,6 +11,16 @@
} }
} }
.last-commit {
max-width: 506px;
.last-commit-content {
@include str-truncated;
width: calc(100% - 140px);
margin-left: 3px;
}
}
.tree-table { .tree-table {
margin-bottom: 0; margin-bottom: 0;
......
...@@ -8,10 +8,14 @@ module ToggleAwardEmoji ...@@ -8,10 +8,14 @@ module ToggleAwardEmoji
def toggle_award_emoji def toggle_award_emoji
name = params.require(:name) name = params.require(:name)
awardable.toggle_award_emoji(name, current_user) if awardable.user_can_award?(current_user, name)
TodoService.new.new_award_emoji(to_todoable(awardable), current_user) awardable.toggle_award_emoji(name, current_user)
TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
render json: { ok: true } render json: { ok: true }
else
render json: { ok: false }
end
end end
private private
......
...@@ -37,7 +37,7 @@ class JwtController < ApplicationController ...@@ -37,7 +37,7 @@ class JwtController < ApplicationController
def authenticate_project(login, password) def authenticate_project(login, password)
if login == 'gitlab-ci-token' if login == 'gitlab-ci-token'
Project.find_by(builds_enabled: true, runners_token: password) Project.with_builds_enabled.find_by(runners_token: password)
end end
end end
......
...@@ -88,6 +88,6 @@ class Projects::ApplicationController < ApplicationController ...@@ -88,6 +88,6 @@ class Projects::ApplicationController < ApplicationController
end end
def builds_enabled def builds_enabled
return render_404 unless @project.builds_enabled? return render_404 unless @project.feature_available?(:builds, current_user)
end end
end end
class Projects::ArtifactsController < Projects::ApplicationController class Projects::ArtifactsController < Projects::ApplicationController
include ExtractsPath
layout 'project' layout 'project'
before_action :authorize_read_build! before_action :authorize_read_build!
before_action :authorize_update_build!, only: [:keep] before_action :authorize_update_build!, only: [:keep]
before_action :extract_ref_name_and_path
before_action :validate_artifacts! before_action :validate_artifacts!
def download def download
unless artifacts_file.file_storage? if artifacts_file.file_storage?
return redirect_to artifacts_file.url send_file artifacts_file.path, disposition: 'attachment'
else
redirect_to artifacts_file.url
end end
send_file artifacts_file.path, disposition: 'attachment'
end end
def browse def browse
directory = params[:path] ? "#{params[:path]}/" : '' directory = params[:path] ? "#{params[:path]}/" : ''
@entry = build.artifacts_metadata_entry(directory) @entry = build.artifacts_metadata_entry(directory)
return render_404 unless @entry.exists? render_404 unless @entry.exists?
end end
def file def file
...@@ -34,14 +37,41 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -34,14 +37,41 @@ class Projects::ArtifactsController < Projects::ApplicationController
redirect_to namespace_project_build_path(project.namespace, project, build) redirect_to namespace_project_build_path(project.namespace, project, build)
end end
def latest_succeeded
target_path = artifacts_action_path(@path, project, build)
if target_path
redirect_to(target_path)
else
render_404
end
end
private private
def extract_ref_name_and_path
return unless params[:ref_name_and_path]
@ref_name, @path = extract_ref(params[:ref_name_and_path])
end
def validate_artifacts! def validate_artifacts!
render_404 unless build.artifacts? render_404 unless build && build.artifacts?
end end
def build def build
@build ||= project.builds.find_by!(id: params[:build_id]) @build ||= build_from_id || build_from_ref
end
def build_from_id
project.builds.find_by(id: params[:build_id]) if params[:build_id]
end
def build_from_ref
return unless @ref_name
builds = project.latest_successful_builds_for(@ref_name)
builds.find_by(name: params[:job])
end end
def artifacts_file def artifacts_file
......
...@@ -8,12 +8,15 @@ module Projects ...@@ -8,12 +8,15 @@ module Projects
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
issues = issues.page(params[:page]) issues = issues.page(params[:page])
render json: issues.as_json( render json: {
only: [:iid, :title, :confidential], issues: issues.as_json(
include: { only: [:iid, :title, :confidential],
assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, include: {
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
}) labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
}),
size: issues.total_count
}
end end
def update def update
......
...@@ -38,6 +38,6 @@ class Projects::DiscussionsController < Projects::ApplicationController ...@@ -38,6 +38,6 @@ class Projects::DiscussionsController < Projects::ApplicationController
end end
def module_enabled def module_enabled
render_404 unless @project.merge_requests_enabled render_404 unless @project.feature_available?(:merge_requests, current_user)
end end
end end
...@@ -207,7 +207,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -207,7 +207,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def module_enabled def module_enabled
return render_404 unless @project.issues_enabled && @project.default_issues_tracker? return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker?
end end
def redirect_to_external_issue_tracker def redirect_to_external_issue_tracker
......
...@@ -99,7 +99,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -99,7 +99,7 @@ class Projects::LabelsController < Projects::ApplicationController
protected protected
def module_enabled def module_enabled
unless @project.issues_enabled || @project.merge_requests_enabled unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user)
return render_404 return render_404
end end
end end
......
...@@ -453,7 +453,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -453,7 +453,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def module_enabled def module_enabled
return render_404 unless @project.merge_requests_enabled return render_404 unless @project.feature_available?(:merge_requests, current_user)
end end
def validates_merge_request def validates_merge_request
......
...@@ -106,7 +106,7 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -106,7 +106,7 @@ class Projects::MilestonesController < Projects::ApplicationController
end end
def module_enabled def module_enabled
unless @project.issues_enabled || @project.merge_requests_enabled unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user)
return render_404 return render_404
end end
end end
......
...@@ -94,7 +94,7 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -94,7 +94,7 @@ class Projects::SnippetsController < Projects::ApplicationController
end end
def module_enabled def module_enabled
return render_404 unless @project.snippets_enabled return render_404 unless @project.feature_available?(:snippets, current_user)
end end
def snippet_params def snippet_params
......
class Projects::TagsController < Projects::ApplicationController class Projects::TagsController < Projects::ApplicationController
include SortingHelper
# Authorize # Authorize
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :authorize_download_code! before_action :authorize_download_code!
...@@ -6,8 +8,10 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -6,8 +8,10 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy] before_action :authorize_admin_project!, only: [:destroy]
def index def index
@sort = params[:sort] || 'name' params[:sort] = params[:sort].presence || 'name'
@tags = @repository.tags_sorted_by(@sort)
@sort = params[:sort]
@tags = TagsFinder.new(@repository, params).execute
@tags = Kaminari.paginate_array(@tags).page(params[:page]) @tags = Kaminari.paginate_array(@tags).page(params[:page])
@releases = project.releases.where(tag: @tags.map(&:name)) @releases = project.releases.where(tag: @tags.map(&:name))
......
...@@ -304,13 +304,23 @@ class ProjectsController < Projects::ApplicationController ...@@ -304,13 +304,23 @@ class ProjectsController < Projects::ApplicationController
end end
def project_params def project_params
project_feature_attributes =
{
project_feature_attributes:
[
:issues_access_level, :builds_access_level,
:wiki_access_level, :merge_requests_access_level, :snippets_access_level
]
}
params.require(:project).permit( params.require(:project).permit(
:name, :path, :description, :issues_tracker, :tag_list, :runners_token, :name, :path, :description, :issues_tracker, :tag_list, :runners_token,
:issues_enabled, :merge_requests_enabled, :snippets_enabled, :container_registry_enabled, :container_registry_enabled,
:issues_tracker_id, :default_branch, :issues_tracker_id, :default_branch,
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
:builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled, :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled,
:lfs_enabled, project_feature_attributes,
# EE-only # EE-only
:approvals_before_merge, :approvals_before_merge,
......
class TagsFinder
def initialize(repository, params)
@repository = repository
@params = params
end
def execute
tags = @repository.tags_sorted_by(sort)
filter_by_name(tags)
end
private
def sort
@params[:sort].presence
end
def search
@params[:search].presence
end
def filter_by_name(tags)
if search
tags.select { |tag| tag.name.include?(search) }
else
tags
end
end
end
...@@ -110,7 +110,7 @@ module ApplicationHelper ...@@ -110,7 +110,7 @@ module ApplicationHelper
project = event.project project = event.project
# Skip if project repo is empty or MR disabled # Skip if project repo is empty or MR disabled
return false unless project && !project.empty_repo? && project.merge_requests_enabled return false unless project && !project.empty_repo? && project.feature_available?(:merge_requests, current_user)
# Skip if user already created appropriate MR # Skip if user already created appropriate MR
return false if project.merge_requests.where(source_branch: event.branch_name).opened.any? return false if project.merge_requests.where(source_branch: event.branch_name).opened.any?
......
...@@ -25,6 +25,11 @@ module CiStatusHelper ...@@ -25,6 +25,11 @@ module CiStatusHelper
end end
end end
def ci_status_for_statuseable(subject)
status = subject.try(:status) || 'not found'
status.humanize
end
def ci_icon_for_status(status) def ci_icon_for_status(status)
icon_name = icon_name =
case status case status
...@@ -41,7 +46,7 @@ module CiStatusHelper ...@@ -41,7 +46,7 @@ module CiStatusHelper
when 'play' when 'play'
'icon_play' 'icon_play'
when 'created' when 'created'
'icon_status_pending' 'icon_status_created'
else else
'icon_status_cancel' 'icon_status_cancel'
end end
......
...@@ -3,7 +3,7 @@ module CompareHelper ...@@ -3,7 +3,7 @@ module CompareHelper
from.present? && from.present? &&
to.present? && to.present? &&
from != to && from != to &&
project.merge_requests_enabled && project.feature_available?(:merge_requests, current_user) &&
project.repository.branch_names.include?(from) && project.repository.branch_names.include?(from) &&
project.repository.branch_names.include?(to) project.repository.branch_names.include?(to)
end end
......
...@@ -149,4 +149,20 @@ module GitlabRoutingHelper ...@@ -149,4 +149,20 @@ module GitlabRoutingHelper
def resend_invite_group_member_path(group_member, *args) def resend_invite_group_member_path(group_member, *args)
resend_invite_group_group_member_path(group_member.source, group_member) resend_invite_group_group_member_path(group_member.source, group_member)
end end
# Artifacts
def artifacts_action_path(path, project, build)
action, path_params = path.split('/', 2)
args = [project.namespace, project, build, path_params]
case action
when 'download'
download_namespace_project_build_artifacts_path(*args)
when 'browse'
browse_namespace_project_build_artifacts_path(*args)
when 'file'
file_namespace_project_build_artifacts_path(*args)
end
end
end end
...@@ -49,6 +49,19 @@ module IssuablesHelper ...@@ -49,6 +49,19 @@ module IssuablesHelper
end end
end end
def project_dropdown_label(project_id, default_label)
return default_label if project_id.nil?
return "Any project" if project_id == "0"
project = Project.find_by(id: project_id)
if project
project.name_with_namespace
else
default_label
end
end
def milestone_dropdown_label(milestone_title, default_label = "Milestone") def milestone_dropdown_label(milestone_title, default_label = "Milestone")
if milestone_title == Milestone::Upcoming.name if milestone_title == Milestone::Upcoming.name
milestone_title = Milestone::Upcoming.title milestone_title = Milestone::Upcoming.title
......
...@@ -23,10 +23,14 @@ module LfsHelper ...@@ -23,10 +23,14 @@ module LfsHelper
end end
def lfs_download_access? def lfs_download_access?
return false unless project.lfs_enabled?
project.public? || ci? || (user && user.can?(:download_code, project)) project.public? || ci? || (user && user.can?(:download_code, project))
end end
def lfs_upload_access? def lfs_upload_access?
return false unless project.lfs_enabled?
user && user.can?(:push_code, project) user && user.can?(:push_code, project)
end end
......
...@@ -135,6 +135,6 @@ module MergeRequestsHelper ...@@ -135,6 +135,6 @@ module MergeRequestsHelper
end end
def merge_request_button_visibility(merge_request, closed) def merge_request_button_visibility(merge_request, closed)
return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
end end
end end
...@@ -25,6 +25,8 @@ module NavHelper ...@@ -25,6 +25,8 @@ module NavHelper
current_path?('merge_requests#commits') || current_path?('merge_requests#commits') ||
current_path?('merge_requests#builds') || current_path?('merge_requests#builds') ||
current_path?('merge_requests#conflicts') || current_path?('merge_requests#conflicts') ||
current_path?('merge_requests#pipelines') ||
current_path?('issues#show') current_path?('issues#show')
if cookies[:collapsed_gutter] == 'true' if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed" "page-gutter right-sidebar-collapsed"
......
...@@ -61,7 +61,9 @@ module ProjectsHelper ...@@ -61,7 +61,9 @@ module ProjectsHelper
project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" }
if current_user if current_user
project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) project_link << button_tag(type: 'button', class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) do
icon("chevron-down")
end
end end
full_title = "#{namespace_link} / #{project_link}".html_safe full_title = "#{namespace_link} / #{project_link}".html_safe
...@@ -187,6 +189,18 @@ module ProjectsHelper ...@@ -187,6 +189,18 @@ module ProjectsHelper
nav_tabs.flatten nav_tabs.flatten
end end
def project_lfs_status(project)
if project.lfs_enabled?
content_tag(:span, class: 'lfs-enabled') do
'Enabled'
end
else
content_tag(:span, class: 'lfs-disabled') do
'Disabled'
end
end
end
def git_user_name def git_user_name
if current_user if current_user
current_user.name current_user.name
...@@ -417,4 +431,23 @@ module ProjectsHelper ...@@ -417,4 +431,23 @@ module ProjectsHelper
message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]") message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
end end
def project_feature_options
{
'Disabled' => ProjectFeature::DISABLED,
'Only team members' => ProjectFeature::PRIVATE,
'Everyone with access' => ProjectFeature::ENABLED
}
end
def project_feature_access_select(field)
# Don't show option "everyone with access" if project is private
options = project_feature_options
level = @project.project_feature.public_send(field)
options.delete('Everyone with access') if @project.private? && level != ProjectFeature::ENABLED
options = options_for_select(options, selected: @project.project_feature.public_send(field) || ProjectFeature::ENABLED)
content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control").html_safe
end
end end
module SentryHelper module SentryHelper
def sentry_enabled? def sentry_enabled?
Rails.env.production? && current_application_settings.sentry_enabled? Gitlab::Sentry.enabled?
end end
def sentry_context def sentry_context
return unless sentry_enabled? Gitlab::Sentry.context(current_user)
if current_user
Raven.user_context(
id: current_user.id,
email: current_user.email,
username: current_user.username,
)
end
Raven.tags_context(program: sentry_program_context)
end
def sentry_program_context
if Sidekiq.server?
'sidekiq'
else
'rails'
end
end end
end end
...@@ -3,6 +3,16 @@ module TagsHelper ...@@ -3,6 +3,16 @@ module TagsHelper
"/tags/#{tag}" "/tags/#{tag}"
end end
def filter_tags_path(options = {})
exist_opts = {
search: params[:search],
sort: params[:sort]
}
options = exist_opts.merge(options)
namespace_project_tags_path(@project.namespace, @project, @id, options)
end
def tag_list(project) def tag_list(project)
html = '' html = ''
project.tag_list.each do |tag| project.tag_list.each do |tag|
......
...@@ -78,13 +78,11 @@ module TodosHelper ...@@ -78,13 +78,11 @@ module TodosHelper
end end
def todo_actions_options def todo_actions_options
actions = [ [
OpenStruct.new(id: '', title: 'Any Action'), { id: '', text: 'Any Action' },
OpenStruct.new(id: Todo::ASSIGNED, title: 'Assigned'), { id: Todo::ASSIGNED, text: 'Assigned' },
OpenStruct.new(id: Todo::MENTIONED, title: 'Mentioned') { id: Todo::MENTIONED, text: 'Mentioned' }
] ]
options_from_collection_for_select(actions, 'id', 'title', params[:action_id])
end end
def todo_projects_options def todo_projects_options
...@@ -92,22 +90,28 @@ module TodosHelper ...@@ -92,22 +90,28 @@ module TodosHelper
projects = projects.includes(:namespace) projects = projects.includes(:namespace)
projects = projects.map do |project| projects = projects.map do |project|
OpenStruct.new(id: project.id, title: project.name_with_namespace) { id: project.id, text: project.name_with_namespace }
end end
projects.unshift(OpenStruct.new(id: '', title: 'Any Project')) projects.unshift({ id: '', text: 'Any Project' }).to_json
options_from_collection_for_select(projects, 'id', 'title', params[:project_id])
end end
def todo_types_options def todo_types_options
types = [ [
OpenStruct.new(title: 'Any Type', name: ''), { id: '', text: 'Any Type' },
OpenStruct.new(title: 'Issue', name: 'Issue'), { id: 'Issue', text: 'Issue' },
OpenStruct.new(title: 'Merge Request', name: 'MergeRequest') { id: 'MergeRequest', text: 'Merge Request' }
] ]
end
def todo_actions_dropdown_label(selected_action_id, default_action)
selected_action = todo_actions_options.find { |action| action[:id] == selected_action_id.to_i}
selected_action ? selected_action[:text] : default_action
end
options_from_collection_for_select(types, 'name', 'title', params[:type]) def todo_types_dropdown_label(selected_type, default_type)
selected_type = todo_types_options.find { |type| type[:id] == selected_type && type[:id] != '' }
selected_type ? selected_type[:text] : default_type
end end
private private
......
...@@ -65,8 +65,8 @@ module Ci ...@@ -65,8 +65,8 @@ module Ci
end end
# ref can't be HEAD or SHA, can only be branch/tag name # ref can't be HEAD or SHA, can only be branch/tag name
scope :latest_successful_for, ->(ref = default_branch) do def self.latest_successful_for(ref)
where(ref: ref).success.order(id: :desc).limit(1) where(ref: ref).order(id: :desc).success.first
end end
def self.truncate_sha(sha) def self.truncate_sha(sha)
......
...@@ -108,15 +108,6 @@ class Commit ...@@ -108,15 +108,6 @@ class Commit
@diff_line_count @diff_line_count
end end
# Returns a string describing the commit for use in a link title
#
# Example
#
# "Commit: Alex Denisov - Project git clone panel"
def link_title
"Commit: #{author_name} - #{title}"
end
# Returns the commits title. # Returns the commits title.
# #
# Usually, the commit title is the first line of the commit message. # Usually, the commit title is the first line of the commit message.
......
...@@ -4,12 +4,10 @@ ...@@ -4,12 +4,10 @@
# #
# range = CommitRange.new('f3f85602...e86e1013', project) # range = CommitRange.new('f3f85602...e86e1013', project)
# range.exclude_start? # => false # range.exclude_start? # => false
# range.reference_title # => "Commits f3f85602 through e86e1013"
# range.to_s # => "f3f85602...e86e1013" # range.to_s # => "f3f85602...e86e1013"
# #
# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project) # range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project)
# range.exclude_start? # => true # range.exclude_start? # => true
# range.reference_title # => "Commits f3f85602^ through e86e1013"
# range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"} # range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"}
# range.to_s # => "f3f85602..e86e1013" # range.to_s # => "f3f85602..e86e1013"
# #
...@@ -109,11 +107,6 @@ class CommitRange ...@@ -109,11 +107,6 @@ class CommitRange
reference reference
end end
# Returns a String for use in a link's title attribute
def reference_title
"Commits #{sha_start} through #{sha_to}"
end
# Return a Hash of parameters for passing to a URL helper # Return a Hash of parameters for passing to a URL helper
# #
# See `namespace_project_compare_url` # See `namespace_project_compare_url`
......
...@@ -2,7 +2,7 @@ module Awardable ...@@ -2,7 +2,7 @@ module Awardable
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
has_many :award_emoji, -> { includes(:user) }, as: :awardable, dependent: :destroy has_many :award_emoji, -> { includes(:user).order(:id) }, as: :awardable, dependent: :destroy
if self < Participable if self < Participable
# By default we always load award_emoji user association # By default we always load award_emoji user association
...@@ -59,6 +59,18 @@ module Awardable ...@@ -59,6 +59,18 @@ module Awardable
true true
end end
def awardable_votes?(name)
AwardEmoji::UPVOTE_NAME == name || AwardEmoji::DOWNVOTE_NAME == name
end
def user_can_award?(current_user, name)
if user_authored?(current_user)
!awardable_votes?(normalize_name(name))
else
true
end
end
def awarded_emoji?(emoji_name, current_user) def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: current_user).exists? award_emoji.where(name: emoji_name, user: current_user).exists?
end end
......
...@@ -12,7 +12,7 @@ module Elastic ...@@ -12,7 +12,7 @@ module Elastic
end end
def self.repositories_count def self.repositories_count
Project.where(wiki_enabled: true).count Project.with_wiki_enabled.count
end end
def client_for_indexing def client_for_indexing
...@@ -20,7 +20,7 @@ module Elastic ...@@ -20,7 +20,7 @@ module Elastic
end end
def self.import def self.import
Project.where(wiki_enabled: true).find_each do |project| Project.with_wiki_enabled.find_each do |project|
unless project.wiki.empty? unless project.wiki.empty?
project.wiki.index_blobs project.wiki.index_blobs
end end
......
...@@ -199,6 +199,10 @@ module Issuable ...@@ -199,6 +199,10 @@ module Issuable
end end
end end
def user_authored?(user)
user == author
end
def subscribed_without_subscriptions?(user) def subscribed_without_subscriptions?(user)
participants(user).include?(user) participants(user).include?(user)
end end
......
...@@ -28,4 +28,8 @@ module NoteOnDiff ...@@ -28,4 +28,8 @@ module NoteOnDiff
def can_be_award_emoji? def can_be_award_emoji?
false false
end end
def to_discussion
Discussion.new([self])
end
end end
# Makes api V3 compatible with old project features permissions methods
#
# After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled
# fields to a new table "project_features", support for the old fields is still needed in the API.
module ProjectFeaturesCompatibility
extend ActiveSupport::Concern
def wiki_enabled=(value)
write_feature_attribute(:wiki_access_level, value)
end
def builds_enabled=(value)
write_feature_attribute(:builds_access_level, value)
end
def merge_requests_enabled=(value)
write_feature_attribute(:merge_requests_access_level, value)
end
def issues_enabled=(value)
write_feature_attribute(:issues_access_level, value)
end
def snippets_enabled=(value)
write_feature_attribute(:snippets_access_level, value)
end
private
def write_feature_attribute(field, value)
build_project_feature unless project_feature
access_level = value == "true" ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
project_feature.update_attribute(field, access_level)
end
end
...@@ -52,11 +52,11 @@ module Taskable ...@@ -52,11 +52,11 @@ module Taskable
end end
# Return a string that describes the current state of this Taskable's task # Return a string that describes the current state of this Taskable's task
# list items, e.g. "20 tasks (12 completed, 8 remaining)" # list items, e.g. "12 of 20 tasks completed"
def task_status def task_status
return '' if description.blank? return '' if description.blank?
sum = tasks.summary sum = tasks.summary
"#{sum.item_count} tasks (#{sum.complete_count} completed, #{sum.incomplete_count} remaining)" "#{sum.complete_count} of #{sum.item_count} #{'task'.pluralize(sum.item_count)} completed"
end end
end end
...@@ -107,10 +107,6 @@ class DiffNote < Note ...@@ -107,10 +107,6 @@ class DiffNote < Note
self.noteable.find_diff_discussion(self.discussion_id) self.noteable.find_diff_discussion(self.discussion_id)
end end
def to_discussion
Discussion.new([self])
end
private private
def supported? def supported?
......
...@@ -94,13 +94,13 @@ class MergeRequest < ActiveRecord::Base ...@@ -94,13 +94,13 @@ class MergeRequest < ActiveRecord::Base
end end
end end
validates :source_project, presence: true, unless: [:allow_broken, :importing?] validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
validates :source_branch, presence: true validates :source_branch, presence: true
validates :target_project, presence: true validates :target_project, presence: true
validates :target_branch, presence: true validates :target_branch, presence: true
validates :merge_user, presence: true, if: :merge_when_build_succeeds? validates :merge_user, presence: true, if: :merge_when_build_succeeds?
validate :validate_branches, unless: [:allow_broken, :importing?] validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork validate :validate_fork, unless: :closed_without_fork?
validate :validate_approvals_before_merge validate :validate_approvals_before_merge
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
...@@ -245,12 +245,12 @@ class MergeRequest < ActiveRecord::Base ...@@ -245,12 +245,12 @@ class MergeRequest < ActiveRecord::Base
def source_branch_head def source_branch_head
source_branch_ref = @source_branch_sha || source_branch source_branch_ref = @source_branch_sha || source_branch
source_project.repository.commit(source_branch) if source_branch_ref source_project.repository.commit(source_branch_ref) if source_branch_ref
end end
def target_branch_head def target_branch_head
target_branch_ref = @target_branch_sha || target_branch target_branch_ref = @target_branch_sha || target_branch
target_project.repository.commit(target_branch) if target_branch_ref target_project.repository.commit(target_branch_ref) if target_branch_ref
end end
def branch_merge_base_commit def branch_merge_base_commit
...@@ -310,19 +310,22 @@ class MergeRequest < ActiveRecord::Base ...@@ -310,19 +310,22 @@ class MergeRequest < ActiveRecord::Base
def validate_fork def validate_fork
return true unless target_project && source_project return true unless target_project && source_project
return true if target_project == source_project
return true unless forked_source_project_missing?
if target_project == source_project errors.add :validate_fork,
true 'Source project is not a fork of the target project'
else end
# If source and target projects are different
# we should check if source project is actually a fork of target project def closed_without_fork?
if source_project.forked_from?(target_project) closed? && forked_source_project_missing?
true end
else
errors.add :validate_fork, def forked_source_project_missing?
'Source project is not a fork of target project' return false unless for_fork?
end return true unless source_project
end
!source_project.forked_from?(target_project)
end end
def validate_approvals_before_merge def validate_approvals_before_merge
...@@ -856,7 +859,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -856,7 +859,9 @@ class MergeRequest < ActiveRecord::Base
end end
def pipeline def pipeline
@pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project return unless diff_head_sha && source_project
@pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
end end
def all_pipelines def all_pipelines
......
...@@ -229,6 +229,10 @@ class Note < ActiveRecord::Base ...@@ -229,6 +229,10 @@ class Note < ActiveRecord::Base
end end
end end
def user_authored?(user)
user == author
end
def award_emoji? def award_emoji?
can_be_award_emoji? && contains_emoji_only? can_be_award_emoji? && contains_emoji_only?
end end
......
...@@ -12,24 +12,23 @@ class Project < ActiveRecord::Base ...@@ -12,24 +12,23 @@ class Project < ActiveRecord::Base
include CaseSensitivity include CaseSensitivity
include TokenAuthenticatable include TokenAuthenticatable
include Elastic::ProjectsSearch include Elastic::ProjectsSearch
include ProjectFeaturesCompatibility
extend Gitlab::ConfigHelper extend Gitlab::ConfigHelper
UNKNOWN_IMPORT_URL = 'http://unknown.git' UNKNOWN_IMPORT_URL = 'http://unknown.git'
delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
default_value_for :archived, false default_value_for :archived, false
default_value_for :visibility_level, gitlab_config_features.visibility_level default_value_for :visibility_level, gitlab_config_features.visibility_level
default_value_for :issues_enabled, gitlab_config_features.issues
default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
default_value_for :builds_enabled, gitlab_config_features.builds
default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :snippets_enabled, gitlab_config_features.snippets
default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for :container_registry_enabled, gitlab_config_features.container_registry
default_value_for(:repository_storage) { current_application_settings.repository_storage } default_value_for(:repository_storage) { current_application_settings.repository_storage }
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
after_create :ensure_dir_exist after_create :ensure_dir_exist
after_save :ensure_dir_exist, if: :namespace_id_changed? after_save :ensure_dir_exist, if: :namespace_id_changed?
after_initialize :setup_project_feature
# set last_activity_at to the same as created_at # set last_activity_at to the same as created_at
after_create :set_last_activity_at after_create :set_last_activity_at
...@@ -58,9 +57,8 @@ class Project < ActiveRecord::Base ...@@ -58,9 +57,8 @@ class Project < ActiveRecord::Base
belongs_to :mirror_user, foreign_key: 'mirror_user_id', class_name: 'User' belongs_to :mirror_user, foreign_key: 'mirror_user_id', class_name: 'User'
has_one :push_rule, dependent: :destroy has_one :push_rule, dependent: :destroy
has_one :board, dependent: :destroy
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id' has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
has_one :board, dependent: :destroy
# Project services # Project services
has_many :services has_many :services
...@@ -132,6 +130,7 @@ class Project < ActiveRecord::Base ...@@ -132,6 +130,7 @@ class Project < ActiveRecord::Base
has_many :notification_settings, dependent: :destroy, as: :source has_many :notification_settings, dependent: :destroy, as: :source
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :project_feature, dependent: :destroy
has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
...@@ -148,6 +147,7 @@ class Project < ActiveRecord::Base ...@@ -148,6 +147,7 @@ class Project < ActiveRecord::Base
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :remote_mirrors, accepts_nested_attributes_for :remote_mirrors,
allow_destroy: true, reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? } allow_destroy: true, reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }
accepts_nested_attributes_for :project_feature
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
...@@ -165,8 +165,6 @@ class Project < ActiveRecord::Base ...@@ -165,8 +165,6 @@ class Project < ActiveRecord::Base
length: { within: 0..255 }, length: { within: 0..255 },
format: { with: Gitlab::Regex.project_path_regex, format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message } message: Gitlab::Regex.project_path_regex_message }
validates :issues_enabled, :merge_requests_enabled,
:wiki_enabled, inclusion: { in: [true, false] }
validates :namespace, presence: true validates :namespace, presence: true
validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id validates_uniqueness_of :path, scope: :namespace_id
...@@ -207,6 +205,10 @@ class Project < ActiveRecord::Base ...@@ -207,6 +205,10 @@ class Project < ActiveRecord::Base
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct } scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
scope :with_builds_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') }
scope :with_issues_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.issues_access_level IS NULL or project_features.issues_access_level > 0') }
scope :with_wiki_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.wiki_access_level IS NULL or project_features.wiki_access_level > 0') }
scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') } scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) } scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
...@@ -420,6 +422,13 @@ class Project < ActiveRecord::Base ...@@ -420,6 +422,13 @@ class Project < ActiveRecord::Base
end end
end end
def lfs_enabled?
return false unless Gitlab.config.lfs.enabled
return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?
self[:lfs_enabled]
end
def repository_storage_path def repository_storage_path
Gitlab.config.repositories.storages[repository_storage] Gitlab.config.repositories.storages[repository_storage]
end end
...@@ -466,7 +475,7 @@ class Project < ActiveRecord::Base ...@@ -466,7 +475,7 @@ class Project < ActiveRecord::Base
# ref can't be HEAD, can only be branch/tag name or SHA # ref can't be HEAD, can only be branch/tag name or SHA
def latest_successful_builds_for(ref = default_branch) def latest_successful_builds_for(ref = default_branch)
latest_pipeline = pipelines.latest_successful_for(ref).first latest_pipeline = pipelines.latest_successful_for(ref)
if latest_pipeline if latest_pipeline
latest_pipeline.builds.latest.with_artifacts latest_pipeline.builds.latest.with_artifacts
...@@ -1229,16 +1238,21 @@ class Project < ActiveRecord::Base ...@@ -1229,16 +1238,21 @@ class Project < ActiveRecord::Base
!namespace.share_with_group_lock !namespace.share_with_group_lock
end end
def pipeline(sha, ref) def pipeline_for(ref, sha = nil)
sha ||= commit(ref).try(:sha)
return unless sha
pipelines.order(id: :desc).find_by(sha: sha, ref: ref) pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
end end
def ensure_pipeline(sha, ref, current_user = nil) def ensure_pipeline(ref, sha, current_user = nil)
pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref, user: current_user) pipeline_for(ref, sha) ||
pipelines.create(sha: sha, ref: ref, user: current_user)
end end
def enable_ci def enable_ci
self.builds_enabled = true project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end end
def any_runners?(&block) def any_runners?(&block)
...@@ -1513,6 +1527,11 @@ class Project < ActiveRecord::Base ...@@ -1513,6 +1527,11 @@ class Project < ActiveRecord::Base
private private
# Prevents the creation of project_feature record for every project
def setup_project_feature
build_project_feature unless project_feature
end
def default_branch_protected? def default_branch_protected?
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
......
class ProjectFeature < ActiveRecord::Base
# == Project features permissions
#
# Grants access level to project tools
#
# Tools can be enabled only for users, everyone or disabled
# Access control is made only for non private projects
#
# levels:
#
# Disabled: not enabled for anyone
# Private: enabled only for team members
# Enabled: enabled for everyone able to access the project
#
# Permision levels
DISABLED = 0
PRIVATE = 10
ENABLED = 20
FEATURES = %i(issues merge_requests wiki snippets builds)
belongs_to :project
def feature_available?(feature, user)
raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
get_permission(user, public_send("#{feature}_access_level"))
end
def builds_enabled?
return true unless builds_access_level
builds_access_level > DISABLED
end
def wiki_enabled?
return true unless wiki_access_level
wiki_access_level > DISABLED
end
def merge_requests_enabled?
return true unless merge_requests_access_level
merge_requests_access_level > DISABLED
end
private
def get_permission(user, level)
case level
when DISABLED
false
when PRIVATE
user && (project.team.member?(user) || user.admin?)
when ENABLED
true
else
true
end
end
end
...@@ -137,8 +137,21 @@ class Repository ...@@ -137,8 +137,21 @@ class Repository
end end
end end
def find_branch(name) def find_branch(name, fresh_repo: true)
raw_repository.branches.find { |branch| branch.name == name } # Since the Repository object may have in-memory index changes, invalidating the memoized Repository object may
# cause unintended side effects. Because finding a branch is a read-only operation, we can safely instantiate
# a new repo here to ensure a consistent state to avoid a libgit2 bug where concurrent access (e.g. via git gc)
# may cause the branch to "disappear" erroneously or have the wrong SHA.
#
# See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392
raw_repo =
if fresh_repo
Gitlab::Git::Repository.new(path_to_repo)
else
raw_repository
end
raw_repo.find_branch(name)
end end
def find_tag(name) def find_tag(name)
......
...@@ -470,7 +470,7 @@ class User < ActiveRecord::Base ...@@ -470,7 +470,7 @@ class User < ActiveRecord::Base
# #
# This logic is duplicated from `Ability#project_abilities` into a SQL form. # This logic is duplicated from `Ability#project_abilities` into a SQL form.
def projects_where_can_admin_issues def projects_where_can_admin_issues
authorized_projects(Gitlab::Access::REPORTER).non_archived.where.not(issues_enabled: false) authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
end end
def is_admin? def is_admin?
......
...@@ -157,28 +157,28 @@ class ProjectPolicy < BasePolicy ...@@ -157,28 +157,28 @@ class ProjectPolicy < BasePolicy
end end
def disabled_features! def disabled_features!
unless project.issues_enabled unless project.feature_available?(:issues, user)
cannot!(*named_abilities(:issue)) cannot!(*named_abilities(:issue))
end end
unless project.merge_requests_enabled unless project.feature_available?(:merge_requests, user)
cannot!(*named_abilities(:merge_request)) cannot!(*named_abilities(:merge_request))
end end
unless project.issues_enabled || project.merge_requests_enabled unless project.feature_available?(:issues, user) || project.feature_available?(:merge_requests, user)
cannot!(*named_abilities(:label)) cannot!(*named_abilities(:label))
cannot!(*named_abilities(:milestone)) cannot!(*named_abilities(:milestone))
end end
unless project.snippets_enabled unless project.feature_available?(:snippets, user)
cannot!(*named_abilities(:project_snippet)) cannot!(*named_abilities(:project_snippet))
end end
unless project.has_wiki? unless project.feature_available?(:wiki, user) || project.has_external_wiki?
cannot!(*named_abilities(:wiki)) cannot!(*named_abilities(:wiki))
end end
unless project.builds_enabled unless project.feature_available?(:builds, user)
cannot!(*named_abilities(:build)) cannot!(*named_abilities(:build))
cannot!(*named_abilities(:pipeline)) cannot!(*named_abilities(:pipeline))
cannot!(*named_abilities(:environment)) cannot!(*named_abilities(:environment))
......
...@@ -3,7 +3,10 @@ module Boards ...@@ -3,7 +3,10 @@ module Boards
class CreateService < Boards::BaseService class CreateService < Boards::BaseService
def execute def execute
List.transaction do List.transaction do
create_list_at(next_position) label = project.labels.find(params[:label_id])
position = next_position
create_list(label, position)
end end
end end
...@@ -14,8 +17,8 @@ module Boards ...@@ -14,8 +17,8 @@ module Boards
max_position.nil? ? 0 : max_position.succ max_position.nil? ? 0 : max_position.succ
end end
def create_list_at(position) def create_list(label, position)
board.lists.create(params.merge(list_type: :label, position: position)) board.lists.create(label: label, list_type: :label, position: position)
end end
end end
end end
......
...@@ -10,13 +10,15 @@ module Ci ...@@ -10,13 +10,15 @@ module Ci
create_builds! create_builds!
end end
new_builds = @pipeline.with_lock do
stage_indexes_of_created_builds.map do |index| new_builds =
process_stage(index) stage_indexes_of_created_builds.map do |index|
end process_stage(index)
end
# Return a flag if a when builds got enqueued # Return a flag if a when builds got enqueued
new_builds.flatten.any? new_builds.flatten.any?
end
end end
private private
......
...@@ -8,16 +8,18 @@ module Ci ...@@ -8,16 +8,18 @@ module Ci
builds = builds =
if current_runner.shared? if current_runner.shared?
builds. builds.
# don't run projects which have not enabled shared runners # don't run projects which have not enabled shared runners and builds
joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }). joins(:project).where(projects: { shared_runners_enabled: true }).
joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
# this returns builds that are ordered by number of running builds # this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all # 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"). 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') order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
else else
# do run projects which are only assigned to this runner (FIFO) # do run projects which are only assigned to this runner (FIFO)
builds.where(project: current_runner.projects.where(builds_enabled: true)).order('created_at ASC') builds.where(project: current_runner.projects.with_builds_enabled).order('created_at ASC')
end end
build = builds.find do |build| build = builds.find do |build|
......
module Ci
class WebHookService
def build_end(build)
execute_hooks(build.project, build_data(build))
end
def execute_hooks(project, data)
project.web_hooks.each do |web_hook|
async_execute_hook(web_hook, data)
end
end
def async_execute_hook(hook, data)
Sidekiq::Client.enqueue(Ci::WebHookWorker, hook.id, data)
end
def build_data(build)
project = build.project
data = {}
data.merge!({
build_id: build.id,
build_name: build.name,
build_status: build.status,
build_started_at: build.started_at,
build_finished_at: build.finished_at,
project_id: project.id,
project_name: project.name,
gitlab_url: project.gitlab_url,
ref: build.ref,
before_sha: build.before_sha,
sha: build.sha,
})
end
end
end
...@@ -45,6 +45,7 @@ class IssuableBaseService < BaseService ...@@ -45,6 +45,7 @@ class IssuableBaseService < BaseService
unless can?(current_user, ability, project) unless can?(current_user, ability, project)
params.delete(:milestone_id) params.delete(:milestone_id)
params.delete(:labels)
params.delete(:add_label_ids) params.delete(:add_label_ids)
params.delete(:remove_label_ids) params.delete(:remove_label_ids)
params.delete(:label_ids) params.delete(:label_ids)
...@@ -72,6 +73,7 @@ class IssuableBaseService < BaseService ...@@ -72,6 +73,7 @@ class IssuableBaseService < BaseService
filter_labels_in_param(:add_label_ids) filter_labels_in_param(:add_label_ids)
filter_labels_in_param(:remove_label_ids) filter_labels_in_param(:remove_label_ids)
filter_labels_in_param(:label_ids) filter_labels_in_param(:label_ids)
find_or_create_label_ids
end end
def filter_labels_in_param(key) def filter_labels_in_param(key)
...@@ -80,6 +82,17 @@ class IssuableBaseService < BaseService ...@@ -80,6 +82,17 @@ class IssuableBaseService < BaseService
params[key] = project.labels.where(id: params[key]).pluck(:id) params[key] = project.labels.where(id: params[key]).pluck(:id)
end end
def find_or_create_label_ids
labels = params.delete(:labels)
return unless labels
params[:label_ids] = labels.split(",").map do |label_name|
project.labels.create_with(color: Label::DEFAULT_COLOR)
.find_or_create_by(title: label_name.strip)
.id
end
end
def process_label_ids(attributes, existing_label_ids: nil) def process_label_ids(attributes, existing_label_ids: nil)
label_ids = attributes.delete(:label_ids) label_ids = attributes.delete(:label_ids)
add_label_ids = attributes.delete(:add_label_ids) add_label_ids = attributes.delete(:add_label_ids)
...@@ -162,7 +175,12 @@ class IssuableBaseService < BaseService ...@@ -162,7 +175,12 @@ class IssuableBaseService < BaseService
if params.present? && update_issuable(issuable, params) if params.present? && update_issuable(issuable, params)
issuable.reset_events_cache issuable.reset_events_cache
handle_common_system_notes(issuable, old_labels: old_labels)
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
handle_common_system_notes(issuable, old_labels: old_labels)
end
handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users) handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
issuable.create_new_cross_references!(current_user) issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update') execute_hooks(issuable, 'update')
......
...@@ -31,7 +31,7 @@ module MergeRequests ...@@ -31,7 +31,7 @@ module MergeRequests
def get_branches(changes) def get_branches(changes)
return [] if project.empty_repo? return [] if project.empty_repo?
return [] unless project.merge_requests_enabled return [] unless project.merge_requests_enabled?
changes_list = Gitlab::ChangesList.new(changes) changes_list = Gitlab::ChangesList.new(changes)
changes_list.map do |change| changes_list.map do |change|
......
module MergeRequests module MergeRequests
class ResolveService < MergeRequests::BaseService class ResolveService < MergeRequests::BaseService
attr_accessor :conflicts, :rugged, :merge_index attr_accessor :conflicts, :rugged, :merge_index, :merge_request
def execute(merge_request) def execute(merge_request)
@conflicts = merge_request.conflicts @conflicts = merge_request.conflicts
@rugged = project.repository.rugged @rugged = project.repository.rugged
@merge_index = conflicts.merge_index @merge_index = conflicts.merge_index
@merge_request = merge_request
fetch_their_commit!
conflicts.files.each do |file| conflicts.files.each do |file|
write_resolved_file_to_index(file, params[:sections]) write_resolved_file_to_index(file, params[:sections])
...@@ -27,5 +30,21 @@ module MergeRequests ...@@ -27,5 +30,21 @@ module MergeRequests
merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode) merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
merge_index.conflict_remove(our_path) merge_index.conflict_remove(our_path)
end end
# If their commit (in the target project) doesn't exist in the source project, it
# can't be a parent for the merge commit we're about to create. If that's the case,
# fetch the target branch ref into the source project so the commit exists in both.
#
def fetch_their_commit!
return if rugged.include?(conflicts.their_commit.oid)
random_string = SecureRandom.hex
project.repository.fetch_ref(
merge_request.target_project.repository.path_to_repo,
"refs/heads/#{merge_request.target_branch}",
"refs/tmp/#{random_string}/head"
)
end
end end
end end
...@@ -11,6 +11,10 @@ module MergeRequests ...@@ -11,6 +11,10 @@ module MergeRequests
params.except!(:target_project_id) params.except!(:target_project_id)
params.except!(:source_branch) params.except!(:source_branch)
if merge_request.closed_without_fork?
params.except!(:target_branch, :force_remove_source_branch)
end
merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
old_approvers = merge_request.overall_approvers.to_a old_approvers = merge_request.overall_approvers.to_a
......
...@@ -7,7 +7,6 @@ module Projects ...@@ -7,7 +7,6 @@ module Projects
def execute def execute
forked_from_project_id = params.delete(:forked_from_project_id) forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data) import_data = params.delete(:import_data)
@project = Project.new(params) @project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility level # Make sure that the user is allowed to use the specified visibility level
...@@ -81,8 +80,7 @@ module Projects ...@@ -81,8 +80,7 @@ module Projects
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
unless @project.gitlab_project_import? unless @project.gitlab_project_import?
@project.create_wiki if @project.wiki_enabled? @project.create_wiki if @project.feature_available?(:wiki, current_user)
@project.build_missing_services @project.build_missing_services
@project.create_labels @project.create_labels
......
...@@ -8,7 +8,6 @@ module Projects ...@@ -8,7 +8,6 @@ module Projects
name: @project.name, name: @project.name,
path: @project.path, path: @project.path,
shared_runners_enabled: @project.shared_runners_enabled, shared_runners_enabled: @project.shared_runners_enabled,
builds_enabled: @project.builds_enabled,
namespace_id: @params[:namespace].try(:id) || current_user.namespace.id namespace_id: @params[:namespace].try(:id) || current_user.namespace.id
} }
...@@ -17,6 +16,9 @@ module Projects ...@@ -17,6 +16,9 @@ module Projects
end end
new_project = CreateService.new(current_user, new_params).execute new_project = CreateService.new(current_user, new_params).execute
builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
new_project new_project
end end
......
...@@ -73,6 +73,12 @@ ...@@ -73,6 +73,12 @@
%span.light last commit: %span.light last commit:
%strong %strong
= last_commit(@project) = last_commit(@project)
%li
%span.light Git LFS status:
%strong
= project_lfs_status(@project)
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- else - else
%li %li
%span.light repository: %span.light repository:
......
...@@ -28,21 +28,25 @@ ...@@ -28,21 +28,25 @@
.row-content-block.second-block .row-content-block.second-block
= form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do
.filter-item.inline .filter-item.inline
= select_tag('project_id', todo_projects_options, - if params[:project_id].present?
class: 'select2 trigger-submit', include_blank: true, = hidden_field_tag(:project_id, params[:project_id])
data: {placeholder: 'Project'}) = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
placeholder: 'Search projects', data: { data: todo_projects_options } })
.filter-item.inline .filter-item.inline
= users_select_tag(:author_id, selected: params[:author_id], - if params[:author_id].present?
placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true) = hidden_field_tag(:author_id, params[:author_id])
= dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit',
placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author' } })
.filter-item.inline .filter-item.inline
= select_tag('type', todo_types_options, - if params[:type].present?
class: 'select2 trigger-submit', include_blank: true, = hidden_field_tag(:type, params[:type])
data: {placeholder: 'Type'}) = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
data: { data: todo_types_options } })
.filter-item.inline.actions-filter .filter-item.inline.actions-filter
= select_tag('action_id', todo_actions_options, - if params[:action_id].present?
class: 'select2 trigger-submit', include_blank: true, = hidden_field_tag(:action_id, params[:action_id])
data: {placeholder: 'Action'}) = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
data: { data: todo_actions_options }})
.pull-right .pull-right
.dropdown.inline.prepend-left-10 .dropdown.inline.prepend-left-10
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
...@@ -66,7 +70,7 @@ ...@@ -66,7 +70,7 @@
- if @todos.any? - if @todos.any?
.js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
- @todos.group_by(&:project).each do |group| - @todos.group_by(&:project).each do |group|
.panel.panel-default.panel-small.js-todos-list .panel.panel-default.panel-small
- project = group[0] - project = group[0]
.panel-heading .panel-heading
= link_to project.name_with_namespace, namespace_project_path(project.namespace, project) = link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
...@@ -76,11 +80,3 @@ ...@@ -76,11 +80,3 @@
= paginate @todos, theme: "gitlab" = paginate @todos, theme: "gitlab"
- else - else
.nothing-here-block You're all done! .nothing-here-block You're all done!
:javascript
new UsersSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
});
- if event.visible_to_user?(current_user) - if event.visible_to_user?(current_user)
.event-item{ class: event_row_class(event) } .event-item{ class: event_row_class(event) }
.event-item-timestamp .event-item-timestamp
#{time_ago_with_tooltip(event.created_at)} #{time_ago_with_tooltip(event.created_at, skip_js: true)}
= cache [event, current_application_settings, "v2.2"] do = cache [event, current_application_settings, "v2.2"] do
= author_avatar(event, size: 40) = author_avatar(event, size: 40)
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
%span %span
Protected Branches Protected Branches
- if @project.builds_enabled? - if @project.feature_available?(:builds, current_user)
= nav_link(controller: :runners) do = nav_link(controller: :runners) do
= link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
%span %span
......
%fieldset.features.append-bottom-0.issues-feature %fieldset.features.append-bottom-0.issue-feature
%h5.prepend-top-0 %h5.prepend-top-0
Issues Issues
.form-group .form-group
......
...@@ -13,19 +13,13 @@ ...@@ -13,19 +13,13 @@
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" } %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
{{ list.title }} {{ list.title }}
%span.pull-right{ "v-if" => "list.type !== 'blank'" } %span.pull-right{ "v-if" => "list.type !== 'blank'" }
{{ list.issues.length }} {{ list.issuesSize }}
- if can?(current_user, :admin_list, @project) - if can?(current_user, :admin_list, @project)
%board-delete{ "inline-template" => true, %board-delete{ "inline-template" => true,
":list" => "list", ":list" => "list",
"v-if" => "!list.preset && list.id" } "v-if" => "!list.preset && list.id" }
%button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" } %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash") = icon("trash")
= icon("spinner spin", class: "board-header-loading-spinner pull-right", "v-show" => "list.loadingMore")
.board-inner-container.board-search-container{ "v-if" => "list.canSearch()" }
%input.form-control{ type: "text", placeholder: "Search issues", "v-model" => "query", "debounce" => "250" }
= icon("search", class: "board-search-icon", "v-show" => "!query")
%button.board-search-clear-btn{ type: "button", role: "button", "aria-label" => "Clear search", "@click" => "query = ''", "v-show" => "query" }
= icon("times", class: "board-search-clear")
%board-list{ "inline-template" => true, %board-list{ "inline-template" => true,
"v-if" => "list.type !== 'blank'", "v-if" => "list.type !== 'blank'",
":list" => "list", ":list" => "list",
...@@ -39,5 +33,11 @@ ...@@ -39,5 +33,11 @@
"v-show" => "!loading", "v-show" => "!loading",
":data-board" => "list.id" } ":data-board" => "list.id" }
= render "projects/boards/components/card" = render "projects/boards/components/card"
%li.board-list-count.text-center{ "v-if" => "showCount" }
= icon("spinner spin", "v-show" => "list.loadingMore" )
%span{ "v-if" => "list.issues.length === list.issuesSize" }
Showing all issues
%span{ "v-else" => true }
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
- if can?(current_user, :admin_list, @project) - if can?(current_user, :admin_list, @project)
= render "projects/boards/components/blank_state" = render "projects/boards/components/blank_state"
...@@ -33,6 +33,8 @@ ...@@ -33,6 +33,8 @@
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do
Compare Compare
= render 'projects/buttons/download', project: @project, ref: branch.name
- if can_remove_branch?(@project, branch.name) - if can_remove_branch?(@project, branch.name)
= link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o") = icon("trash-o")
......
- unless @project.empty_repo? - if !project.empty_repo? && can?(current_user, :download_code, project)
- if can? current_user, :download_code, @project %span.btn-group{class: 'hidden-xs hidden-sm btn-grouped'}
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has-tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do .dropdown.inline
= icon('download') %button.btn{ 'data-toggle' => 'dropdown' }
= icon('download')
%span.caret
%span.sr-only
Select Archive Format
%ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
%li.dropdown-header Source code
%li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
%i.fa.fa-download
%span Download zip
%li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
%i.fa.fa-download
%span Download tar.gz
%li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do
%i.fa.fa-download
%span Download tar.bz2
%li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do
%i.fa.fa-download
%span Download tar
- pipeline = project.pipelines.latest_successful_for(ref)
- if pipeline
- artifacts = pipeline.builds.latest.with_artifacts
- if artifacts.any?
%li.dropdown-header Artifacts
- unless pipeline.latest?
- latest_pipeline = project.pipeline_for(ref)
%li
.unclickable= ci_status_for_statuseable(latest_pipeline)
%li.dropdown-header Previous Artifacts
- artifacts.each do |job|
%li
= link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do
%i.fa.fa-download
%span Download '#{job.name}'
...@@ -89,4 +89,4 @@ ...@@ -89,4 +89,4 @@
= icon('repeat') = icon('repeat')
- elsif build.playable? - elsif build.playable?
= link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do = link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
= icon('play') = custom_icon('icon_play')
...@@ -66,13 +66,13 @@ ...@@ -66,13 +66,13 @@
- if actions.any? - if actions.any?
.btn-group .btn-group
%a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
= icon("play") = custom_icon('icon_play')
%b.caret %b.caret
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |build| - actions.each do |build|
%li %li
= link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do
= icon("play") = custom_icon('icon_play')
%span= build.name.humanize %span= build.name.humanize
- if artifacts.present? - if artifacts.present?
.btn-group .btn-group
......
...@@ -10,7 +10,10 @@ ...@@ -10,7 +10,10 @@
%th Commit %th Commit
- pipelines.stages.each do |stage| - pipelines.stages.each do |stage|
%th.stage %th.stage
%span.has-tooltip{ title: "#{stage.titleize}" } - if stage.titleize.length > 12
%span.has-tooltip{ title: "#{stage.titleize}" }
= stage.titleize
- else
= stage.titleize = stage.titleize
%th %th
%th %th
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count] - cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count]
- cache_key.push(commit.status) if commit.status - cache_key.push(commit.status) if commit.status
= cache(cache_key) do = cache(cache_key, expires_in: 1.day) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
= author_avatar(commit, size: 36) = author_avatar(commit, size: 36)
......
...@@ -5,13 +5,13 @@ ...@@ -5,13 +5,13 @@
.inline .inline
.dropdown .dropdown
%a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
= icon("play") = custom_icon('icon_play')
%b.caret %b.caret
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |action| - actions.each do |action|
%li %li
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
= icon("play") = custom_icon('icon_play')
%span= action.name.humanize %span= action.name.humanize
- if local_assigns.fetch(:allow_rollback, false) - if local_assigns.fetch(:allow_rollback, false)
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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