Merge remote-tracking branch 'upstream/master' into smart-pipeline-duration
* upstream/master: (289 commits) Fix a typo Change minimum Unicorns required to two Update memory requirements Change the inline code to codeblocks for the new features doc guideline Update CHANGELOG with 8.11.4 entries. removed null return - renamed 'placeTop' to 'placeProfileAvatarsToTop' Change widths of content in MR pipeline tab Add curve to generic commit status pipeline Rubocop syntax 2.3 Some minor updates for upgrade guides for 8.12. Remove inconsistent font weight for sidebar's labels Replace play icon font with svg Project tools visibility level Added todo filter tests Fixed project filtering Review changes, simplified dropdown init Removed select2 from todos feature spec Removed inline JS and improved dropdown labels Added type and action dropdowns, need to finalize by removing all inline and polishing off the selected dropdown states Completed project filter dropdown, still need to move it from inline to ProjectSelect.js (or different) ...
This commit is contained in:
commit
245103e888
|
|
@ -0,0 +1,44 @@
|
|||
### Summary
|
||||
|
||||
(Summarize the bug encountered concisely)
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
(How one can reproduce the issue - this is very important)
|
||||
|
||||
### Expected behavior
|
||||
|
||||
(What you should see instead)
|
||||
|
||||
### Actual behaviour
|
||||
|
||||
(What actually happens)
|
||||
|
||||
### Relevant logs and/or screenshots
|
||||
|
||||
(Paste any relevant logs - please use code blocks (```) to format console output,
|
||||
logs, and code as it's very hard to read otherwise.)
|
||||
|
||||
### Output of checks
|
||||
|
||||
#### Results of GitLab application Check
|
||||
|
||||
(For installations with omnibus-gitlab package run and paste the output of:
|
||||
`sudo gitlab-rake gitlab:check SANITIZE=true`)
|
||||
|
||||
(For installations from source run and paste the output of:
|
||||
`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`)
|
||||
|
||||
(we will only investigate if the tests are passing)
|
||||
|
||||
#### Results of GitLab environment info
|
||||
|
||||
(For installations with omnibus-gitlab package run and paste the output of:
|
||||
`sudo gitlab-rake gitlab:env:info`)
|
||||
|
||||
(For installations from source run and paste the output of:
|
||||
`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
|
||||
|
||||
### Possible fixes
|
||||
|
||||
(If you can, link to the line of code that might be responsible for the problem)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
### Description
|
||||
|
||||
(Include problem, use cases, benefits, and/or goals)
|
||||
|
||||
### Proposal
|
||||
|
||||
### Links / references
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
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
|
||||
|
|
|
|||
61
CHANGELOG
61
CHANGELOG
|
|
@ -1,28 +1,58 @@
|
|||
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)
|
||||
- Change pipeline duration to be jobs running time instead of simple wall time from start to end !6084
|
||||
- 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)
|
||||
|
|
@ -31,19 +61,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
|
||||
- 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.3 (unreleased)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ request that potentially fixes it.
|
|||
|
||||
### Feature proposals
|
||||
|
||||
To create a feature proposal for CE and CI, open an issue on the
|
||||
To create a feature proposal for CE, open an issue on the
|
||||
[issue tracker of CE][ce-tracker].
|
||||
|
||||
For feature proposals for EE, open an issue on the
|
||||
|
|
@ -144,16 +144,7 @@ code snippet right after your description in a new line: `~"feature proposal"`.
|
|||
Please keep feature proposals as small and simple as possible, complex ones
|
||||
might be edited to make them small and simple.
|
||||
|
||||
You are encouraged to use the template below for feature proposals.
|
||||
|
||||
```
|
||||
## Description
|
||||
Include problem, use cases, benefits, and/or goals
|
||||
|
||||
## Proposal
|
||||
|
||||
## Links / references
|
||||
```
|
||||
Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker.
|
||||
|
||||
For changes in the interface, it can be helpful to create a mockup first.
|
||||
If you want to create something yourself, consider opening an issue first to
|
||||
|
|
@ -166,55 +157,11 @@ submitting your own, there's a good chance somebody else had the same issue or
|
|||
feature proposal. Show your support with an award emoji and/or join the
|
||||
discussion.
|
||||
|
||||
Please submit bugs using the following template in the issue description area.
|
||||
Please submit bugs using the ['Bug' issue template](.gitlab/issue_templates/Bug.md) provided on the issue tracker.
|
||||
The text in the parenthesis is there to help you with what to include. Omit it
|
||||
when submitting the actual issue. You can copy-paste it and then edit as you
|
||||
see fit.
|
||||
|
||||
```
|
||||
## Summary
|
||||
|
||||
(Summarize your issue in one sentence - what goes wrong, what did you expect to happen)
|
||||
|
||||
## Steps to reproduce
|
||||
|
||||
(How one can reproduce the issue - this is very important)
|
||||
|
||||
## Expected behavior
|
||||
|
||||
(What you should see instead)
|
||||
|
||||
## Relevant logs and/or screenshots
|
||||
|
||||
(Paste any relevant logs - please use code blocks (```) to format console output,
|
||||
logs, and code as it's very hard to read otherwise.)
|
||||
|
||||
## Output of checks
|
||||
|
||||
### Results of GitLab Application Check
|
||||
|
||||
(For installations with omnibus-gitlab package run and paste the output of:
|
||||
sudo gitlab-rake gitlab:check SANITIZE=true)
|
||||
|
||||
(For installations from source run and paste the output of:
|
||||
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true)
|
||||
|
||||
(we will only investigate if the tests are passing)
|
||||
|
||||
### Results of GitLab Environment Info
|
||||
|
||||
(For installations with omnibus-gitlab package run and paste the output of:
|
||||
sudo gitlab-rake gitlab:env:info)
|
||||
|
||||
(For installations from source run and paste the output of:
|
||||
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production)
|
||||
|
||||
## Possible fixes
|
||||
|
||||
(If you can, link to the line of code that might be responsible for the problem)
|
||||
|
||||
```
|
||||
|
||||
### Issue weight
|
||||
|
||||
Issue weight allows us to get an idea of the amount of work required to solve
|
||||
|
|
@ -340,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
|
||||
|
|
|
|||
5
Gemfile
5
Gemfile
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
(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,14 +94,20 @@ class List {
|
|||
}
|
||||
|
||||
addIssue (issue, listFrom) {
|
||||
this.issues.push(issue);
|
||||
if (!this.findIssue(issue.id)) {
|
||||
this.issues.push(issue);
|
||||
|
||||
if (this.label) {
|
||||
issue.addLabel(this.label);
|
||||
}
|
||||
if (this.label) {
|
||||
issue.addLabel(this.label);
|
||||
}
|
||||
|
||||
if (listFrom) {
|
||||
gl.boardService.moveIssue(issue.id, listFrom.id, this.id);
|
||||
if (listFrom) {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@
|
|||
break;
|
||||
case 'labels':
|
||||
switch (path[2]) {
|
||||
case 'new':
|
||||
case 'edit':
|
||||
new Labels();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@
|
|||
}
|
||||
});
|
||||
} else {
|
||||
return elements.show();
|
||||
return elements.show().removeClass('option-hidden');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -190,9 +190,9 @@
|
|||
|
||||
currentIndex = -1;
|
||||
|
||||
NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link, .option-hidden';
|
||||
NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
|
||||
|
||||
SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ")";
|
||||
SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
|
||||
|
||||
CURSOR_SELECT_SCROLL_PADDING = 5
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
@ -565,10 +565,6 @@
|
|||
} else {
|
||||
field.remove();
|
||||
}
|
||||
if (this.options.toggleLabel) {
|
||||
this.updateLabel(selectedObject, el, this);
|
||||
}
|
||||
return selectedObject;
|
||||
} else if (el.hasClass(INDETERMINATE_CLASS)) {
|
||||
el.addClass(ACTIVE_CLASS);
|
||||
el.removeClass(INDETERMINATE_CLASS);
|
||||
|
|
@ -578,7 +574,6 @@
|
|||
if (!field.length && fieldName) {
|
||||
this.addInput(fieldName, value, selectedObject);
|
||||
}
|
||||
return selectedObject;
|
||||
} else {
|
||||
if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
|
||||
this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
|
||||
|
|
@ -590,9 +585,6 @@
|
|||
field.remove();
|
||||
}
|
||||
el.addClass(ACTIVE_CLASS);
|
||||
if (this.options.toggleLabel) {
|
||||
this.updateLabel(selectedObject, el, this);
|
||||
}
|
||||
if (value != null) {
|
||||
if (!field.length && fieldName) {
|
||||
this.addInput(fieldName, value, selectedObject);
|
||||
|
|
@ -600,8 +592,14 @@
|
|||
field.val(value).trigger('change');
|
||||
}
|
||||
}
|
||||
return selectedObject;
|
||||
}
|
||||
|
||||
// Update label right after input has been added
|
||||
if (this.options.toggleLabel) {
|
||||
this.updateLabel(selectedObject, el, this);
|
||||
}
|
||||
|
||||
return selectedObject;
|
||||
};
|
||||
|
||||
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
|
||||
|
|
|
|||
|
|
@ -164,7 +164,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) {
|
||||
|
|
|
|||
|
|
@ -1,54 +1,12 @@
|
|||
(function() {
|
||||
var clearHighlights, currentTimer, defaultClass, delay, firstPiece, pieceIndex, pieces, start, stop, work;
|
||||
|
||||
Turbolinks.enableProgressBar();
|
||||
|
||||
defaultClass = 'tanuki-shape';
|
||||
$(document).on('page:fetch', function() {
|
||||
$('.tanuki-logo').addClass('animate');
|
||||
});
|
||||
|
||||
pieces = ['path#tanuki-right-cheek', 'path#tanuki-right-eye, path#tanuki-right-ear', 'path#tanuki-nose', 'path#tanuki-left-eye, path#tanuki-left-ear', 'path#tanuki-left-cheek'];
|
||||
|
||||
pieceIndex = 0;
|
||||
|
||||
firstPiece = pieces[0];
|
||||
|
||||
currentTimer = null;
|
||||
|
||||
delay = 150;
|
||||
|
||||
clearHighlights = function() {
|
||||
return $("." + defaultClass + ".highlight").attr('class', defaultClass);
|
||||
};
|
||||
|
||||
start = function() {
|
||||
clearHighlights();
|
||||
pieceIndex = 0;
|
||||
if (pieces[0] !== firstPiece) {
|
||||
pieces.reverse();
|
||||
}
|
||||
if (currentTimer) {
|
||||
clearInterval(currentTimer);
|
||||
}
|
||||
return currentTimer = setInterval(work, delay);
|
||||
};
|
||||
|
||||
stop = function() {
|
||||
clearInterval(currentTimer);
|
||||
return clearHighlights();
|
||||
};
|
||||
|
||||
work = function() {
|
||||
clearHighlights();
|
||||
$(pieces[pieceIndex]).attr('class', defaultClass + " highlight");
|
||||
if (pieceIndex === pieces.length - 1) {
|
||||
pieceIndex = 0;
|
||||
return pieces.reverse();
|
||||
} else {
|
||||
return pieceIndex++;
|
||||
}
|
||||
};
|
||||
|
||||
$(document).on('page:fetch', start);
|
||||
|
||||
$(document).on('page:change', stop);
|
||||
$(document).on('page:change', function() {
|
||||
$('.tanuki-logo').removeClass('animate');
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
|
|
|
|||
|
|
@ -75,10 +75,8 @@ class MergeConflictResolver {
|
|||
window.location.href = data.redirect_to;
|
||||
})
|
||||
.error(() => {
|
||||
new Flash('Something went wrong!');
|
||||
})
|
||||
.always(() => {
|
||||
this.vue.isSubmitting = false;
|
||||
new Flash('Something went wrong!');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
(function() {
|
||||
this.User = (function() {
|
||||
function User(opts) {
|
||||
this.opts = opts;
|
||||
$('.profile-groups-avatars').tooltip({
|
||||
"placement": "top"
|
||||
});
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
User.prototype.initTabs = function() {
|
||||
return new UserTabs({
|
||||
parentEl: '.user-profile',
|
||||
action: this.opts.action
|
||||
});
|
||||
};
|
||||
|
||||
return User;
|
||||
|
||||
})();
|
||||
|
||||
}).call(this);
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
(global => {
|
||||
global.User = class {
|
||||
constructor(opts) {
|
||||
this.opts = opts;
|
||||
this.placeProfileAvatarsToTop();
|
||||
this.initTabs();
|
||||
this.hideProjectLimitMessage();
|
||||
}
|
||||
|
||||
placeProfileAvatarsToTop() {
|
||||
$('.profile-groups-avatars').tooltip({
|
||||
placement: 'top'
|
||||
});
|
||||
}
|
||||
|
||||
initTabs() {
|
||||
return new UserTabs({
|
||||
parentEl: '.user-profile',
|
||||
action: this.opts.action
|
||||
});
|
||||
}
|
||||
|
||||
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 = {}));
|
||||
|
|
@ -24,6 +24,7 @@
|
|||
@import "framework/issue_box.scss";
|
||||
@import "framework/jquery.scss";
|
||||
@import "framework/lists.scss";
|
||||
@import "framework/logo.scss";
|
||||
@import "framework/markdown_area.scss";
|
||||
@import "framework/mobile.scss";
|
||||
@import "framework/modal.scss";
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -2,16 +2,6 @@
|
|||
* Application Header
|
||||
*
|
||||
*/
|
||||
@mixin tanuki-logo-colors($path-color) {
|
||||
fill: $path-color;
|
||||
transition: all 0.8s;
|
||||
|
||||
&:hover,
|
||||
&.highlight {
|
||||
fill: lighten($path-color, 25%);
|
||||
transition: all 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
transition: padding $sidebar-transition-duration;
|
||||
|
|
@ -25,7 +15,7 @@ header {
|
|||
margin: 8px 0;
|
||||
text-align: center;
|
||||
|
||||
#tanuki-logo, img {
|
||||
.tanuki-logo, img {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
|
@ -94,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;
|
||||
|
|
@ -146,6 +136,8 @@ header {
|
|||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
padding-right: 20px;
|
||||
margin: 0;
|
||||
font-size: 19px;
|
||||
max-width: 400px;
|
||||
|
|
@ -158,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;
|
||||
}
|
||||
|
||||
|
|
@ -170,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;
|
||||
|
|
@ -205,26 +205,6 @@ header {
|
|||
}
|
||||
}
|
||||
|
||||
#tanuki-logo {
|
||||
|
||||
#tanuki-left-ear,
|
||||
#tanuki-right-ear,
|
||||
#tanuki-nose {
|
||||
@include tanuki-logo-colors($tanuki-red);
|
||||
}
|
||||
|
||||
#tanuki-left-eye,
|
||||
#tanuki-right-eye {
|
||||
@include tanuki-logo-colors($tanuki-orange);
|
||||
}
|
||||
|
||||
#tanuki-left-cheek,
|
||||
#tanuki-right-cheek {
|
||||
@include tanuki-logo-colors($tanuki-yellow);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
header .container-fluid {
|
||||
font-size: 18px;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
@mixin unique-keyframes {
|
||||
$animation-name: unique-id();
|
||||
@include webkit-prefix(animation-name, $animation-name);
|
||||
|
||||
@-webkit-keyframes #{$animation-name} {
|
||||
@content;
|
||||
}
|
||||
@keyframes #{$animation-name} {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin tanuki-logo-colors($path-color) {
|
||||
fill: $path-color;
|
||||
transition: all 0.8s;
|
||||
|
||||
&:hover {
|
||||
fill: lighten($path-color, 25%);
|
||||
transition: all 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin tanuki-second-highlight-animations($tanuki-color) {
|
||||
@include unique-keyframes {
|
||||
10%, 80% {
|
||||
fill: #{$tanuki-color}
|
||||
}
|
||||
20%, 90% {
|
||||
fill: lighten($tanuki-color, 25%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin tanuki-forth-highlight-animations($tanuki-color) {
|
||||
@include unique-keyframes {
|
||||
30%, 60% {
|
||||
fill: #{$tanuki-color};
|
||||
}
|
||||
40%, 70% {
|
||||
fill: lighten($tanuki-color, 25%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tanuki-logo {
|
||||
|
||||
.tanuki-left-ear,
|
||||
.tanuki-right-ear,
|
||||
.tanuki-nose {
|
||||
@include tanuki-logo-colors($tanuki-red);
|
||||
}
|
||||
|
||||
.tanuki-left-eye,
|
||||
.tanuki-right-eye {
|
||||
@include tanuki-logo-colors($tanuki-orange);
|
||||
}
|
||||
|
||||
.tanuki-left-cheek,
|
||||
.tanuki-right-cheek {
|
||||
@include tanuki-logo-colors($tanuki-yellow);
|
||||
}
|
||||
|
||||
&.animate {
|
||||
.tanuki-shape {
|
||||
@include webkit-prefix(animation-duration, 1.5s);
|
||||
@include webkit-prefix(animation-iteration-count, infinite);
|
||||
}
|
||||
|
||||
.tanuki-left-cheek {
|
||||
@include unique-keyframes {
|
||||
0%, 10%, 100% {
|
||||
fill: lighten($tanuki-yellow, 25%);
|
||||
}
|
||||
90% {
|
||||
fill: $tanuki-yellow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tanuki-left-eye {
|
||||
@include tanuki-second-highlight-animations($tanuki-orange);
|
||||
}
|
||||
|
||||
.tanuki-left-ear {
|
||||
@include tanuki-second-highlight-animations($tanuki-red);
|
||||
}
|
||||
|
||||
.tanuki-nose {
|
||||
@include unique-keyframes {
|
||||
20%, 70% {
|
||||
fill: $tanuki-red;
|
||||
}
|
||||
30%, 80% {
|
||||
fill: lighten($tanuki-red, 25%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tanuki-right-eye {
|
||||
@include tanuki-forth-highlight-animations($tanuki-orange);
|
||||
}
|
||||
|
||||
.tanuki-right-ear {
|
||||
@include tanuki-forth-highlight-animations($tanuki-red);
|
||||
}
|
||||
|
||||
.tanuki-right-cheek {
|
||||
@include unique-keyframes {
|
||||
40% {
|
||||
fill: $tanuki-yellow;
|
||||
}
|
||||
60% {
|
||||
fill: lighten($tanuki-yellow, 25%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,43 +9,11 @@
|
|||
border-radius: $radius;
|
||||
}
|
||||
|
||||
@mixin border-radius-left($radius) {
|
||||
@include border-radius($radius 0 0 $radius)
|
||||
}
|
||||
|
||||
@mixin border-radius-right($radius) {
|
||||
@include border-radius(0 0 $radius $radius)
|
||||
}
|
||||
|
||||
@mixin linear-gradient($from, $to) {
|
||||
background-image: -webkit-gradient(linear, 0 0, 0 100%, from($from), to($to));
|
||||
background-image: -webkit-linear-gradient($from, $to);
|
||||
background-image: -moz-linear-gradient($from, $to);
|
||||
background-image: -ms-linear-gradient($from, $to);
|
||||
background-image: -o-linear-gradient($from, $to);
|
||||
}
|
||||
|
||||
@mixin transition($transition) {
|
||||
-webkit-transition: $transition;
|
||||
-moz-transition: $transition;
|
||||
-ms-transition: $transition;
|
||||
-o-transition: $transition;
|
||||
transition: $transition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefilled mixins
|
||||
* Mixins with fixed values
|
||||
*/
|
||||
|
||||
@mixin shade {
|
||||
@include box-shadow(0 0 3px #ddd);
|
||||
}
|
||||
|
||||
@mixin solid-shade {
|
||||
@include box-shadow(0 0 0 3px #f1f1f1);
|
||||
}
|
||||
|
||||
@mixin str-truncated($max_width: 82%) {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
|
|
@ -94,23 +62,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
@mixin input-big {
|
||||
height: 36px;
|
||||
padding: 5px 10px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: #7f8fa4;
|
||||
background-color: #fff;
|
||||
border-color: #e7e9ed;
|
||||
}
|
||||
|
||||
@mixin btn-big {
|
||||
height: 36px;
|
||||
padding: 5px 10px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
@mixin bulleted-list {
|
||||
> ul {
|
||||
list-style-type: disc;
|
||||
|
|
@ -129,3 +80,8 @@
|
|||
color: rgba(255, 255, 255, 0.3);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
@mixin webkit-prefix($property, $value) {
|
||||
#{'-webkit-' + $property}: $value;
|
||||
#{$property}: $value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -161,6 +158,7 @@
|
|||
> .dropdown {
|
||||
margin-right: $gl-padding-top;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
|
|
@ -210,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;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
.is-dragging {
|
||||
// Important because plugin sets inline CSS
|
||||
opacity: 1!important;
|
||||
|
||||
|
||||
* {
|
||||
// !important to make sure no style can override this when dragging
|
||||
cursor: -webkit-grabbing!important;
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -115,11 +115,8 @@
|
|||
}
|
||||
|
||||
&.commits-stat {
|
||||
margin-top: 3px;
|
||||
display: block;
|
||||
padding: 3px;
|
||||
padding-left: 0;
|
||||
|
||||
padding: 0 3px 0 0;
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -319,6 +324,14 @@
|
|||
|
||||
a {
|
||||
color: $layout-link-gray;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
.ci-status-text {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -461,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,18 +608,25 @@ pre.light-well {
|
|||
}
|
||||
}
|
||||
|
||||
.project-show-readme .readme-holder {
|
||||
padding: $gl-padding 0;
|
||||
border-top: 0;
|
||||
|
||||
.edit-project-readme {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
.project-show-readme {
|
||||
.row-content-block {
|
||||
background-color: inherit;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.wiki h1 {
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
.readme-holder {
|
||||
padding: $gl-padding 0;
|
||||
border-top: 0;
|
||||
|
||||
.edit-project-readme {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wiki h1 {
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
awardable.toggle_award_emoji(name, current_user)
|
||||
TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
|
||||
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 }
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
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
|
||||
if artifacts_file.file_storage?
|
||||
send_file artifacts_file.path, disposition: 'attachment'
|
||||
else
|
||||
redirect_to artifacts_file.url
|
||||
end
|
||||
|
||||
send_file artifacts_file.path, disposition: 'attachment'
|
||||
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(
|
||||
only: [:iid, :title, :confidential],
|
||||
include: {
|
||||
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
|
||||
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
|
||||
})
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
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
|
||||
|
|
@ -66,10 +71,10 @@ module CiStatusHelper
|
|||
Ci::Runner.shared.blank?
|
||||
end
|
||||
|
||||
def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '')
|
||||
def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '', container: 'body')
|
||||
klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
|
||||
title = "#{type.titleize}: #{ci_label_for_status(status)}"
|
||||
data = { toggle: 'tooltip', placement: tooltip_placement }
|
||||
data = { toggle: 'tooltip', placement: tooltip_placement, container: container }
|
||||
|
||||
if path
|
||||
link_to ci_icon_for_status(status), path,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -49,6 +49,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
|
||||
|
|
|
|||
|
|
@ -1,27 +1,9 @@
|
|||
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
|
||||
|
||||
options_from_collection_for_select(types, 'name', 'title', params[:type])
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,34 +1,5 @@
|
|||
class Ability
|
||||
class << self
|
||||
# rubocop: disable Metrics/CyclomaticComplexity
|
||||
def allowed(user, subject)
|
||||
return anonymous_abilities(user, subject) if user.nil?
|
||||
return [] unless user.is_a?(User)
|
||||
return [] if user.blocked?
|
||||
|
||||
abilities_by_subject_class(user: user, subject: subject)
|
||||
end
|
||||
|
||||
def abilities_by_subject_class(user:, subject:)
|
||||
case subject
|
||||
when CommitStatus then commit_status_abilities(user, subject)
|
||||
when Project then project_abilities(user, subject)
|
||||
when Issue then issue_abilities(user, subject)
|
||||
when Note then note_abilities(user, subject)
|
||||
when ProjectSnippet then project_snippet_abilities(user, subject)
|
||||
when PersonalSnippet then personal_snippet_abilities(user, subject)
|
||||
when MergeRequest then merge_request_abilities(user, subject)
|
||||
when Group then group_abilities(user, subject)
|
||||
when Namespace then namespace_abilities(user, subject)
|
||||
when GroupMember then group_member_abilities(user, subject)
|
||||
when ProjectMember then project_member_abilities(user, subject)
|
||||
when User then user_abilities
|
||||
when ExternalIssue, Deployment, Environment then project_abilities(user, subject.project)
|
||||
when Ci::Runner then runner_abilities(user, subject)
|
||||
else []
|
||||
end.concat(global_abilities(user))
|
||||
end
|
||||
|
||||
# Given a list of users and a project this method returns the users that can
|
||||
# read the given project.
|
||||
def users_that_can_read_project(users, project)
|
||||
|
|
@ -61,359 +32,7 @@ class Ability
|
|||
issues.select { |issue| issue.visible_to_user?(user) }
|
||||
end
|
||||
|
||||
# List of possible abilities for anonymous user
|
||||
def anonymous_abilities(user, subject)
|
||||
if subject.is_a?(PersonalSnippet)
|
||||
anonymous_personal_snippet_abilities(subject)
|
||||
elsif subject.is_a?(ProjectSnippet)
|
||||
anonymous_project_snippet_abilities(subject)
|
||||
elsif subject.is_a?(CommitStatus)
|
||||
anonymous_commit_status_abilities(subject)
|
||||
elsif subject.is_a?(Project) || subject.respond_to?(:project)
|
||||
anonymous_project_abilities(subject)
|
||||
elsif subject.is_a?(Group) || subject.respond_to?(:group)
|
||||
anonymous_group_abilities(subject)
|
||||
elsif subject.is_a?(User)
|
||||
anonymous_user_abilities
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def anonymous_project_abilities(subject)
|
||||
project = if subject.is_a?(Project)
|
||||
subject
|
||||
else
|
||||
subject.project
|
||||
end
|
||||
|
||||
if project && project.public?
|
||||
rules = [
|
||||
:read_project,
|
||||
:read_board,
|
||||
:read_list,
|
||||
:read_wiki,
|
||||
:read_label,
|
||||
:read_milestone,
|
||||
:read_project_snippet,
|
||||
:read_project_member,
|
||||
:read_merge_request,
|
||||
:read_note,
|
||||
:read_pipeline,
|
||||
:read_commit_status,
|
||||
:read_container_image,
|
||||
:download_code
|
||||
]
|
||||
|
||||
# Allow to read builds by anonymous user if guests are allowed
|
||||
rules << :read_build if project.public_builds?
|
||||
|
||||
# Allow to read issues by anonymous user if issue is not confidential
|
||||
rules << :read_issue unless subject.is_a?(Issue) && subject.confidential?
|
||||
|
||||
rules - project_disabled_features_rules(project)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def anonymous_commit_status_abilities(subject)
|
||||
rules = anonymous_project_abilities(subject.project)
|
||||
# If subject is Ci::Build which inherits from CommitStatus filter the abilities
|
||||
rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build)
|
||||
rules
|
||||
end
|
||||
|
||||
def anonymous_group_abilities(subject)
|
||||
rules = []
|
||||
|
||||
group = if subject.is_a?(Group)
|
||||
subject
|
||||
else
|
||||
subject.group
|
||||
end
|
||||
|
||||
rules << :read_group if group.public?
|
||||
|
||||
rules
|
||||
end
|
||||
|
||||
def anonymous_personal_snippet_abilities(snippet)
|
||||
if snippet.public?
|
||||
[:read_personal_snippet]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def anonymous_project_snippet_abilities(snippet)
|
||||
if snippet.public?
|
||||
[:read_project_snippet]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def anonymous_user_abilities
|
||||
[:read_user] unless restricted_public_level?
|
||||
end
|
||||
|
||||
def global_abilities(user)
|
||||
rules = []
|
||||
rules << :create_group if user.can_create_group
|
||||
rules << :read_users_list
|
||||
rules
|
||||
end
|
||||
|
||||
def project_abilities(user, project)
|
||||
key = "/user/#{user.id}/project/#{project.id}"
|
||||
|
||||
if RequestStore.active?
|
||||
RequestStore.store[key] ||= uncached_project_abilities(user, project)
|
||||
else
|
||||
uncached_project_abilities(user, project)
|
||||
end
|
||||
end
|
||||
|
||||
def uncached_project_abilities(user, project)
|
||||
rules = []
|
||||
# Push abilities on the users team role
|
||||
rules.push(*project_team_rules(project.team, user))
|
||||
|
||||
owner = user.admin? ||
|
||||
project.owner == user ||
|
||||
(project.group && project.group.has_owner?(user))
|
||||
|
||||
if owner
|
||||
rules.push(*project_owner_rules)
|
||||
end
|
||||
|
||||
if project.public? || (project.internal? && !user.external?)
|
||||
rules.push(*public_project_rules)
|
||||
|
||||
# Allow to read builds for internal projects
|
||||
rules << :read_build if project.public_builds?
|
||||
|
||||
unless owner || project.team.member?(user) || project_group_member?(project, user)
|
||||
rules << :request_access if project.request_access_enabled
|
||||
end
|
||||
end
|
||||
|
||||
if project.archived?
|
||||
rules -= project_archived_rules
|
||||
end
|
||||
|
||||
(rules - project_disabled_features_rules(project)).uniq
|
||||
end
|
||||
|
||||
def project_team_rules(team, user)
|
||||
# Rules based on role in project
|
||||
if team.master?(user)
|
||||
project_master_rules
|
||||
elsif team.developer?(user)
|
||||
project_dev_rules
|
||||
elsif team.reporter?(user)
|
||||
project_report_rules
|
||||
elsif team.guest?(user)
|
||||
project_guest_rules
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def public_project_rules
|
||||
@public_project_rules ||= project_guest_rules + [
|
||||
:download_code,
|
||||
:fork_project,
|
||||
:read_commit_status,
|
||||
:read_pipeline,
|
||||
:read_container_image
|
||||
]
|
||||
end
|
||||
|
||||
def project_guest_rules
|
||||
@project_guest_rules ||= [
|
||||
:read_project,
|
||||
:read_wiki,
|
||||
:read_issue,
|
||||
:read_board,
|
||||
:read_list,
|
||||
:read_label,
|
||||
:read_milestone,
|
||||
:read_project_snippet,
|
||||
:read_project_member,
|
||||
:read_merge_request,
|
||||
:read_note,
|
||||
:create_project,
|
||||
:create_issue,
|
||||
:create_note,
|
||||
:upload_file
|
||||
]
|
||||
end
|
||||
|
||||
def project_report_rules
|
||||
@project_report_rules ||= project_guest_rules + [
|
||||
:download_code,
|
||||
:fork_project,
|
||||
:create_project_snippet,
|
||||
:update_issue,
|
||||
:admin_issue,
|
||||
:admin_label,
|
||||
:admin_list,
|
||||
:read_commit_status,
|
||||
:read_build,
|
||||
:read_container_image,
|
||||
:read_pipeline,
|
||||
:read_environment,
|
||||
:read_deployment
|
||||
]
|
||||
end
|
||||
|
||||
def project_dev_rules
|
||||
@project_dev_rules ||= project_report_rules + [
|
||||
:admin_merge_request,
|
||||
:update_merge_request,
|
||||
:create_commit_status,
|
||||
:update_commit_status,
|
||||
:create_build,
|
||||
:update_build,
|
||||
:create_pipeline,
|
||||
:update_pipeline,
|
||||
:create_merge_request,
|
||||
:create_wiki,
|
||||
:push_code,
|
||||
:resolve_note,
|
||||
:create_container_image,
|
||||
:update_container_image,
|
||||
:create_environment,
|
||||
:create_deployment
|
||||
]
|
||||
end
|
||||
|
||||
def project_archived_rules
|
||||
@project_archived_rules ||= [
|
||||
:create_merge_request,
|
||||
:push_code,
|
||||
:push_code_to_protected_branches,
|
||||
:update_merge_request,
|
||||
:admin_merge_request
|
||||
]
|
||||
end
|
||||
|
||||
def project_master_rules
|
||||
@project_master_rules ||= project_dev_rules + [
|
||||
:push_code_to_protected_branches,
|
||||
:update_project_snippet,
|
||||
:update_environment,
|
||||
:update_deployment,
|
||||
:admin_milestone,
|
||||
:admin_project_snippet,
|
||||
:admin_project_member,
|
||||
:admin_merge_request,
|
||||
:admin_note,
|
||||
:admin_wiki,
|
||||
:admin_project,
|
||||
:admin_commit_status,
|
||||
:admin_build,
|
||||
:admin_container_image,
|
||||
:admin_pipeline,
|
||||
:admin_environment,
|
||||
:admin_deployment
|
||||
]
|
||||
end
|
||||
|
||||
def project_owner_rules
|
||||
@project_owner_rules ||= project_master_rules + [
|
||||
:change_namespace,
|
||||
:change_visibility_level,
|
||||
:rename_project,
|
||||
:remove_project,
|
||||
:archive_project,
|
||||
:remove_fork_project,
|
||||
:destroy_merge_request,
|
||||
:destroy_issue
|
||||
]
|
||||
end
|
||||
|
||||
def project_disabled_features_rules(project)
|
||||
rules = []
|
||||
|
||||
unless project.issues_enabled
|
||||
rules += named_abilities('issue')
|
||||
end
|
||||
|
||||
unless project.merge_requests_enabled
|
||||
rules += named_abilities('merge_request')
|
||||
end
|
||||
|
||||
unless project.issues_enabled or project.merge_requests_enabled
|
||||
rules += named_abilities('label')
|
||||
rules += named_abilities('milestone')
|
||||
end
|
||||
|
||||
unless project.snippets_enabled
|
||||
rules += named_abilities('project_snippet')
|
||||
end
|
||||
|
||||
unless project.wiki_enabled
|
||||
rules += named_abilities('wiki')
|
||||
end
|
||||
|
||||
unless project.builds_enabled
|
||||
rules += named_abilities('build')
|
||||
rules += named_abilities('pipeline')
|
||||
rules += named_abilities('environment')
|
||||
rules += named_abilities('deployment')
|
||||
end
|
||||
|
||||
unless project.container_registry_enabled
|
||||
rules += named_abilities('container_image')
|
||||
end
|
||||
|
||||
rules
|
||||
end
|
||||
|
||||
def group_abilities(user, group)
|
||||
rules = []
|
||||
rules << :read_group if can_read_group?(user, group)
|
||||
|
||||
owner = user.admin? || group.has_owner?(user)
|
||||
master = owner || group.has_master?(user)
|
||||
|
||||
# Only group masters and group owners can create new projects
|
||||
if master
|
||||
rules += [
|
||||
:create_projects,
|
||||
:admin_milestones
|
||||
]
|
||||
end
|
||||
|
||||
# Only group owner and administrators can admin group
|
||||
if owner
|
||||
rules += [
|
||||
:admin_group,
|
||||
:admin_namespace,
|
||||
:admin_group_member,
|
||||
:change_visibility_level
|
||||
]
|
||||
end
|
||||
|
||||
if group.public? || (group.internal? && !user.external?)
|
||||
rules << :request_access if group.request_access_enabled && group.users.exclude?(user)
|
||||
end
|
||||
|
||||
rules.flatten
|
||||
end
|
||||
|
||||
def can_read_group?(user, group)
|
||||
return true if user.admin?
|
||||
return true if group.public?
|
||||
return true if group.internal? && !user.external?
|
||||
return true if group.users.include?(user)
|
||||
|
||||
GroupProjectsFinder.new(group).execute(user).any?
|
||||
end
|
||||
|
||||
# TODO: make this private and use the actual abilities stuff for this
|
||||
def can_edit_note?(user, note)
|
||||
return false if !note.editable? || !user.present?
|
||||
return true if note.author == user || user.admin?
|
||||
|
|
@ -426,207 +45,23 @@ class Ability
|
|||
end
|
||||
end
|
||||
|
||||
def namespace_abilities(user, namespace)
|
||||
rules = []
|
||||
|
||||
# Only namespace owner and administrators can admin it
|
||||
if namespace.owner == user || user.admin?
|
||||
rules += [
|
||||
:create_projects,
|
||||
:admin_namespace
|
||||
]
|
||||
end
|
||||
|
||||
rules.flatten
|
||||
def allowed?(user, action, subject)
|
||||
allowed(user, subject).include?(action)
|
||||
end
|
||||
|
||||
[:issue, :merge_request].each do |name|
|
||||
define_method "#{name}_abilities" do |user, subject|
|
||||
rules = []
|
||||
def allowed(user, subject)
|
||||
return uncached_allowed(user, subject) unless RequestStore.active?
|
||||
|
||||
if subject.author == user || (subject.respond_to?(:assignee) && subject.assignee == user)
|
||||
rules += [
|
||||
:"read_#{name}",
|
||||
:"update_#{name}",
|
||||
]
|
||||
end
|
||||
|
||||
rules += project_abilities(user, subject.project)
|
||||
rules = filter_confidential_issues_abilities(user, subject, rules) if subject.is_a?(Issue)
|
||||
rules
|
||||
end
|
||||
end
|
||||
|
||||
def note_abilities(user, note)
|
||||
rules = []
|
||||
|
||||
if note.author == user
|
||||
rules += [
|
||||
:read_note,
|
||||
:update_note,
|
||||
:admin_note,
|
||||
:resolve_note
|
||||
]
|
||||
end
|
||||
|
||||
if note.respond_to?(:project) && note.project
|
||||
rules += project_abilities(user, note.project)
|
||||
end
|
||||
|
||||
if note.for_merge_request? && note.noteable.author == user
|
||||
rules << :resolve_note
|
||||
end
|
||||
|
||||
rules
|
||||
end
|
||||
|
||||
def personal_snippet_abilities(user, snippet)
|
||||
rules = []
|
||||
|
||||
if snippet.author == user
|
||||
rules += [
|
||||
:read_personal_snippet,
|
||||
:update_personal_snippet,
|
||||
:admin_personal_snippet
|
||||
]
|
||||
end
|
||||
|
||||
if snippet.public? || (snippet.internal? && !user.external?)
|
||||
rules << :read_personal_snippet
|
||||
end
|
||||
|
||||
rules
|
||||
end
|
||||
|
||||
def project_snippet_abilities(user, snippet)
|
||||
rules = []
|
||||
|
||||
if snippet.author == user || user.admin?
|
||||
rules += [
|
||||
:read_project_snippet,
|
||||
:update_project_snippet,
|
||||
:admin_project_snippet
|
||||
]
|
||||
end
|
||||
|
||||
if snippet.public? || (snippet.internal? && !user.external?) || (snippet.private? && snippet.project.team.member?(user))
|
||||
rules << :read_project_snippet
|
||||
end
|
||||
|
||||
rules
|
||||
end
|
||||
|
||||
def group_member_abilities(user, subject)
|
||||
rules = []
|
||||
target_user = subject.user
|
||||
group = subject.group
|
||||
|
||||
unless group.last_owner?(target_user)
|
||||
can_manage = group_abilities(user, group).include?(:admin_group_member)
|
||||
|
||||
if can_manage
|
||||
rules << :update_group_member
|
||||
rules << :destroy_group_member
|
||||
elsif user == target_user
|
||||
rules << :destroy_group_member
|
||||
end
|
||||
end
|
||||
|
||||
rules
|
||||
end
|
||||
|
||||
def project_member_abilities(user, subject)
|
||||
rules = []
|
||||
target_user = subject.user
|
||||
project = subject.project
|
||||
|
||||
unless target_user == project.owner
|
||||
can_manage = project_abilities(user, project).include?(:admin_project_member)
|
||||
|
||||
if can_manage
|
||||
rules << :update_project_member
|
||||
rules << :destroy_project_member
|
||||
elsif user == target_user
|
||||
rules << :destroy_project_member
|
||||
end
|
||||
end
|
||||
|
||||
rules
|
||||
end
|
||||
|
||||
def commit_status_abilities(user, subject)
|
||||
rules = project_abilities(user, subject.project)
|
||||
# If subject is Ci::Build which inherits from CommitStatus filter the abilities
|
||||
rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build)
|
||||
rules
|
||||
end
|
||||
|
||||
def filter_build_abilities(rules)
|
||||
# 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|
|
||||
rules.delete(:"#{rule}_commit_status") unless rules.include?(:"#{rule}_build")
|
||||
end
|
||||
rules
|
||||
end
|
||||
|
||||
def runner_abilities(user, runner)
|
||||
if user.is_admin?
|
||||
[:assign_runner]
|
||||
elsif runner.is_shared? || runner.locked?
|
||||
[]
|
||||
elsif user.ci_authorized_runners.include?(runner)
|
||||
[:assign_runner]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def user_abilities
|
||||
[:read_user]
|
||||
end
|
||||
|
||||
def abilities
|
||||
@abilities ||= begin
|
||||
abilities = Six.new
|
||||
abilities << self
|
||||
abilities
|
||||
end
|
||||
user_key = user ? user.id : 'anonymous'
|
||||
subject_key = subject ? "#{subject.class.name}/#{subject.id}" : 'global'
|
||||
key = "/ability/#{user_key}/#{subject_key}"
|
||||
RequestStore[key] ||= uncached_allowed(user, subject).freeze
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def restricted_public_level?
|
||||
current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
|
||||
end
|
||||
|
||||
def named_abilities(name)
|
||||
[
|
||||
:"read_#{name}",
|
||||
:"create_#{name}",
|
||||
:"update_#{name}",
|
||||
:"admin_#{name}"
|
||||
]
|
||||
end
|
||||
|
||||
def filter_confidential_issues_abilities(user, issue, rules)
|
||||
return rules if user.admin? || !issue.confidential?
|
||||
|
||||
unless issue.author == user || issue.assignee == user || issue.project.team.member?(user, Gitlab::Access::REPORTER)
|
||||
rules.delete(:admin_issue)
|
||||
rules.delete(:read_issue)
|
||||
rules.delete(:update_issue)
|
||||
end
|
||||
|
||||
rules
|
||||
end
|
||||
|
||||
def project_group_member?(project, user)
|
||||
project.group &&
|
||||
(
|
||||
project.group.members.exists?(user_id: user.id) ||
|
||||
project.group.requesters.exists?(user_id: user.id)
|
||||
)
|
||||
def uncached_allowed(user, subject)
|
||||
BasePolicy.class_for(subject).abilities(user, subject)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
# 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'
|
||||
end
|
||||
end
|
||||
errors.add :validate_fork,
|
||||
'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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
class CommitStatusPolicy < BasePolicy
|
||||
def rules
|
||||
delegate! @subject.project
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
class DeploymentPolicy < BasePolicy
|
||||
def rules
|
||||
delegate! @subject.project
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
class EnvironmentPolicy < BasePolicy
|
||||
def rules
|
||||
delegate! @subject.project
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
class ExternalIssuePolicy < BasePolicy
|
||||
def rules
|
||||
delegate! @subject.project
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
class GlobalPolicy < BasePolicy
|
||||
def rules
|
||||
return unless @user
|
||||
|
||||
can! :create_group if @user.can_create_group
|
||||
can! :read_users_list
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue