Commit d2e2da4c authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into revert-c676283b

parents 236178c3 fd1741b4
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:
inherit_from: .rubocop_todo.yml
AllCops:
TargetRubyVersion: 2.1
# Cop names are not displayed in offense messages by default. Change behavior
TargetRubyVersion: 2.3
# Cop names are not d§splayed in offense messages by default. Change behavior
# by overriding DisplayCopNames, or by giving the -D/--display-cop-names
# option.
DisplayCopNames: true
......@@ -192,6 +192,9 @@ Style/FlipFlop:
Style/For:
Enabled: true
# Checks if there is a magic comment to enforce string literals
Style/FrozenStringLiteralComment:
Enabled: false
# Do not introduce global variables.
Style/GlobalVars:
Enabled: true
......
Please view this file on the master branch, on stable branches it's out of date.
v 8.12.0 (unreleased)
- Filter tags by name !6121
- Make push events have equal vertical spacing.
- 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)
- Change logo animation to CSS (ClemMakesApps)
- Instructions for enabling Git packfile bitmaps !6104
- Change merge_error column from string to text type
- Reduce contributions calendar data payload (ClemMakesApps)
- 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)
- 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)
- 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)
- Add `wiki_page_events` to project hook APIs (Ben Boeckel)
- 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 BroadcastMessage API
- Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
- Remove unused mixins (ClemMakesApps)
- Add search to all issue board lists
- Fix groups sort dropdown alignment (ClemMakesApps)
- Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps)
- Use JavaScript tooltips for mentions !5301 (winniehell)
- 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
- 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)
- Fix badge count alignment (ClemMakesApps)
- Fix repo title alignment (ClemMakesApps)
- Fix branch title trailing space on hover (ClemMakesApps)
- Award emoji tooltips containing more than 10 usernames are now truncated !4780 (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)
- Update merge_requests.md with a simpler way to check out a merge request. !5944
- Fix button missing type (ClemMakesApps)
......@@ -34,20 +60,46 @@ v 8.12.0 (unreleased)
- Load branches asynchronously in Cherry Pick and Revert dialogs.
- Add merge request versions !5467
- 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)
- Reduce number of database queries on builds tab
- 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)
- Adds response mime type to transaction metric action when it's not HTML
- Fix hover leading space bug in pipeline graph !5980
v 8.11.3 (unreleased)
- User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496
- 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
- Label list shows all issues (opened or closed) with that label
- Don't show resolve conflicts link before MR status is updated
- 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
- 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
- Show "Create Merge Request" widget for push events to fork projects on the source project. !5978
......
......@@ -287,6 +287,8 @@ request is as follows:
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.
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 7th day of the month. This is the best time to submit an MR and get
......
......@@ -53,7 +53,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
gem 'gitlab_git', '~> 10.4.7'
gem 'gitlab_git', '~> 10.5'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
......@@ -97,9 +97,6 @@ gem 'fog-rackspace', '~> 0.1.1'
# for aws storage
gem 'unf', '~> 0.1.4'
# Authorization
gem 'six', '~> 0.2.0'
# Seed data
gem 'seed-fu', '~> 2.3.5'
......
......@@ -279,7 +279,7 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
gitlab_git (10.4.7)
gitlab_git (10.5.0)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
......@@ -683,7 +683,6 @@ GEM
rack (~> 1.5)
rack-protection (~> 1.4)
tilt (>= 1.3, < 3)
six (0.2.0)
slack-notifier (1.2.1)
slop (3.6.0)
spinach (0.8.10)
......@@ -859,7 +858,7 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_git (~> 10.4.7)
gitlab_git (~> 10.5)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
......@@ -954,7 +953,6 @@ DEPENDENCIES
sidekiq-cron (~> 0.4.0)
simplecov (= 0.12.0)
sinatra (~> 1.4.4)
six (~> 0.2.0)
slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2)
......
......@@ -50,7 +50,7 @@ etc.).
The most important thing is making sure valid issues receive feedback from the
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
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
......
......@@ -12,7 +12,7 @@
}
Activities.prototype.updateTooltips = function() {
return gl.utils.localTimeAgo($('.js-timeago', '#activity'));
return gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
};
Activities.prototype.reloadActivities = function() {
......
(function(w) {
$(function() {
$('.js-toggle-button').on('click', function(e) {
$('body').on('click', '.js-toggle-button', function(e) {
e.preventDefault();
$(this)
.find('.fa')
......
......@@ -54,4 +54,11 @@ $(() => {
});
}
});
gl.IssueBoardsSearch = new Vue({
el: '#js-boards-seach',
data: {
filters: Store.state.filters
}
});
});
......@@ -21,15 +21,10 @@
},
data () {
return {
query: '',
filters: Store.state.filters
};
},
watch: {
query () {
this.list.filters = this.getFilterData();
this.list.getIssues(true);
},
filters: {
handler () {
this.list.page = 1;
......@@ -38,16 +33,6 @@
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 () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
......
......@@ -20,7 +20,8 @@
data () {
return {
scrollOffset: 250,
filters: Store.state.filters
filters: Store.state.filters,
showCount: false
};
},
watch: {
......@@ -30,6 +31,15 @@
this.$els.list.scrollTop = 0;
},
deep: true
},
issues () {
this.$nextTick(() => {
if (this.scrollHeight() > this.listHeight()) {
this.showCount = true;
} else {
this.showCount = false;
}
});
}
},
methods: {
......@@ -58,6 +68,7 @@
group: 'issues',
sort: false,
disabled: this.disabled,
filter: '.board-list-count',
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
......
......@@ -11,6 +11,7 @@ class List {
this.loading = true;
this.loadingMore = false;
this.issues = [];
this.issuesSize = 0;
if (obj.label) {
this.label = new ListLabel(obj.label);
......@@ -51,17 +52,13 @@ class List {
}
nextPage () {
if (Math.floor(this.issues.length / 20) === this.page) {
if (this.issuesSize > this.issues.length) {
this.page++;
return this.getIssues(false);
}
}
canSearch () {
return this.type === 'backlog';
}
getIssues (emptyIssues = true) {
const filters = this.filters;
let data = { page: this.page };
......@@ -80,12 +77,13 @@ class List {
.then((resp) => {
const data = resp.json();
this.loading = false;
this.issuesSize = data.size;
if (emptyIssues) {
this.issues = [];
}
this.createIssues(data);
this.createIssues(data.issues);
});
}
......@@ -96,6 +94,7 @@ class List {
}
addIssue (issue, listFrom) {
if (!this.findIssue(issue.id)) {
this.issues.push(issue);
if (this.label) {
......@@ -103,7 +102,12 @@ class List {
}
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 {
const matchesRemove = removeIssue.id === issue.id;
if (matchesRemove) {
this.issuesSize--;
issue.removeLabel(this.label);
}
......
......@@ -15,7 +15,8 @@
author_id: gl.utils.getParameterValues('author_id')[0],
assignee_id: gl.utils.getParameterValues('assignee_id')[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) {
......
......@@ -203,6 +203,7 @@
break;
case 'labels':
switch (path[2]) {
case 'new':
case 'edit':
new Labels();
}
......
......@@ -556,7 +556,7 @@
if (isInput) {
field = $(this.el);
} 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)) {
el.removeClass(ACTIVE_CLASS);
......
......@@ -171,7 +171,7 @@
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');
}
if ($dropdown.hasClass('js-multiselect') && removesAll) {
......
(function() {
Turbolinks.enableProgressBar();
start = function() {
$(document).on('page:fetch', function() {
$('.tanuki-logo').addClass('animate');
};
});
stop = function() {
$(document).on('page:change', function() {
$('.tanuki-logo').removeClass('animate');
};
$(document).on('page:fetch', start);
$(document).on('page:change', stop);
});
}).call(this);
......@@ -13,6 +13,7 @@
this.perPage = this.el.data('perPage');
this.clearListeners();
this.initBtnListeners();
this.initFilters();
}
Todos.prototype.clearListeners = function() {
......@@ -27,6 +28,31 @@
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) {
var $this;
e.preventDefault();
......@@ -66,7 +92,7 @@
success: (function(_this) {
return function(data) {
$this.remove();
$('.js-todos-list').remove();
$('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
return _this.updateBadges(data);
};
})(this)
......
(function() {
this.User = (function() {
function User(opts) {
(global => {
global.User = class {
constructor(opts) {
this.opts = opts;
$('.profile-groups-avatars').tooltip({
"placement": "top"
});
this.placeProfileAvatarsToTop();
this.initTabs();
$('.hide-project-limit-message').on('click', function(e) {
$.cookie('hide_project_limit_message', 'false', {
path: gon.relative_url_root || '/'
});
$(this).parents('.project-limit-message').remove();
return e.preventDefault();
this.hideProjectLimitMessage();
}
placeProfileAvatarsToTop() {
$('.profile-groups-avatars').tooltip({
placement: 'top'
});
}
User.prototype.initTabs = function() {
initTabs() {
return new UserTabs({
parentEl: '.user-profile',
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 @@
&.dropdown-menu-user-link {
line-height: 16px;
}
.icon-play {
fill: $table-text-gray;
margin-right: 6px;
height: 12px;
width: 11px;
}
}
.dropdown-header {
......@@ -195,6 +202,12 @@
.separator + .dropdown-header {
padding-top: 2px;
}
.unclickable {
cursor: not-allowed;
padding: 5px 8px;
color: $dropdown-header-color;
}
}
.dropdown-menu-large {
......
......@@ -84,7 +84,7 @@ header {
.side-nav-toggle {
position: absolute;
left: -10px;
margin: 6px 0;
margin: 7px 0;
font-size: 18px;
padding: 6px 10px;
border: none;
......@@ -136,6 +136,8 @@ header {
}
.title {
position: relative;
padding-right: 20px;
margin: 0;
font-size: 19px;
max-width: 400px;
......@@ -148,7 +150,11 @@ header {
vertical-align: top;
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;
}
......@@ -160,11 +166,15 @@ header {
}
.dropdown-toggle-caret {
position: relative;
top: -2px;
color: $gl-text-color;
border: transparent;
background: transparent;
position: absolute;
right: 3px;
width: 12px;
line-height: 12px;
margin-left: 5px;
line-height: 19px;
margin-top: (($header-height - 19) / 2);
padding: 0;
font-size: 10px;
text-align: center;
cursor: pointer;
......
......@@ -9,14 +9,6 @@
border-radius: $radius;
}
@mixin transition($transition) {
-webkit-transition: $transition;
-moz-transition: $transition;
-ms-transition: $transition;
-o-transition: $transition;
transition: $transition;
}
/**
* Prefilled mixins
* Mixins with fixed values
......
......@@ -8,10 +8,7 @@
height: 30px;
transition-duration: .3s;
-webkit-transform: translateZ(0);
background: -webkit-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%);
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%);
background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4));
&.scrolling {
visibility: visible;
......@@ -211,12 +208,6 @@
}
}
.project-filter-form {
input {
background-color: $background-color;
}
}
@media (max-width: $screen-xs-max) {
padding-bottom: 0;
width: 100%;
......
......@@ -151,7 +151,7 @@
background-position: right 0 bottom 6px;
border: 1px solid $input-border;
@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 {
border-color: $input-border-focus;
......
......@@ -142,11 +142,6 @@
}
}
.board-header-loading-spinner {
margin-right: 10px;
color: $gray-darkest;
}
.board-inner-container {
border-bottom: 1px solid $border-color;
padding: $gl-padding;
......@@ -160,40 +155,6 @@
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 {
margin-right: 10px;
padding: 0;
......@@ -304,3 +265,22 @@
margin-right: 8px;
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 @@
&.affix {
right: 30px;
bottom: 15px;
z-index: 1;
@media (min-width: $screen-md-min) {
right: 26%;
......
......@@ -4,8 +4,9 @@
margin: 0;
}
.fa-play {
font-size: 14px;
.icon-play {
height: 13px;
width: 12px;
}
.dropdown-new {
......
......@@ -12,6 +12,10 @@
padding-right: 8px;
margin-bottom: 10px;
min-width: 15px;
.selected_issue {
vertical-align: text-top;
}
}
.issue-labels {
......
......@@ -2,6 +2,7 @@
.stage {
max-width: 90px;
width: 90px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
......@@ -146,6 +147,7 @@
}
.stage-cell {
text-align: center;
svg {
height: 18px;
......@@ -153,10 +155,6 @@
vertical-align: middle;
overflow: visible;
}
.light {
width: 3px;
}
}
.duration,
......@@ -215,6 +213,13 @@
border-color: $border-white-normal;
}
}
.btn {
.icon-play {
height: 13px;
width: 12px;
}
}
}
}
......@@ -469,11 +474,22 @@
.pipelines.tab-pane {
.content-list.pipelines {
overflow: scroll;
overflow: auto;
}
.stage {
max-width: 60px;
width: 60px;
max-width: 100px;
width: 100px;
}
.pipeline-actions {
min-width: initial;
}
}
.ci-status-icon-created {
svg {
fill: $table-text-gray;
}
}
......@@ -311,6 +311,14 @@ a.deploy-project-label {
color: $gl-success;
}
.lfs-enabled {
color: $gl-success;
}
.lfs-disabled {
color: $gl-warning;
}
.breadcrumb.repo-breadcrumb {
padding: 0;
background: transparent;
......@@ -600,7 +608,13 @@ pre.light-well {
}
}
.project-show-readme .readme-holder {
.project-show-readme {
.row-content-block {
background-color: inherit;
border: none;
}
.readme-holder {
padding: $gl-padding 0;
border-top: 0;
......@@ -613,6 +627,7 @@ pre.light-well {
border-bottom: none;
padding: 0;
}
}
}
.git-clone-holder {
......
......@@ -80,7 +80,7 @@
.search-icon {
@extend .fa-search;
@include transition(color .15s);
transition: color 0.15s;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
......@@ -125,7 +125,7 @@
}
.location-badge {
@include transition(all .15s);
transition: all 0.15s;
background-color: $location-badge-active-bg;
color: $white-light;
}
......
......@@ -43,6 +43,15 @@
border-color: $blue-normal;
}
&.ci-created {
color: $table-text-gray;
border-color: $table-text-gray;
svg {
fill: $table-text-gray;
}
}
svg {
height: 13px;
width: 13px;
......
......@@ -11,6 +11,16 @@
}
}
.last-commit {
max-width: 506px;
.last-commit-content {
@include str-truncated;
width: calc(100% - 140px);
margin-left: 3px;
}
}
.tree-table {
margin-bottom: 0;
......
......@@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :abilities, :can?, :current_application_settings
helper_method :can?, :current_application_settings
helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
......@@ -97,12 +97,8 @@ class ApplicationController < ActionController::Base
current_application_settings.after_sign_out_path.presence || new_user_session_path
end
def abilities
Ability.abilities
end
def can?(object, action, subject)
abilities.allowed?(object, action, subject)
Ability.allowed?(object, action, subject)
end
def access_denied!
......
......@@ -8,10 +8,14 @@ module ToggleAwardEmoji
def toggle_award_emoji
name = params.require(:name)
if awardable.user_can_award?(current_user, name)
awardable.toggle_award_emoji(name, current_user)
TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
render json: { ok: true }
else
render json: { ok: false }
end
end
private
......
......@@ -37,7 +37,7 @@ class JwtController < ApplicationController
def authenticate_project(login, password)
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
......
......@@ -14,7 +14,7 @@ class NamespacesController < ApplicationController
if user
redirect_to user_path(user)
elsif group && can?(current_user, :read_group, namespace)
elsif group && can?(current_user, :read_group, group)
redirect_to group_path(group)
elsif current_user.nil?
authenticate_user!
......
......@@ -88,6 +88,6 @@ class Projects::ApplicationController < ApplicationController
end
def builds_enabled
return render_404 unless @project.builds_enabled?
return render_404 unless @project.feature_available?(:builds, current_user)
end
end
class Projects::ArtifactsController < Projects::ApplicationController
include ExtractsPath
layout 'project'
before_action :authorize_read_build!
before_action :authorize_update_build!, only: [:keep]
before_action :extract_ref_name_and_path
before_action :validate_artifacts!
def download
unless artifacts_file.file_storage?
return redirect_to artifacts_file.url
end
if artifacts_file.file_storage?
send_file artifacts_file.path, disposition: 'attachment'
else
redirect_to artifacts_file.url
end
end
def browse
directory = params[:path] ? "#{params[:path]}/" : ''
@entry = build.artifacts_metadata_entry(directory)
return render_404 unless @entry.exists?
render_404 unless @entry.exists?
end
def file
......@@ -34,14 +37,41 @@ class Projects::ArtifactsController < Projects::ApplicationController
redirect_to namespace_project_build_path(project.namespace, project, build)
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
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!
render_404 unless build.artifacts?
render_404 unless build && build.artifacts?
end
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
def artifacts_file
......
......@@ -8,12 +8,15 @@ module Projects
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
issues = issues.page(params[:page])
render json: issues.as_json(
render json: {
issues: issues.as_json(
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
})
}),
size: issues.total_count
}
end
def update
......
......@@ -38,6 +38,6 @@ class Projects::DiscussionsController < Projects::ApplicationController
end
def module_enabled
render_404 unless @project.merge_requests_enabled
render_404 unless @project.feature_available?(:merge_requests, current_user)
end
end
......@@ -201,7 +201,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
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
def redirect_to_external_issue_tracker
......
......@@ -99,7 +99,7 @@ class Projects::LabelsController < Projects::ApplicationController
protected
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
end
end
......
......@@ -413,7 +413,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def module_enabled
return render_404 unless @project.merge_requests_enabled
return render_404 unless @project.feature_available?(:merge_requests, current_user)
end
def validates_merge_request
......
......@@ -106,7 +106,7 @@ class Projects::MilestonesController < Projects::ApplicationController
end
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
end
end
......
......@@ -94,7 +94,7 @@ class Projects::SnippetsController < Projects::ApplicationController
end
def module_enabled
return render_404 unless @project.snippets_enabled
return render_404 unless @project.feature_available?(:snippets, current_user)
end
def snippet_params
......
class Projects::TagsController < Projects::ApplicationController
include SortingHelper
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
......@@ -6,8 +8,10 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy]
def index
@sort = params[:sort] || 'name'
@tags = @repository.tags_sorted_by(@sort)
params[:sort] = params[:sort].presence || 'name'
@sort = params[:sort]
@tags = TagsFinder.new(@repository, params).execute
@tags = Kaminari.paginate_array(@tags).page(params[:page])
@releases = project.releases.where(tag: @tags.map(&:name))
......
......@@ -303,13 +303,23 @@ class ProjectsController < Projects::ApplicationController
end
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(
: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,
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
:builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled
:visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
:build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled,
:lfs_enabled, project_feature_attributes
)
end
......
......@@ -64,7 +64,7 @@ class IssuableFinder
if project?
@project = Project.find(params[:project_id])
unless Ability.abilities.allowed?(current_user, :read_project, @project)
unless Ability.allowed?(current_user, :read_project, @project)
@project = nil
end
else
......
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
......@@ -83,7 +83,7 @@ class TodosFinder
if project?
@project = Project.find(params[:project_id])
unless Ability.abilities.allowed?(current_user, :read_project, @project)
unless Ability.allowed?(current_user, :read_project, @project)
@project = nil
end
else
......
......@@ -110,7 +110,7 @@ module ApplicationHelper
project = event.project
# 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
return false if project.merge_requests.where(source_branch: event.branch_name).opened.any?
......
......@@ -25,6 +25,11 @@ module CiStatusHelper
end
end
def ci_status_for_statuseable(subject)
status = subject.try(:status) || 'not found'
status.humanize
end
def ci_icon_for_status(status)
icon_name =
case status
......@@ -41,7 +46,7 @@ module CiStatusHelper
when 'play'
'icon_play'
when 'created'
'icon_status_pending'
'icon_status_created'
else
'icon_status_cancel'
end
......
......@@ -3,7 +3,7 @@ module CompareHelper
from.present? &&
to.present? &&
from != to &&
project.merge_requests_enabled &&
project.feature_available?(:merge_requests, current_user) &&
project.repository.branch_names.include?(from) &&
project.repository.branch_names.include?(to)
end
......
......@@ -149,4 +149,20 @@ module GitlabRoutingHelper
def resend_invite_group_member_path(group_member, *args)
resend_invite_group_group_member_path(group_member.source, group_member)
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
......@@ -43,6 +43,19 @@ module IssuablesHelper
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")
if milestone_title == Milestone::Upcoming.name
milestone_title = Milestone::Upcoming.title
......
......@@ -23,10 +23,14 @@ module LfsHelper
end
def lfs_download_access?
return false unless project.lfs_enabled?
project.public? || ci? || (user && user.can?(:download_code, project))
end
def lfs_upload_access?
return false unless project.lfs_enabled?
user && user.can?(:push_code, project)
end
......
......@@ -98,6 +98,6 @@ module MergeRequestsHelper
end
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
......@@ -25,6 +25,8 @@ module NavHelper
current_path?('merge_requests#commits') ||
current_path?('merge_requests#builds') ||
current_path?('merge_requests#conflicts') ||
current_path?('merge_requests#pipelines') ||
current_path?('issues#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
......
......@@ -61,7 +61,9 @@ module ProjectsHelper
project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" }
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
full_title = "#{namespace_link} / #{project_link}".html_safe
......@@ -187,6 +189,18 @@ module ProjectsHelper
nav_tabs.flatten
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
if current_user
current_user.name
......@@ -400,4 +414,23 @@ module ProjectsHelper
message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
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
module SentryHelper
def sentry_enabled?
Rails.env.production? && current_application_settings.sentry_enabled?
Gitlab::Sentry.enabled?
end
def sentry_context
return unless sentry_enabled?
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
Gitlab::Sentry.context(current_user)
end
end
......@@ -3,6 +3,16 @@ module TagsHelper
"/tags/#{tag}"
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)
html = ''
project.tag_list.each do |tag|
......
......@@ -78,13 +78,11 @@ module TodosHelper
end
def todo_actions_options
actions = [
OpenStruct.new(id: '', title: 'Any Action'),
OpenStruct.new(id: Todo::ASSIGNED, title: 'Assigned'),
OpenStruct.new(id: Todo::MENTIONED, title: 'Mentioned')
[
{ id: '', text: 'Any Action' },
{ id: Todo::ASSIGNED, text: 'Assigned' },
{ id: Todo::MENTIONED, text: 'Mentioned' }
]
options_from_collection_for_select(actions, 'id', 'title', params[:action_id])
end
def todo_projects_options
......@@ -92,22 +90,28 @@ module TodosHelper
projects = projects.includes(:namespace)
projects = projects.map do |project|
OpenStruct.new(id: project.id, title: project.name_with_namespace)
{ id: project.id, text: project.name_with_namespace }
end
projects.unshift(OpenStruct.new(id: '', title: 'Any Project'))
options_from_collection_for_select(projects, 'id', 'title', params[:project_id])
projects.unshift({ id: '', text: 'Any Project' }).to_json
end
def todo_types_options
types = [
OpenStruct.new(title: 'Any Type', name: ''),
OpenStruct.new(title: 'Issue', name: 'Issue'),
OpenStruct.new(title: 'Merge Request', name: 'MergeRequest')
[
{ id: '', text: 'Any Type' },
{ id: 'Issue', text: 'Issue' },
{ 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
private
......
......@@ -9,7 +9,7 @@ class BaseMailer < ActionMailer::Base
default reply_to: Proc.new { default_reply_to_address.format }
def can?
Ability.abilities.allowed?(current_user, action, subject)
Ability.allowed?(current_user, action, subject)
end
private
......
This diff is collapsed.
......@@ -65,8 +65,8 @@ module Ci
end
# ref can't be HEAD or SHA, can only be branch/tag name
scope :latest_successful_for, ->(ref = default_branch) do
where(ref: ref).success.order(id: :desc).limit(1)
def self.latest_successful_for(ref)
where(ref: ref).order(id: :desc).success.first
end
def self.truncate_sha(sha)
......
......@@ -108,15 +108,6 @@ class Commit
@diff_line_count
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.
#
# Usually, the commit title is the first line of the commit message.
......
......@@ -4,12 +4,10 @@
#
# range = CommitRange.new('f3f85602...e86e1013', project)
# range.exclude_start? # => false
# range.reference_title # => "Commits f3f85602 through e86e1013"
# range.to_s # => "f3f85602...e86e1013"
#
# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project)
# range.exclude_start? # => true
# range.reference_title # => "Commits f3f85602^ through e86e1013"
# range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"}
# range.to_s # => "f3f85602..e86e1013"
#
......@@ -109,11 +107,6 @@ class CommitRange
reference
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
#
# See `namespace_project_compare_url`
......
......@@ -2,7 +2,7 @@ module Awardable
extend ActiveSupport::Concern
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
# By default we always load award_emoji user association
......@@ -59,6 +59,18 @@ module Awardable
true
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)
award_emoji.where(name: emoji_name, user: current_user).exists?
end
......
......@@ -196,6 +196,10 @@ module Issuable
end
end
def user_authored?(user)
user == author
end
def subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
......
......@@ -28,4 +28,8 @@ module NoteOnDiff
def can_be_award_emoji?
false
end
def to_discussion
Discussion.new([self])
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
end
# 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
return '' if description.blank?
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
......@@ -107,10 +107,6 @@ class DiffNote < Note
self.noteable.find_diff_discussion(self.discussion_id)
end
def to_discussion
Discussion.new([self])
end
private
def supported?
......
......@@ -65,7 +65,7 @@ class Event < ActiveRecord::Base
elsif created_project?
true
elsif issue? || issue_note?
Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target)
Ability.allowed?(user, :read_issue, note? ? note_target : target)
else
((merge_request? || note?) && target.present?) || milestone?
end
......
......@@ -91,13 +91,13 @@ class MergeRequest < ActiveRecord::Base
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 :target_project, presence: true
validates :target_branch, presence: true
validates :merge_user, presence: true, if: :merge_when_build_succeeds?
validate :validate_branches, unless: [:allow_broken, :importing?]
validate :validate_fork
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork?
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
......@@ -240,12 +240,12 @@ class MergeRequest < ActiveRecord::Base
def source_branch_head
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
def target_branch_head
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
def branch_merge_base_commit
......@@ -305,19 +305,22 @@ class MergeRequest < ActiveRecord::Base
def validate_fork
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
true
else
# If source and target projects are different
# we should check if source project is actually a fork of target project
if source_project.forked_from?(target_project)
true
else
errors.add :validate_fork,
'Source project is not a fork of target project'
'Source project is not a fork of the target project'
end
def closed_without_fork?
closed? && forked_source_project_missing?
end
def forked_source_project_missing?
return false unless for_fork?
return true unless source_project
!source_project.forked_from?(target_project)
end
def ensure_merge_request_diff
......@@ -408,7 +411,7 @@ class MergeRequest < ActiveRecord::Base
def can_remove_source_branch?(current_user)
!source_project.protected_branch?(source_branch) &&
!source_project.root_ref?(source_branch) &&
Ability.abilities.allowed?(current_user, :push_code, source_project) &&
Ability.allowed?(current_user, :push_code, source_project) &&
diff_head_commit == source_branch_head
end
......@@ -726,7 +729,9 @@ class MergeRequest < ActiveRecord::Base
end
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
def all_pipelines
......
......@@ -223,6 +223,10 @@ class Note < ActiveRecord::Base
end
end
def user_authored?(user)
user == author
end
def award_emoji?
can_be_award_emoji? && contains_emoji_only?
end
......
......@@ -11,24 +11,23 @@ class Project < ActiveRecord::Base
include AfterCommitQueue
include CaseSensitivity
include TokenAuthenticatable
include ProjectFeaturesCompatibility
extend Gitlab::ConfigHelper
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 :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(:repository_storage) { current_application_settings.repository_storage }
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
after_create :ensure_dir_exist
after_save :ensure_dir_exist, if: :namespace_id_changed?
after_initialize :setup_project_feature
# set last_activity_at to the same as created_at
after_create :set_last_activity_at
......@@ -62,10 +61,10 @@ class Project < ActiveRecord::Base
belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id'
belongs_to :namespace
has_one :board, dependent: :destroy
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
has_one :board, dependent: :destroy
# Project services
has_many :services
has_one :campfire_service, dependent: :destroy
......@@ -130,6 +129,7 @@ class Project < ActiveRecord::Base
has_many :notification_settings, dependent: :destroy, as: :source
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 :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
......@@ -142,6 +142,7 @@ class Project < ActiveRecord::Base
has_many :deployments, dependent: :destroy
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
......@@ -159,8 +160,6 @@ class Project < ActiveRecord::Base
length: { within: 0..255 },
format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message }
validates :issues_enabled, :merge_requests_enabled,
:wiki_enabled, inclusion: { in: [true, false] }
validates :namespace, presence: true
validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id
......@@ -196,6 +195,9 @@ class Project < ActiveRecord::Base
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
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 :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) }
......@@ -390,6 +392,13 @@ class Project < ActiveRecord::Base
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
Gitlab.config.repositories.storages[repository_storage]
end
......@@ -436,7 +445,7 @@ class Project < ActiveRecord::Base
# ref can't be HEAD, can only be branch/tag name or SHA
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
latest_pipeline.builds.latest.with_artifacts
......@@ -680,6 +689,10 @@ class Project < ActiveRecord::Base
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
end
def has_wiki?
wiki_enabled? || has_external_wiki?
end
def external_wiki
if has_external_wiki.nil?
cache_has_external_wiki # Populate
......@@ -1096,16 +1109,21 @@ class Project < ActiveRecord::Base
!namespace.share_with_group_lock
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)
end
def ensure_pipeline(sha, ref, current_user = nil)
pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref, user: current_user)
def ensure_pipeline(ref, sha, current_user = nil)
pipeline_for(ref, sha) ||
pipelines.create(sha: sha, ref: ref, user: current_user)
end
def enable_ci
self.builds_enabled = true
project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end
def any_runners?(&block)
......@@ -1272,6 +1290,11 @@ class Project < ActiveRecord::Base
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?
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
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
......@@ -120,8 +120,21 @@ class Repository
commits
end
def find_branch(name)
raw_repository.branches.find { |branch| branch.name == name }
def find_branch(name, fresh_repo: true)
# 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
def find_tag(name)
......
......@@ -433,7 +433,7 @@ class User < ActiveRecord::Base
#
# This logic is duplicated from `Ability#project_abilities` into a SQL form.
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
def is_admin?
......@@ -460,16 +460,12 @@ class User < ActiveRecord::Base
can?(:create_group, nil)
end
def abilities
Ability.abilities
end
def can_select_namespace?
several_namespaces? || admin
end
def can?(action, subject)
abilities.allowed?(self, action, subject)
Ability.allowed?(self, action, subject)
end
def first_name
......
class BasePolicy
class RuleSet
attr_reader :can_set, :cannot_set
def initialize(can_set, cannot_set)
@can_set = can_set
@cannot_set = cannot_set
end
def size
to_set.size
end
def self.empty
new(Set.new, Set.new)
end
def can?(ability)
@can_set.include?(ability) && !@cannot_set.include?(ability)
end
def include?(ability)
can?(ability)
end
def to_set
@can_set - @cannot_set
end
def merge(other)
@can_set.merge(other.can_set)
@cannot_set.merge(other.cannot_set)
end
def can!(*abilities)
@can_set.merge(abilities)
end
def cannot!(*abilities)
@cannot_set.merge(abilities)
end
def freeze
@can_set.freeze
@cannot_set.freeze
super
end
end
def self.abilities(user, subject)
new(user, subject).abilities
end
def self.class_for(subject)
return GlobalPolicy if subject.nil?
subject.class.ancestors.each do |klass|
next unless klass.name
begin
policy_class = "#{klass.name}Policy".constantize
# NOTE: the < operator here tests whether policy_class
# inherits from BasePolicy
return policy_class if policy_class < BasePolicy
rescue NameError
nil
end
end
raise "no policy for #{subject.class.name}"
end
attr_reader :user, :subject
def initialize(user, subject)
@user = user
@subject = subject
end
def abilities
return RuleSet.empty if @user && @user.blocked?
return anonymous_abilities if @user.nil?
collect_rules { rules }
end
def anonymous_abilities
collect_rules { anonymous_rules }
end
def anonymous_rules
rules
end
def delegate!(new_subject)
@rule_set.merge(Ability.allowed(@user, new_subject))
end
def can?(rule)
@rule_set.can?(rule)
end
def can!(*rules)
@rule_set.can!(*rules)
end
def cannot!(*rules)
@rule_set.cannot!(*rules)
end
private
def collect_rules(&b)
@rule_set = RuleSet.empty
yield
@rule_set
end
end
module Ci
class BuildPolicy < CommitStatusPolicy
def rules
super
# If we can't read build we should also not have that
# ability when looking at this in context of commit_status
%w(read create update admin).each do |rule|
cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build"
end
end
end
end
module Ci
class RunnerPolicy < BasePolicy
def rules
return unless @user
can! :assign_runner if @user.is_admin?
return if @subject.is_shared? || @subject.locked?
can! :assign_runner if @user.ci_authorized_runners.include?(@subject)
end
end
end
class CommitStatusPolicy < BasePolicy
def rules
delegate! @subject.project
end
end
class DeploymentPolicy < BasePolicy
def rules
delegate! @subject.project
end
end
class EnvironmentPolicy < BasePolicy
def rules
delegate! @subject.project
end
end
class ExternalIssuePolicy < BasePolicy
def rules
delegate! @subject.project
end
end
class GlobalPolicy < BasePolicy
def rules
return unless @user
can! :create_group if @user.can_create_group
can! :read_users_list
end
end
class GroupMemberPolicy < BasePolicy
def rules
return unless @user
target_user = @subject.user
group = @subject.group
return if group.last_owner?(target_user)
can_manage = Ability.allowed?(@user, :admin_group_member, group)
if can_manage
can! :update_group_member
can! :destroy_group_member
elsif @user == target_user
can! :destroy_group_member
end
end
end
class GroupPolicy < BasePolicy
def rules
can! :read_group if @subject.public?
return unless @user
globally_viewable = @subject.public? || (@subject.internal? && !@user.external?)
member = @subject.users.include?(@user)
owner = @user.admin? || @subject.has_owner?(@user)
master = owner || @subject.has_master?(@user)
can_read = false
can_read ||= globally_viewable
can_read ||= member
can_read ||= @user.admin?
can_read ||= GroupProjectsFinder.new(@subject).execute(@user).any?
can! :read_group if can_read
# Only group masters and group owners can create new projects
if master
can! :create_projects
can! :admin_milestones
end
# Only group owner and administrators can admin group
if owner
can! :admin_group
can! :admin_namespace
can! :admin_group_member
can! :change_visibility_level
end
if globally_viewable && @subject.request_access_enabled && !member
can! :request_access
end
end
def can_read_group?
return true if @subject.public?
return true if @user.admin?
return true if @subject.internal? && !@user.external?
return true if @subject.users.include?(@user)
GroupProjectsFinder.new(@subject).execute(@user).any?
end
end
class IssuablePolicy < BasePolicy
def action_name
@subject.class.name.underscore
end
def rules
if @user && (@subject.author == @user || @subject.assignee == @user)
can! :"read_#{action_name}"
can! :"update_#{action_name}"
end
delegate! @subject.project
end
end
class IssuePolicy < IssuablePolicy
def issue
@subject
end
def rules
super
if @subject.confidential? && !can_read_confidential?
cannot! :read_issue
cannot! :admin_issue
cannot! :update_issue
cannot! :read_issue
end
end
private
def can_read_confidential?
return false unless @user
return true if @user.admin?
return true if @subject.author == @user
return true if @subject.assignee == @user
return true if @subject.project.team.member?(@user, Gitlab::Access::REPORTER)
false
end
end
class MergeRequestPolicy < IssuablePolicy
# pass
end
class NamespacePolicy < BasePolicy
def rules
return unless @user
if @subject.owner == @user || @user.admin?
can! :create_projects
can! :admin_namespace
end
end
end
class NotePolicy < BasePolicy
def rules
delegate! @subject.project
return unless @user
if @subject.author == @user
can! :read_note
can! :update_note
can! :admin_note
can! :resolve_note
end
if @subject.for_merge_request? &&
@subject.noteable.author == @user
can! :resolve_note
end
end
end
class PersonalSnippetPolicy < BasePolicy
def rules
can! :read_personal_snippet if @subject.public?
return unless @user
if @subject.author == @user
can! :read_personal_snippet
can! :update_personal_snippet
can! :admin_personal_snippet
end
if @subject.internal? && !@user.external?
can! :read_personal_snippet
end
end
end
class ProjectMemberPolicy < BasePolicy
def rules
# anonymous users have no abilities here
return unless @user
target_user = @subject.user
project = @subject.project
return if target_user == project.owner
can_manage = Ability.allowed?(@user, :admin_project_member, project)
if can_manage
can! :update_project_member
can! :destroy_project_member
end
if @user == target_user
can! :destroy_project_member
end
end
end
class ProjectPolicy < BasePolicy
def rules
team_access!(user)
owner = user.admin? ||
project.owner == user ||
(project.group && project.group.has_owner?(user))
owner_access! if owner
if project.public? || (project.internal? && !user.external?)
guest_access!
public_access!
# Allow to read builds for internal projects
can! :read_build if project.public_builds?
if project.request_access_enabled &&
!(owner || project.team.member?(user) || project_group_member?(user))
can! :request_access
end
end
archived_access! if project.archived?
disabled_features!
end
def project
@subject
end
def guest_access!
can! :read_project
can! :read_board
can! :read_list
can! :read_wiki
can! :read_issue
can! :read_label
can! :read_milestone
can! :read_project_snippet
can! :read_project_member
can! :read_merge_request
can! :read_note
can! :create_project
can! :create_issue
can! :create_note
can! :upload_file
end
def reporter_access!
can! :download_code
can! :fork_project
can! :create_project_snippet
can! :update_issue
can! :admin_issue
can! :admin_label
can! :admin_list
can! :read_commit_status
can! :read_build
can! :read_container_image
can! :read_pipeline
can! :read_environment
can! :read_deployment
end
def developer_access!
can! :admin_merge_request
can! :update_merge_request
can! :create_commit_status
can! :update_commit_status
can! :create_build
can! :update_build
can! :create_pipeline
can! :update_pipeline
can! :create_merge_request
can! :create_wiki
can! :push_code
can! :resolve_note
can! :create_container_image
can! :update_container_image
can! :create_environment
can! :create_deployment
end
def master_access!
can! :push_code_to_protected_branches
can! :update_project_snippet
can! :update_environment
can! :update_deployment
can! :admin_milestone
can! :admin_project_snippet
can! :admin_project_member
can! :admin_merge_request
can! :admin_note
can! :admin_wiki
can! :admin_project
can! :admin_commit_status
can! :admin_build
can! :admin_container_image
can! :admin_pipeline
can! :admin_environment
can! :admin_deployment
end
def public_access!
can! :download_code
can! :fork_project
can! :read_commit_status
can! :read_pipeline
can! :read_container_image
end
def owner_access!
guest_access!
reporter_access!
developer_access!
master_access!
can! :change_namespace
can! :change_visibility_level
can! :rename_project
can! :remove_project
can! :archive_project
can! :remove_fork_project
can! :destroy_merge_request
can! :destroy_issue
end
# Push abilities on the users team role
def team_access!(user)
access = project.team.max_member_access(user.id)
guest_access! if access >= Gitlab::Access::GUEST
reporter_access! if access >= Gitlab::Access::REPORTER
developer_access! if access >= Gitlab::Access::DEVELOPER
master_access! if access >= Gitlab::Access::MASTER
end
def archived_access!
cannot! :create_merge_request
cannot! :push_code
cannot! :push_code_to_protected_branches
cannot! :update_merge_request
cannot! :admin_merge_request
end
def disabled_features!
unless project.feature_available?(:issues, user)
cannot!(*named_abilities(:issue))
end
unless project.feature_available?(:merge_requests, user)
cannot!(*named_abilities(:merge_request))
end
unless project.feature_available?(:issues, user) || project.feature_available?(:merge_requests, user)
cannot!(*named_abilities(:label))
cannot!(*named_abilities(:milestone))
end
unless project.feature_available?(:snippets, user)
cannot!(*named_abilities(:project_snippet))
end
unless project.feature_available?(:wiki, user) || project.has_external_wiki?
cannot!(*named_abilities(:wiki))
end
unless project.feature_available?(:builds, user)
cannot!(*named_abilities(:build))
cannot!(*named_abilities(:pipeline))
cannot!(*named_abilities(:environment))
cannot!(*named_abilities(:deployment))
end
unless project.container_registry_enabled
cannot!(*named_abilities(:container_image))
end
end
def anonymous_rules
return unless project.public?
can! :read_project
can! :read_board
can! :read_list
can! :read_wiki
can! :read_label
can! :read_milestone
can! :read_project_snippet
can! :read_project_member
can! :read_merge_request
can! :read_note
can! :read_pipeline
can! :read_commit_status
can! :read_container_image
can! :download_code
# NOTE: may be overridden by IssuePolicy
can! :read_issue
# Allow to read builds by anonymous user if guests are allowed
can! :read_build if project.public_builds?
disabled_features!
end
def project_group_member?(user)
project.group &&
(
project.group.members.exists?(user_id: user.id) ||
project.group.requesters.exists?(user_id: user.id)
)
end
def named_abilities(name)
[
:"read_#{name}",
:"create_#{name}",
:"update_#{name}",
:"admin_#{name}"
]
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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