Commit 25e078c1 authored by James Lopez's avatar James Lopez

Merge branches 'feature/project-export' and 'feature/project-import' of...

Merge branches 'feature/project-export' and 'feature/project-import' of gitlab.com:gitlab-org/gitlab-ce into feature/project-import
parents 37c68fe0 ba1fcf36
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
.sass-cache/ .sass-cache/
.secret .secret
.vagrant .vagrant
.byebug_history
Vagrantfile Vagrantfile
backups/* backups/*
config/aws.yml config/aws.yml
......
...@@ -12,16 +12,18 @@ cache: ...@@ -12,16 +12,18 @@ cache:
variables: variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1" MYSQL_ALLOW_EMPTY_PASSWORD: "1"
# retry tests only in CI environment
RSPEC_RETRY_RETRY_COUNT: "3"
before_script: before_script:
- source ./scripts/prepare_build.sh - source ./scripts/prepare_build.sh
- ruby -v - ruby -v
- which ruby - which ruby
- gem install bundler --no-ri --no-rdoc - retry gem install bundler --no-ri --no-rdoc
- cp config/gitlab.yml.example config/gitlab.yml - cp config/gitlab.yml.example config/gitlab.yml
- touch log/application.log - touch log/application.log
- touch log/test.log - touch log/test.log
- bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"
- RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate - RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate
stages: stages:
...@@ -69,15 +71,6 @@ spec:services: ...@@ -69,15 +71,6 @@ spec:services:
- ruby - ruby
- mysql - mysql
spec:benchmark:
stage: test
script:
- RAILS_ENV=test bundle exec rake spec:benchmark
tags:
- ruby
- mysql
allow_failure: true
spec:other: spec:other:
stage: test stage: test
script: script:
...@@ -241,22 +234,6 @@ spec:services:ruby22: ...@@ -241,22 +234,6 @@ spec:services:ruby22:
- ruby - ruby
- mysql - mysql
spec:benchmark:ruby22:
stage: test
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test bundle exec rake spec:benchmark
cache:
key: "ruby22"
paths:
- vendor
tags:
- ruby
- mysql
allow_failure: true
spec:other:ruby22: spec:other:ruby22:
stage: test stage: test
image: ruby:2.2 image: ruby:2.2
...@@ -330,4 +307,4 @@ notify:slack: ...@@ -330,4 +307,4 @@ notify:slack:
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
- tags@gitlab-org/gitlab-ce - tags@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee - master@gitlab-org/gitlab-ee
- tags@gitlab-org/gitlab-ee - tags@gitlab-org/gitlab-ee
\ No newline at end of file
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.6.0 (unreleased) v 8.6.0 (unreleased)
- Support Golang subpackage fetching (Stan Hu)
- Contributions to forked projects are included in calendar - Contributions to forked projects are included in calendar
- Improve the formatting for the user page bio (Connor Shea) - Improve the formatting for the user page bio (Connor Shea)
- Removed the default password from the initial admin account created during
setup. A password can be provided during setup (see installation docs), or
GitLab will ask the user to create a new one upon first visit.
- Fix issue when pushing to projects ending in .wiki - Fix issue when pushing to projects ending in .wiki
- Fix avatar stretching by providing a cropping feature (Johann Pardanaud) - Fix avatar stretching by providing a cropping feature (Johann Pardanaud)
- Don't load all of GitLab in mail_room - Don't load all of GitLab in mail_room
- Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
- Memoize @group in Admin::GroupsController (Yatish Mehta)
- Indicate how much an MR diverged from the target branch (Pierre de La Morinerie) - Indicate how much an MR diverged from the target branch (Pierre de La Morinerie)
- Strip leading and trailing spaces in URL validator (evuez) - Strip leading and trailing spaces in URL validator (evuez)
- Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez)
- Return empty array instead of 404 when commit has no statuses in commit status API - Return empty array instead of 404 when commit has no statuses in commit status API
- Decrease the font size and the padding of the `.anchor` icons used in the README (Roberto Dip)
- Rewrite logo to simplify SVG code (Sean Lang)
- Refactor and greatly improve search performance
- Add support for cross-project label references - Add support for cross-project label references
- Update documentation to reflect Guest role not being enforced on internal projects - Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users - Allow search for logged out users
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
- Don't show Issues/MRs from archived projects in Groups view - Don't show Issues/MRs from archived projects in Groups view
- Increase the notes polling timeout over time (Roberto Dip) - Increase the notes polling timeout over time (Roberto Dip)
- Add shortcut to toggle markdown preview (Florent Baldino)
- Show labels in dashboard and group milestone views
- Add main language of a project in the list of projects (Tiago Botelho)
- Add ability to show archived projects on dashboard, explore and group pages
- Move group activity to separate page
v 8.5.5
- Ensure removing a project removes associated Todo entries
- Prevent a 500 error in Todos when author was removed
- Fix pagination for filtered dashboard and explore pages
- Fix "Show all" link behavior
- Add #upcoming filter to Milestone filter (Tiago Botelho)
v 8.5.4 v 8.5.4
- Do not cache requests for badges (including builds badge) - Do not cache requests for badges (including builds badge)
...@@ -23,6 +46,7 @@ v 8.5.3 ...@@ -23,6 +46,7 @@ v 8.5.3
- Sort starred projects on dashboard based on last activity by default - Sort starred projects on dashboard based on last activity by default
- Show commit message in JIRA mention comment - Show commit message in JIRA mention comment
- Makes issue page and merge request page usable on mobile browsers. - Makes issue page and merge request page usable on mobile browsers.
- Improved UI for profile settings
v 8.5.2 v 8.5.2
- Fix sidebar overlapping content when screen width was below 1200px - Fix sidebar overlapping content when screen width was below 1200px
...@@ -64,7 +88,7 @@ v 8.5.1 ...@@ -64,7 +88,7 @@ v 8.5.1
v 8.5.0 v 8.5.0
- Fix duplicate "me" in tooltip of the "thumbsup" awards Emoji (Stan Hu) - Fix duplicate "me" in tooltip of the "thumbsup" awards Emoji (Stan Hu)
- Cache various Repository methods to improve performance (Yorick Peterse) - Cache various Repository methods to improve performance (Yorick Peterse)
- Fix duplicated branch creation/deletion Web hooks/service notifications when using Web UI (Stan Hu) - Fix duplicated branch creation/deletion Webhooks/service notifications when using Web UI (Stan Hu)
- Ensure rake tasks that don't need a DB connection can be run without one - Ensure rake tasks that don't need a DB connection can be run without one
- Update New Relic gem to 3.14.1.311 (Stan Hu) - Update New Relic gem to 3.14.1.311 (Stan Hu)
- Add "visibility" flag to GET /projects api endpoint - Add "visibility" flag to GET /projects api endpoint
...@@ -197,7 +221,7 @@ v 8.4.0 ...@@ -197,7 +221,7 @@ v 8.4.0
- Add housekeeping function to project settings page - Add housekeeping function to project settings page
- The default GitLab logo now acts as a loading indicator - The default GitLab logo now acts as a loading indicator
- Fix caching issue where build status was not updating in project dashboard (Stan Hu) - Fix caching issue where build status was not updating in project dashboard (Stan Hu)
- Accept 2xx status codes for successful Web hook triggers (Stan Hu) - Accept 2xx status codes for successful Webhook triggers (Stan Hu)
- Fix missing date of month in network graph when commits span a month (Stan Hu) - Fix missing date of month in network graph when commits span a month (Stan Hu)
- Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu) - Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu)
- Don't notify users twice if they are both project watchers and subscribers (Stan Hu) - Don't notify users twice if they are both project watchers and subscribers (Stan Hu)
...@@ -297,7 +321,7 @@ v 8.3.0 ...@@ -297,7 +321,7 @@ v 8.3.0
- Fix broken group avatar upload under "New group" (Stan Hu) - Fix broken group avatar upload under "New group" (Stan Hu)
- Update project repositorize size and commit count during import:repos task (Stan Hu) - Update project repositorize size and commit count during import:repos task (Stan Hu)
- Fix API setting of 'public' attribute to false will make a project private (Stan Hu) - Fix API setting of 'public' attribute to false will make a project private (Stan Hu)
- Handle and report SSL errors in Web hook test (Stan Hu) - Handle and report SSL errors in Webhook test (Stan Hu)
- Bump Redis requirement to 2.8 for Sidekiq 4 (Stan Hu) - Bump Redis requirement to 2.8 for Sidekiq 4 (Stan Hu)
- Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera)
- WIP identifier on merge requests no longer requires trailing space - WIP identifier on merge requests no longer requires trailing space
...@@ -517,7 +541,7 @@ v 8.1.0 ...@@ -517,7 +541,7 @@ v 8.1.0
- Ensure code blocks are properly highlighted after a note is updated - Ensure code blocks are properly highlighted after a note is updated
- Fix wrong access level badge on MR comments - Fix wrong access level badge on MR comments
- Hide password in the service settings form - Hide password in the service settings form
- Move CI web hooks page to project settings area - Move CI webhooks page to project settings area
- Fix User Identities API. It now allows you to properly create or update user's identities. - Fix User Identities API. It now allows you to properly create or update user's identities.
- Add user preference to change layout width (Peter Göbel) - Add user preference to change layout width (Peter Göbel)
- Use commit status in merge request widget as preferred source of CI status - Use commit status in merge request widget as preferred source of CI status
...@@ -560,7 +584,7 @@ v 8.0.3 ...@@ -560,7 +584,7 @@ v 8.0.3
- Fix URL shown in Slack notifications - Fix URL shown in Slack notifications
- Fix bug where projects would appear to be stuck in the forked import state (Stan Hu) - Fix bug where projects would appear to be stuck in the forked import state (Stan Hu)
- Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu) - Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu)
- Add work_in_progress key to MR web hooks (Ben Boeckel) - Add work_in_progress key to MR webhooks (Ben Boeckel)
v 8.0.2 v 8.0.2
- Fix default avatar not rendering in network graph (Stan Hu) - Fix default avatar not rendering in network graph (Stan Hu)
...@@ -851,7 +875,7 @@ v 7.12.0 ...@@ -851,7 +875,7 @@ v 7.12.0
- Fix milestone "Browse Issues" button. - Fix milestone "Browse Issues" button.
- Set milestone on new issue when creating issue from index with milestone filter active. - Set milestone on new issue when creating issue from index with milestone filter active.
- Make namespace API available to all users (Stan Hu) - Make namespace API available to all users (Stan Hu)
- Add web hook support for note events (Stan Hu) - Add webhook support for note events (Stan Hu)
- Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu) - Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu)
- Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu) - Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu)
- Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu) - Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu)
...@@ -958,7 +982,7 @@ v 7.11.0 ...@@ -958,7 +982,7 @@ v 7.11.0
- Add "Create Merge Request" buttons to commits and branches pages and push event. - Add "Create Merge Request" buttons to commits and branches pages and push event.
- Show user roles by comments. - Show user roles by comments.
- Fix automatic blocking of auto-created users from Active Directory. - Fix automatic blocking of auto-created users from Active Directory.
- Call merge request web hook for each new commits (Arthur Gautier) - Call merge request webhook for each new commits (Arthur Gautier)
- Use SIGKILL by default in Sidekiq::MemoryKiller - Use SIGKILL by default in Sidekiq::MemoryKiller
- Fix mentioning of private groups. - Fix mentioning of private groups.
- Add style for <kbd> element in markdown - Add style for <kbd> element in markdown
...@@ -1132,7 +1156,7 @@ v 7.9.0 ...@@ -1132,7 +1156,7 @@ v 7.9.0
- Add brakeman (security scanner for Ruby on Rails) - Add brakeman (security scanner for Ruby on Rails)
- Slack username and channel options - Slack username and channel options
- Add grouped milestones from all projects to dashboard. - Add grouped milestones from all projects to dashboard.
- Web hook sends pusher email as well as commiter - Webhook sends pusher email as well as commiter
- Add Bitbucket omniauth provider. - Add Bitbucket omniauth provider.
- Add Bitbucket importer. - Add Bitbucket importer.
- Support referencing issues to a project whose name starts with a digit - Support referencing issues to a project whose name starts with a digit
...@@ -1255,7 +1279,7 @@ v 7.8.0 ...@@ -1255,7 +1279,7 @@ v 7.8.0
- Allow notification email to be set separately from primary email. - Allow notification email to be set separately from primary email.
- API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger) - API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger)
- Don't have Markdown preview fail for long comments/wiki pages. - Don't have Markdown preview fail for long comments/wiki pages.
- When test web hook - show error message instead of 500 error page if connection to hook url was reset - When test webhook - show error message instead of 500 error page if connection to hook url was reset
- Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov) - Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov)
- Added persistent collapse button for left side nav bar (Jason Blanchard) - Added persistent collapse button for left side nav bar (Jason Blanchard)
- Prevent losing unsaved comments by automatically restoring them when comment page is loaded again. - Prevent losing unsaved comments by automatically restoring them when comment page is loaded again.
...@@ -1272,7 +1296,7 @@ v 7.8.0 ...@@ -1272,7 +1296,7 @@ v 7.8.0
- Show projects user contributed to on user page. Show stars near project on user page. - Show projects user contributed to on user page. Show stars near project on user page.
- Improve database performance for GitLab - Improve database performance for GitLab
- Add Asana service (Jeremy Benoist) - Add Asana service (Jeremy Benoist)
- Improve project web hooks with extra data - Improve project webhooks with extra data
v 7.7.2 v 7.7.2
- Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch
...@@ -1757,7 +1781,7 @@ v 6.4.0 ...@@ -1757,7 +1781,7 @@ v 6.4.0
- Side-by-side diff view (Steven Thonus) - Side-by-side diff view (Steven Thonus)
- Internal projects (Jason Hollingsworth) - Internal projects (Jason Hollingsworth)
- Allow removal of avatar (Drew Blessing) - Allow removal of avatar (Drew Blessing)
- Project web hooks now support issues and merge request events - Project webhooks now support issues and merge request events
- Visiting project page while not logged in will redirect to sign-in instead of 404 (Jason Hollingsworth) - Visiting project page while not logged in will redirect to sign-in instead of 404 (Jason Hollingsworth)
- Expire event cache on avatar creation/removal (Drew Blessing) - Expire event cache on avatar creation/removal (Drew Blessing)
- Archiving old projects (Steven Thonus) - Archiving old projects (Steven Thonus)
...@@ -1827,7 +1851,7 @@ v 6.2.0 ...@@ -1827,7 +1851,7 @@ v 6.2.0
- Added search for projects by name to api (Izaak Alpert) - Added search for projects by name to api (Izaak Alpert)
- Make default user theme configurable (Izaak Alpert) - Make default user theme configurable (Izaak Alpert)
- Update logic for validates_merge_request for tree of MR (Andrew Kumanyaev) - Update logic for validates_merge_request for tree of MR (Andrew Kumanyaev)
- Rake tasks for web hooks management (Jonhnny Weslley) - Rake tasks for webhooks management (Jonhnny Weslley)
- Extended User API to expose admin and can_create_group for user creation/updating (Boyan Tabakov) - Extended User API to expose admin and can_create_group for user creation/updating (Boyan Tabakov)
- API: Remove group - API: Remove group
- API: Remove project - API: Remove project
...@@ -2030,7 +2054,7 @@ v 4.2.0 ...@@ -2030,7 +2054,7 @@ v 4.2.0
- Async gitolite calls - Async gitolite calls
- added satellites logs - added satellites logs
- can_create_group, can_create_team booleans for User - can_create_group, can_create_team booleans for User
- Process web hooks async - Process webhooks async
- GFM: Fix images escaped inside links - GFM: Fix images escaped inside links
- Network graph improved - Network graph improved
- Switchable branches for network graph - Switchable branches for network graph
...@@ -2064,7 +2088,7 @@ v 4.1.0 ...@@ -2064,7 +2088,7 @@ v 4.1.0
v 4.0.0 v 4.0.0
- Remove project code and path from API. Use id instead - Remove project code and path from API. Use id instead
- Return valid cloneable url to repo for web hook - Return valid cloneable url to repo for webhook
- Fixed backup issue - Fixed backup issue
- Reorganized settings - Reorganized settings
- Fixed commits compare - Fixed commits compare
......
...@@ -30,7 +30,7 @@ gem 'omniauth-github', '~> 1.1.1' ...@@ -30,7 +30,7 @@ gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0' gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-google-oauth2', '~> 0.2.0' gem 'omniauth-google-oauth2', '~> 0.2.0'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-saml', '~> 1.4.2' gem 'omniauth-saml', '~> 1.5.0'
gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth_crowd', '~> 2.2.0'
...@@ -263,7 +263,9 @@ group :development, :test do ...@@ -263,7 +263,9 @@ group :development, :test do
gem 'database_cleaner', '~> 1.4.0' gem 'database_cleaner', '~> 1.4.0'
gem 'factory_girl_rails', '~> 4.6.0' gem 'factory_girl_rails', '~> 4.6.0'
gem 'rspec-rails', '~> 3.3.0' gem 'rspec-rails', '~> 3.3.0'
gem 'rspec-retry'
gem 'spinach-rails', '~> 0.2.1' gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0' gem 'minitest', '~> 5.7.0'
...@@ -273,7 +275,7 @@ group :development, :test do ...@@ -273,7 +275,7 @@ group :development, :test do
gem 'capybara', '~> 2.4.0' gem 'capybara', '~> 2.4.0'
gem 'capybara-screenshot', '~> 1.0.0' gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.8.1' gem 'poltergeist', '~> 1.9.0'
gem 'teaspoon', '~> 1.0.0' gem 'teaspoon', '~> 1.0.0'
gem 'teaspoon-jasmine', '~> 2.2.0' gem 'teaspoon-jasmine', '~> 2.2.0'
......
...@@ -358,7 +358,7 @@ GEM ...@@ -358,7 +358,7 @@ GEM
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab_emoji (0.3.1) gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1) gemojione (~> 2.2, >= 2.2.1)
gitlab_git (9.0.0) gitlab_git (9.0.1)
activesupport (~> 4.0) activesupport (~> 4.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
...@@ -532,8 +532,8 @@ GEM ...@@ -532,8 +532,8 @@ GEM
omniauth-oauth2 (1.3.1) omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0) oauth2 (~> 1.0)
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-saml (1.4.2) omniauth-saml (1.5.0)
omniauth (~> 1.1) omniauth (~> 1.3)
ruby-saml (~> 1.1, >= 1.1.1) ruby-saml (~> 1.1, >= 1.1.1)
omniauth-shibboleth (1.2.1) omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0) omniauth (>= 1.0.0)
...@@ -552,7 +552,7 @@ GEM ...@@ -552,7 +552,7 @@ GEM
parser (2.2.3.0) parser (2.2.3.0)
ast (>= 1.1, < 3.0) ast (>= 1.1, < 3.0)
pg (0.18.4) pg (0.18.4)
poltergeist (1.8.1) poltergeist (1.9.0)
capybara (~> 2.1) capybara (~> 2.1)
cliver (~> 0.3.1) cliver (~> 0.3.1)
multi_json (~> 1.0) multi_json (~> 1.0)
...@@ -679,6 +679,8 @@ GEM ...@@ -679,6 +679,8 @@ GEM
rspec-expectations (~> 3.3.0) rspec-expectations (~> 3.3.0)
rspec-mocks (~> 3.3.0) rspec-mocks (~> 3.3.0)
rspec-support (~> 3.3.0) rspec-support (~> 3.3.0)
rspec-retry (0.4.5)
rspec-core
rspec-support (3.3.0) rspec-support (3.3.0)
rubocop (0.35.1) rubocop (0.35.1)
astrolabe (~> 1.3) astrolabe (~> 1.3)
...@@ -690,7 +692,7 @@ GEM ...@@ -690,7 +692,7 @@ GEM
ruby-fogbugz (0.2.1) ruby-fogbugz (0.2.1)
crack (~> 0.4) crack (~> 0.4)
ruby-progressbar (1.7.5) ruby-progressbar (1.7.5)
ruby-saml (1.1.1) ruby-saml (1.1.2)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
uuid (~> 2.3) uuid (~> 2.3)
ruby2ruby (2.2.0) ruby2ruby (2.2.0)
...@@ -764,6 +766,8 @@ GEM ...@@ -764,6 +766,8 @@ GEM
capybara (>= 2.0.0) capybara (>= 2.0.0)
railties (>= 3) railties (>= 3)
spinach (>= 0.4) spinach (>= 0.4)
spinach-rerun-reporter (0.0.2)
spinach (~> 0.8)
spring (1.6.4) spring (1.6.4)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
spring (>= 0.9.1) spring (>= 0.9.1)
...@@ -971,14 +975,14 @@ DEPENDENCIES ...@@ -971,14 +975,14 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.0) omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.2.0) omniauth-google-oauth2 (~> 0.2.0)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-saml (~> 1.4.2) omniauth-saml (~> 1.5.0)
omniauth-shibboleth (~> 1.2.0) omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0) omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0) omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12) org-ruby (~> 0.9.12)
paranoia (~> 2.0) paranoia (~> 2.0)
pg (~> 0.18.2) pg (~> 0.18.2)
poltergeist (~> 1.8.1) poltergeist (~> 1.9.0)
pry-rails pry-rails
quiet_assets (~> 1.0.2) quiet_assets (~> 1.0.2)
rack-attack (~> 4.3.1) rack-attack (~> 4.3.1)
...@@ -999,6 +1003,7 @@ DEPENDENCIES ...@@ -999,6 +1003,7 @@ DEPENDENCIES
rouge (~> 1.10.1) rouge (~> 1.10.1)
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.3.0) rspec-rails (~> 3.3.0)
rspec-retry
rubocop (~> 0.35.0) rubocop (~> 0.35.0)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
sanitize (~> 2.0) sanitize (~> 2.0)
...@@ -1017,6 +1022,7 @@ DEPENDENCIES ...@@ -1017,6 +1022,7 @@ DEPENDENCIES
six (~> 0.2.0) six (~> 0.2.0)
slack-notifier (~> 1.2.0) slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1) spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2)
spring (~> 1.6.4) spring (~> 1.6.4)
spring-commands-rspec (~> 1.0.4) spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.0.0) spring-commands-spinach (~> 1.0.0)
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
namespaces_path: "/api/:version/namespaces.json" namespaces_path: "/api/:version/namespaces.json"
group_projects_path: "/api/:version/groups/:id/projects.json" group_projects_path: "/api/:version/groups/:id/projects.json"
projects_path: "/api/:version/projects.json" projects_path: "/api/:version/projects.json"
labels_path: "/api/:version/projects/:id/labels"
group: (group_id, callback) -> group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path) url = Api.buildUrl(Api.group_path)
...@@ -61,6 +62,19 @@ ...@@ -61,6 +62,19 @@
).done (projects) -> ).done (projects) ->
callback(projects) callback(projects)
newLabel: (project_id, data, callback) ->
url = Api.buildUrl(Api.labels_path)
url = url.replace(':id', project_id)
data.private_token = gon.api_token
$.ajax(
url: url
type: "POST"
data: data
dataType: "json"
).done (label) ->
callback(label)
# Return group projects list. Filtered by query # Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) -> groupProjects: (group_id, query, callback) ->
url = Api.buildUrl(Api.group_projects_path) url = Api.buildUrl(Api.group_projects_path)
......
...@@ -220,17 +220,17 @@ $ -> ...@@ -220,17 +220,17 @@ $ ->
.off 'breakpoint:change' .off 'breakpoint:change'
.on 'breakpoint:change', (e, breakpoint) -> .on 'breakpoint:change', (e, breakpoint) ->
if breakpoint is 'sm' or breakpoint is 'xs' if breakpoint is 'sm' or breakpoint is 'xs'
$gutterIcon = $('aside .gutter-toggle').find('i') $gutterIcon = $('.js-sidebar-toggle').find('i')
if $gutterIcon.hasClass('fa-angle-double-right') if $gutterIcon.hasClass('fa-angle-double-right')
$gutterIcon.closest('a').trigger('click') $gutterIcon.closest('a').trigger('click')
$(document) $(document)
.off 'click', 'aside .gutter-toggle' .off 'click', '.js-sidebar-toggle'
.on 'click', 'aside .gutter-toggle', (e, triggered) -> .on 'click', '.js-sidebar-toggle', (e, triggered) ->
e.preventDefault() e.preventDefault()
$this = $(this) $this = $(this)
$thisIcon = $this.find 'i' $thisIcon = $this.find 'i'
$allGutterToggleIcons = $('.gutter-toggle i') $allGutterToggleIcons = $('.js-sidebar-toggle i')
if $thisIcon.hasClass('fa-angle-double-right') if $thisIcon.hasClass('fa-angle-double-right')
$allGutterToggleIcons $allGutterToggleIcons
.removeClass('fa-angle-double-right') .removeClass('fa-angle-double-right')
......
class @AwardsHandler class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) -> constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
$(".add-award").click (event) => $(".js-add-award").on "click", (event) =>
event.stopPropagation() event.stopPropagation()
event.preventDefault() event.preventDefault()
...@@ -9,27 +9,46 @@ class @AwardsHandler ...@@ -9,27 +9,46 @@ class @AwardsHandler
$("html").on 'click', (event) -> $("html").on 'click', (event) ->
if !$(event.target).closest(".emoji-menu").length if !$(event.target).closest(".emoji-menu").length
if $(".emoji-menu").is(":visible") if $(".emoji-menu").is(":visible")
$(".emoji-menu").hide() $(".emoji-menu").removeClass "is-visible"
$(".awards")
.off "click"
.on "click", ".js-emoji-btn", @handleClick
@renderFrequentlyUsedBlock() @renderFrequentlyUsedBlock()
@setupSearch()
handleClick: (e) ->
e.preventDefault()
emoji = $(this)
.find(".icon")
.data "emoji"
awards_handler.addAward emoji
showEmojiMenu: -> showEmojiMenu: ->
if $(".emoji-menu").length if $(".emoji-menu").length
$(".emoji-menu").show() if $(".emoji-menu").is ".is-visible"
$("#emoji_search").focus() $(".emoji-menu").removeClass "is-visible"
else $("#emoji_search").blur()
$.get "/emojis", (response) -> else
$(".add-award").after response $(".emoji-menu").addClass "is-visible"
$(".emoji-menu").show()
$("#emoji_search").focus() $("#emoji_search").focus()
else
$('.js-add-award').addClass "is-loading"
$.get "/emojis", (response) =>
$('.js-add-award').removeClass "is-loading"
$(".js-award-holder").append response
setTimeout =>
$(".emoji-menu").addClass "is-visible"
$("#emoji_search").focus()
@setupSearch()
, 200
addAward: (emoji) -> addAward: (emoji) ->
emoji = @normilizeEmojiName(emoji) emoji = @normilizeEmojiName(emoji)
@postEmoji emoji, => @postEmoji emoji, =>
@addAwardToEmojiBar(emoji) @addAwardToEmojiBar(emoji)
$(".emoji-menu").hide() $(".emoji-menu").removeClass "is-visible"
addAwardToEmojiBar: (emoji) -> addAwardToEmojiBar: (emoji) ->
@addEmojiToFrequentlyUsedList(emoji) @addEmojiToFrequentlyUsedList(emoji)
...@@ -39,7 +58,7 @@ class @AwardsHandler ...@@ -39,7 +58,7 @@ class @AwardsHandler
if @isActive(emoji) if @isActive(emoji)
@decrementCounter(emoji) @decrementCounter(emoji)
else else
counter = @findEmojiIcon(emoji).siblings(".counter") counter = @findEmojiIcon(emoji).siblings(".js-counter")
counter.text(parseInt(counter.text()) + 1) counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass("active") counter.parent().addClass("active")
@addMeToAuthorList(emoji) @addMeToAuthorList(emoji)
...@@ -53,7 +72,7 @@ class @AwardsHandler ...@@ -53,7 +72,7 @@ class @AwardsHandler
@findEmojiIcon(emoji).parent().hasClass("active") @findEmojiIcon(emoji).parent().hasClass("active")
decrementCounter: (emoji) -> decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings(".counter") counter = @findEmojiIcon(emoji).siblings(".js-counter")
emojiIcon = counter.parent() emojiIcon = counter.parent()
if parseInt(counter.text()) > 1 if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1) counter.text(parseInt(counter.text()) - 1)
...@@ -70,9 +89,13 @@ class @AwardsHandler ...@@ -70,9 +89,13 @@ class @AwardsHandler
removeMeFromAuthorList: (emoji) -> removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent() award_block = @findEmojiIcon(emoji).parent()
authors = award_block.attr("data-original-title").split(", ") authors = award_block
.attr("data-original-title")
.split(", ")
authors.splice(authors.indexOf("me"),1) authors.splice(authors.indexOf("me"),1)
award_block.closest(".award").attr("data-original-title", authors.join(", ")) award_block
.closest(".js-emoji-btn")
.attr("data-original-title", authors.join(", "))
@resetTooltip(award_block) @resetTooltip(award_block)
addMeToAuthorList: (emoji) -> addMeToAuthorList: (emoji) ->
...@@ -98,14 +121,18 @@ class @AwardsHandler ...@@ -98,14 +121,18 @@ class @AwardsHandler
emojiCssClass = @resolveNameToCssClass(emoji) emojiCssClass = @resolveNameToCssClass(emoji)
nodes = [] nodes = []
nodes.push("<div class='award active' title='me'>") nodes.push(
nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>") "<button class='btn award-control js-emoji-btn has_tooltip active' title='me'>",
nodes.push("<div class='counter'>1</div>") "<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
nodes.push("</div>") "<span class='award-control-text js-counter'>1</span>",
"</button>"
emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji) )
$(".award").tooltip() emoji_node = $(nodes.join("\n"))
.insertBefore(".js-award-holder")
.find(".emoji-icon")
.data("emoji", emoji)
$('.award-control').tooltip()
resolveNameToCssClass: (emoji) -> resolveNameToCssClass: (emoji) ->
emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
...@@ -128,7 +155,7 @@ class @AwardsHandler ...@@ -128,7 +155,7 @@ class @AwardsHandler
callback.call() callback.call()
findEmojiIcon: (emoji) -> findEmojiIcon: (emoji) ->
$(".award [data-emoji='#{emoji}']") $(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
scrollToAwards: -> scrollToAwards: ->
$('body, html').animate({ $('body, html').animate({
...@@ -164,13 +191,13 @@ class @AwardsHandler ...@@ -164,13 +191,13 @@ class @AwardsHandler
term = $(ev.target).val() term = $(ev.target).val()
# Clean previous search results # Clean previous search results
$("ul.emoji-search,h5.emoji-search").remove() $("ul.emoji-menu-search, h5.emoji-search").remove()
if term if term
# Generate a search result block # Generate a search result block
h5 = $("<h5>").text("Search results").addClass("emoji-search") h5 = $("<h5>").text("Search results").addClass("emoji-search")
found_emojis = @searchEmojis(term).show() found_emojis = @searchEmojis(term).show()
ul = $("<ul>").addClass("emoji-search").append(found_emojis) ul = $("<ul>").addClass("emoji-menu-list emoji-menu-search").append(found_emojis)
$(".emoji-menu-content ul, .emoji-menu-content h5").hide() $(".emoji-menu-content ul, .emoji-menu-content h5").hide()
$(".emoji-menu-content").append(h5).append(ul) $(".emoji-menu-content").append(h5).append(ul)
else else
......
...@@ -4,6 +4,8 @@ class CiBuild ...@@ -4,6 +4,8 @@ class CiBuild
constructor: (build_url, build_status) -> constructor: (build_url, build_status) ->
clearInterval(CiBuild.interval) clearInterval(CiBuild.interval)
@initScrollButtonAffix()
if build_status == "running" || build_status == "pending" if build_status == "running" || build_status == "pending"
# #
# Bind autoscroll button to follow build output # Bind autoscroll button to follow build output
...@@ -38,4 +40,15 @@ class CiBuild ...@@ -38,4 +40,15 @@ class CiBuild
checkAutoscroll: -> checkAutoscroll: ->
$("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state") $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
initScrollButtonAffix: ->
$buildScroll = $('#js-build-scroll')
$body = $('body')
$buildTrace = $('#build-trace')
$buildScroll.affix(
offset:
bottom: ->
$body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top)
)
@CiBuild = CiBuild @CiBuild = CiBuild
...@@ -23,7 +23,7 @@ class Dispatcher ...@@ -23,7 +23,7 @@ class Dispatcher
new Issue() new Issue()
shortcut_handler = new ShortcutsIssuable() shortcut_handler = new ShortcutsIssuable()
new ZenMode() new ZenMode()
when 'projects:milestones:show' when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone() new Milestone()
when 'projects:milestones:new', 'projects:milestones:edit' when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode() new ZenMode()
...@@ -74,8 +74,9 @@ class Dispatcher ...@@ -74,8 +74,9 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new TreeView() if $('#tree-slider').length new TreeView() if $('#tree-slider').length
when 'groups:show' when 'groups:activity'
new Activities() new Activities()
when 'groups:show'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
when 'groups:group_members:index' when 'groups:group_members:index'
new GroupMembers() new GroupMembers()
......
class GitLabDropdownFilter
BLUR_KEYCODES = [27, 40]
constructor: (@dropdown, @options) ->
@input = @dropdown.find(".dropdown-input .dropdown-input-field")
# Key events
timeout = ""
@input.on "keyup", (e) =>
if e.keyCode is 13 && @input.val() isnt ""
if @options.enterCallback
@options.enterCallback()
return
clearTimeout timeout
timeout = setTimeout =>
blur_field = @shouldBlur e.keyCode
search_text = @input.val()
if blur_field
@input.blur()
if @options.remote
@options.query search_text, (data) =>
@options.callback(data)
else
@filter search_text
, 250
shouldBlur: (keyCode) ->
return BLUR_KEYCODES.indexOf(keyCode) >= 0
filter: (search_text) ->
data = @options.data()
results = data
if search_text isnt ""
results = fuzzaldrinPlus.filter(data, search_text,
key: @options.keys
)
@options.callback results
class GitLabDropdownRemote
constructor: (@dataEndpoint, @options) ->
execute: ->
if typeof @dataEndpoint is "string"
@fetchData()
else if typeof @dataEndpoint is "function"
if @options.beforeSend
@options.beforeSend()
# Fetch the data by calling the data funcfion
@dataEndpoint "", (data) =>
if @options.success
@options.success(data)
if @options.beforeSend
@options.beforeSend()
# Fetch the data through ajax if the data is a string
fetchData: ->
$.ajax(
url: @dataEndpoint,
dataType: @options.dataType,
beforeSend: =>
if @options.beforeSend
@options.beforeSend()
success: (data) =>
if @options.success
@options.success(data)
)
class GitLabDropdown
LOADING_CLASS = "is-loading"
PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active"
constructor: (@el, @options) ->
self = @
@dropdown = $(@el).parent()
search_fields = if @options.search then @options.search.fields else [];
if @options.data
# Remote data
@remote = new GitLabDropdownRemote @options.data, {
dataType: @options.dataType,
beforeSend: @toggleLoading.bind(@)
success: (data) =>
@fullData = data
@parseData @fullData
}
# Init filiterable
if @options.filterable
@filter = new GitLabDropdownFilter @dropdown,
remote: @options.filterRemote
query: @options.data
keys: @options.search.fields
data: =>
return @fullData
callback: (data) =>
@parseData data
enterCallback: =>
@selectFirstRow()
# Event listeners
@dropdown.on "shown.bs.dropdown", @opened
@dropdown.on "hidden.bs.dropdown", @hidden
if @dropdown.find(".dropdown-toggle-page").length
@dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
e.preventDefault()
e.stopPropagation()
@togglePage()
if @options.selectable
selector = ".dropdown-content a"
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content a"
@dropdown.on "click", selector, (e) ->
self.rowClicked $(@)
if self.options.clicked
self.options.clicked()
toggleLoading: ->
$('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
togglePage: ->
menu = $('.dropdown-menu', @dropdown)
if menu.hasClass(PAGE_TWO_CLASS)
if @remote
@remote.execute()
menu.toggleClass PAGE_TWO_CLASS
parseData: (data) ->
@renderedData = data
# Render each row
html = $.map data, (obj) =>
return @renderItem(obj)
if @options.filterable and data.length is 0
# render no matching results
html = [@noResults()]
# Render the full menu
full_html = @renderMenu(html.join(""))
@appendMenu(full_html)
opened: =>
contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is ""
@remote.execute()
if @options.filterable
@dropdown.find(".dropdown-input-field").focus()
hidden: =>
if @options.filterable
@dropdown.find(".dropdown-input-field").blur().val("")
if @dropdown.find(".dropdown-toggle-page").length
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
# Render the full menu
renderMenu: (html) ->
menu_html = ""
if @options.renderMenu
menu_html = @options.renderMenu(html)
else
menu_html = "<ul>#{html}</ul>"
return menu_html
# Append the menu into the dropdown
appendMenu: (html) ->
selector = '.dropdown-content'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content"
$(selector, @dropdown).html html
# Render the row
renderItem: (data) ->
html = ""
return "<li class='divider'></li>" if data is "divider"
if @options.renderRow
# Call the render function
html = @options.renderRow(data)
else
selected = if @options.isSelected then @options.isSelected(data) else false
url = if @options.url then @options.url(data) else "#"
text = if @options.text then @options.text(data) else ""
cssClass = "";
if selected
cssClass = "is-active"
html = "<li>"
html += "<a href='#{url}' class='#{cssClass}'>"
html += text
html += "</a>"
html += "</li>"
return html
noResults: ->
html = "<li>"
html += "<a href='#' class='is-focused'>"
html += "No matching results."
html += "</a>"
html += "</li>"
rowClicked: (el) ->
fieldName = @options.fieldName
field = @dropdown.parent().find("input[name='#{fieldName}']")
if el.hasClass(ACTIVE_CLASS)
field.remove()
else
fieldName = @options.fieldName
selectedIndex = el.parent().index()
if @renderedData
selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
if @options.multiSelect
oldValue = field.val()
if oldValue
value = "#{oldValue},#{value}"
else
@dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS
field.remove()
# Toggle active class for the tick mark
el.toggleClass "is-active"
if value
if !field.length
# Create hidden input for form
input = "<input type='hidden' name='#{fieldName}' />"
@dropdown.before input
@dropdown.parent().find("input[name='#{fieldName}']").val value
selectFirstRow: ->
selector = '.dropdown-content li:first-child a'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content li:first-child a"
# similute a click on the first link
$(selector).trigger "click"
$.fn.glDropdown = (opts) ->
return @.each ->
new GitLabDropdown @, opts
class @IssueStatusSelect
constructor: ->
$('.js-issue-status').each (i, el) ->
fieldName = $(el).data("field-name")
$(el).glDropdown(
selectable: true
fieldName: fieldName
id: (obj, el) ->
$(el).data("id")
)
class @LabelsSelect
constructor: ->
$('.js-label-select').each (i, dropdown) ->
projectId = $(dropdown).data('project-id')
labelUrl = $(dropdown).data("labels")
selectedLabel = $(dropdown).data('selected')
if selectedLabel
selectedLabel = selectedLabel.split(",")
newLabelField = $('#new_label_name')
newColorField = $('#new_label_color')
showNo = $(dropdown).data('show-no')
showAny = $(dropdown).data('show-any')
if newLabelField.length
$('.suggest-colors-dropdown a').on "click", (e) ->
e.preventDefault()
e.stopPropagation()
newColorField.val $(this).data("color")
$('.js-dropdown-label-color-preview')
.css 'background-color', $(this).data("color")
.addClass 'is-active'
$('.js-new-label-btn').on "click", (e) ->
e.preventDefault()
e.stopPropagation()
if newLabelField.val() isnt "" && newColorField.val() isnt ""
$('.js-new-label-btn').disable()
# Create new label with API
Api.newLabel projectId, {
name: newLabelField.val()
color: newColorField.val()
}, (label) ->
$('.js-new-label-btn').enable()
$('.dropdown-menu-back', $(dropdown).parent()).trigger "click"
$(dropdown).glDropdown(
data: (term, callback) ->
# We have to fetch the JS version of the labels list because there is no
# public facing JSON url for labels
$.ajax(
url: labelUrl
).done (data) ->
html = $(data)
data = []
html.find('.label-row a').each ->
data.push(
title: $(@).text().trim()
)
if showNo
data.unshift(
id: "0"
title: 'No label'
)
if showAny
data.unshift(
title: 'Any label'
)
if data.length > 2
data.splice 2, 0, "divider"
callback data
renderRow: (label) ->
if $.isArray(selectedLabel)
selected = ""
$.each selectedLabel, (i, selectedLbl) ->
selectedLbl = selectedLbl.trim()
if selected is "" && label.title is selectedLbl
selected = "is-active"
else
selected = if label.title is selectedLabel then "is-active" else ""
"<li>
<a href='#' class='#{selected}'>
#{label.title}
</a>
</li>"
filterable: true
search:
fields: ['title']
selectable: true
fieldName: $(dropdown).data('field-name')
id: (label) ->
label.title
clicked: ->
if $(dropdown).hasClass "js-filter-submit"
$(dropdown).parents('form').submit()
)
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
class @MarkdownPreview class @MarkdownPreview
# Minimum number of users referenced before triggering a warning # Minimum number of users referenced before triggering a warning
referenceThreshold: 10 referenceThreshold: 10
ajaxCache: {}
showPreview: (form) -> showPreview: (form) ->
preview = form.find('.js-md-preview') preview = form.find('.js-md-preview')
...@@ -24,12 +25,16 @@ class @MarkdownPreview ...@@ -24,12 +25,16 @@ class @MarkdownPreview
renderMarkdown: (text, success) -> renderMarkdown: (text, success) ->
return unless window.markdown_preview_path return unless window.markdown_preview_path
return success(@ajaxCache.response) if text == @ajaxCache.text
$.ajax $.ajax
type: 'POST' type: 'POST'
url: window.markdown_preview_path url: window.markdown_preview_path
data: { text: text } data: { text: text }
dataType: 'json' dataType: 'json'
success: success success: (response) =>
@ajaxCache = text: text, response: response
success(response)
hideReferencedUsers: (form) -> hideReferencedUsers: (form) ->
referencedUsers = form.find('.referenced-users') referencedUsers = form.find('.referenced-users')
...@@ -49,6 +54,7 @@ markdownPreview = new MarkdownPreview() ...@@ -49,6 +54,7 @@ markdownPreview = new MarkdownPreview()
previewButtonSelector = '.js-md-preview-button' previewButtonSelector = '.js-md-preview-button'
writeButtonSelector = '.js-md-write-button' writeButtonSelector = '.js-md-write-button'
lastTextareaPreviewed = null
$.fn.setupMarkdownPreview = -> $.fn.setupMarkdownPreview = ->
$form = $(this) $form = $(this)
...@@ -58,10 +64,10 @@ $.fn.setupMarkdownPreview = -> ...@@ -58,10 +64,10 @@ $.fn.setupMarkdownPreview = ->
form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form) form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form)
form_textarea.on 'blur', -> markdownPreview.showPreview($form) form_textarea.on 'blur', -> markdownPreview.showPreview($form)
$(document).on 'click', previewButtonSelector, (e) -> $(document).on 'markdown-preview:show', (e, $form) ->
e.preventDefault() return unless $form
$form = $(this).closest('form') lastTextareaPreviewed = $form.find('textarea.markdown-area')
# toggle tabs # toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active') $form.find(writeButtonSelector).parent().removeClass('active')
...@@ -73,10 +79,10 @@ $(document).on 'click', previewButtonSelector, (e) -> ...@@ -73,10 +79,10 @@ $(document).on 'click', previewButtonSelector, (e) ->
markdownPreview.showPreview($form) markdownPreview.showPreview($form)
$(document).on 'click', writeButtonSelector, (e) -> $(document).on 'markdown-preview:hide', (e, $form) ->
e.preventDefault() return unless $form
$form = $(this).closest('form') lastTextareaPreviewed = null
# toggle tabs # toggle tabs
$form.find(writeButtonSelector).parent().addClass('active') $form.find(writeButtonSelector).parent().addClass('active')
...@@ -84,4 +90,30 @@ $(document).on 'click', writeButtonSelector, (e) -> ...@@ -84,4 +90,30 @@ $(document).on 'click', writeButtonSelector, (e) ->
# toggle content # toggle content
$form.find('.md-write-holder').show() $form.find('.md-write-holder').show()
$form.find('textarea.markdown-area').focus()
$form.find('.md-preview-holder').hide() $form.find('.md-preview-holder').hide()
$(document).on 'markdown-preview:toggle', (e, keyboardEvent) ->
$target = $(keyboardEvent.target)
if $target.is('textarea.markdown-area')
$(document).triggerHandler('markdown-preview:show', [$target.closest('form')])
keyboardEvent.preventDefault()
else if lastTextareaPreviewed
$target = lastTextareaPreviewed
$(document).triggerHandler('markdown-preview:hide', [$target.closest('form')])
keyboardEvent.preventDefault()
$(document).on 'click', previewButtonSelector, (e) ->
e.preventDefault()
$form = $(this).closest('form')
$(document).triggerHandler('markdown-preview:show', [$form])
$(document).on 'click', writeButtonSelector, (e) ->
e.preventDefault()
$form = $(this).closest('form')
$(document).triggerHandler('markdown-preview:hide', [$form])
...@@ -189,7 +189,7 @@ class @MergeRequestTabs ...@@ -189,7 +189,7 @@ class @MergeRequestTabs
$('.container-fluid').removeClass('container-limited') $('.container-fluid').removeClass('container-limited')
shrinkView: -> shrinkView: ->
$gutterIcon = $('.gutter-toggle i') $gutterIcon = $('.js-sidebar-toggle i')
# Wait until listeners are set # Wait until listeners are set
setTimeout( -> setTimeout( ->
...@@ -197,4 +197,3 @@ class @MergeRequestTabs ...@@ -197,4 +197,3 @@ class @MergeRequestTabs
if $gutterIcon.is('.fa-angle-double-right') if $gutterIcon.is('.fa-angle-double-right')
$gutterIcon.closest('a').trigger('click',[true]) $gutterIcon.closest('a').trigger('click',[true])
, 0) , 0)
...@@ -69,7 +69,7 @@ class @Milestone ...@@ -69,7 +69,7 @@ class @Milestone
@bindIssuesSorting() @bindIssuesSorting()
@bindMergeRequestSorting() @bindMergeRequestSorting()
@bindTabsSwitching @bindTabsSwitching()
bindIssuesSorting: -> bindIssuesSorting: ->
$("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable( $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable(
...@@ -104,7 +104,7 @@ class @Milestone ...@@ -104,7 +104,7 @@ class @Milestone
).disableSelection() ).disableSelection()
bindMergeRequestSorting: -> bindTabsSwitching: ->
$('a[data-toggle="tab"]').on 'show.bs.tab', (e) -> $('a[data-toggle="tab"]').on 'show.bs.tab', (e) ->
currentTabClass = $(e.target).data('show') currentTabClass = $(e.target).data('show')
previousTabClass = $(e.relatedTarget).data('show') previousTabClass = $(e.relatedTarget).data('show')
...@@ -112,7 +112,8 @@ class @Milestone ...@@ -112,7 +112,8 @@ class @Milestone
$(previousTabClass).hide() $(previousTabClass).hide()
$(currentTabClass).removeClass('hidden') $(currentTabClass).removeClass('hidden')
$(currentTabClass).show() $(currentTabClass).show()
bindMergeRequestSorting: ->
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable( $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable(
connectWith: ".merge_requests-sortable-list", connectWith: ".merge_requests-sortable-list",
dropOnEmpty: true, dropOnEmpty: true,
......
class @MilestoneSelect
constructor: ->
$('.js-milestone-select').each (i, dropdown) ->
projectId = $(dropdown).data('project-id')
milestonesUrl = $(dropdown).data('milestones')
selectedMilestone = $(dropdown).data('selected')
showNo = $(dropdown).data('show-no')
showAny = $(dropdown).data('show-any')
useId = $(dropdown).data('use-id')
$(dropdown).glDropdown(
data: (term, callback) ->
$.ajax(
url: milestonesUrl
).done (data) ->
html = $(data)
data = []
html.find('.milestone strong a').each ->
link = $(@).attr("href").split("/")
data.push(
id: link[link.length - 1]
title: $(@).text().trim()
)
if showNo
data.unshift(
id: "0"
title: 'No Milestone'
)
if showAny
data.unshift(
title: 'Any Milestone'
)
if data.length > 2
data.splice 2, 0, "divider"
callback(data)
filterable: true
search:
fields: ['title']
selectable: true
fieldName: $(dropdown).data('field-name')
text: (milestone) ->
milestone.title
id: (milestone) ->
if !useId
if milestone.title isnt "Any milestone"
milestone.title
else
""
else
milestone.id
isSelected: (milestone) ->
milestone.title is selectedMilestone
clicked: ->
if $(dropdown).hasClass "js-filter-submit"
$(dropdown).parents('form').submit()
)
...@@ -30,8 +30,11 @@ class @Notes ...@@ -30,8 +30,11 @@ class @Notes
$(document).on "ajax:success", ".js-main-target-form", @addNote $(document).on "ajax:success", ".js-main-target-form", @addNote
$(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote $(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote
# catch note ajax errors
$(document).on "ajax:error", ".js-main-target-form", @addNoteError
# change note in UI after update # change note in UI after update
$(document).on "ajax:success", "form.edit_note", @updateNote $(document).on "ajax:success", "form.edit-note", @updateNote
# Edit note link # Edit note link
$(document).on "click", ".js-note-edit", @showEditForm $(document).on "click", ".js-note-edit", @showEditForm
...@@ -51,6 +54,9 @@ class @Notes ...@@ -51,6 +54,9 @@ class @Notes
$(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton $(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton
$(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm $(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm
# reset main target form when clicking discard
$(document).on "click", ".js-note-discard", @resetMainTargetForm
# update the file name when an attachment is selected # update the file name when an attachment is selected
$(document).on "change", ".js-note-attachment-input", @updateFormAttachment $(document).on "change", ".js-note-attachment-input", @updateFormAttachment
...@@ -72,7 +78,7 @@ class @Notes ...@@ -72,7 +78,7 @@ class @Notes
cleanBinding: -> cleanBinding: ->
$(document).off "ajax:success", ".js-main-target-form" $(document).off "ajax:success", ".js-main-target-form"
$(document).off "ajax:success", ".js-discussion-note-form" $(document).off "ajax:success", ".js-discussion-note-form"
$(document).off "ajax:success", "form.edit_note" $(document).off "ajax:success", "form.edit-note"
$(document).off "click", ".js-note-edit" $(document).off "click", ".js-note-edit"
$(document).off "click", ".note-edit-cancel" $(document).off "click", ".note-edit-cancel"
$(document).off "click", ".js-note-delete" $(document).off "click", ".js-note-delete"
...@@ -85,6 +91,7 @@ class @Notes ...@@ -85,6 +91,7 @@ class @Notes
$(document).off "keyup", ".js-note-text" $(document).off "keyup", ".js-note-text"
$(document).off "click", ".js-note-target-reopen" $(document).off "click", ".js-note-target-reopen"
$(document).off "click", ".js-note-target-close" $(document).off "click", ".js-note-target-close"
$(document).off "click", ".js-note-discard"
$('.note .js-task-list-container').taskList('disable') $('.note .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.note .js-task-list-container' $(document).off 'tasklist:changed', '.note .js-task-list-container'
...@@ -219,7 +226,7 @@ class @Notes ...@@ -219,7 +226,7 @@ class @Notes
Resets text and preview. Resets text and preview.
Resets buttons. Resets buttons.
### ###
resetMainTargetForm: -> resetMainTargetForm: (e) =>
form = $(".js-main-target-form") form = $(".js-main-target-form")
# remove validation errors # remove validation errors
...@@ -231,6 +238,8 @@ class @Notes ...@@ -231,6 +238,8 @@ class @Notes
form.find(".js-note-text").data("autosave").reset() form.find(".js-note-text").data("autosave").reset()
@updateTargetButtons(e)
reenableTargetFormSubmitButton: -> reenableTargetFormSubmitButton: ->
form = $(".js-main-target-form") form = $(".js-main-target-form")
...@@ -274,8 +283,10 @@ class @Notes ...@@ -274,8 +283,10 @@ class @Notes
form.removeClass "js-new-note-form" form.removeClass "js-new-note-form"
form.find('.div-dropzone').remove() form.find('.div-dropzone').remove()
# hide discard button
form.find('.js-note-discard').hide()
# setup preview buttons # setup preview buttons
form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left"
previewButton = form.find(".js-md-preview-button") previewButton = form.find(".js-md-preview-button")
textarea = form.find(".js-note-text") textarea = form.find(".js-note-text")
...@@ -309,6 +320,10 @@ class @Notes ...@@ -309,6 +320,10 @@ class @Notes
addNote: (xhr, note, status) => addNote: (xhr, note, status) =>
@renderNote(note) @renderNote(note)
addNoteError: (xhr, note, status) =>
flash = new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert')
flash.pinTo('.md-area')
### ###
Called in response to the new note form being submitted Called in response to the new note form being submitted
...@@ -347,22 +362,26 @@ class @Notes ...@@ -347,22 +362,26 @@ class @Notes
note = $(this).closest(".note") note = $(this).closest(".note")
note.find(".note-body > .note-text").hide() note.find(".note-body > .note-text").hide()
note.find(".note-header").hide() note.find(".note-header").hide()
base_form = note.find(".note-edit-form") form = note.find(".note-edit-form")
form = base_form.clone().insertAfter(base_form) isNewForm = form.is(':not(.gfm-form)')
form.addClass('current-note-edit-form gfm-form') if isNewForm
form.find('.div-dropzone').remove() form.addClass('gfm-form')
form.addClass('current-note-edit-form')
form.show()
# Show the attachment delete link # Show the attachment delete link
note.find(".js-note-attachment-delete").show() note.find(".js-note-attachment-delete").show()
# Setup markdown form # Setup markdown form
GitLab.GfmAutoComplete.setup() if isNewForm
new DropzoneInput(form) GitLab.GfmAutoComplete.setup()
new DropzoneInput(form)
form.show()
textarea = form.find("textarea") textarea = form.find("textarea")
textarea.focus() textarea.focus()
autosize(textarea)
if isNewForm
autosize(textarea)
# HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
# The textarea has the correct value, Chrome just won't show it unless we # The textarea has the correct value, Chrome just won't show it unless we
...@@ -371,7 +390,8 @@ class @Notes ...@@ -371,7 +390,8 @@ class @Notes
textarea.val "" textarea.val ""
textarea.val value textarea.val value
disableButtonIfEmptyField textarea, form.find(".js-comment-button") if isNewForm
disableButtonIfEmptyField textarea, form.find(".js-comment-button")
### ###
Called in response to clicking the edit note link Called in response to clicking the edit note link
...@@ -383,7 +403,9 @@ class @Notes ...@@ -383,7 +403,9 @@ class @Notes
note = $(this).closest(".note") note = $(this).closest(".note")
note.find(".note-body > .note-text").show() note.find(".note-body > .note-text").show()
note.find(".note-header").show() note.find(".note-header").show()
note.find(".current-note-edit-form").remove() note.find(".current-note-edit-form")
.removeClass("current-note-edit-form")
.hide()
### ###
Called in response to deleting a note of any kind. Called in response to deleting a note of any kind.
...@@ -462,6 +484,11 @@ class @Notes ...@@ -462,6 +484,11 @@ class @Notes
form.find("#note_line_code").val dataHolder.data("lineCode") form.find("#note_line_code").val dataHolder.data("lineCode")
form.find("#note_noteable_type").val dataHolder.data("noteableType") form.find("#note_noteable_type").val dataHolder.data("noteableType")
form.find("#note_noteable_id").val dataHolder.data("noteableId") form.find("#note_noteable_id").val dataHolder.data("noteableId")
form.find('.js-note-discard')
.show()
.removeClass('js-note-discard')
.addClass('js-close-discussion-note-form')
.text(form.find('.js-close-discussion-note-form').data('cancel-text'))
@setupNoteForm form @setupNoteForm form
form.find(".js-note-text").focus() form.find(".js-note-text").focus()
form.addClass "js-discussion-note-form" form.addClass "js-discussion-note-form"
...@@ -561,21 +588,52 @@ class @Notes ...@@ -561,21 +588,52 @@ class @Notes
updateCloseButton: (e) => updateCloseButton: (e) =>
textarea = $(e.target) textarea = $(e.target)
form = textarea.parents('form') form = textarea.parents('form')
form.find('.js-note-target-close').text('Close') closebtn = form.find('.js-note-target-close')
closebtn.text(closebtn.data('original-text'))
updateTargetButtons: (e) => updateTargetButtons: (e) =>
textarea = $(e.target) textarea = $(e.target)
form = textarea.parents('form') form = textarea.parents('form')
reopenbtn = form.find('.js-note-target-reopen')
closebtn = form.find('.js-note-target-close')
discardbtn = form.find('.js-note-discard')
if textarea.val().trim().length > 0 if textarea.val().trim().length > 0
form.find('.js-note-target-reopen').text('Comment & reopen') reopentext = reopenbtn.data('alternative-text')
form.find('.js-note-target-close').text('Comment & close') closetext = closebtn.data('alternative-text')
form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen')
form.find('.js-note-target-close').addClass('btn-comment-and-close') if reopenbtn.text() isnt reopentext
reopenbtn.text(reopentext)
if closebtn.text() isnt closetext
closebtn.text(closetext)
if reopenbtn.is(':not(.btn-comment-and-reopen)')
reopenbtn.addClass('btn-comment-and-reopen')
if closebtn.is(':not(.btn-comment-and-close)')
closebtn.addClass('btn-comment-and-close')
if discardbtn.is(':hidden')
discardbtn.show()
else else
form.find('.js-note-target-reopen').text('Reopen') reopentext = reopenbtn.data('original-text')
form.find('.js-note-target-close').text('Close') closetext = closebtn.data('original-text')
form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen')
form.find('.js-note-target-close').removeClass('btn-comment-and-close') if reopenbtn.text() isnt reopentext
reopenbtn.text(reopentext)
if closebtn.text() isnt closetext
closebtn.text(closetext)
if reopenbtn.is(':not(.btn-comment-and-reopen)')
reopenbtn.removeClass('btn-comment-and-reopen')
if closebtn.is(':not(.btn-comment-and-close)')
closebtn.removeClass('btn-comment-and-close')
if discardbtn.is(':visible')
discardbtn.hide()
initTaskList: -> initTaskList: ->
@enableTaskList() @enableTaskList()
......
...@@ -4,12 +4,13 @@ class @Profile ...@@ -4,12 +4,13 @@ class @Profile
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', -> $('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit() $(this).parents('form').submit()
$('.update-username form').on 'ajax:before', -> $('.update-username').on 'ajax:before', ->
$('.loading-gif').show() $('.loading-username').show()
$(this).find('.update-success').hide() $(this).find('.update-success').hide()
$(this).find('.update-failed').hide() $(this).find('.update-failed').hide()
$('.update-username form').on 'ajax:complete', -> $('.update-username').on 'ajax:complete', ->
$('.loading-username').hide()
$(this).find('.btn-save').enable() $(this).find('.btn-save').enable()
$(this).find('.loading-gif').hide() $(this).find('.loading-gif').hide()
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
init: -> init: ->
$(".projects-list-filter").off('keyup') $(".projects-list-filter").off('keyup')
this.initSearch() this.initSearch()
this.initPagination()
initSearch: -> initSearch: ->
@timer = null @timer = null
...@@ -29,3 +30,8 @@ ...@@ -29,3 +30,8 @@
# Change url so if user reload a page - search results are saved # Change url so if user reload a page - search results are saved
history.replaceState {page: project_filter_url}, document.title, project_filter_url history.replaceState {page: project_filter_url}, document.title, project_filter_url
dataType: "json" dataType: "json"
initPagination: ->
$('.projects-list-holder .pagination').on('ajax:success', (e, data) ->
$('.projects-list-holder').replaceWith(data.html)
)
...@@ -4,11 +4,15 @@ class @Shortcuts ...@@ -4,11 +4,15 @@ class @Shortcuts
Mousetrap.reset() Mousetrap.reset()
Mousetrap.bind('?', @selectiveHelp) Mousetrap.bind('?', @selectiveHelp)
Mousetrap.bind('s', Shortcuts.focusSearch) Mousetrap.bind('s', Shortcuts.focusSearch)
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)
Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL? Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL?
selectiveHelp: (e) => selectiveHelp: (e) =>
Shortcuts.showHelp(e, @enabledHelp) Shortcuts.showHelp(e, @enabledHelp)
toggleMarkdownPreview: (e) =>
$(document).triggerHandler('markdown-preview:toggle', [e])
@showHelp: (e, location) -> @showHelp: (e, location) ->
if $('#modal-shortcuts').length > 0 if $('#modal-shortcuts').length > 0
$('#modal-shortcuts').modal('show') $('#modal-shortcuts').modal('show')
...@@ -35,3 +39,14 @@ $(document).on 'click.more_help', '.js-more-help-button', (e) -> ...@@ -35,3 +39,14 @@ $(document).on 'click.more_help', '.js-more-help-button', (e) ->
$(@).remove() $(@).remove()
$('.hidden-shortcut').show() $('.hidden-shortcut').show()
e.preventDefault() e.preventDefault()
Mousetrap.stopCallback = (->
defaultStopCallback = Mousetrap.stopCallback
return (e, element, combo) ->
# allowed shortcuts if textarea, input, contenteditable are focused
if ['ctrl+shift+p', 'command+shift+p'].indexOf(combo) != -1
return false
else
return defaultStopCallback.apply(@, arguments)
)()
...@@ -3,6 +3,81 @@ class @UsersSelect ...@@ -3,6 +3,81 @@ class @UsersSelect
@usersPath = "/autocomplete/users.json" @usersPath = "/autocomplete/users.json"
@userPath = "/autocomplete/users/:id.json" @userPath = "/autocomplete/users/:id.json"
$('.js-user-search').each (i, dropdown) =>
@projectId = $(dropdown).data('project-id')
@showCurrentUser = $(dropdown).data('current-user')
showNullUser = $(dropdown).data('null-user')
showAnyUser = $(dropdown).data('any-user')
firstUser = $(dropdown).data('first-user')
selectedId = $(dropdown).data('selected')
$(dropdown).glDropdown(
data: (term, callback) =>
@users term, (users) =>
if term.length is 0
showDivider = 0
if firstUser
# Move current user to the front of the list
for obj, index in users
if obj.username == firstUser
users.splice(index, 1)
users.unshift(obj)
break
if showNullUser
showDivider += 1
users.unshift(
name: 'Unassigned',
id: 0
)
if showAnyUser
showDivider += 1
name = showAnyUser
name = 'Any User' if name == true
anyUser = {
name: name,
id: null
}
users.unshift(anyUser)
if showDivider
users.splice(showDivider, 0, "divider")
# Send the data back
callback users
filterable: true
filterRemote: true
search:
fields: ['name', 'username']
selectable: true
fieldName: $(dropdown).data('field-name')
clicked: ->
if $(dropdown).hasClass "js-filter-submit"
$(dropdown).parents('form').submit()
renderRow: (user) ->
username = if user.username then "@#{user.username}" else ""
avatar = if user.avatar_url then user.avatar_url else false
selected = if user.id is selectedId then "is-active" else ""
img = ""
if avatar
img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
"<li>
<a href='#' class='dropdown-menu-user-link #{selected}'>
#{img}
<strong class='dropdown-menu-user-full-name'>
#{user.name}
</strong>
<span class='dropdown-menu-user-username'>
#{username}
</span>
</a>
</li>"
)
$('.ajax-users-select').each (i, select) => $('.ajax-users-select').each (i, select) =>
@projectId = $(select).data('project-id') @projectId = $(select).data('project-id')
@groupId = $(select).data('group-id') @groupId = $(select).data('group-id')
......
...@@ -28,6 +28,10 @@ ...@@ -28,6 +28,10 @@
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
color: $gl-gray; color: $gl-gray;
a {
color: $md-link-color;
}
&.oneline-block { &.oneline-block {
line-height: 42px; line-height: 42px;
} }
...@@ -153,3 +157,7 @@ ...@@ -153,3 +157,7 @@
float: right; float: right;
} }
} }
.content-block-small {
padding: 10px 0;
}
...@@ -12,11 +12,13 @@ ...@@ -12,11 +12,13 @@
.prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top:20px } .prepend-top-20 { margin-top:20px }
.prepend-left-10 { margin-left:10px } .prepend-left-10 { margin-left:10px }
.prepend-left-default { margin-left:$gl-padding } .prepend-left-default { margin-left: $gl-padding; }
.prepend-left-20 { margin-left:20px } .prepend-left-20 { margin-left:20px }
.append-right-5 { margin-right: 5px } .append-right-5 { margin-right: 5px }
.append-right-10 { margin-right:10px } .append-right-10 { margin-right:10px }
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right:20px } .append-right-20 { margin-right:20px }
.append-bottom-0 { margin-bottom:0 }
.append-bottom-10 { margin-bottom:10px } .append-bottom-10 { margin-bottom:10px }
.append-bottom-15 { margin-bottom:15px } .append-bottom-15 { margin-bottom:15px }
.append-bottom-20 { margin-bottom:20px } .append-bottom-20 { margin-bottom:20px }
......
...@@ -17,6 +17,47 @@ ...@@ -17,6 +17,47 @@
.dropdown-menu { .dropdown-menu {
display: block; display: block;
} }
.dropdown-menu-toggle {
border-color: $dropdown-toggle-hover-border-color;
.fa {
color: $dropdown-toggle-hover-icon-color;
}
}
}
.dropdown-menu-toggle {
position: relative;
width: 160px;
padding: 6px 20px 6px 10px;
background-color: $dropdown-toggle-bg;
color: $dropdown-toggle-color;
font-size: 15px;
text-align: left;
border: 1px solid $dropdown-toggle-border-color;
border-radius: 2px;
outline: 0;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
.fa {
position: absolute;
top: 50%;
right: 6px;
margin-top: -4px;
color: $dropdown-toggle-icon-color;
font-size: 10px;
}
&:hover, {
border-color: $dropdown-toggle-hover-border-color;
.fa {
color: $dropdown-toggle-hover-icon-color;
}
}
} }
.dropdown-menu { .dropdown-menu {
...@@ -24,7 +65,7 @@ ...@@ -24,7 +65,7 @@
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; left: 0;
z-index: 9999; z-index: 9;
width: 240px; width: 240px;
margin-top: 2px; margin-top: 2px;
margin-bottom: 0; margin-bottom: 0;
...@@ -36,6 +77,21 @@ ...@@ -36,6 +77,21 @@
border-radius: $border-radius-base; border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color; box-shadow: 0 2px 4px $dropdown-shadow-color;
&.is-loading {
.dropdown-content {
display: none;
}
.dropdown-loading {
display: block;
}
}
ul {
margin: 0;
padding: 0;
}
li { li {
text-align: left; text-align: left;
list-style: none; list-style: none;
...@@ -61,13 +117,70 @@ ...@@ -61,13 +117,70 @@
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
&:hover { &:hover,
&:focus,
&.is-focused {
background-color: $dropdown-link-hover-bg; background-color: $dropdown-link-hover-bg;
text-decoration: none; text-decoration: none;
outline: 0;
} }
} }
} }
.dropdown-menu-paging {
.dropdown-page-two,
.dropdown-menu-back {
display: none;
}
&.is-page-two {
.dropdown-page-one {
display: none;
}
.dropdown-page-two,
.dropdown-menu-back {
display: block;
}
}
}
.dropdown-menu-user {
.avatar {
float: left;
width: 30px;
height: 30px;
margin: 0 10px 0 0;
}
}
.dropdown-menu-user-link {
padding-top: 7px;
padding-bottom: 7px;
}
.dropdown-menu-user-full-name {
display: block;
margin-bottom: 2px;
font-weight: 600;
line-height: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.dropdown-menu-user-username {
display: block;
line-height: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.dropdown-select {
width: 280px;
}
.dropdown-menu-align-right { .dropdown-menu-align-right {
left: auto; left: auto;
right: 0; right: 0;
...@@ -81,8 +194,9 @@ ...@@ -81,8 +194,9 @@
&::before { &::before {
content: "\f00c"; content: "\f00c";
position: absolute; position: absolute;
left: 4px; left: 5px;
top: 8px; top: 50%;
margin-top: -7px;
font: normal normal normal 14px/1 FontAwesome; font: normal normal normal 14px/1 FontAwesome;
font-size: inherit; font-size: inherit;
text-rendering: auto; text-rendering: auto;
...@@ -94,9 +208,136 @@ ...@@ -94,9 +208,136 @@
} }
.dropdown-header { .dropdown-header {
padding-left: 10px; padding-left: 5px;
padding-right: 10px; padding-right: 5px;
color: $dropdown-header-color; color: $dropdown-header-color;
font-size: 13px; font-size: 13px;
line-height: 22px; line-height: 22px;
} }
.dropdown-title {
position: relative;
margin-bottom: 10px;
padding-left: 30px;
padding-right: 30px;
padding-bottom: 10px;
font-weight: 600;
line-height: 1;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
border-bottom: 1px solid $dropdown-divider-color;
overflow: hidden;
}
.dropdown-title-button {
position: absolute;
top: -1px;
padding: 0;
color: $dropdown-title-btn-color;
font-size: 14px;
border: 0;
background: none;
outline: 0;
&:hover {
color: darken($dropdown-title-btn-color, 15%);
}
}
.dropdown-menu-close {
right: 0;
}
.dropdown-menu-back {
left: 0;
}
.dropdown-input {
position: relative;
margin-bottom: 10px;
.fa {
position: absolute;
top: 10px;
right: 10px;
color: #C7C7C7;
font-size: 12px;
pointer-events: none;
}
}
.dropdown-input-field {
width: 100%;
padding: 0 7px;
color: $dropdown-input-color;
line-height: 30px;
border: 1px solid $dropdown-divider-color;
border-radius: 2px;
outline: 0;
&:focus {
color: $dropdown-link-color;
border-color: $dropdown-input-focus-border;
box-shadow: 0 0 4px $dropdown-input-focus-shadow;
+ .fa {
color: $dropdown-link-color;
}
}
&:hover {
+ .fa {
color: $dropdown-link-color;
}
}
}
.dropdown-content {
max-height: 215px;
overflow-y: scroll;
}
.dropdown-footer {
padding-top: 10px;
margin-top: 10px;
font-size: 13px;
border-top: 1px solid $dropdown-divider-color;
}
.dropdown-footer-list {
font-size: 14px;
a {
padding-left: 10px;
}
}
.dropdown-loading {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: none;
z-index: 9;
background-color: $dropdown-loading-bg;
font-size: 28px;
.fa {
position: absolute;
top: 50%;
left: 50%;
margin-top: -14px;
margin-left: -14px;
}
}
.dropdown-menu-labels {
.label {
position: relative;
width: 30px;
margin-right: 5px;
text-indent: -99999px;
}
}
...@@ -169,6 +169,7 @@ ...@@ -169,6 +169,7 @@
*/ */
&.code { &.code {
padding: 0; padding: 0;
-webkit-overflow-scrolling: auto; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
} }
} }
} }
......
.filter-item { .filter-item {
margin-right: 6px; margin-right: 6px;
vertical-align: top;
} }
@media (min-width: 800px) { @media (min-width: 800px) {
......
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
border-bottom: none; border-bottom: none;
/* Small devices (phones, tablets, 768px and lower) */ /* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-min) { @media (max-width: $screen-sm-max) {
width: 100%; width: 100%;
} }
} }
......
...@@ -149,13 +149,13 @@ ...@@ -149,13 +149,13 @@
} }
&:hover > a.anchor { &:hover > a.anchor {
$size: 16px; $size: 14px;
position: absolute; position: absolute;
right: 100%; right: 100%;
top: 50%; top: 50%;
margin-top: -$size/2; margin-top: -11px;
margin-right: 0px; margin-right: 0;
padding-right: 20px; padding-right: 15px;
display: inline-block; display: inline-block;
width: $size; width: $size;
height: $size; height: $size;
......
...@@ -34,13 +34,15 @@ $error-exclamation-point: #E62958; ...@@ -34,13 +34,15 @@ $error-exclamation-point: #E62958;
$border-radius-default: 3px; $border-radius-default: 3px;
$list-title-color: #333333; $list-title-color: #333333;
$list-text-color: #555555; $list-text-color: #555555;
$profile-settings-link-color: $md-link-color;
$btn-transparent-color: #8F8F8F; $btn-transparent-color: #8F8F8F;
$ssh-key-icon-color: #8F8F8F; $ssh-key-icon-color: #8F8F8F;
$ssh-key-icon-size: 18px; $ssh-key-icon-size: 18px;
$provider-btn-group-border: #E5E5E5;
$provider-btn-not-active-color: #4688F1;
/* /*
* Color schema * Color schema
*/ */
...@@ -70,7 +72,7 @@ $orange-light: rgba(252, 109, 38, 0.80); ...@@ -70,7 +72,7 @@ $orange-light: rgba(252, 109, 38, 0.80);
$orange-normal: #E75E40; $orange-normal: #E75E40;
$orange-dark: #CE5237; $orange-dark: #CE5237;
$red-light: #F43263; $red-light: #F06559;
$red-normal: #E52C5A; $red-normal: #E52C5A;
$red-dark: #D22852; $red-dark: #D22852;
...@@ -94,7 +96,7 @@ $border-orange-light: #fc6d26; ...@@ -94,7 +96,7 @@ $border-orange-light: #fc6d26;
$border-orange-normal: #CE5237; $border-orange-normal: #CE5237;
$border-orange-dark: #C14E35; $border-orange-dark: #C14E35;
$border-red-light: #E52C5A; $border-red-light: #F24F41;
$border-red-normal: #D22852; $border-red-normal: #D22852;
$border-red-dark: #CA264F; $border-red-dark: #CA264F;
...@@ -138,3 +140,22 @@ $dropdown-shadow-color: rgba(#000, .1); ...@@ -138,3 +140,22 @@ $dropdown-shadow-color: rgba(#000, .1);
$dropdown-divider-color: rgba(#000, .1); $dropdown-divider-color: rgba(#000, .1);
$dropdown-header-color: #959494; $dropdown-header-color: #959494;
$dropdown-caret-color: #54565B; $dropdown-caret-color: #54565B;
$dropdown-title-btn-color: #BFBFBF;
$dropdown-input-color: #C7C7C7;
$dropdown-input-focus-border: rgb(58, 171, 240);
$dropdown-input-focus-shadow: rgba(#000, .2);
$dropdown-loading-bg: rgba(#fff, .6);
$dropdown-toggle-bg: #fff;
$dropdown-toggle-color: #626262;
$dropdown-toggle-border-color: #EAEAEA;
$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
$dropdown-toggle-icon-color: #C4C4C4;
$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
/*
* Award emoji
*/
$award-emoji-menu-bg: #FFF;
$award-emoji-menu-border: #F1F2F4;
$award-emoji-new-btn-icon-color: #DCDCDC;
.awards { .awards {
@include clearfix;
line-height: 34px; line-height: 34px;
.emoji-icon { .emoji-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
margin: 7px 0 0 5px;
} }
}
.award { .emoji-menu {
@include border-radius(5px); position: absolute;
top: 100%;
border: 1px solid; left: 0;
padding: 0px 10px; margin-top: 3px;
float: left; z-index: 1000;
margin-right: 5px; min-width: 160px;
border-color: $border-color; font-size: 14px;
cursor: pointer; background-color: $award-emoji-menu-bg;
border: 1px solid $award-emoji-menu-border;
border-radius: $border-radius-base;
box-shadow: 0 6px 12px rgba(0,0,0,.175);
pointer-events: none;
opacity: 0;
transform: scale(.2);
transform-origin: 0 -45px;
transition: all .3s cubic-bezier(.87,-.41,.19,1.44);
&.is-visible {
pointer-events: all;
opacity: 1;
transform: scale(1);
}
&:hover { .emoji-menu-content {
background-color: #dce0e5; padding: $gl-padding;
width: 300px;
height: 300px;
overflow-y: scroll;
input.emoji-search{
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC");
background-repeat: no-repeat;
background-position: right 5px center;
background-size: 16px;
} }
}
}
&.active { .emoji-menu-list {
border-color: $border-gray-light; list-style: none;
background-color: $gray-light; padding-left: 0;
margin-bottom: 0;
&:hover { }
background-color: #dce0e5;
}
.counter { .emoji-menu-list-item {
font-weight: bold; padding: 3px;
} margin-left: 1px;
} margin-right: 1px;
}
.icon { .emoji-menu-btn {
float: left; display: block;
margin-right: 10px; cursor: pointer;
} width: 30px;
height: 30px;
padding: 0;
background: none;
border: 0;
border-radius: $border-radius-base;
transition: transform .15s cubic-bezier(.3, 0, .2, 2);
&:hover {
background-color: transparent;
outline: 0;
transform: scale(1.3);
}
.counter { &:focus,
float: left; &:active {
} outline: 0;
} }
.awards-controls { .emoji-icon {
display: inline-block;
position: relative; position: relative;
margin-left: 10px; top: 3px;
float: left; }
}
.add-award { .award-menu-holder {
font-size: 24px; display: inline-block;
color: $gl-gray; position: relative;
position: relative; }
top: 2px;
&:hover, .award-control {
&:link { margin-right: 5px;
text-decoration: none; padding-left: 5px;
} padding-right: 5px;
} line-height: 20px;
outline: 0;
&.active,
&:active {
background-color: $white-dark;
box-shadow: none;
outline: 0;
}
.emoji-menu{ &.is-loading {
position: absolute; .award-control-icon {
top: 100%;
left: 0;
z-index: 1000;
display: none; display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
font-size: 14px;
text-align: left;
list-style: none;
background-color: #fff;
-webkit-background-clip: padding-box;
background-clip: padding-box;
border: 1px solid #ccc;
border: 1px solid rgba(0,0,0,.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
box-shadow: 0 6px 12px rgba(0,0,0,.175);
.emoji-menu-content {
padding: $gl-padding;
width: 300px;
height: 300px;
overflow-y: scroll;
h5 {
clear: left;
}
ul {
list-style-type: none;
margin-left: -20px;
margin-bottom: 20px;
overflow: auto;
}
input.emoji-search{
background: image-url("icon-search.png") 240px no-repeat;
}
li {
cursor: pointer;
width: 30px;
height: 30px;
text-align: center;
float: left;
margin: 3px;
list-decorate: none;
@include border-radius(5px);
&:hover {
background-color: #ccc;
}
}
}
} }
.award-control-icon-loading {
display: block;
}
}
.icon,
.award-control-icon {
float: left;
margin-right: 5px;
font-size: 20px;
}
.award-control-icon-loading {
display: none;
}
.award-control-icon {
color: $award-emoji-new-btn-icon-color;
} }
} }
...@@ -27,10 +27,25 @@ ...@@ -27,10 +27,25 @@
} }
.scroll-controls { .scroll-controls {
position: fixed; &.affix-top {
bottom: 10px; position: absolute;
left: 250px; top: 10px;
z-index: 100; right: 25px;
}
&.affix-bottom {
position: absolute;
right: 25px;
}
&.affix {
right: 30px;
bottom: 15px;
@media (min-width: $screen-md-min) {
right: 26%;
}
}
a { a {
display: block; display: block;
......
...@@ -90,6 +90,7 @@ ...@@ -90,6 +90,7 @@
position: relative; position: relative;
font-family: $monospace_font; font-family: $monospace_font;
$left: 12px; $left: 12px;
overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
.max-width-marker { .max-width-marker {
width: 72ch; width: 72ch;
color: rgba(0, 0, 0, 0.0); color: rgba(0, 0, 0, 0.0);
......
...@@ -7,6 +7,28 @@ ...@@ -7,6 +7,28 @@
display: inline-block; display: inline-block;
margin-right: 10px; margin-right: 10px;
} }
&.suggest-colors-dropdown {
margin-bottom: 5px;
a {
@include border-radius(0);
width: 36.7px;
margin-right: 0;
margin-bottom: -5px;
}
}
}
.dropdown-label-color-preview {
display: none;
margin-top: 5px;
width: 100%;
height: 25px;
&.is-active {
display: block;
}
} }
.label-row { .label-row {
......
...@@ -19,10 +19,11 @@ li.milestone { ...@@ -19,10 +19,11 @@ li.milestone {
width: 105px; width: 105px;
} }
.issue-row { .issuable-row {
.color-label { .color-label {
border-radius: 2px; border-radius: 2px;
padding: 3px !important; padding: 3px !important;
margin-right: 7px;
} }
// Issue title // Issue title
...@@ -44,20 +45,15 @@ li.milestone { ...@@ -44,20 +45,15 @@ li.milestone {
} }
} }
.issues-sortable-list { .issues-sortable-list, .merge_requests-sortable-list {
.issue-detail { .issuable-detail {
display: block; display: block;
margin-top: 7px;
.issue-number{ .issuable-number {
color: rgba(0,0,0,0.44); color: rgba(0,0,0,0.44);
margin-right: 5px; margin-right: 5px;
} }
.color-label {
padding: 6px 10px;
margin-right: 7px;
margin-top: 10px;
}
.avatar { .avatar {
float: none; float: none;
} }
......
.account-page { .profile-avatar-form-option {
fieldset { hr {
margin-bottom: 15px; margin: 10px 0;
padding-bottom: 15px;
} }
} }
...@@ -20,7 +19,7 @@ ...@@ -20,7 +19,7 @@
.account-btn-link, .account-btn-link,
.profile-settings-sidebar a { .profile-settings-sidebar a {
color: $profile-settings-link-color; color: $md-link-color;
} }
.oauth-buttons { .oauth-buttons {
...@@ -172,6 +171,47 @@ ...@@ -172,6 +171,47 @@
.profile-settings-content { .profile-settings-content {
a { a {
color: $profile-settings-link-color; color: $md-link-color;
}
}
.change-username-title {
color: $gl-warning;
}
.remove-account-title {
color: $gl-danger;
}
.provider-btn-group {
display: inline-block;
margin-right: 10px;
border: 1px solid $provider-btn-group-border;
border-radius: 3px;
&:last-child {
margin-right: 0;
}
}
.provider-btn-image {
display: inline-block;
padding: 5px 10px;
border-right: 1px solid $provider-btn-group-border;
> img {
width: 20px;
}
}
.provider-btn {
display: inline-block;
padding: 5px 10px;
margin-left: -3px;
line-height: 22px;
background-color: $gray-light;
&.not-active {
color: $provider-btn-not-active-color;
} }
} }
...@@ -26,5 +26,13 @@ ...@@ -26,5 +26,13 @@
margin-right: 10px; margin-right: 10px;
font-size: $gl-font-size; font-size: $gl-font-size;
border: 1px solid; border: 1px solid;
line-height: 40px; line-height: 32px;
}
.markdown-snippet-copy {
position: fixed;
top: -10px;
left: -10px;
max-height: 0;
max-width: 0;
} }
...@@ -55,7 +55,7 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -55,7 +55,7 @@ class Admin::GroupsController < Admin::ApplicationController
private private
def group def group
@group = Group.find_by(path: params[:id]) @group ||= Group.find_by(path: params[:id])
end end
def group_params def group_params
......
...@@ -246,6 +246,8 @@ class ApplicationController < ActionController::Base ...@@ -246,6 +246,8 @@ class ApplicationController < ActionController::Base
def ldap_security_check def ldap_security_check
if current_user && current_user.requires_ldap_check? if current_user && current_user.requires_ldap_check?
return unless current_user.try_obtain_ldap_lease
unless Gitlab::LDAP::Access.allowed?(current_user) unless Gitlab::LDAP::Access.allowed?(current_user)
sign_out current_user sign_out current_user
flash[:alert] = "Access denied for your LDAP account." flash[:alert] = "Access denied for your LDAP account."
......
# == FilterProjects
#
# Controller concern to handle projects filtering
# * by name
# * by archived state
#
module FilterProjects
extend ActiveSupport::Concern
def filter_projects(projects)
projects = projects.search(params[:filter_projects]) if params[:filter_projects].present?
projects = projects.non_archived if params[:archived].blank?
projects
end
end
class Dashboard::ProjectsController < Dashboard::ApplicationController class Dashboard::ProjectsController < Dashboard::ApplicationController
include FilterProjects
before_action :event_filter before_action :event_filter
def index def index
@projects = current_user.authorized_projects.sorted_by_activity.non_archived @projects = current_user.authorized_projects.sorted_by_activity
@projects = @projects.sort(@sort = params[:sort]) @projects = filter_projects(@projects)
@projects = @projects.includes(:namespace) @projects = @projects.includes(:namespace)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE)
terms = params[:filter_projects]
if terms.present?
@projects = @projects.search(terms)
end
@projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank?
@last_push = current_user.recent_push @last_push = current_user.recent_push
respond_to do |format| respond_to do |format|
...@@ -32,16 +29,11 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -32,16 +29,11 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
def starred def starred
@projects = current_user.starred_projects.sorted_by_activity @projects = current_user.starred_projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE)
terms = params[:filter_projects]
if terms.present?
@projects = @projects.search(terms)
end
@projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank?
@last_push = current_user.recent_push @last_push = current_user.recent_push
@groups = [] @groups = []
......
class Explore::ProjectsController < Explore::ApplicationController class Explore::ProjectsController < Explore::ApplicationController
include FilterProjects
def index def index
@projects = ProjectsFinder.new.execute(current_user) @projects = ProjectsFinder.new.execute(current_user)
@tags = @projects.tags_on(:tags) @tags = @projects.tags_on(:tags)
@projects = @projects.tagged_with(params[:tag]) if params[:tag].present? @projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = @projects.non_archived @projects = filter_projects(@projects)
@projects = @projects.search(params[:search]) if params[:search].present?
@projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present?
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE)
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -22,9 +22,8 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -22,9 +22,8 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending def trending
@projects = TrendingProjectsFinder.new.execute(current_user) @projects = TrendingProjectsFinder.new.execute(current_user)
@projects = @projects.non_archived @projects = filter_projects(@projects)
@projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? @projects = @projects.page(params[:page]).per(PER_PAGE)
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -38,9 +37,9 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -38,9 +37,9 @@ class Explore::ProjectsController < Explore::ApplicationController
def starred def starred
@projects = ProjectsFinder.new.execute(current_user) @projects = ProjectsFinder.new.execute(current_user)
@projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? @projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC') @projects = @projects.reorder('star_count DESC')
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @projects = @projects.page(params[:page]).per(PER_PAGE)
respond_to do |format| respond_to do |format|
format.html format.html
......
class GroupsController < Groups::ApplicationController class GroupsController < Groups::ApplicationController
include FilterProjects
include IssuesAction include IssuesAction
include MergeRequestsAction include MergeRequestsAction
...@@ -14,7 +15,7 @@ class GroupsController < Groups::ApplicationController ...@@ -14,7 +15,7 @@ class GroupsController < Groups::ApplicationController
# Load group projects # Load group projects
before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete] before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete]
before_action :event_filter, only: [:show, :events] before_action :event_filter, only: [:activity]
layout :determine_layout layout :determine_layout
...@@ -41,7 +42,8 @@ class GroupsController < Groups::ApplicationController ...@@ -41,7 +42,8 @@ class GroupsController < Groups::ApplicationController
def show def show
@last_push = current_user.recent_push if current_user @last_push = current_user.recent_push if current_user
@projects = @projects.includes(:namespace) @projects = @projects.includes(:namespace)
@projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? @projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
respond_to do |format| respond_to do |format|
...@@ -60,8 +62,10 @@ class GroupsController < Groups::ApplicationController ...@@ -60,8 +62,10 @@ class GroupsController < Groups::ApplicationController
end end
end end
def events def activity
respond_to do |format| respond_to do |format|
format.html
format.json do format.json do
load_events load_events
pager_json("events/_events", @events.count) pager_json("events/_events", @events.count)
...@@ -98,7 +102,7 @@ class GroupsController < Groups::ApplicationController ...@@ -98,7 +102,7 @@ class GroupsController < Groups::ApplicationController
end end
def load_projects def load_projects
@projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity.non_archived @projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity
end end
# Dont allow unauthorized access to group # Dont allow unauthorized access to group
......
...@@ -7,6 +7,9 @@ class Projects::AvatarsController < Projects::ApplicationController ...@@ -7,6 +7,9 @@ class Projects::AvatarsController < Projects::ApplicationController
@blob = @repository.blob_at_branch('master', @project.avatar_in_git) @blob = @repository.blob_at_branch('master', @project.avatar_in_git)
if @blob if @blob
headers['X-Content-Type-Options'] = 'nosniff' headers['X-Content-Type-Options'] = 'nosniff'
return if cached_blob?
headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob)) headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
headers['Content-Disposition'] = 'inline' headers['Content-Disposition'] = 'inline'
headers['Content-Type'] = safe_content_type(@blob) headers['Content-Type'] = safe_content_type(@blob)
......
...@@ -32,10 +32,6 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -32,10 +32,6 @@ class Projects::MilestonesController < Projects::ApplicationController
end end
def show def show
@issues = @milestone.issues
@users = @milestone.participants.uniq
@merge_requests = @milestone.merge_requests
@labels = @milestone.labels
end end
def create def create
......
...@@ -13,6 +13,8 @@ class Projects::RawController < Projects::ApplicationController ...@@ -13,6 +13,8 @@ class Projects::RawController < Projects::ApplicationController
if @blob if @blob
headers['X-Content-Type-Options'] = 'nosniff' headers['X-Content-Type-Options'] = 'nosniff'
return if cached_blob?
if @blob.lfs_pointer? if @blob.lfs_pointer?
send_lfs_object send_lfs_object
else else
......
class ProjectsController < ApplicationController class ProjectsController < ApplicationController
include ExtractsPath include ExtractsPath
prepend_before_action :render_go_import, only: [:show]
skip_before_action :authenticate_user!, only: [:show, :activity] skip_before_action :authenticate_user!, only: [:show, :activity]
before_action :project, except: [:new, :create] before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create] before_action :repository, except: [:new, :create]
...@@ -242,16 +241,6 @@ class ProjectsController < ApplicationController ...@@ -242,16 +241,6 @@ class ProjectsController < ApplicationController
end end
end end
def render_go_import
return unless params["go-get"] == "1"
@namespace = params[:namespace_id]
@id = params[:project_id] || params[:id]
@id = @id.gsub(/\.git\Z/, "")
render "go_import", layout: false
end
def repo_exists? def repo_exists?
project.repository_exists? && !project.empty_repo? project.repository_exists? && !project.empty_repo?
end end
......
...@@ -244,10 +244,17 @@ class IssuableFinder ...@@ -244,10 +244,17 @@ class IssuableFinder
items items
end end
def filter_by_upcoming_milestone?
params[:milestone_title] == '#upcoming'
end
def by_milestone(items) def by_milestone(items)
if milestones? if milestones?
if filter_by_no_milestone? if filter_by_no_milestone?
items = items.where(milestone_id: [-1, nil]) items = items.where(milestone_id: [-1, nil])
elsif filter_by_upcoming_milestone?
upcoming = Milestone.where(project_id: projects).upcoming
items = items.joins(:milestone).where(milestones: { title: upcoming.title })
else else
items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
...@@ -263,11 +270,9 @@ class IssuableFinder ...@@ -263,11 +270,9 @@ class IssuableFinder
def by_label(items) def by_label(items)
if labels? if labels?
if filter_by_no_label? if filter_by_no_label?
items = items. items = items.without_label
joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{klass.name}' AND label_links.target_id = #{klass.table_name}.id").
where(label_links: { id: nil })
else else
items = items.joins(:labels).where(labels: { title: label_names }) items = items.with_label(label_names)
if projects if projects
items = items.where(labels: { project_id: projects }) items = items.where(labels: { project_id: projects })
......
...@@ -52,7 +52,10 @@ class ProjectsFinder ...@@ -52,7 +52,10 @@ class ProjectsFinder
def all_projects(current_user) def all_projects(current_user)
if current_user if current_user
[current_user.authorized_projects, public_and_internal_projects] [
current_user.authorized_projects,
public_and_internal_projects
]
else else
[Project.public_only] [Project.public_only]
end end
......
...@@ -4,7 +4,7 @@ class SnippetsFinder ...@@ -4,7 +4,7 @@ class SnippetsFinder
case filter case filter
when :all then when :all then
snippets(current_user).fresh.non_expired snippets(current_user).fresh
when :by_user then when :by_user then
by_user(current_user, params[:user], params[:scope]) by_user(current_user, params[:user], params[:scope])
when :by_project when :by_project
...@@ -27,7 +27,7 @@ class SnippetsFinder ...@@ -27,7 +27,7 @@ class SnippetsFinder
end end
def by_user(current_user, user, scope) def by_user(current_user, user, scope)
snippets = user.snippets.fresh.non_expired snippets = user.snippets.fresh
return snippets.are_public unless current_user return snippets.are_public unless current_user
...@@ -48,7 +48,7 @@ class SnippetsFinder ...@@ -48,7 +48,7 @@ class SnippetsFinder
end end
def by_project(current_user, project) def by_project(current_user, project)
snippets = project.snippets.fresh.non_expired snippets = project.snippets.fresh
if current_user if current_user
if project.team.member?(current_user.id) if project.team.member?(current_user.id)
......
...@@ -72,7 +72,7 @@ module ApplicationHelper ...@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User) if user_or_email.is_a?(User)
user = user_or_email user = user_or_email
else else
user = User.find_by(email: user_or_email.downcase) user = User.find_by_any_email(user_or_email.try(:downcase))
end end
if user if user
......
...@@ -152,4 +152,25 @@ module BlobHelper ...@@ -152,4 +152,25 @@ module BlobHelper
'application/octet-stream' 'application/octet-stream'
end end
end end
def cached_blob?
stale = stale?(etag: @blob.id) # The #stale? method sets cache headers.
# Because we are opionated we set the cache headers ourselves.
response.cache_control[:public] = @project.public?
if @ref && @commit && @ref == @commit.id
# This is a link to a commit by its commit SHA. That means that the blob
# is immutable. The only reason to invalidate the cache is if the commit
# was deleted or if the user lost access to the repository.
response.cache_control[:max_age] = Blob::CACHE_TIME_IMMUTABLE
else
# A branch or tag points at this blob. That means that the expected blob
# value may change over time.
response.cache_control[:max_age] = Blob::CACHE_TIME
end
response.etag = @blob.id
!stale
end
end end
...@@ -42,12 +42,12 @@ module CiStatusHelper ...@@ -42,12 +42,12 @@ module CiStatusHelper
icon(icon_name + ' fw') icon(icon_name + ' fw')
end end
def render_ci_status(ci_commit) def render_ci_status(ci_commit, tooltip_placement: 'auto left')
link_to ci_status_icon(ci_commit), link_to ci_status_icon(ci_commit),
ci_status_path(ci_commit), ci_status_path(ci_commit),
class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}",
title: "Build #{ci_status_label(ci_commit)}", title: "Build #{ci_status_label(ci_commit)}",
data: { toggle: 'tooltip', placement: 'left' } data: { toggle: 'tooltip', placement: tooltip_placement }
end end
def no_runners_for_project?(project) def no_runners_for_project?(project)
......
...@@ -211,4 +211,15 @@ module CommitsHelper ...@@ -211,4 +211,15 @@ module CommitsHelper
def clean(string) def clean(string)
Sanitize.clean(string, remove_contents: true) Sanitize.clean(string, remove_contents: true)
end end
def limited_commits(commits)
if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
[
commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE),
commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE
]
else
[commits, 0]
end
end
end end
module DropdownsHelper
def dropdown_tag(toggle_text, options: {}, &block)
content_tag :div, class: "dropdown" do
data_attr = { toggle: "dropdown" }
if options.has_key?(:data)
data_attr = options[:data].merge(data_attr)
end
dropdown_output = dropdown_toggle(toggle_text, data_attr, options)
dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") do
output = ""
if options.has_key?(:title)
output << dropdown_title(options[:title])
end
if options.has_key?(:filter)
output << dropdown_filter(options[:placeholder])
end
output << content_tag(:div, class: "dropdown-content") do
capture(&block) if block && !options.has_key?(:footer_content)
end
if block && options.has_key?(:footer_content)
output << content_tag(:div, class: "dropdown-footer") do
capture(&block)
end
end
output << dropdown_loading
output.html_safe
end
dropdown_output.html_safe
end
end
def dropdown_toggle(toggle_text, data_attr, options)
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text")
output << icon('chevron-down')
output.html_safe
end
end
def dropdown_title(title, back: false)
content_tag :div, class: "dropdown-title" do
title_output = ""
if back
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do
icon('arrow-left')
end
end
title_output << content_tag(:span, title)
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
icon('times')
end
title_output.html_safe
end
end
def dropdown_filter(placeholder)
content_tag :div, class: "dropdown-input" do
filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder
filter_output << icon('search')
filter_output.html_safe
end
end
def dropdown_content(&block)
content_tag(:div, class: "dropdown-content") do
if block
capture(&block)
end
end
end
def dropdown_footer(&block)
content_tag(:div, class: "dropdown-footer") do
if block
capture(&block)
end
end
end
def dropdown_loading
content_tag :div, class: "dropdown-loading" do
icon('spinner spin')
end
end
end
module ExploreHelper module ExploreHelper
def explore_projects_filter_path(options={}) def filter_projects_path(options={})
exist_opts = { exist_opts = {
sort: params[:sort], sort: params[:sort],
scope: params[:scope], scope: params[:scope],
...@@ -9,15 +9,7 @@ module ExploreHelper ...@@ -9,15 +9,7 @@ module ExploreHelper
} }
options = exist_opts.merge(options) options = exist_opts.merge(options)
path = request.path
path = if explore_controller?
explore_projects_path
elsif current_action?(:starred)
starred_dashboard_projects_path
else
dashboard_projects_path
end
path << "?#{options.to_param}" path << "?#{options.to_param}"
path path
end end
......
...@@ -9,6 +9,32 @@ module MilestonesHelper ...@@ -9,6 +9,32 @@ module MilestonesHelper
end end
end end
def milestones_label_path(opts = {})
if @project
namespace_project_issues_path(@project.namespace, @project, opts)
elsif @group
issues_group_path(@group, opts)
else
issues_dashboard_path(opts)
end
end
def milestones_browse_issuables_path(milestone, type:)
opts = { milestone_title: milestone.title }
if @project
polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts)
elsif @group
polymorphic_url([type, @group], opts)
else
polymorphic_url([type, :dashboard], opts)
end
end
def milestone_issues_by_label_count(milestone, label, state:)
milestone.issues.with_label(label.title).send(state).size
end
def milestone_progress_bar(milestone) def milestone_progress_bar(milestone)
options = { options = {
class: 'progress-bar progress-bar-success', class: 'progress-bar progress-bar-success',
...@@ -33,6 +59,7 @@ module MilestonesHelper ...@@ -33,6 +59,7 @@ module MilestonesHelper
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
grouped_milestones.unshift(Milestone::None) grouped_milestones.unshift(Milestone::None)
grouped_milestones.unshift(Milestone::Any) grouped_milestones.unshift(Milestone::Any)
grouped_milestones.unshift(Milestone::Upcoming)
options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title]) options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
end end
......
...@@ -40,7 +40,7 @@ module SearchHelper ...@@ -40,7 +40,7 @@ module SearchHelper
{ label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") }, { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") },
{ label: "help: SSH Keys Help", url: help_page_path("ssh", "README") }, { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") },
{ label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
{ label: "help: Web Hooks Help", url: help_page_path("web_hooks", "web_hooks") }, { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
{ label: "help: Workflow Help", url: help_page_path("workflow", "README") }, { label: "help: Workflow Help", url: help_page_path("workflow", "README") },
] ]
end end
......
module SnippetsHelper module SnippetsHelper
def lifetime_select_options
options = [
['forever', nil],
['1 day', "#{Date.current + 1.day}"],
['1 week', "#{Date.current + 1.week}"],
['1 month', "#{Date.current + 1.month}"]
]
options_for_select(options)
end
def reliable_snippet_path(snippet) def reliable_snippet_path(snippet)
if snippet.project_id? if snippet.project_id?
namespace_project_snippet_path(snippet.project.namespace, namespace_project_snippet_path(snippet.project.namespace,
......
...@@ -16,6 +16,16 @@ module SortingHelper ...@@ -16,6 +16,16 @@ module SortingHelper
} }
end end
def projects_sort_options_hash
{
sort_value_name => sort_title_name,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created,
}
end
def sort_title_oldest_updated def sort_title_oldest_updated
'Oldest updated' 'Oldest updated'
end end
......
...@@ -9,6 +9,7 @@ class Ability ...@@ -9,6 +9,7 @@ class Ability
when CommitStatus then commit_status_abilities(user, subject) when CommitStatus then commit_status_abilities(user, subject)
when Project then project_abilities(user, subject) when Project then project_abilities(user, subject)
when Issue then issue_abilities(user, subject) when Issue then issue_abilities(user, subject)
when ExternalIssue then external_issue_abilities(user, subject)
when Note then note_abilities(user, subject) when Note then note_abilities(user, subject)
when ProjectSnippet then project_snippet_abilities(user, subject) when ProjectSnippet then project_snippet_abilities(user, subject)
when PersonalSnippet then personal_snippet_abilities(user, subject) when PersonalSnippet then personal_snippet_abilities(user, subject)
...@@ -424,6 +425,10 @@ class Ability ...@@ -424,6 +425,10 @@ class Ability
end end
end end
def external_issue_abilities(user, subject)
project_abilities(user, subject.project)
end
private private
def named_abilities(name) def named_abilities(name)
......
# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects # Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects
class Blob < SimpleDelegator class Blob < SimpleDelegator
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
# Wrap a Gitlab::Git::Blob object, or return nil when given nil # Wrap a Gitlab::Git::Blob object, or return nil when given nil
# #
# This method prevents the decorated object from evaluating to "truthy" when # This method prevents the decorated object from evaluating to "truthy" when
......
...@@ -23,7 +23,7 @@ module Ci ...@@ -23,7 +23,7 @@ module Ci
LAST_CONTACT_TIME = 5.minutes.ago LAST_CONTACT_TIME = 5.minutes.ago
AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online'] AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online']
has_many :builds, class_name: 'Ci::Build' has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id
...@@ -46,9 +46,23 @@ module Ci ...@@ -46,9 +46,23 @@ module Ci
acts_as_taggable acts_as_taggable
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# This method performs a *partial* match on tokens, thus a query for "a"
# will match any runner where the token contains the letter "a". As a result
# you should *not* use this method for non-admin purposes as otherwise users
# might be able to query a list of all runners.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def self.search(query) def self.search(query)
where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query', t = arel_table
query: "%#{query.try(:downcase)}%") pattern = "%#{query}%"
where(t[:token].matches(pattern).or(t[:description].matches(pattern)))
end end
def set_default_values def set_default_values
......
...@@ -29,12 +29,15 @@ module Issuable ...@@ -29,12 +29,15 @@ module Issuable
scope :assigned, -> { where("assignee_id IS NOT NULL") } scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") } scope :unassigned, -> { where("assignee_id IS NULL") }
scope :of_projects, ->(ids) { where(project_id: ids) } scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :opened, -> { with_state(:opened, :reopened) } scope :opened, -> { with_state(:opened, :reopened) }
scope :only_opened, -> { with_state(:opened) } scope :only_opened, -> { with_state(:opened) }
scope :only_reopened, -> { with_state(:reopened) } scope :only_reopened, -> { with_state(:reopened) }
scope :closed, -> { with_state(:closed) } scope :closed, -> { with_state(:closed) }
scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') } scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') }
scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') } scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') }
scope :with_label, ->(title) { joins(:labels).where(labels: { title: title }) }
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :join_project, -> { joins(:project) } scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) } scope :references_project, -> { references(:project) }
...@@ -58,12 +61,29 @@ module Issuable ...@@ -58,12 +61,29 @@ module Issuable
end end
module ClassMethods module ClassMethods
# Searches for records with a matching title.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
where("LOWER(title) like :query", query: "%#{query.downcase}%") where(arel_table[:title].matches("%#{query}%"))
end end
# Searches for records with a matching title or description.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def full_search(query) def full_search(query)
where("LOWER(title) like :query OR LOWER(description) like :query", query: "%#{query.downcase}%") t = arel_table
pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end end
def sort(method) def sort(method)
......
module Milestoneish
def closed_items_count
issues.closed.size + merge_requests.closed_and_merged.size
end
def total_items_count
issues.size + merge_requests.size
end
def complete?
total_items_count == closed_items_count
end
def percent_complete
((closed_items_count * 100) / total_items_count).abs
rescue ZeroDivisionError
0
end
def remaining_days
return 0 if !due_date || expired?
(due_date - Date.today).to_i
end
end
...@@ -2,16 +2,19 @@ class GlobalLabel ...@@ -2,16 +2,19 @@ class GlobalLabel
attr_accessor :title, :labels attr_accessor :title, :labels
alias_attribute :name, :title alias_attribute :name, :title
delegate :color, :description, to: :@first_label
def self.build_collection(labels) def self.build_collection(labels)
labels = labels.group_by(&:title) labels = labels.group_by(&:title)
labels.map do |title, label| labels.map do |title, labels|
new(title, label) new(title, labels)
end end
end end
def initialize(title, labels) def initialize(title, labels)
@title = title @title = title
@labels = labels @labels = labels
@first_label = labels.find { |lbl| lbl.description.present? } || labels.first
end end
end end
class GlobalMilestone class GlobalMilestone
include Milestoneish
attr_accessor :title, :milestones attr_accessor :title, :milestones
alias_attribute :name, :title alias_attribute :name, :title
...@@ -28,33 +30,7 @@ class GlobalMilestone ...@@ -28,33 +30,7 @@ class GlobalMilestone
end end
def projects def projects
milestones.map { |milestone| milestone.project } @projects ||= Project.for_milestones(milestones.map(&:id))
end
def issue_count
milestones.map { |milestone| milestone.issues.count }.sum
end
def merge_requests_count
milestones.map { |milestone| milestone.merge_requests.count }.sum
end
def open_items_count
milestones.map { |milestone| milestone.open_items_count }.sum
end
def closed_items_count
milestones.map { |milestone| milestone.closed_items_count }.sum
end
def total_items_count
milestones.map { |milestone| milestone.total_items_count }.sum
end
def percent_complete
((closed_items_count * 100) / total_items_count).abs
rescue ZeroDivisionError
0
end end
def state def state
...@@ -76,35 +52,20 @@ class GlobalMilestone ...@@ -76,35 +52,20 @@ class GlobalMilestone
end end
def issues def issues
@issues ||= milestones.map(&:issues).flatten.group_by(&:state) @issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project)
end end
def merge_requests def merge_requests
@merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state) @merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project)
end end
def participants def participants
@participants ||= milestones.map(&:participants).flatten.compact.uniq @participants ||= milestones.map(&:participants).flatten.compact.uniq
end end
def opened_issues def labels
issues.values_at("opened", "reopened").compact.flatten @labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten)
end .sort_by!(&:title)
def closed_issues
issues['closed']
end
def opened_merge_requests
merge_requests.values_at("opened", "reopened").compact.flatten
end
def closed_merge_requests
merge_requests.values_at("closed", "merged", "locked").compact.flatten
end
def complete?
total_items_count == closed_items_count
end end
def due_date def due_date
......
...@@ -33,8 +33,18 @@ class Group < Namespace ...@@ -33,8 +33,18 @@ class Group < Namespace
after_destroy :post_destroy_hook after_destroy :post_destroy_hook
class << self class << self
# Searches for groups matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
where("LOWER(namespaces.name) LIKE :query or LOWER(namespaces.path) LIKE :query", query: "%#{query.downcase}%") table = Namespace.arel_table
pattern = "%#{query}%"
where(table[:name].matches(pattern).or(table[:path].matches(pattern)))
end end
def sort(method) def sort(method)
......
...@@ -137,11 +137,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -137,11 +137,8 @@ class MergeRequest < ActiveRecord::Base
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
scope :of_projects, ->(ids) { where(target_project_id: ids) } scope :of_projects, ->(ids) { where(target_project_id: ids) }
scope :opened, -> { with_states(:opened, :reopened) }
scope :merged, -> { with_state(:merged) } scope :merged, -> { with_state(:merged) }
scope :closed, -> { with_state(:closed) }
scope :closed_and_merged, -> { with_states(:closed, :merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) }
scope :join_project, -> { joins(:target_project) } scope :join_project, -> { joins(:target_project) }
...@@ -165,6 +162,24 @@ class MergeRequest < ActiveRecord::Base ...@@ -165,6 +162,24 @@ class MergeRequest < ActiveRecord::Base
super("merge_requests", /(?<merge_request>\d+)/) super("merge_requests", /(?<merge_request>\d+)/)
end end
# Returns all the merge requests from an ActiveRecord:Relation.
#
# This method uses a UNION as it usually operates on the result of
# ProjectsFinder#execute. PostgreSQL in particular doesn't always like queries
# using multiple sub-queries especially when combined with an OR statement.
# UNIONs on the other hand perform much better in these cases.
#
# relation - An ActiveRecord::Relation that returns a list of Projects.
#
# Returns an ActiveRecord::Relation.
def self.in_projects(relation)
source = where(source_project_id: relation).select(:id)
target = where(target_project_id: relation).select(:id)
union = Gitlab::SQL::Union.new([source, target])
where("merge_requests.id IN (#{union.to_sql})")
end
def to_reference(from_project = nil) def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
......
...@@ -17,7 +17,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -17,7 +17,7 @@ class MergeRequestDiff < ActiveRecord::Base
include Sortable include Sortable
# Prevent store of diff if commits amount more then 500 # Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 500 COMMITS_SAFE_SIZE = 100
belongs_to :merge_request belongs_to :merge_request
......
...@@ -19,17 +19,19 @@ class Milestone < ActiveRecord::Base ...@@ -19,17 +19,19 @@ class Milestone < ActiveRecord::Base
MilestoneStruct = Struct.new(:title, :name, :id) MilestoneStruct = Struct.new(:title, :name, :id)
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0) None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1) Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
include InternalId include InternalId
include Sortable include Sortable
include Referable include Referable
include StripAttribute include StripAttribute
include Milestoneish
belongs_to :project belongs_to :project
has_many :issues has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests has_many :merge_requests
has_many :participants, through: :issues, source: :assignee has_many :participants, -> { distinct.reorder('users.name') }, through: :issues, source: :assignee
scope :active, -> { with_state(:active) } scope :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) } scope :closed, -> { with_state(:closed) }
...@@ -57,9 +59,18 @@ class Milestone < ActiveRecord::Base ...@@ -57,9 +59,18 @@ class Milestone < ActiveRecord::Base
alias_attribute :name, :title alias_attribute :name, :title
class << self class << self
# Searches for milestones matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
query = "%#{query}%" t = arel_table
where("title like ? or description like ?", query, query) pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end end
end end
...@@ -71,6 +82,10 @@ class Milestone < ActiveRecord::Base ...@@ -71,6 +82,10 @@ class Milestone < ActiveRecord::Base
super("milestones", /(?<milestone>\d+)/) super("milestones", /(?<milestone>\d+)/)
end end
def self.upcoming
self.where('due_date > ?', Time.now).order(due_date: :asc).first
end
def to_reference(from_project = nil) def to_reference(from_project = nil)
escaped_title = self.title.gsub("]", "\\]") escaped_title = self.title.gsub("]", "\\]")
...@@ -92,30 +107,6 @@ class Milestone < ActiveRecord::Base ...@@ -92,30 +107,6 @@ class Milestone < ActiveRecord::Base
end end
end end
def open_items_count
self.issues.opened.count + self.merge_requests.opened.count
end
def closed_items_count
self.issues.closed.count + self.merge_requests.closed_and_merged.count
end
def total_items_count
self.issues.count + self.merge_requests.count
end
def percent_complete
((closed_items_count * 100) / total_items_count).abs
rescue ZeroDivisionError
0
end
def remaining_days
return 0 if !due_date || expired?
(due_date - Date.today).to_i
end
def expires_at def expires_at
if due_date if due_date
if due_date.past? if due_date.past?
......
...@@ -52,8 +52,18 @@ class Namespace < ActiveRecord::Base ...@@ -52,8 +52,18 @@ class Namespace < ActiveRecord::Base
find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
end end
# Searches for namespaces matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation
def search(query) def search(query)
where("name LIKE :query OR path LIKE :query", query: "%#{query}%") t = arel_table
pattern = "%#{query}%"
where(t[:name].matches(pattern).or(t[:path].matches(pattern)))
end end
def clean_path(path) def clean_path(path)
......
...@@ -44,6 +44,7 @@ class Note < ActiveRecord::Base ...@@ -44,6 +44,7 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true delegate :name, :email, to: :author, prefix: true
before_validation :set_award! before_validation :set_award!
before_validation :clear_blank_line_code!
validates :note, :project, presence: true validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
...@@ -63,7 +64,7 @@ class Note < ActiveRecord::Base ...@@ -63,7 +64,7 @@ class Note < ActiveRecord::Base
scope :nonawards, ->{ where(is_award: false) } scope :nonawards, ->{ where(is_award: false) }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :inline, ->{ where("line_code IS NOT NULL") } scope :inline, ->{ where("line_code IS NOT NULL") }
scope :not_inline, ->{ where(line_code: [nil, '']) } scope :not_inline, ->{ where(line_code: nil) }
scope :system, ->{ where(system: true) } scope :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) } scope :user, ->{ where(system: false) }
scope :common, ->{ where(noteable_type: ["", nil]) } scope :common, ->{ where(noteable_type: ["", nil]) }
...@@ -105,8 +106,18 @@ class Note < ActiveRecord::Base ...@@ -105,8 +106,18 @@ class Note < ActiveRecord::Base
[:discussion, type.try(:underscore), id, line_code].join("-").to_sym [:discussion, type.try(:underscore), id, line_code].join("-").to_sym
end end
# Searches for notes matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
where("LOWER(note) like :query", query: "%#{query.downcase}%") table = arel_table
pattern = "%#{query}%"
where(table[:note].matches(pattern))
end end
def grouped_awards def grouped_awards
...@@ -365,6 +376,10 @@ class Note < ActiveRecord::Base ...@@ -365,6 +376,10 @@ class Note < ActiveRecord::Base
private private
def clear_blank_line_code!
self.line_code = nil if self.line_code.blank?
end
def awards_supported? def awards_supported?
(for_issue? || for_merge_request?) && !for_diff_line? (for_issue? || for_merge_request?) && !for_diff_line?
end end
......
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# file_name :string(255) # file_name :string(255)
# expires_at :datetime
# type :string(255) # type :string(255)
# visibility_level :integer default(0), not null # visibility_level :integer default(0), not null
# #
......
...@@ -151,6 +151,7 @@ class Project < ActiveRecord::Base ...@@ -151,6 +151,7 @@ class Project < ActiveRecord::Base
has_many :releases, dependent: :destroy has_many :releases, dependent: :destroy
has_many :lfs_objects_projects, dependent: :destroy has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects has_many :lfs_objects, through: :lfs_objects_projects
has_many :todos, dependent: :destroy
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
...@@ -215,6 +216,7 @@ class Project < ActiveRecord::Base ...@@ -215,6 +216,7 @@ class Project < ActiveRecord::Base
scope :public_only, -> { where(visibility_level: Project::PUBLIC) } scope :public_only, -> { where(visibility_level: Project::PUBLIC) }
scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) } scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) }
scope :non_archived, -> { where(archived: false) } scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
state_machine :import_status, initial: :none do state_machine :import_status, initial: :none do
event :import_start do event :import_start do
...@@ -264,13 +266,31 @@ class Project < ActiveRecord::Base ...@@ -264,13 +266,31 @@ class Project < ActiveRecord::Base
joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC')
end end
# Searches for a list of projects based on the query given in `query`.
#
# On PostgreSQL this method uses "ILIKE" to perform a case-insensitive
# search. On MySQL a regular "LIKE" is used as it's already
# case-insensitive.
#
# query - The search query as a String.
def search(query) def search(query)
joins(:namespace). ptable = arel_table
where('LOWER(projects.name) LIKE :query OR ntable = Namespace.arel_table
LOWER(projects.path) LIKE :query OR pattern = "%#{query}%"
LOWER(namespaces.name) LIKE :query OR
LOWER(projects.description) LIKE :query', projects = select(:id).where(
query: "%#{query.try(:downcase)}%") ptable[:path].matches(pattern).
or(ptable[:name].matches(pattern)).
or(ptable[:description].matches(pattern))
)
namespaces = select(:id).
joins(:namespace).
where(ntable[:name].matches(pattern))
union = Gitlab::SQL::Union.new([projects, namespaces])
where("projects.id IN (#{union.to_sql})")
end end
def search_by_visibility(level) def search_by_visibility(level)
...@@ -278,7 +298,10 @@ class Project < ActiveRecord::Base ...@@ -278,7 +298,10 @@ class Project < ActiveRecord::Base
end end
def search_by_title(query) def search_by_title(query)
non_archived.where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%") pattern = "%#{query}%"
table = Project.arel_table
non_archived.where(table[:name].matches(pattern))
end end
def find_with_namespace(id) def find_with_namespace(id)
...@@ -526,11 +549,11 @@ class Project < ActiveRecord::Base ...@@ -526,11 +549,11 @@ class Project < ActiveRecord::Base
end end
def ci_services def ci_services
services.select { |service| service.category == :ci } services.where(category: :ci)
end end
def ci_service def ci_service
@ci_service ||= ci_services.find(&:activated?) @ci_service ||= ci_services.reorder(nil).find_by(active: true)
end end
def jira_tracker? def jira_tracker?
...@@ -907,13 +930,13 @@ class Project < ActiveRecord::Base ...@@ -907,13 +930,13 @@ class Project < ActiveRecord::Base
end end
def valid_runners_token? token def valid_runners_token? token
self.runners_token && self.runners_token == token self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end end
# TODO (ayufan): For now we use runners_token (backward compatibility) # TODO (ayufan): For now we use runners_token (backward compatibility)
# In 8.4 every build will have its own individual token valid for time of build # In 8.4 every build will have its own individual token valid for time of build
def valid_build_token? token def valid_build_token? token
self.builds_enabled? && self.runners_token && self.runners_token == token self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end end
def build_coverage_enabled? def build_coverage_enabled?
......
...@@ -26,7 +26,7 @@ class CiService < Service ...@@ -26,7 +26,7 @@ class CiService < Service
default_value_for :category, 'ci' default_value_for :category, 'ci'
def valid_token?(token) def valid_token?(token)
self.respond_to?(:token) && self.token.present? && self.token == token self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end end
def supported_events def supported_events
......
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# file_name :string(255) # file_name :string(255)
# expires_at :datetime
# type :string(255) # type :string(255)
# visibility_level :integer default(0), not null # visibility_level :integer default(0), not null
# #
...@@ -23,6 +22,4 @@ class ProjectSnippet < Snippet ...@@ -23,6 +22,4 @@ class ProjectSnippet < Snippet
# Scopes # Scopes
scope :fresh, -> { order("created_at DESC") } scope :fresh, -> { order("created_at DESC") }
scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) }
scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) }
end end
...@@ -133,18 +133,18 @@ class Repository ...@@ -133,18 +133,18 @@ class Repository
rugged.branches.create(branch_name, target) rugged.branches.create(branch_name, target)
end end
expire_branches_cache after_create_branch
find_branch(branch_name) find_branch(branch_name)
end end
def add_tag(tag_name, ref, message = nil) def add_tag(tag_name, ref, message = nil)
expire_tags_cache before_push_tag
gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message) gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
end end
def rm_branch(user, branch_name) def rm_branch(user, branch_name)
expire_branches_cache before_remove_branch
branch = find_branch(branch_name) branch = find_branch(branch_name)
oldrev = branch.try(:target) oldrev = branch.try(:target)
...@@ -155,12 +155,12 @@ class Repository ...@@ -155,12 +155,12 @@ class Repository
rugged.branches.delete(branch_name) rugged.branches.delete(branch_name)
end end
expire_branches_cache after_remove_branch
true true
end end
def rm_tag(tag_name) def rm_tag(tag_name)
expire_tags_cache before_remove_tag
gitlab_shell.rm_tag(path_with_namespace, tag_name) gitlab_shell.rm_tag(path_with_namespace, tag_name)
end end
...@@ -183,6 +183,14 @@ class Repository ...@@ -183,6 +183,14 @@ class Repository
end end
end end
def branch_count
@branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count }
end
def tag_count
@tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count }
end
# Return repo size in megabytes # Return repo size in megabytes
# Cached in redis # Cached in redis
def size def size
...@@ -278,6 +286,16 @@ class Repository ...@@ -278,6 +286,16 @@ class Repository
@has_visible_content = nil @has_visible_content = nil
end end
def expire_branch_count_cache
cache.expire(:branch_count)
@branch_count = nil
end
def expire_tag_count_cache
cache.expire(:tag_count)
@tag_count = nil
end
def rebuild_cache def rebuild_cache
cache_keys.each do |key| cache_keys.each do |key|
cache.expire(key) cache.expire(key)
...@@ -313,9 +331,17 @@ class Repository ...@@ -313,9 +331,17 @@ class Repository
expire_root_ref_cache expire_root_ref_cache
end end
# Runs code before creating a new tag. # Runs code before pushing (= creating or removing) a tag.
def before_create_tag def before_push_tag
expire_cache expire_cache
expire_tags_cache
expire_tag_count_cache
end
# Runs code before removing a tag.
def before_remove_tag
expire_tags_cache
expire_tag_count_cache
end end
# Runs code after a repository has been forked/imported. # Runs code after a repository has been forked/imported.
...@@ -330,12 +356,21 @@ class Repository ...@@ -330,12 +356,21 @@ class Repository
# Runs code after a new branch has been created. # Runs code after a new branch has been created.
def after_create_branch def after_create_branch
expire_branches_cache
expire_has_visible_content_cache expire_has_visible_content_cache
expire_branch_count_cache
end
# Runs code before removing an existing branch.
def before_remove_branch
expire_branches_cache
end end
# Runs code after an existing branch has been removed. # Runs code after an existing branch has been removed.
def after_remove_branch def after_remove_branch
expire_has_visible_content_cache expire_has_visible_content_cache
expire_branch_count_cache
expire_branches_cache
end end
def method_missing(m, *args, &block) def method_missing(m, *args, &block)
...@@ -812,6 +847,12 @@ class Repository ...@@ -812,6 +847,12 @@ class Repository
raw_repository.ls_files(actual_ref) raw_repository.ls_files(actual_ref)
end end
def main_language
unless empty?
Linguist::Repository.new(rugged, rugged.head.target_id).language
end
end
private private
def cache def cache
......
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# file_name :string(255) # file_name :string(255)
# expires_at :datetime
# type :string(255) # type :string(255)
# visibility_level :integer default(0), not null # visibility_level :integer default(0), not null
# #
...@@ -46,8 +45,6 @@ class Snippet < ActiveRecord::Base ...@@ -46,8 +45,6 @@ class Snippet < ActiveRecord::Base
scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) } scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) }
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
scope :fresh, -> { order("created_at DESC") } scope :fresh, -> { order("created_at DESC") }
scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) }
scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) }
participant :author, :notes participant :author, :notes
...@@ -111,21 +108,37 @@ class Snippet < ActiveRecord::Base ...@@ -111,21 +108,37 @@ class Snippet < ActiveRecord::Base
nil nil
end end
def expired?
expires_at && expires_at < Time.current
end
def visibility_level_field def visibility_level_field
visibility_level visibility_level
end end
class << self class << self
# Searches for snippets with a matching title or file name.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%") t = arel_table
pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:file_name].matches(pattern)))
end end
# Searches for snippets with matching content.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def search_code(query) def search_code(query)
where('(content LIKE :query)', query: "%#{query}%") table = Snippet.arel_table
pattern = "%#{query}%"
where(table[:content].matches(pattern))
end end
def accessible_to(user) def accessible_to(user)
......
...@@ -286,8 +286,22 @@ class User < ActiveRecord::Base ...@@ -286,8 +286,22 @@ class User < ActiveRecord::Base
end end
end end
# Searches users matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
where("lower(name) LIKE :query OR lower(email) LIKE :query OR lower(username) LIKE :query", query: "%#{query.downcase}%") table = arel_table
pattern = "%#{query}%"
where(
table[:name].matches(pattern).
or(table[:email].matches(pattern)).
or(table[:username].matches(pattern))
)
end end
def by_login(login) def by_login(login)
...@@ -612,6 +626,13 @@ class User < ActiveRecord::Base ...@@ -612,6 +626,13 @@ class User < ActiveRecord::Base
end end
end end
def try_obtain_ldap_lease
# After obtaining this lease LDAP checks will be blocked for 600 seconds
# (10 minutes) for this user.
lease = Gitlab::ExclusiveLease.new("user_ldap_check:#{id}", timeout: 600)
lease.try_obtain
end
def solo_owned_groups def solo_owned_groups
@solo_owned_groups ||= owned_groups.select do |group| @solo_owned_groups ||= owned_groups.select do |group|
group.owners == [self] group.owners == [self]
......
...@@ -12,8 +12,9 @@ class GitPushService < BaseService ...@@ -12,8 +12,9 @@ class GitPushService < BaseService
# 1. Creates the push event # 1. Creates the push event
# 2. Updates merge requests # 2. Updates merge requests
# 3. Recognizes cross-references from commit messages # 3. Recognizes cross-references from commit messages
# 4. Executes the project's web hooks # 4. Executes the project's webhooks
# 5. Executes the project's services # 5. Executes the project's services
# 6. Checks if the project's main language has changed
# #
def execute def execute
@project.repository.after_push_commit(branch_name) @project.repository.after_push_commit(branch_name)
...@@ -42,11 +43,24 @@ class GitPushService < BaseService ...@@ -42,11 +43,24 @@ class GitPushService < BaseService
@push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev]) @push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev])
process_commit_messages process_commit_messages
end end
# Checks if the main language has changed in the project and if so
# it updates it accordingly
update_main_language
# Update merge requests that may be affected by this push. A new branch # Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change. # could cause the last commit of a merge request to change.
update_merge_requests update_merge_requests
end end
def update_main_language
current_language = @project.repository.main_language
unless current_language == @project.main_language
return @project.update_attributes(main_language: current_language)
end
true
end
protected protected
def update_merge_requests def update_merge_requests
...@@ -96,7 +110,9 @@ class GitPushService < BaseService ...@@ -96,7 +110,9 @@ class GitPushService < BaseService
# a different branch. # a different branch.
closed_issues = commit.closes_issues(current_user) closed_issues = commit.closes_issues(current_user)
closed_issues.each do |issue| closed_issues.each do |issue|
Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit) if can?(current_user, :update_issue, issue)
Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
end
end end
end end
......
...@@ -2,7 +2,7 @@ class GitTagPushService ...@@ -2,7 +2,7 @@ class GitTagPushService
attr_accessor :project, :user, :push_data attr_accessor :project, :user, :push_data
def execute(project, user, oldrev, newrev, ref) def execute(project, user, oldrev, newrev, ref)
project.repository.before_create_tag project.repository.before_push_tag
@project, @user = project, user @project, @user = project, user
@push_data = build_push_data(oldrev, newrev, ref) @push_data = build_push_data(oldrev, newrev, ref)
......
...@@ -21,7 +21,9 @@ module MergeRequests ...@@ -21,7 +21,9 @@ module MergeRequests
closed_issues = merge_request.closes_issues(current_user) closed_issues = merge_request.closes_issues(current_user)
closed_issues.each do |issue| closed_issues.each do |issue|
Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request) if can?(current_user, :update_issue, issue)
Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
end
end end
end end
......
...@@ -11,13 +11,21 @@ module Projects ...@@ -11,13 +11,21 @@ module Projects
end end
def project_tree def project_tree
%i(issues merge_requests labels milestones snippets releases events commit_statuses) + members %i(issues labels milestones snippets releases events) + [members, merge_requests, commit_statuses]
end end
private private
def merge_requests
{ merge_requests: { include: :merge_request_diff } }
end
def commit_statuses
{ commit_statuses: { include: :commit } }
end
def members def members
[{ project_members: { include: [user: { only: [:id, :email, :username] }] } }] { project_members: { include: [user: { only: [:id, :email, :username] }] } }
end end
def storage_path def storage_path
......
...@@ -10,9 +10,8 @@ module Search ...@@ -10,9 +10,8 @@ module Search
group = Group.find_by(id: params[:group_id]) if params[:group_id].present? group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
projects = ProjectsFinder.new.execute(current_user) projects = ProjectsFinder.new.execute(current_user)
projects = projects.in_namespace(group.id) if group projects = projects.in_namespace(group.id) if group
project_ids = projects.pluck(:id)
Gitlab::SearchResults.new(project_ids, params[:search]) Gitlab::SearchResults.new(projects, params[:search])
end end
end end
end end
...@@ -7,7 +7,7 @@ module Search ...@@ -7,7 +7,7 @@ module Search
end end
def execute def execute
Gitlab::ProjectSearchResults.new(project.id, Gitlab::ProjectSearchResults.new(project,
params[:search], params[:search],
params[:repository_ref]) params[:repository_ref])
end end
......
...@@ -7,8 +7,9 @@ module Search ...@@ -7,8 +7,9 @@ module Search
end end
def execute def execute
snippet_ids = Snippet.accessible_to(current_user).pluck(:id) snippets = Snippet.accessible_to(current_user)
Gitlab::SnippetSearchResults.new(snippet_ids, params[:search])
Gitlab::SnippetSearchResults.new(snippets, params[:search])
end end
end end
end end
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
.nav-controls .nav-controls
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2" = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2"
= render 'explore/projects/dropdown' = render 'shared/projects/dropdown'
- if current_user.can_create_project? - if current_user.can_create_project?
= link_to new_project_path, class: 'btn btn-new' do = link_to new_project_path, class: 'btn btn-new' do
= icon('plus') = icon('plus')
......
%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid }
%span.milestone-row
- project = issue.project
%strong #{project.name_with_namespace} &middot;
= link_to [project.namespace.becomes(Namespace), project, issue] do
%span.cgray ##{issue.iid}
= link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title
.pull-right.assignee-icon
- if issue.assignee
= image_tag avatar_icon(issue.assignee, 16), class: "avatar s16"
.panel.panel-default
.panel-heading= title
%ul{ class: "well-list issues-sortable-list" }
- if issues
- issues.each do |issue|
= render 'issue', issue: issue
%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid }
%span.milestone-row
- project = merge_request.project
%strong #{project.name_with_namespace} &middot;
= link_to [project.namespace.becomes(Namespace), project, merge_request] do
%span.cgray ##{merge_request.iid}
= link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title
.pull-right.assignee-icon
- if merge_request.assignee
= image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16"
.panel.panel-default
.panel-heading= title
%ul{ class: "well-list merge_requests-sortable-list" }
- if merge_requests
- merge_requests.each do |merge_request|
= render 'merge_request', merge_request: merge_request
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } = render 'shared/milestones/milestone',
.row milestone_path: dashboard_milestone_path(milestone.safe_title, title: milestone.title),
.col-sm-6 issues_path: issues_dashboard_path(milestone_title: milestone.title),
%strong merge_requests_path: merge_requests_dashboard_path(milestone_title: milestone.title),
= link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title) milestone: milestone,
.col-sm-6 dashboard: true
.pull-right.light #{milestone.percent_complete}% complete
.row
.col-sm-6
= link_to issues_dashboard_path(milestone_title: milestone.title) do
= pluralize milestone.issue_count, 'Issue'
&middot;
= link_to merge_requests_dashboard_path(milestone_title: milestone.title) do
= pluralize milestone.merge_requests_count, 'Merge Request'
.col-sm-6
= milestone_progress_bar(milestone)
.row
.col-sm-6
.expiration
= render 'shared/milestone_expired', milestone: milestone
.projects
- milestone.milestones.each do |milestone|
= link_to milestone_path(milestone) do
%span.label.label-gray
= milestone.project.name_with_namespace
- page_title @milestone.title, "Milestones"
- header_title "Milestones", dashboard_milestones_path - header_title "Milestones", dashboard_milestones_path
.detail-page-header = render 'shared/milestones/top', milestone: @milestone
.status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" } = render 'shared/milestones/summary', milestone: @milestone
- if @milestone.closed? = render 'shared/milestones/tabs', milestone: @milestone, show_full_project_name: true
Closed
- else
Open
%span.identifier
Milestone #{@milestone.title}
.detail-page-description.gray-content-block.second-block
%h2.title
= markdown escape_once(@milestone.title), pipeline: :single_line
- if @milestone.complete? && @milestone.active?
.alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. Navigate to the project to close the milestone.
.table-holder
%table.table
%thead
%tr
%th Project
%th Open issues
%th State
%th Due date
- @milestone.milestones.each do |milestone|
%tr
%td
= link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
%td
= milestone.issues.opened.count
%td
- if milestone.closed?
Closed
- else
Open
%td
= milestone.expires_at
.context
%p.lead
Progress:
#{@milestone.closed_items_count} closed
&ndash;
#{@milestone.open_items_count} open
= milestone_progress_bar(@milestone)
%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
%span.badge= @milestone.issue_count
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
Merge Requests
%span.badge= @milestone.merge_requests_count
%li
= link_to '#tab-participants', 'data-toggle' => 'tab' do
Participants
%span.badge= @milestone.participants.count
.tab-content
.tab-pane.active#tab-issues
.gray-content-block.middle-block
.pull-right
= link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
.oneline
All issues in this milestone
.row.prepend-top-default
.col-md-6
= render 'issues', title: "Open", issues: @milestone.opened_issues
.col-md-6
= render 'issues', title: "Closed", issues: @milestone.closed_issues
.tab-pane#tab-merge-requests
.gray-content-block.middle-block
.pull-right
= link_to 'Browse Merge Requests', merge_requests_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
.oneline
All merge requests in this milestone
.row.prepend-top-default
.col-md-6
= render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
.col-md-6
= render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
.tab-pane#tab-participants
.gray-content-block.middle-block
.oneline
All participants to this milestone
%ul.bordered-list
- @milestone.participants.each do |user|
%li
= link_to user, title: user.name, class: "darken" do
= image_tag avatar_icon(user, 32), class: "avatar s32"
%strong= truncate(user.name, lenght: 40)
%br
%small.cgray= user.username
...@@ -4,7 +4,10 @@ ...@@ -4,7 +4,10 @@
.todo-title .todo-title
%span.author-name %span.author-name
= link_to_author todo - if todo.author
= link_to_author(todo)
- else
(removed)
%span.todo-label %span.todo-label
= todo_action_name(todo) = todo_action_name(todo)
= todo_target_link(todo) = todo_target_link(todo)
......
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
.emoji-menu-content .emoji-menu-content
= text_field_tag :emoji_search, "", class: "emoji-search search-input form-control" = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
- AwardEmoji.emoji_by_category.each do |category, emojis| - AwardEmoji.emoji_by_category.each do |category, emojis|
%h5= AwardEmoji::CATEGORIES[category] %h5.emoji-menu-title
%ul = AwardEmoji::CATEGORIES[category]
%ul.clearfix.emoji-menu-list
- emojis.each do |emoji| - emojis.each do |emoji|
%li %li.pull-left.text-center.emoji-menu-list-item
= emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"]) %button.emoji-menu-btn.text-center.js-emoji-btn{type: "button"}
\ No newline at end of file = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
.dropdown.inline
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_updated
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to explore_projects_filter_path(sort: sort_value_name) do
= sort_title_name
= link_to explore_projects_filter_path(sort: sort_value_recently_created) do
= sort_title_recently_created
= link_to explore_projects_filter_path(sort: sort_value_oldest_created) do
= sort_title_oldest_created
= link_to explore_projects_filter_path(sort: sort_value_recently_updated) do
= sort_title_recently_updated
= link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do
= sort_title_oldest_updated
...@@ -10,11 +10,11 @@ ...@@ -10,11 +10,11 @@
%b.caret %b.caret
%ul.dropdown-menu %ul.dropdown-menu
%li %li
= link_to explore_projects_filter_path(visibility_level: nil) do = link_to filter_projects_path(visibility_level: nil) do
Any Any
- Gitlab::VisibilityLevel.values.each do |level| - Gitlab::VisibilityLevel.values.each do |level|
%li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' } %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' }
= link_to explore_projects_filter_path(visibility_level: level) do = link_to filter_projects_path(visibility_level: level) do
= visibility_level_icon(level) = visibility_level_icon(level)
= visibility_level_label(level) = visibility_level_label(level)
...@@ -30,11 +30,11 @@ ...@@ -30,11 +30,11 @@
%b.caret %b.caret
%ul.dropdown-menu %ul.dropdown-menu
%li %li
= link_to explore_projects_filter_path(tag: nil) do = link_to filter_projects_path(tag: nil) do
Any Any
- @tags.each do |tag| - @tags.each do |tag|
%li{ class: (tag.name == params[:tag]) ? 'active' : 'light' } %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' }
= link_to explore_projects_filter_path(tag: tag.name) do = link_to filter_projects_path(tag: tag.name) do
%i.fa.fa-tag = icon('tag')
= tag.name = tag.name
.hidden-xs
= render "events/event_last_push", event: @last_push
.nav-block
- if current_user
.controls
= link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
%i.fa.fa-rss
= render 'shared/event_filter'
.content_list
= spinner
...@@ -3,9 +3,10 @@ ...@@ -3,9 +3,10 @@
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
- if @projects.present? - if @projects.present?
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
- if can? current_user, :create_projects, @group = render 'shared/projects/dropdown'
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do - if can? current_user, :create_projects, @group
= icon('plus') = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
New Project = icon('plus')
New Project
= render 'shared/projects/list', projects: @projects, stars: false, skip_namespace: true = render 'shared/projects/list', projects: @projects, stars: false, skip_namespace: 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.
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