Commit 419518df authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into feature/improve-mrwbs-and-todos-for-pipelines

* master: (221 commits)
  Add CHANGELOG entry for 8.12.6
  Added 'Download' button to snippet view
  Merge branch 'api-fix-project-group-sharing' into 'security'
  Add 8.12.5, 8.11.9, and 8.10.12 CHANGELOG entries
  FIx JS bug with select2 because of missing `data-field` attribute in select box.
  Remove pointless `.vagrant_enabled` file
  allow multiple labels commands
  Move some CHANGELOG entries to the 8.13.0 part
  Move operations/ to new location
  Move health check docs under user/admin_area/monitoring
  Make guests unable to view MRs
  Add examples of fake tokens to be used in docs
  Remove duplicate CHANGELOG entry
  Allow browsing branches that end with '.atom'
  Refactor the SubGit/SVN documentation
  Document the new CI_DEBUG_TRACE variable
  Remove redundant images
  changed the scss for the top line connectors to be exactly centered
  Rearrange GitLab basics READMEs
  New images for GitLab basics "Create MR" docs
  ...

Conflicts:
  app/models/commit_status.rb
parents 6f7afaa8 d3a98380
......@@ -19,6 +19,8 @@ variables:
before_script:
- source ./scripts/prepare_build.sh
- cp config/gitlab.yml.example config/gitlab.yml
- mkdir -p tmp/tests
- mount -t tmpfs tmpfs tmp/tests || echo "tmpfs mount failed, falling back to disc"
- bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
- retry gem install knapsack
......
......@@ -2,42 +2,71 @@ Please view this file on the master branch, on stable branches it's out of date.
v 8.13.0 (unreleased)
- Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675)
- Respond with 404 Not Found for non-existent tags (Linus Thiel)
- Truncate long labels with ellipsis in labels page
- Update runner version only when updating contacted_at
- Add link from system note to compare with previous version
- Use gitlab-shell v3.6.2 (GIT TRACE logging)
- Add `/projects/visible` API endpoint (Ben Boeckel)
- Fix centering of custom header logos (Ashley Dumaine)
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
- Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
- Updating verbiage on git basics to be more intuitive
- Clarify documentation for Runners API (Gennady Trafimenkov)
- Change user & group landing page routing from /u/:username to /:username
- Prevent running GfmAutocomplete setup for each diff note !6569
- AbstractReferenceFilter caches project_refs on RequestStore when active
- Replaced the check sign to arrow in the show build view. !6501
- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
- Fix Error 500 when viewing old merge requests with bad diff data
- Speed-up group milestones show page
- Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps)
- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
- Add tag shortcut from the Commit page. !6543
- Keep refs for each deployment
- Allow browsing branches that end with '.atom'
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- Add more tests for calendar contribution (ClemMakesApps)
- Update Gitlab Shell to fix some problems with moving projects between storages
- Cache rendered markdown in the database, rather than Redis
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- Simplify Mentionable concern instance methods
- Fix permission for setting an issue's due date
- API: Multi-file commit !6096 (mahcsig)
- Revert "Label list shows all issues (opened or closed) with that label"
- Expose expires_at field when sharing project on API
- Fix VueJS template tags being rendered in code comments
- Added copy file path button to merge request diff files
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
- Add Issue Board API support (andrebsguedes)
- Allow the Koding integration to be configured through the API
- Add new issue button to each list on Issues Board
- Added soft wrap button to repository file/blob editor
- Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
- Fix todos page mobile viewport layout (ClemMakesApps)
- Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
- Remove redundant mixins (ClemMakesApps)
- Added 'Download' button to the Snippets page (Justin DiPierro)
- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
- Fix that manual jobs would no longer block jobs in the next stage. !6604
- Add configurable email subject suffix (Fu Xu)
- Added tooltip to fork count on project show page. (Justin DiPierro)
- Use a ConnectionPool for Rails.cache on Sidekiq servers
- Replace `alias_method_chain` with `Module#prepend`
- Enable GitLab Import/Export for non-admin users.
- Preserve label filters when sorting !6136 (Joseph Frazier)
- MergeRequest#new form load diff asynchronously
- Only update issuable labels if they have been changed
- Take filters in account in issuable counters. !6496
- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
- Prevent flash alert text from being obscured when container is fluid
- Append issue template to existing description !6149 (Joseph Frazier)
- Trending projects now only show public projects and the list of projects is cached for a day
- Memoize Gitlab Shell's secret token (!6599, Justin DiPierro)
- Revoke button in Applications Settings underlines on hover.
- Use higher size on Gitlab::Redis connection pool on Sidekiq servers
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- Fix Long commit messages overflow viewport in file tree
- Revert avoid touching file system on Build#artifacts?
......@@ -45,22 +74,44 @@ v 8.13.0 (unreleased)
- Add broadcast messages and alerts below sub-nav
- Better empty state for Groups view
- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
- Replace bootstrap caret with fontawesome caret (ClemMakesApps)
- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- Add organization field to user profile
- Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts)
- Fix deploy status responsiveness error !6633
- Make searching for commits case insensitive
- Fix resolved discussion display in side-by-side diff view !6575
- Optimize GitHub importing for speed and memory
- API: expose pipeline data in builds API (!6502, Guilherme Salazar)
- Notify the Merger about merge after successful build (Dimitris Karakasilis)
- Reorder issue and merge request titles to show IDs first. !6503 (Greg Laubenstein)
- Reduce queries needed to find users using their SSH keys when pushing commits
- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
- Fix broken repository 500 errors in project list
- Fix Pipeline list commit column width should be adjusted
- Close todos when accepting merge requests via the API !6486 (tonygambone)
- Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo)
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
- Retouch environments list and deployments list
- Add multiple command support for all label related slash commands !6780 (barthc)
- Add Container Registry on/off status to Admin Area !6638 (the-undefined)
- Allow empty merge requests !6384 (Artem Sidorenko)
- Grouped pipeline dropdown is a scrollable container
v 8.12.5 (unreleased)
- Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi)
- Fix a typo in doc/api/labels.md
- API: all unknown routing will be handled with 404 Not Found
- Make guests unable to view MRs on private projects
v 8.12.6
- Update mailroom to 0.8.1 in Gemfile.lock !6814
v 8.12.5
- Switch from request to env in ::API::Helpers. !6615
- Update the mail_room gem to 0.8.1 to fix a race condition with the mailbox watching thread. !6714
- Improve issue load time performance by avoiding ORDER BY in find_by call. !6724
- Add a new gitlab:users:clear_all_authentication_tokens task. !6745
- Don't send Private-Token (API authentication) headers to Sentry
- Share projects via the API only with groups the authenticated user can access
v 8.12.4
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell)
......@@ -287,6 +338,10 @@ v 8.12.0
- Fix non-master branch readme display in tree view
- Add UX improvements for merge request version diffs
v 8.11.9
- Don't send Private-Token (API authentication) headers to Sentry
- Share projects via the API only with groups the authenticated user can access
v 8.11.8
- Respect the fork_project permission when forking projects
- Set a restrictive CORS policy on the API for credentialed requests
......@@ -512,6 +567,10 @@ v 8.11.0
- Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done
v 8.10.12
- Don't send Private-Token (API authentication) headers to Sentry
- Share projects via the API only with groups the authenticated user can access
v 8.10.11
- Respect the fork_project permission when forking projects
- Set a restrictive CORS policy on the API for credentialed requests
......
......@@ -226,8 +226,7 @@ a feedback issue (if there isn't one already) and leave a comment asking for it
to be marked as `Accepting merge requests`. Please include screenshots or
wireframes if the feature will also change the UI.
Merge requests can be filed either at [GitLab.com][gitlab-mr-tracker] or at
[github.com][github-mr-tracker].
Merge requests should be opened at [GitLab.com][gitlab-mr-tracker].
If you are new to GitLab development (or web development in general), see the
[I want to contribute!](#i-want-to-contribute) section to get you started with
......@@ -246,10 +245,17 @@ tests are least likely to receive timely feedback. The workflow to make a merge
request is as follows:
1. Fork the project into your personal space on GitLab.com
1. Create a feature branch, branch away from `master`.
1. Create a feature branch, branch away from `master`
1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
1. Add your changes to the [CHANGELOG](CHANGELOG)
1. If you are writing documentation, make sure to read the [documentation styleguide][doc-styleguide]
1. Add your changes to the [CHANGELOG](CHANGELOG):
1. If you are fixing a ~regression issue, you can add your entry to the next
patch release (e.g. `8.12.5` if current version is `8.12.4`)
1. Otherwise, add your entry to the next minor release (e.g. `8.13.0` if
current version is `8.12.4`
1. Please add your entry at a random place among the entries of the targeted
release
1. If you are writing documentation, make sure to follow the
[documentation styleguide][doc-styleguide]
1. If you have multiple commits please combine them into one commit by
[squashing them][git-squash]
1. Push the commit(s) to your fork
......@@ -258,7 +264,7 @@ request is as follows:
1. The MR description should give a motive for your change and the method you
used to achieve it, see the [merge request description format]
(#merge-request-description-format)
1. If the MR changes the UI it should include before and after screenshots
1. If the MR changes the UI it should include *Before* and *After* screenshots
1. If the MR changes CSS classes please include the list of affected pages,
`grep css-class ./app -R`
1. Link any relevant [issues][ce-tracker] in the merge request description and
......@@ -270,7 +276,9 @@ request is as follows:
[shell command guidelines](doc/development/shell_commands.md)
1. If your code creates new files on disk please read the
[shared files guidelines](doc/development/shared_files.md).
1. When writing commit messages please follow [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [guidelines](http://chris.beams.io/posts/git-commit/).
1. When writing commit messages please follow
[these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
[guidelines](http://chris.beams.io/posts/git-commit/).
1. If your merge request adds one or more migrations, make sure to execute all
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.
......@@ -305,23 +313,6 @@ Please ensure that your merge request meets the contribution acceptance criteria
When having your code reviewed and when reviewing merge requests please take the
[code review guidelines](doc/development/code_review.md) into account.
### Merge request description format
Please submit merge requests using the following template in the merge request
description area. Copy-paste it to retain the markdown format.
```
## What does this MR do?
## Are there points in the code the reviewer needs to double check?
## Why was this MR needed?
## What are the relevant issue numbers?
## Screenshots (if relevant)
```
### Contribution acceptance criteria
1. The change is as small as possible
......@@ -333,8 +324,8 @@ description area. Copy-paste it to retain the markdown format.
aforementioned failing test
1. Your MR initially contains a single commit (please use `git rebase -i` to
squash commits)
1. Your changes can merge without problems (if not please merge `master`, never
rebase commits pushed to the remote server)
1. Your changes can merge without problems (if not please rebase if you're the
only one working on your feature branch, otherwise, merge `master`)
1. Does not break any existing functionality
1. Fixes one specific issue or implements one specific feature (do not combine
things, send separate merge requests if needed)
......@@ -352,7 +343,10 @@ description area. Copy-paste it to retain the markdown format.
entire line to follow it. This prevents linting tools from generating warnings.
- Don't touch neighbouring lines. As an exception, automatic mass
refactoring modifications may leave style non-compliant.
1. If the merge request adds any new libraries (gems, JavaScript libraries, etc.), they should conform to our [Licensing guidelines][license-finder-doc]. See the instructions in that document for help if your MR fails the "license-finder" test with a "Dependencies that need approval" error.
1. If the merge request adds any new libraries (gems, JavaScript libraries,
etc.), they should conform to our [Licensing guidelines][license-finder-doc].
See the instructions in that document for help if your MR fails the
"license-finder" test with a "Dependencies that need approval" error.
## Changes for Stable Releases
......@@ -468,7 +462,6 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[accepting-mrs-ce]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests
[accepting-mrs-ee]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Accepting+Merge+Requests
[gitlab-mr-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests
[github-mr-tracker]: https://github.com/gitlabhq/gitlabhq/pulls
[gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit
[git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits
[closed-merge-requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
......
......@@ -110,6 +110,7 @@ gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
......@@ -323,7 +324,7 @@ gem 'newrelic_rpm', '~> 3.16'
gem 'octokit', '~> 4.3.0'
gem 'mail_room', '~> 0.8'
gem 'mail_room', '~> 0.8.1'
gem 'email_reply_parser', '~> 0.5.8'
......
......@@ -399,7 +399,7 @@ GEM
systemu (~> 2.6.2)
mail (2.6.4)
mime-types (>= 1.16, < 4)
mail_room (0.8.0)
mail_room (0.8.1)
method_source (0.8.2)
mime-types (2.99.3)
mimemagic (0.3.0)
......@@ -745,6 +745,9 @@ GEM
tilt (2.0.5)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
truncato (0.7.8)
htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1)
turbolinks (2.5.3)
coffee-rails
tzinfo (1.2.2)
......@@ -886,7 +889,7 @@ DEPENDENCIES
license_finder (~> 2.1.0)
licensee (~> 8.0.0)
loofah (~> 2.0.3)
mail_room (~> 0.8)
mail_room (~> 0.8.1)
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
......@@ -971,6 +974,7 @@ DEPENDENCIES
test_after_commit (~> 0.4.2)
thin (~> 1.7.0)
timecop (~> 0.8.0)
truncato (~> 0.7.8)
turbolinks (~> 2.5.0)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
......
# GitLab
[![build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](http://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
......
......@@ -21,16 +21,14 @@
};
Activities.prototype.toggleFilter = function(sender) {
var event_filters, filter;
var filter = sender.attr("id").split("_")[0];
$('.event-filter .active').removeClass("active");
event_filters = $.cookie("event_filter");
filter = sender.attr("id").split("_")[0];
$.cookie("event_filter", (event_filters !== filter ? filter : ""), {
$.cookie("event_filter", filter, {
path: gon.relative_url_root || '/'
});
if (event_filters !== filter) {
return sender.closest('li').toggleClass("active");
}
sender.closest('li').toggleClass("active");
};
return Activities;
......
......@@ -21,7 +21,8 @@
},
data () {
return {
filters: Store.state.filters
filters: Store.state.filters,
showIssueForm: false
};
},
watch: {
......@@ -33,6 +34,11 @@
deep: true
}
},
methods: {
showNewIssueForm() {
this.showIssueForm = !this.showIssueForm;
}
},
ready () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
......
......@@ -8,10 +8,8 @@
data () {
return {
predefinedLabels: [
new ListLabel({ title: 'Development', color: '#5CB85C' }),
new ListLabel({ title: 'Testing', color: '#F0AD4E' }),
new ListLabel({ title: 'Production', color: '#FF5F00' }),
new ListLabel({ title: 'Ready', color: '#FF0000' })
new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
new ListLabel({ title: 'Doing', color: '#5CB85C' })
]
}
},
......
//= require ./board_card
//= require ./board_new_issue
(() => {
const Store = gl.issueBoards.BoardsStore;
......@@ -8,14 +9,16 @@
gl.issueBoards.BoardList = Vue.extend({
components: {
'board-card': gl.issueBoards.BoardCard
'board-card': gl.issueBoards.BoardCard,
'board-new-issue': gl.issueBoards.BoardNewIssue
},
props: {
disabled: Boolean,
list: Object,
issues: Array,
loading: Boolean,
issueLinkBase: String
issueLinkBase: String,
showIssueForm: Boolean
},
data () {
return {
......@@ -73,7 +76,7 @@
group: 'issues',
sort: false,
disabled: this.disabled,
filter: '.board-list-count',
filter: '.board-list-count, .is-disabled',
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
......
(() => {
window.gl = window.gl || {};
gl.issueBoards.BoardNewIssue = Vue.extend({
props: {
list: Object,
showIssueForm: Boolean
},
data() {
return {
title: '',
error: false
};
},
watch: {
showIssueForm () {
this.$els.input.focus();
}
},
methods: {
submit(e) {
e.preventDefault();
if (this.title.trim() === '') return;
this.error = false;
const labels = this.list.label ? [this.list.label] : [];
const issue = new ListIssue({
title: this.title,
labels
});
this.list.newIssue(issue)
.then((data) => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable();
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable();
// Remove the issue
this.list.removeIssue(issue);
// Show error message
this.error = true;
this.showIssueForm = true;
});
this.cancel();
},
cancel() {
this.showIssueForm = false;
this.title = '';
}
}
});
})();
......@@ -21,7 +21,7 @@
fallbackClass: 'is-dragging',
fallbackOnBody: true,
ghostClass: 'is-ghost',
filter: '.has-tooltip',
filter: '.has-tooltip, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
......
......@@ -87,6 +87,17 @@ class List {
});
}
newIssue (issue) {
this.addIssue(issue);
this.issuesSize++;
return gl.boardService.newIssue(this.id, issue)
.then((resp) => {
const data = resp.json();
issue.id = data.iid;
});
}
createIssues (data) {
data.forEach((issueObj) => {
this.addIssue(new ListIssue(issueObj));
......
......@@ -58,4 +58,10 @@ class BoardService {
to_list_id
});
}
newIssue (id, issue) {
return this.issues.save({ id }, {
issue
});
}
};
......@@ -126,6 +126,9 @@
new TreeView();
}
break;
case 'projects:pipelines:show':
new gl.Pipelines();
break;
case 'groups:activity':
new Activities();
break;
......
......@@ -52,37 +52,27 @@
}
}
},
setup: function(input) {
setup: _.debounce(function(input) {
// Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input');
// destroy previous instances
this.destroyAtWho();
// set up instances
this.setupAtWho();
if (this.dataSource) {
if (!this.dataLoading && !this.cachedData) {
this.dataLoading = true;
setTimeout((function(_this) {
return function() {
var fetch;
fetch = _this.fetchData(_this.dataSource);
return fetch.done(function(data) {
_this.dataLoading = false;
return _this.loadData(data);
});
};
// We should wait until initializations are done
// and only trigger the last .setup since
// The previous .dataSource belongs to the previous issuable
// and the last one will have the **proper** .dataSource property
// TODO: Make this a singleton and turn off events when moving to another page
})(this), 1000);
}
if (this.cachedData != null) {
return this.loadData(this.cachedData);
}
if (this.dataSource && !this.dataLoading && !this.cachedData) {
this.dataLoading = true;
return this.fetchData(this.dataSource)
.done((data) => {
this.dataLoading = false;
this.loadData(data);
});
};
if (this.cachedData != null) {
return this.loadData(this.cachedData);
}
},
}, 1000),
setupAtWho: function() {
// Emoji
this.input.atwho({
......
......@@ -738,6 +738,7 @@
return false;
}
if (currentKeyCode === 13 && currentIndex !== -1) {
e.preventDefault();
_this.selectRowAtIndex();
}
};
......
......@@ -38,6 +38,11 @@
gl.utils.getPagePath = function() {
return $('body').data('page').split(':')[0];
};
gl.utils.parseUrl = function (url) {
var parser = document.createElement('a');
parser.href = url;
return parser;
};
return jQuery.timefor = function(time, suffix, expiredLabel) {
var suffixFromNow, timefor;
if (!time) {
......
......@@ -61,6 +61,9 @@
function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {};
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
this.buildsLoaded = this.opts.buildsLoaded || false;
this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this);
......@@ -93,7 +96,7 @@
this.loadCommits($target.attr('href'));
this.expandView();
this.resetViewContainer();
} else if (action === 'diffs') {
} else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href'));
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
this.shrinkView();
......@@ -170,8 +173,9 @@
action = 'notes';
}
this.currentAction = action;
// Remove a trailing '/commits' or '/diffs'
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
// Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
// Append the new action if we're on a tab other than 'notes'
if (action !== 'notes') {
new_state += "/" + action;
......@@ -210,8 +214,13 @@
if (this.diffsLoaded) {
return;
}
// We extract pathname for the current Changes tab anchor href
// some pages like MergeRequestsController#new has query parameters on that anchor
var url = gl.utils.parseUrl(source);
return this._get({
url: (source + ".json") + this._location.search,
url: (url.pathname + ".json") + this._location.search,
success: (function(_this) {
return function(data) {
$('#diffs').html(data.html);
......@@ -223,7 +232,7 @@
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff();
if (_this.diffViewType() === 'parallel' && _this.currentAction === 'diffs') {
if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) {
_this.expandViewContainer();
}
_this.diffsLoaded = true;
......@@ -324,6 +333,10 @@
return $('.inline-parallel-buttons a.active').data('view-type');
};
MergeRequestTabs.prototype.isDiffAction = function(action) {
return action === 'diffs' || action === 'new/diffs'
};
MergeRequestTabs.prototype.expandViewContainer = function() {
var $wrapper = $('.content-wrapper .container-fluid');
if (this.fixedLayoutPref === null) {
......
(function() {
function toggleGraph() {
const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
const $btnText = $(this).find('.toggle-btn-text');
((global) => {
$($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
class Pipelines {
constructor() {
$(document).off('click', '.toggle-pipeline-btn').on('click', '.toggle-pipeline-btn', this.toggleGraph);
this.addMarginToBuildColumns();
}
const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
toggleGraph() {
const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
const $btnText = $(this).find('.toggle-btn-text');
const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
$($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
}
addMarginToBuildColumns() {
const $secondChildBuildNode = $('.build:nth-child(2)');
if ($secondChildBuildNode.length) {
const $firstChildBuildNode = $secondChildBuildNode.prev('.build');
const $multiBuildColumn = $secondChildBuildNode.closest('.stage-column');
const $previousColumn = $multiBuildColumn.prev('.stage-column');
$multiBuildColumn.addClass('left-margin');
$firstChildBuildNode.addClass('left-connector');
$previousColumn.each(function() {
$this = $(this);
if ($('.build', $this).length === 1) $this.addClass('no-margin');
});
}
$('.pipeline-graph').removeClass('hidden');
}
}
$(document).on('click', '.toggle-pipeline-btn', toggleGraph);
})();
global.Pipelines = Pipelines;
})(window.gl || (window.gl = {}));
......@@ -89,7 +89,7 @@ content on the Users#show page.
const action = $target.data('action');
const source = $target.attr('href');
this.setTab(source, action);
return this.setCurrentAction(action);
return this.setCurrentAction(source, action);
}
activateTab(action) {
......@@ -142,14 +142,9 @@ content on the Users#show page.
.toggle(status);
}
setCurrentAction(action) {
const regExp = new RegExp(`\/(${this.actions.join('|')})(\.html)?\/?$`);
let new_state = this._location.pathname;
setCurrentAction(source, action) {
let new_state = source
new_state = new_state.replace(/\/+$/, '');
new_state = new_state.replace(regExp, '');
if (action !== this.defaultAction) {
new_state += `/${action}`;
}
new_state += this._location.search + this._location.hash;
history.replaceState({
turbolinks: true,
......
......@@ -71,8 +71,8 @@
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
};
collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
......
......@@ -4,7 +4,7 @@
width: 40px;
height: 40px;
padding: 0;
@include border-radius($avatar_radius);
border-radius: $avatar_radius;
border: 1px solid rgba(0, 0, 0, .1);
&.avatar-inline {
......@@ -17,7 +17,7 @@
}
&.avatar-tile {
@include border-radius(0);
border-radius: 0;
border: none;
}
......
......@@ -133,7 +133,7 @@
}
.identicon {
@include border-radius(50%);
border-radius: 50%;
}
}
......
@mixin btn-default {
@include border-radius(3px);
border-radius: 3px;
font-size: $gl-font-size;
font-weight: 500;
padding: $gl-vert-padding $gl-btn-padding;
......@@ -8,7 +8,7 @@
&:active {
outline: none;
background-color: $btn-active-gray;
@include box-shadow($gl-btn-active-background);
box-shadow: $gl-btn-active-background;
}
}
......@@ -43,7 +43,7 @@
&:active,
&.active {
@include box-shadow ($gl-btn-active-background);
box-shadow: $gl-btn-active-background;
background-color: $dark;
border-color: $border-dark;
......@@ -194,10 +194,17 @@
pointer-events: none !important;
}
.caret {
.fa-caret-down,
.fa-caret-up {
margin-left: 5px;
}
&.dropdown-toggle {
.fa-caret-down {
margin-left: 3px;
}
}
svg {
height: 15px;
width: 15px;
......@@ -272,7 +279,7 @@
}
.active {
@include box-shadow($gl-btn-active-background);
box-shadow: $gl-btn-active-background;
border: 1px solid #c6cacf !important;
background-color: #e4e7ed !important;
......
.caret {
display: inline-block;
width: 0;
height: 0;
margin-left: 2px;
vertical-align: middle;
border-top: $caret-width-base dashed;
border-right: $caret-width-base solid transparent;
border-left: $caret-width-base solid transparent;
}
.btn-group {
.caret {
margin-left: 0;
}
}
.dropdown {
position: relative;
......
......@@ -73,7 +73,7 @@ label {
}
.form-control {
@include box-shadow(none);
box-shadow: none;
border-radius: 3px;
padding: $gl-vert-padding $gl-input-padding;
}
......@@ -81,10 +81,10 @@ label {
.select-wrapper {
position: relative;
.caret {
.fa-caret-down {
position: absolute;
right: 10px;
top: $gl-padding;
top: 10px;
color: $gray-darkest;
pointer-events: none;
}
......
......@@ -57,6 +57,10 @@ header {
&:hover, &:focus, &:active {
background-color: $background-color;
}
.fa-caret-down {
font-size: 15px;
}
}
.navbar-toggle {
......
......@@ -16,7 +16,7 @@
margin-top: 5px;
}
@include border-radius(3px);
border-radius: 3px;
display: block;
float: left;
margin-right: 10px;
......
......@@ -86,7 +86,7 @@
}
.markdown-area {
@include border-radius(0);
border-radius: 0;
background: #fff;
border: 1px solid #ddd;
min-height: 140px;
......
/**
* Generic mixins
*/
@mixin box-shadow($shadow) {
box-shadow: $shadow;
}
@mixin border-radius($radius) {
border-radius: $radius;
}
/**
* Prefilled mixins
* Mixins with fixed values
......
......@@ -133,5 +133,5 @@
font-size: 20px;
color: #777;
z-index: 100;
@include box-shadow(0 1px 2px #ddd);
box-shadow: 0 1px 2px #ddd;
}
......@@ -21,7 +21,14 @@
padding-right: 10px;
b {
@extend .caret;
display: inline-block;
width: 0;
height: 0;
margin-left: 2px;
vertical-align: middle;
border-top: $caret-width-base dashed;
border-right: $caret-width-base solid transparent;
border-left: $caret-width-base solid transparent;
color: $gray-darkest;
}
}
......@@ -39,8 +46,8 @@
}
.select2-drop {
@include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0);
@include border-radius ($border-radius-default);
box-shadow: rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0;
border-radius: $border-radius-default;
border: none;
min-width: 175px;
}
......@@ -65,7 +72,7 @@
.select2-container-active {
.select2-choice, .select2-choices {
@include box-shadow(none);
box-shadow: none;
}
}
......@@ -75,13 +82,13 @@
outline: 0;
background-image: none;
background-color: $white-dark;
@include box-shadow($gl-btn-active-gradient);
box-shadow: $gl-btn-active-gradient;
}
}
.select2-container-multi {
.select2-choices {
@include border-radius($border-radius-default);
border-radius: $border-radius-default;
border-color: $input-border;
background: none;
......@@ -116,7 +123,7 @@
&.select2-container-active .select2-choices,
&.select2-dropdown-open .select2-choices {
border-color: $border-white-normal;
@include box-shadow($gl-btn-active-gradient);
box-shadow: $gl-btn-active-gradient;
}
}
......@@ -150,7 +157,7 @@
background-repeat: no-repeat;
background-position: right 0 bottom 6px;
border: 1px solid $input-border;
@include border-radius($border-radius-default);
border-radius: $border-radius-default;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus {
......
......@@ -4,7 +4,7 @@
&.page-sidebar-pinned {
.sidebar-wrapper {
@include box-shadow(none);
box-shadow: none;
}
}
......@@ -17,7 +17,7 @@
width: 0;
overflow: hidden;
transition: width $sidebar-transition-duration;
@include box-shadow(2px 0 16px 0 $black-transparent);
box-shadow: 2px 0 16px 0 $black-transparent;
}
}
......@@ -100,7 +100,7 @@
.count {
float: right;
padding: 0 8px;
@include border-radius(6px);
border-radius: 6px;
}
}
......
......@@ -116,7 +116,7 @@
font-size: 13px;
line-height: 1.6em;
overflow-x: auto;
@include border-radius(2px);
border-radius: 2px;
}
p > code {
......
......@@ -17,8 +17,10 @@ $white-normal: #ededed;
$white-dark: #ececec;
$gray-light: #fafafa;
$gray-lighter: #f9f9f9;
$gray-normal: #f5f5f5;
$gray-dark: #ededed;
$gray-darker: #eee;
$gray-darkest: #c9c9c9;
$green-light: #38ae67;
......@@ -33,6 +35,8 @@ $blue-medium-light: #3498cb;
$blue-medium: #2f8ebf;
$blue-medium-dark: #2d86b4;
$blue-light-transparent: rgba(44, 159, 216, 0.05);
$orange-light: #fc8a51;
$orange-normal: #e75e40;
$orange-dark: #ce5237;
......@@ -91,6 +95,7 @@ $table-text-gray: #8f8f8f;
$gl-font-size: 15px;
$gl-title-color: #333;
$gl-text-color: #5c5c5c;
$gl-text-color-light: #8c8c8c;
$gl-text-green: #4a2;
$gl-text-red: #d12f19;
$gl-text-orange: #d90;
......
......@@ -162,6 +162,10 @@ lex
list-style: none;
overflow-y: scroll;
overflow-x: hidden;
&.is-smaller {
height: calc(100% - 185px);
}
}
.board-list-loading {
......@@ -233,3 +237,31 @@ lex
margin-right: 5px;
}
}
.board-new-issue-form {
margin: 5px;
}
.board-issue-count-holder {
margin-top: -3px;
.btn {
line-height: 12px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
.board-issue-count {
padding-right: 10px;
padding-left: 10px;
line-height: 21px;
border-radius: $border-radius-base;
border: 1px solid $border-color;
&.has-btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-width: 1px 0 1px 1px;
}
}
......@@ -50,7 +50,7 @@
.bordered-box {
border: 1px solid $border-color;
@include border-radius($border-radius-default);
border-radius: $border-radius-default;
}
......
.file-editor {
#editor {
border: none;
@include border-radius(0);
border-radius: 0;
height: 500px;
margin: 0;
padding: 0;
......
.environments-container,
.deployments-container {
width: 100%;
overflow: auto;
}
.environments {
.deployment-column {
.avatar {
float: none;
}
}
.commit-title {
margin: 0;
......@@ -9,6 +20,7 @@
width: 12px;
}
.external-url,
.dropdown-new {
color: $table-text-gray;
}
......@@ -21,16 +33,35 @@
}
}
.build-link,
.branch-name {
color: $gl-dark-link-color;
}
.deployment {
.build-column {
.build-link {
color: $gl-dark-link-color;
}
.avatar {
float: none;
}
}
}
}
.table.builds.environments {
min-width: 500px;
.icon-container {
width: 20px;
text-align: center;
}
.branch-commit {
.commit-id {
margin-right: 0;
}
}
}
......@@ -91,7 +91,7 @@
float: right;
border: 1px solid #eee;
padding: 5px;
@include border-radius(5px);
border-radius: 5px;
background: $gray-light;
margin-left: 10px;
top: -6px;
......
.suggest-colors {
margin-top: 5px;
a {
@include border-radius(4px);
border-radius: 4px;
width: 30px;
height: 30px;
display: inline-block;
......@@ -17,7 +17,7 @@
overflow: hidden;
a {
@include border-radius(0);
border-radius: 0;
width: (100% / 7);
margin-right: 0;
margin-bottom: -5px;
......@@ -59,6 +59,13 @@
width: 200px;
margin-bottom: 0;
}
.label {
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
max-width: 100%;
}
}
.label-description {
......
......@@ -73,12 +73,12 @@
height: auto;
&.top {
@include border-radius(5px 5px 0 0);
border-radius: 5px 5px 0 0;
margin-bottom: 0;
}
&.bottom {
@include border-radius(0 0 5px 5px);
border-radius: 0 0 5px 5px;
border-top: 0;
margin-bottom: 20px;
}
......@@ -86,7 +86,7 @@
&.middle {
border-top: 0;
margin-bottom: 0;
@include border-radius(0);
border-radius: 0;
}
&:active, &:focus {
......
......@@ -6,7 +6,7 @@
background: $background-color;
color: $gl-gray;
border: 1px solid $border-color;
@include border-radius(2px);
border-radius: 2px;
form {
margin-bottom: 0;
......@@ -204,6 +204,18 @@
word-break: break-all;
}
.commits-empty {
text-align: center;
h4 {
padding-top: 20px;
padding-bottom: 10px;
}
svg {
width: 230px;
}
}
.mr-list {
.merge-request {
padding: 10px 15px;
......
......@@ -334,7 +334,7 @@ ul.notes {
.add-diff-note {
margin-top: -4px;
@include border-radius(40px);
border-radius: 40px;
background: #fff;
padding: 4px;
font-size: 16px;
......
......@@ -229,9 +229,12 @@
.fa {
color: $table-text-gray;
margin-right: 6px;
font-size: 14px;
}
svg, .fa {
margin-right: 0;
}
}
.btn-remove {
......@@ -272,18 +275,8 @@
.toggle-pipeline-btn {
background-color: $gray-dark;
.caret {
border-top: none;
border-bottom: 4px solid;
}
&.graph-collapsed {
background-color: $white-light;
.caret {
border-bottom: none;
border-top: 4px solid;
}
}
}
......@@ -310,16 +303,41 @@
.stage-column {
display: inline-block;
vertical-align: top;
margin-right: 65px;
&:not(:last-child) {
margin-right: 44px;
}
&.left-margin {
&:not(:first-child) {
margin-left: 44px;
.left-connector {
&::before {
content: '';
position: absolute;
top: 48%;
left: -48px;
border-top: 2px solid $border-color;
width: 48px;
height: 1px;
}
}
}
}
&.no-margin {
margin: 0;
}
li {
list-style: none;
}
.stage-name {
margin-bottom: 15px;
margin: 0 0 15px 10px;
font-weight: bold;
width: 150px;
width: 176px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
......@@ -328,17 +346,23 @@
.build {
border: 1px solid $border-color;
position: relative;
padding: 6px 10px;
padding: 7px 10px 8px;
border-radius: 30px;
width: 150px;
width: 186px;
margin-bottom: 10px;
&:hover {
background-color: $gray-lighter;
.dropdown-menu-toggle {
background-color: transparent;
}
}
&.playable {
background-color: $gray-light;
svg {
height: 12px;
width: 12px;
height: 13px;
width: 20px;
position: relative;
top: 1px;
......@@ -349,10 +373,20 @@
}
.build-content {
width: 130px;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
width: 164px;
.ci-status-icon {
svg {
height: 20px;
width: 20px;
}
}
.ci-status-text {
width: 110px;
width: 135px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
......@@ -363,44 +397,53 @@
}
a {
color: $layout-link-gray;
color: $gl-text-color-light;
text-decoration: none;
&:hover {
.ci-status-text {
text-decoration: underline;
}
}
}
.dropdown-menu-toggle {
border: none;
width: auto;
padding: 0;
color: $layout-link-gray;
color: $gl-text-color-light;
flex-grow: 1;
.ci-status-text {
width: 80px;
max-width: 112px;
width: auto;
}
}
.grouped-pipeline-dropdown {
padding: 8px 0;
width: 200px;
width: 186px;
left: auto;
right: -214px;
right: -197px;
top: -9px;
max-height: 245px;
overflow-y: scroll;
a:hover {
.ci-status-text {
text-decoration: none;
a {
color: $gl-text-color;
padding: 7px 8px 8px;
&:hover {
background-color: $blue-light-transparent;
border-radius: 3px;
.ci-status-text {
text-decoration: none;
}
}
}
svg {
width: 14px;
height: 14px;
}
.ci-status-text {
width: 145px;
width: 112px;
}
.arrow {
......@@ -433,9 +476,10 @@
}
.badge {
background-color: $gray-dark;
color: $layout-link-gray;
background-color: $gray-darker;
color: $gl-text-color-light;
font-weight: normal;
margin-left: $btn-xs-side-margin;
}
}
......@@ -449,10 +493,10 @@
&::after {
content: '';
position: absolute;
top: 50%;
right: -69px;
top: 48%;
right: -48px;
border-top: 2px solid $border-color;
width: 69px;
width: 48px;
height: 1px;
}
}
......@@ -461,25 +505,25 @@
&:not(:first-child) {
&::after, &::before {
content: '';
top: -47px;
top: -49px;
position: absolute;
border-bottom: 2px solid $border-color;
width: 20px;
height: 65px;
width: 25px;
height: 69px;
}
// Right connecting curves
&::after {
right: -20px;
right: -25px;
border-right: 2px solid $border-color;
border-radius: 0 0 15px;
border-radius: 0 0 20px;
}
// Left connecting curves
&::before {
left: -20px;
left: -25px;
border-left: 2px solid $border-color;
border-radius: 0 0 0 15px;
border-radius: 0 0 0 20px;
}
}
......@@ -487,7 +531,7 @@
&:nth-child(2) {
&::after, &::before {
height: 29px;
top: -10px;
top: -9px;
}
.curve {
display: block;
......@@ -545,20 +589,20 @@
width: 21px;
height: 25px;
position: absolute;
top: -29px;
top: -32px;
border-top: 2px solid $border-color;
}
&::after {
left: -39px;
left: -44px;
border-right: 2px solid $border-color;
border-radius: 0 15px;
border-radius: 0 20px;
}
&::before {
right: -39px;
right: -44px;
border-left: 2px solid $border-color;
border-radius: 15px 0 0;
border-radius: 20px 0 0;
}
}
}
......
......@@ -94,7 +94,7 @@
.profile-user-bio {
// Limits the width of the user bio for readability.
max-width: 600px;
margin: 15px auto 0;
margin: 10px auto;
padding: 0 16px;
}
......@@ -213,29 +213,22 @@
}
.user-profile {
.cover-controls a {
margin-left: 5px;
}
.profile-header {
margin: 0 auto;
.avatar-holder {
width: 90px;
display: inline-block;
}
.user-info {
display: inline-block;
text-align: left;
vertical-align: middle;
margin-left: 15px;
.handle {
color: $gl-gray-light;
}
.member-date {
margin-bottom: 4px;
}
margin: 0 auto 10px;
}
}
@media (max-width: $screen-xs-max) {
.cover-block {
padding-top: 20px;
}
......@@ -258,10 +251,6 @@
}
}
.user-profile-nav {
margin-top: 15px;
}
table.u2f-registrations {
th:not(:last-child), td:not(:last-child) {
border-right: solid 1px transparent;
......
......@@ -4,7 +4,7 @@
text-align: center;
.preview {
@include border-radius(4px);
border-radius: 4px;
height: 80px;
margin-bottom: 10px;
......@@ -47,7 +47,7 @@
width: 160px;
img {
@include border-radius(4px);
border-radius: 4px;
max-width: 100%;
}
......
......@@ -354,7 +354,7 @@ a.deploy-project-label {
justify-content: flex-start;
.fork-thumbnail {
@include border-radius($border-radius-base);
border-radius: $border-radius-base;
background-color: $white-light;
border: 1px solid $border-white-light;
height: 202px;
......@@ -371,7 +371,7 @@ a.deploy-project-label {
background-color: $gray-light;
border: 1px solid $gray-dark;
margin: 0 auto;
@include border-radius(50%);
border-radius: 50%;
i {
font-size: 100px;
color: $gray-dark;
......@@ -390,7 +390,7 @@ a.deploy-project-label {
}
img {
@include border-radius(50%);
border-radius: 50%;
max-width: 100px;
}
}
......@@ -496,7 +496,7 @@ pre.light-well {
}
.light-well {
@include border-radius (2px);
border-radius: 2px;
color: #5b6169;
font-size: 13px;
......
......@@ -4,7 +4,7 @@
margin-right: 10px;
border: 1px solid #eee;
white-space: nowrap;
@include border-radius(4px);
border-radius: 4px;
&:hover {
text-decoration: none;
......
......@@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end
def preview
@message = broadcast_message_params[:message]
@broadcast_message = BroadcastMessage.new(broadcast_message_params)
end
protected
......
......@@ -173,7 +173,8 @@ class ApplicationController < ActionController::Base
end
def event_filter
filters = cookies['event_filter'].split(',') if cookies['event_filter'].present?
# Split using comma to maintain backward compatibility Ex/ "filter1,filter2"
filters = cookies['event_filter'].split(',')[0] if cookies['event_filter'].present?
@event_filter ||= EventFilter.new(filters)
end
......
module Ci
class ApplicationController < ::ApplicationController
def self.railtie_helpers_paths
"app/helpers/ci"
end
end
end
module Ci
class LintsController < ApplicationController
class LintsController < ::ApplicationController
before_action :authenticate_user!
def show
......
module Ci
class ProjectsController < Ci::ApplicationController
class ProjectsController < ::ApplicationController
before_action :project
before_action :no_cache, only: [:badge]
before_action :authorize_read_project!, except: [:badge, :index]
......
......@@ -21,8 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def trending
@projects = TrendingProjectsFinder.new.execute
@projects = filter_projects(@projects)
@projects = filter_projects(Project.trending)
@projects = @projects.page(params[:page])
respond_to do |format|
......
class NamespacesController < ApplicationController
skip_before_action :authenticate_user!
def show
namespace = Namespace.find_by(path: params[:id])
if namespace
if namespace.is_a?(Group)
group = namespace
else
user = namespace.owner
end
end
if user
redirect_to user_path(user)
elsif group && can?(current_user, :read_group, group)
redirect_to group_path(group)
elsif current_user.nil?
authenticate_user!
else
render_404
end
end
end
......@@ -2,6 +2,7 @@ module Projects
module Boards
class IssuesController < Boards::ApplicationController
before_action :authorize_read_issue!, only: [:index]
before_action :authorize_create_issue!, only: [:create]
before_action :authorize_update_issue!, only: [:update]
def index
......@@ -9,16 +10,23 @@ module Projects
issues = issues.page(params[:page])
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] }
}),
issues: serialize_as_json(issues),
size: issues.total_count
}
end
def create
list = project.board.lists.find(params[:list_id])
service = ::Boards::Issues::CreateService.new(project, current_user, issue_params)
issue = service.execute(list)
if issue.valid?
render json: serialize_as_json(issue)
else
render json: issue.errors, status: :unprocessable_entity
end
end
def update
service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
......@@ -43,6 +51,10 @@ module Projects
return render_403 unless can?(current_user, :read_issue, project)
end
def authorize_create_issue!
return render_403 unless can?(current_user, :admin_issue, project)
end
def authorize_update_issue!
return render_403 unless can?(current_user, :update_issue, issue)
end
......@@ -54,6 +66,19 @@ module Projects
def move_params
params.permit(:id, :from_list_id, :to_list_id)
end
def issue_params
params.require(:issue).permit(:title).merge(request: request)
end
def serialize_as_json(resource)
resource.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] }
})
end
end
end
end
......@@ -159,7 +159,8 @@ class Projects::IssuesController < Projects::ApplicationController
protected
def issue
@noteable = @issue ||= @project.issues.find_by(iid: params[:id]) || redirect_old
# The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old
end
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
......
......@@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check,
:ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts
:ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines]
......@@ -19,6 +19,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :apply_diff_view_cookie!, only: [:new_diffs]
before_action :build_merge_request, only: [:new, :new_diffs]
# Allow read any merge_request
before_action :authorize_read_merge_request!
......@@ -29,6 +31,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
# Allow modify merge_request
before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :authenticate_user!, only: [:assign_related_issues]
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts]
def index
......@@ -210,29 +214,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def new
apply_diff_view_cookie!
build_merge_request
@noteable = @merge_request
@target_branches = if @merge_request.target_project
@merge_request.target_project.repository.branch_names
else
[]
end
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@diffs = @merge_request.diffs(diff_options) if @merge_request.compare
@diff_notes_disabled = true
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
define_new_vars
end
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
def new_diffs
respond_to do |format|
format.html do
define_new_vars
render "new"
end
format.json do
@diffs = if @merge_request.can_be_created
@merge_request.diffs(diff_options)
else
[]
end
@diff_notes_disabled = true
render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
end
end
end
def create
......@@ -355,6 +356,25 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render layout: false
end
def assign_related_issues
result = MergeRequests::AssignIssuesService.new(project, current_user, merge_request: @merge_request).execute
respond_to do |format|
format.html do
case result[:count]
when 0
flash[:error] = "Failed to assign you issues related to the merge request"
when 1
flash[:notice] = "1 issue has been assigned to you"
else
flash[:notice] = "#{result[:count]} issues have been assigned to you"
end
redirect_to(merge_request_path(@merge_request))
end
end
end
def ci_status
pipeline = @merge_request.pipeline
if pipeline
......@@ -490,6 +510,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController
)
end
def define_new_vars
@noteable = @merge_request
@target_branches = if @merge_request.target_project
@merge_request.target_project.repository.branch_names
else
[]
end
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
end
def invalid_mr
# Render special view for MR with removed target branch
render 'invalid'
......@@ -521,7 +562,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
end
def compared_diff_version
......
......@@ -20,6 +20,8 @@ class Projects::TagsController < Projects::ApplicationController
def show
@tag = @repository.find_tag(params[:id])
return render_404 unless @tag
@release = @project.releases.find_or_initialize_by(tag: @tag.name)
@commit = @repository.commit(@tag.target)
end
......
class SnippetsController < ApplicationController
include ToggleAwardEmoji
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
# Allow read snippet
before_action :authorize_read_snippet!, only: [:show, :raw]
before_action :authorize_read_snippet!, only: [:show, :raw, :download]
# Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update]
......@@ -12,7 +12,7 @@ class SnippetsController < ApplicationController
# Allow destroy snippet
before_action :authorize_admin_snippet!, only: [:destroy]
skip_before_action :authenticate_user!, only: [:index, :show, :raw]
skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download]
layout 'snippets'
respond_to :html
......@@ -75,6 +75,14 @@ class SnippetsController < ApplicationController
)
end
def download
send_data(
@snippet.content,
type: 'text/plain; charset=utf-8',
filename: @snippet.sanitized_file_name
)
end
protected
def snippet
......
# Finder for retrieving public trending projects in a given time range.
class TrendingProjectsFinder
# current_user - The currently logged in User, if any.
# last_months - The number of months to limit the trending data to.
def execute(months_limit = 1)
Rails.cache.fetch(cache_key_for(months_limit), expires_in: 1.day) do
Project.public_only.trending(months_limit.months.ago)
end
end
private
def cache_key_for(months)
"trending_projects/#{months}"
end
end
......@@ -16,7 +16,7 @@ module AppearancesHelper
end
def brand_text
markdown(brand_item.description)
markdown_field(brand_item, :description)
end
def brand_item
......
......@@ -11,18 +11,6 @@ module ApplicationSettingsHelper
current_application_settings.signin_enabled?
end
def extra_sign_in_text
current_application_settings.sign_in_text
end
def after_sign_up_text
current_application_settings.after_sign_up_text
end
def shared_runners_text
current_application_settings.shared_runners_text
end
def user_oauth_applications?
current_application_settings.user_oauth_applications
end
......
......@@ -4,15 +4,18 @@ module AvatarsHelper
user: commit_or_event.author,
user_name: commit_or_event.author_name,
user_email: commit_or_event.author_email,
css_class: 'hidden-xs'
}))
end
def user_avatar(options = {})
avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name]
css_class = options[:css_class] || ''
avatar = image_tag(
avatar_icon(options[:user] || options[:user_email], avatar_size),
class: "avatar has-tooltip hidden-xs s#{avatar_size}",
class: "avatar has-tooltip s#{avatar_size} #{css_class}",
alt: "#{user_name}'s avatar",
title: user_name,
data: { container: 'body' }
......
......@@ -3,7 +3,7 @@ module BroadcastMessagesHelper
return unless message.present?
content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do
icon('bullhorn') << ' ' << render_broadcast_message(message.message)
icon('bullhorn') << ' ' << render_broadcast_message(message)
end
end
......@@ -32,7 +32,7 @@ module BroadcastMessagesHelper
end
end
def render_broadcast_message(message)
Banzai.render(message, pipeline: :broadcast_message).html_safe
def render_broadcast_message(broadcast_message)
Banzai.render_field(broadcast_message, :message).html_safe
end
end
......@@ -15,10 +15,11 @@ module ButtonHelper
#
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard'
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button,
icon('clipboard'),
class: "btn btn-clipboard",
class: "btn #{css_class}",
data: data,
type: :button,
title: "Copy to Clipboard"
......
......@@ -13,14 +13,12 @@ module GitlabMarkdownHelper
def link_to_gfm(body, url, html_options = {})
return "" if body.blank?
escaped_body = if body.start_with?('<img')
body
else
escape_once(body)
end
user = current_user if defined?(current_user)
gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line)
context = {
project: @project,
current_user: (current_user if defined?(current_user)),
pipeline: :single_line,
}
gfm_body = Banzai.render(body, context)
fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
if fragment.children.size == 1 && fragment.children[0].name == 'a'
......@@ -51,17 +49,15 @@ module GitlabMarkdownHelper
context[:project] ||= @project
html = Banzai.render(text, context)
banzai_postprocess(html, context)
end
context.merge!(
current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
requested_path: @path,
project_wiki: @project_wiki,
ref: @ref
)
def markdown_field(object, field)
object = object.for_display if object.respond_to?(:for_display)
return "" unless object.present?
Banzai.post_process(html, context)
html = Banzai.render_field(object, field)
banzai_postprocess(html, object.banzai_render_context(field))
end
def asciidoc(text)
......@@ -196,4 +192,18 @@ module GitlabMarkdownHelper
icon(options[:icon])
end
end
# Calls Banzai.post_process with some common context options
def banzai_postprocess(html, context)
context.merge!(
current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
requested_path: @path,
project_wiki: @project_wiki,
ref: @ref
)
Banzai.post_process(html, context)
end
end
......@@ -113,14 +113,13 @@ module IssuesHelper
end
end
def award_user_list(awards, current_user)
def award_user_list(awards, current_user, limit: 10)
names = awards.map do |award|
award.user == current_user ? 'You' : award.user.name
end
# Take first 9 OR current user + first 9
current_user_name = names.delete('You')
names = names.first(9).insert(0, current_user_name).compact
names = names.insert(0, current_user_name).compact.first(limit)
names << "#{awards.size - names.size} more." if awards.size > names.size
......
......@@ -72,6 +72,19 @@ module MergeRequestsHelper
)
end
def mr_assign_issues_link
issues = MergeRequests::AssignIssuesService.new(@project,
current_user,
merge_request: @merge_request,
closes_issues: mr_closes_issues
).assignable_issues
path = assign_related_issues_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
if issues.present?
pluralize_this_issue = issues.count > 1 ? "these issues" : "this issue"
link_to "Assign yourself to #{pluralize_this_issue}", path, method: :post
end
end
def source_branch_with_namespace(merge_request)
branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch))
......
......@@ -153,8 +153,18 @@ module SearchHelper
search_path(options)
end
# Sanitize html generated after parsing markdown from issue description or comment
def search_md_sanitize(html)
# Sanitize a HTML field for search display. Most tags are stripped out and the
# maximum length is set to 200 characters.
def search_md_sanitize(object, field)
html = markdown_field(object, field)
html = Truncato.truncate(
html,
count_tags: false,
count_tail: false,
max_length: 200
)
# Truncato's filtered_tags and filtered_attributes are not quite the same
sanitize(html, tags: %w(a p ol ul li pre code))
end
end
class AbuseReport < ActiveRecord::Base
include CacheMarkdownField
cache_markdown_field :message, pipeline: :single_line
belongs_to :reporter, class_name: 'User'
belongs_to :user
......@@ -7,6 +11,9 @@ class AbuseReport < ActiveRecord::Base
validates :message, presence: true
validates :user_id, uniqueness: { message: 'has already been reported' }
# For CacheMarkdownField
alias_method :author, :reporter
def remove_user(deleted_by:)
user.block
DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true)
......
class Appearance < ActiveRecord::Base
include CacheMarkdownField
cache_markdown_field :description
validates :title, presence: true
validates :description, presence: true
validates :logo, file_size: { maximum: 1.megabyte }
......
class ApplicationSetting < ActiveRecord::Base
include CacheMarkdownField
include TokenAuthenticatable
add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token
......@@ -17,6 +19,11 @@ class ApplicationSetting < ActiveRecord::Base
serialize :domain_whitelist, Array
serialize :domain_blacklist, Array
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
cache_markdown_field :shared_runners_text, pipeline: :plain_markdown
cache_markdown_field :after_sign_up_text
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
validates :session_expire_delay,
......
class BroadcastMessage < ActiveRecord::Base
include CacheMarkdownField
include Sortable
cache_markdown_field :message, pipeline: :broadcast_message
validates :message, presence: true
validates :starts_at, presence: true
validates :ends_at, presence: true
......
class CommitStatus < ActiveRecord::Base
include HasStatus
include Importable
include AfterCommitQueue
self.table_name = 'ci_builds'
......@@ -85,21 +86,24 @@ class CommitStatus < ActiveRecord::Base
end
after_transition do |commit_status, transition|
commit_status.pipeline.try do |pipeline|
break if transition.loopback?
if commit_status.complete?
PipelineProcessWorker.perform_async(pipeline.id)
return if transition.loopback?
commit_status.run_after_commit do
pipeline.try do |pipeline|
if complete?
PipelineProcessWorker.perform_async(pipeline.id)
else
PipelineUpdateWorker.perform_async(pipeline.id)
end
end
PipelineUpdateWorker.perform_async(pipeline.id)
end
true
end
after_transition any => :failed do |commit_status|
MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status)
commit_status.run_after_commit do
MergeRequests::AddTodoWhenBuildFailsService
.new(pipeline.project, nil).execute(self)
end
end
end
......
# This module takes care of updating cache columns for Markdown-containing
# fields. Use like this in the body of your class:
#
# include CacheMarkdownField
# cache_markdown_field :foo
# cache_markdown_field :bar
# cache_markdown_field :baz, pipeline: :single_line
#
# Corresponding foo_html, bar_html and baz_html fields should exist.
module CacheMarkdownField
# Knows about the relationship between markdown and html field names, and
# stores the rendering contexts for the latter
class FieldData
extend Forwardable
def initialize
@data = {}
end
def_delegators :@data, :[], :[]=
def_delegator :@data, :keys, :markdown_fields
def html_field(markdown_field)
"#{markdown_field}_html"
end
def html_fields
markdown_fields.map {|field| html_field(field) }
end
end
# Dynamic registries don't really work in Rails as it's not guaranteed that
# every class will be loaded, so hardcode the list.
CACHING_CLASSES = %w[
AbuseReport
Appearance
ApplicationSetting
BroadcastMessage
Issue
Label
MergeRequest
Milestone
Namespace
Note
Project
Release
Snippet
]
def self.caching_classes
CACHING_CLASSES.map(&:constantize)
end
extend ActiveSupport::Concern
included do
cattr_reader :cached_markdown_fields do
FieldData.new
end
# Returns the default Banzai render context for the cached markdown field.
def banzai_render_context(field)
raise ArgumentError.new("Unknown field: #{field.inspect}") unless
cached_markdown_fields.markdown_fields.include?(field)
# Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project)
context = cached_markdown_fields[field].merge(project: project)
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
context
end
# Allow callers to look up the cache field name, rather than hardcoding it
def markdown_cache_field_for(field)
raise ArgumentError.new("Unknown field: #{field}") unless
cached_markdown_fields.markdown_fields.include?(field)
cached_markdown_fields.html_field(field)
end
# Always exclude _html fields from attributes (including serialization).
# They contain unredacted HTML, which would be a security issue
alias_method :attributes_before_markdown_cache, :attributes
def attributes
attrs = attributes_before_markdown_cache
cached_markdown_fields.html_fields.each do |field|
attrs.delete(field)
end
attrs
end
end
class_methods do
private
# Specify that a field is markdown. Its rendered output will be cached in
# a corresponding _html field. Any custom rendering options may be provided
# as a context.
def cache_markdown_field(markdown_field, context = {})
raise "Add #{self} to CacheMarkdownField::CACHING_CLASSES" unless
CacheMarkdownField::CACHING_CLASSES.include?(self.to_s)
cached_markdown_fields[markdown_field] = context
html_field = cached_markdown_fields.html_field(markdown_field)
cache_method = "#{markdown_field}_cache_refresh".to_sym
invalidation_method = "#{html_field}_invalidated?".to_sym
define_method(cache_method) do
html = Banzai::Renderer.cacheless_render_field(self, markdown_field)
__send__("#{html_field}=", html)
true
end
# The HTML becomes invalid if any dependent fields change. For now, assume
# author and project invalidate the cache in all circumstances.
define_method(invalidation_method) do
changed_fields = changed_attributes.keys
invalidations = changed_fields & [markdown_field.to_s, "author", "project"]
!invalidations.empty?
end
before_save cache_method, if: invalidation_method
end
end
end
......@@ -6,6 +6,7 @@
#
module Issuable
extend ActiveSupport::Concern
include CacheMarkdownField
include Participable
include Mentionable
include Subscribable
......@@ -13,6 +14,9 @@ module Issuable
include Awardable
included do
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User"
belongs_to :updated_by, class_name: "User"
......
......@@ -68,8 +68,10 @@ class Event < ActiveRecord::Base
true
elsif issue? || issue_note?
Ability.allowed?(user, :read_issue, note? ? note_target : target)
elsif merge_request? || merge_request_note?
Ability.allowed?(user, :read_merge_request, note? ? note_target : target)
else
((merge_request? || note?) && target.present?) || milestone?
milestone?
end
end
......@@ -280,6 +282,10 @@ class Event < ActiveRecord::Base
note? && target && target.for_issue?
end
def merge_request_note?
note? && target && target.for_merge_request?
end
def project_snippet_note?
target.for_snippet?
end
......@@ -335,7 +341,7 @@ class Event < ActiveRecord::Base
# update the project. Only one query should actually perform the update,
# hence we add the extra WHERE clause for last_activity_at.
Project.unscoped.where(id: project_id).
where('last_activity_at > ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago).
where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago).
update_all(last_activity_at: created_at)
end
......
......@@ -4,6 +4,10 @@ class GlobalLabel
delegate :color, :description, to: :@first_label
def for_display
@first_label
end
def self.build_collection(labels)
labels = labels.group_by(&:title)
......
......@@ -4,6 +4,10 @@ class GlobalMilestone
attr_accessor :title, :milestones
alias_attribute :name, :title
def for_display
@first_milestone
end
def self.build_collection(milestones)
milestones = milestones.group_by(&:title)
......@@ -17,6 +21,7 @@ class GlobalMilestone
@title = title
@name = title
@milestones = milestones
@first_milestone = milestones.find {|m| m.description.present? } || milestones.first
end
def safe_title
......
class Label < ActiveRecord::Base
include CacheMarkdownField
include Referable
include Subscribable
......@@ -8,6 +9,8 @@ class Label < ActiveRecord::Base
None = LabelStruct.new('No Label', 'No Label')
Any = LabelStruct.new('Any Label', '')
cache_markdown_field :description, pipeline: :single_line
DEFAULT_COLOR = '#428BCA'
default_value_for :color, DEFAULT_COLOR
......
......@@ -103,7 +103,12 @@ class Member < ActiveRecord::Base
}
if member.request?
::Members::ApproveAccessRequestService.new(source, current_user, id: member.id).execute
::Members::ApproveAccessRequestService.new(
source,
current_user,
id: member.id,
access_level: access_level
).execute
else
member.save
end
......
......@@ -31,7 +31,7 @@ class MergeRequest < ActiveRecord::Base
# Temporary fields to store compare vars
# when creating new merge request
attr_accessor :can_be_created, :compare_commits, :compare
attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
state_machine :state, initial: :opened do
event :close do
......@@ -196,7 +196,7 @@ class MergeRequest < ActiveRecord::Base
end
def diff_size
merge_request_diff.size
diffs(diff_options).size
end
def diff_base_commit
......
......@@ -6,6 +6,9 @@ class MergeRequestDiff < ActiveRecord::Base
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100
# Valid types of serialized diffs allowed by Gitlab::Git::Diff
VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta]
belongs_to :merge_request
state_machine :state, initial: :empty do
......@@ -170,6 +173,15 @@ class MergeRequestDiff < ActiveRecord::Base
private
# Old GitLab implementations may have generated diffs as ["--broken-diff"].
# Avoid an error 500 by ignoring bad elements. See:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/20776
def valid_raw_diff?(raw)
return false unless raw.respond_to?(:each)
raw.any? { |element| VALID_CLASSES.include?(element.class) }
end
def dump_commits(commits)
commits.map(&:to_hash)
end
......@@ -200,7 +212,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def load_diffs(raw, options)
if raw.respond_to?(:each)
if valid_raw_diff?(raw)
if paths = options[:paths]
raw = raw.select do |diff|
paths.include?(diff[:old_path]) || paths.include?(diff[:new_path])
......
......@@ -6,12 +6,16 @@ class Milestone < ActiveRecord::Base
Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
include CacheMarkdownField
include InternalId
include Sortable
include Referable
include StripAttribute
include Milestoneish
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
belongs_to :project
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
......
class Namespace < ActiveRecord::Base
acts_as_paranoid
include CacheMarkdownField
include Sortable
include Gitlab::ShellAdapter
cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy
belongs_to :owner, class_name: "User"
......@@ -58,15 +61,13 @@ class Namespace < ActiveRecord::Base
def clean_path(path)
path = path.dup
# Get the email username by removing everything after an `@` sign.
path.gsub!(/@.*\z/, "")
# Usernames can't end in .git, so remove it.
path.gsub!(/\.git\z/, "")
# Remove dashes at the start of the username.
path.gsub!(/\A-+/, "")
# Remove periods at the end of the username.
path.gsub!(/\.+\z/, "")
path.gsub!(/@.*\z/, "")
# Remove everything that's not in the list of allowed characters.
path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
# Remove trailing violations ('.atom', '.git', or '.')
path.gsub!(/(\.atom|\.git|\.)*\z/, "")
# Remove leading violations ('-')
path.gsub!(/\A\-+/, "")
# Users with the great usernames of "." or ".." would end up with a blank username.
# Work around that by setting their username to "blank", followed by a counter.
......
......@@ -6,10 +6,13 @@ class Note < ActiveRecord::Base
include Awardable
include Importable
include FasterCacheKeys
include CacheMarkdownField
cache_markdown_field :note, pipeline: :note
# Attribute containing rendered and redacted Markdown as generated by
# Banzai::ObjectRenderer.
attr_accessor :note_html
attr_accessor :redacted_note_html
# An Array containing the number of visible references as generated by
# Banzai::ObjectRenderer
......
......@@ -6,6 +6,7 @@ class Project < ActiveRecord::Base
include Gitlab::VisibilityLevel
include Gitlab::CurrentSettings
include AccessRequestable
include CacheMarkdownField
include Referable
include Sortable
include AfterCommitQueue
......@@ -17,6 +18,8 @@ class Project < ActiveRecord::Base
UNKNOWN_IMPORT_URL = 'http://unknown.git'
cache_markdown_field :description, pipeline: :description
delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
default_value_for :archived, false
......@@ -372,19 +375,9 @@ class Project < ActiveRecord::Base
%r{(?<project>#{name_pattern}/#{name_pattern})}
end
def trending(since = 1.month.ago)
# By counting in the JOIN we don't expose the GROUP BY to the outer query.
# This means that calls such as "any?" and "count" just return a number of
# the total count, instead of the counts grouped per project as a Hash.
join_body = "INNER JOIN (
SELECT project_id, COUNT(*) AS amount
FROM notes
WHERE created_at >= #{sanitize(since)}
AND system IS FALSE
GROUP BY project_id
) join_note_counts ON projects.id = join_note_counts.project_id"
joins(join_body).reorder('join_note_counts.amount DESC')
def trending
joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id').
reorder('trending_projects.id ASC')
end
def cached_count
......
......@@ -10,7 +10,7 @@ class ProjectGroupLink < ActiveRecord::Base
belongs_to :group
validates :project_id, presence: true
validates :group_id, presence: true
validates :group, presence: true
validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" }
validates :group_access, presence: true
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
......
class Release < ActiveRecord::Base
include CacheMarkdownField
cache_markdown_field :description
belongs_to :project
validates :description, :project, :tag, presence: true
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment