Commit dd328184 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'master' of dev.gitlab.org:gitlab/gitlabhq

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>

Conflicts:
	VERSION
	app/assets/javascripts/project_new.js.coffee
	app/helpers/appearances_helper.rb
	app/helpers/application_helper.rb
	app/helpers/groups_helper.rb
	app/helpers/oauth_helper.rb
	app/models/user.rb
	app/views/admin/application_settings/_form.html.haml
	app/views/layouts/nav/_group.html.haml
	app/views/layouts/nav/_project_settings.html.haml
	app/views/profiles/accounts/show.html.haml
	doc/integration/README.md
	features/steps/project/merge_requests.rb
	features/support/env.rb
	spec/controllers/import/github_controller_spec.rb
	spec/helpers/oauth_helper_spec.rb
	spec/lib/gitlab/ldap/access_spec.rb
	spec/lib/gitlab/upgrader_spec.rb
	spec/models/project_team_spec.rb
	spec/routing/admin_routing_spec.rb
	spec/services/git_push_service_spec.rb
	spec/services/test_hook_service_spec.rb
parents 82693c40 a6a0792e
# This file is generated by GitLab CI
before_script:
- export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin
- ./scripts/prepare_build.sh
- ruby -v
- which ruby
- gem install bundler
- which bundle
- echo $PATH
- cp config/database.yml.mysql config/database.yml
- gem install bundler --no-ri --no-rdoc
- cp config/gitlab.yml.example config/gitlab.yml
- 'sed "s/username\:.*$/username\: runner/" -i config/database.yml'
- 'sed "s/password\:.*$/password\: ''password''/" -i config/database.yml'
- sed "s/gitlabhq_test/gitlabhq_test_$((RANDOM/5000))/" -i config/database.yml
- touch log/application.log
- touch log/test.log
- bundle install --without postgres production --jobs $(nproc)
- bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"
- bundle exec rake db:create RAILS_ENV=test
Rspec:
spec:feature:
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
tags:
- ruby
- mysql
Spinach:
spec:api:
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
tags:
- ruby
- mysql
Jasmine:
spec:other:
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake jasmine:ci
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
tags:
- ruby
- mysql
Rubocop:
spinach:project:
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project
tags:
- ruby
- mysql
spinach:other:
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other
tags:
- ruby
- mysql
teaspoon:
script:
- RAILS_ENV=test bundle exec teaspoon
tags:
- ruby
- mysql
rubocop:
script:
- bundle exec rubocop
tags:
- ruby
- mysql
Brakeman:
brakeman:
script:
- bundle exec rake brakeman
tags:
- ruby
- mysql
\ No newline at end of file
- mysql
--color
--format Fuubar
......@@ -993,8 +993,6 @@ Rails/Validation:
AllCops:
RunRailsCops: true
Exclude:
- 'spec/**/*'
- 'features/**/*'
- 'vendor/**/*'
- 'db/**/*'
- 'tmp/**/*'
......
Please view this file on the master branch, on stable branches it's out of date.
v 7.13.0 (unreleased)
- Fix external issue tracker hook/test for HTTPS URLs (Daniel Gerhardt)
- Remove link leading to a 404 error in Deploy Keys page (Stan Hu)
- Add support for unlocking users in admin settings (Stan Hu)
- Fix order of issues imported form GitHub (Hiroyuki Sato)
- Bump rugments to 1.0.0beta8 to fix C prototype function highlighting (Jonathon Reinhart)
- Fix Merge Request webhook to properly fire "merge" action when accepted from the web UI
- Add `two_factor_enabled` field to admin user API (Stan Hu)
- Fix invalid timestamps in RSS feeds (Rowan Wookey)
- Fix error when deleting a user who has projects (Stan Hu)
- Fix downloading of patches on public merge requests when user logged out (Stan Hu)
- The password for the default administrator (root) account has been changed from "5iveL!fe" to "password".
- Fix Error 500 when relative submodule resolves to a namespace that has a different name from its path (Stan Hu)
- Extract the longest-matching ref from a commit path when multiple matches occur (Stan Hu)
- Update maintenance documentation to explain no need to recompile asssets for omnibus installations (Stan Hu)
- Support commenting on diffs in side-by-side mode (Stan Hu)
- Fix JavaScript error when clicking on the comment button on a diff line that has a comment already (Stan Hu)
- Remove project visibility icons from dashboard projects list
- Rename "Design" profile settings page to "Preferences".
- Allow users to customize their default Dashboard page.
- Update ssl_ciphers in Nginx example to remove DHE settings. This will deny forward secrecy for Android 2.3.7, Java 6 and OpenSSL 0.9.8
- Admin can edit and remove user identities
- Convert CRLF newlines to LF when committing using the web editor.
- API request /projects/:project_id/merge_requests?state=closed will return only closed merge requests without merged one. If you need ones that were merged - use state=merged.
- Allow Administrators to filter the user list by those with or without Two-factor Authentication enabled.
- Show a user's Two-factor Authentication status in the administration area.
- Explicit error when commit not found in the CI
- Improve performance for issue and merge request pages
- Users with guest access level can not set assignee, labels or milestones for issue and merge request
- Reporter role can manage issue tracker now: edit any issue, set assignee or milestone and manage labels
- Better performance for pages with events list, issues list and commits list
- Faster automerge check and merge itself when source and target branches are in same repository
- Correctly show anonymous authorized applications under Profile > Applications.
- Query Optimization in MySQL.
v 7.12.1
- Fix error when deleting a user who has projects (Stan Hu)
- Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu)
- Add SAML to list of social_provider (Matt Firtion)
- Fix merge requests API scope to keep compatibility in 7.12.x patch release (Dmitriy Zaporozhets)
- Fix closed merge request scope at milestone page (Dmitriy Zaporozhets)
- Revert merge request states renaming
- Fix hooks for web based events with external issue references (Daniel Gerhardt)
v 7.12.0 (unreleased)
v 7.12.0
- Fix Error 500 when one user attempts to access a personal, internal snippet (Stan Hu)
- Disable changing of target branch in new merge request page when a branch has already been specified (Stan Hu)
- Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu)
- Update oauth button logos for Twitter and Google to recommended assets
- Fix hooks for web based events with external issue references (Daniel Gerhardt)
- Update browser gem to version 0.8.0 for IE11 support (Stan Hu)
- Fix timeout when rendering file with thousands of lines.
- Add "Remember me" checkbox to LDAP signin form.
- Add session expiration delay configuration through UI application settings
- Fix external issue tracker hook/test for HTTPS URLs (Daniel Gerhardt)
- Don't notify users mentioned in code blocks or blockquotes.
- Omit link to generate labels if user does not have access to create them (Stan Hu)
- Show warning when a comment will add 10 or more people to the discussion.
......@@ -47,8 +83,8 @@ v 7.12.0 (unreleased)
- Add validation to wiki page creation (only [a-zA-Z0-9/_-] are allowed) (Jeroen van Baarsen)
- Fix new/empty milestones showing 100% completion value (Jonah Bishop)
- Add a note when an Issue or Merge Request's title changes
- Consistently refer to MRs as either Accepted or Rejected.
- Add Accepted and Rejected tabs to MR lists.
- Consistently refer to MRs as either Merged or Closed.
- Add Merged tab to MR lists.
- Prefix EmailsOnPush email subject with `[Git]`.
- Group project contributions by both name and email.
- Clarify navigation labels for Project Settings and Group Settings.
......@@ -60,7 +96,7 @@ v 7.12.0 (unreleased)
- Allow to configure a URL to show after sign out
- Add an option to automatically sign-in with an Omniauth provider
- Better performance for web editor (switched from satellites to rugged)
- GitLab CI service sends .gitlab-ci.yaml in each push call
- GitLab CI service sends .gitlab-ci.yml in each push call
- When remove project - move repository and schedule it removal
- Improve group removing logic
- Trigger create-hooks on backup restore task
......
......@@ -167,15 +167,17 @@ If you add a dependency in GitLab (such as an operating system package) please c
This is also the style used by linting tools such as [RuboCop](https://github.com/bbatsov/rubocop), [PullReview](https://www.pullreview.com/) and [Hound CI](https://houndci.com).
## Code of conduct
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
Instances of abusive, harassing, or otherwise unacceptable behavior can be
reported by emailing contact@gitlab.com
This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by emailing contact@gitlab.com
This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
\ No newline at end of file
This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/)
......@@ -2,6 +2,10 @@ source "https://rubygems.org"
gem 'rails', '4.1.11'
# Specify a sprockets version due to security issue
# See https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY
gem 'sprockets', '~> 2.12.3'
# Default values for AR models
gem "default_value_for", "~> 3.0.0"
......@@ -9,7 +13,7 @@ gem "default_value_for", "~> 3.0.0"
gem "mysql2", group: :mysql
gem "pg", group: :postgres
# Auth
# Authentication libraries
gem "devise", '3.2.4'
gem "devise-async", '0.9.0'
gem 'omniauth', "~> 1.2.2"
......@@ -34,7 +38,7 @@ gem "browser", '~> 0.8.0'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
gem "gitlab_git", '~> 7.2.3'
gem "gitlab_git", '~> 7.2.5'
# Ruby/Rack Git Smart-HTTP Server Handler
# GitLab fork with a lot of changes (improved thread-safety, better memory usage etc)
......@@ -95,7 +99,7 @@ gem "seed-fu"
gem 'html-pipeline', '~> 1.11.0'
gem 'task_list', '1.0.2', require: 'task_list/railtie'
gem 'github-markup'
gem 'redcarpet', '~> 3.3.0'
gem 'redcarpet', '~> 3.3.2'
gem 'RedCloth'
gem 'rdoc', '~>3.6'
gem 'org-ruby', '= 0.9.12'
......@@ -182,7 +186,7 @@ gem 'mousetrap-rails'
# Detect and convert string character encoding
gem 'charlock_holmes'
gem "sass-rails", '~> 4.0.2'
gem "sass-rails", '~> 4.0.5'
gem "coffee-rails"
gem "uglifier"
gem 'turbolinks', '~> 2.5.0'
......@@ -225,16 +229,23 @@ group :development do
end
group :development, :test do
gem 'awesome_print'
gem 'byebug'
gem 'fuubar', '~> 2.0.0'
gem 'pry-rails'
gem 'coveralls', require: false
gem 'database_cleaner', '~> 1.4.0'
gem 'factory_girl_rails'
gem 'rspec-rails', '~> 3.3.0'
gem 'rubocop', '0.28.0', require: false
gem 'spinach-rails'
gem "rspec-rails", '2.99'
gem 'capybara', '~> 2.2.1'
gem 'capybara-screenshot', '~> 1.0.0'
gem "pry-rails"
gem "awesome_print"
gem "database_cleaner"
gem 'factory_girl_rails'
# rest-client is a coveralls dependency and not used directly in GitLab, but
# we specify a version here to pick up some security fixes.
# See https://github.com/rest-client/rest-client/issues/369
# and http://www.osvdb.org/show/osvdb/117461
gem 'rest-client', '~> 1.8.0'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.3.0'
......@@ -242,8 +253,9 @@ group :development, :test do
# Generate Fake data
gem 'ffaker', '~> 2.0.0'
# PhantomJS driver for Capybara
gem 'poltergeist', '~> 1.5.1'
gem 'capybara', '~> 2.3.0'
gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.6.0'
gem 'teaspoon', '~> 1.0.0'
gem 'teaspoon-jasmine'
......@@ -252,14 +264,12 @@ group :development, :test do
gem 'spring-commands-rspec', '~> 1.0.0'
gem 'spring-commands-spinach', '~> 1.0.0'
gem 'spring-commands-teaspoon', '~> 0.0.2'
gem "byebug"
end
group :test do
gem 'simplecov', require: false
gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec'
gem 'email_spec', '~> 1.6.0'
gem 'webmock', '~> 1.21.0'
gem 'test_after_commit'
end
......@@ -271,4 +281,4 @@ end
gem "newrelic_rpm"
gem 'octokit', '3.7.0'
gem "rugments", "~> 1.0.0.beta7"
gem "rugments", "~> 1.0.0.beta8"
......@@ -82,7 +82,7 @@ GEM
columnize (~> 0.8)
debugger-linecache (~> 1.2)
cal-heatmap-rails (0.0.1)
capybara (2.2.1)
capybara (2.3.0)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
......@@ -113,19 +113,19 @@ GEM
colorize (0.5.8)
columnize (0.9.0)
connection_pool (2.1.0)
coveralls (0.7.0)
multi_json (~> 1.3)
rest-client
simplecov (>= 0.7)
term-ansicolor
thor
coveralls (0.8.2)
json (~> 1.8)
rest-client (>= 1.6.8, < 2)
simplecov (~> 0.10.0)
term-ansicolor (~> 1.3)
thor (~> 0.19.1)
crack (0.4.2)
safe_yaml (~> 1.0.0)
creole (0.3.8)
d3_rails (3.5.5)
railties (>= 3.1.0)
daemons (1.1.9)
database_cleaner (1.3.0)
database_cleaner (1.4.1)
debug_inspector (0.0.2)
debugger-linecache (1.2.0)
default_value_for (3.0.0)
......@@ -149,12 +149,14 @@ GEM
diff-lcs (1.2.5)
diffy (3.0.3)
docile (1.1.5)
domain_name (0.5.24)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (2.1.3)
railties (>= 3.2)
dotenv (0.9.0)
dropzonejs-rails (0.4.14)
rails (> 3.1)
email_spec (1.5.0)
email_spec (1.6.0)
launchy (~> 2.1)
mail (~> 2.2)
encryptor (1.3.0)
......@@ -242,6 +244,9 @@ GEM
dotenv (>= 0.7)
thor (>= 0.13.6)
formatador (0.2.5)
fuubar (2.0.0)
rspec (~> 3.0)
ruby-progressbar (~> 1.4)
gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
gemojione (2.0.0)
......@@ -267,7 +272,7 @@ GEM
mime-types (~> 1.19)
gitlab_emoji (0.1.0)
gemojione (~> 2.0)
gitlab_git (7.2.3)
gitlab_git (7.2.5)
activesupport (~> 4.0)
charlock_holmes (~> 0.6)
gitlab-linguist (~> 3.0)
......@@ -320,6 +325,8 @@ GEM
html-pipeline (1.11.0)
activesupport (>= 2)
nokogiri (~> 1.4)
http-cookie (1.0.2)
domain_name (~> 0.5)
http_parser.rb (0.5.3)
httparty (0.13.3)
json (~> 1.8)
......@@ -349,7 +356,7 @@ GEM
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
kgio (2.9.2)
launchy (2.4.2)
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.1.2)
launchy (~> 2.2)
......@@ -375,6 +382,7 @@ GEM
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (2.9.2)
netrc (0.10.3)
newrelic_rpm (3.9.4.245)
nokogiri (1.6.6.2)
mini_portile (~> 0.6.0)
......@@ -432,8 +440,8 @@ GEM
orm_adapter (0.5.0)
parser (2.2.0.2)
ast (>= 1.1, < 3.0)
pg (0.15.1)
poltergeist (1.5.1)
pg (0.18.2)
poltergeist (1.6.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
multi_json (~> 1.0)
......@@ -450,7 +458,7 @@ GEM
quiet_assets (1.0.2)
railties (>= 3.1, < 5.0)
racc (1.4.10)
rack (1.5.4)
rack (1.5.5)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.3.0)
......@@ -500,7 +508,7 @@ GEM
trollop
rdoc (3.12.2)
json (~> 1.4)
redcarpet (3.3.1)
redcarpet (3.3.2)
redis (3.1.0)
redis-actionpack (4.0.0)
actionpack (~> 4)
......@@ -523,29 +531,37 @@ GEM
request_store (1.0.5)
rerun (0.10.0)
listen (~> 2.7, >= 2.7.3)
rest-client (1.6.7)
mime-types (>= 1.16)
rest-client (1.8.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 3.0)
netrc (~> 0.7)
rinku (1.7.3)
rotp (1.6.1)
rouge (1.7.7)
rqrcode (0.4.2)
rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2)
rspec-collection_matchers (1.1.2)
rspec-expectations (>= 2.99.0.beta1)
rspec-core (2.99.2)
rspec-expectations (2.99.2)
diff-lcs (>= 1.1.3, < 2.0)
rspec-mocks (2.99.3)
rspec-rails (2.99.0)
actionpack (>= 3.0)
activemodel (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-collection_matchers
rspec-core (~> 2.99.0)
rspec-expectations (~> 2.99.0)
rspec-mocks (~> 2.99.0)
rspec (3.3.0)
rspec-core (~> 3.3.0)
rspec-expectations (~> 3.3.0)
rspec-mocks (~> 3.3.0)
rspec-core (3.3.1)
rspec-support (~> 3.3.0)
rspec-expectations (3.3.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.3.0)
rspec-mocks (3.3.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.3.0)
rspec-rails (3.3.2)
actionpack (>= 3.0, < 4.3)
activesupport (>= 3.0, < 4.3)
railties (>= 3.0, < 4.3)
rspec-core (~> 3.3.0)
rspec-expectations (~> 3.3.0)
rspec-mocks (~> 3.3.0)
rspec-support (~> 3.3.0)
rspec-support (3.3.0)
rubocop (0.28.0)
astrolabe (~> 1.3)
parser (>= 2.2.0.pre.7, < 3.0)
......@@ -564,15 +580,15 @@ GEM
rubyntlm (0.5.0)
rubypants (0.2.0)
rugged (0.22.2)
rugments (1.0.0.beta7)
rugments (1.0.0.beta8)
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
sass (3.2.19)
sass-rails (4.0.3)
sass-rails (4.0.5)
railties (>= 4.0.0, < 5.0)
sass (~> 3.2.0)
sprockets (~> 2.8, <= 2.11.0)
sass (~> 3.2.2)
sprockets (~> 2.8, < 3.0)
sprockets-rails (~> 2.0)
sawyer (0.6.0)
addressable (~> 2.3.5)
......@@ -600,11 +616,11 @@ GEM
ice_cube (= 0.11.1)
sidekiq (>= 3.0.0)
simple_oauth (0.1.9)
simplecov (0.9.0)
simplecov (0.10.0)
docile (~> 1.1.0)
multi_json
simplecov-html (~> 0.8.0)
simplecov-html (0.8.0)
json (~> 1.8)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
sinatra (1.4.4)
rack (~> 1.4)
rack-protection (~> 1.4)
......@@ -629,12 +645,12 @@ GEM
spring (>= 0.9.1)
spring-commands-teaspoon (0.0.2)
spring (>= 0.9.1)
sprockets (2.11.0)
sprockets (2.12.4)
hike (~> 1.2)
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sprockets-rails (2.3.1)
sprockets-rails (2.3.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (>= 2.8, < 4.0)
......@@ -649,8 +665,8 @@ GEM
teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0)
temple (0.6.7)
term-ansicolor (1.2.2)
tins (~> 0.8)
term-ansicolor (1.3.2)
tins (~> 1.0)
terminal-table (1.4.5)
test_after_commit (0.2.2)
thin (1.6.1)
......@@ -672,7 +688,7 @@ GEM
mime-types (~> 1.19)
multi_json (~> 1.7)
twitter-stream (~> 0.1)
tins (0.13.1)
tins (1.5.4)
trollop (2.1.2)
turbolinks (2.5.3)
coffee-rails
......@@ -708,7 +724,9 @@ GEM
webmock (1.21.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
websocket-driver (0.3.3)
websocket-driver (0.5.4)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
wikicloth (0.8.1)
builder
expression_parser
......@@ -736,7 +754,7 @@ DEPENDENCIES
browser (~> 0.8.0)
byebug
cal-heatmap-rails (~> 0.0.1)
capybara (~> 2.2.1)
capybara (~> 2.3.0)
capybara-screenshot (~> 1.0.0)
carrierwave
charlock_holmes
......@@ -745,7 +763,7 @@ DEPENDENCIES
coveralls
creole (~> 0.3.6)
d3_rails (~> 3.5.5)
database_cleaner
database_cleaner (~> 1.4.0)
default_value_for (~> 3.0.0)
devise (= 3.2.4)
devise-async (= 0.9.0)
......@@ -753,13 +771,14 @@ DEPENDENCIES
diffy (~> 3.0.3)
doorkeeper (= 2.1.3)
dropzonejs-rails
email_spec
email_spec (~> 1.6.0)
enumerize
factory_girl_rails
ffaker (~> 2.0.0)
fog (~> 1.25.0)
font-awesome-rails (~> 4.2)
foreman
fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2)
github-markup
gitlab-flowdock-git-hook (~> 0.4.2)
......@@ -767,7 +786,7 @@ DEPENDENCIES
gitlab-license (~> 0.0.2)
gitlab-linguist (~> 3.0.1)
gitlab_emoji (~> 0.1)
gitlab_git (~> 7.2.3)
gitlab_git (~> 7.2.5)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (= 1.2.1)
gollum-lib (~> 4.0.2)
......@@ -803,7 +822,7 @@ DEPENDENCIES
omniauth-twitter
org-ruby (= 0.9.12)
pg
poltergeist (~> 1.5.1)
poltergeist (~> 1.6.0)
pry-rails
quiet_assets (~> 1.0.1)
rack-attack (~> 4.3.0)
......@@ -813,16 +832,17 @@ DEPENDENCIES
rails (= 4.1.11)
raphael-rails (~> 2.1.2)
rdoc (~> 3.6)
redcarpet (~> 3.3.0)
redcarpet (~> 3.3.2)
redis-rails
request_store
rerun (~> 0.10.0)
rest-client (~> 1.8.0)
rqrcode-rails3
rspec-rails (= 2.99)
rspec-rails (~> 3.3.0)
rubocop (= 0.28.0)
rugments (~> 1.0.0.beta7)
rugments (~> 1.0.0.beta8)
sanitize (~> 2.0)
sass-rails (~> 4.0.2)
sass-rails (~> 4.0.5)
sdoc
seed-fu
select2-rails
......@@ -840,6 +860,7 @@ DEPENDENCIES
spring-commands-rspec (~> 1.0.0)
spring-commands-spinach (~> 1.0.0)
spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 2.12.3)
stamp
state_machine
task_list (= 1.0.2)
......@@ -858,3 +879,6 @@ DEPENDENCIES
virtus
webmock (~> 1.21.0)
wikicloth (= 0.8.1)
BUNDLED WITH
1.10.4
......@@ -86,7 +86,7 @@ The recommended way to install GitLab is using the provided [Omnibus packages](h
There are various other options to install GitLab, please refer to the [installation page on the GitLab website](https://about.gitlab.com/installation/) for more information.
You can access a new installation with the login **`root`** and password **`5iveL!fe`**, after login you are required to set a unique password.
You can access a new installation with the login **`root`** and password **`password`**, after login you are required to set a unique password.
## Third-party applications
......@@ -125,4 +125,4 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on
## Is it awesome?
Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua.
[These people](https://twitter.com/gitlab/favorites) seem to like it.
\ No newline at end of file
[These people](https://twitter.com/gitlab/favorites) seem to like it.
7.12.0.pre-ee
7.13.0.pre-ee
app/assets/images/favicon.ico

32.2 KB | W: | H:

app/assets/images/favicon.ico

5.3 KB | W: | H:

app/assets/images/favicon.ico
app/assets/images/favicon.ico
app/assets/images/favicon.ico
app/assets/images/favicon.ico
  • 2-up
  • Swipe
  • Onion skin
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="210px" height="210px" viewBox="0 0 210 210" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch -->
<title>Slice 1</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="logo" sketch:type="MSLayerGroup" transform="translate(0.000000, 10.000000)">
<g id="Page-1" sketch:type="MSShapeGroup">
<g id="Fill-1-+-Group-24">
<g id="Group-24">
<g id="Group">
<path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329"></path>
<path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26"></path>
<path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326"></path>
<path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329"></path>
<path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26"></path>
<path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326"></path>
<path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="546px" height="194px" viewBox="0 0 546 194" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch -->
<title>Fill 1 + Group 24</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="Fill-1-+-Group-24" sketch:type="MSLayerGroup">
<g id="Group-24" sketch:type="MSShapeGroup">
<path d="M316.7906,65.3001 C301.5016,65.3001 292.0046,77.4461 292.0046,97.0001 C292.0046,116.5541 301.5016,128.7001 316.7906,128.7001 C322.5346,128.7001 327.8716,127.0711 332.2226,123.9881 L332.4336,123.8391 L332.4336,101.8711 L310.4336,101.8711 L310.4336,94.0711 L341.4336,94.0711 L341.4336,126.8061 C334.8706,133.1501 326.3546,136.5001 316.7906,136.5001 C296.2666,136.5001 283.0046,120.9951 283.0046,97.0001 C283.0046,73.0051 296.2666,57.5001 316.7906,57.5001 C326.7826,57.5001 335.2176,61.1481 341.2206,68.0561 L335.2246,73.0381 C330.6986,67.9041 324.4986,65.3001 316.7906,65.3001 L316.7906,65.3001 Z M489.8836,135.2501 L482.9356,135.2501 L480.6016,128.8021 L480.0486,129.2991 C479.9716,129.3681 472.2196,136.2501 462.4606,136.2501 C452.6096,136.2501 445.4606,129.6961 445.4606,120.6671 C445.4606,107.5951 456.7446,104.8511 466.2096,104.8511 C473.5836,104.8511 480.1886,106.5111 480.2546,106.5281 L480.8776,106.6871 L480.8776,105.1011 C480.8776,97.9861 476.4356,94.3781 467.6726,94.3781 C462.3646,94.3781 456.7556,95.6891 451.4236,98.1701 L447.8206,91.9581 C452.5266,88.8961 459.6726,85.3781 467.6726,85.3781 C481.5806,85.3781 489.8836,92.9341 489.8836,105.5891 L489.8836,135.2501 Z M470.6886,111.7771 C460.0716,111.7771 454.4606,114.8511 454.4606,120.6671 C454.4606,124.7281 457.5256,127.2501 462.4606,127.2501 C470.5906,127.2501 477.7276,123.9181 480.6626,121.9481 L480.8836,121.8001 L480.8836,112.6201 L480.4676,112.5491 C480.4226,112.5411 475.8766,111.7771 470.6886,111.7771 L470.6886,111.7771 Z M440.4576,127.4501 L440.4576,135.2501 L410.4606,135.2501 L410.4606,61.2501 L419.4606,61.2501 L419.4606,127.4501 L440.4576,127.4501 Z M520.9416,136.5001 C515.0966,136.5001 508.6886,135.6961 501.8926,134.1091 L501.8926,61.2501 L510.8926,61.2501 L510.8926,89.3131 L511.6656,88.8111 C511.7146,88.7791 516.7346,85.5711 523.6536,85.5711 C525.0336,85.5711 526.4146,85.7001 527.7486,85.9521 C539.0936,88.2761 545.8666,97.4301 545.8666,110.4391 C545.8666,125.7831 535.6176,136.5001 520.9416,136.5001 L520.9416,136.5001 Z M521.9426,94.3781 C518.3636,94.3781 514.6196,95.6031 511.1166,97.9191 L510.8926,98.0681 L510.8926,127.9021 L511.3196,127.9651 C514.6986,128.4601 517.9356,128.7121 520.9416,128.7121 C530.3176,128.7121 536.8666,121.1971 536.8666,110.4391 C536.8666,100.2321 531.4266,94.3781 521.9426,94.3781 L521.9426,94.3781 Z M398.4516,86.2501 L398.4516,94.0501 L383.4516,94.0501 L383.4516,116.9501 C383.4516,119.7551 384.5436,122.3921 386.5276,124.3741 C388.5096,126.3581 391.1466,127.4501 393.9516,127.4501 L398.4516,127.4501 L398.4516,135.2501 L393.9516,135.2501 C383.1996,135.2501 374.4516,126.5021 374.4516,115.7501 L374.4516,61.2501 L383.4516,61.2501 L383.4516,86.2501 L398.4516,86.2501 Z M353.4426,66.2501 L362.4426,66.2501 L362.4426,75.2501 L353.4426,75.2501 L353.4426,66.2501 Z M353.4426,86.2501 L362.4426,86.2501 L362.4426,135.2501 L353.4426,135.2501 L353.4426,86.2501 Z" id="Fill-2" fill="#8C929D"></path>
<g id="Group">
<path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329"></path>
<path id="Fill-6" fill="#FC6D26"></path>
<path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26"></path>
<path id="Fill-10" fill="#FC6D26"></path>
<path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326"></path>
<path id="Fill-14" fill="#FC6D26"></path>
<path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329"></path>
<path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26"></path>
<path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326"></path>
<path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329"></path>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
......@@ -143,8 +143,7 @@ $ ->
$('.trigger-submit').on 'change', ->
$(@).parents('form').submit()
$("abbr.timeago").timeago()
$('.js-timeago').timeago()
$('abbr.timeago, .js-timeago').timeago()
# Flash
if (flash = $(".flash-container")).length > 0
......
# Requires Input behavior
#
# When called on a form with input fields with the `required` attribute, the
# form's submit button will be disabled until all required fields have values.
#
#= require extensions/jquery
#
# ### Example Markup
#
# <form class="js-requires-input">
# <input type="text" required="required">
# <input type="submit" value="Submit">
# </form>
#
$.fn.requiresInput = ->
$form = $(this)
$button = $('button[type=submit], input[type=submit]', $form)
required = '[required=required]'
fieldSelector = "input#{required}, select#{required}, textarea#{required}"
requireInput = ->
# Collect the input values of *all* required fields
values = _.map $(fieldSelector, $form), (field) -> field.value
# Disable the button if any required fields are empty
if values.length && _.any(values, _.isEmpty)
$button.disable()
else
$button.enable()
# Set initial button state
requireInput()
$form.on 'change input', fieldSelector, requireInput
# Triggered on standard document `ready` and on Turbolinks `page:load` events
$(document).on 'ready page:load', ->
$('form.js-requires-input').requiresInput()
class @BlobView
constructor: ->
# handle multi-line select
handleMultiSelect = (e) ->
[ first_line, last_line ] = parseSelectedLines()
[ line_number ] = parseSelectedLines($(this).attr("id"))
hash = "L#{line_number}"
if e.shiftKey and not isNaN(first_line) and not isNaN(line_number)
if line_number < first_line
last_line = first_line
first_line = line_number
else
last_line = line_number
hash = if first_line == last_line then "L#{first_line}" else "L#{first_line}-#{last_line}"
setHash(hash)
e.preventDefault()
# See if there are lines selected
# "#L12" and "#L34-56" supported
highlightBlobLines = (e) ->
[ first_line, last_line ] = parseSelectedLines()
unless isNaN first_line
$("#tree-content-holder .highlight .line").removeClass("hll")
$("#LC#{line}").addClass("hll") for line in [first_line..last_line]
$.scrollTo("#L#{first_line}", offset: -50) unless e?
# parse selected lines from hash
# always return first and last line (initialized to NaN)
parseSelectedLines = (str) ->
first_line = NaN
last_line = NaN
hash = str || window.location.hash
if hash isnt ""
matches = hash.match(/\#?L(\d+)(\-(\d+))?/)
first_line = parseInt(matches?[1])
last_line = parseInt(matches?[3])
last_line = first_line if isNaN(last_line)
[ first_line, last_line ]
setHash = (hash) ->
hash = hash.replace(/^\#/, "")
nodes = $("#" + hash)
# if any nodes are using this id, they must be temporarily changed
# also, add a temporary div at the top of the screen to prevent scrolling
if nodes.length > 0
scroll_top = $(document).scrollTop()
nodes.attr("id", "")
tmp = $("<div></div>")
.css({ position: "absolute", visibility: "hidden", top: scroll_top + "px" })
.attr("id", hash)
.appendTo(document.body)
window.location.hash = hash
# restore the nodes
if nodes.length > 0
tmp.remove()
nodes.attr("id", hash)
# initialize multi-line select
$("#tree-content-holder .line-numbers a[id^=L]").on("click", handleMultiSelect)
# Highlight the correct lines on load
highlightBlobLines()
# Highlight the correct lines when the hash part of the URL changes
$(window).on("hashchange", highlightBlobLines)
......@@ -11,7 +11,6 @@ class @EditBlob
if ace_mode
editor.getSession().setMode "ace/mode/" + ace_mode
disableButtonIfEmptyField "#commit_message", ".js-commit-button"
$(".js-commit-button").click ->
$("#file-content").val editor.getValue()
$(".file-editor form").submit()
......
......@@ -11,7 +11,6 @@ class @NewBlob
if ace_mode
editor.getSession().setMode "ace/mode/" + ace_mode
disableButtonIfEmptyField "#commit_message", ".js-commit-button"
$(".js-commit-button").click ->
$("#file-content").val editor.getValue()
$(".file-editor form").submit()
......
......@@ -31,20 +31,14 @@ class Dispatcher
when 'projects:compare:show'
new Diff()
when 'projects:issues:new','projects:issues:edit'
GitLab.GfmAutoComplete.setup()
shortcut_handler = new ShortcutsNavigation()
new ZenMode()
new DropzoneInput($('.issue-form'))
if page == 'projects:issues:new'
new IssuableForm($('.issue-form'))
new IssuableForm($('.issue-form'))
when 'projects:merge_requests:new', 'projects:merge_requests:edit'
GitLab.GfmAutoComplete.setup()
new Diff()
shortcut_handler = new ShortcutsNavigation()
new ZenMode()
new DropzoneInput($('.merge-request-form'))
if page == 'projects:merge_requests:new'
new IssuableForm($('.merge-request-form'))
new IssuableForm($('.merge-request-form'))
when 'projects:merge_requests:show'
new Diff()
shortcut_handler = new ShortcutsIssuable()
......@@ -87,7 +81,7 @@ class Dispatcher
new TreeView()
shortcut_handler = new ShortcutsNavigation()
when 'projects:blob:show'
new BlobView()
new LineHighlighter()
shortcut_handler = new ShortcutsNavigation()
when 'projects:labels:new', 'projects:labels:edit'
new Labels()
......@@ -117,13 +111,6 @@ class Dispatcher
new NamespaceSelect()
when 'dashboard'
shortcut_handler = new ShortcutsDashboardNavigation()
switch path[1]
when 'issues', 'merge_requests'
new UsersSelect()
when 'groups'
switch path[1]
when 'issues', 'merge_requests'
new UsersSelect()
when 'profiles'
new Profile()
when 'projects'
......@@ -139,8 +126,6 @@ class Dispatcher
new ProjectNew()
when 'show'
new ProjectShow()
when 'issues', 'merge_requests'
new UsersSelect()
when 'wikis'
new Wikis()
shortcut_handler = new ShortcutsNavigation()
......
#= require jquery.waitforimages
class @IssuableContext
constructor: ->
new UsersSelect()
$('select.select2').select2({width: 'resolve', dropdownAutoWidth: true})
$(".context .inline-update").on "change", "select", ->
$(this).submit()
$(".context .inline-update").on "change", ".js-assignee", ->
$(this).submit()
$('.issuable-details').waitForImages ->
$('.issuable-affix').affix offset:
top: ->
@top = ($('.issuable-affix').offset().top - 70)
bottom: ->
@bottom = $('.footer').outerHeight(true)
$('.issuable-affix').on 'affix.bs.affix', ->
$(@).width($(@).outerWidth())
.on 'affixed-top.bs.affix affixed-bottom.bs.affix', ->
$(@).width('')
class @IssuableForm
constructor: (@form) ->
GitLab.GfmAutoComplete.setup()
new UsersSelect()
new ZenMode()
@titleField = @form.find("input[name*='[title]']")
@descriptionField = @form.find("textarea[name*='[description]']")
......
......@@ -3,29 +3,12 @@
class @Issue
constructor: ->
$('.edit-issue.inline-update input[type="submit"]').hide()
$(".context .inline-update").on "change", "select", ->
$(this).submit()
$(".context .inline-update").on "change", "#issue_assignee_id", ->
$(this).submit()
# Prevent duplicate event bindings
@disableTaskList()
if $("a.btn-close").length
@initTaskList()
$('.issue-details').waitForImages ->
$('.issuable-affix').affix offset:
top: ->
@top = ($('.issuable-affix').offset().top - 70)
bottom: ->
@bottom = $('.footer').outerHeight(true)
$('.issuable-affix').on 'affix.bs.affix', ->
$(@).width($(@).outerWidth())
.on 'affixed-top.bs.affix affixed-bottom.bs.affix', ->
$(@).width('')
initTaskList: ->
$('.issue-details .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.issue-details .js-task-list-container', @updateTaskList
......@@ -42,5 +25,5 @@ class @Issue
$.ajax
type: 'PATCH'
url: $('form.js-issue-update').attr('action')
url: $('form.js-issuable-update').attr('action')
data: patchData
class @Labels
constructor: ->
form = $('.label-form')
@setupLabelForm(form)
@cleanBinding()
@addBinding()
@updateColorPreview()
......@@ -14,10 +13,6 @@ class @Labels
$(document).off 'click', '.suggest-colors a'
$(document).off 'input', 'input#label_color'
# Initializes the form to disable the save button if no color or title is entered
setupLabelForm: (form) ->
disableButtonIfAnyEmptyField form, '.form-control', form.find('.js-save-button')
# Updates the the preview color with the hex-color input
updateColorPreview: =>
previewColor = $('input#label_color').val()
......
# LineHighlighter
#
# Handles single- and multi-line selection and highlight for blob views.
#
#= require jquery.scrollTo
#
# ### Example Markup
#
# <div id="tree-content-holder">
# <div class="file-content">
# <div class="line-numbers">
# <a href="#L1" id="L1" data-line-number="1">1</a>
# <a href="#L2" id="L2" data-line-number="2">2</a>
# <a href="#L3" id="L3" data-line-number="3">3</a>
# <a href="#L4" id="L4" data-line-number="4">4</a>
# <a href="#L5" id="L5" data-line-number="5">5</a>
# </div>
# <pre class="code highlight">
# <code>
# <span id="LC1" class="line">...</span>
# <span id="LC2" class="line">...</span>
# <span id="LC3" class="line">...</span>
# <span id="LC4" class="line">...</span>
# <span id="LC5" class="line">...</span>
# </code>
# </pre>
# </div>
# </div>
#
class @LineHighlighter
# CSS class applied to highlighted lines
highlightClass: 'hll'
# Internal copy of location.hash so we're not dependent on `location` in tests
_hash: ''
# Initialize a LineHighlighter object
#
# hash - String URL hash for dependency injection in tests
constructor: (hash = location.hash) ->
@_hash = hash
@bindEvents()
unless hash == ''
range = @hashToRange(hash)
if range[0]
@highlightRange(range)
# Scroll to the first highlighted line on initial load
# Offset -50 for the sticky top bar, and another -100 for some context
$.scrollTo("#L#{range[0]}", offset: -150)
bindEvents: ->
$('#tree-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler
# While it may seem odd to bind to the mousedown event and then throw away
# the click event, there is a method to our madness.
#
# If not done this way, the line number anchor will sometimes keep its
# active state even when the event is cancelled, resulting in an ugly border
# around the link and/or a persisted underline text decoration.
$('#tree-content-holder').on 'click', 'a[data-line-number]', (event) ->
event.preventDefault()
clickHandler: (event) =>
event.preventDefault()
@clearHighlight()
lineNumber = $(event.target).data('line-number')
current = @hashToRange(@_hash)
unless current[0] && event.shiftKey
# If there's no current selection, or there is but Shift wasn't held,
# treat this like a single-line selection.
@setHash(lineNumber)
@highlightLine(lineNumber)
else if event.shiftKey
if lineNumber < current[0]
range = [lineNumber, current[0]]
else
range = [current[0], lineNumber]
@setHash(range[0], range[1])
@highlightRange(range)
# Unhighlight previously highlighted lines
clearHighlight: ->
$(".#{@highlightClass}").removeClass(@highlightClass)
# Convert a URL hash String into line numbers
#
# hash - Hash String
#
# Examples:
#
# hashToRange('#L5') # => [5, null]
# hashToRange('#L5-15') # => [5, 15]
# hashToRange('#foo') # => [null, null]
#
# Returns an Array
hashToRange: (hash) ->
matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/)
if matches && matches.length
first = parseInt(matches[1])
last = if matches[2] then parseInt(matches[2]) else null
[first, last]
else
[null, null]
# Highlight a single line
#
# lineNumber - Line number to highlight
highlightLine: (lineNumber) =>
$("#LC#{lineNumber}").addClass(@highlightClass)
# Highlight all lines within a range
#
# range - Array containing the starting and ending line numbers
highlightRange: (range) ->
if range[1]
for lineNumber in [range[0]..range[1]]
@highlightLine(lineNumber)
else
@highlightLine(range[0])
# Set the URL hash string
setHash: (firstLineNumber, lastLineNumber) =>
if lastLineNumber
hash = "#L#{firstLineNumber}-#{lastLineNumber}"
else
hash = "#L#{firstLineNumber}"
@_hash = hash
@__setLocationHash__(hash)
# Make the actual hash change in the browser
#
# This method is stubbed in tests.
__setLocationHash__: (value) ->
# We're using pushState instead of assigning location.hash directly to
# prevent the page from scrolling on the hashchange event
history.pushState({turbolinks: false, url: value}, document.title, value)
#= require jquery.waitforimages
#= require task_list
#= require merge_request_tabs
class @MergeRequest
# Initialize MergeRequest behavior
#
# Options:
# action - String, current controller action
# diffs_loaded - Boolean, have diffs been pre-rendered server-side?
# (default: true if `action` is 'diffs', otherwise false)
# commits_loaded - Boolean, have commits been pre-rendered server-side?
# (default: false)
# action - String, current controller action
#
constructor: (@opts) ->
@initContextWidget()
this.$el = $('.merge-request')
@diffs_loaded = @opts.diffs_loaded or @opts.action == 'diffs'
@commits_loaded = @opts.commits_loaded or false
this.bindEvents()
this.activateTabFromPath()
this.$('.show-all-commits').on 'click', =>
this.showAllCommits()
# `MergeRequests#new` has no tab-persisting or lazy-loading behavior
unless @opts.action == 'new'
new MergeRequestTabs(@opts)
# Prevent duplicate event bindings
@disableTaskList()
if $("a.btn-close").length
@initTaskList()
$('.merge-request-details').waitForImages ->
$('.issuable-affix').affix offset:
top: ->
@top = ($('.issuable-affix').offset().top - 70)
bottom: ->
@bottom = $('.footer').outerHeight(true)
$('.issuable-affix').on 'affix.bs.affix', ->
$(@).width($(@).outerWidth())
.on 'affixed-top.bs.affix affixed-bottom.bs.affix', ->
$(@).width('')
# Local jQuery finder
$: (selector) ->
this.$el.find(selector)
initContextWidget: ->
$('.edit-merge_request.inline-update input[type="submit"]').hide()
$(".context .inline-update").on "change", "select", ->
$(this).submit()
$(".context .inline-update").on "change", "#merge_request_assignee_id", ->
$(this).submit()
bindEvents: ->
this.$('.merge-request-tabs a[data-toggle="tab"]').on 'shown.bs.tab', (e) =>
$target = $(e.target)
tab_action = $target.data('action')
# Lazy-load diffs
if tab_action == 'diffs'
this.loadDiff() unless @diffs_loaded
$('.diff-header').trigger('sticky_kit:recalc')
# Skip tab-persisting behavior on MergeRequests#new
unless @opts.action == 'new'
@setCurrentAction(tab_action)
# Activate a tab based on the current URL path
#
# If the current action is 'show' or 'new' (i.e., initial page load),
# activates the first tab, otherwise activates the tab corresponding to the
# current action (diffs, commits).
activateTabFromPath: ->
if @opts.action == 'show' || @opts.action == 'new'
this.$('.merge-request-tabs a[data-toggle="tab"]:first').tab('show')
else
this.$(".merge-request-tabs a[data-action='#{@opts.action}']").tab('show')
# Replaces the current Merge Request-specific action in the URL with a new one
#
# If the action is "notes", the URL is reset to the standard
# `MergeRequests#show` route.
#
# Examples:
#
# location.pathname # => "/namespace/project/merge_requests/1"
# setCurrentAction('diffs')
# location.pathname # => "/namespace/project/merge_requests/1/diffs"
#
# location.pathname # => "/namespace/project/merge_requests/1/diffs"
# setCurrentAction('notes')
# location.pathname # => "/namespace/project/merge_requests/1"
#
# location.pathname # => "/namespace/project/merge_requests/1/diffs"
# setCurrentAction('commits')
# location.pathname # => "/namespace/project/merge_requests/1/commits"
setCurrentAction: (action) ->
# Normalize action, just to be safe
action = 'notes' if action == 'show'
# Remove a trailing '/commits' or '/diffs'
new_state = location.pathname.replace(/\/(commits|diffs)\/?$/, '')
# Append the new action if we're on a tab other than 'notes'
unless action == 'notes'
new_state += "/#{action}"
# Ensure parameters and hash come along for the ride
new_state += location.search + location.hash
# Replace the current history state with the new one without breaking
# Turbolinks' history.
#
# See https://github.com/rails/turbolinks/issues/363
history.replaceState {turbolinks: true, url: new_state}, '', new_state
loadDiff: (event) ->
$.ajax
type: 'GET'
url: this.$('.merge-request-tabs .diffs-tab a').attr('href') + ".json"
beforeSend: =>
this.$('.mr-loading-status .loading').show()
complete: =>
@diffs_loaded = true
this.$('.mr-loading-status .loading').hide()
success: (data) =>
this.$(".diffs").html(data.html)
dataType: 'json'
showAllCommits: ->
this.$('.first-commits').remove()
this.$('.all-commits').removeClass 'hide'
......@@ -149,5 +49,5 @@ class @MergeRequest
$.ajax
type: 'PATCH'
url: $('form.js-merge-request-update').attr('action')
url: $('form.js-issuable-update').attr('action')
data: patchData
# MergeRequestTabs
#
# Handles persisting and restoring the current tab selection and lazily-loading
# content on the MergeRequests#show page.
#
# ### Example Markup
#
# <ul class="nav nav-tabs merge-request-tabs">
# <li class="notes-tab active">
# <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
# Discussion
# </a>
# </li>
# <li class="commits-tab">
# <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits">
# Commits
# </a>
# </li>
# <li class="diffs-tab">
# <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs">
# Diffs
# </a>
# </li>
# </ul>
#
# <div class="tab-content">
# <div class="notes tab-pane active" id="notes">
# Notes Content
# </div>
# <div class="commits tab-pane" id="commits">
# Commits Content
# </div>
# <div class="diffs tab-pane" id="diffs">
# Diffs Content
# </div>
# </div>
#
# <div class="mr-loading-status">
# <div class="loading">
# Loading Animation
# </div>
# </div>
#
class @MergeRequestTabs
diffsLoaded: false
commitsLoaded: false
constructor: (@opts = {}) ->
# Store the `location` object, allowing for easier stubbing in tests
@_location = location
switch @opts.action
when 'commits'
@commitsLoaded = true
when 'diffs'
@diffsLoaded = true
@bindEvents()
@activateTab(@opts.action)
bindEvents: ->
$(document).on 'shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', @tabShown
tabShown: (event) =>
$target = $(event.target)
action = $target.data('action')
if action == 'commits'
@loadCommits($target.attr('href'))
else if action == 'diffs'
@loadDiff($target.attr('href'))
@stickyDiffHeaders()
@setCurrentAction(action)
# Activate a tab based on the current action
activateTab: (action) ->
action = 'notes' if action == 'show'
$(".merge-request-tabs a[data-action='#{action}']").tab('show')
# Replaces the current Merge Request-specific action in the URL with a new one
#
# If the action is "notes", the URL is reset to the standard
# `MergeRequests#show` route.
#
# Examples:
#
# location.pathname # => "/namespace/project/merge_requests/1"
# setCurrentAction('diffs')
# location.pathname # => "/namespace/project/merge_requests/1/diffs"
#
# location.pathname # => "/namespace/project/merge_requests/1/diffs"
# setCurrentAction('notes')
# location.pathname # => "/namespace/project/merge_requests/1"
#
# location.pathname # => "/namespace/project/merge_requests/1/diffs"
# setCurrentAction('commits')
# location.pathname # => "/namespace/project/merge_requests/1/commits"
#
# Returns the new URL String
setCurrentAction: (action) =>
# Normalize action, just to be safe
action = 'notes' if action == 'show'
# Remove a trailing '/commits' or '/diffs'
new_state = @_location.pathname.replace(/\/(commits|diffs)\/?$/, '')
# Append the new action if we're on a tab other than 'notes'
unless action == 'notes'
new_state += "/#{action}"
# Ensure parameters and hash come along for the ride
new_state += @_location.search + @_location.hash
# Replace the current history state with the new one without breaking
# Turbolinks' history.
#
# See https://github.com/rails/turbolinks/issues/363
history.replaceState {turbolinks: true, url: new_state}, document.title, new_state
new_state
loadCommits: (source) ->
return if @commitsLoaded
@_get
url: "#{source}.json"
success: (data) =>
document.getElementById('commits').innerHTML = data.html
$('.js-timeago').timeago()
@commitsLoaded = true
loadDiff: (source) ->
return if @diffsLoaded
@_get
url: "#{source}.json"
success: (data) =>
document.getElementById('diffs').innerHTML = data.html
@stickyDiffHeaders()
@diffsLoaded = true
toggleLoading: ->
$('.mr-loading-status .loading').toggle()
stickyDiffHeaders: ->
$('.diff-header').trigger('sticky_kit:recalc')
_get: (options) ->
defaults = {
beforeSend: @toggleLoading
complete: @toggleLoading
dataType: 'json'
type: 'GET'
}
options = $.extend({}, defaults, options)
$.ajax(options)
......@@ -36,11 +36,11 @@ class @MergeRequestWidget
showCiState: (state) ->
$('.ci_widget').hide()
allowed_states = ["failed", "canceled", "running", "pending", "success"]
allowed_states = ["failed", "canceled", "running", "pending", "success", "not_found"]
if state in allowed_states
$('.ci_widget.ci-' + state).show()
switch state
when "failed", "canceled"
when "failed", "canceled", "not_found"
@setMergeButtonClass('btn-danger')
when "running", "pending"
@setMergeButtonClass('btn-warning')
......
......@@ -3,16 +3,8 @@ class @ProjectNew
$('.project-edit-container').on 'ajax:before', =>
$('.project-edit-container').hide()
$('.save-project-loader').show()
@toggleSettings()
@initEvents()
initEvents: ->
disableButtonIfEmptyField '#project_name', '.project-submit'
$("#project_merge_requests_enabled").change =>
@toggleSettings()
toggleSettings: ->
checked = $("#project_merge_requests_enabled").prop("checked")
......@@ -20,4 +12,3 @@ class @ProjectNew
$('.merge-request-feature').show()
else
$('.merge-request-feature').hide()
......@@ -373,3 +373,12 @@ table {
margin-top: 8px;
}
}
.profiler-results {
top: 50px !important;
.profiler-button,
.profiler-controls {
border-color: #EEE !important;
}
}
......@@ -19,3 +19,7 @@
height: 14em;
}
}
.gfm-commit, .gfm-commit_range {
font-family: $monospace_font;
}
......@@ -3,6 +3,8 @@
*
*/
header {
transition-duration: .3s;
&.navbar-empty {
background: #FFF;
border-bottom: 1px solid #EEE;
......@@ -10,6 +12,10 @@ header {
.center-logo {
margin: 8px 0;
text-align: center;
img {
height: 32px;
}
}
}
......@@ -63,28 +69,35 @@ header {
float: left;
height: $header-height;
width: $sidebar_width;
overflow: hidden;
transition-duration: .3s;
a {
float: left;
height: $header-height;
width: 100%;
padding: ($header-height - 36 ) / 2 8px;
h3 {
width: 158px;
float: left;
margin: 0;
margin-left: 14px;
font-size: 18px;
line-height: $header-height - 14;
font-weight: normal;
}
overflow: hidden;
img {
width: 36px;
height: 36px;
float: left;
}
.gitlab-text-container {
width: 230px;
h3 {
width: 158px;
float: left;
margin: 0;
margin-left: 14px;
font-size: 18px;
line-height: $header-height - 14;
font-weight: normal;
}
}
}
&:hover {
......@@ -157,10 +170,6 @@ header {
@mixin collapsed-header {
.header-logo {
width: $sidebar_collapsed_width;
h3 {
display: none;
}
}
.header-content {
......
......@@ -4,66 +4,69 @@
top: 0;
left: 0;
height: 100%;
transition-duration: .3s;
}
}
.sidebar-wrapper {
z-index: 99;
background: $background-color;
transition-duration: .3s;
}
.content-wrapper {
width: 100%;
padding: 15px;
padding: 20px;
background: #FFF;
}
.nav-sidebar {
transition-duration: .3s;
margin: 0;
list-style: none;
overflow: hidden;
&.navbar-collapse {
padding: 0px !important;
}
}
.nav-sidebar li a .count {
float: right;
background: #eee;
padding: 0px 8px;
@include border-radius(6px);
}
li {
width: $sidebar_width;
.nav-sidebar li {
}
&.separate-item {
padding-top: 10px;
margin-top: 10px;
}
.nav-sidebar li {
&.separate-item {
padding-top: 10px;
margin-top: 10px;
}
a {
color: $gray;
display: block;
text-decoration: none;
padding: 8px 15px;
font-size: 14px;
line-height: 20px;
padding-left: 16px;
a {
color: $gray;
display: block;
text-decoration: none;
padding: 8px 15px;
font-size: 13px;
line-height: 20px;
padding-left: 16px;
&:hover {
text-decoration: none;
}
&:hover {
text-decoration: none;
}
&:active, &:focus {
text-decoration: none;
}
&:active, &:focus {
text-decoration: none;
}
i {
width: 20px;
color: $gray-light;
margin-right: 23px;
}
i {
width: 20px;
color: $gray-light;
margin-right: 23px;
.count {
float: right;
background: #eee;
padding: 0px 8px;
@include border-radius(6px);
}
}
}
}
......@@ -79,6 +82,7 @@
@mixin expanded-sidebar {
padding-left: $sidebar_width;
transition-duration: .3s;
.sidebar-wrapper {
width: $sidebar_width;
......@@ -89,15 +93,16 @@
top: $header-height;
width: $sidebar_width;
}
}
.content-wrapper {
padding: 20px;
.nav-sidebar li a{
width: 230px;
}
}
}
@mixin folded-sidebar {
padding-left: 50px;
transition-duration: .3s;
.sidebar-wrapper {
width: $sidebar_collapsed_width;
......@@ -109,15 +114,10 @@
width: $sidebar_collapsed_width;
li a {
padding-left: 18px;
font-size: 14px;
padding: 8px 15px;
text-align: center;
& > span {
display: none;
}
text-align: left;
padding-left: 16px;
}
}
......@@ -127,9 +127,7 @@
}
.sidebar-user {
.username {
display: none;
}
width: $sidebar_collapsed_width;
}
}
}
......@@ -144,6 +142,7 @@
height: 28px;
text-align: center;
line-height: 28px;
transition-duration: .3s;
}
.collapse-nav a:hover {
......@@ -178,10 +177,13 @@
.sidebar-user {
position: absolute;
bottom: 0;
width: 100%;
width: $sidebar_width;
padding: 10px;
overflow: hidden;
transition-duration: .3s;
.username {
margin-top: 5px;
width: $sidebar_width - 2 * 10px;
}
}
......@@ -63,43 +63,24 @@
}
}
// Make the placeholder text in the standard textarea the same color as the
// background, effectively hiding it
.zen-backdrop textarea::-webkit-input-placeholder {
color: white;
}
.zen-backdrop textarea:-moz-placeholder {
color: white;
}
.zen-backdrop textarea::-moz-placeholder {
color: white;
}
.zen-backdrop textarea:-ms-input-placeholder {
color: white;
}
// Make the color of the placeholder text in the Zenned-out textarea darker,
// so it becomes visible
input:checked ~ .zen-backdrop textarea::-webkit-input-placeholder {
color: #999;
color: #A8A8A8;
}
input:checked ~ .zen-backdrop textarea:-moz-placeholder {
color: #999;
color: #A8A8A8;
opacity: 1;
}
input:checked ~ .zen-backdrop textarea::-moz-placeholder {
color: #999;
color: #A8A8A8;
opacity: 1;
}
input:checked ~ .zen-backdrop textarea:-ms-input-placeholder {
color: #999;
color: #A8A8A8;
}
}
......@@ -145,9 +145,3 @@ h2.issue-title {
.issue-form .select2-container {
width: 250px !important;
}
.issues-holder {
.issue-info {
margin-left: 20px;
}
}
......@@ -72,7 +72,7 @@
&.active a {
color: #FFF;
font-weight: bold;
background: $color-dark;
&.no-highlight {
border: none;
......
class Admin::IdentitiesController < Admin::ApplicationController
before_action :user
before_action :identity, except: :index
def index
@identities = @user.identities
end
def edit
end
def update
if @identity.update_attributes(identity_params)
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully updated.'
else
render :edit
end
end
def destroy
if @identity.destroy
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully removed.'
else
redirect_to admin_user_identities_path(@user), alert: 'Failed to remove user identity.'
end
end
protected
def user
@user ||= User.find_by!(username: params[:user_id])
end
def identity
@identity ||= user.identities.find(params[:id])
end
def identity_params
params.require(:identity).permit(:provider, :extern_uid)
end
end
class Admin::UsersController < Admin::ApplicationController
before_action :user, only: [:show, :edit, :update, :destroy]
before_action :user, except: [:index, :new, :create]
def index
@users = User.order_name_asc.filter(params[:filter])
......@@ -9,8 +9,17 @@ class Admin::UsersController < Admin::ApplicationController
end
def show
end
def projects
@personal_projects = user.personal_projects
@joined_projects = user.projects.joined(@user)
end
def groups
end
def keys
@keys = user.keys
end
......@@ -38,6 +47,14 @@ class Admin::UsersController < Admin::ApplicationController
end
end
def unlock
if user.unlock_access!
redirect_to :back, alert: "Successfully unlocked"
else
redirect_to :back, alert: "Error occurred. User was not unlocked"
end
end
def create
opts = {
force_random_password: true,
......@@ -86,7 +103,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def destroy
DeleteUserService.new.execute(user)
DeleteUserService.new(current_user).execute(user)
respond_to do |format|
format.html { redirect_to admin_users_path }
......
......@@ -89,7 +89,7 @@ class ApplicationController < ActionController::Base
end
def after_sign_out_path_for(resource)
current_application_settings.after_sign_out_path || new_user_session_path
current_application_settings.after_sign_out_path || new_user_session_path
end
def abilities
......@@ -140,11 +140,6 @@ class ApplicationController < ActionController::Base
return access_denied! unless can?(current_user, action, project)
end
def authorize_labels!
# Labels should be accessible for issues and/or merge requests
authorize_read_issue! || authorize_read_merge_request!
end
def access_denied!
render "errors/access_denied", layout: "errors", status: 404
end
......
class DashboardController < Dashboard::ApplicationController
before_action :load_projects, except: [:projects]
before_action :load_projects
before_action :event_filter, only: :show
respond_to :html
def show
......
......@@ -123,6 +123,8 @@ class GroupsController < Groups::ApplicationController
def determine_layout
if [:new, :create].include?(action_name.to_sym)
'application'
elsif [:edit, :update, :projects].include?(action_name.to_sym)
'group_settings'
else
'group'
end
......
......@@ -4,7 +4,12 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
layout 'profile'
def destroy
Doorkeeper::AccessToken.revoke_all_for(params[:id], current_resource_owner)
if params[:token_id].present?
current_resource_owner.oauth_authorized_tokens.find(params[:token_id]).revoke
else
Doorkeeper::AccessToken.revoke_all_for(params[:id], current_resource_owner)
end
redirect_to applications_profile_url, notice: I18n.t(:notice, scope: [:doorkeeper, :flash, :authorized_applications, :destroy])
end
end
......@@ -24,7 +24,7 @@ class PasswordsController < Devise::PasswordsController
super do |resource|
# TODO (rspeicher): In Devise master (> 3.4.1), we can set
# `Devise.sign_in_after_reset_password = false` and avoid this mess.
if resource.errors.empty? && resource.try(:otp_required_for_login?)
if resource.errors.empty? && resource.try(:two_factor_enabled?)
resource.unlock_access! if unlockable?(resource)
# Since we are not signing this user in, we use the :updated_not_active
......
......@@ -10,7 +10,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def create
if current_user.valid_otp?(params[:pin_code])
current_user.otp_required_for_login = true
current_user.two_factor_enabled = true
@codes = current_user.generate_otp_backup_codes!
current_user.save!
......@@ -30,7 +30,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def destroy
current_user.update_attributes({
otp_required_for_login: false,
two_factor_enabled: false,
encrypted_otp_secret: nil,
encrypted_otp_secret_iv: nil,
encrypted_otp_secret_salt: nil,
......@@ -43,8 +43,12 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
private
def build_qr_code
issuer = "GitLab | #{current_user.email}"
issuer = "#{issuer_host} | #{current_user.email}"
uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer)
RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3)
end
def issuer_host
Gitlab.config.gitlab.host
end
end
......@@ -11,7 +11,8 @@ class ProfilesController < Profiles::ApplicationController
def applications
@applications = current_user.oauth_applications
@authorized_tokens = current_user.oauth_authorized_tokens
@authorized_apps = @authorized_tokens.map(&:application).uniq
@authorized_anonymous_tokens = @authorized_tokens.reject(&:application)
@authorized_apps = @authorized_tokens.map(&:application).uniq - [nil]
end
def update
......
......@@ -6,10 +6,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_read_issue!
# Allow write(create) issue
before_action :authorize_write_issue!, only: [:new, :create]
before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue
before_action :authorize_modify_issue!, only: [:edit, :update]
before_action :authorize_update_issue!, only: [:edit, :update]
# Allow issues bulk update
before_action :authorize_admin_issues!, only: [:bulk_update]
......@@ -55,6 +55,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def show
@participants = @issue.participants(current_user, @project)
@note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.inc_author.fresh
@noteable = @issue
......@@ -121,8 +122,8 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
def authorize_modify_issue!
return render_404 unless can?(current_user, :modify_issue, @issue)
def authorize_update_issue!
return render_404 unless can?(current_user, :update_issue, @issue)
end
def authorize_admin_issues!
......
class Projects::LabelsController < Projects::ApplicationController
before_action :module_enabled
before_action :label, only: [:edit, :update, :destroy]
before_action :authorize_labels!
before_action :authorize_read_label!
before_action :authorize_admin_labels!, except: [:index]
respond_to :js, :html
......
......@@ -14,10 +14,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authorize_read_merge_request!
# Allow write(create) merge_request
before_action :authorize_write_merge_request!, only: [:new, :create]
before_action :authorize_create_merge_request!, only: [:new, :create]
# Allow modify merge_request
before_action :authorize_modify_merge_request!, only: [:close, :edit, :update, :sort]
before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :sort]
def index
terms = params['issue_search']
......@@ -71,7 +71,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def commits
render 'show'
respond_to do |format|
format.html { render 'show' }
format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_commits') } }
end
end
def new
......@@ -226,8 +229,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@closes_issues ||= @merge_request.closes_issues
end
def authorize_modify_merge_request!
return render_404 unless can?(current_user, :modify_merge_request, @merge_request)
def authorize_update_merge_request!
return render_404 unless can?(current_user, :update_merge_request, @merge_request)
end
def authorize_admin_merge_request!
......@@ -254,6 +257,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def define_show_vars
@participants = @merge_request.participants(current_user, @project)
# Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request)
@notes = @merge_request.mr_and_commit_notes.inc_author.fresh
......
class Projects::NotesController < Projects::ApplicationController
# Authorize
before_action :authorize_read_note!
before_action :authorize_write_note!, only: [:create]
before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :find_current_user_notes, except: [:destroy, :delete_attachment]
......
......@@ -6,10 +6,10 @@ class Projects::SnippetsController < Projects::ApplicationController
before_action :authorize_read_project_snippet!
# Allow write(create) snippet
before_action :authorize_write_project_snippet!, only: [:new, :create]
before_action :authorize_create_project_snippet!, only: [:new, :create]
# Allow modify snippet
before_action :authorize_modify_project_snippet!, only: [:edit, :update]
before_action :authorize_update_project_snippet!, only: [:edit, :update]
# Allow destroy snippet
before_action :authorize_admin_project_snippet!, only: [:destroy]
......@@ -75,8 +75,8 @@ class Projects::SnippetsController < Projects::ApplicationController
@snippet ||= @project.snippets.find(params[:id])
end
def authorize_modify_project_snippet!
return render_404 unless can?(current_user, :modify_project_snippet, @snippet)
def authorize_update_project_snippet!
return render_404 unless can?(current_user, :update_project_snippet, @snippet)
end
def authorize_admin_project_snippet!
......
......@@ -2,7 +2,7 @@ require 'project_wiki'
class Projects::WikisController < Projects::ApplicationController
before_action :authorize_read_wiki!
before_action :authorize_write_wiki!, only: [:edit, :create, :history]
before_action :authorize_create_wiki!, only: [:edit, :create, :history]
before_action :authorize_admin_wiki!, only: :destroy
before_action :load_project_wiki
include WikiHelper
......@@ -28,7 +28,7 @@ class Projects::WikisController < Projects::ApplicationController
)
end
else
return render('empty') unless can?(current_user, :write_wiki, @project)
return render('empty') unless can?(current_user, :create_wiki, @project)
@page = WikiPage.new(@project_wiki)
@page.title = params[:id]
......@@ -43,7 +43,7 @@ class Projects::WikisController < Projects::ApplicationController
def update
@page = @project_wiki.find_page(params[:id])
return render('empty') unless can?(current_user, :write_wiki, @project)
return render('empty') unless can?(current_user, :create_wiki, @project)
if @page.update(content, format, message)
redirect_to(
......
......@@ -6,7 +6,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def destroy
DeleteUserService.new.execute(current_user)
DeleteUserService.new(current_user).execute(current_user)
respond_to do |format|
format.html { redirect_to new_user_session_path, notice: "Account successfully removed." }
......
......@@ -57,7 +57,7 @@ class SessionsController < Devise::SessionsController
def authenticate_with_two_factor
user = self.resource = find_user
return unless user && user.otp_required_for_login
return unless user && user.two_factor_enabled?
if user_params[:otp_attempt].present? && session[:otp_user_id]
if valid_otp_attempt?(user)
......
......@@ -2,7 +2,7 @@ class SnippetsController < ApplicationController
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow modify snippet
before_action :authorize_modify_snippet!, only: [:edit, :update]
before_action :authorize_update_snippet!, only: [:edit, :update]
# Allow destroy snippet
before_action :authorize_admin_snippet!, only: [:destroy]
......@@ -87,8 +87,8 @@ class SnippetsController < ApplicationController
end
end
def authorize_modify_snippet!
return render_404 unless can?(current_user, :modify_personal_snippet, @snippet)
def authorize_update_snippet!
return render_404 unless can?(current_user, :update_personal_snippet, @snippet)
end
def authorize_admin_snippet!
......
......@@ -45,10 +45,10 @@ class IssuableFinder
def group
return @group if defined?(@group)
@group =
@group =
if params[:group_id].present?
Group.find(params[:group_id])
else
else
nil
end
end
......@@ -56,10 +56,10 @@ class IssuableFinder
def project
return @project if defined?(@project)
@project =
@project =
if params[:project_id].present?
Project.find(params[:project_id])
else
else
nil
end
end
......@@ -76,7 +76,7 @@ class IssuableFinder
return @milestones if defined?(@milestones)
@milestones =
if milestones? && params[:milestone_title] != NONE
if milestones? && params[:milestone_title] != NONE
Milestone.where(title: params[:milestone_title])
else
nil
......@@ -90,7 +90,7 @@ class IssuableFinder
def assignee
return @assignee if defined?(@assignee)
@assignee =
@assignee =
if assignee? && params[:assignee_id] != NONE
User.find(params[:assignee_id])
else
......@@ -105,7 +105,7 @@ class IssuableFinder
def author
return @author if defined?(@author)
@author =
@author =
if author? && params[:author_id] != NONE
User.find(params[:author_id])
else
......@@ -148,8 +148,6 @@ class IssuableFinder
case params[:state]
when 'closed'
items.closed
when 'rejected'
items.respond_to?(:rejected) ? items.rejected : items.closed
when 'merged'
items.respond_to?(:merged) ? items.merged : items.closed
when 'all'
......
......@@ -27,7 +27,7 @@ module AppearancesHelper
if brand_item && brand_item.light_logo?
image_tag brand_item.light_logo
else
image_tag 'logo-white.png'
image_tag 'logo.svg'
end
end
end
......@@ -179,20 +179,33 @@ module ApplicationHelper
BroadcastMessage.current
end
def time_ago_with_tooltip(date, placement = 'top', html_class = 'time_ago')
capture_haml do
if date.is_a?(Date)
haml_tag :time, date.to_s,
class: html_class, datetime: date.iso8601, title: date.stamp('Aug 21, 2011'),
data: { toggle: 'tooltip', placement: placement }
else
haml_tag :time, date.to_s,
class: html_class, datetime: date.getutc.iso8601, title: date.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
data: { toggle: 'tooltip', placement: placement }
end
# Render a `time` element with Javascript-based relative date and tooltip
#
# time - Time object
# placement - Tooltip placement String (default: "top")
# html_class - Custom class for `time` element (default: "time_ago")
# skip_js - When true, exclude the `script` tag (default: false)
#
# By default also includes a `script` element with Javascript necessary to
# initialize the `timeago` jQuery extension. If this method is called many
# times, for example rendering hundreds of commits, it's advisable to disable
# this behavior using the `skip_js` argument and re-initializing `timeago`
# manually once all of the elements have been rendered.
#
# A `js-timeago` class is always added to the element, even when a custom
# `html_class` argument is provided.
#
# Returns an HTML-safe String
def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
element = content_tag :time, time.to_s,
class: "#{html_class} js-timeago",
datetime: time.getutc.iso8601,
title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
data: { toggle: 'tooltip', placement: placement }
element += javascript_tag "$('.js-timeago').timeago()" unless skip_js
haml_tag :script, "$('." + html_class + "').timeago().tooltip()"
end.html_safe
element
end
def render_markup(file_name, file_content)
......@@ -220,39 +233,6 @@ module ApplicationHelper
Gitlab::MarkupHelper.asciidoc?(filename)
end
# Overrides ActionView::Helpers::UrlHelper#link_to to add `rel="nofollow"` to
# external links
def link_to(name = nil, options = nil, html_options = {})
if options.kind_of?(String)
if !options.start_with?('#', '/')
html_options = add_nofollow(options, html_options)
end
end
super
end
# Add `"rel=nofollow"` to external links
#
# link - String link to check
# html_options - Hash of `html_options` passed to `link_to`
#
# Returns `html_options`, adding `rel: nofollow` for external links
def add_nofollow(link, html_options = {})
begin
uri = URI(link)
if uri && uri.absolute? && uri.host != Gitlab.config.gitlab.host
rel = html_options.fetch(:rel, '')
html_options[:rel] = (rel + ' nofollow').strip
end
rescue URI::Error
# noop
end
html_options
end
def promo_host
'about.gitlab.com'
end
......@@ -301,10 +281,9 @@ module ApplicationHelper
def state_filters_text_for(entity, project)
titles = {
opened: "Open",
merged: "Accepted"
opened: "Open"
}
entity_title = titles[entity] || entity.to_s.humanize
count =
......
module BroadcastMessagesHelper
def broadcast_styling(broadcast_message)
if(broadcast_message.color || broadcast_message.font)
"background-color:#{broadcast_message.color};color:#{broadcast_message.font}"
else
""
styling = ''
if broadcast_message.color.present?
styling << "background-color: #{broadcast_message.color}"
styling << '; ' if broadcast_message.font.present?
end
if broadcast_message.font.present?
styling << "color: #{broadcast_message.font}"
end
styling
end
end
......@@ -189,7 +189,7 @@ module EventsHelper
xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
xml.link href: event_link
xml.title truncate(event_title, length: 80)
xml.updated event.created_at.strftime("%Y-%m-%dT%H:%M:%S%Z")
xml.updated event.created_at.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(event.author_email)
xml.author do |author|
xml.name event.author_name
......
......@@ -98,6 +98,29 @@ module GitlabMarkdownHelper
end
end
MARKDOWN_TIPS = [
"End a line with two or more spaces for a line-break, or soft-return",
"Inline code can be denoted by `surrounding it with backticks`",
"Blocks of code can be denoted by three backticks ``` or four leading spaces",
"Emoji can be added by :emoji_name:, for example :thumbsup:",
"Notify other participants using @user_name",
"Notify a specific group using @group_name",
"Notify the entire team using @all",
"Reference an issue using a hash, for example issue #123",
"Reference a merge request using an exclamation point, for example MR !123",
"Italicize words or phrases using *asterisks* or _underscores_",
"Bold words or phrases using **double asterisks** or __double underscores__",
"Strikethrough words or phrases using ~~two tildes~~",
"Make a bulleted list using + pluses, - minuses, or * asterisks",
"Denote blockquotes using > at the beginning of a line",
"Make a horizontal line using three or more hyphens ---, asterisks ***, or underscores ___"
].freeze
# Returns a random markdown tip for use as a textarea placeholder
def random_markdown_tip
"Tip: #{MARKDOWN_TIPS.sample}"
end
private
# Return +text+, truncated to +max_chars+ characters, excluding any HTML
......
......@@ -52,4 +52,12 @@ module GitlabRoutingHelper
def project_snippet_url(entity, *args)
namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args)
end
def toggle_subscription_path(entity, *args)
if entity.is_a?(Issue)
toggle_subscription_namespace_project_issue_path(entity.project.namespace, entity.project, entity)
else
toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity)
end
end
end
......@@ -19,16 +19,6 @@ module GroupsHelper
end
end
def group_settings_page?
if current_controller?('groups')
current_action?('edit') || current_action?('projects')
elsif ['ldap_group_links', 'audit_events', 'hooks'].any?{ |c| current_controller? c }
true
else
false
end
end
def group_icon(group)
if group.is_a?(String)
group = Group.find_by(path: group)
......
module IconsHelper
include FontAwesome::Rails::IconHelper
# Creates an icon tag given icon name(s) and possible icon modifiers.
#
# Right now this method simply delegates directly to `fa_icon` from the
......
......@@ -45,13 +45,13 @@ module IssuesHelper
def issue_timestamp(issue)
# Shows the created at time and the updated at time if different
ts = "#{time_ago_with_tooltip(issue.created_at, 'bottom', 'note_created_ago')}"
ts = time_ago_with_tooltip(issue.created_at, placement: 'bottom', html_class: 'note_created_ago')
if issue.updated_at != issue.created_at
ts << capture_haml do
haml_tag :span do
haml_concat '&middot;'
haml_concat icon('edit', title: 'edited')
haml_concat time_ago_with_tooltip(issue.updated_at, 'bottom', 'issue_edited_ago')
haml_concat time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago')
end
end
end
......
......@@ -25,13 +25,13 @@ module NotesHelper
def note_timestamp(note)
# Shows the created at time and the updated at time if different
ts = "#{time_ago_with_tooltip(note.created_at, 'bottom', 'note_created_ago')}"
ts = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago')
if note.updated_at != note.created_at
ts << capture_haml do
haml_tag :span do
haml_concat '&middot;'
haml_concat icon('edit', title: 'edited')
haml_concat time_ago_with_tooltip(note.updated_at, 'bottom', 'note_edited_ago')
haml_concat time_ago_with_tooltip(note.updated_at, placement: 'bottom', html_class: 'note_edited_ago')
end
end
end
......
module NotificationsHelper
include IconsHelper
def notification_icon(notification)
if notification.disabled?
icon('volume-off', class: 'ns-mute')
......
......@@ -21,7 +21,7 @@ module OauthHelper
def enabled_social_providers
enabled_oauth_providers.select do |name|
[:twitter, :gitlab, :github, :bitbucket, :google_oauth2, :kerberos].include?(name.to_sym)
[:saml, :twitter, :gitlab, :github, :bitbucket, :google_oauth2, :kerberos].include?(name.to_sym)
end
end
......
......@@ -211,7 +211,7 @@ module ProjectsHelper
def project_last_activity(project)
if project.last_activity_at
time_ago_with_tooltip(project.last_activity_at, 'bottom', 'last_activity_time_ago')
time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago')
else
"Never"
end
......
......@@ -63,7 +63,7 @@ module SubmoduleHelper
namespace = components.pop.gsub(/^\.\.$/, '')
if namespace.empty?
namespace = @project.namespace.name
namespace = @project.namespace.path
end
[
......
......@@ -84,6 +84,7 @@ class Ability
def project_abilities(user, project)
rules = []
key = "/user/#{user.id}/project/#{project.id}"
RequestStore.store[key] ||= begin
team = project.team
......@@ -154,14 +155,15 @@ class Ability
:read_project,
:read_wiki,
:read_issue,
:read_label,
:read_milestone,
:read_project_snippet,
:read_project_member,
:read_merge_request,
:read_note,
:write_project,
:write_issue,
:write_note
:create_project,
:create_issue,
:create_note
]
end
......@@ -169,27 +171,27 @@ class Ability
project_guest_rules + [
:download_code,
:fork_project,
:write_project_snippet
:create_project_snippet,
:update_issue,
:admin_issue,
:admin_label,
]
end
def project_dev_rules
project_report_rules + [
:write_merge_request,
:write_wiki,
:modify_issue,
:admin_issue,
:admin_label,
:create_merge_request,
:create_wiki,
:push_code
]
end
def project_archived_rules
[
:write_merge_request,
:create_merge_request,
:push_code,
:push_code_to_protected_branches,
:modify_merge_request,
:update_merge_request,
:admin_merge_request
]
end
......@@ -197,10 +199,8 @@ class Ability
def project_master_rules
project_dev_rules + [
:push_code_to_protected_branches,
:modify_issue,
:modify_project_snippet,
:modify_merge_request,
:admin_issue,
:update_project_snippet,
:update_merge_request,
:admin_milestone,
:admin_project_snippet,
:admin_project_member,
......@@ -260,30 +260,40 @@ class Ability
rules.flatten
end
[:issue, :note, :project_snippet, :personal_snippet, :merge_request].each do |name|
[:issue, :merge_request].each do |name|
define_method "#{name}_abilities" do |user, subject|
if subject.author == user || user.is_admin?
rules = [
rules = []
if subject.author == user || (subject.respond_to?(:assignee) && subject.assignee == user)
rules += [
:"read_#{name}",
:"write_#{name}",
:"modify_#{name}",
:"admin_#{name}"
:"update_#{name}",
]
rules.push(:change_visibility_level) if subject.is_a?(Snippet)
rules
elsif subject.respond_to?(:assignee) && subject.assignee == user
[
end
rules += project_abilities(user, subject.project)
rules
end
end
[:note, :project_snippet, :personal_snippet].each do |name|
define_method "#{name}_abilities" do |user, subject|
rules = []
if subject.author == user
rules += [
:"read_#{name}",
:"write_#{name}",
:"modify_#{name}",
:"update_#{name}",
:"admin_#{name}"
]
else
if subject.respond_to?(:project)
project_abilities(user, subject.project)
else
[]
end
end
if subject.respond_to?(:project) && subject.project
rules += project_abilities(user, subject.project)
end
rules
end
end
......@@ -292,13 +302,16 @@ class Ability
target_user = subject.user
group = subject.group
can_manage = group_abilities(user, group).include?(:admin_group)
if can_manage && (user != target_user)
rules << :modify_group_member
rules << :update_group_member
rules << :destroy_group_member
end
if !group.last_owner?(user) && (can_manage || (user == target_user))
rules << :destroy_group_member
end
rules
end
......@@ -315,8 +328,8 @@ class Ability
def named_abilities(name)
[
:"read_#{name}",
:"write_#{name}",
:"modify_#{name}",
:"create_#{name}",
:"update_#{name}",
:"admin_#{name}"
]
end
......
......@@ -157,11 +157,11 @@ class Commit
end
def author
User.find_for_commit(author_email, author_name)
@author ||= User.find_by_any_email(author_email)
end
def committer
User.find_for_commit(committer_email, committer_name)
@committer ||= User.find_by_any_email(committer_email)
end
def notes
......
......@@ -75,7 +75,7 @@ module Mentionable
refs.reject! { |ref| without.include?(ref) }
refs.each do |ref|
Note.create_cross_reference_note(ref, local_reference, a)
SystemNoteService.cross_reference(ref, local_reference, a)
end
end
......
......@@ -14,7 +14,7 @@
#
# participant :author, :assignee, :mentioned_users, :notes
# end
#
#
# issue = Issue.last
# users = issue.participants
# # `users` will contain the issue's author, its assignee,
......@@ -35,11 +35,13 @@ module Participable
end
end
# Be aware that this method makes a lot of sql queries.
# Save result into variable if you are going to reuse it inside same request
def participants(current_user = self.author, project = self.project)
participants = self.class.participant_attrs.flat_map do |attr|
meth = method(attr)
value =
value =
if meth.arity == 1 || meth.arity == -1
meth.call(current_user)
else
......@@ -59,7 +61,7 @@ module Participable
end
private
def participants_for(value, current_user = nil, project = nil)
case value
when User
......
......@@ -44,7 +44,7 @@ class Event < ActiveRecord::Base
after_create :reset_project_activity
# Scopes
scope :recent, -> { order("created_at DESC") }
scope :recent, -> { order(created_at: :desc) }
scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent }
scope :with_associations, -> { includes(project: :namespace) }
......
......@@ -14,6 +14,7 @@ class Identity < ActiveRecord::Base
include Sortable
belongs_to :user
validates :provider, presence: true
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
validates :user_id, uniqueness: { scope: :provider }
end
......@@ -129,16 +129,14 @@ class MergeRequest < ActiveRecord::Base
validate :validate_fork
scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.project_ids) }
scope :merged, -> { with_state(:merged) }
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 :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) }
# Closed scope for merge request should return
# both merged and closed mr's
scope :closed, -> { with_states(:closed, :merged) }
scope :rejected, -> { with_states(:closed) }
scope :merged, -> { with_state(:merged) }
scope :closed, -> { with_state(:closed) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
def self.reference_prefix
'!'
......@@ -211,7 +209,14 @@ class MergeRequest < ActiveRecord::Base
end
def check_if_can_be_merged
if Gitlab::Satellite::MergeAction.new(self.author, self).can_be_merged?
can_be_merged =
if for_fork?
Gitlab::Satellite::MergeAction.new(self.author, self).can_be_merged?
else
project.repository.can_be_merged?(source_branch, target_branch)
end
if can_be_merged
mark_as_mergeable
else
mark_as_unmergeable
......@@ -445,4 +450,14 @@ class MergeRequest < ActiveRecord::Base
def can_be_merged_by?(user)
::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch)
end
def state_human_name
if merged?
"Merged"
elsif closed?
"Closed"
else
"Open"
end
end
end
......@@ -56,7 +56,7 @@ class Milestone < ActiveRecord::Base
end
def closed_items_count
self.issues.closed.count + self.merge_requests.closed.count
self.issues.closed.count + self.merge_requests.closed_and_merged.count
end
def total_items_count
......
......@@ -63,11 +63,6 @@ class Note < ActiveRecord::Base
after_update :set_references
class << self
# TODO (rspeicher): Update usages
def create_cross_reference_note(*args)
SystemNoteService.cross_reference(*args)
end
def discussions_from_notes(notes)
discussion_ids = []
discussions = []
......
......@@ -76,6 +76,7 @@ class GitlabCiService < CiService
params = {
id: new_project.id,
name_with_namespace: new_project.name_with_namespace,
path_with_namespace: new_project.path_with_namespace,
web_url: new_project.web_url,
default_branch: new_project.default_branch,
ssh_url_to_repo: new_project.ssh_url_to_repo
......
......@@ -5,8 +5,13 @@ class Repository
def initialize(path_with_namespace, default_branch = nil, project = nil)
@path_with_namespace = path_with_namespace
@raw_repository = Gitlab::Git::Repository.new(path_to_repo) if path_with_namespace
@project = project
if path_with_namespace
@raw_repository = Gitlab::Git::Repository.new(path_to_repo)
@raw_repository.autocrlf = :input
end
rescue Gitlab::Git::Repository::NoRepository
nil
end
......@@ -168,7 +173,9 @@ class Repository
end
def blob_at(sha, path)
Gitlab::Git::Blob.find(self, sha, path)
unless Gitlab::Git.blank_ref?(sha)
Gitlab::Git::Blob.find(self, sha, path)
end
end
def blob_by_oid(oid)
......@@ -407,8 +414,6 @@ class Repository
Gitlab::Git::Blob.remove(raw_repository, options)
end
private
def user_to_comitter(user)
{
email: user.email,
......@@ -417,6 +422,17 @@ class Repository
}
end
def can_be_merged?(source_branch, target_branch)
our_commit = rugged.branches[target_branch].target
their_commit = rugged.branches[source_branch].target
if our_commit && their_commit
!rugged.merge_commits(our_commit, their_commit).conflicts?
end
end
private
def cache
@cache ||= RepositoryCache.new(path_with_namespace)
end
......
......@@ -34,7 +34,6 @@ class Snippet < ActiveRecord::Base
validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 }
validates :file_name,
presence: true,
length: { within: 0..255 },
format: { with: Gitlab::Regex.file_name_regex,
message: Gitlab::Regex.file_name_regex_message }
......
......@@ -50,12 +50,12 @@
# bitbucket_access_token :string(255)
# bitbucket_access_token_secret :string(255)
# location :string(255)
# public_email :string(255) default(""), not null
# encrypted_otp_secret :string(255)
# encrypted_otp_secret_iv :string(255)
# encrypted_otp_secret_salt :string(255)
# otp_required_for_login :boolean
# otp_required_for_login :boolean default(FALSE), not null
# otp_backup_codes :text
# public_email :string(255) default(""), not null
# dashboard :integer default(0)
#
......@@ -80,6 +80,7 @@ class User < ActiveRecord::Base
devise :two_factor_authenticatable,
otp_secret_encryption_key: File.read(Rails.root.join('.secret')).chomp
alias_attribute :two_factor_enabled, :otp_required_for_login
devise :two_factor_backupable, otp_number_of_backup_codes: 10
serialize :otp_backup_codes, JSON
......@@ -138,7 +139,9 @@ class User < ActiveRecord::Base
# Validations
#
validates :name, presence: true
validates :email, presence: true, email: { strict_mode: true }, uniqueness: true
# Note that a 'uniqueness' and presence check is provided by devise :validatable for email. We do not need to
# duplicate that here as the validation framework will have duplicate errors in the event of a failure.
validates :email, presence: true, email: { strict_mode: true }
validates :notification_email, presence: true, email: { strict_mode: true }
validates :public_email, presence: true, email: { strict_mode: true }, allow_blank: true, uniqueness: true
validates :bio, length: { maximum: 255 }, allow_blank: true
......@@ -171,6 +174,9 @@ class User < ActiveRecord::Base
after_create :post_create_hook
after_destroy :post_destroy_hook
# User's Dashboard preference
# Note: When adding an option, it MUST go on the end of the array.
enum dashboard: [:projects, :stars]
alias_attribute :private_token, :authentication_token
......@@ -189,13 +195,15 @@ class User < ActiveRecord::Base
mount_uploader :avatar, AvatarUploader
# Scopes
scope :admins, -> { where(admin: true) }
scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_state(:blocked) }
scope :active, -> { with_state(:active) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
scope :subscribed_for_admin_email, -> { where(admin_email_unsubscribed_at: nil) }
scope :ldap, -> { joins(:identities).where('identities.provider LIKE ?', 'ldap%') }
scope :with_two_factor, -> { where(two_factor_enabled: true) }
scope :without_two_factor, -> { where(two_factor_enabled: false) }
#
# Class methods
......@@ -220,11 +228,23 @@ class User < ActiveRecord::Base
end
end
def find_for_commit(email, name)
# Prefer email match over name match
User.where(email: email).first ||
User.joins(:emails).where(emails: { email: email }).first ||
User.where(name: name).first
# Find a User by their primary email or any associated secondary email
def find_by_any_email(email)
user_table = arel_table
email_table = Email.arel_table
# Use ARel to build a query:
query = user_table.
# SELECT "users".* FROM "users"
project(user_table[Arel.star]).
# LEFT OUTER JOIN "emails"
join(email_table, Arel::Nodes::OuterJoin).
# ON "users"."id" = "emails"."user_id"
on(user_table[:id].eq(email_table[:user_id])).
# WHERE ("user"."email" = '<email>' OR "emails"."email" = '<email>')
where(user_table[:email].eq(email).or(email_table[:email].eq(email)))
find_by_sql(query.to_sql).first
end
def existing_member?(email)
......@@ -233,9 +253,16 @@ class User < ActiveRecord::Base
def filter(filter_name)
case filter_name
when "admins"; self.admins
when "blocked"; self.blocked
when "wop"; self.without_projects
when 'admins'
self.admins
when 'blocked'
self.blocked
when 'two_factor_disabled'
self.without_two_factor
when 'two_factor_enabled'
self.with_two_factor
when 'wop'
self.without_projects
else
self.active
end
......@@ -348,6 +375,8 @@ class User < ActiveRecord::Base
end
def owns_public_email
return if self.public_email.blank?
self.errors.add(:public_email, "is not an email you own") unless self.all_emails.include?(self.public_email)
end
......@@ -529,7 +558,7 @@ class User < ActiveRecord::Base
def set_public_email
if self.public_email.blank? || !self.all_emails.include?(self.public_email)
self.public_email = nil
self.public_email = ''
end
end
......@@ -735,8 +764,4 @@ class User < ActiveRecord::Base
def can_be_removed?
!solo_owned_groups.present?
end
# User's Dashboard preference
# Note: When adding an option, it MUST go on the end of the array.
enum dashboard: [:projects, :stars]
end
class DeleteUserService
attr_accessor :current_user
def initialize(current_user)
@current_user = current_user
end
def execute(user)
if user.solo_owned_groups.present?
user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
......
......@@ -105,7 +105,7 @@ class GitPushService
author ||= commit_user(commit)
refs.each do |r|
Note.create_cross_reference_note(r, commit, author)
SystemNoteService.cross_reference(r, commit, author)
end
end
end
......
......@@ -26,4 +26,12 @@ class IssuableBaseService < BaseService
issuable, issuable.project, current_user, branch_type,
old_branch, new_branch)
end
def filter_params
unless can?(current_user, :admin_issue, project)
params.delete(:milestone_id)
params.delete(:label_ids)
params.delete(:assignee_id)
end
end
end
......@@ -10,7 +10,7 @@ module Issues
issues = Issue.where(id: issues_ids)
issues.each do |issue|
next unless can?(current_user, :modify_issue, issue)
next unless can?(current_user, :update_issue, issue)
Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue)
end
......
module Issues
class CreateService < Issues::BaseService
def execute
filter_params
label_params = params[:label_ids]
issue = project.issues.new(params.except(:label_ids))
issue.author = current_user
......
......@@ -17,6 +17,7 @@ module Issues
params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE
params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE
filter_params
old_labels = issue.labels.to_a
if params.present? && issue.update_attributes(params.except(:state_event,
......
......@@ -5,17 +5,20 @@ module MergeRequests
# mark merge request as merged and execute all hooks and notifications
# Called when you do merge via GitLab UI
class AutoMergeService < BaseMergeService
attr_reader :merge_request, :commit_message
def execute(merge_request, commit_message)
@commit_message = commit_message
@merge_request = merge_request
merge_request.lock_mr
if Gitlab::Satellite::MergeAction.new(current_user, merge_request).merge!(commit_message)
if merge!
merge_request.merge
create_merge_event(merge_request, current_user)
create_note(merge_request)
notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request)
execute_hooks(merge_request, 'merge')
true
else
merge_request.unlock_mr
......@@ -26,5 +29,39 @@ module MergeRequests
merge_request.mark_as_unmergeable
false
end
def merge!
if merge_request.for_fork?
Gitlab::Satellite::MergeAction.new(current_user, merge_request).merge!(commit_message)
else
# Merge local branches using rugged instead of satellites
if sha = commit
after_commit(sha, merge_request.target_branch)
end
end
end
def commit
committer = repository.user_to_comitter(current_user)
options = {
message: commit_message,
author: committer,
committer: committer
}
repository.merge(merge_request.source_branch, merge_request.target_branch, options)
end
def after_commit(sha, branch)
commit = repository.commit(sha)
full_ref = 'refs/heads/' + branch
old_sha = commit.parent_id || Gitlab::Git::BLANK_SHA
GitPushService.new.execute(project, current_user, old_sha, sha, full_ref)
end
def repository
project.repository
end
end
end
module MergeRequests
class CreateService < MergeRequests::BaseService
def execute
filter_params
label_params = params[:label_ids]
merge_request = MergeRequest.new(params.except(:label_ids))
merge_request.source_project = project
......
......@@ -27,6 +27,7 @@ module MergeRequests
params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE
params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE
filter_params
old_labels = merge_request.labels.to_a
if params.present? && merge_request.update_attributes(
......
......@@ -15,7 +15,7 @@ module Notes
# Create a cross-reference note if this Note contains GFM that names an
# issue, merge request, or commit.
note.references.each do |mentioned|
Note.create_cross_reference_note(mentioned, note.noteable, note.author)
SystemNoteService.cross_reference(mentioned, note.noteable, note.author)
end
execute_hooks(note)
......
......@@ -13,7 +13,7 @@ module Notes
# Create a cross-reference note if this Note contains GFM that
# names an issue, merge request, or commit.
note.references.each do |mentioned|
Note.create_cross_reference_note(mentioned, note.noteable, note.author)
SystemNoteService.cross_reference(mentioned, note.noteable, note.author)
end
end
end
......
......@@ -9,9 +9,9 @@ class UpdateSnippetService < BaseService
def execute
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
if new_visibility && new_visibility.to_i != snippet.visibility_level
unless can?(current_user, :change_visibility_level, snippet) &&
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(snippet, new_visibility)
return snippet
end
......
......@@ -6,19 +6,36 @@
%p= msg
%fieldset
%legend Features
%legend Visibility and Access Controls
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :signup_enabled do
= f.check_box :signup_enabled
Signup enabled
= f.label :default_branch_protection, class: 'control-label col-sm-2'
.col-sm-10
= f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control'
.form-group.project-visibility-level-holder
= f.label :default_project_visibility, class: 'control-label col-sm-2'
.col-sm-10
= render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: 'Project')
.form-group.project-visibility-level-holder
= f.label :default_snippet_visibility, class: 'control-label col-sm-2'
.col-sm-10
= render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: 'Snippet')
.form-group
= f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
.col-sm-10
- data_attrs = { toggle: 'buttons' }
.btn-group{ data: data_attrs }
- restricted_level_checkboxes('restricted-visibility-help').each do |level|
= level
%span.help-block#restricted-visibility-help Selected levels cannot be used by non-admin users for projects or snippets
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :signin_enabled do
= f.check_box :signin_enabled
Signin enabled
= f.label :version_check_enabled do
= f.check_box :version_check_enabled
Version check enabled
%fieldset
%legend Account and Limit Settings
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
......@@ -32,38 +49,46 @@
= f.check_box :twitter_sharing_enabled, :'aria-describedby' => 'twitter_help_block'
Twitter enabled
%span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :version_check_enabled do
= f.check_box :version_check_enabled
Version check enabled
%fieldset
%legend Misc
.form-group
= f.label :default_projects_limit, class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :default_projects_limit, class: 'form-control'
.form-group
= f.label :default_branch_protection, class: 'control-label col-sm-2'
= f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2'
.col-sm-10
= f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control'
.form-group.project-visibility-level-holder
= f.label :default_project_visibility, class: 'control-label col-sm-2'
= f.number_field :max_attachment_size, class: 'form-control'
.form-group
= f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2'
.col-sm-10
= render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: 'Project')
.form-group.project-visibility-level-holder
= f.label :default_snippet_visibility, class: 'control-label col-sm-2'
= f.number_field :session_expire_delay, class: 'form-control'
%span.help-block#session_expire_delay_help_block GitLab restart is required to apply changes
.form-group
= f.label :user_oauth_applications, 'User OAuth applications', class: 'control-label col-sm-2'
.col-sm-10
= render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: 'Snippet')
.checkbox
= f.label :user_oauth_applications do
= f.check_box :user_oauth_applications
Allow users to register any application to use GitLab as an OAuth provider
%fieldset
%legend Sign-in Restrictions
.form-group
= f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :signup_enabled do
= f.check_box :signup_enabled
Sign-up enabled
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :signin_enabled do
= f.check_box :signin_enabled
Sign-in enabled
.form-group
= f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2'
.col-sm-10
- data_attrs = { toggle: 'buttons' }
.btn-group{ data: data_attrs }
- restricted_level_checkboxes('restricted-visibility-help').each do |level|
= level
%span.help-block#restricted-visibility-help Selected levels cannot be used by non-admin users for projects or snippets
= f.text_area :restricted_signup_domains_raw, placeholder: 'domain.com', class: 'form-control'
.help-block Only users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
.form-group
= f.label :home_page_url, class: 'control-label col-sm-2'
.col-sm-10
......@@ -84,27 +109,6 @@
.col-sm-10
= f.text_area :help_text, class: 'form-control', rows: 4
.help-block Markdown enabled
.form-group
= f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :max_attachment_size, class: 'form-control', min: 0
.form-group
= f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :session_expire_delay, class: 'form-control'
%span.help-block#session_expire_delay_help_block GitLab restart is required to apply changes
.form-group
= f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :restricted_signup_domains_raw, placeholder: 'domain.com', class: 'form-control'
.help-block Only users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
.form_group
= f.label :user_oauth_applications, 'User OAuth applications', class: 'control-label col-sm-2'
.col-sm-10
.checkbox
= f.label :user_oauth_applications do
= f.check_box :user_oauth_applications
Allow users to register any application to use GitLab as an OAuth provider
.form-actions
= f.submit 'Save', class: 'btn btn-primary'
- page_title "Settings"
%h3.page-title Application settings
%h3.page-title Settings
%hr
= render 'form'
......@@ -16,8 +16,7 @@
- @deploy_keys.each do |deploy_key|
%tr
%td
= link_to admin_deploy_key_path(deploy_key) do
%strong= deploy_key.title
%strong= deploy_key.title
%td
%code.key-fingerprint= deploy_key.fingerprint
%td
......
= form_for [:admin, @user, @identity], html: { class: 'form-horizontal fieldset-form' } do |f|
- if @identity.errors.any?
#error_explanation
.alert.alert-danger
- @identity.errors.full_messages.each do |msg|
%p= msg
.form-group
= f.label :provider, class: 'control-label'
.col-sm-10
= f.select :provider, Gitlab::OAuth::Provider.names, { allow_blank: false }, class: 'form-control'
.form-group
= f.label :extern_uid, "Identifier", class: 'control-label'
.col-sm-10
= f.text_field :extern_uid, class: 'form-control', required: true
.form-actions
= f.submit 'Save changes', class: "btn btn-save"
%tr
%td
= identity.provider
%td
= identity.extern_uid
%td
= link_to edit_admin_user_identity_path(@user, identity), class: 'btn btn-xs btn-grouped' do
Edit
= link_to [:admin, @user, identity], method: :delete,
class: 'btn btn-xs btn-danger',
data: { confirm: "Are you sure you want to remove this identity?" } do
Delete
- page_title "Edit", @identity.provider, "Identities", @user.name, "Users"
%h3.page-title
Edit identity for #{@user.name}
%hr
= render 'form'
- page_title "Identities", @user.name, "Users"
= render 'admin/users/head'
- if @identities.present?
%table.table
%thead
%tr
%th Provider
%th Identifier
%th
= render @identities
- else
%h4 This user has no identities
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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