diff options
549 files changed, 9160 insertions, 3403 deletions
diff --git a/.gitignore b/.gitignore index 4f778371512..2c6b65b7b7d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ public/assets/ .envrc dump.rdb tags +.gitlab_shell_secret diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000000..ac2cdeba013 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.1.3 diff --git a/CHANGELOG b/CHANGELOG index 7e6e0f5f64b..0ddae406cf6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,72 @@ +v 7.6.0 + - Fork repository to groups + - New rugged version + - Add CRON=1 backup setting for quiet backups + - Fix failing wiki restore + - Add optional Sidekiq MemoryKiller middleware (enabled via SIDEKIQ_MAX_RSS env variable) + - Monokai highlighting style now more faithful to original design (Mark Riedesel) + - Create project with repository in synchrony + - Added ability to create empty repo or import existing one if project does not have repository + - Reactivate highlight.js language autodetection + - Mobile UI improvements + - Change maximum avatar file size from 100KB to 200KB + - Strict validation for snippet file names + - Enable Markdown preview for issues, merge requests, milestones, and notes (Vinnie Okada) + - In the docker directory is a container template based on the Omnibus packages. + - Update Sidekiq to version 2.17.8 + - Add author filter to project issues and merge requests pages + - Atom feed for user activity + - Support multiple omniauth providers for the same user + - Rendering cross reference in issue title and tooltip for merge request + - Show username in comments + - Possibility to create Milestones or Labels when Issues are disabled + - Fix bug with showing gpg signature in tag + +v 7.5.2 + - Don't log Sidekiq arguments by default + +v 7.5.0 + - API: Add support for Hipchat (Kevin Houdebert) + - Add time zone configuration in gitlab.yml (Sullivan Senechal) + - Fix LDAP authentication for Git HTTP access + - Run 'GC.start' after every EmailsOnPushWorker job + - Fix LDAP config lookup for provider 'ldap' + - Drop all sequences during Postgres database restore + - Project title links to project homepage (Ben Bodenmiller) + - Add Atlassian Bamboo CI service (Drew Blessing) + - Mentioned @user will receive email even if he is not participating in issue or commit + - Session API: Use case-insensitive authentication like in UI (Andrey Krivko) + - Tie up loose ends with annotated tags: API & UI (Sean Edge) + - Return valid json for deleting branch via API (sponsored by O'Reilly Media) + - Expose username in project events API (sponsored by O'Reilly Media) + - Adds comments to commits in the API + - Performance improvements + - Fix post-receive issue for projects with deleted forks + - New gitlab-shell version with custom hooks support + - Improve code + - GitLab CI 5.2+ support (does not support older versions) + - Fixed bug when you can not push commits starting with 000000 to protected branches + - Added a password strength indicator + - Change project name and path in one form + - Display renamed files in diff views (Vinnie Okada) + - Fix raw view for public snippets + - Use secret token with GitLab internal API. + - Add missing timestamps to 'members' table + +v 7.4.3 + - Fix raw snippets view + - Fix security issue for member api + - Fix buildbox integration + +v 7.4.2 + - Fix internal snippet exposing for unauthenticated users + +v 7.4.1 + - Fix LDAP authentication for Git HTTP access + - Fix LDAP config lookup for provider 'ldap' + - Fix public snippets + - Fix 500 error on projects with nested submodules + v 7.4.0 - Refactored membership logic - Improve error reporting on users API (Julien Bianchi) @@ -9,6 +78,7 @@ v 7.4.0 - Do not delete tmp/repositories itself during clean-up, only its contents - Support for backup uploads to remote storage - Prevent notes polling when there are not notes + - Internal ForkService: Prepare support for fork to a given namespace - API: Add support for forking a project via the API (Bernhard Kaindl) - API: filter project issues by milestone (Julien Bianchi) - Fail harder in the backup script @@ -20,6 +90,21 @@ v 7.4.0 - Add select field type for services options (Sullivan Senechal) - Add cross-project references to the Markdown parser (Vinnie Okada) - Add task lists to issue and merge request descriptions (Vinnie Okada) + - Snippets can be public, internal or private + - Improve danger zone: ask project path to confirm data-loss action + - Raise exception on forgery + - Show build coverage in Merge Requests (requires GitLab CI v5.1) + - New milestone and label links on issue edit form + - Improved repository graphs + - Improve event note display in dashboard and project activity views (Vinnie Okada) + - Add users sorting to admin area + - UI improvements + - Fix ambiguous sha problem with mentioned commit + - Fixed bug with apostrophe when at mentioning users + - Add active directory ldap option + - Developers can push to wiki repo. Protected branches does not affect wiki repo any more + - Faster rev list + - Fix branch removal v 7.3.2 - Fix creating new file via web editor diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d1e8270f46..9531b27089b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ By submitting code as an individual you agree to the [individual contributor lic ## Security vulnerability disclosure -Please report suspected security vulnerabilities in private to support@gitlab.com, also see the [disclosure section on the GitLab.com website](http://www.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities. +Please report suspected security vulnerabilities in private to support@gitlab.com, also see the [disclosure section on the GitLab.com website](http://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities. ## Closing policy for issues and merge requests @@ -22,7 +22,7 @@ Issues and merge requests should be in English and contain appropriate language ## Issue tracker -To get support for your particular problem please use the channels as detailed in the [getting help section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#getting-help). Professional [support subscriptions](http://www.gitlab.com/subscription/) and [consulting services](http://www.gitlab.com/consultancy/) are available from [GitLab.com](http://www.gitlab.com/). +To get support for your particular problem please use the channels as detailed in the [getting help section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#getting-help). Professional [support subscriptions](http://about.gitlab.com/subscription/) and [consulting services](http://about.gitlab.com/consultancy/) are available from [GitLab.com](http://about.gitlab.com/). The [issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues) is only for obvious errors in the latest [stable or development release of GitLab](MAINTENANCE.md). If something is wrong but it is not a regression compared to older versions of GitLab please do not open an issue but a feature request. When submitting an issue please conform to the issue submission guidelines listed below. Not all issues will be addressed and your issue is more likely to be addressed if you submit a merge request which partially or fully addresses the issue. @@ -37,7 +37,7 @@ Please send a merge request with a tested solution or a merge request with a fai **[Search the issues](https://gitlab.com/gitlab-org/gitlab-ce/issues)** for similar entries before submitting your own, there's a good chance somebody else had the same issue. Show your support with `:+1:` and/or join the discussion. Please submit issues in the following format (as the first post): 1. **Summary:** Summarize your issue in one sentence (what goes wrong, what did you expect to happen) -1. **Steps to reproduce:** How can we reproduce the issue, preferably on the [GitLab development virtual machine with vagrant](https://gitlab.com/gitlab-org/cookbook-gitlab/blob/master/doc/development.md) (start your issue with: `vagrant destroy && vagrant up && vagrant ssh`) +1. **Steps to reproduce:** How can we reproduce the issue 1. **Expected behavior:** Describe your issue in detail 1. **Observed behavior** 1. **Relevant logs and/or screenshots:** Please use code blocks (\`\`\`) to format console output, logs, and code as it's very hard to read otherwise. @@ -54,6 +54,8 @@ We welcome merge requests with fixes and improvements to GitLab code, tests, and Merge requests can be filed either at [gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests) or [github.com](https://github.com/gitlabhq/gitlabhq/pulls). +If you are new to GitLab development (or web development in general), search for the label `easyfix` ([gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=easyfix), [github](https://github.com/gitlabhq/gitlabhq/labels/easyfix)). Those are issues easy to fix, marked by the GitLab core-team. If you are unsure how to proceed but want to help, mention one of the core-team members to give you a hint. + ### Merge request guidelines If you can, please submit a merge request with the fix or improvements including tests. If you don't know how to fix the issue but can write a test that exposes the issue we will accept that as well. In general bug fixes that include a regression test are merged quickly while new features without proper tests are least likely to receive timely feedback. The workflow to make a merge request is as follows: @@ -73,25 +75,45 @@ If you can, please submit a merge request with the fix or improvements including 1. Link relevant [issues](https://gitlab.com/gitlab-org/gitlab-ce/issues) and/or [feature requests](http://feedback.gitlab.com/) from the merge request description and leave a comment on them with a link back to the MR 1. Be prepared to answer questions and incorporate feedback even if requests for this arrive weeks or months after your MR submission 1. If your MR touches code that executes shell commands, make sure it adheres to the [shell command guidelines]( doc/development/shell_commands.md). +1. Also have a look at the [shell command guidelines](doc/development/shell_commands.md) if your code reads or opens files, or handles paths to files on disk. The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. The best time to submit a MR and get feedback fast. Before this time the GitLab B.V. team is still dealing with work that is created by the monthly release such as assisting subscribers with upgrade issues, the release of Enterprise Edition and the upgrade of GitLab Cloud. After the 7th it is already getting closer to the release date of the next version. This means there is less time to fix the issues created by merging large new features. Please keep the change in a single MR **as small as possible**. If you want to contribute a large feature think very hard what the minimum viable change is. Can you split functionality? Can you only submit the backend/API code? Can you start with a very simple UI? Can you do part of the refactor? The increased reviewability of small MR's that leads to higher code quality is more important to us than having a minimal commit log. The smaller a MR is the more likely it is it will be merged (quickly), after that you can send more MR's to enhance it. -For examples of feedback on merge requests please look at already [closed merge requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed). If you would like quick feedback on your merge request feel free to mention one of the Merge Marshalls of [the core-team](https://about.gitlab.com/core-team/). Please ensure that your merge request meets the following contribution acceptance criteria. +For examples of feedback on merge requests please look at already [closed merge requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed). If you would like quick feedback on your merge request feel free to mention one of the Merge Marshalls of [the core-team](https://about.gitlab.com/core-team/). Please ensure that your merge request meets the contribution acceptance criteria. + +## Definition of done + +If you contribute to GitLab please know that changes involve more than just code. +We have the following [definition of done](http://guide.agilealliance.org/guide/definition-of-done.html). +Please ensure you support the feature you contribute through all of these steps. -**Please format your merge request description as follows:** +1. Description explaning the relevancy (see following item) +1. Working and clean code that is commented where needed +1. Unit and integration tests that pass on the CI server +1. Documented in the /doc directory +1. Changelog entry added +1. Reviewed and any concerns are addressed +1. Merged by the project lead +1. Added to the release blog article +1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/) if relevant +1. Community questions answered +1. Answers to questions radiated (in docs/wiki/etc.) + +## Merge request description format 1. What does this MR do? 1. Are there points in the code the reviewer needs to double check? 1. Why was this MR needed? 1. What are the relevant issue numbers / [Feature requests](http://feedback.gitlab.com/)? -1. Screenshots (If appropriate) +1. Screenshots (if relevant) ## Contribution acceptance criteria 1. The change is as small as possible (see the above paragraph for details) 1. Include proper tests and make all tests pass (unless it contains a test exposing a bug in existing code) +1. All tests have to pass, if you suspect a failing CI build is unrelated to your contribution ask for tests to be restarted. See [the CI setup document](http://doc.gitlab.com/ce/development/ci_setup.html) on who you can ask for test restart. 1. Initially contains a single commit (please use `git rebase -i` to squash commits) 1. Can merge without problems (if not please merge `master`, never rebase commits pushed to the remote server) 1. Does not break any existing functionality @@ -100,7 +122,11 @@ For examples of feedback on merge requests please look at already [closed merge 1. Contains functionality we think other users will benefit from too 1. Doesn't add configuration options since they complicate future changes 1. Changes after submitting the merge request should be in separate commits (no squashing). You will be asked to squash when the review is over, before merging. -1. It conforms to the following style guides +1. It conforms to the following style guides. + If your change touches a line that does not follow the style, + modify the entire line to follow it. This prevents linting tools from generating warnings. + Don't touch neighbouring lines. As an exception, automatic mass refactoring modifications + may leave style non-compliant. ## Style guides @@ -116,3 +142,17 @@ For examples of feedback on merge requests please look at already [closed merge 1. [Markdown](http://www.cirosantilli.com/markdown-styleguide) 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. + +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 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/) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 38f77a65b30..197c4d5c2d7 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.0.1 +2.4.0 @@ -28,16 +28,17 @@ gem 'omniauth-google-oauth2' gem 'omniauth-twitter' gem 'omniauth-github' gem 'omniauth-shibboleth' +gem 'omniauth-kerberos' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '7.0.0.rc9' +gem "gitlab_git", '7.0.0.rc12' # Ruby/Rack Git Smart-HTTP Server Handler gem 'gitlab-grack', '~> 2.0.0.pre', require: 'grack' # LDAP Auth -gem 'gitlab_omniauth-ldap', '1.1.0', require: "omniauth-ldap" +gem 'gitlab_omniauth-ldap', '1.2.0', require: "omniauth-ldap" # Git Wiki gem 'gollum-lib', '~> 3.0.0' @@ -112,7 +113,7 @@ gem "acts-as-taggable-on" # Background jobs gem 'slim' gem 'sinatra', require: nil -gem 'sidekiq', '2.17.0' +gem 'sidekiq', '2.17.8' # HTTP requests gem "httparty" @@ -134,7 +135,7 @@ gem "redis-rails" gem 'tinder', '~> 1.9.2' # HipChat integration -gem "hipchat", "~> 0.14.0" +gem "hipchat", "~> 1.4.0" # Flowdock integration gem "gitlab-flowdock-git-hook", "~> 0.4.2" @@ -143,7 +144,7 @@ gem "gitlab-flowdock-git-hook", "~> 0.4.2" gem "gemnasium-gitlab-service", "~> 0.2" # Slack integration -gem "slack-notifier", "~> 0.3.2" +gem "slack-notifier", "~> 1.0.0" # d3 gem "d3_rails", "~> 3.1.4" @@ -186,6 +187,7 @@ gem "gon", '~> 5.0.0' gem 'nprogress-rails' gem 'request_store' gem "virtus" +gem 'addressable' group :development do gem "annotate", "~> 2.6.0.beta2" diff --git a/Gemfile.lock b/Gemfile.lock index babb23ed606..a93935ff5cb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,7 +78,7 @@ GEM coffee-script-source (1.6.3) colored (1.2) colorize (0.5.8) - connection_pool (1.2.0) + connection_pool (2.1.0) coveralls (0.7.0) multi_json (~> 1.3) rest-client @@ -168,7 +168,7 @@ GEM multi_json gitlab-grack (2.0.0.pre) rack (~> 1.5.1) - gitlab-grit (2.6.11) + gitlab-grit (2.6.12) charlock_holmes (~> 0.6) diff-lcs (~> 1.1) mime-types (~> 1.15) @@ -179,17 +179,17 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.0.1.1) emoji (~> 1.0.1) - gitlab_git (7.0.0.rc9) + gitlab_git (7.0.0.rc12) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) - rugged (~> 0.21.0) + rugged (~> 0.21.2) gitlab_meta (7.0) - gitlab_omniauth-ldap (1.1.0) - net-ldap (~> 0.7.0) + gitlab_omniauth-ldap (1.2.0) + net-ldap (~> 0.9) omniauth (~> 1.0) pyu-ruby-sasl (~> 0.0.3.1) - rubyntlm (~> 0.1.1) + rubyntlm (~> 0.3) gollum-lib (3.0.0) github-markup (~> 1.1.0) gitlab-grit (~> 2.6.5) @@ -235,15 +235,16 @@ GEM railties (>= 4.0.1) hashie (2.1.2) hike (1.2.3) - hipchat (0.14.0) - httparty + hipchat (1.4.0) httparty html-pipeline (1.11.0) activesupport (>= 2) nokogiri (~> 1.4) - html-pipeline-gitlab (0.1.0) - gitlab_emoji (~> 0.0.1.1) + html-pipeline-gitlab (0.1.5) + actionpack (~> 4) + gitlab_emoji (~> 0.0.1) html-pipeline (~> 1.11.0) + sanitize (~> 2.1) http_parser.rb (0.5.3) httparty (0.13.0) json (~> 1.8) @@ -279,7 +280,7 @@ GEM addressable (~> 2.3) letter_opener (1.1.2) launchy (~> 2.2) - libv8 (3.16.14.3) + libv8 (3.16.14.7) listen (2.3.1) celluloid (>= 0.15.2) rb-fsevent (>= 0.9.3) @@ -297,7 +298,7 @@ GEM multi_xml (0.5.5) multipart-post (1.2.0) mysql2 (0.3.16) - net-ldap (0.7.0) + net-ldap (0.9.0) net-scp (1.1.2) net-ssh (>= 2.6.5) net-ssh (2.8.0) @@ -321,6 +322,11 @@ GEM omniauth-google-oauth2 (0.2.5) omniauth (> 1.0) omniauth-oauth2 (~> 1.1) + omniauth-kerberos (0.2.0) + omniauth-multipassword + timfel-krb5-auth (~> 0.8) + omniauth-multipassword (0.4.1) + omniauth (~> 1.0) omniauth-oauth (1.0.1) oauth omniauth (~> 1.0) @@ -401,7 +407,7 @@ GEM rdoc (3.12.2) json (~> 1.4) redcarpet (3.1.2) - redis (3.0.6) + redis (3.1.0) redis-actionpack (4.0.0) actionpack (~> 4) redis-rack (~> 1.5.0) @@ -409,8 +415,8 @@ GEM redis-activesupport (4.0.0) activesupport (~> 4) redis-store (~> 1.1.0) - redis-namespace (1.4.1) - redis (~> 3.0.4) + redis-namespace (1.5.1) + redis (~> 3.0, >= 3.0.4) redis-rack (1.5.0) rack (~> 1.5) redis-store (~> 1.1.0) @@ -443,9 +449,9 @@ GEM rspec-expectations (~> 2.14.0) rspec-mocks (~> 2.14.0) ruby-progressbar (1.2.0) - rubyntlm (0.1.1) + rubyntlm (0.4.0) rubypants (0.2.0) - rugged (0.21.0) + rugged (0.21.2) safe_yaml (0.9.7) sanitize (2.1.0) nokogiri (>= 1.4.4) @@ -469,12 +475,12 @@ GEM sexp_processor (4.4.0) shoulda-matchers (2.1.0) activesupport (>= 3.0.0) - sidekiq (2.17.0) - celluloid (>= 0.15.2) - connection_pool (>= 1.0.0) + sidekiq (2.17.8) + celluloid (= 0.15.2) + connection_pool (~> 2.0) json - redis (>= 3.0.4) - redis-namespace (>= 1.3.1) + redis (~> 3.1) + redis-namespace (~> 1.3) simple_oauth (0.1.9) simplecov (0.9.0) docile (~> 1.1.0) @@ -486,7 +492,7 @@ GEM rack-protection (~> 1.4) tilt (~> 1.3, >= 1.3.4) six (0.2.0) - slack-notifier (0.3.2) + slack-notifier (1.0.0) slim (2.0.2) temple (~> 0.6.6) tilt (>= 1.3.3, < 2.1) @@ -530,6 +536,7 @@ GEM thread_safe (0.3.4) tilt (1.4.1) timers (1.1.0) + timfel-krb5-auth (0.8) tinder (1.9.3) eventmachine (~> 1.0) faraday (~> 0.8) @@ -590,6 +597,7 @@ DEPENDENCIES RedCloth ace-rails-ap acts-as-taggable-on + addressable annotate (~> 2.6.0.beta2) asciidoctor (= 0.1.4) awesome_print @@ -622,9 +630,9 @@ DEPENDENCIES gitlab-grack (~> 2.0.0.pre) gitlab-linguist (~> 3.0.0) gitlab_emoji (~> 0.0.1.1) - gitlab_git (= 7.0.0.rc9) + gitlab_git (= 7.0.0.rc12) gitlab_meta (= 7.0) - gitlab_omniauth-ldap (= 1.1.0) + gitlab_omniauth-ldap (= 1.2.0) gollum-lib (~> 3.0.0) gon (~> 5.0.0) grape (~> 0.6.1) @@ -633,7 +641,7 @@ DEPENDENCIES guard-rspec guard-spinach haml-rails - hipchat (~> 0.14.0) + hipchat (~> 1.4.0) html-pipeline-gitlab (~> 0.1.0) httparty jasmine (= 2.0.2) @@ -653,6 +661,7 @@ DEPENDENCIES omniauth (~> 1.1.3) omniauth-github omniauth-google-oauth2 + omniauth-kerberos omniauth-shibboleth omniauth-twitter org-ruby (= 0.9.9) @@ -682,11 +691,11 @@ DEPENDENCIES semantic-ui-sass (~> 0.16.1.0) settingslogic shoulda-matchers (~> 2.1.0) - sidekiq (= 2.17.0) + sidekiq (= 2.17.8) simplecov sinatra six - slack-notifier (~> 0.3.2) + slack-notifier (~> 1.0.0) slim spinach-rails spring (= 1.1.3) diff --git a/PROCESS.md b/PROCESS.md index c3a787662f7..5cc25de05a4 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -18,7 +18,7 @@ Below we describe the contributing process to GitLab for two reasons. So that co - Responds to merge requests the issue team mentions them in and monitors for new merge requests - Provides feedback to the merge request submitter to improve the merge request (style, tests, etc.) - Mark merge requests 'ready-for-merge' when they meet the contribution guidelines -- Mention developer(s) based on the [list of members and their specialities](https://www.gitlab.com/core-team/) +- Mention developer(s) based on the [list of members and their specialities](https://about.gitlab.com/core-team/) - Closes merge requests with no feedback from the reporter for two weeks ## Priorities of the issue team @@ -30,7 +30,7 @@ Below we describe the contributing process to GitLab for two reasons. So that co ## Mentioning people -The most important thing is making sure valid issues receive feedback from the development team. Therefore the priority is mentioning developers that can help on those issue. Please select someone with relevant experience from [GitLab core team](https://www.gitlab.com/core-team/). If there is nobody mentioned with that expertise look in the commit history for the affected files to find someone. Avoid mentioning the lead developer, this is the person that is least likely to give a timely response. If the involvement of the lead developer is needed the other core team members will mention this person. +The most important thing is making sure valid issues receive feedback from the development team. Therefore the priority is mentioning developers that can help on those issue. Please select someone with relevant experience from [GitLab core team](https://about.gitlab.com/core-team/). If there is nobody mentioned with that expertise look in the commit history for the affected files to find someone. Avoid mentioning the lead developer, this is the person that is least likely to give a timely response. If the involvement of the lead developer is needed the other core team members will mention this person. ## Workflow labels @@ -79,7 +79,7 @@ Thanks for the issue report but we only support issues for the latest stable ver ### Support requests and configuration questions -Thanks for your interest in GitLab. We don't use the issue tracker for support requests and configuration questions. Please use the \[support forum\]\(https://groups.google.com/forum/#!forum/gitlabhq), \[Stack Overflow\]\(http://stackoverflow.com/questions/tagged/gitlab), the #gitlab IRC channel on Freenode or the http://www.gitlab.com paid services for this purpose. Have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) for more information. +Thanks for your interest in GitLab. We don't use the issue tracker for support requests and configuration questions. Please use the \[support forum\]\(https://groups.google.com/forum/#!forum/gitlabhq), \[Stack Overflow\]\(http://stackoverflow.com/questions/tagged/gitlab), the #gitlab IRC channel on Freenode or the http://about.gitlab.com paid services for this purpose. Have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) for more information. ### Code format @@ -104,3 +104,10 @@ This merge request has been closed because a request for more information has no ### Accepting merge requests Is there a request on [the feature request forum](http://feedback.gitlab.com/forums/176466-general) that is similar to this? If so, can you make a comment with a link to it? Please be aware that new functionality that is not marked [accepting merge/pull requests](http://feedback.gitlab.com/forums/176466-general/status/796455) on the forum might not make it into GitLab. You might be asked to make changes and even after implementing them your feature might still be declined. If you want to reduce the chance of this happening please have a discussion in the forum first. + +### Only accepting merge requests with green tests + +We can only accept a merge request if all the tests are green. I've just +restarted the build. When the tests are still not passing after this restart and +you're sure that is does not have anything to do with your code changes, please +rebase with master to see if that solves the issue. diff --git a/README.md b/README.md index c0461543f2a..afcaaf0f0fa 100644 --- a/README.md +++ b/README.md @@ -51,76 +51,31 @@ On [about.gitlab.com](https://about.gitlab.com/) you can find more information a ## Installation Please see [the installation page on the GitLab website](https://about.gitlab.com/installation/) for the various options. -Since a manual installation is a lot of work and error prone we strongly recommend fast and reliable Omnibus package installation (deb/rpm) on that page. +Since a manual installation is a lot of work and error prone we strongly recommend the fast and reliable [Omnibus package installation](https://about.gitlab.com/downloads/) (deb/rpm). +You can access new installation with the login `root` and password `5iveL!fe`, after login you are required to set a unique password. ## Third-party applications -Access GitLab from multiple platforms with applications below. -These applications are maintained by contributors, GitLab B.V. does not offer support for them. +There are a lot of applications and API wrappers for GitLab. +Find them [on our website](https://about.gitlab.com/applications/). -- [iPhone app](http://gitlabcontrol.com/) -- [Android app](https://play.google.com/store/apps/details?id=com.bd.gitlab&hl=en) -- [Chrome app](https://chrome.google.com/webstore/detail/chrome-gitlab-notifier/eageapgbnjicdjjihgclpclilenjbobi) -- [Command line client](https://github.com/drewblessing/gitlab-cli) -- [Ruby API wrapper](https://github.com/NARKOZ/gitlab) - -### New versions +## New versions Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases come out when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). For more information about the release process see the release [documentation](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/release). Features that will likely be in the next releases can be found on the [feature request forum](http://feedback.gitlab.com/forums/176466-general) with the status [started](http://feedback.gitlab.com/forums/176466-general/status/796456) and [completed](http://feedback.gitlab.com/forums/176466-general/status/796457). -### Upgrading +## Upgrading For updating the the Omnibus installation please see the [update documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/update.md). For manual installations there is an [upgrader script](doc/update/upgrader.md) and there are [upgrade guides](doc/update). -## Run in production mode - -The Installation guide contains instructions on how to download an init script and run it automatically on boot. You can also start the init script manually: - - sudo service gitlab start - -or by directly calling the script: - - sudo /etc/init.d/gitlab start - -Please login with `root` / `5iveL!fe` - ## Install a development environment We recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit). -If you do not use the development kit you might need to copy the example development unicorn configuration file +If you do not use the GitLab Development Development kit you need to install and setup all the dependencies yourself, this is a lot of work and error prone. +One small thing you also have to do when installing it yourself is to copy the example development unicorn configuration file: cp config/unicorn.rb.example.development config/unicorn.rb -## Run in development mode - -Start it with [Foreman](https://github.com/ddollar/foreman) - - bundle exec foreman start -p 3000 - -or start each component separately: - - bundle exec rails s - bin/background_jobs start - -And surf to [localhost:3000](http://localhost:3000/) and login with `root` / `5iveL!fe`. - -## Run the tests - -- Run all tests: - - bundle exec rake test - -- [RSpec](http://rspec.info/) unit and functional tests. - - All RSpec tests: `bundle exec rake spec` - - Single RSpec file: `bundle exec rspec spec/controllers/commit_controller_spec.rb` - -- [Spinach](https://github.com/codegram/spinach) integration tests. - - All Spinach tests: `bundle exec rake spinach` - - Single Spinach test: `bundle exec spinach features/project/issues/milestones.feature` +Instructions on how to start Gitlab and how to run the tests can be found in the [development section of the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit#development). ## Documentation @@ -137,4 +92,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/gitlabhq/favorites) seem to like it. +[These people](https://twitter.com/gitlab/favorites) seem to like it. @@ -1 +1 @@ -7.4.0-pre +7.6.0.pre diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee index fdefbfb92bd..4f76d8ce486 100644 --- a/app/assets/javascripts/activities.js.coffee +++ b/app/assets/javascripts/activities.js.coffee @@ -1,4 +1,4 @@ -class Activities +class @Activities constructor: -> Pager.init 20, true $(".event_filter_link").bind "click", (event) => @@ -27,5 +27,3 @@ class Activities event_filters.splice index, 1 $.cookie "event_filter", event_filters.join(","), { path: '/' } - -@Activities = Activities diff --git a/app/assets/javascripts/admin.js.coffee b/app/assets/javascripts/admin.js.coffee index a333eed87f2..bcb2e6df7c0 100644 --- a/app/assets/javascripts/admin.js.coffee +++ b/app/assets/javascripts/admin.js.coffee @@ -1,4 +1,4 @@ -class Admin +class @Admin constructor: -> $('input#user_force_random_password').on 'change', (elem) -> elems = $('#user_password, #user_password_confirmation') @@ -51,5 +51,3 @@ class Admin $('li.group_member').bind 'ajax:success', -> Turbolinks.visit(location.href) - -@Admin = Admin diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index a1a4dc8e24f..4cda8b75d8e 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -18,6 +18,7 @@ #= require jquery.turbolinks #= require turbolinks #= require bootstrap +#= require password_strength #= require select2 #= require raphael #= require g.raphael-min @@ -50,12 +51,6 @@ window.ajaxGet = (url) -> window.showAndHide = (selector) -> -window.errorMessage = (message) -> - ehtml = $("<p>") - ehtml.addClass("error_message") - ehtml.html(message) - ehtml - window.split = (val) -> return val.split( /,\s*/ ) @@ -63,7 +58,7 @@ window.extractLast = (term) -> return split( term ).pop() window.rstrip = (val) -> - return val.replace(/\s+$/, '') + return if val then val.replace(/\s+$/, '') else val # Disable button if text field is empty window.disableButtonIfEmptyField = (field_selector, button_selector) -> @@ -177,6 +172,13 @@ $ -> $(@).closest(".diff-file").find(".notes_holder").toggle() e.preventDefault() + $(document).on "click", '.js-confirm-danger', (e) -> + e.preventDefault() + btn = $(e.target) + text = btn.data("confirm-danger-message") + form = btn.closest("form") + new ConfirmDangerModal(form, text) + (($) -> # Disable an element and add the 'disabled' Bootstrap class $.fn.extend disable: -> diff --git a/app/assets/javascripts/behaviors/taskable.js.coffee b/app/assets/javascripts/behaviors/taskable.js.coffee new file mode 100644 index 00000000000..ddce71c1886 --- /dev/null +++ b/app/assets/javascripts/behaviors/taskable.js.coffee @@ -0,0 +1,21 @@ +window.updateTaskState = (taskableType) -> + objType = taskableType.data + isChecked = $(this).prop("checked") + if $(this).is(":checked") + stateEvent = "task_check" + else + stateEvent = "task_uncheck" + + taskableUrl = $("form.edit-" + objType).first().attr("action") + taskableNum = taskableUrl.match(/\d+$/) + taskNum = 0 + $("li.task-list-item input:checkbox").each( (index, e) => + if e == this + taskNum = index + 1 + ) + + $.ajax + type: "PATCH" + url: taskableUrl + data: objType + "[state_event]=" + stateEvent + + "&" + objType + "[task_num]=" + taskNum diff --git a/app/assets/javascripts/blob.js.coffee b/app/assets/javascripts/blob.js.coffee index 9db919e5a62..a5f15f80c5c 100644 --- a/app/assets/javascripts/blob.js.coffee +++ b/app/assets/javascripts/blob.js.coffee @@ -1,4 +1,4 @@ -class BlobView +class @BlobView constructor: -> # handle multi-line select handleMultiSelect = (e) -> @@ -71,6 +71,3 @@ class BlobView # Highlight the correct lines when the hash part of the URL changes $(window).on("hashchange", highlightBlobLines) - - -@BlobView = BlobView diff --git a/app/assets/javascripts/commit.js.coffee b/app/assets/javascripts/commit.js.coffee index 5f53439ca4b..0566e239191 100644 --- a/app/assets/javascripts/commit.js.coffee +++ b/app/assets/javascripts/commit.js.coffee @@ -1,6 +1,4 @@ -class Commit +class @Commit constructor: -> $('.files .diff-file').each -> new CommitFile(this) - -@Commit = Commit diff --git a/app/assets/javascripts/commit/file.js.coffee b/app/assets/javascripts/commit/file.js.coffee index 4db9116a9de..83e793863b6 100644 --- a/app/assets/javascripts/commit/file.js.coffee +++ b/app/assets/javascripts/commit/file.js.coffee @@ -1,7 +1,5 @@ -class CommitFile +class @CommitFile constructor: (file) -> if $('.image', file).length new ImageFile(file) - -@CommitFile = CommitFile diff --git a/app/assets/javascripts/commit/image-file.js.coffee b/app/assets/javascripts/commit/image-file.js.coffee index 607b85eb45c..9e5f49b1f69 100644 --- a/app/assets/javascripts/commit/image-file.js.coffee +++ b/app/assets/javascripts/commit/image-file.js.coffee @@ -1,4 +1,4 @@ -class ImageFile +class @ImageFile # Width where images must fits in, for 2-up this gets divided by 2 @availWidth = 900 @@ -124,5 +124,3 @@ class ImageFile else img.on 'load', => callback.call(this, domImg.naturalWidth, domImg.naturalHeight) - -@ImageFile = ImageFile diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee index 784d7d20bb1..c183e78e513 100644 --- a/app/assets/javascripts/commits.js.coffee +++ b/app/assets/javascripts/commits.js.coffee @@ -1,4 +1,4 @@ -class CommitsList +class @CommitsList @data = ref: null limit: 0 @@ -53,5 +53,3 @@ class CommitsList @disable callback: => this.getOld() - -this.CommitsList = CommitsList diff --git a/app/assets/javascripts/confirm_danger_modal.js.coffee b/app/assets/javascripts/confirm_danger_modal.js.coffee new file mode 100644 index 00000000000..bb99edbd09e --- /dev/null +++ b/app/assets/javascripts/confirm_danger_modal.js.coffee @@ -0,0 +1,18 @@ +class @ConfirmDangerModal + constructor: (form, text) -> + @form = form + $('.js-confirm-text').text(text || '') + $('.js-confirm-danger-input').val('') + $('#modal-confirm-danger').modal('show') + project_path = $('.js-confirm-danger-match').text() + submit = $('.js-confirm-danger-submit') + submit.disable() + + $('.js-confirm-danger-input').on 'input', -> + if rstrip($(@).val()) is project_path + submit.enable() + else + submit.disable() + + $('.js-confirm-danger-submit').on 'click', => + @form.submit() diff --git a/app/assets/javascripts/dashboard.js.coffee b/app/assets/javascripts/dashboard.js.coffee index c4a0ccd9c2a..6ef5a539b8f 100644 --- a/app/assets/javascripts/dashboard.js.coffee +++ b/app/assets/javascripts/dashboard.js.coffee @@ -1,4 +1,4 @@ -class Dashboard +class @Dashboard constructor: -> @initSidebarTab() @@ -28,6 +28,3 @@ class Dashboard # show tab from cookie sidebar_filter = $.cookie(key) $("#" + sidebar_filter).tab('show') if sidebar_filter - - -@Dashboard = Dashboard diff --git a/app/assets/javascripts/diff.js.coffee b/app/assets/javascripts/diff.js.coffee index dbe00c487dc..52b4208524f 100644 --- a/app/assets/javascripts/diff.js.coffee +++ b/app/assets/javascripts/diff.js.coffee @@ -1,4 +1,4 @@ -class Diff +class @Diff UNFOLD_COUNT = 20 constructor: -> $(document).on('click', '.js-unfold', (event) => @@ -41,6 +41,3 @@ class Diff lines = line.children().slice(0, 2) line_numbers = ($(l).attr('data-linenumber') for l in lines) (parseInt(line_number) for line_number in line_numbers) - - -@Diff = Diff diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 00b52758fa8..e8b71a71945 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -58,15 +58,11 @@ class Dispatcher when 'groups:show', 'projects:show' new Activities() shortcut_handler = new ShortcutsNavigation() - when 'projects:new' - new Project() - when 'projects:edit' - new Project() - shortcut_handler = new ShortcutsNavigation() - when 'projects:teams:members:index' - new TeamMembers() when 'groups:members' new GroupMembers() + new UsersSelect() + when 'groups:new', 'groups:edit', 'admin:groups:edit' + new GroupAvatar() when 'projects:tree:show' new TreeView() shortcut_handler = new ShortcutsNavigation() @@ -79,13 +75,35 @@ class Dispatcher # Ensure we don't create a particular shortcut handler here. This is # already created, where the network graph is created. shortcut_handler = true + when 'projects:forks:new' + new ProjectFork() + when 'users:show' + new User() switch path.first() - when 'admin' then new Admin() + when 'admin' + new Admin() + switch path[1] + when 'groups' + new UsersSelect() + when 'projects' + new NamespaceSelect() when 'dashboard' shortcut_handler = new ShortcutsDashboardNavigation() + when 'profiles' + new Profile() when 'projects' + new Project() switch path[1] + when 'edit' + shortcut_handler = new ShortcutsNavigation() + new ProjectNew() + when 'new' + new ProjectNew() + when 'show' + new ProjectShow() + when 'issues', 'merge_requests' + new ProjectUsersSelect() when 'wikis' new Wikis() shortcut_handler = new ShortcutsNavigation() @@ -94,6 +112,7 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() when 'team_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' shortcut_handler = new ShortcutsNavigation() + new UsersSelect() # If we haven't installed a custom shortcut handler, install the default one diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee index cf1a37eae3e..b39ab0c4475 100644 --- a/app/assets/javascripts/flash.js.coffee +++ b/app/assets/javascripts/flash.js.coffee @@ -1,4 +1,4 @@ -class Flash +class @Flash constructor: (message, type)-> flash = $(".flash-container") flash.html("") @@ -10,5 +10,3 @@ class Flash flash.click -> $(@).fadeOut() flash.show() - -@Flash = Flash diff --git a/app/assets/javascripts/group_avatar.js.coffee b/app/assets/javascripts/group_avatar.js.coffee new file mode 100644 index 00000000000..0825fd3ce52 --- /dev/null +++ b/app/assets/javascripts/group_avatar.js.coffee @@ -0,0 +1,9 @@ +class @GroupAvatar + constructor: -> + $('.js-choose-group-avatar-button').bind "click", -> + form = $(this).closest("form") + form.find(".js-group-avatar-input").click() + $('.js-group-avatar-input').bind "change", -> + form = $(this).closest("form") + filename = $(this).val().replace(/^.*[\\\/]/, '') + form.find(".js-avatar-filename").text(filename) diff --git a/app/assets/javascripts/groups.js.coffee b/app/assets/javascripts/groups.js.coffee index 4b1000f9a6a..cc905e91ea2 100644 --- a/app/assets/javascripts/groups.js.coffee +++ b/app/assets/javascripts/groups.js.coffee @@ -1,17 +1,4 @@ -class GroupMembers +class @GroupMembers constructor: -> $('li.group_member').bind 'ajax:success', -> $(this).fadeOut() - -@GroupMembers = GroupMembers - -$ -> - # avatar - $('.js-choose-group-avatar-button').bind "click", -> - form = $(this).closest("form") - form.find(".js-group-avatar-input").click() - - $('.js-group-avatar-input').bind "change", -> - form = $(this).closest("form") - filename = $(this).val().replace(/^.*[\\\/]/, '') - form.find(".js-avatar-filename").text(filename) diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index f2b531fb2b1..597b4695a6d 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -1,4 +1,4 @@ -class Issue +class @Issue constructor: -> $('.edit-issue.inline-update input[type="submit"]').hide() $(".issue-box .inline-update").on "change", "select", -> @@ -9,25 +9,9 @@ class Issue if $("a.btn-close").length $("li.task-list-item input:checkbox").prop("disabled", false) - $(".task-list-item input:checkbox").on "click", -> - is_checked = $(this).prop("checked") - if $(this).is(":checked") - state_event = "task_check" - else - state_event = "task_uncheck" - - mr_url = $("form.edit-issue").first().attr("action") - mr_num = mr_url.match(/\d+$/) - task_num = 0 - $("li.task-list-item input:checkbox").each( (index, e) => - if e == this - task_num = index + 1 - ) - - $.ajax - type: "PATCH" - url: mr_url - data: "issue[state_event]=" + state_event + - "&issue[task_num]=" + task_num - -@Issue = Issue + $(".task-list-item input:checkbox").on( + "click" + null + "issue" + updateTaskState + ) diff --git a/app/assets/javascripts/labels.js.coffee b/app/assets/javascripts/labels.js.coffee index d306ad64f5b..1bc8840f9ac 100644 --- a/app/assets/javascripts/labels.js.coffee +++ b/app/assets/javascripts/labels.js.coffee @@ -1,4 +1,4 @@ -class Labels +class @Labels constructor: -> form = $('.label-form') @setupLabelForm(form) @@ -31,5 +31,3 @@ class Labels # Notify the form, that color has changed $('.label-form').trigger('keyup') e.preventDefault() - -@Labels = Labels diff --git a/app/assets/javascripts/markdown_area.js.coffee b/app/assets/javascripts/markdown_area.js.coffee index a0ebfc98ce6..0ca7070dc8b 100644 --- a/app/assets/javascripts/markdown_area.js.coffee +++ b/app/assets/javascripts/markdown_area.js.coffee @@ -24,6 +24,51 @@ $(document).ready -> "opacity": 0 "display": "none" + # Preview button + $(document).off "click", ".js-md-preview-button" + $(document).on "click", ".js-md-preview-button", (e) -> + ### + Shows the Markdown preview. + + Lets the server render GFM into Html and displays it. + ### + e.preventDefault() + form = $(this).closest("form") + # toggle tabs + form.find(".js-md-write-button").parent().removeClass "active" + form.find(".js-md-preview-button").parent().addClass "active" + + # toggle content + form.find(".md-write-holder").hide() + form.find(".md-preview-holder").show() + + preview = form.find(".js-md-preview") + mdText = form.find(".markdown-area").val() + if mdText.trim().length is 0 + preview.text "Nothing to preview." + else + preview.text "Loading..." + $.get($(this).data("url"), + md_text: mdText + ).success (previewData) -> + preview.html previewData + + # Write button + $(document).off "click", ".js-md-write-button" + $(document).on "click", ".js-md-write-button", (e) -> + ### + Shows the Markdown textarea. + ### + e.preventDefault() + form = $(this).closest("form") + # toggle tabs + form.find(".js-md-write-button").parent().addClass "active" + form.find(".js-md-preview-button").parent().removeClass "active" + + # toggle content + form.find(".md-write-holder").show() + form.find(".md-preview-holder").hide() + dropzone = $(".div-dropzone").dropzone( url: project_image_path_upload dictDefaultMessage: "" diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 203c721c30c..46e06424e5a 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -1,4 +1,4 @@ -class MergeRequest +class @MergeRequest constructor: (@opts) -> @initContextWidget() this.$el = $('.merge-request') @@ -17,7 +17,7 @@ class MergeRequest disableButtonIfEmptyField '#commit_message', '.accept_merge_request' - if $("a.close-mr-link").length + if $("a.btn-close").length $("li.task-list-item input:checkbox").prop("disabled", false) # Local jQuery finder @@ -74,26 +74,12 @@ class MergeRequest this.$('.remove_source_branch_in_progress').hide() this.$('.remove_source_branch_widget.failed').show() - this.$(".task-list-item input:checkbox").on "click", -> - is_checked = $(this).prop("checked") - if $(this).is(":checked") - state_event = "task_check" - else - state_event = "task_uncheck" - - mr_url = $("form.edit-merge_request").first().attr("action") - mr_num = mr_url.match(/\d+$/) - task_num = 0 - $("li.task-list-item input:checkbox").each( (index, e) => - if e == this - task_num = index + 1 - ) - - $.ajax - type: "PATCH" - url: mr_url - data: "merge_request[state_event]=" + state_event + - "&merge_request[task_num]=" + task_num + $(".task-list-item input:checkbox").on( + "click" + null + "merge_request" + updateTaskState + ) activateTab: (action) -> this.$('.merge-request-tabs li').removeClass 'active' @@ -119,14 +105,6 @@ class MergeRequest else $('.ci_widget.ci-error').show() - switch state - when "success" - $('.mr-state-widget').addClass("panel-success") - when "failed" - $('.mr-state-widget').addClass("panel-danger") - when "running", "pending" - $('.mr-state-widget').addClass("panel-warning") - showCiCoverage: (coverage) -> cov_html = $('<span>') cov_html.addClass('ci-coverage') @@ -154,5 +132,3 @@ class MergeRequest this.$('.automerge_widget').hide() this.$('.merge-in-progress').hide() this.$('.automerge_widget.already_cannot_be_merged').show() - -this.MergeRequest = MergeRequest diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee index ea01c318d4f..c42f31933d3 100644 --- a/app/assets/javascripts/milestone.js.coffee +++ b/app/assets/javascripts/milestone.js.coffee @@ -1,4 +1,4 @@ -class Milestone +class @Milestone @updateIssue: (li, issue_url, data) -> $.ajax type: "PUT" @@ -115,5 +115,3 @@ class Milestone Milestone.updateMergeRequest(ui.item, merge_request_url, data) ).disableSelection() - -@Milestone = Milestone diff --git a/app/assets/javascripts/namespace_select.js.coffee b/app/assets/javascripts/namespace_select.js.coffee index 00d135d1449..a02c4515ccc 100644 --- a/app/assets/javascripts/namespace_select.js.coffee +++ b/app/assets/javascripts/namespace_select.js.coffee @@ -1,24 +1,25 @@ -$ -> - namespaceFormatResult = (namespace) -> - markup = "<div class='namespace-result'>" - markup += "<span class='namespace-kind'>" + namespace.kind + "</span>" - markup += "<span class='namespace-path'>" + namespace.path + "</span>" - markup += "</div>" - markup +class @NamespaceSelect + constructor: -> + namespaceFormatResult = (namespace) -> + markup = "<div class='namespace-result'>" + markup += "<span class='namespace-kind'>" + namespace.kind + "</span>" + markup += "<span class='namespace-path'>" + namespace.path + "</span>" + markup += "</div>" + markup - formatSelection = (namespace) -> - namespace.kind + ": " + namespace.path + formatSelection = (namespace) -> + namespace.kind + ": " + namespace.path - $('.ajax-namespace-select').each (i, select) -> - $(select).select2 - placeholder: "Search for namespace" - multiple: $(select).hasClass('multiselect') - minimumInputLength: 0 - query: (query) -> - Api.namespaces query.term, (namespaces) -> - data = { results: namespaces } - query.callback(data) + $('.ajax-namespace-select').each (i, select) -> + $(select).select2 + placeholder: "Search for namespace" + multiple: $(select).hasClass('multiselect') + minimumInputLength: 0 + query: (query) -> + Api.namespaces query.term, (namespaces) -> + data = { results: namespaces } + query.callback(data) - dropdownCssClass: "ajax-namespace-dropdown" - formatResult: namespaceFormatResult - formatSelection: formatSelection + dropdownCssClass: "ajax-namespace-dropdown" + formatResult: namespaceFormatResult + formatSelection: formatSelection diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index ba8d7a9a2f5..30f8530dfda 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -1,4 +1,4 @@ -class Notes +class @Notes @interval: null constructor: (notes_url, note_ids, last_fetched_at) -> @@ -36,12 +36,6 @@ class Notes # delete note attachment $(document).on "click", ".js-note-attachment-delete", @removeAttachment - # Preview button - $(document).on "click", ".js-note-preview-button", @previewNote - - # Preview button - $(document).on "click", ".js-note-write-button", @writeNote - # reset main target form after submit $(document).on "ajax:complete", ".js-main-target-form", @resetMainTargetForm @@ -77,8 +71,6 @@ class Notes $(document).off "click", ".note-edit-cancel" $(document).off "click", ".js-note-delete" $(document).off "click", ".js-note-attachment-delete" - $(document).off "click", ".js-note-preview-button" - $(document).off "click", ".js-note-write-button" $(document).off "ajax:complete", ".js-main-target-form" $(document).off "click", ".js-choose-note-attachment-button" $(document).off "click", ".js-discussion-reply-button" @@ -166,47 +158,6 @@ class Notes @removeDiscussionNoteForm(form) ### - Shows write note textarea. - ### - writeNote: (e) -> - e.preventDefault() - form = $(this).closest("form") - # toggle tabs - form.find(".js-note-write-button").parent().addClass "active" - form.find(".js-note-preview-button").parent().removeClass "active" - - # toggle content - form.find(".note-write-holder").show() - form.find(".note-preview-holder").hide() - - ### - Shows the note preview. - - Lets the server render GFM into Html and displays it. - ### - previewNote: (e) -> - e.preventDefault() - form = $(this).closest("form") - # toggle tabs - form.find(".js-note-write-button").parent().removeClass "active" - form.find(".js-note-preview-button").parent().addClass "active" - - # toggle content - form.find(".note-write-holder").hide() - form.find(".note-preview-holder").show() - - preview = form.find(".js-note-preview") - noteText = form.find(".js-note-text").val() - if noteText.trim().length is 0 - preview.text "Nothing to preview." - else - preview.text "Loading..." - $.post($(this).data("url"), - note: noteText - ).success (previewData) -> - preview.html previewData - - ### Called in response the main target form has been successfully submitted. Removes any errors. @@ -220,7 +171,7 @@ class Notes form.find(".js-errors").remove() # reset text and preview - form.find(".js-note-write-button").click() + form.find(".js-md-write-button").click() form.find(".js-note-text").val("").trigger "input" ### @@ -270,8 +221,8 @@ class Notes form.removeClass "js-new-note-form" # setup preview buttons - form.find(".js-note-write-button, .js-note-preview-button").tooltip placement: "left" - previewButton = form.find(".js-note-preview-button") + form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left" + previewButton = form.find(".js-md-preview-button") form.find(".js-note-text").on "input", -> if $(this).val().trim() isnt "" previewButton.removeClass("turn-off").addClass "turn-on" @@ -514,7 +465,3 @@ class Notes else form.find('.js-note-target-reopen').text('Reopen') form.find('.js-note-target-close').text('Close') - - - -@Notes = Notes diff --git a/app/assets/javascripts/notes_votes.js.coffee b/app/assets/javascripts/notes_votes.js.coffee index b31eb9ac9de..65c149b7886 100644 --- a/app/assets/javascripts/notes_votes.js.coffee +++ b/app/assets/javascripts/notes_votes.js.coffee @@ -1,4 +1,4 @@ -class NotesVotes +class @NotesVotes updateVotes: -> votes = $("#votes .votes") notes = $("#notes-list .note .vote") @@ -18,5 +18,3 @@ class NotesVotes # replace vote numbers votes.find(".upvotes").text votes.find(".upvotes").text().replace(/\d+/, upvotes) votes.find(".downvotes").text votes.find(".downvotes").text().replace(/\d+/, downvotes) - -@NotesVotes = NotesVotes diff --git a/app/assets/javascripts/password_strength.js.coffee b/app/assets/javascripts/password_strength.js.coffee new file mode 100644 index 00000000000..825f5630266 --- /dev/null +++ b/app/assets/javascripts/password_strength.js.coffee @@ -0,0 +1,31 @@ +#= require pwstrength-bootstrap-1.2.2 +overwritten_messages = + wordSimilarToUsername: "Your password should not contain your username" + +overwritten_rules = + wordSequences: false + +options = + showProgressBar: false + showVerdicts: false + showPopover: true + showErrors: true + showStatus: true + errorMessages: overwritten_messages + +$(document).ready -> + profileOptions = {} + profileOptions.ui = options + profileOptions.rules = + activated: overwritten_rules + + deviseOptions = {} + deviseOptions.common = + usernameField: "#user_username" + deviseOptions.ui = options + deviseOptions.rules = + activated: overwritten_rules + + $("#user_password_profile").pwstrength profileOptions + $("#user_password_sign_up").pwstrength deviseOptions + $("#user_password_recover").pwstrength deviseOptions diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index 0e99921f899..de356fbec77 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -1,30 +1,29 @@ -$ -> - $('.edit_user .application-theme input, .edit_user .code-preview-theme input').click -> - # Submit the form - $('.edit_user').submit() +class @Profile + constructor: -> + $('.edit_user .application-theme input, .edit_user .code-preview-theme input').click -> + # Submit the form + $('.edit_user').submit() - new Flash("Appearance settings saved", "notice") + new Flash("Appearance settings saved", "notice") - $('.update-username form').on 'ajax:before', -> - $('.loading-gif').show() - $(this).find('.update-success').hide() - $(this).find('.update-failed').hide() + $('.update-username form').on 'ajax:before', -> + $('.loading-gif').show() + $(this).find('.update-success').hide() + $(this).find('.update-failed').hide() - $('.update-username form').on 'ajax:complete', -> - $(this).find('.btn-save').enableButton() - $(this).find('.loading-gif').hide() + $('.update-username form').on 'ajax:complete', -> + $(this).find('.btn-save').enableButton() + $(this).find('.loading-gif').hide() - $('.update-notifications').on 'ajax:complete', -> - $(this).find('.btn-save').enableButton() + $('.update-notifications').on 'ajax:complete', -> + $(this).find('.btn-save').enableButton() - $('.js-choose-user-avatar-button').bind "click", -> - form = $(this).closest("form") - form.find(".js-user-avatar-input").click() + $('.js-choose-user-avatar-button').bind "click", -> + form = $(this).closest("form") + form.find(".js-user-avatar-input").click() - $('.js-user-avatar-input').bind "change", -> - form = $(this).closest("form") - filename = $(this).val().replace(/^.*[\\\/]/, '') - form.find(".js-avatar-filename").text(filename) - - $('.profile-groups-avatars').tooltip("placement": "top") + $('.js-user-avatar-input').bind "change", -> + form = $(this).closest("form") + filename = $(this).val().replace(/^.*[\\\/]/, '') + form.find(".js-avatar-filename").text(filename) diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index f4a8a178e76..5a9cc66c8f0 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -1,62 +1,20 @@ -class Project +class @Project constructor: -> - $('.project-edit-container').on 'ajax:before', => - $('.project-edit-container').hide() - $('.save-project-loader').show() - - @initEvents() - - - initEvents: -> - disableButtonIfEmptyField '#project_name', '.project-submit' - - $('#project_issues_enabled').change -> - if ($(this).is(':checked') == true) - $('#project_issues_tracker').removeAttr('disabled') - else - $('#project_issues_tracker').attr('disabled', 'disabled') - - $('#project_issues_tracker').change() - - $('#project_issues_tracker').change -> - if ($(this).val() == gon.default_issues_tracker || $(this).is(':disabled')) - $('#project_issues_tracker_id').attr('disabled', 'disabled') - else - $('#project_issues_tracker_id').removeAttr('disabled') - - -@Project = Project - -$ -> - # Git clone panel switcher - scope = $ '.git-clone-holder' - if scope.length > 0 - $('a, button', scope).click -> - $('a, button', scope).removeClass 'active' - $(@).addClass 'active' - $('#project_clone', scope).val $(@).data 'clone' - $(".clone").text("").append $(@).data 'clone' - - # Ref switcher - $('.project-refs-select').on 'change', -> - $(@).parents('form').submit() - - $('.hide-no-ssh-message').on 'click', (e) -> - path = '/' - $.cookie('hide_no_ssh_message', 'false', { path: path }) - $(@).parents('.no-ssh-key-message').hide() - e.preventDefault() - - $('.project-home-panel .star').on 'ajax:success', (e, data, status, xhr) -> - $(@).toggleClass('on').find('.count').html(data.star_count) - .on 'ajax:error', (e, xhr, status, error) -> - new Flash('Star toggle failed. Try again later.', 'alert') - - $("a[data-toggle='tab']").on "shown.bs.tab", (e) -> - $.cookie "default_view", $(e.target).attr("href") - - defaultView = $.cookie("default_view") - if defaultView - $("a[href=" + defaultView + "]").tab "show" - else - $("a[data-toggle='tab']:first").tab "show" + # Git clone panel switcher + scope = $ '.git-clone-holder' + if scope.length > 0 + $('a, button', scope).click -> + $('a, button', scope).removeClass 'active' + $(@).addClass 'active' + $('#project_clone', scope).val $(@).data 'clone' + $(".clone").text("").append $(@).data 'clone' + + # Ref switcher + $('.project-refs-select').on 'change', -> + $(@).parents('form').submit() + + $('.hide-no-ssh-message').on 'click', (e) -> + path = '/' + $.cookie('hide_no_ssh_message', 'false', { path: path }) + $(@).parents('.no-ssh-key-message').hide() + e.preventDefault() diff --git a/app/assets/javascripts/project_fork.js.coffee b/app/assets/javascripts/project_fork.js.coffee new file mode 100644 index 00000000000..e15a1c4ef76 --- /dev/null +++ b/app/assets/javascripts/project_fork.js.coffee @@ -0,0 +1,5 @@ +class @ProjectFork + constructor: -> + $('.fork-thumbnail a').on 'click', -> + $('.fork-namespaces').hide() + $('.save-project-loader').show() diff --git a/app/assets/javascripts/project_import.js.coffee b/app/assets/javascripts/project_import.js.coffee index 7cf44da99fe..6633564a079 100644 --- a/app/assets/javascripts/project_import.js.coffee +++ b/app/assets/javascripts/project_import.js.coffee @@ -1,7 +1,5 @@ -class ProjectImport +class @ProjectImport constructor: -> setTimeout -> Turbolinks.visit(location.href) , 5000 - -@ProjectImport = ProjectImport diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee new file mode 100644 index 00000000000..f4a2ca813d2 --- /dev/null +++ b/app/assets/javascripts/project_new.js.coffee @@ -0,0 +1,25 @@ +class @ProjectNew + constructor: -> + $('.project-edit-container').on 'ajax:before', => + $('.project-edit-container').hide() + $('.save-project-loader').show() + + @initEvents() + + + initEvents: -> + disableButtonIfEmptyField '#project_name', '.project-submit' + + $('#project_issues_enabled').change -> + if ($(this).is(':checked') == true) + $('#project_issues_tracker').removeAttr('disabled') + else + $('#project_issues_tracker').attr('disabled', 'disabled') + + $('#project_issues_tracker').change() + + $('#project_issues_tracker').change -> + if ($(this).val() == gon.default_issues_tracker || $(this).is(':disabled')) + $('#project_issues_tracker_id').attr('disabled', 'disabled') + else + $('#project_issues_tracker_id').removeAttr('disabled') diff --git a/app/assets/javascripts/project_show.js.coffee b/app/assets/javascripts/project_show.js.coffee new file mode 100644 index 00000000000..02a7d7b731d --- /dev/null +++ b/app/assets/javascripts/project_show.js.coffee @@ -0,0 +1,15 @@ +class @ProjectShow + constructor: -> + $('.project-home-panel .star').on 'ajax:success', (e, data, status, xhr) -> + $(@).toggleClass('on').find('.count').html(data.star_count) + .on 'ajax:error', (e, xhr, status, error) -> + new Flash('Star toggle failed. Try again later.', 'alert') + + $("a[data-toggle='tab']").on "shown.bs.tab", (e) -> + $.cookie "default_view", $(e.target).attr("href") + + defaultView = $.cookie("default_view") + if defaultView + $("a[href=" + defaultView + "]").tab "show" + else + $("a[data-toggle='tab']:first").tab "show" diff --git a/app/assets/javascripts/project_users_select.js.coffee b/app/assets/javascripts/project_users_select.js.coffee index cfbcd5108c8..7fb33926096 100644 --- a/app/assets/javascripts/project_users_select.js.coffee +++ b/app/assets/javascripts/project_users_select.js.coffee @@ -1,6 +1,6 @@ -@projectUsersSelect = - init: -> - $('.ajax-project-users-select').each (i, select) -> +class @ProjectUsersSelect + constructor: -> + $('.ajax-project-users-select').each (i, select) => project_id = $(select).data('project-id') || $('body').data('project-id') $(select).select2 @@ -28,14 +28,16 @@ Api.user(id, callback) - formatResult: projectUsersSelect.projectUserFormatResult - formatSelection: projectUsersSelect.projectUserFormatSelection + formatResult: (args...) => + @formatResult(args...) + formatSelection: (args...) => + @formatSelection(args...) dropdownCssClass: "ajax-project-users-dropdown" dropdownAutoWidth: true escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results m - projectUserFormatResult: (user) -> + formatResult: (user) -> if user.avatar_url avatar = user.avatar_url else @@ -52,8 +54,5 @@ <div class='user-username'>#{user.username}</div> </div>" - projectUserFormatSelection: (user) -> + formatSelection: (user) -> user.name - -$ -> - projectUsersSelect.init() diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index e144dfa1d68..c1801365266 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -1,4 +1,4 @@ -class SearchAutocomplete +class @SearchAutocomplete constructor: (search_autocomplete_path, project_id, project_ref) -> project_id = '' unless project_id project_ref = '' unless project_ref @@ -9,5 +9,3 @@ class SearchAutocomplete minLength: 1 select: (event, ui) -> location.href = ui.item.url - -@SearchAutocomplete = SearchAutocomplete diff --git a/app/assets/javascripts/stat_graph.js.coffee b/app/assets/javascripts/stat_graph.js.coffee index b129619696f..f36c71fd25e 100644 --- a/app/assets/javascripts/stat_graph.js.coffee +++ b/app/assets/javascripts/stat_graph.js.coffee @@ -1,4 +1,4 @@ -class window.StatGraph +class @StatGraph @log: {} @get_log: -> @log diff --git a/app/assets/javascripts/stat_graph_contributors.js.coffee b/app/assets/javascripts/stat_graph_contributors.js.coffee index ab785a54543..27f0fd31d50 100644 --- a/app/assets/javascripts/stat_graph_contributors.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors.js.coffee @@ -1,4 +1,4 @@ -class window.ContributorsStatGraph +class @ContributorsStatGraph init: (log) -> @parsed_log = ContributorsStatGraphUtil.parse_log(log) @set_current_field("commits") diff --git a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee index 834c7e5dab0..9952fa0b00a 100644 --- a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee @@ -1,4 +1,4 @@ -class window.ContributorsGraph +class @ContributorsGraph MARGIN: top: 20 right: 20 @@ -44,7 +44,7 @@ class window.ContributorsGraph set_data: (data) -> @data = data -class window.ContributorsMasterGraph extends ContributorsGraph +class @ContributorsMasterGraph extends ContributorsGraph constructor: (@data) -> @width = $('.container').width() - 70 @height = 200 @@ -117,7 +117,7 @@ class window.ContributorsMasterGraph extends ContributorsGraph @svg.select("path").attr("d", @area) @svg.select(".y.axis").call(@y_axis) -class window.ContributorsAuthorGraph extends ContributorsGraph +class @ContributorsAuthorGraph extends ContributorsGraph constructor: (@data) -> @width = $('.container').width()/2 - 100 @height = 200 diff --git a/app/assets/javascripts/team_members.js.coffee b/app/assets/javascripts/team_members.js.coffee deleted file mode 100644 index 5eaa8ad4ff9..00000000000 --- a/app/assets/javascripts/team_members.js.coffee +++ /dev/null @@ -1,6 +0,0 @@ -class TeamMembers - constructor: -> - $('.team-members .project-access-select').on "change", -> - $(this.form).submit() - -@TeamMembers = TeamMembers diff --git a/app/assets/javascripts/tree.js.coffee b/app/assets/javascripts/tree.js.coffee index 4852e879b68..d428db5b422 100644 --- a/app/assets/javascripts/tree.js.coffee +++ b/app/assets/javascripts/tree.js.coffee @@ -1,4 +1,4 @@ -class TreeView +class @TreeView constructor: -> @initKeyNav() @@ -39,5 +39,3 @@ class TreeView else if e.which is 13 path = $('.tree-item.selected .tree-item-file-name a').attr('href') Turbolinks.visit(path) - -@TreeView = TreeView diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee new file mode 100644 index 00000000000..8a2e2421c2e --- /dev/null +++ b/app/assets/javascripts/user.js.coffee @@ -0,0 +1,3 @@ +class @User + constructor: -> + $('.profile-groups-avatars').tooltip("placement": "top") diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 86318bd7d94..9eee7406511 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -1,5 +1,30 @@ -$ -> - userFormatResult = (user) -> +class @UsersSelect + constructor: -> + $('.ajax-users-select').each (i, select) => + $(select).select2 + placeholder: "Search for a user" + multiple: $(select).hasClass('multiselect') + minimumInputLength: 0 + query: (query) -> + Api.users query.term, (users) -> + data = { results: users } + query.callback(data) + + initSelection: (element, callback) -> + id = $(element).val() + if id isnt "" + Api.user(id, callback) + + + formatResult: (args...) => + @formatResult(args...) + formatSelection: (args...) => + @formatSelection(args...) + dropdownCssClass: "ajax-users-dropdown" + escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results + m + + formatResult: (user) -> if user.avatar_url avatar = user.avatar_url else @@ -11,27 +36,5 @@ $ -> <div class='user-username'>#{user.username}</div> </div>" - userFormatSelection = (user) -> + formatSelection: (user) -> user.name - - $('.ajax-users-select').each (i, select) -> - $(select).select2 - placeholder: "Search for a user" - multiple: $(select).hasClass('multiselect') - minimumInputLength: 0 - query: (query) -> - Api.users query.term, (users) -> - data = { results: users } - query.callback(data) - - initSelection: (element, callback) -> - id = $(element).val() - if id isnt "" - Api.user(id, callback) - - - formatResult: userFormatResult - formatSelection: userFormatSelection - dropdownCssClass: "ajax-users-dropdown" - escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results - m diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee index 17e790e5b7c..66757565d3a 100644 --- a/app/assets/javascripts/wikis.js.coffee +++ b/app/assets/javascripts/wikis.js.coffee @@ -1,4 +1,4 @@ -class Wikis +class @Wikis constructor: -> $('.build-new-wiki').bind "click", -> field = $('#new_wiki_path') @@ -7,6 +7,3 @@ class Wikis if(slug.length > 0) location.href = path + "/" + slug - - -@Wikis = Wikis diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss index be4c4d07f1c..469f4f296ae 100644 --- a/app/assets/stylesheets/behaviors.scss +++ b/app/assets/stylesheets/behaviors.scss @@ -1,12 +1,22 @@ // Details //-------- -.js-details-container .content { display: none; } -.js-details-container .content.hide { display: block; } -.js-details-container.open .content { display: block; } -.js-details-container.open .content.hide { display: none; } +.js-details-container { + .content { + display: none; + &.hide { display: block; } + } + &.open .content { + display: block; + &.hide { display: none; } + } +} // Toggle between two states. -.js-toggler-container .turn-on { display: block; } -.js-toggler-container .turn-off { display: none; } -.js-toggler-container.on .turn-on { display: none; } -.js-toggler-container.on .turn-off { display: block; } +.js-toggler-container { + .turn-on { display: block; } + .turn-off { display: none; } + &.on { + .turn-on { display: none; } + .turn-off { display: block; } + } +} diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index cd2f4e45e3c..2fc738c18d8 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -330,10 +330,6 @@ table { } } -@media (max-width: $screen-xs-max) { - .container .content { margin-top: 20px; } -} - .wiki .highlight, .note-body .highlight { margin-bottom: 9px; } diff --git a/app/assets/stylesheets/generic/files.scss b/app/assets/stylesheets/generic/files.scss index e2b0ef0c5ea..1ed41272ac5 100644 --- a/app/assets/stylesheets/generic/files.scss +++ b/app/assets/stylesheets/generic/files.scss @@ -42,7 +42,6 @@ } .file-content { background: #fff; - font-size: 11px; &.image_file { background: #eee; @@ -54,8 +53,6 @@ } &.wiki { - font-size: 14px; - line-height: 1.6; padding: 25px; .highlight { diff --git a/app/assets/stylesheets/generic/highlight.scss b/app/assets/stylesheets/generic/highlight.scss index 4110bddf4f3..ae08539d454 100644 --- a/app/assets/stylesheets/generic/highlight.scss +++ b/app/assets/stylesheets/generic/highlight.scss @@ -59,6 +59,10 @@ pre { white-space: pre; word-wrap: normal; + + code { + font-family: $monospace_font; + } } } } diff --git a/app/assets/stylesheets/generic/issue_box.scss b/app/assets/stylesheets/generic/issue_box.scss index 0486955d6e1..79fbad4b946 100644 --- a/app/assets/stylesheets/generic/issue_box.scss +++ b/app/assets/stylesheets/generic/issue_box.scss @@ -10,8 +10,7 @@ .issue-box { color: #555; margin:20px 0; - background: #f9f9f9; - border-top-left-radius: 5px; + background: $box_bg; @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09)); &.issue-box-closed { @@ -112,7 +111,11 @@ float: left; font-weight: bold; padding: 10px 15px; - border-top-left-radius: 5px; + } + + .cross-project-ref { + float: left; + padding: 10px 15px; } .creator { diff --git a/app/assets/stylesheets/generic/markdown_area.scss b/app/assets/stylesheets/generic/markdown_area.scss index fbfa72c5e5e..4168e235cae 100644 --- a/app/assets/stylesheets/generic/markdown_area.scss +++ b/app/assets/stylesheets/generic/markdown_area.scss @@ -20,6 +20,7 @@ opacity: 0; font-size: 50px; transition: opacity 200ms ease-in-out; + pointer-events: none; } .div-dropzone-spinner { @@ -50,3 +51,28 @@ margin-bottom: 0; transition: opacity 200ms ease-in-out; } + +.md-preview-holder { + background: #FFF; + border: 1px solid #ddd; + min-height: 100px; + padding: 5px; + font-size: 14px; + box-shadow: none; +} + +.new_note, +.edit_note, +.issuable-description, +.milestone-description, +.merge-request-form { + .nav-tabs { + margin-bottom: 0; + border: none; + + li a, + li.active a { + border: 1px solid #DDD; + } + } +} diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss new file mode 100644 index 00000000000..c164b07b104 --- /dev/null +++ b/app/assets/stylesheets/generic/mobile.scss @@ -0,0 +1,17 @@ +/** Common mobile (screen XS) styles **/ +@media (max-width: $screen-xs-max) { + .container .content { + margin-top: 20px; + } + + .nav.nav-tabs > li > a { + padding: 10px; + font-size: 12px; + margin-right: 3px; + + .badge { + display: none; + } + } +} + diff --git a/app/assets/stylesheets/generic/timeline.scss b/app/assets/stylesheets/generic/timeline.scss index f29cf25fa4c..57e9e8ae5c5 100644 --- a/app/assets/stylesheets/generic/timeline.scss +++ b/app/assets/stylesheets/generic/timeline.scss @@ -75,3 +75,20 @@ } } } + +@media (max-width: $screen-xs-max) { + .timeline { + &:before { + background: none; + } + .timeline-entry .timeline-entry-inner { + .timeline-icon { + display: none; + } + + .timeline-content { + margin-left: 0; + } + } + } +} diff --git a/app/assets/stylesheets/gl_bootstrap.scss b/app/assets/stylesheets/gl_bootstrap.scss index 45044c5acb6..9c5e76ab8e2 100644 --- a/app/assets/stylesheets/gl_bootstrap.scss +++ b/app/assets/stylesheets/gl_bootstrap.scss @@ -233,8 +233,8 @@ $list-group-active-bg: $bg_primary; } .form-actions { - margin-bottom: 0; - background: #FFF; + margin: -15px; + margin-top: 18px; } } @@ -262,53 +262,33 @@ $list-group-active-bg: $bg_primary; } .panel-danger { - border-color: $border_danger; + @include panel-colored; .panel-heading { - color: #ffffff; - background-color: $bg_danger; + color: $border_danger; border-color: $border_danger; - a { - color: #FFF; - text-decoration: underline; - } } } .panel-success { - border-color: $border_success; + @include panel-colored; .panel-heading { - color: #ffffff; - background-color: $bg_success; + color: $border_success; border-color: $border_success; - a { - color: #FFF; - text-decoration: underline; - } } } .panel-primary { - border-color: $border_primary; + @include panel-colored; .panel-heading { - color: #ffffff; - background-color: $bg_primary; + color: $border_primary; border-color: $border_primary; - a { - color: #FFF; - text-decoration: underline; - } } } .panel-warning { - border-color: $border_warning; + @include panel-colored; .panel-heading { - color: #ffffff; - background-color: $bg_warning; + color: $border_warning; border-color: $border_warning; - a { - color: #FFF; - text-decoration: underline; - } } } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 36bc5df2f44..dffa2dc9ed2 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -29,28 +29,30 @@ .hljs-tag, .hljs-tag .hljs-title, - .hljs-keyword, - .hljs-literal, .hljs-strong, .hljs-change, .hljs-winutils, .hljs-flow, .lisp .hljs-title, .clojure .hljs-built_in, + .hljs-keyword, .nginx .hljs-title, .tex .hljs-special { color: #F92672; } .hljs { - color: #DDD; + color: #F8F8F2; } - .hljs .hljs-constant, - .asciidoc .hljs-code { + .asciidoc .hljs-code, + .markdown .hljs-code, + .hljs-literal, + .hljs-function .hljs-keyword { color: #66D9EF; } + .hljs-code, .hljs-class .hljs-title, .hljs-header { @@ -62,18 +64,27 @@ .hljs-symbol, .hljs-symbol .hljs-string, .hljs-value, + .hljs-constant, + .hljs-number, .hljs-regexp { - color: #BF79DB; + color: #AE81FF; + } + + .hljs-string { + color: #E6DB74; + } + + .hljs-params { + color: #fd971f; } .hljs-link_url, .hljs-tag .hljs-value, - .hljs-string, .hljs-bullet, .hljs-subst, .hljs-title, .hljs-emphasis, - .haskell .hljs-type, + .hljs-type, .hljs-preprocessor, .hljs-pragma, .ruby .hljs-class .hljs-parent, @@ -99,12 +110,12 @@ } .hljs-comment, - .java .hljs-annotation, + .hljs-annotation, .smartquote, .hljs-blockquote, .hljs-horizontal_rule, - .python .hljs-decorator, .hljs-template_comment, + .hljs-decorator, .hljs-pi, .hljs-doctype, .hljs-deletion, diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 815cf367ae8..8d5822937a0 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -186,3 +186,11 @@ } } } + +.readme-holder .wiki, .note-body, .wiki-holder { + .white { + .highlight, pre, .hljs { + background: #F9F9F9; + } + } +} diff --git a/app/assets/stylesheets/main/fonts.scss b/app/assets/stylesheets/main/fonts.scss index d90274a0db9..f945aaca848 100644 --- a/app/assets/stylesheets/main/fonts.scss +++ b/app/assets/stylesheets/main/fonts.scss @@ -1,3 +1,3 @@ /** Typo **/ -$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'Courier New', 'andale mono', 'lucida console', monospace; +$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; $regular_font: "Helvetica Neue", Helvetica, Arial, sans-serif; diff --git a/app/assets/stylesheets/main/mixins.scss b/app/assets/stylesheets/main/mixins.scss index 93faf5ced65..5f83913b73b 100644 --- a/app/assets/stylesheets/main/mixins.scss +++ b/app/assets/stylesheets/main/mixins.scss @@ -58,8 +58,8 @@ } @mixin md-typography { - font-size: 14px; - line-height: 1.6; + font-size: 15px; + line-height: 1.5; img { max-width: 100%; @@ -93,7 +93,7 @@ blockquote p { color: #888; - font-size: 14px; + font-size: 15px; line-height: 1.5; } @@ -132,3 +132,14 @@ white-space: nowrap; max-width: $max_width; } + +@mixin panel-colored { + border: none; + background: $box_bg; + @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09)); + + .panel-heading { + font-weight: bold; + background-color: $box_bg; + } +} diff --git a/app/assets/stylesheets/main/variables.scss b/app/assets/stylesheets/main/variables.scss index 72d84226fe7..c71984a5665 100644 --- a/app/assets/stylesheets/main/variables.scss +++ b/app/assets/stylesheets/main/variables.scss @@ -3,6 +3,7 @@ */ $style_color: #474D57; $hover: #FFECDB; +$box_bg: #F9F9F9; /* * Link colors diff --git a/app/assets/stylesheets/sections/events.scss b/app/assets/stylesheets/sections/events.scss index 656aa5b18a6..a766d6e77ab 100644 --- a/app/assets/stylesheets/sections/events.scss +++ b/app/assets/stylesheets/sections/events.scss @@ -47,7 +47,7 @@ .event-title { @include str-truncated(72%); color: #333; - font-weight: normal; + font-weight: 500; font-size: 14px; .author_name { color: #333; @@ -56,12 +56,9 @@ .event-body { margin-left: 35px; margin-right: 100px; + color: #777; - .event-info { - color: #666; - } .event-note { - color: #666; margin-top: 5px; .md { @@ -72,7 +69,7 @@ border: none; background: #f9f9f9; border-radius: 0; - color: #666; + color: #777; margin: 0 20px; } @@ -120,7 +117,6 @@ padding: 3px; padding-left: 0; border: none; - color: #666; .commit-row-title { font-size: 12px; } @@ -186,7 +182,24 @@ } @media (max-width: $screen-xs-max) { - .event-item .event-title { - @include str-truncated(65%); + .event-item { + .event-title { + white-space: normal; + overflow: visible; + max-width: 100%; + } + .avatar { + display: none; + } + + .event-body { + margin: 0; + border-left: 2px solid #DDD; + padding-left: 10px; + } + + .event-item-timestamp { + display: none; + } } } diff --git a/app/assets/stylesheets/sections/header.scss b/app/assets/stylesheets/sections/header.scss index e0e0d60c387..9ad1a1db2cd 100644 --- a/app/assets/stylesheets/sections/header.scss +++ b/app/assets/stylesheets/sections/header.scss @@ -59,6 +59,7 @@ header { } .navbar-collapse { + margin-top: 47px; padding-right: 0; padding-left: 0; } diff --git a/app/assets/stylesheets/sections/issues.scss b/app/assets/stylesheets/sections/issues.scss index a7fa715d2e0..9a5400fffbc 100644 --- a/app/assets/stylesheets/sections/issues.scss +++ b/app/assets/stylesheets/sections/issues.scss @@ -75,7 +75,7 @@ } .participants { - margin-bottom: 10px; + margin-bottom: 20px; } .issues_bulk_update { @@ -151,4 +151,14 @@ form.edit-issue { } } } + + .issue { + &:hover .issue-actions { + display: none !important; + } + + .issue-updated-at { + display: none; + } + } } diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index acaad519778..ec844cc00b0 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -104,9 +104,54 @@ } .mr-state-widget { - .panel-body { + background: $box_bg; + margin-bottom: 20px; + @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09)); + + .ci_widget { + padding: 10px 15px; + font-size: 15px; + border-bottom: 1px solid #BBB; + color: #777; + background-color: #F5F5F5; + + &.ci-success { + color: $bg_success; + border-color: $border_success; + background-color: #F1FAF1; + } + + &.ci-pending { + color: #548; + border-color: #548; + background-color: #F4F1FA; + } + + &.ci-running { + color: $bg_warning; + border-color: $border_warning; + background-color: #FAF5F1; + } + + &.ci-failed { + color: $bg_danger; + border-color: $border_danger; + background-color: #FAF1F1; + } + + &.ci-error { + color: $bg_danger; + border-color: $border_danger; + background-color: #FAF1F1; + } + } + + .mr-widget-body { + padding: 10px 15px; + h4 { - margin-top: 0px; + font-size: 20px; + font-weight: normal; } p:last-child { @@ -114,6 +159,11 @@ } } + .mr-widget-footer { + padding: 10px 15px; + border-top: 1px solid #EEE; + } + .ci-coverage { float: right; } diff --git a/app/assets/stylesheets/sections/nav.scss b/app/assets/stylesheets/sections/nav.scss index 31c0a0835db..ccd672c5f67 100644 --- a/app/assets/stylesheets/sections/nav.scss +++ b/app/assets/stylesheets/sections/nav.scss @@ -63,7 +63,6 @@ @media (max-width: $screen-xs-max) { font-size: 18px; margin: 0; - max-height: none; &, .container { @@ -86,6 +85,7 @@ color: #fff; font-weight: normal; text-shadow: none; + border: none; &:after { display: none; } } diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/sections/notes.scss index 7eb42fddade..e1f9c0cb258 100644 --- a/app/assets/stylesheets/sections/notes.scss +++ b/app/assets/stylesheets/sections/notes.scss @@ -36,13 +36,16 @@ ul.notes { font-size: 13px; } .author { - color: #555; + color: #333; font-weight: bold; font-size: 14px; &:hover { - color: $link_hover_color; + color: $link_color; } } + .author-username { + font-size: 14px; + } } .discussion { @@ -224,7 +227,6 @@ ul.notes { margin-bottom: 0; } - .note-preview-holder, .note_text { background: #FFF; border: 1px solid #ddd; @@ -243,15 +245,6 @@ ul.notes { .note_text { width: 100%; } - .nav-tabs { - margin-bottom: 0; - border: none; - - li a, - li.active a { - border: 1px solid #DDD; - } - } } /* loading indicator */ diff --git a/app/assets/stylesheets/sections/profile.scss b/app/assets/stylesheets/sections/profile.scss index 086875582f3..b9f4e317e9c 100644 --- a/app/assets/stylesheets/sections/profile.scss +++ b/app/assets/stylesheets/sections/profile.scss @@ -111,3 +111,20 @@ height: 50px; } } + +//CSS for password-strength indicator +#password-strength { + margin-bottom: 0; +} + +.has-success input { + background-color: #D6F1D7 !important; +} + +.has-error input { + background-color: #F3CECE !important; +} + +.has-warning input { + background-color: #FFE9A4 !important; +} diff --git a/app/assets/stylesheets/sections/projects.scss b/app/assets/stylesheets/sections/projects.scss index b4ee5ccc8d7..7b894cf00bb 100644 --- a/app/assets/stylesheets/sections/projects.scss +++ b/app/assets/stylesheets/sections/projects.scss @@ -270,3 +270,41 @@ ul.nav.nav-projects-tabs { color: #999; } } + +.fork-namespaces { + .thumbnail { + + &.fork-exists-thumbnail { + border-color: #EEE; + + .caption { + color: #999; + } + } + + &.fork-thumbnail { + border-color: #AAA; + + &:hover { + background-color: $hover; + } + } + + a { + text-decoration: none; + } + } +} + +@media (max-width: $screen-xs-max) { + .project-home-panel { + .star-fork-buttons { + padding-top: 10px; + padding-right: 15px; + } + } + + .project-home-links { + display: none; + } +} diff --git a/app/controllers/admin/background_jobs_controller.rb b/app/controllers/admin/background_jobs_controller.rb index 4c1d0df4110..338496013a0 100644 --- a/app/controllers/admin/background_jobs_controller.rb +++ b/app/controllers/admin/background_jobs_controller.rb @@ -1,6 +1,6 @@ class Admin::BackgroundJobsController < Admin::ApplicationController def show - ps_output, _ = Gitlab::Popen.popen(%W(ps -U #{Settings.gitlab.user} -o pid,pcpu,pmem,stat,start,command)) + ps_output, _ = Gitlab::Popen.popen(%W(ps -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command)) @sidekiq_processes = ps_output.split("\n").grep(/sidekiq/) end end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 2f0d344802f..7c2388e81be 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -31,17 +31,11 @@ class Admin::ProjectsController < Admin::ApplicationController protected def project - id = params[:project_id] || params[:id] - - @project = Project.find_with_namespace(id) + @project = Project.find_with_namespace(params[:id]) @project || render_404 end def group - @group ||= project.group - end - - def repository - @repository ||= project.repository + @group ||= @project.group end end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index f63df27eebd..baad9095b70 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -4,6 +4,7 @@ class Admin::UsersController < Admin::ApplicationController def index @users = User.filter(params[:filter]) @users = @users.search(params[:name]) if params[:name].present? + @users = @users.sort(@sort = params[:sort]) @users = @users.alphabetically.page(params[:page]) end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 13d8d2a3e0a..f1e1bebe5ce 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,9 +5,7 @@ class ApplicationController < ActionController::Base before_filter :authenticate_user! before_filter :reject_blocked! before_filter :check_password_expiration - before_filter :add_abilities before_filter :ldap_security_check - before_filter :dev_tools if Rails.env == 'development' before_filter :default_headers before_filter :add_gon_variables before_filter :configure_permitted_parameters, if: :devise_controller? @@ -73,7 +71,7 @@ class ApplicationController < ActionController::Base end def abilities - @abilities ||= Six.new + Ability.abilities end def can?(object, action, subject) @@ -81,28 +79,31 @@ class ApplicationController < ActionController::Base end def project - id = params[:project_id] || params[:id] - - # Redirect from - # localhost/group/project.git - # to - # localhost/group/project - # - if id =~ /\.git\Z/ - redirect_to request.original_url.gsub(/\.git\Z/, '') and return - end + unless @project + id = params[:project_id] || params[:id] + + # Redirect from + # localhost/group/project.git + # to + # localhost/group/project + # + if id =~ /\.git\Z/ + redirect_to request.original_url.gsub(/\.git\Z/, '') and return + end - @project = Project.find_with_namespace(id) + @project = Project.find_with_namespace(id) - if @project and can?(current_user, :read_project, @project) - @project - elsif current_user.nil? - @project = nil - authenticate_user! - else - @project = nil - render_404 and return + if @project and can?(current_user, :read_project, @project) + @project + elsif current_user.nil? + @project = nil + authenticate_user! + else + @project = nil + render_404 and return + end end + @project end def repository @@ -111,22 +112,10 @@ class ApplicationController < ActionController::Base nil end - def add_abilities - abilities << Ability - end - def authorize_project!(action) return access_denied! unless can?(current_user, action, project) end - def authorize_code_access! - return access_denied! unless can?(current_user, :download_code, project) - end - - def authorize_push! - return access_denied! unless can?(current_user, :push_code, project) - end - def authorize_labels! # Labels should be accessible for issues and/or merge requests authorize_read_issue! || authorize_read_merge_request! @@ -170,9 +159,6 @@ class ApplicationController < ActionController::Base response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" end - def dev_tools - end - def default_headers headers['X-Frame-Options'] = 'DENY' headers['X-XSS-Protection'] = '1; mode=block' diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb index f8e1a31e0b3..ada7031fea4 100644 --- a/app/controllers/explore/groups_controller.rb +++ b/app/controllers/explore/groups_controller.rb @@ -1,7 +1,6 @@ class Explore::GroupsController < ApplicationController skip_before_filter :authenticate_user!, - :reject_blocked, :set_current_user_for_observers, - :add_abilities + :reject_blocked, :set_current_user_for_observers layout "explore" diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index b6fa8b7e387..d75fd8e72fa 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -1,7 +1,6 @@ class Explore::ProjectsController < ApplicationController skip_before_filter :authenticate_user!, - :reject_blocked, - :add_abilities + :reject_blocked layout 'explore' diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 63c05d4f33b..ca88d033878 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -19,6 +19,7 @@ class Groups::GroupMembersController < ApplicationController def destroy @users_group = @group.group_members.find(params[:id]) + if can?(current_user, :destroy, @users_group) # May fail if last owner. @users_group.destroy respond_to do |format| diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 3ed6a69c2d8..3e984e5007a 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -15,15 +15,17 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController error.to_s.humanize if error end + # We only find ourselves here + # if the authentication to LDAP was successful. def ldap - # We only find ourselves here - # if the authentication to LDAP was successful. - @user = Gitlab::LDAP::User.find_or_create(oauth) - @user.remember_me = true if @user.persisted? + @user = Gitlab::LDAP::User.new(oauth) + @user.save if @user.changed? # will also save new users + gl_user = @user.gl_user + gl_user.remember_me = true if @user.persisted? # Do additional LDAP checks for the user filter and EE features - if Gitlab::LDAP::Access.allowed?(@user) - sign_in_and_redirect(@user) + if @user.allowed? + sign_in_and_redirect(gl_user) else flash[:alert] = "Access denied for your LDAP account." redirect_to new_user_session_path @@ -40,32 +42,32 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def handle_omniauth if current_user - # Change a logged-in user's authentication method: - current_user.extern_uid = oauth['uid'] - current_user.provider = oauth['provider'] - current_user.save + # Add new authentication method + current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider']) redirect_to profile_path else - @user = Gitlab::OAuth::User.find(oauth) + @user = Gitlab::OAuth::User.new(oauth) + @user.save - # Create user if does not exist - # and allow_single_sign_on is true - if Gitlab.config.omniauth['allow_single_sign_on'] && !@user - @user, errors = Gitlab::OAuth::User.create(oauth) - end - - if @user && !errors - sign_in_and_redirect(@user) + # Only allow properly saved users to login. + if @user.persisted? && @user.valid? + sign_in_and_redirect(@user.gl_user) else - if errors - error_message = errors.map{ |attribute, message| "#{attribute} #{message}" }.join(", ") - redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return - else - flash[:notice] = "There's no such user!" - end - redirect_to new_user_session_path + error_message = + if @user.gl_user.errors.any? + @user.gl_user.errors.map do |attribute, message| + "#{attribute} #{message}" + end.join(", ") + else + '' + end + + redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return end end + rescue ForbiddenAction => e + flash[:notice] = e.message + redirect_to new_user_session_path end def oauth diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 7e4580017dd..6b7fe06d59f 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -29,4 +29,31 @@ class Projects::ApplicationController < ApplicationController redirect_to project_tree_path(@project, @ref), notice: "This action is not allowed unless you are on top of a branch" end end + + def set_filter_variables(collection) + params[:sort] ||= 'newest' + params[:scope] = 'all' if params[:scope].blank? + params[:state] = 'opened' if params[:state].blank? + + @sort = params[:sort].humanize + + assignee_id = params[:assignee_id] + author_id = params[:author_id] + milestone_id = params[:milestone_id] + + if assignee_id.present? && !assignee_id.to_i.zero? + @assignee = @project.team.find(assignee_id) + end + + if author_id.present? && !author_id.to_i.zero? + @author = @project.team.find(assignee_id) + end + + if milestone_id.present? && !milestone_id.to_i.zero? + @milestone = @project.milestones.find(milestone_id) + end + + @assignees = User.where(id: collection.pluck(:assignee_id)) + @authors = User.where(id: collection.pluck(:author_id)) + end end diff --git a/app/controllers/projects/base_tree_controller.rb b/app/controllers/projects/base_tree_controller.rb index 5e305934433..a7b1b7b40e8 100644 --- a/app/controllers/projects/base_tree_controller.rb +++ b/app/controllers/projects/base_tree_controller.rb @@ -1,8 +1,7 @@ class Projects::BaseTreeController < Projects::ApplicationController include ExtractsPath - before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project end diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index a3c41301676..367d1295f34 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -3,8 +3,7 @@ class Projects::BlameController < Projects::ApplicationController include ExtractsPath # Authorize - before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project def show diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 7009e3b1bc8..2412800c493 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -3,10 +3,9 @@ class Projects::BlobController < Projects::ApplicationController include ExtractsPath # Authorize - before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project - before_filter :authorize_push!, only: [:destroy] + before_filter :authorize_push_code!, only: [:destroy] before_filter :blob @@ -20,7 +19,7 @@ class Projects::BlobController < Projects::ApplicationController flash[:notice] = "Your changes have been successfully committed" redirect_to project_tree_path(@project, @ref) else - flash[:alert] = result[:error] + flash[:alert] = result[:message] render :show end end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index faa0ce67ca8..cff1a907dc2 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -1,10 +1,10 @@ class Projects::BranchesController < Projects::ApplicationController + include ActionView::Helpers::SanitizeHelper # Authorize - before_filter :authorize_read_project! before_filter :require_non_empty_project - before_filter :authorize_code_access! - before_filter :authorize_push!, only: [:create, :destroy] + before_filter :authorize_download_code! + before_filter :authorize_push_code!, only: [:create, :destroy] def index @sort = params[:sort] || 'name' @@ -17,8 +17,11 @@ class Projects::BranchesController < Projects::ApplicationController end def create + branch_name = sanitize(strip_tags(params[:branch_name])) + ref = sanitize(strip_tags(params[:ref])) result = CreateBranchService.new(project, current_user). - execute(params[:branch_name], params[:ref]) + execute(branch_name, ref) + if result[:status] == :success @branch = result[:branch] redirect_to project_tree_path(@project, @branch.name) diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 66c67b661db..dac858d8e16 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -3,20 +3,19 @@ # Not to be confused with CommitsController, plural. class Projects::CommitController < Projects::ApplicationController # Authorize - before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project before_filter :commit def show return git_not_found! unless @commit - @line_notes = project.notes.for_commit_id(commit.id).inline - @branches = project.repository.branch_names_contains(commit.id) + @line_notes = @project.notes.for_commit_id(commit.id).inline + @branches = @project.repository.branch_names_contains(commit.id) @diffs = @commit.diffs - @note = project.build_commit_note(commit) - @notes_count = project.notes.for_commit_id(commit.id).count - @notes = project.notes.for_commit_id(@commit.id).not_inline.fresh + @note = @project.build_commit_note(commit) + @notes_count = @project.notes.for_commit_id(commit.id).count + @notes = @project.notes.for_commit_id(@commit.id).not_inline.fresh @noteable = @commit @comments_allowed = @reply_allowed = true @comments_target = { @@ -32,6 +31,6 @@ class Projects::CommitController < Projects::ApplicationController end def commit - @commit ||= project.repository.commit(params[:id]) + @commit ||= @project.repository.commit(params[:id]) end end diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 038645aa497..9476b6c0284 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -4,8 +4,7 @@ class Projects::CommitsController < Projects::ApplicationController include ExtractsPath # Authorize - before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project def show @@ -17,7 +16,7 @@ class Projects::CommitsController < Projects::ApplicationController group(:commit_id).count respond_to do |format| - format.html # index.html.erb + format.html format.json { pager_json("projects/commits/_commits", @commits.size) } format.atom { render layout: false } end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 7a671e8455d..ffb8c2e4af1 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -1,7 +1,6 @@ class Projects::CompareController < Projects::ApplicationController # Authorize - before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project def index diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index d20937ea8ea..024b9520d30 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -42,7 +42,7 @@ class Projects::DeployKeysController < Projects::ApplicationController end def enable - project.deploy_keys << available_keys.find(params[:id]) + @project.deploy_keys << available_keys.find(params[:id]) redirect_to project_deploy_keys_path(@project) end diff --git a/app/controllers/projects/edit_tree_controller.rb b/app/controllers/projects/edit_tree_controller.rb index 8976d7c7be8..65661c80410 100644 --- a/app/controllers/projects/edit_tree_controller.rb +++ b/app/controllers/projects/edit_tree_controller.rb @@ -1,7 +1,7 @@ class Projects::EditTreeController < Projects::BaseTreeController before_filter :require_branch_head before_filter :blob - before_filter :authorize_push! + before_filter :authorize_push_code! before_filter :from_merge_request before_filter :after_edit_path @@ -22,7 +22,7 @@ class Projects::EditTreeController < Projects::BaseTreeController redirect_to after_edit_path else - flash[:alert] = result[:error] + flash[:alert] = result[:message] render :show end end diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb new file mode 100644 index 00000000000..a0481d11582 --- /dev/null +++ b/app/controllers/projects/forks_controller.rb @@ -0,0 +1,22 @@ +class Projects::ForksController < Projects::ApplicationController + # Authorize + before_filter :authorize_download_code! + before_filter :require_non_empty_project + + def new + @namespaces = current_user.manageable_namespaces + @namespaces.delete(@project.namespace) + end + + def create + namespace = Namespace.find(params[:namespace_id]) + @forked_project = ::Projects::ForkService.new(project, current_user, namespace: namespace).execute + + if @forked_project.saved? && @forked_project.forked? + redirect_to(@forked_project, notice: 'Project was successfully forked.') + else + @title = 'Fork project' + render :error + end + end +end diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 610b4967fea..4a318cb7d56 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -1,7 +1,6 @@ class Projects::GraphsController < Projects::ApplicationController # Authorize - before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project def show diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index cab8fd76e6c..2d6c3111192 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -26,6 +26,7 @@ class Projects::HooksController < Projects::ApplicationController def test if !@project.empty_repo? status = TestHookService.new.execute(hook, current_user) + if status flash[:notice] = 'Hook successfully executed.' else diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb new file mode 100644 index 00000000000..b8350642804 --- /dev/null +++ b/app/controllers/projects/imports_controller.rb @@ -0,0 +1,49 @@ +class Projects::ImportsController < Projects::ApplicationController + # Authorize + before_filter :authorize_admin_project! + before_filter :require_no_repo + before_filter :redirect_if_progress, except: :show + + def new + end + + def create + @project.import_url = params[:project][:import_url] + + if @project.save + @project.reload + + if @project.import_failed? + @project.import_retry + else + @project.import_start + end + end + + redirect_to project_import_path(@project) + end + + def show + unless @project.import_in_progress? + if @project.import_finished? + redirect_to(@project) and return + else + redirect_to new_project_import_path(@project) and return + end + end + end + + private + + def require_no_repo + if @project.repository_exists? + redirect_to(@project) and return + end + end + + def redirect_if_progress + if @project.import_in_progress? + redirect_to project_import_path(@project) and return + end + end +end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index c6d526f05c5..22235123826 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -18,18 +18,12 @@ class Projects::IssuesController < Projects::ApplicationController def index terms = params['issue_search'] + set_filter_variables(@project.issues) - @issues = issues_filtered + @issues = IssuesFinder.new.execute(current_user, params.merge(project_id: @project.id)) @issues = @issues.full_search(terms) if terms.present? @issues = @issues.page(params[:page]).per(20) - assignee_id, milestone_id = params[:assignee_id], params[:milestone_id] - @assignee = @project.team.find(assignee_id) if assignee_id.present? && !assignee_id.to_i.zero? - @milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero? - sort_param = params[:sort] || 'newest' - @sort = sort_param.humanize unless sort_param.empty? - @assignees = User.where(id: @project.issues.pluck(:assignee_id)).active - respond_to do |format| format.html format.atom { render layout: false } @@ -127,12 +121,6 @@ class Projects::IssuesController < Projects::ApplicationController return render_404 unless @project.issues_enabled end - def issues_filtered - params[:scope] = 'all' if params[:scope].blank? - params[:state] = 'opened' if params[:state].blank? - @issues = IssuesFinder.new.execute(current_user, params.merge(project_id: @project.id)) - end - # Since iids are implemented only in 6.1 # user may navigate to issue page using old global ids. # diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 20a733b10e1..4d6f41e9de5 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -17,18 +17,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_filter :authorize_modify_merge_request!, only: [:close, :edit, :update, :sort] def index - params[:sort] ||= 'newest' - params[:scope] = 'all' if params[:scope].blank? - params[:state] = 'opened' if params[:state].blank? + set_filter_variables(@project.merge_requests) @merge_requests = MergeRequestsFinder.new.execute(current_user, params.merge(project_id: @project.id)) @merge_requests = @merge_requests.page(params[:page]).per(20) - - @sort = params[:sort].humanize - assignee_id, milestone_id = params[:assignee_id], params[:milestone_id] - @assignee = @project.team.find(assignee_id) if assignee_id.present? && !assignee_id.to_i.zero? - @milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero? - @assignees = User.where(id: @project.merge_requests.pluck(:assignee_id)) end def show @@ -225,6 +217,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController @allowed_to_merge = allowed_to_merge? @show_merge_controls = @merge_request.open? && @commits.any? && @allowed_to_merge @source_branch = @merge_request.source_project.repository.find_branch(@merge_request.source_branch).try(:name) + + if @merge_request.locked_long_ago? + @merge_request.unlock_mr + @merge_request.close + end end def allowed_to_merge? diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index d338cdedfaf..f362f449e70 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -103,7 +103,9 @@ class Projects::MilestonesController < Projects::ApplicationController end def module_enabled - return render_404 unless @project.issues_enabled + unless @project.issues_enabled || @project.merge_requests_enabled + return render_404 + end end def milestone_params diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb index 9832495c64f..ada1aed0df7 100644 --- a/app/controllers/projects/network_controller.rb +++ b/app/controllers/projects/network_controller.rb @@ -3,8 +3,7 @@ class Projects::NetworkController < Projects::ApplicationController include ApplicationHelper # Authorize - before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project def show diff --git a/app/controllers/projects/new_tree_controller.rb b/app/controllers/projects/new_tree_controller.rb index 71a5c6499ec..ffba706b2f6 100644 --- a/app/controllers/projects/new_tree_controller.rb +++ b/app/controllers/projects/new_tree_controller.rb @@ -1,6 +1,6 @@ class Projects::NewTreeController < Projects::BaseTreeController before_filter :require_branch_head - before_filter :authorize_push! + before_filter :authorize_push_code! def show end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 7b08b79d236..2f1d631c14a 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -61,10 +61,6 @@ class Projects::NotesController < Projects::ApplicationController end end - def preview - render text: view_context.markdown(params[:note]) - end - private def note diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 5ec9c576a66..fdbc4c5a098 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -3,8 +3,7 @@ class Projects::RawController < Projects::ApplicationController include ExtractsPath # Authorize - before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project def show diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 7997c726fbb..5d9336bdc49 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -2,8 +2,7 @@ class Projects::RefsController < Projects::ApplicationController include ExtractsPath # Authorize - before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project def switch diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index 4e0f190ed1c..3a90c1c806d 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -1,8 +1,14 @@ class Projects::RepositoriesController < Projects::ApplicationController # Authorize - before_filter :authorize_read_project! - before_filter :authorize_code_access! - before_filter :require_non_empty_project + before_filter :authorize_download_code! + before_filter :require_non_empty_project, except: :create + before_filter :authorize_admin_project!, only: :create + + def create + @project.create_repository + + redirect_to @project + end def archive unless can?(current_user, :download_code, @project) diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index b50f6286459..c50a1f1e75b 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -41,7 +41,8 @@ class Projects::ServicesController < Projects::ApplicationController params.require(:service).permit( :title, :token, :type, :active, :api_key, :subdomain, :room, :recipients, :project_url, :webhook, - :user_key, :device, :priority, :sound + :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, + :build_key, :server ) end end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index cba058fe214..25c887deafa 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -17,7 +17,10 @@ class Projects::SnippetsController < Projects::ApplicationController respond_to :html def index - @snippets = @project.snippets.fresh.non_expired + @snippets = SnippetsFinder.new.execute(current_user, { + filter: :by_project, + project: @project + }) end def new @@ -65,7 +68,7 @@ class Projects::SnippetsController < Projects::ApplicationController @snippet.content, type: 'text/plain; charset=utf-8', disposition: 'inline', - filename: @snippet.file_name + filename: @snippet.sanitized_file_name ) end @@ -88,6 +91,6 @@ class Projects::SnippetsController < Projects::ApplicationController end def snippet_params - params.require(:project_snippet).permit(:title, :content, :file_name, :private) + params.require(:project_snippet).permit(:title, :content, :file_name, :private, :visibility_level) end end diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 537c94bda20..162ddef0fec 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -1,10 +1,8 @@ class Projects::TagsController < Projects::ApplicationController # Authorize - before_filter :authorize_read_project! before_filter :require_non_empty_project - - before_filter :authorize_code_access! - before_filter :authorize_push!, only: [:create] + before_filter :authorize_download_code! + before_filter :authorize_push_code!, only: [:create] before_filter :authorize_admin_project!, only: [:destroy] def index diff --git a/app/controllers/projects/team_members_controller.rb b/app/controllers/projects/team_members_controller.rb index 7bb799eba64..0791e6080fb 100644 --- a/app/controllers/projects/team_members_controller.rb +++ b/app/controllers/projects/team_members_controller.rb @@ -10,7 +10,7 @@ class Projects::TeamMembersController < Projects::ApplicationController end def new - @user_project_relation = project.project_members.new + @user_project_relation = @project.project_members.new end def create @@ -26,7 +26,7 @@ class Projects::TeamMembersController < Projects::ApplicationController end def update - @user_project_relation = project.project_members.find_by(user_id: member) + @user_project_relation = @project.project_members.find_by(user_id: member) @user_project_relation.update_attributes(member_params) unless @user_project_relation.valid? @@ -36,7 +36,7 @@ class Projects::TeamMembersController < Projects::ApplicationController end def destroy - @user_project_relation = project.project_members.find_by(user_id: member) + @user_project_relation = @project.project_members.find_by(user_id: member) @user_project_relation.destroy respond_to do |format| @@ -46,7 +46,7 @@ class Projects::TeamMembersController < Projects::ApplicationController end def leave - project.project_members.find_by(user_id: current_user).destroy + @project.project_members.find_by(user_id: current_user).destroy respond_to do |format| format.html { redirect_to :back } diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index b3380a6ff23..e541b6fd872 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -4,9 +4,7 @@ class ProjectsController < ApplicationController before_filter :repository, except: [:new, :create] # Authorize - before_filter :authorize_read_project!, except: [:index, :new, :create] - before_filter :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive, :retry_import] - before_filter :require_non_empty_project, only: [:blob, :tree, :graph] + before_filter :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive] layout 'navless', only: [:new, :create, :fork] before_filter :set_title, only: [:new, :create] @@ -21,10 +19,11 @@ class ProjectsController < ApplicationController def create @project = ::Projects::CreateService.new(current_user, project_params).execute - flash[:notice] = 'Project was successfully created.' if @project.saved? - respond_to do |format| - format.js + if @project.saved? + redirect_to project_path(@project), notice: 'Project was successfully created.' + else + render 'new' end end @@ -45,16 +44,17 @@ class ProjectsController < ApplicationController def transfer ::Projects::TransferService.new(project, current_user, project_params).execute + if @project.errors[:namespace_id].present? + flash[:alert] = @project.errors[:namespace_id].first + end end def show if @project.import_in_progress? - redirect_to import_project_path(@project) + redirect_to project_import_path(@project) return end - return authenticate_user! unless @project.public? || current_user - limit = (params[:limit] || 20).to_i @events = @project.events.recent @events = event_filter.apply_filter(@events) @@ -64,41 +64,24 @@ class ProjectsController < ApplicationController respond_to do |format| format.html do - if @project.empty_repo? - render "projects/empty", layout: user_layout + if @project.repository_exists? + if @project.empty_repo? + render "projects/empty", layout: user_layout + else + @last_push = current_user.recent_push(@project.id) if current_user + render :show, layout: user_layout + end else - @last_push = current_user.recent_push(@project.id) if current_user - render :show, layout: user_layout + render "projects/no_repo", layout: user_layout end end - format.json { pager_json("events/_events", @events.count) } - end - end - - def import - if project.import_finished? - redirect_to @project - return - end - end - - def retry_import - unless @project.import_failed? - redirect_to import_project_path(@project) - end - - @project.import_url = project_params[:import_url] - if @project.save - @project.reload - @project.import_retry + format.json { pager_json("events/_events", @events.count) } end - - redirect_to import_project_path(@project) end def destroy - return access_denied! unless can?(current_user, :remove_project, project) + return access_denied! unless can?(current_user, :remove_project, @project) ::Projects::DestroyService.new(@project, current_user, {}).execute @@ -115,22 +98,6 @@ class ProjectsController < ApplicationController end end - def fork - @forked_project = ::Projects::ForkService.new(project, current_user).execute - - respond_to do |format| - format.html do - if @forked_project.saved? && @forked_project.forked? - redirect_to(@forked_project, notice: 'Project was successfully forked.') - else - @title = 'Fork project' - render "fork" - end - end - format.js - end - end - def autocomplete_sources note_type = params['type'] note_id = params['type_id'] @@ -148,8 +115,8 @@ class ProjectsController < ApplicationController end def archive - return access_denied! unless can?(current_user, :archive_project, project) - project.archive! + return access_denied! unless can?(current_user, :archive_project, @project) + @project.archive! respond_to do |format| format.html { redirect_to @project } @@ -157,8 +124,8 @@ class ProjectsController < ApplicationController end def unarchive - return access_denied! unless can?(current_user, :archive_project, project) - project.unarchive! + return access_denied! unless can?(current_user, :archive_project, @project) + @project.unarchive! respond_to do |format| format.html { redirect_to @project } @@ -183,6 +150,10 @@ class ProjectsController < ApplicationController render json: { star_count: @project.star_count } end + def markdown_preview + render text: view_context.markdown(params[:md_text]) + end + private def upload_path diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 1bdba75c5e7..5ced98152a5 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -18,6 +18,10 @@ class SessionsController < Devise::SessionsController store_location_for(:redirect, redirect_path) end + if Gitlab.config.ldap.enabled + @ldap_servers = Gitlab::LDAP::Config.servers + end + super end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 3927584235e..312e561b522 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -9,12 +9,14 @@ class SnippetsController < ApplicationController before_filter :set_title + skip_before_filter :authenticate_user!, only: [:index, :user_index, :show, :raw] + respond_to :html - layout 'navless' + layout :determine_layout def index - @snippets = Snippet.are_public.fresh.non_expired.page(params[:page]).per(20) + @snippets = SnippetsFinder.new.execute(current_user, filter: :all).page(params[:page]).per(20) end def user_index @@ -22,22 +24,11 @@ class SnippetsController < ApplicationController render_404 and return unless @user - @snippets = @user.snippets.fresh.non_expired - - if @user == current_user - @snippets = case params[:scope] - when 'are_public' then - @snippets.are_public - when 'are_private' then - @snippets.are_private - else - @snippets - end - else - @snippets = @snippets.are_public - end - - @snippets = @snippets.page(params[:page]).per(20) + @snippets = SnippetsFinder.new.execute(current_user, { + filter: :by_user, + user: @user, + scope: params[:scope]}). + page(params[:page]).per(20) if @user == current_user render 'current_user_index' @@ -88,14 +79,21 @@ class SnippetsController < ApplicationController @snippet.content, type: 'text/plain; charset=utf-8', disposition: 'inline', - filename: @snippet.file_name + filename: @snippet.sanitized_file_name ) end protected def snippet - @snippet ||= PersonalSnippet.where('author_id = :user_id or private is false', user_id: current_user.id).find(params[:id]) + @snippet ||= if current_user + PersonalSnippet.where("author_id = ? OR visibility_level IN (?)", + current_user.id, + [Snippet::PUBLIC, Snippet::INTERNAL]). + find(params[:id]) + else + PersonalSnippet.are_public.find(params[:id]) + end end def authorize_modify_snippet! @@ -111,6 +109,10 @@ class SnippetsController < ApplicationController end def snippet_params - params.require(:personal_snippet).permit(:title, :content, :file_name, :private) + params.require(:personal_snippet).permit(:title, :content, :file_name, :private, :visibility_level) + end + + def determine_layout + current_user ? 'navless' : 'public_users' end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 0b442f5383a..67af1801bda 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -20,9 +20,14 @@ class UsersController < ApplicationController # Get user activity feed for projects common for both users @events = @user.recent_events. - where(project_id: authorized_projects_ids).limit(20) + where(project_id: authorized_projects_ids).limit(30) @title = @user.name + + respond_to do |format| + format.html + format.atom { render layout: false } + end end def determine_layout diff --git a/app/finders/README.md b/app/finders/README.md index 47823c51efb..1f46518d230 100644 --- a/app/finders/README.md +++ b/app/finders/README.md @@ -1,7 +1,7 @@ # Finders -This type of classes responsible for collectiong items based on different conditions. -To prevent lookup methods in models like this: +This type of classes responsible for collection items based on different conditions. +To prevent lookup methods in models like this: ```ruby class Project @@ -13,10 +13,10 @@ end issues = project.issues_for_user_filtered_by(user, params) ``` -Better use this: +Better use this: ```ruby issues = IssuesFinder.new.execute(project, user, filter) ``` -It will help keep models thiner +It will help keep models thiner. diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 56c4f22120d..e1477510065 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -33,6 +33,7 @@ class IssuableFinder items = by_search(items) items = by_milestone(items) items = by_assignee(items) + items = by_author(items) items = by_label(items) items = sort(items) end @@ -48,7 +49,7 @@ class IssuableFinder else [] end - elsif current_user && params[:authorized_only].presence + elsif current_user && params[:authorized_only].presence && !current_user_related? klass.of_projects(current_user.authorized_projects).references(:project) else klass.of_projects(ProjectsFinder.new.execute(current_user)).references(:project) @@ -125,6 +126,14 @@ class IssuableFinder items end + def by_author(items) + if params[:author_id].present? + items = items.where(author_id: (params[:author_id] == '0' ? nil : params[:author_id])) + end + + items + end + def by_label(items) if params[:label_name].present? label_names = params[:label_name].split(",") @@ -142,4 +151,8 @@ class IssuableFinder def project Project.where(id: params[:project_id]).first if params[:project_id].present? end + + def current_user_related? + params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' + end end diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb new file mode 100644 index 00000000000..4b0c69f2d2f --- /dev/null +++ b/app/finders/snippets_finder.rb @@ -0,0 +1,63 @@ +class SnippetsFinder + def execute(current_user, params = {}) + filter = params[:filter] + + case filter + when :all then + snippets(current_user).fresh.non_expired + when :by_user then + by_user(current_user, params[:user], params[:scope]) + when :by_project + by_project(current_user, params[:project]) + end + end + + private + + def snippets(current_user) + if current_user + Snippet.public_and_internal + else + # Not authenticated + # + # Return only: + # public snippets + Snippet.are_public + end + end + + def by_user(current_user, user, scope) + snippets = user.snippets.fresh.non_expired + + return snippets.are_public unless current_user + + if user == current_user + case scope + when 'are_internal' then + snippets.are_internal + when 'are_private' then + snippets.are_private + when 'are_public' then + snippets.are_public + else + snippets + end + else + snippets.public_and_internal + end + end + + def by_project(current_user, project) + snippets = project.snippets.fresh.non_expired + + if current_user + if project.team.member?(current_user.id) + snippets + else + snippets.public_and_internal + end + else + snippets.are_public + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e8a9c2efadf..01aa4a60d4c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -114,6 +114,10 @@ module ApplicationHelper Gitlab::Theme.css_class_by_id(current_user.try(:theme_id)) end + def theme_type + Gitlab::Theme.type_css_class_by_id(current_user.try(:theme_id)) + end + def user_color_scheme_class COLOR_SCHEMES[current_user.try(:color_scheme_id)] if defined?(current_user) end @@ -263,4 +267,12 @@ module ApplicationHelper def escaped_autolink(text) auto_link ERB::Util.html_escape(text), link: :urls end + + def promo_host + 'about.gitlab.com' + end + + def promo_url + 'https://' + promo_host + end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 11fbf1baae7..420ac3f77c7 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -1,14 +1,9 @@ module BlobHelper def highlightjs_class(blob_name) - if blob_name.include?('.') - ext = blob_name.split('.').last - return 'language-' + ext + if no_highlight_files.include?(blob_name.downcase) + 'no-highlight' else - if no_highlight_files.include?(blob_name.downcase) - 'no-highlight' - else - blob_name.downcase - end + blob_name.downcase end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index cab2984a4c4..36adeadd8a5 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -87,8 +87,8 @@ module CommitsHelper # avatar: true will prepend the avatar image # size: size of the avatar image in px def commit_person_link(commit, options = {}) - source_name = commit.send "#{options[:source]}_name".to_sym - source_email = commit.send "#{options[:source]}_email".to_sym + source_name = clean(commit.send "#{options[:source]}_name".to_sym) + source_email = clean(commit.send "#{options[:source]}_email".to_sym) user = User.find_for_commit(source_email, source_name) person_name = user.nil? ? source_name : user.name @@ -120,4 +120,12 @@ module CommitsHelper class: 'commit-short-id') end end + + def truncate_sha(sha) + Commit.truncate_sha(sha) + end + + def clean(string) + Sanitize.clean(string, remove_contents: true) + end end diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index c4e33e3308f..acc0eeb76b3 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -37,40 +37,31 @@ module DashboardHelper end def assigned_entities_count(current_user, entity, scope = nil) - items = current_user.send("assigned_" + entity.pluralize).opened - - if scope.kind_of?(Group) - items = items.of_group(scope) - elsif scope.kind_of?(Project) - items = items.of_projects(scope) - end - - items.count + items = current_user.send('assigned_' + entity.pluralize) + get_count(items, scope) end def authored_entities_count(current_user, entity, scope = nil) - items = current_user.send(entity.pluralize).opened - - if scope.kind_of?(Group) - items = items.of_group(scope) - elsif scope.kind_of?(Project) - items = items.of_projects(scope) - end - - items.count + items = current_user.send(entity.pluralize) + get_count(items, scope) end def authorized_entities_count(current_user, entity, scope = nil) - items = entity.classify.constantize.opened + items = entity.classify.constantize + get_count(items, scope, true, current_user) + end + + protected + def get_count(items, scope, get_authorized = false, current_user = nil) + items = items.opened if scope.kind_of?(Group) items = items.of_group(scope) elsif scope.kind_of?(Project) items = items.of_projects(scope) - else + elsif get_authorized items = items.of_projects(current_user.authorized_projects) end - items.count end end diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb new file mode 100644 index 00000000000..24d67c21d6b --- /dev/null +++ b/app/helpers/emails_helper.rb @@ -0,0 +1,32 @@ +module EmailsHelper + + # Google Actions + # https://developers.google.com/gmail/markup/reference/go-to-action + def email_action(url) + name = action_title(url) + if name + data = { + "@context" => "http://schema.org", + "@type" => "EmailMessage", + "action" => { + "@type" => "ViewAction", + "name" => name, + "url" => url, + } + } + + content_tag :script, type: 'application/ld+json' do + data.to_json.html_safe + end + end + end + + def action_title(url) + return unless url + ["merge_requests", "issues", "commit"].each do |action| + if url.split("/").include?(action) + return "View #{action.humanize.singularize}" + end + end + end +end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 6aeab7bb8ce..a3136926b38 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -136,9 +136,8 @@ module EventsHelper end def event_note(text) - text = first_line_in_markdown(text) - text = truncate(text, length: 150) - sanitize(markdown(text), tags: %w(a img b pre p)) + text = first_line_in_markdown(text, 150) + sanitize(text, tags: %w(a img b pre code p)) end def event_commit_title(message) @@ -146,4 +145,26 @@ module EventsHelper rescue "--broken encoding" end + + def event_to_atom(xml, event) + if event.proper? + xml.entry do + event_link = event_feed_url(event) + event_title = event_feed_title(event) + event_summary = event_feed_summary(event) + + 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:%SZ") + xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(event.author_email) + xml.author do |author| + xml.name event.author_name + xml.email event.author_email + end + + xml.summary(type: "xhtml") { |x| x << event_summary unless event_summary.nil? } + end + end + end end diff --git a/app/helpers/git_helper.rb b/app/helpers/git_helper.rb new file mode 100644 index 00000000000..09684955233 --- /dev/null +++ b/app/helpers/git_helper.rb @@ -0,0 +1,5 @@ +module GitHelper + def strip_gpg_signature(text) + text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "") + end +end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 0365681a128..800cacdc2c2 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -51,12 +51,14 @@ module GitlabMarkdownHelper @markdown.render(text).html_safe end - def first_line_in_markdown(text) - line = text.split("\n").detect do |i| - i.present? && markdown(i).present? - end - line += '...' unless line.nil? - line + # Return the first line of +text+, up to +max_chars+, after parsing the line + # as Markdown. HTML tags in the parsed output are not counted toward the + # +max_chars+ limit. If the length limit falls within a tag's contents, then + # the tag contents are truncated without removing the closing tag. + def first_line_in_markdown(text, max_chars = nil) + md = markdown(text).strip + + truncate_visible(md, max_chars || md.length) if md.present? end def render_wiki_content(wiki_page) @@ -204,4 +206,64 @@ module GitlabMarkdownHelper def correct_ref @ref ? @ref : "master" end + + private + + # Return +text+, truncated to +max_chars+ characters, excluding any HTML + # tags. + def truncate_visible(text, max_chars) + doc = Nokogiri::HTML.fragment(text) + content_length = 0 + truncated = false + + doc.traverse do |node| + if node.text? || node.content.empty? + if truncated + node.remove + next + end + + # Handle line breaks within a node + if node.content.strip.lines.length > 1 + node.content = "#{node.content.lines.first.chomp}..." + truncated = true + end + + num_remaining = max_chars - content_length + if node.content.length > num_remaining + node.content = node.content.truncate(num_remaining) + truncated = true + end + content_length += node.content.length + end + + truncated = truncate_if_block(node, truncated) + end + + doc.to_html + end + + # Used by #truncate_visible. If +node+ is the first block element, and the + # text hasn't already been truncated, then append "..." to the node contents + # and return true. Otherwise return false. + def truncate_if_block(node, truncated) + if node.element? && node.description.block? && !truncated + node.content = "#{node.content}..." if node.next_sibling + true + else + truncated + end + end + + def cross_project_reference(project, entity) + path = project.path_with_namespace + + if entity.kind_of?(Issue) + [path, entity.iid].join('#') + elsif entity.kind_of?(MergeRequest) + [path, entity.iid].join('!') + else + raise 'Not supported type' + end + end end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 7671033b539..a5b393c1e3c 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -62,6 +62,19 @@ module IssuesHelper '' end + 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')}" + if issue.updated_at != issue.created_at + ts << capture_haml do + haml_tag :small do + haml_concat " (Edited #{time_ago_with_tooltip(issue.updated_at, 'bottom', 'issue_edited_ago')})" + end + end + end + ts.html_safe + end + # Checks if issues_tracker setting exists in gitlab.yml def external_issues_tracker_enabled? Gitlab.config.issues_tracker && Gitlab.config.issues_tracker.values.any? @@ -100,4 +113,19 @@ module IssuesHelper 'issue-box-open' end end + + def issue_to_atom(xml, issue) + xml.entry do + xml.id project_issue_url(issue.project, issue) + xml.link href: project_issue_url(issue.project, issue) + xml.title truncate(issue.title, length: 80) + xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") + xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(issue.author_email) + xml.author do |author| + xml.name issue.author_name + xml.email issue.author_email + end + xml.summary issue.title + end + end end diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index bf25dce2301..2bcfde62830 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -25,4 +25,12 @@ module NamespacesHelper hidden_field_tag(id, value, class: css_class) end + + def namespace_icon(namespace, size = 40) + if namespace.kind_of?(Group) + group_icon(namespace.path) + else + avatar_icon(namespace.owner.email, size) + end + end end diff --git a/app/helpers/oauth_helper.rb b/app/helpers/oauth_helper.rb index c0177dacbf8..df18db71c84 100644 --- a/app/helpers/oauth_helper.rb +++ b/app/helpers/oauth_helper.rb @@ -1,6 +1,6 @@ module OauthHelper def ldap_enabled? - Devise.omniauth_providers.include?(:ldap) + Gitlab.config.ldap.enabled end def default_providers @@ -16,4 +16,8 @@ module OauthHelper [:twitter, :github, :google_oauth2].include?(name.to_sym) end end + + def additional_providers + enabled_oauth_providers.reject{|provider| provider.to_s.starts_with?('ldap')} + end end diff --git a/app/helpers/profile_helper.rb b/app/helpers/profile_helper.rb index 0b375558305..6480fd3886f 100644 --- a/app/helpers/profile_helper.rb +++ b/app/helpers/profile_helper.rb @@ -1,6 +1,6 @@ module ProfileHelper def oauth_active_class(provider) - if current_user.provider == provider.to_s + if current_user.identities.exists?(provider: provider.to_s) 'active' end end @@ -10,10 +10,10 @@ module ProfileHelper end def show_profile_social_tab? - enabled_social_providers.any? && !current_user.ldap_user? + enabled_social_providers.any? end def show_profile_remove_tab? - gitlab_config.signup_enabled && !current_user.ldap_user? + gitlab_config.signup_enabled end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 6df7dae7314..fb5470d98e5 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -42,12 +42,12 @@ module ProjectsHelper def project_title(project) if project.group content_tag :span do - link_to(simple_sanitize(project.group.name), group_path(project.group)) + " / " + project.name + link_to(simple_sanitize(project.group.name), group_path(project.group)) + ' / ' + link_to(simple_sanitize(project.name), project_path(project)) end else owner = project.namespace.owner content_tag :span do - link_to(simple_sanitize(owner.name), user_path(owner)) + " / " + project.name + link_to(simple_sanitize(owner.name), user_path(owner)) + ' / ' + link_to(simple_sanitize(project.name), project_path(project)) end end end @@ -56,6 +56,10 @@ module ProjectsHelper "You are going to remove #{project.name_with_namespace}.\n Removed project CANNOT be restored!\n Are you ABSOLUTELY sure?" end + def transfer_project_message(project) + "You are going to transfer #{project.name_with_namespace} to another owner. Are you ABSOLUTELY sure?" + end + def project_nav_tabs @nav_tabs ||= get_project_nav_tabs(@project, current_user) end @@ -128,9 +132,9 @@ module ProjectsHelper toggle_html = content_tag('span', class: 'toggle') do toggle_text = if starred - 'Unstar' + ' Unstar' else - 'Star' + ' Star' end content_tag('i', ' ', class: 'fa fa-star') + toggle_text diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb new file mode 100644 index 00000000000..492e065b713 --- /dev/null +++ b/app/helpers/sorting_helper.rb @@ -0,0 +1,17 @@ +module SortingHelper + def sort_title_oldest_updated + 'Oldest updated' + end + + def sort_title_recently_updated + 'Recently updated' + end + + def sort_title_oldest_created + 'Oldest created' + end + + def sort_title_recently_created + 'Recently created' + end +end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index c3b537eac47..8e209498323 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -66,7 +66,7 @@ module TreeHelper def tree_breadcrumbs(tree, max_links = 2) if @path.present? part_path = "" - parts = @path.split("\/") + parts = @path.split('/') yield('..', nil) if parts.count > max_links @@ -90,7 +90,7 @@ module TreeHelper end def editing_preview_title(filename) - if gitlab_markdown?(filename) || markup?(filename) + if Gitlab::MarkdownHelper.previewable?(filename) 'Preview' else 'Diff' diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 8b83b8ff640..deb9c8b4d49 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -28,6 +28,23 @@ module VisibilityLevelHelper end end + def snippet_visibility_level_description(level) + capture_haml do + haml_tag :span do + case level + when Gitlab::VisibilityLevel::PRIVATE + haml_concat "The snippet is visible only for me" + when Gitlab::VisibilityLevel::INTERNAL + haml_concat "The snippet is visible for any logged in user." + when Gitlab::VisibilityLevel::PUBLIC + haml_concat "The snippet can be accessed" + haml_concat "without any" + haml_concat "authentication." + end + end + end + end + def visibility_level_icon(level) case level when Gitlab::VisibilityLevel::PRIVATE diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index f8a7d133d1d..6d7f8eb4b02 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -1,8 +1,7 @@ module Emails module Profile - def new_user_email(user_id, password, token = nil) + def new_user_email(user_id, token = nil) @user = User.find(user_id) - @password = password @target_url = user_url(@user) @token = token mail(to: @user.email, subject: subject("Account was created for you")) diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index bd438bab89a..6d671e6e0bd 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -11,6 +11,7 @@ class Notify < ActionMailer::Base add_template_helper ApplicationHelper add_template_helper GitlabMarkdownHelper add_template_helper MergeRequestsHelper + add_template_helper EmailsHelper default_url_options[:host] = Gitlab.config.gitlab.host default_url_options[:protocol] = Gitlab.config.gitlab.protocol @@ -25,6 +26,14 @@ class Notify < ActionMailer::Base delay_for(2.seconds) end + def test_email(recepient_email, subject, body) + mail(to: recepient_email, + subject: subject, + body: body.html_safe, + content_type: 'text/html' + ) + end + private # The default email address to send emails from diff --git a/app/models/ability.rb b/app/models/ability.rb index e155abc1449..97a72bf3635 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -262,5 +262,13 @@ class Ability end rules end + + def abilities + @abilities ||= begin + abilities = Six.new + abilities << self + abilities + end + end end end diff --git a/app/models/commit.rb b/app/models/commit.rb index a1343b65c72..37dd371ec00 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -10,22 +10,33 @@ class Commit # Used to prevent 500 error on huge commits by suppressing diff # # User can force display of diff above this size - DIFF_SAFE_FILES = 100 - DIFF_SAFE_LINES = 5000 + DIFF_SAFE_FILES = 100 unless defined?(DIFF_SAFE_FILES) + DIFF_SAFE_LINES = 5000 unless defined?(DIFF_SAFE_LINES) # Commits above this size will not be rendered in HTML - DIFF_HARD_LIMIT_FILES = 1000 - DIFF_HARD_LIMIT_LINES = 50000 + DIFF_HARD_LIMIT_FILES = 1000 unless defined?(DIFF_HARD_LIMIT_FILES) + DIFF_HARD_LIMIT_LINES = 50000 unless defined?(DIFF_HARD_LIMIT_LINES) class << self def decorate(commits) - commits.map { |c| self.new(c) } + commits.map do |commit| + if commit.kind_of?(Commit) + commit + else + self.new(commit) + end + end end # Calculate number of lines to render for diffs def diff_line_count(diffs) diffs.reduce(0) { |sum, d| sum + d.diff.lines.count } end + + # Truncate sha to 8 characters + def truncate_sha(sha) + sha[0..7] + end end attr_accessor :raw @@ -111,7 +122,7 @@ class Commit # Mentionable override. def gfm_reference - "commit #{sha[0..5]}" + "commit #{id}" end def method_missing(m, *args, &block) @@ -124,6 +135,11 @@ class Commit super end + # Truncate sha to 8 characters + def short_id + @raw.short_id(7) + end + def parents @parents ||= Commit.decorate(super) end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 553087946d6..f49708fd6eb 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -131,9 +131,10 @@ module Issuable users.concat(mentions.reduce([], :|)).uniq end - def to_hook_data + def to_hook_data(user) { object_kind: self.class.name.underscore, + user: user.hook_attrs, object_attributes: hook_attrs } end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 5938d9cb28e..6c1aa99668a 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -52,11 +52,7 @@ module Mentionable if identifier == "all" users += project.team.members.flatten else - if has_project - id = project.team.members.find_by(username: identifier).try(:id) - else - id = User.find_by(username: identifier).try(:id) - end + id = User.find_by(username: identifier).try(:id) users << User.find(id) unless id.blank? end end diff --git a/app/models/event.rb b/app/models/event.rb index 9e296c00281..65b4c2edfee 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -186,10 +186,6 @@ class Event < ActiveRecord::Base data[:ref]["refs/heads"] end - def new_branch? - commit_from =~ /^00000/ - end - def new_ref? commit_from =~ /^00000/ end @@ -266,7 +262,7 @@ class Event < ActiveRecord::Base end def note_short_commit_id - note_commit_id[0..8] + Commit.truncate_sha(note_commit_id) end def note_commit? diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 23fa01e0b70..8479d4aecf6 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -32,7 +32,10 @@ class WebHook < ActiveRecord::Base def execute(data) parsed_url = URI.parse(url) if parsed_url.userinfo.blank? - WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" }, verify: false) + WebHook.post(url, + body: data.to_json, + headers: { "Content-Type" => "application/json" }, + verify: false) else post_url = url.gsub("#{parsed_url.userinfo}@", "") auth = { @@ -45,6 +48,9 @@ class WebHook < ActiveRecord::Base verify: false, basic_auth: auth) end + rescue SocketError, Errno::ECONNREFUSED => e + logger.error("WebHook Error => #{e}") + false end def async_execute(data) diff --git a/app/models/identity.rb b/app/models/identity.rb new file mode 100644 index 00000000000..5fb1850c30b --- /dev/null +++ b/app/models/identity.rb @@ -0,0 +1,5 @@ +class Identity < ActiveRecord::Base + belongs_to :user + + validates :extern_uid, allow_blank: true, uniqueness: {scope: :provider} +end
\ No newline at end of file diff --git a/app/models/member.rb b/app/models/member.rb index 7dc13c18bf3..671ef466baa 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -1,3 +1,18 @@ +# == Schema Information +# +# Table name: members +# +# id :integer not null, primary key +# access_level :integer not null +# source_id :integer not null +# source_type :string(255) not null +# user_id :integer not null +# notification_level :integer not null +# type :string(255) +# created_at :datetime +# updated_at :datetime +# + class Member < ActiveRecord::Base include Notifiable include Gitlab::Access diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index e72393c4278..b7f296b13fb 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -1,3 +1,18 @@ +# == Schema Information +# +# Table name: members +# +# id :integer not null, primary key +# access_level :integer not null +# source_id :integer not null +# source_type :string(255) not null +# user_id :integer not null +# notification_level :integer not null +# type :string(255) +# created_at :datetime +# updated_at :datetime +# + class GroupMember < Member SOURCE_TYPE = 'Namespace' diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 71525f91961..30c09f768d7 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -1,3 +1,18 @@ +# == Schema Information +# +# Table name: members +# +# id :integer not null, primary key +# access_level :integer not null +# source_id :integer not null +# source_type :string(255) not null +# user_id :integer not null +# notification_level :integer not null +# type :string(255) +# created_at :datetime +# updated_at :datetime +# + class ProjectMember < Member SOURCE_TYPE = 'Project' diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7c525b02f48..2cc427d35c2 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -70,6 +70,16 @@ class MergeRequest < ActiveRecord::Base transition locked: :reopened end + after_transition any => :locked do |merge_request, transition| + merge_request.locked_at = Time.now + merge_request.save + end + + after_transition :locked => (any - :locked) do |merge_request, transition| + merge_request.locked_at = nil + merge_request.save + end + state :opened state :reopened state :closed @@ -336,4 +346,8 @@ class MergeRequest < ActiveRecord::Base source_project.repository.branch_names end end + + def locked_long_ago? + locked_at && locked_at < (Time.now - 1.day) + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 409e82ed1ef..a71122d5e07 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -55,7 +55,7 @@ class MergeRequestDiff < ActiveRecord::Base end def last_commit_short_sha - @last_commit_short_sha ||= last_commit.sha[0..10] + @last_commit_short_sha ||= last_commit.short_id end private diff --git a/app/models/namespace.rb b/app/models/namespace.rb index c0c6de0ee7d..ea4b48fdd7f 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -90,4 +90,8 @@ class Namespace < ActiveRecord::Base def kind type == 'Group' ? 'group' : 'user' end + + def find_fork_of(project) + projects.joins(:forked_project_link).where('forked_project_links.forked_from_project_id = ?', project.id).first + end end diff --git a/app/models/note.rb b/app/models/note.rb index 6f1b1a4da94..5996298be22 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -80,7 +80,7 @@ class Note < ActiveRecord::Base note_options = { project: project, author: author, - note: "_mentioned in #{gfm_reference}_", + note: cross_reference_note_content(gfm_reference), system: true } @@ -90,7 +90,7 @@ class Note < ActiveRecord::Base note_options.merge!(noteable: noteable) end - create(note_options) + create(note_options) unless cross_reference_disallowed?(noteable, mentioner) end def create_milestone_change_note(noteable, project, author, milestone) @@ -165,6 +165,15 @@ class Note < ActiveRecord::Base [:discussion, type.try(:underscore), id, line_code].join("-").to_sym end + # Determine if cross reference note should be created. + # eg. mentioning a commit in MR comments which exists inside a MR + # should not create "mentioned in" note. + def cross_reference_disallowed?(noteable, mentioner) + if mentioner.kind_of?(MergeRequest) + mentioner.commits.map(&:id).include? noteable.id + end + end + # Determine whether or not a cross-reference note already exists. def cross_reference_exists?(noteable, mentioner) gfm_reference = mentioner_gfm_ref(noteable, mentioner) @@ -174,7 +183,7 @@ class Note < ActiveRecord::Base where(noteable_id: noteable.id) end - notes.where('note like ?', "_mentioned in #{gfm_reference}_"). + notes.where('note like ?', cross_reference_note_content(gfm_reference)). system.any? end @@ -182,8 +191,16 @@ class Note < ActiveRecord::Base where("note like :query", query: "%#{query}%") end + def cross_reference_note_prefix + '_mentioned in ' + end + private + def cross_reference_note_content(gfm_reference) + cross_reference_note_prefix + "#{gfm_reference}_" + end + # Prepend the mentioner's namespaced project path to the GFM reference for # cross-project references. For same-project references, return the # unmodified GFM reference. @@ -243,12 +260,16 @@ class Note < ActiveRecord::Base def commit_author @commit_author ||= - project.users.find_by(email: noteable.author_email) || - project.users.find_by(name: noteable.author_name) + project.team.users.find_by(email: noteable.author_email) || + project.team.users.find_by(name: noteable.author_name) rescue nil end + def cross_reference? + note.start_with?(self.class.cross_reference_note_prefix) + end + def find_diff return nil unless noteable && noteable.diffs.present? @@ -296,7 +317,7 @@ class Note < ActiveRecord::Base end def diff_file_index - line_code.split('_')[0] + line_code.split('_')[0] if line_code end def diff_file_name @@ -312,11 +333,11 @@ class Note < ActiveRecord::Base end def diff_old_line - line_code.split('_')[1].to_i + line_code.split('_')[1].to_i if line_code end def diff_new_line - line_code.split('_')[2].to_i + line_code.split('_')[2].to_i if line_code end def generate_line_code(line) @@ -337,6 +358,20 @@ class Note < ActiveRecord::Base @diff_line end + def diff_line_type + return @diff_line_type if @diff_line_type + + if diff + diff_lines.each do |line| + if generate_line_code(line) == self.line_code + @diff_line_type = line.type + end + end + end + + @diff_line_type + end + def truncated_diff_lines max_number_of_lines = 16 prev_match_line = nil @@ -467,6 +502,6 @@ class Note < ActiveRecord::Base end def editable? - !system + !read_attribute(:system) end end diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb index a3c0d201ee5..9cee3b70cb3 100644 --- a/app/models/personal_snippet.rb +++ b/app/models/personal_snippet.rb @@ -2,17 +2,17 @@ # # Table name: snippets # -# id :integer not null, primary key -# title :string(255) -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string(255) -# expires_at :datetime -# private :boolean default(TRUE), not null -# type :string(255) +# id :integer not null, primary key +# title :string(255) +# content :text +# author_id :integer not null +# project_id :integer +# created_at :datetime +# updated_at :datetime +# file_name :string(255) +# expires_at :datetime +# type :string(255) +# visibility_level :integer default(0), not null # class PersonalSnippet < Snippet diff --git a/app/models/project.rb b/app/models/project.rb index 44d63d37bee..32b0145ca24 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -64,6 +64,8 @@ class Project < ActiveRecord::Base has_one :assembla_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy has_one :slack_service, dependent: :destroy + has_one :buildbox_service, dependent: :destroy + has_one :bamboo_service, dependent: :destroy has_one :pushover_service, dependent: :destroy has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link @@ -134,7 +136,7 @@ class Project < ActiveRecord::Base state_machine :import_status, initial: :none do event :import_start do - transition :none => :started + transition [:none, :finished] => :started end event :import_finish do @@ -172,7 +174,7 @@ class Project < ActiveRecord::Base end def with_push - includes(:events).where('events.action = ?', Event::PUSHED) + joins(:events).where('events.action = ?', Event::PUSHED) end def active @@ -312,7 +314,7 @@ class Project < ActiveRecord::Base end def available_services_names - %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push gemnasium slack pushover) + %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push gemnasium slack pushover buildbox bamboo) end def gitlab_ci? @@ -388,55 +390,14 @@ class Project < ActiveRecord::Base end def execute_services(data) - services.each do |service| - - # Call service hook only if it is active - begin - service.execute(data) if service.active - rescue => e - logger.error(e) - end + services.select(&:active).each do |service| + service.async_execute(data) end end def update_merge_requests(oldrev, newrev, ref, user) - return true unless ref =~ /heads/ - branch_name = ref.gsub("refs/heads/", "") - commits = self.repository.commits_between(oldrev, newrev) - c_ids = commits.map(&:id) - - # Close merge requests - mrs = self.merge_requests.opened.where(target_branch: branch_name).to_a - mrs = mrs.select(&:last_commit).select { |mr| c_ids.include?(mr.last_commit.id) } - - mrs.uniq.each do |merge_request| - MergeRequests::MergeService.new.execute(merge_request, user, nil) - end - - # Update code for merge requests into project between project branches - mrs = self.merge_requests.opened.by_branch(branch_name).to_a - # Update code for merge requests between project and project fork - mrs += self.fork_merge_requests.opened.by_branch(branch_name).to_a - - mrs.uniq.each do |merge_request| - merge_request.reload_code - merge_request.mark_as_unchecked - end - - # Add comment about pushing new commits to merge requests - comment_mr_with_commits(branch_name, commits, user) - - true - end - - def comment_mr_with_commits(branch_name, commits, user) - mrs = self.origin_merge_requests.opened.where(source_branch: branch_name).to_a - mrs += self.fork_merge_requests.opened.where(source_branch: branch_name).to_a - - mrs.uniq.each do |merge_request| - Note.create_new_commits_note(merge_request, merge_request.project, - user, commits) - end + MergeRequests::RefreshService.new(self, user). + execute(oldrev, newrev, ref) end def valid_repo? @@ -619,4 +580,25 @@ class Project < ActiveRecord::Base def origin_merge_requests merge_requests.where(source_project_id: self.id) end + + def create_repository + if gitlab_shell.add_repository(path_with_namespace) + true + else + errors.add(:base, "Failed to create repository") + false + end + end + + def repository_exists? + !!repository.exists? + end + + def create_wiki + ProjectWiki.new(self, self.owner).wiki + true + rescue ProjectWiki::CouldNotCreateWikiError => ex + errors.add(:base, "Failed create wiki") + false + end end diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index 3421a0330aa..0b90a14f39c 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # class AssemblaService < Service diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb new file mode 100644 index 00000000000..b9eec9ab21e --- /dev/null +++ b/app/models/project_services/bamboo_service.rb @@ -0,0 +1,105 @@ +class BambooService < CiService + include HTTParty + + prop_accessor :bamboo_url, :build_key, :username, :password + + validates :bamboo_url, presence: true, + format: { with: URI::regexp }, if: :activated? + validates :build_key, presence: true, if: :activated? + validates :username, presence: true, + if: ->(service) { service.password? }, if: :activated? + validates :password, presence: true, + if: ->(service) { service.username? }, if: :activated? + + attr_accessor :response + + after_save :compose_service_hook, if: :activated? + + def compose_service_hook + hook = service_hook || build_service_hook + hook.save + end + + def title + 'Atlassian Bamboo CI' + end + + def description + 'A continuous integration and build server' + end + + def help + 'You must set up automatic revision labeling and a repository trigger in Bamboo.' + end + + def to_param + 'bamboo' + end + + def fields + [ + { type: 'text', name: 'bamboo_url', + placeholder: 'Bamboo root URL like https://bamboo.example.com' }, + { type: 'text', name: 'build_key', + placeholder: 'Bamboo build plan key like KEY' }, + { type: 'text', name: 'username', + placeholder: 'A user with API access, if applicable' }, + { type: 'password', name: 'password' }, + ] + end + + def build_info(sha) + url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}") + + if username.blank? && password.blank? + @response = HTTParty.get(parsed_url.to_s, verify: false) + else + get_url = "#{url}&os_authType=basic" + auth = { + username: username, + password: password, + } + @response = HTTParty.get(get_url, verify: false, basic_auth: auth) + end + end + + def build_page(sha) + build_info(sha) if @response.nil? || !@response.code + + if @response.code != 200 || @response['results']['results']['size'] == '0' + # If actual build link can't be determined, send user to build summary page. + "#{bamboo_url}/browse/#{build_key}" + else + # If actual build link is available, go to build result page. + result_key = @response['results']['results']['result']['planResultKey']['key'] + "#{bamboo_url}/browse/#{result_key}" + end + end + + def commit_status(sha) + build_info(sha) if @response.nil? || !@response.code + return :error unless @response.code == 200 || @response.code == 404 + + status = if @response.code == 404 || @response['results']['results']['size'] == '0' + 'Pending' + else + @response['results']['results']['result']['buildState'] + end + + if status.include?('Success') + 'success' + elsif status.include?('Failed') + 'failed' + elsif status.include?('Pending') + 'pending' + else + :error + end + end + + def execute(_data) + # Bamboo requires a GET and does not take any data. + self.class.get("#{bamboo_url}/updateAndBuild.action?buildKey=#{build_key}", + verify: false) + end +end diff --git a/app/models/project_services/buildbox_service.rb b/app/models/project_services/buildbox_service.rb new file mode 100644 index 00000000000..0ab67b79fe4 --- /dev/null +++ b/app/models/project_services/buildbox_service.rb @@ -0,0 +1,123 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + +require "addressable/uri" + +class BuildboxService < CiService + prop_accessor :project_url, :token + + validates :project_url, presence: true, if: :activated? + validates :token, presence: true, if: :activated? + + after_save :compose_service_hook, if: :activated? + + def webhook_url + "#{buildbox_endpoint('webhook')}/deliver/#{webhook_token}" + end + + def compose_service_hook + hook = service_hook || build_service_hook + hook.url = webhook_url + hook.save + end + + def execute(data) + service_hook.execute(data) + end + + def commit_status(sha) + response = HTTParty.get(commit_status_path(sha), verify: false) + + if response.code == 200 && response['status'] + response['status'] + else + :error + end + end + + def commit_status_path(sha) + "#{buildbox_endpoint('gitlab')}/status/#{status_token}.json?commit=#{sha}" + end + + def build_page(sha) + "#{project_url}/builds?commit=#{sha}" + end + + def builds_path + "#{project_url}/builds?branch=#{project.default_branch}" + end + + def status_img_path + "#{buildbox_endpoint('badge')}/#{status_token}.svg" + end + + def title + 'Buildbox' + end + + def description + 'Continuous integration and deployments' + end + + def to_param + 'buildbox' + end + + def fields + [ + { type: 'text', + name: 'token', + placeholder: 'Buildbox project GitLab token' }, + + { type: 'text', + name: 'project_url', + placeholder: 'https://buildbox.io/example/project' } + ] + end + + private + + def webhook_token + token_parts.first + end + + def status_token + token_parts.second + end + + def token_parts + if token.present? + token.split(':') + else + [] + end + end + + def buildbox_endpoint(subdomain = nil) + endpoint = 'https://buildbox.io' + + if subdomain.present? + uri = Addressable::URI.parse(endpoint) + new_endpoint = "#{uri.scheme || 'http'}://#{subdomain}.#{uri.host}" + + if uri.port.present? + "#{new_endpoint}:#{uri.port}" + else + new_endpoint + end + else + endpoint + end + end +end diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 2d8950db491..0736ddab99b 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # class CampfireService < Service diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 829f495abc6..b1d5e49ede3 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # # Base class for CI services diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 5c4537cfca5..b9071b98295 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # class EmailsOnPushService < Service diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 4d11b00c192..86705f5dabd 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require "flowdock-git-hook" @@ -37,13 +37,12 @@ class FlowdockService < Service end def execute(push_data) - repo_path = File.join(Gitlab.config.gitlab_shell.repos_path, "#{project.path_with_namespace}.git") Flowdock::Git.post( push_data[:ref], push_data[:before], push_data[:after], token: token, - repo: repo_path, + repo: project.repository.path_to_repo, repo_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}", commit_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/%s", diff_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/compare/%s...%s", diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 7b6c87e4cec..18fdd204ecd 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require "gemnasium/gitlab_service" @@ -38,14 +38,13 @@ class GemnasiumService < Service end def execute(push_data) - repo_path = File.join(Gitlab.config.gitlab_shell.repos_path, "#{project.path_with_namespace}.git") Gemnasium::GitlabService.execute( ref: push_data[:ref], before: push_data[:before], after: push_data[:after], token: token, api_key: api_key, - repo: repo_path + repo: project.repository.path_to_repo ) end end diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 001b11c5966..fadebf968bc 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# property :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # class GitlabCiService < CiService @@ -28,7 +28,7 @@ class GitlabCiService < CiService end def commit_status_path(sha) - project_url + "/builds/#{sha}/status.json?token=#{token}" + project_url + "/commits/#{sha}/status.json?token=#{token}" end def get_ci_build(sha) @@ -55,7 +55,7 @@ class GitlabCiService < CiService end def build_page(sha) - project_url + "/builds/#{sha}" + project_url + "/commits/#{sha}" end def builds_path diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 3a1ba168e6a..a848d74044c 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -2,24 +2,24 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # class HipchatService < Service MAX_COMMITS = 3 - prop_accessor :token, :room + prop_accessor :token, :room, :server validates :token, presence: true, if: :activated? def title - 'Hipchat' + 'HipChat' end def description @@ -33,7 +33,9 @@ class HipchatService < Service def fields [ { type: 'text', name: 'token', placeholder: '' }, - { type: 'text', name: 'room', placeholder: '' } + { type: 'text', name: 'room', placeholder: '' }, + { type: 'text', name: 'server', + placeholder: 'Leave blank for default. https://chat.hipchat.com' } ] end @@ -44,7 +46,9 @@ class HipchatService < Service private def gate - @gate ||= HipChat::Client.new(token) + options = { api_version: 'v2' } + options[:server_url] = server unless server.nil? + @gate ||= HipChat::Client.new(token, options) end def create_message(push) diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index 3aa928b92a0..09e114f9cca 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # class PivotaltrackerService < Service diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index dfa1e9c9820..963f5440b6f 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # class SlackService < Service @@ -30,23 +30,20 @@ class SlackService < Service def fields [ - { type: 'text', name: 'webhook', placeholder: '' } + { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' } ] end def execute(push_data) + return unless webhook.present? + message = SlackMessage.new(push_data.merge( project_url: project_url, project_name: project_name )) - credentials = webhook.match(/(\w*).slack.com.*services\/(.*)/) - if credentials.present? - subdomain = credentials[1] - token = credentials[2].split("token=").last - notifier = Slack::Notifier.new(subdomain, token) - notifier.ping(message.pretext, attachments: message.attachments) - end + notifier = Slack::Notifier.new(webhook) + notifier.ping(message.pretext, attachments: message.attachments) end private diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index 14c88046423..9e2c1b0e18e 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -2,17 +2,17 @@ # # Table name: snippets # -# id :integer not null, primary key -# title :string(255) -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string(255) -# expires_at :datetime -# private :boolean default(TRUE), not null -# type :string(255) +# id :integer not null, primary key +# title :string(255) +# content :text +# author_id :integer not null +# project_id :integer +# created_at :datetime +# updated_at :datetime +# file_name :string(255) +# expires_at :datetime +# type :string(255) +# visibility_level :integer default(0), not null # class ProjectSnippet < Snippet diff --git a/app/models/project_team.rb b/app/models/project_team.rb index e065554d3b8..657ee23ae23 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -133,6 +133,10 @@ class ProjectTeam max_tm_access(user.id) == Gitlab::Access::MASTER end + def member?(user_id) + !!find_tm(user_id) + end + def max_tm_access(user_id) access = [] access << project.project_members.find_by(user_id: user_id).try(:access_field) diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 770a26ed894..f8a28ca9866 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -5,7 +5,7 @@ class ProjectWiki 'Markdown' => :markdown, 'RDoc' => :rdoc, 'AsciiDoc' => :asciidoc - } + } unless defined?(MARKUPS) class CouldNotCreateWikiError < StandardError; end diff --git a/app/models/repository.rb b/app/models/repository.rb index 339e485e6d2..93994123a90 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -30,6 +30,8 @@ class Repository commit = Gitlab::Git::Commit.find(raw_repository, id) commit = Commit.new(commit) if commit commit + rescue Rugged::OdbError => ex + nil end def commits(ref, path = nil, limit = nil, offset = nil, skip_merges = false) diff --git a/app/models/service.rb b/app/models/service.rb index 1f3a6520473..71c8aa39e45 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -2,14 +2,15 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# # To add new service you should build a class inherited from Service # and implement a set of methods @@ -81,4 +82,8 @@ class Service < ActiveRecord::Base } end end + + def async_execute(data) + Sidekiq::Client.enqueue(ProjectServiceWorker, id, data) + end end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 80c1af8f337..9aba42a0622 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -2,23 +2,24 @@ # # Table name: snippets # -# id :integer not null, primary key -# title :string(255) -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string(255) -# expires_at :datetime -# private :boolean default(TRUE), not null -# type :string(255) +# id :integer not null, primary key +# title :string(255) +# content :text +# author_id :integer not null +# project_id :integer +# created_at :datetime +# updated_at :datetime +# file_name :string(255) +# expires_at :datetime +# type :string(255) +# visibility_level :integer default(0), not null # class Snippet < ActiveRecord::Base include Linguist::BlobHelper + include Gitlab::VisibilityLevel - default_value_for :private, true + default_value_for :visibility_level, Snippet::PRIVATE belongs_to :author, class_name: "User" @@ -28,12 +29,17 @@ 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 } + validates :file_name, presence: true, length: { within: 0..255 }, + format: { with: Gitlab::Regex.path_regex, + message: Gitlab::Regex.path_regex_message } validates :content, presence: true + validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values } # Scopes - scope :are_public, -> { where(private: false) } - scope :are_private, -> { where(private: true) } + scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) } + scope :are_private, -> { where(visibility_level: Snippet::PRIVATE) } + scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) } + scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } scope :fresh, -> { order("created_at DESC") } scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } @@ -58,6 +64,10 @@ class Snippet < ActiveRecord::Base file_name end + def sanitized_file_name + file_name.gsub(/[^a-zA-Z0-9_\-\.]+/, '') + end + def mode nil end @@ -66,6 +76,10 @@ class Snippet < ActiveRecord::Base expires_at && expires_at < Time.current end + def visibility_level_field + visibility_level + end + class << self def search(query) where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%") @@ -76,7 +90,7 @@ class Snippet < ActiveRecord::Base end def accessible_to(user) - where('private = ? OR author_id = ?', false, user) + where('visibility_level IN (?) OR author_id = ?', [Snippet::INTERNAL, Snippet::PUBLIC], user) end end end diff --git a/app/models/tree.rb b/app/models/tree.rb index 07c9a825e24..4f5d81f0a5e 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -15,7 +15,7 @@ class Tree # by markup renderer. if available_readmes.length > 1 supported_readmes = available_readmes.select do |readme| - gitlab_markdown?(readme.name) || markup?(readme.name) + previewable?(readme.name) end # Take the first supported readme, or the first available readme, if we diff --git a/app/models/user.rb b/app/models/user.rb index c90f2462426..7faeef1b5b0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -79,6 +79,7 @@ class User < ActiveRecord::Base # Profile has_many :keys, dependent: :destroy has_many :emails, dependent: :destroy + has_many :identities, dependent: :destroy # Groups has_many :members, dependent: :destroy @@ -113,7 +114,6 @@ class User < ActiveRecord::Base validates :name, presence: true validates :email, presence: true, email: {strict_mode: true}, uniqueness: true validates :bio, length: { maximum: 255 }, allow_blank: true - validates :extern_uid, allow_blank: true, uniqueness: {scope: :provider} validates :projects_limit, presence: true, numericality: {greater_than_or_equal_to: 0} validates :username, presence: true, uniqueness: { case_sensitive: false }, exclusion: { in: Gitlab::Blacklist.path }, @@ -124,7 +124,7 @@ class User < ActiveRecord::Base validate :namespace_uniq, if: ->(user) { user.username_changed? } validate :avatar_type, if: ->(user) { user.avatar_changed? } validate :unique_email, if: ->(user) { user.email_changed? } - validates :avatar, file_size: { maximum: 100.kilobytes.to_i } + validates :avatar, file_size: { maximum: 200.kilobytes.to_i } before_validation :generate_password, on: :create before_validation :sanitize_attrs @@ -178,8 +178,6 @@ class User < ActiveRecord::Base scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) } 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 :ldap, -> { where(provider: 'ldap') } - scope :potential_team_members, ->(team) { team.members.any? ? active.not_in_team(team) : active } # @@ -196,6 +194,16 @@ class User < ActiveRecord::Base end end + def sort(method) + case method.to_s + when 'recent_sign_in' then reorder('users.last_sign_in_at DESC') + when 'oldest_sign_in' then reorder('users.last_sign_in_at ASC') + when 'recently_created' then reorder('users.created_at DESC') + when 'late_created' then reorder('users.created_at ASC') + else reorder("users.name ASC") + end + end + def find_for_commit(email, name) # Prefer email match over name match User.where(email: email).first || @@ -217,6 +225,11 @@ class User < ActiveRecord::Base where("lower(name) LIKE :query OR lower(email) LIKE :query OR lower(username) LIKE :query", query: "%#{query.downcase}%") end + def by_login(login) + where('lower(username) = :value OR lower(email) = :value', + value: login.to_s.downcase).first + end + def by_username_or_id(name_or_id) where('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i).first end @@ -321,11 +334,7 @@ class User < ActiveRecord::Base end def abilities - @abilities ||= begin - abilities = Six.new - abilities << Ability - abilities - end + Ability.abilities end def can_select_namespace? @@ -397,7 +406,11 @@ class User < ActiveRecord::Base end def ldap_user? - extern_uid && provider == 'ldap' + identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"]) + end + + def ldap_identity + @ldap_identity ||= identities.find_by(["provider LIKE ?", "ldap%"]) end def accessible_deploy_keys @@ -488,6 +501,14 @@ class User < ActiveRecord::Base end end + def hook_attrs + { + name: name, + username: username, + avatar_url: avatar_url + } + end + def ensure_namespace_correct # Ensure user has namespace self.create_namespace!(path: self.username, name: self.username) unless self.namespace @@ -533,4 +554,14 @@ class User < ActiveRecord::Base UsersStarProject.create!(project: project, user: self) end end + + def manageable_namespaces + @manageable_namespaces ||= + begin + namespaces = [] + namespaces << namespace + namespaces += owned_groups + namespaces += masters_groups + end + end end diff --git a/app/services/base_service.rb b/app/services/base_service.rb index ed286c04095..0d46eeaa18f 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -6,11 +6,7 @@ class BaseService end def abilities - @abilities ||= begin - abilities = Six.new - abilities << Ability - abilities - end + Ability.abilities end def can?(object, action, subject) diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index db6f0831f8b..bd245100955 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -10,12 +10,6 @@ module Files private - def success - out = super() - out[:error] = '' - out - end - def repository project.repository end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 8f2b0e347f6..529af1970f6 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -83,9 +83,14 @@ class GitPushService # closing regex. Exclude any mentioned Issues from cross-referencing even if the commits are being pushed to # a different branch. issues_to_close = commit.closes_issues(project) - author = commit_user(commit) - if !issues_to_close.empty? && is_default_branch + # Load commit author only if needed. + # For push with 1k commits it prevents 900+ requests in database + author = nil + + if issues_to_close.present? && is_default_branch + author ||= commit_user(commit) + issues_to_close.each do |issue| Issues::CloseService.new(project, author, {}).execute(issue, commit) end @@ -96,8 +101,13 @@ class GitPushService # being pushed to a different branch). refs = commit.references(project) - issues_to_close refs.reject! { |r| commit.has_mentioned?(r) } - refs.each do |r| - Note.create_cross_reference_note(r, commit, author, project) + + if refs.present? + author ||= commit_user(commit) + + refs.each do |r| + Note.create_cross_reference_note(r, commit, author, project) + end end end end @@ -160,19 +170,19 @@ class GitPushService ref_parts = ref.split('/') # Return if this is not a push to a branch (e.g. new commits) - ref_parts[1] =~ /heads/ && oldrev != "0000000000000000000000000000000000000000" + ref_parts[1] =~ /heads/ && oldrev != Gitlab::Git::BLANK_SHA end def push_to_new_branch?(ref, oldrev) ref_parts = ref.split('/') - ref_parts[1] =~ /heads/ && oldrev == "0000000000000000000000000000000000000000" + ref_parts[1] =~ /heads/ && oldrev == Gitlab::Git::BLANK_SHA end def push_remove_branch?(ref, newrev) ref_parts = ref.split('/') - ref_parts[1] =~ /heads/ && newrev == "0000000000000000000000000000000000000000" + ref_parts[1] =~ /heads/ && newrev == Gitlab::Git::BLANK_SHA end def push_to_branch?(ref) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb new file mode 100644 index 00000000000..e3371ec3c1b --- /dev/null +++ b/app/services/issuable_base_service.rb @@ -0,0 +1,13 @@ +class IssuableBaseService < BaseService + private + + def create_assignee_note(issuable) + Note.create_assignee_change_note( + issuable, issuable.project, current_user, issuable.assignee) + end + + def create_milestone_note(issuable) + Note.create_milestone_change_note( + issuable, issuable.project, current_user, issuable.milestone) + end +end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 71b9ffc3489..755c0ef45a8 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -1,21 +1,13 @@ module Issues - class BaseService < ::BaseService + class BaseService < ::IssuableBaseService private - def create_assignee_note(issue) - Note.create_assignee_change_note(issue, issue.project, current_user, issue.assignee) - end - def execute_hooks(issue, action = 'open') - issue_data = issue.to_hook_data + issue_data = issue.to_hook_data(current_user) issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id) issue_data[:object_attributes].merge!(url: issue_url, action: action) issue.project.execute_hooks(issue_data, :issue_hooks) end - - def create_milestone_note(issue) - Note.create_milestone_change_note(issue, issue.project, current_user, issue.milestone) - end end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 5b2746ffecf..0ee9635ed99 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -33,12 +33,5 @@ module Issues issue end - - private - - def update_task(issue, params, checked) - issue.update_nth_task(params[:task_num].to_i, checked) - params.except!(:task_num) - end end end diff --git a/app/services/merge_requests/base_merge_service.rb b/app/services/merge_requests/base_merge_service.rb index 9bc50d3d16c..700a21ca011 100644 --- a/app/services/merge_requests/base_merge_service.rb +++ b/app/services/merge_requests/base_merge_service.rb @@ -13,7 +13,8 @@ module MergeRequests def execute_project_hooks(merge_request) if merge_request.project - merge_request.project.execute_hooks(merge_request.to_hook_data, :merge_request_hooks) + hook_data = merge_request.to_hook_data(current_user) + merge_request.project.execute_hooks(hook_data, :merge_request_hooks) end end end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 2907f3587da..7f3421b8e4b 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -1,11 +1,5 @@ module MergeRequests - class BaseService < ::BaseService - - private - - def create_assignee_note(merge_request) - Note.create_assignee_change_note(merge_request, merge_request.project, current_user, merge_request.assignee) - end + class BaseService < ::IssuableBaseService def create_note(merge_request) Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil) @@ -13,12 +7,9 @@ module MergeRequests def execute_hooks(merge_request) if merge_request.project - merge_request.project.execute_hooks(merge_request.to_hook_data, :merge_request_hooks) + hook_data = merge_request.to_hook_data(current_user) + merge_request.project.execute_hooks(hook_data, :merge_request_hooks) end end - - def create_milestone_note(merge_request) - Note.create_milestone_change_note(merge_request, merge_request.project, current_user, merge_request.milestone) - end end end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb new file mode 100644 index 00000000000..baf0936cc3d --- /dev/null +++ b/app/services/merge_requests/refresh_service.rb @@ -0,0 +1,86 @@ +module MergeRequests + class RefreshService < MergeRequests::BaseService + def execute(oldrev, newrev, ref) + return true unless ref =~ /heads/ + + @oldrev, @newrev = oldrev, newrev + @branch_name = ref.gsub("refs/heads/", "") + @fork_merge_requests = @project.fork_merge_requests.opened + @commits = @project.repository.commits_between(oldrev, newrev) + + close_merge_requests + reload_merge_requests + comment_mr_with_commits + + true + end + + private + + # Collect open merge requests that target same branch we push into + # and close if push to master include last commit from merge request + # We need this to close(as merged) merge requests that were merged into + # target branch manually + def close_merge_requests + commit_ids = @commits.map(&:id) + merge_requests = @project.merge_requests.opened.where(target_branch: @branch_name).to_a + merge_requests = merge_requests.select(&:last_commit) + + merge_requests = merge_requests.select do |merge_request| + commit_ids.include?(merge_request.last_commit.id) + end + + + merge_requests.uniq.select(&:source_project).each do |merge_request| + MergeRequests::MergeService.new.execute(merge_request, @current_user, nil) + end + end + + def force_push? + Gitlab::ForcePushCheck.force_push?(@project, @oldrev, @newrev) + end + + # Refresh merge request diff if we push to source or target branch of merge request + # Note: we should update merge requests from forks too + def reload_merge_requests + merge_requests = @project.merge_requests.opened.by_branch(@branch_name).to_a + merge_requests += @fork_merge_requests.by_branch(@branch_name).to_a + merge_requests = filter_merge_requests(merge_requests) + + merge_requests.each do |merge_request| + + if merge_request.source_branch == @branch_name || force_push? + merge_request.reload_code + merge_request.mark_as_unchecked + else + mr_commit_ids = merge_request.commits.map(&:id) + push_commit_ids = @commits.map(&:id) + matches = mr_commit_ids & push_commit_ids + + if matches.any? + merge_request.reload_code + merge_request.mark_as_unchecked + else + merge_request.mark_as_unchecked + end + end + end + end + + # Add comment about pushing new commits to merge requests + def comment_mr_with_commits + merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a + merge_requests += @fork_merge_requests.where(source_branch: @branch_name).to_a + merge_requests = filter_merge_requests(merge_requests) + + merge_requests.each do |merge_request| + Note.create_new_commits_note(merge_request, merge_request.project, + @current_user, @commits) + end + end + + def filter_merge_requests(merge_requests) + merge_requests.uniq.select(&:source_project) + end + end +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index fe39f83b400..d1aadd741e1 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -107,7 +107,7 @@ class NotificationService # Notify new user with email after creation def new_user(user, token = nil) # Don't email omniauth created users - mailer.new_user_email(user.id, user.password, token) unless user.extern_uid? + mailer.new_user_email(user.id, token) unless user.identities.any? end # Notify users on new note in system @@ -119,11 +119,12 @@ class NotificationService # ignore gitlab service messages return true if note.note =~ /\A_Status changed to closed_/ - return true if note.note =~ /\A_mentioned in / && note.system == true + return true if note.cross_reference? && note.system == true opts = { noteable_type: note.noteable_type, project_id: note.project_id } target = note.noteable + if target.respond_to?(:participants) recipients = target.participants else diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 12386792aab..3672b623806 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -37,35 +37,22 @@ module Projects @project.creator = current_user - if @project.save - log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") - system_hook_service.execute_hooks_for(@project, :create) + Project.transaction do + @project.save - unless @project.group - @project.team << [current_user, :master] - end - - @project.update_column(:last_activity_at, @project.created_at) - - if @project.import? - @project.import_start - else - GitlabShellWorker.perform_async( - :add_repository, - @project.path_with_namespace - ) + unless @project.import? + unless @project.create_repository + raise 'Failed to create repository' + end end + end + if @project.persisted? if @project.wiki_enabled? - begin - # force the creation of a wiki, - ProjectWiki.new(@project, @project.owner).wiki - rescue ProjectWiki::CouldNotCreateWikiError => ex - # Prevent project observer crash - # if failed to create wiki - nil - end + @project.create_wiki end + + after_create_actions end @project @@ -84,5 +71,20 @@ module Projects namespace = Namespace.find_by(id: namespace_id) current_user.can?(:create_projects, namespace) end + + def after_create_actions + log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") + system_hook_service.execute_hooks_for(@project, :create) + + unless @project.group + @project.team << [current_user, :master] + end + + @project.update_column(:last_activity_at, @project.created_at) + + if @project.import? + @project.import_start + end + end end end diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index a59311bf942..4930660055a 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -2,11 +2,9 @@ module Projects class ForkService < BaseService include Gitlab::ShellAdapter - def initialize(project, user) - @from_project, @current_user = project, user - end - def execute + @from_project = @project + project_params = { visibility_level: @from_project.visibility_level, description: @from_project.description, @@ -15,8 +13,18 @@ module Projects project = Project.new(project_params) project.name = @from_project.name project.path = @from_project.path - project.namespace = current_user.namespace - project.creator = current_user + project.creator = @current_user + + if namespace = @params[:namespace] + project.namespace = namespace + else + project.namespace = @current_user.namespace + end + + unless @current_user.can?(:create_projects, project.namespace) + project.errors.add(:namespace, 'insufficient access rights') + return project + end # If the project cannot save, we do not want to trigger the project destroy # as this can have the side effect of deleting a repo attached to an existing @@ -27,7 +35,7 @@ module Projects #First save the DB entries as they can be rolled back if the repo fork fails project.build_forked_project_link(forked_to_project_id: project.id, forked_from_project_id: @from_project.id) if project.save - project.team << [current_user, :master] + project.team << [@current_user, :master] end #Now fork the repo unless gitlab_shell.fork_repository(@from_project.path_with_namespace, project.namespace.path) @@ -42,8 +50,8 @@ module Projects else project.errors.add(:base, "Invalid fork destination") end - project + project end end end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index a6b68749a71..44e494525b3 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -18,7 +18,7 @@ class SystemHooksService def build_event_data(model, event) data = { event_name: build_event_name(model, event), - created_at: model.created_at + created_at: model.created_at.xmlschema } case model diff --git a/app/services/test_hook_service.rb b/app/services/test_hook_service.rb index b6b1ef29b51..17d86a7a274 100644 --- a/app/services/test_hook_service.rb +++ b/app/services/test_hook_service.rb @@ -2,8 +2,5 @@ class TestHookService def execute(hook, current_user) data = GitPushService.new.sample_data(hook.project, current_user) hook.execute(data) - true - rescue SocketError - false end end diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index 9dcf7b488ee..8db2b2a709c 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -25,7 +25,7 @@ - next unless process.match(/(sidekiq \d+\.\d+\.\d+.+$)/) - data = process.strip.split(' ') %tr - %td= Settings.gitlab.user + %td= gitlab_config.user - 5.times do %td= data.shift %td= data.join(' ') @@ -36,7 +36,7 @@ If '[25 of 25 busy]' is shown, restart GitLab with 'sudo service gitlab reload'. %p %i.fa.fa-exclamation-circle - If more than one sidekiq process is listed, stop GitLab, kill the remaining sidekiq processes (sudo pkill -u #{Settings.gitlab.user} -f sidekiq) and restart GitLab. + If more than one sidekiq process is listed, stop GitLab, kill the remaining sidekiq processes (sudo pkill -u #{gitlab_config.user} -f sidekiq) and restart GitLab. diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index c56863ce274..f4d7e25fd74 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -2,39 +2,20 @@ - if @group.errors.any? .alert.alert-danger %span= @group.errors.full_messages.first - .form-group.group_name_holder - = f.label :name, class: 'control-label' do - Group name - .col-sm-10 - = f.text_field :name, placeholder: "Example Group", class: "form-control" - .form-group.group-description-holder - = f.label :description, "Details", class: 'control-label' - .col-sm-10 - = f.text_area :description, maxlength: 250, class: "form-control js-gfm-input", rows: 4 + = render 'shared/group_form', f: f .form-group.group-description-holder = f.label :avatar, "Group avatar", class: 'control-label' .col-sm-10 - %a.choose-btn.btn.btn-small.js-choose-group-avatar-button - %i.fa.fa-paperclip - %span Choose File ... - - %span.file_name.js-avatar-filename File name... - = f.file_field :avatar, class: "js-group-avatar-input hidden" - .light The maximum file size allowed is 100KB. + = render 'shared/choose_group_avatar_button', f: f - if @group.new_record? .form-group .col-sm-2 .col-sm-10 .bs-callout.bs-callout-info - %ul - %li A group is a collection of several projects - %li Groups are private by default - %li Members of a group may only view projects they have permission to access - %li Group project URLs are prefixed with the group namespace - %li Existing projects may be moved into a group + = render 'shared/group_tips' .form-actions = f.submit 'Create group', class: "btn btn-create" = link_to 'Cancel', admin_groups_path, class: "btn btn-cancel" diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 09105679bd2..1d7fef43184 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -10,7 +10,7 @@ = form_tag admin_groups_path, method: :get, class: 'form-inline' do .form-group = text_field_tag :name, params[:name], class: "form-control input-mn-300" - = submit_tag "Search", class: "btn submit btn-primary" + = button_tag "Search", class: "btn submit btn-primary" %hr diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index c1a9214b77a..8057de38805 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -64,7 +64,7 @@ %div.prepend-top-10 = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" %hr - = submit_tag 'Add users into group', class: "btn btn-create" + = button_tag 'Add users into group', class: "btn btn-create" .panel.panel-default .panel-heading %h3.panel-title @@ -74,13 +74,13 @@ %ul.well-list.group-users-list - @members.each do |member| - user = member.user - %li{class: dom_class(user)} + %li{class: dom_class(member), id: dom_id(user)} .list-item-name %strong = link_to user.name, admin_user_path(user) %span.pull-right.light = member.human_access - = link_to group_group_members_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do + = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse .panel-footer = paginate @members, param_name: 'members_page', theme: 'gitlab' diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index b3f8f012f00..384c6ee9af5 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -1,68 +1,25 @@ +- loggers = [Gitlab::GitLogger, Gitlab::AppLogger, + Gitlab::ProductionLogger, Gitlab::SidekiqLogger] %ul.nav.nav-tabs.log-tabs - %li.active - = link_to "githost.log", "#githost", 'data-toggle' => 'tab' - %li - = link_to "application.log", "#application", 'data-toggle' => 'tab' - %li - = link_to "production.log", "#production", 'data-toggle' => 'tab' - %li - = link_to "sidekiq.log", "#sidekiq", 'data-toggle' => 'tab' - + - loggers.each do |klass| + %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } + = link_to klass::file_name, "##{klass::file_name_noext}", + 'data-toggle' => 'tab' %p.light To prevent performance issues admin logs output the last 2000 lines .tab-content - .tab-pane.active#githost - .file-holder#README - .file-title - %i.fa.fa-file - githost.log - .pull-right - = link_to '#', class: 'log-bottom' do - %i.fa.fa-arrow-down - Scroll down - .file-content.logs - %ol - - Gitlab::GitLogger.read_latest.each do |line| - %li - %p= line - .tab-pane#application - .file-holder#README - .file-title - %i.fa.fa-file - application.log - .pull-right - = link_to '#', class: 'log-bottom' do - %i.fa.fa-arrow-down - Scroll down - .file-content.logs - %ol - - Gitlab::AppLogger.read_latest.each do |line| - %li - %p= line - .tab-pane#production - .file-holder#README - .file-title - %i.fa.fa-file - production.log - .pull-right - = link_to '#', class: 'log-bottom' do - %i.fa.fa-arrow-down - Scroll down - .file-content.logs - %ol - - Gitlab::Logger.read_latest_for('production.log').each do |line| - %li - %p= line - .tab-pane#sidekiq - .file-holder#README - .file-title - %i.fa.fa-file - sidekiq.log - .pull-right - = link_to '#', class: 'log-bottom' do - %i.fa.fa-arrow-down - Scroll down - .file-content.logs - %ol - - Gitlab::Logger.read_latest_for('sidekiq.log').each do |line| - %li - %p= line + - loggers.each do |klass| + .tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''), + id: klass::file_name_noext } + .file-holder#README + .file-title + %i.fa.fa-file + = klass::file_name + .pull-right + = link_to '#', class: 'log-bottom' do + %i.fa.fa-arrow-down + Scroll down + .file-content.logs + %ol + - klass.read_latest.each do |line| + %li + %p= line diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 5ca6090f8d3..aa59f38d213 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -35,7 +35,7 @@ = label %hr = hidden_field_tag :sort, params[:sort] - = submit_tag "Search", class: "btn submit btn-primary" + = button_tag "Search", class: "btn submit btn-primary" = link_to "Reset", admin_projects_path, class: "btn btn-cancel" .col-md-9 @@ -56,13 +56,13 @@ = link_to admin_projects_path(sort: nil) do Name = link_to admin_projects_path(sort: 'newest') do - Newest + = sort_title_recently_created = link_to admin_projects_path(sort: 'oldest') do - Oldest + = sort_title_oldest_created = link_to admin_projects_path(sort: 'recently_updated') do - Recently updated + = sort_title_recently_updated = link_to admin_projects_path(sort: 'last_updated') do - Last updated + = sort_title_oldest_updated = link_to admin_projects_path(sort: 'largest_repository') do Largest repository = link_to 'New Project', new_project_path, class: "btn btn-new" diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 5c2664e14fe..8e1ecb41a85 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -32,6 +32,27 @@ .panel-heading Users (#{@users.total_count}) .panel-head-actions + .dropdown.inline + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %span.light sort: + - if @sort.present? + = @sort.humanize + - else + Name + %b.caret + %ul.dropdown-menu + %li + = link_to admin_users_path(sort: nil) do + Name + = link_to admin_users_path(sort: 'recent_sign_in') do + Recent sign in + = link_to admin_users_path(sort: 'oldest_sign_in') do + Oldest sign in + = link_to admin_users_path(sort: 'recently_created') do + = sort_title_recently_created + = link_to admin_users_path(sort: 'late_created') do + = sort_title_oldest_created + = link_to 'New User', new_admin_user_path, class: "btn btn-new" %ul.well-list - @users.each do |user| diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 211d77d5185..29717aedd80 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -95,7 +95,7 @@ %li %span.light LDAP uid: %strong - = @user.extern_uid + = @user.ldap_identity.extern_uid - if @user.created_by %li diff --git a/app/views/dashboard/_zero_authorized_projects.html.haml b/app/views/dashboard/_zero_authorized_projects.html.haml index 711e607f0bc..5d133cd8285 100644 --- a/app/views/dashboard/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/_zero_authorized_projects.html.haml @@ -46,5 +46,5 @@ %br Public projects are an easy way to allow everyone to have read-only access. .link_holder - = link_to explore_projects_path, class: "btn btn-new" do + = link_to trending_explore_projects_path, class: "btn btn-new" do Browse public projects » diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder index f5413557783..66381310221 100644 --- a/app/views/dashboard/issues.atom.builder +++ b/app/views/dashboard/issues.atom.builder @@ -1,24 +1,13 @@ xml.instruct! -xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do +xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlnsmedia" => "http://search.yahoo.com/mrss/" do xml.title "#{current_user.name} issues" - xml.link :href => issues_dashboard_url(:atom, :private_token => current_user.private_token), :rel => "self", :type => "application/atom+xml" - xml.link :href => issues_dashboard_url(:private_token => current_user.private_token), :rel => "alternate", :type => "text/html" - xml.id issues_dashboard_url(:private_token => current_user.private_token) + xml.link href: issues_dashboard_url(:atom, private_token: current_user.private_token), rel: "self", type: "application/atom+xml" + xml.link href: issues_dashboard_url(private_token: current_user.private_token), rel: "alternate", type: "text/html" + xml.id issues_dashboard_url(private_token: current_user.private_token) xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? @issues.each do |issue| - xml.entry do - xml.id project_issue_url(issue.project, issue) - xml.link :href => project_issue_url(issue.project, issue) - xml.title truncate(issue.title, :length => 80) - xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") - xml.media :thumbnail, :width => "40", :height => "40", :url => avatar_icon(issue.author_email) - xml.author do |author| - xml.name issue.author_name - xml.email issue.author_email - end - xml.summary issue.title - end + issue_to_atom(xml, issue) end end diff --git a/app/views/dashboard/projects.html.haml b/app/views/dashboard/projects.html.haml index f124c688be1..5b7835b097b 100644 --- a/app/views/dashboard/projects.html.haml +++ b/app/views/dashboard/projects.html.haml @@ -14,13 +14,14 @@ = link_to projects_dashboard_filter_path(sort: nil) do Name = link_to projects_dashboard_filter_path(sort: 'newest') do - Newest + = sort_title_recently_created = link_to projects_dashboard_filter_path(sort: 'oldest') do - Oldest + = sort_title_oldest_created = link_to projects_dashboard_filter_path(sort: 'recently_updated') do - Recently updated + = sort_title_recently_updated = link_to projects_dashboard_filter_path(sort: 'last_updated') do - Last updated + = sort_title_oldest_updated + %p.light All projects you have access to are listed here. Public projects are not included here unless you are a member %hr diff --git a/app/views/dashboard/show.atom.builder b/app/views/dashboard/show.atom.builder index f4cf24ccd99..70ac66f8016 100644 --- a/app/views/dashboard/show.atom.builder +++ b/app/views/dashboard/show.atom.builder @@ -1,29 +1,12 @@ xml.instruct! -xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do +xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlnsmedia" => "http://search.yahoo.com/mrss/" do xml.title "Dashboard feed#{" - #{current_user.name}" if current_user.name.present?}" - xml.link :href => dashboard_url(:atom), :rel => "self", :type => "application/atom+xml" - xml.link :href => dashboard_url, :rel => "alternate", :type => "text/html" + xml.link href: dashboard_url(:atom), rel: "self", type: "application/atom+xml" + xml.link href: dashboard_url, rel: "alternate", type: "text/html" xml.id projects_url xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| - if event.proper? - xml.entry do - event_link = event_feed_url(event) - event_title = event_feed_title(event) - event_summary = event_feed_summary(event) - - 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:%SZ") - xml.media :thumbnail, :width => "40", :height => "40", :url => avatar_icon(event.author_email) - xml.author do |author| - xml.name event.author_name - xml.email event.author_email - end - xml.summary(:type => "xhtml") { |x| x << event_summary unless event_summary.nil? } - end - end + event_to_atom(xml, event) end end diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml index 1326cc0aac9..f6cbf9b82ba 100644 --- a/app/views/devise/passwords/edit.html.haml +++ b/app/views/devise/passwords/edit.html.haml @@ -6,8 +6,8 @@ .devise-errors = devise_error_messages! = f.hidden_field :reset_password_token - %div - = f.password_field :password, class: "form-control top", placeholder: "New password", required: true + .form-group#password-strength + = f.password_field :password, class: "form-control top", id: "user_password_recover", placeholder: "New password", required: true %div = f.password_field :password_confirmation, class: "form-control bottom", placeholder: "Confirm new password", required: true .clearfix.append-bottom-10 diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml index d6a952f3dc5..123de881f59 100644 --- a/app/views/devise/registrations/new.html.haml +++ b/app/views/devise/registrations/new.html.haml @@ -11,8 +11,8 @@ = f.text_field :username, class: "form-control middle", placeholder: "Username", required: true %div = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true - %div - = f.password_field :password, class: "form-control middle", placeholder: "Password", required: true + .form-group#password-strength + = f.password_field :password, class: "form-control middle", id: "user_password_sign_up", placeholder: "Password", required: true %div = f.password_field :password_confirmation, class: "form-control bottom", placeholder: "Confirm password", required: true %div diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index 6c5a878e904..bf8a593c254 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,5 +1,5 @@ -= form_tag(user_omniauth_callback_path(:ldap), id: 'new_ldap_user' ) do += form_tag(user_omniauth_callback_path(provider), id: 'new_ldap_user' ) do = text_field_tag :username, nil, {class: "form-control top", placeholder: "LDAP Login", autofocus: "autofocus"} = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} %br/ - = submit_tag "LDAP Sign in", class: "btn-save btn" + = button_tag "LDAP Sign in", class: "btn-save btn" diff --git a/app/views/devise/sessions/_oauth_providers.html.haml b/app/views/devise/sessions/_oauth_providers.html.haml index 15048a78063..8d6aaefb9ff 100644 --- a/app/views/devise/sessions/_oauth_providers.html.haml +++ b/app/views/devise/sessions/_oauth_providers.html.haml @@ -1,4 +1,4 @@ -- providers = (enabled_oauth_providers - [:ldap]) +- providers = additional_providers - if providers.present? .bs-callout.bs-callout-info{:'data-no-turbolink' => 'data-no-turbolink'} %span Sign in with: diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index b70b0d66172..ca7e9570b43 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -2,22 +2,24 @@ .login-heading %h3 Sign in .login-body - - if ldap_enabled? && gitlab_config.signin_enabled + - if ldap_enabled? %ul.nav.nav-tabs - %li.active - = link_to 'LDAP', '#tab-ldap', 'data-toggle' => 'tab' - %li - = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' + - @ldap_servers.each_with_index do |server, i| + %li{class: (:active if i.zero?)} + = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab' + - if gitlab_config.signin_enabled + %li + = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' .tab-content - %div#tab-ldap.tab-pane.active - = render partial: 'devise/sessions/new_ldap' - %div#tab-signin.tab-pane - = render partial: 'devise/sessions/new_base' + - @ldap_servers.each_with_index do |server, i| + %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero?)} + = render 'devise/sessions/new_ldap', provider: server['provider_name'] + - if gitlab_config.signin_enabled + %div#tab-signin.tab-pane + = render 'devise/sessions/new_base' - - elsif ldap_enabled? - = render partial: 'devise/sessions/new_ldap' - elsif gitlab_config.signin_enabled - = render partial: 'devise/sessions/new_base' + = render 'devise/sessions/new_base' - else %div No authentication methods configured. @@ -36,7 +38,6 @@ %span.light Did not receive confirmation email? = link_to "Send again", new_confirmation_path(resource_name) - - if extra_config.has_key?('sign_in_text') %hr = markdown(extra_config.sign_in_text) diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml index 0e03e116e7d..f0c34def145 100644 --- a/app/views/events/_commit.html.haml +++ b/app/views/events/_commit.html.haml @@ -1,5 +1,5 @@ %li.commit .commit-row-title - = link_to commit[:id][0..8], project_commit_path(project, commit[:id]), class: "commit_short_id", alt: '' + = link_to truncate_sha(commit[:id]), project_commit_path(project, commit[:id]), class: "commit_short_id", alt: '' = gfm event_commit_title(commit[:message]), project diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index 17228c430ca..2b63519edac 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -2,7 +2,7 @@ - event.commits.first(15).each do |commit| %p %strong= commit[:author][:name] - = link_to "(##{commit[:id][0...8]})", project_commit_path(event.project, id: commit[:id]) + = link_to "(##{truncate_sha(commit[:id])})", project_commit_path(event.project, id: commit[:id]) %i at = commit[:timestamp].to_time.to_s(:short) diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 1bca64c7d50..b912b5e092f 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -22,4 +22,4 @@ - if event.commits_count > 2 %span ... and #{event.commits_count - 2} more commits. = link_to project_compare_path(event.project, from: event.commit_from, to: event.commit_to) do - %strong Compare → #{event.commit_from[0..7]}...#{event.commit_to[0..7]} + %strong Compare → #{truncate_sha(event.commit_from)}...#{truncate_sha(event.commit_to)} diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index c8243ff782c..9b1d7d0416d 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -4,7 +4,7 @@ .form-group = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input input-mn-300", id: "groups_search" .form-group - = submit_tag 'Search', class: "btn btn-primary wide" + = button_tag 'Search', class: "btn btn-primary wide" .pull-right .dropdown.inline @@ -20,13 +20,13 @@ = link_to explore_groups_path(sort: nil) do Name = link_to explore_groups_path(sort: 'newest') do - Newest + = sort_title_recently_created = link_to explore_groups_path(sort: 'oldest') do - Oldest + = sort_title_oldest_created = link_to explore_groups_path(sort: 'recently_updated') do - Recently updated + = sort_title_recently_updated = link_to explore_groups_path(sort: 'last_updated') do - Last updated + = sort_title_oldest_updated %hr diff --git a/app/views/explore/projects/_project.html.haml b/app/views/explore/projects/_project.html.haml index 4bc79d0a8c7..ffbddbae4d6 100644 --- a/app/views/explore/projects/_project.html.haml +++ b/app/views/explore/projects/_project.html.haml @@ -6,6 +6,7 @@ - if current_page?(starred_explore_projects_path) %strong.pull-right + %i.fa.fa-star = pluralize project.star_count, 'star' .project-info diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index c8bf78385e8..02586077d8c 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -4,7 +4,7 @@ .form-group = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input input-mn-300", id: "projects_search" .form-group - = submit_tag 'Search', class: "btn btn-primary wide" + = button_tag 'Search', class: "btn btn-primary wide" .pull-right .dropdown.inline @@ -20,13 +20,13 @@ = link_to explore_projects_path(sort: nil) do Name = link_to explore_projects_path(sort: 'newest') do - Newest + = sort_title_recently_created = link_to explore_projects_path(sort: 'oldest') do - Oldest + = sort_title_oldest_created = link_to explore_projects_path(sort: 'recently_updated') do - Recently updated + = sort_title_recently_updated = link_to explore_projects_path(sort: 'last_updated') do - Last updated + = sort_title_oldest_updated %hr .public-projects diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml index d4b11405517..420f0693756 100644 --- a/app/views/explore/projects/starred.html.haml +++ b/app/views/explore/projects/starred.html.haml @@ -1,6 +1,6 @@ .explore-trending-block %p.lead - %i.fa.fa-comments-o + %i.fa.fa-star See most starred projects %hr .public-projects diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 0b15affe785..eb24fd65d9e 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -11,16 +11,7 @@ - if @group.errors.any? .alert.alert-danger %span= @group.errors.full_messages.first - .form-group - = f.label :name, class: 'control-label' do - Group name - .col-sm-10 - = f.text_field :name, placeholder: "Ex. OpenSource", class: "form-control left" - - .form-group.group-description-holder - = f.label :description, "Details", class: 'control-label' - .col-sm-10 - = f.text_area :description, maxlength: 250, class: "form-control js-gfm-input", rows: 4 + = render 'shared/group_form', f: f .form-group .col-sm-2 @@ -31,13 +22,7 @@ You can change your group avatar here - else You can upload a group avatar here - %a.choose-btn.btn.btn-small.js-choose-group-avatar-button - %i.fa.fa-paperclip - %span Choose File ... - - %span.file_name.js-avatar-filename File name... - = f.file_field :avatar, class: "js-group-avatar-input hidden" - .light The maximum file size allowed is 100KB. + = render 'shared/choose_group_avatar_button', f: f - if @group.avatar? %hr = link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder index f2005193f83..240001967f3 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -7,18 +7,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? @issues.each do |issue| - xml.entry do - xml.id project_issue_url(issue.project, issue) - xml.link :href => project_issue_url(issue.project, issue) - xml.title truncate(issue.title, :length => 80) - xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") - xml.media :thumbnail, :width => "40", :height => "40", :url => avatar_icon(issue.author_email) - xml.author do |author| - xml.name issue.author_name - xml.email issue.author_email - end - xml.summary issue.title - end + issue_to_atom(xml, issue) end end diff --git a/app/views/groups/members.html.haml b/app/views/groups/members.html.haml index ba554cd5ef1..d2ebcdab7e1 100644 --- a/app/views/groups/members.html.haml +++ b/app/views/groups/members.html.haml @@ -13,7 +13,7 @@ = form_tag members_group_path(@group), method: :get, class: 'form-inline member-search-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input input-mn-300' } - = submit_tag 'Search', class: 'btn' + = button_tag 'Search', class: 'btn' - if current_user && current_user.can?(:manage_group, @group) .pull-right diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 235e299343a..6e17cdaef6f 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -2,37 +2,18 @@ - if @group.errors.any? .alert.alert-danger %span= @group.errors.full_messages.first - .form-group - = f.label :name, class: 'control-label' do - Group name - .col-sm-10 - = f.text_field :name, placeholder: "Ex. OpenSource", class: "form-control", tabindex: 1, autofocus: true - .form-group.group-description-holder - = f.label :description, "Details", class: 'control-label' - .col-sm-10 - = f.text_area :description, maxlength: 250, class: "form-control js-gfm-input", rows: 4, tabindex: 2 + = render 'shared/group_form', f: f, autofocus: true .form-group.group-description-holder = f.label :avatar, "Group avatar", class: 'control-label' .col-sm-10 - %a.choose-btn.btn.btn-small.js-choose-group-avatar-button - %i.fa.fa-paperclip - %span Choose File ... - - %span.file_name.js-avatar-filename File name... - = f.file_field :avatar, class: "js-group-avatar-input hidden" - .light The maximum file size allowed is 100KB. + = render 'shared/choose_group_avatar_button', f: f .form-group .col-sm-2 .col-sm-10 - %ul - %li A group is a collection of several projects - %li Groups are private by default - %li Members of a group may only view projects they have permission to access - %li Group project URLs are prefixed with the group namespace - %li Existing projects may be moved into a group + = render 'shared/group_tips' .form-actions = f.submit 'Create group', class: "btn btn-create", tabindex: 3 diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index e07bb7d2fb7..e765ea8338d 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -1,28 +1,12 @@ xml.instruct! -xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do +xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlnsmedia" => "http://search.yahoo.com/mrss/" do xml.title "Group feed - #{@group.name}" - xml.link :href => group_path(@group, :atom), :rel => "self", :type => "application/atom+xml" - xml.link :href => group_path(@group), :rel => "alternate", :type => "text/html" + xml.link href: group_path(@group, :atom), rel: "self", type: "application/atom+xml" + xml.link href: group_path(@group), rel: "alternate", type: "text/html" xml.id projects_url xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| - if event.proper? - xml.entry do - event_link = event_feed_url(event) - event_title = event_feed_title(event) - - 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:%SZ") - xml.media :thumbnail, :width => "40", :height => "40", :url => avatar_icon(event.author_email) - xml.author do |author| - xml.name event.author_name - xml.email event.author_email - end - xml.summary event_title - end - end + event_to_atom(xml, event) end end diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index 903e093e5fc..7b8193abfdf 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -14,7 +14,7 @@ %br Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises. %br - Read more about GitLab at #{link_to "www.gitlab.com", "https://www.gitlab.com/", target: "_blank"}. + Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}. %hr @@ -34,7 +34,7 @@ %ul.well-list %li See our website for - = link_to "getting help", "https://www.gitlab.com/getting-help/" + = link_to 'getting help', promo_url + '/getting-help/' %li Use the = link_to 'search bar', '#', onclick: 'Shortcuts.focusSearch(event)' diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 5ab82122ad7..04f79846858 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -4,11 +4,20 @@ = hidden_field_tag :group_id, @group.try(:id) - if @project && @project.persisted? = hidden_field_tag :project_id, @project.id - = hidden_field_tag :search_code, true + + - if current_controller?(:issues) + = hidden_field_tag :scope, 'issues' + - elsif current_controller?(:merge_requests) + = hidden_field_tag :scope, 'merge_requests' + - elsif current_controller?(:wikis) + = hidden_field_tag :scope, 'wiki_blobs' + - else + = hidden_field_tag :search_code, true + - if @snippet || @snippets = hidden_field_tag :snippets, true = hidden_field_tag :repository_ref, @ref - = submit_tag 'Go' if ENV['RAILS_ENV'] == 'test' + = button_tag 'Go' if ENV['RAILS_ENV'] == 'test' .search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref } :javascript diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index 207ab22f4c7..744ecaa0297 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "Admin area" - %body{class: "#{app_theme} admin", :'data-page' => body_data_page} + %body{class: "#{app_theme} #{theme_type} admin", :'data-page' => body_data_page} = render "layouts/broadcast" = render "layouts/head_panel", title: "Admin area" %nav.main-nav.navbar-collapse.collapse diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 7d0819aa93e..e35a3915d0e 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "Dashboard" - %body{class: "#{app_theme} application", :'data-page' => body_data_page } + %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page } = render "layouts/broadcast" = render "layouts/head_panel", title: "Dashboard" %nav.main-nav.navbar-collapse.collapse diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 16df9c10fbb..e7d875173e6 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "Error" - %body{class: "#{app_theme} application"} + %body{class: "#{app_theme} #{theme_type} application"} = render "layouts/head_panel", title: "" if current_user .container.navless-container = render "layouts/flash" diff --git a/app/views/layouts/explore.html.haml b/app/views/layouts/explore.html.haml index d023846c5eb..9813d846542 100644 --- a/app/views/layouts/explore.html.haml +++ b/app/views/layouts/explore.html.haml @@ -2,7 +2,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: page_title - %body{class: "#{app_theme} application", :'data-page' => body_data_page} + %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} = render "layouts/broadcast" - if current_user = render "layouts/head_panel", title: page_title diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index f22fb236cb5..6ad285e2468 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: group_head_title - %body{class: "#{app_theme} application", :'data-page' => body_data_page} + %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} = render "layouts/broadcast" = render "layouts/head_panel", title: "group: #{@group.name}" %nav.main-nav.navbar-collapse.collapse diff --git a/app/views/layouts/navless.html.haml b/app/views/layouts/navless.html.haml index 2c5fffe384f..730f3d09277 100644 --- a/app/views/layouts/navless.html.haml +++ b/app/views/layouts/navless.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: @title - %body{class: "#{app_theme} application", :'data-page' => body_data_page} + %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} = render "layouts/broadcast" = render "layouts/head_panel", title: @title .container.navless-container diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index ab421d63f1a..da451961327 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -28,3 +28,4 @@ You're receiving this notification because you are a member of the #{link_to_unless @target_url, @project.name_with_namespace, project_url(@project)} project team. - if @target_url #{link_to "View it on GitLab", @target_url} + = email_action @target_url diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index 1d0ab84d26f..c57047bb1f3 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "Profile" - %body{class: "#{app_theme} profile", :'data-page' => body_data_page} + %body{class: "#{app_theme} #{theme_type} profile", :'data-page' => body_data_page} = render "layouts/broadcast" = render "layouts/head_panel", title: "Profile" %nav.main-nav.navbar-collapse.collapse diff --git a/app/views/layouts/project_settings.html.haml b/app/views/layouts/project_settings.html.haml index c8b8f4ba971..fd233452215 100644 --- a/app/views/layouts/project_settings.html.haml +++ b/app/views/layouts/project_settings.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: @project.name_with_namespace - %body{class: "#{app_theme} project", :'data-page' => body_data_page, :'data-project-id' => @project.id } + %body{class: "#{app_theme} #{theme_type} project", :'data-page' => body_data_page, :'data-project-id' => @project.id } = render "layouts/broadcast" = render "layouts/head_panel", title: project_title(@project) = render "layouts/init_auto_complete" diff --git a/app/views/layouts/projects.html.haml b/app/views/layouts/projects.html.haml index 8ad2f165946..fb64c40e8bb 100644 --- a/app/views/layouts/projects.html.haml +++ b/app/views/layouts/projects.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: project_head_title - %body{class: "#{app_theme} project", :'data-page' => body_data_page, :'data-project-id' => @project.id } + %body{class: "#{app_theme} #{theme_type} project", :'data-page' => body_data_page, :'data-project-id' => @project.id } = render "layouts/broadcast" = render "layouts/head_panel", title: project_title(@project) = render "layouts/init_auto_complete" diff --git a/app/views/layouts/public_group.html.haml b/app/views/layouts/public_group.html.haml index a289b784725..b97b0cf92cb 100644 --- a/app/views/layouts/public_group.html.haml +++ b/app/views/layouts/public_group.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: group_head_title - %body{class: "#{app_theme} application", :'data-page' => body_data_page} + %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} = render "layouts/broadcast" = render "layouts/public_head_panel", title: "group: #{@group.name}" %nav.main-nav.navbar-collapse.collapse diff --git a/app/views/layouts/public_projects.html.haml b/app/views/layouts/public_projects.html.haml index 2a9230244f8..4819b9b135f 100644 --- a/app/views/layouts/public_projects.html.haml +++ b/app/views/layouts/public_projects.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: @project.name_with_namespace - %body{class: "#{app_theme} application", :'data-page' => body_data_page} + %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} = render "layouts/broadcast" = render "layouts/public_head_panel", title: project_title(@project) %nav.main-nav.navbar-collapse.collapse diff --git a/app/views/layouts/public_users.html.haml b/app/views/layouts/public_users.html.haml index 4aa258fea0d..fdba0f099a9 100644 --- a/app/views/layouts/public_users.html.haml +++ b/app/views/layouts/public_users.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: @title - %body{class: "#{app_theme} application", :'data-page' => body_data_page} + %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} = render "layouts/broadcast" = render "layouts/public_head_panel", title: @title .container.navless-container diff --git a/app/views/layouts/search.html.haml b/app/views/layouts/search.html.haml index 084ff7ec830..6d001e7ee1c 100644 --- a/app/views/layouts/search.html.haml +++ b/app/views/layouts/search.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "Search" - %body{class: "#{app_theme} application", :'data-page' => body_data_page} + %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} = render "layouts/broadcast" = render "layouts/head_panel", title: "Search" .container.navless-container diff --git a/app/views/notify/new_ssh_key_email.html.haml b/app/views/notify/new_ssh_key_email.html.haml index deb0822d8f2..63b0cbbd205 100644 --- a/app/views/notify/new_ssh_key_email.html.haml +++ b/app/views/notify/new_ssh_key_email.html.haml @@ -6,5 +6,5 @@ title: %code= @key.title %p - If this key was added in error, you can remove it here: + If this key was added in error, you can remove it under = link_to "SSH Keys", profile_keys_url diff --git a/app/views/notify/new_ssh_key_email.text.erb b/app/views/notify/new_ssh_key_email.text.erb index 5f0080c2b76..05b551c89a0 100644 --- a/app/views/notify/new_ssh_key_email.text.erb +++ b/app/views/notify/new_ssh_key_email.text.erb @@ -2,6 +2,6 @@ Hi <%= @user.name %>! A new public key was added to your account: -title.................. <%= @key.title %> +Title: <%= @key.title %> -If this key was added in error, you can remove it here: <%= profile_keys_url %> +If this key was added in error, you can remove it at <%= profile_keys_url %> diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index f84de4430cc..a044fad8fa3 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -31,12 +31,12 @@ .clearfix %hr - %p - You can also specify notification level per group or per project - %br - By default all projects and groups uses notification level set above .row.all-notifications .col-md-6 + %p + You can also specify notification level per group or per project. + %br + By default all projects and groups uses notification level set above. %h4 Groups: %ul.bordered-list - @group_members.each do |users_group| @@ -44,6 +44,10 @@ = render 'settings', type: 'group', membership: users_group, notification: notification .col-md-6 + %p + To specify notification level per project of a group you belong to, + %br + you need to be a member of the project itself, not only its group. %h4 Projects: %ul.bordered-list - @project_members.each do |project_member| diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 2a7d317aa3e..425200ff523 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -24,7 +24,7 @@ .form-group = f.label :password, 'New password', class: 'control-label' .col-sm-10 - = f.password_field :password, required: true, class: 'form-control' + = f.password_field :password, required: true, class: 'form-control', id: 'user_password_profile' .form-group = f.label :password_confirmation, class: 'control-label' .col-sm-10 diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml index aef7348fd20..42d2d0db29c 100644 --- a/app/views/profiles/passwords/new.html.haml +++ b/app/views/profiles/passwords/new.html.haml @@ -16,7 +16,7 @@ .col-sm-10= f.password_field :current_password, required: true, class: 'form-control' .form-group = f.label :password, class: 'control-label' - .col-sm-10= f.password_field :password, required: true, class: 'form-control' + .col-sm-10= f.password_field :password, required: true, class: 'form-control', id: 'user_password_profile' .form-group = f.label :password_confirmation, class: 'control-label' .col-sm-10 diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index d6b52f86154..640104fdad1 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -83,7 +83,7 @@ %span.file_name.js-avatar-filename File name... = f.file_field :avatar, class: "js-user-avatar-input hidden" - .light The maximum file size allowed is 100KB. + .light The maximum file size allowed is 200KB. - if @user.avatar? %hr = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" diff --git a/app/views/profiles/update.js.erb b/app/views/profiles/update.js.erb index 04b5cf4827d..e664ac2a52a 100644 --- a/app/views/profiles/update.js.erb +++ b/app/views/profiles/update.js.erb @@ -1,6 +1,6 @@ // Remove body class for any previous theme, re-add current one -$('body').removeClass('ui_basic ui_mars ui_modern ui_gray ui_color') -$('body').addClass('<%= app_theme %>') +$('body').removeClass('ui_basic ui_mars ui_modern ui_gray ui_color light_theme dark_theme') +$('body').addClass('<%= app_theme %> <%= theme_type %>') // Re-render the header to reflect the new theme $('header').html('<%= escape_javascript(render("layouts/head_panel", title: "Profile")) %>') diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml new file mode 100644 index 00000000000..fd8320adb8d --- /dev/null +++ b/app/views/projects/_commit_button.html.haml @@ -0,0 +1,9 @@ +.form-actions + .commit-button-annotation + = button_tag 'Commit Changes', + class: 'btn commit-btn js-commit-button btn-create' + .message + to branch + %strong= ref + = link_to 'Cancel', cancel_path, + class: 'btn btn-cancel', data: {confirm: leave_edit_message} diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 672a91e0eef..30d063c7a36 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -16,11 +16,11 @@ - unless @project.empty_repo? .fork-buttons - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace - - if current_user.already_forked?(@project) + - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 = link_to project_path(current_user.fork_of(@project)), title: 'Go to my fork' do = link_to_toggle_fork - else - = link_to fork_project_path(@project), title: "Fork project", method: "POST" do + = link_to new_project_fork_path(@project), title: "Fork project" do = link_to_toggle_fork .star-buttons @@ -31,7 +31,7 @@ - else = link_to_toggle_star('You must sign in to star a project.', false, false) - .project-home-row + .project-home-row.hidden-xs - if current_user && !empty_repo .project-home-dropdown = render "dropdown" diff --git a/app/views/projects/_issuable_filter.html.haml b/app/views/projects/_issuable_filter.html.haml new file mode 100644 index 00000000000..b3e5efd938f --- /dev/null +++ b/app/views/projects/_issuable_filter.html.haml @@ -0,0 +1,72 @@ +.issues-filters + .dropdown.inline + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.fa.fa-user + %span.light assignee: + - if @assignee.present? + %strong= @assignee.name + - elsif params[:assignee_id] == "0" + Unassigned + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to project_filter_path(assignee_id: nil) do + Any + = link_to project_filter_path(assignee_id: 0) do + Unassigned + - @assignees.sort_by(&:name).each do |user| + %li + = link_to project_filter_path(assignee_id: user.id) do + = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' + = user.name + + .dropdown.inline.prepend-left-10 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.fa.fa-user + %span.light author: + - if @author.present? + %strong= @author.name + - elsif params[:author_id] == "0" + Unassigned + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to project_filter_path(author_id: nil) do + Any + = link_to project_filter_path(author_id: 0) do + Unassigned + - @authors.sort_by(&:name).each do |user| + %li + = link_to project_filter_path(author_id: user.id) do + = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' + = user.name + + .dropdown.inline.prepend-left-10 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.fa.fa-clock-o + %span.light milestone: + - if @milestone.present? + %strong= @milestone.title + - elsif params[:milestone_id] == "0" + None (backlog) + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to project_filter_path(milestone_id: nil) do + Any + = link_to project_filter_path(milestone_id: 0) do + None (backlog) + - project_active_milestones.each do |milestone| + %li + = link_to project_filter_path(milestone_id: milestone.id) do + %strong= milestone.title + %small.light= milestone.expires_at + + .pull-right + = render 'shared/sort_dropdown' diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml index 6cdfab933b4..b02f52a5aff 100644 --- a/app/views/projects/_issuable_form.html.haml +++ b/app/views/projects/_issuable_form.html.haml @@ -14,17 +14,20 @@ .form-group.issuable-description = f.label :description, 'Description', class: 'control-label' .col-sm-10 - = render 'projects/zen', f: f, attr: :description, - classes: 'description form-control' - .col-sm-12.hint - .pull-left - Parsed with - #{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}. - .pull-right - Attach images (JPG, PNG, GIF) by dragging & dropping - or #{link_to 'selecting them', '#', class: 'markdown-selector' }. - .clearfix - .error-alert + + = render layout: 'projects/md_preview' do + = render 'projects/zen', f: f, attr: :description, + classes: 'description form-control' + .col-sm-12.hint + .pull-left + Parsed with + #{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}. + .pull-right + Attach images (JPG, PNG, GIF) by dragging & dropping + or #{link_to 'selecting them', '#', class: 'markdown-selector' }. + + .clearfix + .error-alert %hr .form-group .issue-assignee @@ -49,7 +52,7 @@ - else %span.light No open milestones available. - = link_to 'Create new milestone', new_project_milestone_path(issuable.project) + = link_to 'Create new milestone', new_project_milestone_path(issuable.project), target: :blank .form-group = f.label :label_ids, class: 'control-label' do %i.icon-tag @@ -61,7 +64,7 @@ - else %span.light No labels yet. - = link_to 'Create new label', new_project_label_path(issuable.project) + = link_to 'Create new label', new_project_label_path(issuable.project), target: :blank .form-actions - if issuable.new_record? diff --git a/app/views/projects/_issues_nav.html.haml b/app/views/projects/_issues_nav.html.haml new file mode 100644 index 00000000000..18628eb6207 --- /dev/null +++ b/app/views/projects/_issues_nav.html.haml @@ -0,0 +1,55 @@ +%ul.nav.nav-tabs + - if project_nav_tab? :issues + = nav_link(controller: :issues) do + = link_to project_issues_path(@project), class: "tab" do + Issues + - if project_nav_tab? :merge_requests + = nav_link(controller: :merge_requests) do + = link_to project_merge_requests_path(@project), class: "tab" do + Merge Requests + = nav_link(controller: :milestones) do + = link_to 'Milestones', project_milestones_path(@project), class: "tab" + = nav_link(controller: :labels) do + = link_to 'Labels', project_labels_path(@project), class: "tab" + + - if current_controller?(:milestones) + %li.pull-right + %button.btn.btn-default.sidebar-expand-button + %i.icon.fa.fa-list + + - if current_controller?(:issues) + - if current_user + %li.hidden-xs + = link_to project_issues_path(@project, :atom, { private_token: current_user.private_token }) do + %i.fa.fa-rss + + %li.pull-right + .pull-right + %button.btn.btn-default.sidebar-expand-button + %i.icon.fa.fa-list + + .pull-left + = form_tag project_issues_path(@project), method: :get, id: "issue_search_form", class: 'pull-left issue-search-form' do + .append-right-10.hidden-xs.hidden-sm + = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' } + = hidden_field_tag :state, params['state'] + = hidden_field_tag :scope, params['scope'] + = hidden_field_tag :assignee_id, params['assignee_id'] + = hidden_field_tag :milestone_id, params['milestone_id'] + = hidden_field_tag :label_id, params['label_id'] + + - if can? current_user, :write_issue, @project + = link_to new_project_issue_path(@project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do + %i.fa.fa-plus + New Issue + + - if current_controller?(:merge_requests) + %li.pull-right + .pull-right + %button.btn.btn-default.sidebar-expand-button + %i.icon.fa.fa-list + + - if can? current_user, :write_merge_request, @project + = link_to new_project_merge_request_path(@project), class: "btn btn-new pull-left", title: "New Merge Request" do + %i.fa.fa-plus + New Merge Request diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml new file mode 100644 index 00000000000..cb75149434f --- /dev/null +++ b/app/views/projects/_md_preview.html.haml @@ -0,0 +1,13 @@ +%ul.nav.nav-tabs + %li.active + = link_to '#md-write-holder', class: 'js-md-write-button' do + Write + %li + = link_to '#md-preview-holder', class: 'js-md-preview-button', + data: { url: markdown_preview_project_path(@project) } do + Preview +%div + .md-write-holder + = yield + .md-preview-holder.hide + .js-md-preview diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index e5cde488c3c..bdf02c6285d 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -15,7 +15,7 @@ %tr %td.blame-commit %span.commit - = link_to commit.short_id(8), project_commit_path(@project, commit), class: "commit_short_id" + = link_to commit.short_id, project_commit_path(@project, commit), class: "commit_short_id" = commit_author_link(commit, avatar: true, size: 16) diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index 64c19a57803..812d88a8730 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -23,5 +23,6 @@ tree_join(@commit.sha, @path)), class: 'btn btn-small' - if allowed_tree_edit? - = link_to '#modal-remove-blob', class: "remove-blob btn btn-small btn-remove", "data-toggle" => "modal" do + = button_tag class: 'remove-blob btn btn-small btn-remove', + 'data-toggle' => 'modal', 'data-target' => '#modal-remove-blob' do Remove diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml index da84dc4b6cf..c5568315cb1 100644 --- a/app/views/projects/blob/_remove.html.haml +++ b/app/views/projects/blob/_remove.html.haml @@ -15,7 +15,7 @@ .form-group .col-sm-2 .col-sm-10 - = submit_tag 'Remove file', class: 'btn btn-remove btn-remove-file' + = button_tag 'Remove file', class: 'btn btn-remove btn-remove-file' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" :javascript diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 9f2b1b59292..d2aefd815a1 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -20,9 +20,9 @@ = link_to project_branches_path(sort: nil) do Name = link_to project_branches_path(sort: 'recently_updated') do - Recently updated + = sort_title_recently_updated = link_to project_branches_path(sort: 'last_updated') do - Last updated + = sort_title_oldest_updated %hr - unless @branches.empty? %ul.bordered-list.top-list.all-branches diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index f5a530d95f2..a6623240da1 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -15,7 +15,7 @@ .col-sm-10 = text_field_tag :ref, params[:ref], placeholder: 'existing branch name, tag or commit SHA', required: true, tabindex: 2, class: 'form-control' .form-actions - = submit_tag 'Create branch', class: 'btn btn-create', tabindex: 3 + = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel' :javascript diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 0b6b6af4f90..e149f017f84 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -35,7 +35,7 @@ .commit-info-row %span.cgray= pluralize(@commit.parents.count, "parent") - @commit.parents.each do |parent| - = link_to parent.id[0...10], project_commit_path(@project, parent) + = link_to parent.short_id, project_commit_path(@project, parent) - if @branches.any? .commit-info-row diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 68852ba973f..1eb17f760dc 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -1,6 +1,6 @@ %li.commit.js-toggle-container .commit-row-title - = link_to commit.short_id(8), project_commit_path(project, commit), class: "commit_short_id" + = link_to commit.short_id, project_commit_path(project, commit), class: "commit_short_id" %span.str-truncated = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message" diff --git a/app/views/projects/commits/_inline_commit.html.haml b/app/views/projects/commits/_inline_commit.html.haml index b36369b4285..574599aa2d2 100644 --- a/app/views/projects/commits/_inline_commit.html.haml +++ b/app/views/projects/commits/_inline_commit.html.haml @@ -1,6 +1,6 @@ %li.commit.inline-commit .commit-row-title - = link_to commit.short_id(8), project_commit_path(project, commit), class: "commit_short_id" + = link_to commit.short_id, project_commit_path(project, commit), class: "commit_short_id" %span.str-truncated = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message" diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index da6157cf1b6..cb0a3747f7d 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -12,7 +12,7 @@ %span.input-group-addon to = text_field_tag :to, params[:to], class: "form-control" - = submit_tag "Compare", class: "btn btn-create commits-compare-btn" + = button_tag "Compare", class: "btn btn-create commits-compare-btn" - if compare_to_mr_button? = link_to compare_mr_path, class: 'prepend-left-10 btn' do %strong Make a merge request diff --git a/app/views/projects/create.js.haml b/app/views/projects/create.js.haml deleted file mode 100644 index 89710d3a09a..00000000000 --- a/app/views/projects/create.js.haml +++ /dev/null @@ -1,13 +0,0 @@ -- if @project.saved? - - if @project.import? - :plain - location.href = "#{import_project_path(@project)}"; - - else - :plain - location.href = "#{project_path(@project)}"; -- else - :plain - $(".project-edit-errors").html("#{escape_javascript(render('errors'))}"); - $('.project-submit').enable(); - $('.save-project-loader').hide(); - $('.project-edit-container').show(); diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index c415ae2ddc8..bf7770ceedf 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -10,7 +10,10 @@ - if @commit.parent_ids.present? = view_file_btn(@commit.parent_id, diff_file, project) - else - %span= diff_file.new_path + - if diff_file.renamed_file + %span= "#{diff_file.old_path} renamed to #{diff_file.new_path}" + - else + %span= diff_file.new_path - if diff_file.mode_changed? %span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 5ee5641b069..b85cf7d8d37 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -13,7 +13,7 @@ = f.label :name, class: 'control-label' do Project name .col-sm-10 - = f.text_field :name, placeholder: "Example Project", class: "form-control" + = f.text_field :name, placeholder: "Example Project", class: "form-control", id: "project_name_edit" .form-group @@ -86,101 +86,98 @@ - .danger-settings.js-toggle-container - .centered-light-block - %h3 - %i.fa.fa-exclamation-triangle - Dangerous settings - - %p Project settings below may result in data loss! - = link_to '#', class: 'btn js-toggle-button' do - Show them to me - %i.fa.fa-chevron-down - - .js-toggle-content.hide - - if can? current_user, :archive_project, @project - .panel.panel-default.panel.panel-warning + .danger-settings + - if can? current_user, :archive_project, @project + - if @project.archived? + .panel.panel-success .panel-heading - - if @project.archived? - Unarchive project - - else - Archive project + Unarchive project .panel-body - - if @project.archived? - %p - Unarchiving the project will mark its repository as active. - %br - The project can be committed to. - %br - %strong Once active this project shows up in the search and on the dashboard. - = link_to 'Unarchive', unarchive_project_path(@project), - data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, - method: :post, class: "btn btn-remove" - - else - %p - Archiving the project will mark its repository as read-only. - %br - It is hidden from the dashboard and doesn't show up in searches. - %br - %strong Archived projects cannot be committed to! - = link_to 'Archive', archive_project_path(@project), - data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, - method: :post, class: "btn btn-warning" + %p + Unarchiving the project will mark its repository as active. + %br + The project can be committed to. + %br + %strong Once active this project shows up in the search and on the dashboard. + = link_to 'Unarchive', unarchive_project_path(@project), + data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, + method: :post, class: "btn btn-success" - else - .nothing-here-block Only the project owner can archive a project - - .panel.panel-default.panel.panel-warning - .panel-heading Rename repository + .panel.panel-warning + .panel-heading + Archive project + .panel-body + %p + Archiving the project will mark its repository as read-only. + %br + It is hidden from the dashboard and doesn't show up in searches. + %br + %strong Archived projects cannot be committed to! + = link_to 'Archive', archive_project_path(@project), + data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, + method: :post, class: "btn btn-warning" + - else + .nothing-here-block Only the project owner can archive a project + + .panel.panel-default.panel.panel-warning + .panel-heading Rename repository + .errors-holder + .panel-body + = form_for(@project, html: { class: 'form-horizontal' }) do |f| + .form-group.project_name_holder + = f.label :name, class: 'control-label' do + Project name + .col-sm-9 + .form-group + = f.text_field :name, placeholder: "Example Project", class: "form-control" + .form-group + = f.label :path, class: 'control-label' do + %span Path + .col-sm-9 + .form-group + .input-group + = f.text_field :path, class: 'form-control' + %span.input-group-addon .git + %ul + %li Be careful. Renaming a project's repository can have unintended side effects. + %li You will need to update your local repositories to point to the new location. + .form-actions + = f.submit 'Rename', class: "btn btn-warning" + + - if can?(current_user, :change_namespace, @project) + .panel.panel-default.panel.panel-danger + .panel-heading Transfer project .errors-holder .panel-body - = form_for(@project, html: { class: 'form-horizontal' }) do |f| + = form_for(@project, url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f| .form-group - = f.label :path, class: 'control-label' do - %span Path - .col-sm-9 + = f.label :namespace_id, class: 'control-label' do + %span Namespace + .col-sm-10 .form-group - .input-group - = f.text_field :path, class: 'form-control' - %span.input-group-addon .git + = f.select :namespace_id, namespaces_options(@project.namespace_id), { prompt: 'Choose a project namespace' }, { class: 'select2' } %ul - %li Be careful. Renaming a project's repository can have unintended side effects. + %li Be careful. Changing the project's namespace can have unintended side effects. + %li You can only transfer the project to namespaces you manage. %li You will need to update your local repositories to point to the new location. .form-actions - = f.submit 'Rename', class: "btn btn-warning" + = f.submit 'Transfer', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } + - else + .nothing-here-block Only the project owner can transfer a project - - if can?(current_user, :change_namespace, @project) - .panel.panel-default.panel.panel-danger - .panel-heading Transfer project - .errors-holder - .panel-body - = form_for(@project, url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f| - .form-group - = f.label :namespace_id, class: 'control-label' do - %span Namespace - .col-sm-10 - .form-group - = f.select :namespace_id, namespaces_options(@project.namespace_id), { prompt: 'Choose a project namespace' }, { class: 'select2' } - %ul - %li Be careful. Changing the project's namespace can have unintended side effects. - %li You can only transfer the project to namespaces you manage. - %li You will need to update your local repositories to point to the new location. - .form-actions - = f.submit 'Transfer', class: "btn btn-remove" - - else - .nothing-here-block Only the project owner can transfer a project - - - if can?(current_user, :remove_project, @project) - .panel.panel-default.panel.panel-danger - .panel-heading Remove project - .panel-body + - if can?(current_user, :remove_project, @project) + .panel.panel-default.panel.panel-danger + .panel-heading Remove project + .panel-body + = form_tag(project_path(@project), method: :delete, html: { class: 'form-horizontal'}) do %p Removing the project will delete its repository and all related resources including issues, merge requests etc. %br %strong Removed projects cannot be restored! - = link_to 'Remove project', @project, data: { confirm: remove_project_message(@project) }, method: :delete, class: "btn btn-remove" - - else - .nothing-here-block Only project owner can remove a project + = link_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } + - else + .nothing-here-block Only project owner can remove a project .save-project-loader.hide .center @@ -188,3 +185,6 @@ %i.fa.fa-spinner.fa-spin Saving project. %p Please wait a moment, this page will automatically refresh when ready. + + += render 'shared/confirm_modal', phrase: @project.path diff --git a/app/views/projects/edit_tree/show.html.haml b/app/views/projects/edit_tree/show.html.haml index a863f7420a8..5ccde05063e 100644 --- a/app/views/projects/edit_tree/show.html.haml +++ b/app/views/projects/edit_tree/show.html.haml @@ -23,16 +23,11 @@ %i.fa.fa-spinner.fa-spin = render 'shared/commit_message_container', params: params, placeholder: "Update #{@blob.name}" - .form-actions - = hidden_field_tag 'last_commit', @last_commit - = hidden_field_tag 'content', '', id: "file-content" - = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] - .commit-button-annotation - = button_tag "Commit changes", class: 'btn commit-btn js-commit-button btn-primary' - .message - to branch - %strong= @ref - = link_to "Cancel", @after_edit_path, class: "btn btn-cancel", data: { confirm: leave_edit_message} + = hidden_field_tag 'last_commit', @last_commit + = hidden_field_tag 'content', '', id: "file-content" + = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] + = render 'projects/commit_button', ref: @ref, + cancel_path: @after_edit_path :javascript ace.config.set("modePath", gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}/ace") diff --git a/app/views/projects/fork.html.haml b/app/views/projects/fork.html.haml deleted file mode 100644 index d8f5c7b98d6..00000000000 --- a/app/views/projects/fork.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -.alert.alert-danger.alert-block - %h4 - %i.fa.fa-code-fork - Fork Error! - %p - You tried to fork - = link_to_project @project - but it failed for the following reason: - - - - if @forked_project && @forked_project.errors.any? - %p - – - = @forked_project.errors.full_messages.first - - %p - = link_to fork_project_path(@project), title: "Fork", class: "btn", method: "POST" do - %i.fa.fa-code-fork - Try to Fork again diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml new file mode 100644 index 00000000000..76d3aa5bf00 --- /dev/null +++ b/app/views/projects/forks/error.html.haml @@ -0,0 +1,20 @@ +- if @forked_project && !@forked_project.saved? + .alert.alert-danger.alert-block + %h4 + %i.fa.fa-code-fork + Fork Error! + %p + You tried to fork + = link_to_project @project + but it failed for the following reason: + + + - if @forked_project && @forked_project.errors.any? + %p + – + = @forked_project.errors.full_messages.first + + %p + = link_to new_project_fork_path(@project), title: "Fork", class: "btn" do + %i.fa.fa-code-fork + Try to Fork again diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml new file mode 100644 index 00000000000..54f2cef023b --- /dev/null +++ b/app/views/projects/forks/new.html.haml @@ -0,0 +1,38 @@ +%h3.page-title Fork project +%p.lead Select namespace where to fork this project +%hr + +.fork-namespaces + - @namespaces.in_groups_of(6, false) do |group| + .row + - group.each do |namespace| + .col-md-2.col-sm-3 + - if fork = namespace.find_fork_of(@project) + .thumbnail.fork-exists-thumbnail + = link_to project_path(fork), title: "Visit project fork", class: 'has_tooltip' do + = image_tag namespace_icon(namespace, 200) + .caption + %h4=namespace.human_name + %p + = namespace.path + - else + .thumbnail.fork-thumbnail + = link_to project_fork_path(@project, namespace_id: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do + = image_tag namespace_icon(namespace, 200) + .caption + %h4=namespace.human_name + %p + = namespace.path + + %p.light + Fork is a copy of a project repository. + %br + Forking a repository allows you to do changes without affecting the original project. + +.save-project-loader.hide + .center + %h2 + %i.fa.fa-spinner.fa-spin + Forking repository + %p Please wait a moment, this page will automatically refresh when ready. + diff --git a/app/views/projects/import.html.haml b/app/views/projects/import.html.haml deleted file mode 100644 index 1f7fd26c646..00000000000 --- a/app/views/projects/import.html.haml +++ /dev/null @@ -1,30 +0,0 @@ -- if @project.import_in_progress? - .save-project-loader - .center - %h2 - %i.fa.fa-spinner.fa-spin - Import in progress. - %p.monospace git clone --bare #{hidden_pass_url(@project.import_url)} - %p Please wait while we import the repository for you. Refresh at will. - :javascript - new ProjectImport(); - -- elsif @project.import_failed? - .save-project-loader - .center - %h2 - Import failed. Retry? - %hr - - if can?(current_user, :admin_project, @project) - = form_for @project, url: retry_import_project_path(@project), method: :put, html: { class: 'form-horizontal' } do |f| - .form-group.import-url-data - = f.label :import_url, class: 'control-label' do - %span Import existing repo - .col-sm-10 - = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' - .bs-callout.bs-callout-info - This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. - %br - The import will time out after 4 minutes. For big repositories, use a clone/push combination. - .form-actions - = f.submit 'Retry import', class: "btn btn-create", tabindex: 4 diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml new file mode 100644 index 00000000000..6c3083e49f5 --- /dev/null +++ b/app/views/projects/imports/new.html.haml @@ -0,0 +1,21 @@ +%h3.page-title + - if @project.import_failed? + Import failed. Retry? + - else + Import repository + +%hr + += form_for @project, url: project_import_path(@project), method: :post, html: { class: 'form-horizontal' } do |f| + .form-group.import-url-data + = f.label :import_url, class: 'control-label' do + %span Import existing git repo + .col-sm-10 + = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' + .bs-callout.bs-callout-info + This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. + %br + The import will time out after 4 minutes. For big repositories, use a clone/push combination. + For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"} + .form-actions + = f.submit 'Start import', class: "btn btn-create", tabindex: 4 diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml new file mode 100644 index 00000000000..2d1fdafed24 --- /dev/null +++ b/app/views/projects/imports/show.html.haml @@ -0,0 +1,9 @@ +.save-project-loader + .center + %h2 + %i.fa.fa-spinner.fa-spin + Import in progress. + %p.monospace git clone --bare #{hidden_pass_url(@project.import_url)} + %p Please wait while we import the repository for you. Refresh at will. + :javascript + new ProjectImport(); diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml deleted file mode 100644 index 1d2f3ed8118..00000000000 --- a/app/views/projects/issues/_head.html.haml +++ /dev/null @@ -1,36 +0,0 @@ -%ul.nav.nav-tabs - = nav_link(controller: :issues) do - = link_to project_issues_path(@project), class: "tab" do - Browse Issues - = nav_link(controller: :milestones) do - = link_to 'Milestones', project_milestones_path(@project), class: "tab" - = nav_link(controller: :labels) do - = link_to 'Labels', project_labels_path(@project), class: "tab" - - - if current_controller?(:milestones) - %li.pull-right - %button.btn.btn-default.sidebar-expand-button - %i.icon.fa.fa-list - - - if current_controller?(:issues) - - if current_user - %li - = link_to project_issues_path(@project, :atom, { private_token: current_user.private_token }) do - %i.fa.fa-rss - - %li.pull-right - .pull-right - %button.btn.btn-default.sidebar-expand-button - %i.icon.fa.fa-list - = form_tag project_issues_path(@project), method: :get, id: "issue_search_form", class: 'pull-left issue-search-form' do - .append-right-10.hidden-xs.hidden-sm - = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' } - = hidden_field_tag :state, params['state'] - = hidden_field_tag :scope, params['scope'] - = hidden_field_tag :assignee_id, params['assignee_id'] - = hidden_field_tag :milestone_id, params['milestone_id'] - = hidden_field_tag :label_id, params['label_id'] - - if can? current_user, :write_issue, @project - = link_to new_project_issue_path(@project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do - %i.fa.fa-plus - New Issue diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index b125706781c..dc6510be858 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -4,7 +4,6 @@ = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue", disabled: !can?(current_user, :modify_issue, issue) .issue-title - %span.light= "##{issue.iid}" %span.str-truncated = link_to_gfm issue.title, project_issue_path(issue.project, issue), class: "row_title" - if issue.closed? @@ -12,10 +11,9 @@ CLOSED .issue-info + %span.light= "##{issue.iid}" - if issue.assignee assigned to #{link_to_member(@project, issue.assignee)} - - else - unassigned - if issue.votes_count > 0 = render 'votes/votes_inline', votable: issue - if issue.notes.any? @@ -30,7 +28,7 @@ %span.task-status = issue.task_status - .pull-right + .pull-right.issue-updated-at %small updated #{time_ago_with_tooltip(issue.updated_at, 'bottom', 'issue_update_ago')} .issue-labels diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index 8c3f0823386..648f459dc9e 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -19,6 +19,7 @@ = hidden_field_tag :issue_context = f.submit class: 'btn' - elsif issue.milestone - = link_to issue.milestone.title, project_milestone_path + = link_to project_milestone_path(@project, @issue.milestone) do + = @issue.milestone.title - else None diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index 0bff8bdbead..15c84c7ced2 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -1,55 +1,7 @@ .append-bottom-10 .check-all-holder = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left" - .issues-filters - .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-user - %span.light assignee: - - if @assignee.present? - %strong= @assignee.name - - elsif params[:assignee_id] == "0" - Unassigned - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to project_filter_path(assignee_id: nil) do - Any - = link_to project_filter_path(assignee_id: 0) do - Unassigned - - @assignees.sort_by(&:name).each do |user| - %li - = link_to project_filter_path(assignee_id: user.id) do - = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' - = user.name - - .dropdown.inline.prepend-left-10 - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-clock-o - %span.light milestone: - - if @milestone.present? - %strong= @milestone.title - - elsif params[:milestone_id] == "0" - None (backlog) - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to project_filter_path(milestone_id: nil) do - Any - = link_to project_filter_path(milestone_id: 0) do - None (backlog) - - project_active_milestones.each do |milestone| - %li - = link_to project_filter_path(milestone_id: milestone.id) do - %strong= milestone.title - %small.light= milestone.expires_at - - .pull-right - = render 'shared/sort_dropdown' + = render 'projects/issuable_filter' .clearfix .issues_bulk_update.hide diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder index 012ba235951..61e651da932 100644 --- a/app/views/projects/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder @@ -7,17 +7,6 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? @issues.each do |issue| - xml.entry do - xml.id project_issue_url(@project, issue) - xml.link :href => project_issue_url(@project, issue) - xml.title truncate(issue.title, :length => 80) - xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") - xml.media :thumbnail, :width => "40", :height => "40", :url => avatar_icon(issue.author_email) - xml.author do |author| - xml.name issue.author_name - xml.email issue.author_email - end - xml.summary issue.title - end + issue_to_atom(xml, issue) end end diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 4ec362b3063..8db6241f21f 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -1,4 +1,4 @@ -= render "head" += render "projects/issues_nav" .row .fixed.fixed.sidebar-expand-button.hidden-lg.hidden-md.hidden-xs %i.fa.fa-list.fa-2x diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index e1849b3f8b8..01a1fabda26 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -38,8 +38,12 @@ - else Open + .cross-project-ref + %i.fa.fa-link.has_tooltip{:"data-original-title" => 'Cross-project reference'} + = cross_project_reference(@project, @issue) + .creator - Created by #{link_to_member(@project, @issue.author)} #{time_ago_with_tooltip(@issue.created_at)} + Created by #{link_to_member(@project, @issue.author)} #{issue_timestamp(@issue)} %h4.title = gfm escape_once(@issue.title) @@ -62,7 +66,8 @@ = link_to 'Close Issue', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue" .participants - %cite.cgray #{@issue.participants.count} participants + %cite.cgray + = pluralize(@issue.participants.count, 'participant') - @issue.participants.each do |participant| = link_to_member(@project, participant, name: false, size: 24) diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 06568278de8..c7c17c7797e 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,4 +1,4 @@ -= render "projects/issues/head" += render "projects/issues_nav" - if can? current_user, :admin_label, @project = link_to new_project_label_path(@project), class: "pull-right btn btn-new" do diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 1ee2e1bdae8..dedb060a231 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,13 +1,12 @@ %li{ class: mr_css_classes(merge_request) } .merge-request-title - %span.light= "##{merge_request.iid}" = link_to_gfm truncate(merge_request.title, length: 80), project_merge_request_path(merge_request.target_project, merge_request), class: "row_title" - if merge_request.merged? %small.pull-right %i.fa.fa-check MERGED - else - %span.pull-right + %span.pull-right.hidden-xs - if merge_request.for_fork? %span.light #{merge_request.source_project_namespace}: @@ -15,6 +14,7 @@ %i.fa.fa-angle-right.light = merge_request.target_branch .merge-request-info + %span.light= "##{merge_request.iid}" - if merge_request.author authored by #{link_to_member(merge_request.source_project, merge_request.author)} - if merge_request.votes_count > 0 @@ -31,7 +31,7 @@ %span.task-status = merge_request.task_status - .pull-right + .pull-right.hidden-xs %small updated #{time_ago_with_tooltip(merge_request.updated_at, 'bottom', 'merge_request_updated_ago')} .merge-request-labels diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index d4666eacd7e..76813e688b5 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -21,12 +21,13 @@ .form-group .light = f.label :description, "Description" - = render 'projects/zen', f: f, attr: :description, - classes: 'description form-control' - .clearfix.hint - .pull-left Description is parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}. - .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. - .error-alert + = render layout: 'projects/md_preview' do + = render 'projects/zen', f: f, attr: :description, + classes: 'description form-control' + .clearfix.hint + .pull-left Description is parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}. + .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + .error-alert .form-group .issue-assignee = f.label :assignee_id do diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 947e8f58ae5..7b28dd5e7da 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -21,7 +21,7 @@ - content_for :note_actions do - if can?(current_user, :modify_merge_request, @merge_request) - - unless @merge_request.closed? || @merge_request.merged? + - if @merge_request.open? = link_to 'Close', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" - if @merge_request.closed? = link_to 'Reopen', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index be638d7cac1..b93e0f9da3e 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,67 +1,12 @@ -- if can? current_user, :write_merge_request, @project - = link_to new_project_merge_request_path(@project), class: "pull-right btn btn-new", title: "New Merge Request" do - %i.fa.fa-plus - New Merge Request -%h3.page-title - Merge Requests -%hr += render "projects/issues_nav" + .row - .fixed.sidebar-expand-button.hidden-lg.hidden-md - %i.fa.fa-list.fa-2x .col-md-3.responsive-side = render 'shared/project_filter', project_entities_path: project_merge_requests_path(@project), labels: true, redirect: 'merge_requests', entity: 'merge_request' .col-md-9 - .mr-filters.append-bottom-10 - .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-user - %span.light assignee: - - if @assignee.present? - %strong= @assignee.name - - elsif params[:assignee_id] == "0" - Unassigned - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to project_filter_path(assignee_id: nil) do - Any - = link_to project_filter_path(assignee_id: 0) do - Unassigned - - @assignees.sort_by(&:name).each do |user| - %li - = link_to project_filter_path(assignee_id: user.id) do - = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' - = user.name - - .dropdown.inline.prepend-left-10 - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-clock-o - %span.light milestone: - - if @milestone.present? - %strong= @milestone.title - - elsif params[:milestone_id] == "0" - None (backlog) - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to project_filter_path(milestone_id: nil) do - Any - = link_to project_filter_path(milestone_id: 0) do - None (backlog) - - project_active_milestones.each do |milestone| - %li - = link_to project_filter_path(milestone_id: milestone.id) do - %strong= milestone.title - %small.light= milestone.expires_at - - .pull-right - = render 'shared/sort_dropdown' - + .append-bottom-10 + = render 'projects/issuable_filter' .panel.panel-default %ul.well-list.mr-list = render @merge_requests diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index 9540453ce3e..63db4b30968 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -10,11 +10,11 @@ - target_remote = @merge_request.target_project.namespace.nil? ? "target" :@merge_request.target_project.namespace.path %p %strong Step 1. - Checkout the branch we are going to merge and pull in the code + Fetch the code and create a new branch pointing to it %pre.dark :preserve - git checkout -b #{@merge_request.source_project_path}-#{@merge_request.source_branch} #{@merge_request.target_branch} - git pull #{@merge_request.source_project.http_url_to_repo} #{@merge_request.source_branch} + git fetch #{@merge_request.source_project.http_url_to_repo} #{@merge_request.source_branch} + git checkout -b #{@merge_request.source_project_path}-#{@merge_request.source_branch} FETCH_HEAD %p %strong Step 2. Merge the branch and push the changes to GitLab diff --git a/app/views/projects/merge_requests/show/_mr_accept.html.haml b/app/views/projects/merge_requests/show/_mr_accept.html.haml index 213e14268c2..4939ae03994 100644 --- a/app/views/projects/merge_requests/show/_mr_accept.html.haml +++ b/app/views/projects/merge_requests/show/_mr_accept.html.haml @@ -16,15 +16,6 @@ %h4 You can accept this request automatically. .accept-merge-holder.clearfix - .js-toggle-container - %p - You can - %strong= link_to "modify merge commit message", "#", class: "modify-merge-commit-link js-toggle-button", title: "Modify merge commit message" - before accepting merge request - .js-toggle-content.hide - = render 'shared/commit_message_container', params: params, - text: @merge_request.merge_commit_message, - rows: 14, hint: true .accept-group .pull-left = f.submit "Accept Merge Request", class: "btn btn-create accept_merge_request" @@ -33,6 +24,14 @@ = label_tag :should_remove_source_branch, class: "checkbox" do = check_box_tag :should_remove_source_branch Remove source-branch + .js-toggle-container + %label + %i.fa.fa-edit + = link_to "modify merge commit message", "#", class: "modify-merge-commit-link js-toggle-button", title: "Modify merge commit message" + .js-toggle-content.hide + = render 'shared/commit_message_container', params: params, + text: @merge_request.merge_commit_message, + rows: 14, hint: true %hr .light diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index 7e5a4eda508..866b236d827 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -8,6 +8,10 @@ - else Open + .cross-project-ref + %i.fa.fa-link.has_tooltip{:"data-original-title" => 'Cross-project reference'} + = cross_project_reference(@project, @merge_request) + .creator Created by #{link_to_member(@project, @merge_request.author)} #{time_ago_with_tooltip(@merge_request.created_at)} diff --git a/app/views/projects/merge_requests/show/_mr_ci.html.haml b/app/views/projects/merge_requests/show/_mr_ci.html.haml index dc64c096edc..941b15d3b32 100644 --- a/app/views/projects/merge_requests/show/_mr_ci.html.haml +++ b/app/views/projects/merge_requests/show/_mr_ci.html.haml @@ -20,9 +20,8 @@ = link_to "Build page", ci_build_details_path(@merge_request) .ci_widget - %strong - %i.fa.fa-spinner - Checking for CI status for #{@merge_request.last_commit_short_sha} + %i.fa.fa-spinner + Checking for CI status for #{@merge_request.last_commit_short_sha} .ci_widget.ci-error{style: "display:none"} %i.fa.fa-times diff --git a/app/views/projects/merge_requests/show/_state_widget.html.haml b/app/views/projects/merge_requests/show/_state_widget.html.haml index 5db77ab2754..a4f2a890969 100644 --- a/app/views/projects/merge_requests/show/_state_widget.html.haml +++ b/app/views/projects/merge_requests/show/_state_widget.html.haml @@ -1,8 +1,8 @@ -.panel.mr-state-widget.panel-default +.mr-state-widget - if @merge_request.source_project.ci_service && @commits.any? - .panel-heading + .mr-widget-heading = render "projects/merge_requests/show/mr_ci" - .panel-body + .mr-widget-body - if @merge_request.open? - if @merge_request.source_branch_exists? && @merge_request.target_branch_exists? = render "projects/merge_requests/show/mr_accept" @@ -11,16 +11,26 @@ - if @merge_request.closed? %h4 - Closed by #{link_to_member(@project, @merge_request.closed_event.author, avatar: false)} - #{time_ago_with_tooltip(@merge_request.closed_event.created_at)} + Closed + - if @merge_request.closed_event + by #{link_to_member(@project, @merge_request.closed_event.author, avatar: false)} + #{time_ago_with_tooltip(@merge_request.closed_event.created_at)} %p Changes were not merged into target branch - if @merge_request.merged? %h4 - Merged by #{link_to_member(@project, @merge_request.merge_event.author, avatar: false)} - #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} + Merged + - if @merge_request.merge_event + by #{link_to_member(@project, @merge_request.merge_event.author, avatar: false)} + #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} = render "projects/merge_requests/show/remove_source_branch" + - if @merge_request.locked? + %h4 + Merge in progress... + %p + GitLab tries to merge it right now. During this time merge request is locked and can not be closed. + - unless @commits.any? %h4 Nothing to merge %p @@ -31,11 +41,10 @@ %br Try to use different branches or push new code. - - if !@closes_issues.empty? && @merge_request.open? - .panel-footer + - if @closes_issues.present? && @merge_request.open? + .mr-widget-footer %span %i.fa.fa-check Accepting this merge request will close #{@closes_issues.size == 1 ? 'issue' : 'issues'} = succeed '.' do != gfm(issues_sentence(@closes_issues)) - diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 5fb01a11cc5..0f51a347f01 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -18,13 +18,14 @@ .col-sm-10 = f.text_field :title, maxlength: 255, class: "form-control" %p.hint Required - .form-group + .form-group.milestone-description = f.label :description, "Description", class: "control-label" .col-sm-10 - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' - .hint - .pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}. - .pull-left Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + = render layout: 'projects/md_preview' do + = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + .hint + .pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}. + .pull-left Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. .clearfix .error-alert .col-md-6 diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 03367b7cdbf..0db0b114d63 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,4 +1,4 @@ -= render "projects/issues/head" += render "projects/issues_nav" .milestones_content %h3.page-title Milestones diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 8263f7530a2..f08ccc1d570 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -1,4 +1,4 @@ -= render "projects/issues/head" += render "projects/issues_nav" %h3.page-title Milestone ##{@milestone.iid} .pull-right diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 6c986050c45..e77ef84f51c 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -3,7 +3,7 @@ = render 'projects/errors' .project-edit-content - = form_for @project, remote: true, html: { class: 'new_project form-horizontal' } do |f| + = form_for @project, html: { class: 'new_project form-horizontal' } do |f| .form-group.project-name-holder = f.label :name, class: 'control-label' do %strong Project name @@ -44,13 +44,14 @@ .js-toggle-content.hide .form-group.import-url-data = f.label :import_url, class: 'control-label' do - %span Import existing repo + %span Import existing git repo .col-sm-10 = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' .bs-callout.bs-callout-info This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. %br The import will time out after 4 minutes. For big repositories, use a clone/push combination. + For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"} %hr .form-group diff --git a/app/views/projects/new_tree/show.html.haml b/app/views/projects/new_tree/show.html.haml index 49c504c104f..f09d3659774 100644 --- a/app/views/projects/new_tree/show.html.haml +++ b/app/views/projects/new_tree/show.html.haml @@ -19,22 +19,17 @@ Encoding .col-sm-10 = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control' - = render 'shared/commit_message_container', params: params, - placeholder: 'Add new file' .file-holder .file-title %i.fa.fa-file .file-content.code %pre#editor= params[:content] - .form-actions - = hidden_field_tag 'content', '', id: "file-content" - .commit-button-annotation - = button_tag "Commit changes", class: 'btn commit-btn js-commit-button btn-create' - .message - to branch - %strong= @ref - = link_to "Cancel", project_tree_path(@project, @id), class: "btn btn-cancel", data: { confirm: leave_edit_message} + = render 'shared/commit_message_container', params: params, + placeholder: 'Add new file' + = hidden_field_tag 'content', '', id: 'file-content' + = render 'projects/commit_button', ref: @ref, + cancel_path: project_tree_path(@project, @id) :javascript ace.config.set("modePath", gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}/ace-src-noconflict") diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml new file mode 100644 index 00000000000..dd576243510 --- /dev/null +++ b/app/views/projects/no_repo.html.haml @@ -0,0 +1,22 @@ +%h2 + %i.fa.fa-warning + No repository + +%p.slead + The repository for this project does not exist. + %br + This means you can not push code until you create an empty repository or import existing one. +%hr + +.no-repo-actions + = link_to project_repository_path(@project), method: :post, class: 'btn btn-primary' do + Create empty bare repository + + %strong.prepend-left-10.append-right-10 or + + = link_to new_project_import_path(@project), class: 'btn' do + Import repository + +- if can? current_user, :remove_project, @project + .prepend-top-20 + = link_to 'Remove project', @project, data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index c68b3817e79..47ffe1fd2f3 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -5,23 +5,13 @@ = f.hidden_field :noteable_id = f.hidden_field :noteable_type - %ul.nav.nav-tabs - %li.active - = link_to '#note-write-holder', class: 'js-note-write-button' do - Write - %li - = link_to '#note-preview-holder', class: 'js-note-preview-button', data: { url: preview_project_notes_path(@project) } do - Preview - %div - .note-write-holder - = render 'projects/zen', f: f, attr: :note, - classes: 'note_text js-note-text' - .light.clearfix - .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 }} - .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }. + = render layout: 'projects/md_preview' do + = render 'projects/zen', f: f, attr: :note, + classes: 'note_text js-note-text' + .light.clearfix + .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 }} + .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }. - .note-preview-holder.hide - .js-note-preview .note-form-actions .buttons @@ -29,7 +19,7 @@ = yield(:note_actions) %a.btn.grouped.js-close-discussion-note-form Cancel - .note-form-option + .note-form-option.hidden-xs %a.choose-btn.btn.js-choose-note-attachment-button %i.fa.fa-paperclip %span Choose File ... diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 814bf19970c..354afd3e2c9 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -18,6 +18,8 @@ %i.fa.fa-trash-o.cred Remove = link_to_member(@project, note.author, avatar: false) + %span.author-username + = '@' + note.author.username %span.note-last-update = note_timestamp(note) @@ -38,7 +40,8 @@ .note-edit-form = form_for note, url: project_note_path(@project, note), method: :put, remote: true, authenticity_token: true do |f| - = f.text_area :note, class: 'note_text js-note-text js-gfm-input turn-on' + = render layout: 'projects/md_preview' do + = f.text_area :note, class: 'note_text js-note-text markdown-area js-gfm-input turn-on' .form-actions.clearfix = f.submit 'Save changes', class: "btn btn-primary btn-save js-comment-button" @@ -59,7 +62,7 @@ - if note.attachment.image? = link_to note.attachment.secure_url, target: '_blank' do = image_tag note.attachment.secure_url, class: 'note-image-attach' - .attachment.pull-right + .attachment = link_to note.attachment.secure_url, target: "_blank" do %i.fa.fa-paperclip = note.attachment_identifier diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml index da71220af17..b4d1cce7980 100644 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ b/app/views/projects/notes/discussions/_diff.html.haml @@ -21,7 +21,7 @@ - else %td.old_line= raw(line.type == "new" ? " " : line.old_pos) %td.new_line= raw(line.type == "old" ? " " : line.new_pos) - %td.line_content{class: "noteable_line #{line.type} #{line_code}", "line_code" => line_code}= raw "#{line.text} " + %td.line_content{class: "noteable_line #{line.type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line.text) - if line_code == note.line_code = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 49a3ef4c8a7..227a2f9a061 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -1,13 +1,13 @@ %h3.page-title Protected branches -%p.light This ability keeps stable branches secure and forces developers to use code reviews +%p.light Keep stable branches secure and force developers to use Merge Requests %hr .bs-callout.bs-callout-info %p Protected branches are designed to %ul %li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"} - %li prevents anyone from force pushing to the branch - %li prevents anyone from deleting the branch + %li prevent anyone from force pushing to the branch + %li prevent anyone from deleting the branch %p Read more about #{link_to "project permissions", help_page_path("permissions", "permissions"), class: "underlined-link"} - if can? current_user, :admin_project, @project diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 16d59d1fe9d..1151f22c7e8 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -28,7 +28,7 @@ - @service.fields.each do |field| - name = field[:name] - - value = @service.send(name) + - value = @service.send(name) unless field[:type] == 'password' - type = field[:type] - placeholder = field[:placeholder] - choices = field[:choices] @@ -45,6 +45,8 @@ = f.check_box name - elsif type == 'select' = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } + - elsif type == 'password' + = f.password_field name, class: 'form-control' .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 09664ed51eb..9b06ebe95a4 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -62,11 +62,14 @@ - else #{link_to @project.owner_name, @project.owner} - - - if @project.gitlab_ci? - %hr - = link_to @project.gitlab_ci_service.builds_path do - = image_tag @project.gitlab_ci_service.status_img_path, alt: "build status" + - @project.ci_services.each do |ci_service| + - if ci_service.active? && ci_service.respond_to?(:builds_path) + - if ci_service.respond_to?(:status_img_path) + = link_to ci_service.builds_path do + = image_tag ci_service.status_img_path, alt: "build status" + - else + %span.light CI provided by + = link_to ci_service.title, ci_service.builds_path - if readme .tab-pane#tab-readme diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index bce105a033b..4ab102ba96c 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -4,6 +4,9 @@ = link_to project_commits_path(@project, tag.name), class: "" do %i.fa.fa-tag = tag.name + - if tag.message.present? + + = strip_gpg_signature(tag.message) .pull-right - if can? current_user, :download_code, @project = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-small' diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index aa08b397763..ad7ff8d3db8 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -21,7 +21,7 @@ = text_field_tag :message, nil, placeholder: 'Enter message.', required: false, tabindex: 3, class: 'form-control' .light (Optional) Entering a message will create an annotated tag. .form-actions - = submit_tag 'Create tag', class: 'btn btn-create', tabindex: 3 + = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', project_tags_path(@project), class: 'btn btn-cancel' :javascript diff --git a/app/views/projects/team_members/_team_member.html.haml b/app/views/projects/team_members/_team_member.html.haml index 5f29b58de32..7a9c0939ba0 100644 --- a/app/views/projects/team_members/_team_member.html.haml +++ b/app/views/projects/team_members/_team_member.html.haml @@ -5,7 +5,7 @@ - unless @project.personal? && user == current_user .pull-left = form_for(member, as: :project_member, url: project_team_member_path(@project, member.user)) do |f| - = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: "medium project-access-select span2 trigger-submit" + = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: "trigger-submit" = link_to project_team_member_path(@project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from team' do %i.fa.fa-minus.fa-inverse diff --git a/app/views/projects/team_members/import.html.haml b/app/views/projects/team_members/import.html.haml index 510b579fe2f..d1f46c61b2e 100644 --- a/app/views/projects/team_members/import.html.haml +++ b/app/views/projects/team_members/import.html.haml @@ -9,6 +9,6 @@ .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(current_user.authorized_projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true) .form-actions - = submit_tag 'Import project members', class: "btn btn-create" + = button_tag 'Import project members', class: "btn btn-create" = link_to "Cancel", project_team_index_path(@project), class: "btn btn-cancel" diff --git a/app/views/projects/transfer.js.haml b/app/views/projects/transfer.js.haml index 10b0de98c04..6d083c5c516 100644 --- a/app/views/projects/transfer.js.haml +++ b/app/views/projects/transfer.js.haml @@ -1,7 +1,2 @@ -- if @project.errors[:namespace_id].present? - :plain - $("#tab-transfer .errors-holder").replaceWith(errorMessage('#{escape_javascript(@project.errors[:namespace_id].first)}')); - $("#tab-transfer .form-actions input").removeAttr('disabled').removeClass('disabled'); -- else - :plain +:plain location.href = "#{edit_project_path(@project)}"; diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml index a8ec9df2c8f..46e9be4af83 100644 --- a/app/views/projects/tree/_submodule_item.html.haml +++ b/app/views/projects/tree/_submodule_item.html.haml @@ -7,8 +7,8 @@ @ %span.monospace - if commit.nil? - #{submodule_item.id[0..10]} + #{truncate_sha(submodule_item.id)} - else - = link_to "#{submodule_item.id[0..10]}", commit + = link_to "#{truncate_sha(submodule_item.id)}", commit %td %td.hidden-xs diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index d3a66c48c9b..ef4b8f74714 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -17,7 +17,7 @@ %tr %td = link_to project_wiki_path(@project, @page, version_id: commit.id) do - = commit.id[0..10] + = truncate_sha(commit.id) %td = commit.author.name %td diff --git a/app/views/search/_project_filter.html.haml b/app/views/search/_project_filter.html.haml index c201b3d6c47..ad933502a28 100644 --- a/app/views/search/_project_filter.html.haml +++ b/app/views/search/_project_filter.html.haml @@ -25,6 +25,7 @@ = @search_results.notes_count %li{class: ("active" if @scope == 'wiki_blobs')} = link_to search_filter_path(scope: 'wiki_blobs') do + %i.fa.fa-book Wiki .pull-right = @search_results.wiki_blobs_count diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index f2327cd69cc..a44a4542df5 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -10,7 +10,7 @@ = project.name_with_namespace · = link_to project_commit_path(project, note.commit_id, anchor: dom_id(note)) do - Commit #{note.commit_id[0..8]} + Commit #{truncate_sha(note.commit_id)} - else = link_to project do = project.name_with_namespace diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index bae57917a4c..5b4816e4c40 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -6,7 +6,7 @@ .col-sm-6 = search_field_tag :search, params[:search], placeholder: "issue 143", class: "form-control search-text-input", id: "dashboard_search" .col-sm-4 - = submit_tag 'Search', class: "btn btn-create" + = button_tag 'Search', class: "btn btn-create" .form-group .col-sm-2 - unless params[:snippets].eql? 'true' diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml new file mode 100644 index 00000000000..299c0bd42a2 --- /dev/null +++ b/app/views/shared/_choose_group_avatar_button.html.haml @@ -0,0 +1,7 @@ +%a.choose-btn.btn.btn-small.js-choose-group-avatar-button + %i.fa.fa-paperclip + %span Choose File ... + +%span.file_name.js-avatar-filename File name... += f.file_field :avatar, class: 'js-group-avatar-input hidden' +.light The maximum file size allowed is 200KB. diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml new file mode 100644 index 00000000000..30ba361c860 --- /dev/null +++ b/app/views/shared/_confirm_modal.html.haml @@ -0,0 +1,22 @@ +#modal-confirm-danger.modal.hide{tabindex: -1} + .modal-dialog + .modal-content + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h4 Confirmation required + + .modal-body + %p.cred.lead.js-confirm-text + + %p + This action can lead to data loss. + To prevent accidental actions we ask you to confirm your intention. + %br + Please type + %code.js-confirm-danger-match #{phrase} + to proceed or close this modal to cancel + + .form-group + = text_field_tag 'confirm_name_input', '', class: 'form-control js-confirm-danger-input' + .form-group + = submit_tag 'Confirm', class: "btn btn-danger js-confirm-danger-submit" diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml new file mode 100644 index 00000000000..93294e42505 --- /dev/null +++ b/app/views/shared/_group_form.html.haml @@ -0,0 +1,12 @@ +.form-group + = f.label :name, class: 'control-label' do + Group name + .col-sm-10 + = f.text_field :name, placeholder: 'Example Group', class: 'form-control', + autofocus: local_assigns[:autofocus] || false + +.form-group.group-description-holder + = f.label :description, 'Details', class: 'control-label' + .col-sm-10 + = f.text_area :description, maxlength: 250, + class: 'form-control js-gfm-input', rows: 4 diff --git a/app/views/shared/_group_tips.html.haml b/app/views/shared/_group_tips.html.haml new file mode 100644 index 00000000000..e5cf783beb7 --- /dev/null +++ b/app/views/shared/_group_tips.html.haml @@ -0,0 +1,6 @@ +%ul + %li A group is a collection of several projects + %li Groups are private by default + %li Members of a group may only view projects they have permission to access + %li Group project URLs are prefixed with the group namespace + %li Existing projects may be moved into a group diff --git a/app/views/shared/_promo.html.haml b/app/views/shared/_promo.html.haml index 5675e43b05f..3596aabe309 100644 --- a/app/views/shared/_promo.html.haml +++ b/app/views/shared/_promo.html.haml @@ -1,5 +1,5 @@ .gitlab-promo - = link_to "Homepage", "https://www.gitlab.com/" - = link_to "Blog", "https://www.gitlab.com/blog/" - = link_to "@gitlabhq", "https://twitter.com/gitlabhq" + = link_to 'Homepage', promo_url + = link_to "Blog", promo_url + '/blog/' + = link_to "@gitlab", "https://twitter.com/gitlab" = link_to "Requests", "http://feedback.gitlab.com/" diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index 7b37b39780e..54f59245690 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -9,13 +9,13 @@ %ul.dropdown-menu %li = link_to project_filter_path(sort: 'newest') do - Newest + = sort_title_recently_created = link_to project_filter_path(sort: 'oldest') do - Oldest + = sort_title_oldest_created = link_to project_filter_path(sort: 'recently_updated') do - Recently updated + = sort_title_recently_updated = link_to project_filter_path(sort: 'last_updated') do - Last updated + = sort_title_oldest_updated = link_to project_filter_path(sort: 'milestone_due_soon') do Milestone due soon = link_to project_filter_path(sort: 'milestone_due_later') do diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 49ea8460e7d..f729f129e45 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -10,22 +10,8 @@ = f.label :title, class: 'control-label' .col-sm-10= f.text_field :title, placeholder: "Example Snippet", class: 'form-control', required: true - - unless @snippet.respond_to?(:project) - .form-group - = f.label "Access", class: 'control-label' - .col-sm-10 - = f.label :private_true, class: 'radio-label' do - = f.radio_button :private, true - %span - %strong Private - (only you can see this snippet) - %br - = f.label :private_false, class: 'radio-label' do - = f.radio_button :private, false - %span - %strong Public - (GitLab users can see this snippet) - + = render "shared/snippets/visibility_level", f: f, visibility_level: gitlab_config.default_projects_features.visibility_level, can_change_visibility_level: true + .form-group .file-editor = f.label :file_name, "File", class: 'control-label' diff --git a/app/views/shared/snippets/_visibility_level.html.haml b/app/views/shared/snippets/_visibility_level.html.haml new file mode 100644 index 00000000000..9acff18e450 --- /dev/null +++ b/app/views/shared/snippets/_visibility_level.html.haml @@ -0,0 +1,27 @@ +.form-group.project-visibility-level-holder + = f.label :visibility_level, class: 'control-label' do + Visibility Level + = link_to "(?)", help_page_path("public_access", "public_access") + .col-sm-10 + - if can_change_visibility_level + - Gitlab::VisibilityLevel.values.each do |level| + .radio + - restricted = restricted_visibility_levels.include?(level) + = f.radio_button :visibility_level, level, disabled: restricted + = label "#{dom_class(@snippet)}_visibility_level", level do + = visibility_level_icon(level) + .option-title + = visibility_level_label(level) + .option-descr + = snippet_visibility_level_description(level) + - unless restricted_visibility_levels.empty? + .col-sm-10 + %span.info + Some visibility level settings have been restricted by the administrator. + - else + .col-sm-10 + %span.info + = visibility_level_icon(visibility_level) + %strong + = visibility_level_label(visibility_level) + .light= visibility_level_description(visibility_level) diff --git a/app/views/snippets/current_user_index.html.haml b/app/views/snippets/current_user_index.html.haml index e3edd856983..b2b7ea4df0e 100644 --- a/app/views/snippets/current_user_index.html.haml +++ b/app/views/snippets/current_user_index.html.haml @@ -23,6 +23,11 @@ Private %span.pull-right = @user.snippets.are_private.count + = nav_tab :scope, 'are_internal' do + = link_to user_snippets_path(@user, scope: 'are_internal') do + Internal + %span.pull-right + = @user.snippets.are_internal.count = nav_tab :scope, 'are_public' do = link_to user_snippets_path(@user, scope: 'are_public') do Public diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml index cea2517a8e1..0d71c41e2e7 100644 --- a/app/views/snippets/index.html.haml +++ b/app/views/snippets/index.html.haml @@ -2,10 +2,12 @@ Public snippets .pull-right - = link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do - Add new snippet - = link_to user_snippets_path(current_user), class: "btn btn-grouped" do - My snippets + + - if current_user + = link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do + Add new snippet + = link_to user_snippets_path(current_user), class: "btn btn-grouped" do + My snippets %p.light Public snippets created by you and other users are listed here diff --git a/app/views/snippets/user_index.html.haml b/app/views/snippets/user_index.html.haml index 1cb53ec6a25..67f3a68aa22 100644 --- a/app/views/snippets/user_index.html.haml +++ b/app/views/snippets/user_index.html.haml @@ -4,8 +4,9 @@ %span \/ Snippets - = link_to new_snippet_path, class: "btn btn-small add_new pull-right", title: "New Snippet" do - Add new snippet + - if current_user + = link_to new_snippet_path, class: "btn btn-small add_new pull-right", title: "New Snippet" do + Add new snippet %hr diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml index 09b2985d498..ea008c2dede 100644 --- a/app/views/users/_groups.html.haml +++ b/app/views/users/_groups.html.haml @@ -1,3 +1,3 @@ - groups.each do |group| = link_to group, class: 'profile-groups-avatars', :title => group.name do - = image_tag group_icon(group.path) + - image_tag group_icon(group.path) diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder new file mode 100644 index 00000000000..b7216a88765 --- /dev/null +++ b/app/views/users/show.atom.builder @@ -0,0 +1,12 @@ +xml.instruct! +xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlnsmedia" => "http://search.yahoo.com/mrss/" do + xml.title "Activity feed for #{@user.name}" + xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml" + xml.link href: user_url(@user), rel: "alternate", type: "text/html" + xml.id projects_url + xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + + @events.each do |event| + event_to_atom(xml, event) + end +end diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index cb49c030af2..54f2666ce5d 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -18,7 +18,15 @@ %h4 Groups: = render 'groups', groups: @groups %hr - %h4 User Activity: + %h4 + User Activity: + + - if current_user + %span.rss-icon.pull-right + = link_to user_path(@user, :atom, { private_token: current_user.private_token }) do + %strong + %i.fa.fa-rss + = render @events .col-md-4 = render 'profile', user: @user diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 2947c8e3ecd..e3f6f3a6aef 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -21,5 +21,8 @@ class EmailsOnPushWorker recipients.split(" ").each do |recipient| Notify.repository_push_email(project_id, recipient, author_id, branch, compare).deliver end + ensure + compare = nil + GC.start end end diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb new file mode 100644 index 00000000000..cc0a7f25664 --- /dev/null +++ b/app/workers/project_service_worker.rb @@ -0,0 +1,9 @@ +class ProjectServiceWorker + include Sidekiq::Worker + + sidekiq_options queue: :project_web_hook + + def perform(hook_id, data) + Service.find(hook_id).execute(data) + end +end diff --git a/config/application.rb b/config/application.rb index 99dfafdb786..8a280de6fac 100644 --- a/config/application.rb +++ b/config/application.rb @@ -2,7 +2,7 @@ require File.expand_path('../boot', __FILE__) require 'rails/all' require 'devise' - +I18n.config.enforce_available_locales = false Bundler.require(:default, Rails.env) module Gitlab @@ -13,7 +13,6 @@ module Gitlab # Custom directories with classes and modules you want to be autoloadable. config.autoload_paths += %W(#{config.root}/lib - #{config.root}/app/finders #{config.root}/app/models/hooks #{config.root}/app/models/concerns #{config.root}/app/models/project_services @@ -23,10 +22,6 @@ module Gitlab # :all can be used as a placeholder for all plugins not explicitly named. # config.plugins = [ :exception_notification, :ssl_requirement, :all ] - # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. - # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. - # config.time_zone = 'Central Time (US & Canada)' - # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de @@ -97,5 +92,8 @@ module Gitlab redis_config_hash[:namespace] = 'cache:gitlab' config.cache_store = :redis_store, redis_config_hash + + # This is needed for gitlab-shell + ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH'] end end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 857643c006e..7b4c180fccc 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -33,7 +33,14 @@ production: &base # Uncomment and customize if you can't use the default user to run GitLab (default: 'git') # user: git + ## Date & Time settings + # Uncomment and customize if you want to change the default time zone of GitLab application. + # To see all available zones, run `bundle exec rake time:zones:all RAILS_ENV=production` + # time_zone: 'UTC' + ## Email settings + # Uncomment and set to false if you need to disable email sending from GitLab (default: true) + # email_enabled: true # Email address used in the "From" field in mails sent by GitLab email_from: example@example.com @@ -119,6 +126,7 @@ production: &base # new_issue_url: "http://jira.sample/secure/CreateIssue.jspa" ## Gravatar + ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html gravatar: enabled: true # Use user avatar image from Gravatar.com (default: true) # gravatar urls: possible placeholders: %{hash} %{size} %{email} @@ -134,43 +142,61 @@ production: &base # bundle exec rake gitlab:ldap:check RAILS_ENV=production ldap: enabled: false - host: '_your_ldap_server' - port: 636 - uid: 'sAMAccountName' - method: 'ssl' # "tls" or "ssl" or "plain" - bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' - password: '_the_password_of_the_bind_user' - - # This setting specifies if LDAP server is Active Directory LDAP server. - # For non AD servers it skips the AD specific queries. - # If your LDAP server is not AD, set this to false. - active_directory: true - - # If allow_username_or_email_login is enabled, GitLab will ignore everything - # after the first '@' in the LDAP username submitted by the user on login. - # - # Example: - # - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials; - # - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'. - # - # If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to - # disable this setting, because the userPrincipalName contains an '@'. - allow_username_or_email_login: false - - # Base where we can search for users - # - # Ex. ou=People,dc=gitlab,dc=example - # - base: '' - - # Filter LDAP users - # - # Format: RFC 4515 http://tools.ietf.org/search/rfc4515 - # Ex. (employeeType=developer) - # - # Note: GitLab does not support omniauth-ldap's custom filter syntax. - # - user_filter: '' + servers: + main: # 'main' is the GitLab 'provider ID' of this LDAP server + ## label + # + # A human-friendly name for your LDAP server. It is OK to change the label later, + # for instance if you find out it is too large to fit on the web page. + # + # Example: 'Paris' or 'Acme, Ltd.' + label: 'LDAP' + + host: '_your_ldap_server' + port: 636 + uid: 'sAMAccountName' + method: 'ssl' # "tls" or "ssl" or "plain" + bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' + password: '_the_password_of_the_bind_user' + + # This setting specifies if LDAP server is Active Directory LDAP server. + # For non AD servers it skips the AD specific queries. + # If your LDAP server is not AD, set this to false. + active_directory: true + + # If allow_username_or_email_login is enabled, GitLab will ignore everything + # after the first '@' in the LDAP username submitted by the user on login. + # + # Example: + # - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials; + # - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'. + # + # If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to + # disable this setting, because the userPrincipalName contains an '@'. + allow_username_or_email_login: false + + # Base where we can search for users + # + # Ex. ou=People,dc=gitlab,dc=example + # + base: '' + + # Filter LDAP users + # + # Format: RFC 4515 http://tools.ietf.org/search/rfc4515 + # Ex. (employeeType=developer) + # + # Note: GitLab does not support omniauth-ldap's custom filter syntax. + # + user_filter: '' + + # GitLab EE only: add more LDAP servers + # Choose an ID made of a-z and 0-9 . This ID will be stored in the database + # so that GitLab can remember which LDAP server a user belongs to. + # uswest2: + # label: + # host: + # .... ## OmniAuth settings @@ -299,6 +325,20 @@ test: project_url: "http://redmine/projects/:issues_tracker_id" issues_url: "http://redmine/:project_id/:issues_tracker_id/:id" new_issue_url: "http://redmine/projects/:issues_tracker_id/issues/new" + ldap: + enabled: false + servers: + main: + label: ldap + host: 127.0.0.1 + port: 3890 + uid: 'uid' + method: 'plain' # "tls" or "ssl" or "plain" + base: 'dc=example,dc=com' + user_filter: '' + group_base: 'ou=groups,dc=example,dc=com' + admin_group: '' + sync_ssh_keys: false staging: <<: *base diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 0d11ae6f33f..27bb83784ba 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -56,9 +56,25 @@ end # Default settings Settings['ldap'] ||= Settingslogic.new({}) Settings.ldap['enabled'] = false if Settings.ldap['enabled'].nil? -Settings.ldap['allow_username_or_email_login'] = false if Settings.ldap['allow_username_or_email_login'].nil? -Settings.ldap['active_directory'] = true if Settings.ldap['active_directory'].nil? +# backwards compatibility, we only have one host +if Settings.ldap['enabled'] || Rails.env.test? + if Settings.ldap['host'].present? + server = Settings.ldap.except('sync_time') + server['provider_name'] = 'ldap' + Settings.ldap['servers'] = { + 'ldap' => server + } + end + + Settings.ldap['servers'].each do |key, server| + server['label'] ||= 'LDAP' + server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil? + server['active_directory'] = true if server['active_directory'].nil? + server['provider_name'] ||= "ldap#{key}".downcase + server['provider_class'] = OmniAuth::Utils.camelize(server['provider_name']) + end +end Settings['omniauth'] ||= Settingslogic.new({}) Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? @@ -79,6 +95,7 @@ Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? Settings.gitlab['port'] ||= Settings.gitlab.https ? 443 : 80 Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" +Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil? Settings.gitlab['email_from'] ||= "gitlab@#{Settings.gitlab.host}" Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url) Settings.gitlab['user'] ||= 'git' @@ -87,6 +104,7 @@ Settings.gitlab['user_home'] ||= begin rescue ArgumentError # no user configured '/home/' + Settings.gitlab['user'] end +Settings.gitlab['time_zone'] ||= nil Settings.gitlab['signup_enabled'] ||= false Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) diff --git a/config/initializers/4_sidekiq.rb b/config/initializers/4_sidekiq.rb index 228b14cb526..e856499732e 100644 --- a/config/initializers/4_sidekiq.rb +++ b/config/initializers/4_sidekiq.rb @@ -14,7 +14,8 @@ Sidekiq.configure_server do |config| } config.server_middleware do |chain| - chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger + chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS'] + chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] end end diff --git a/config/initializers/7_omniauth.rb b/config/initializers/7_omniauth.rb new file mode 100644 index 00000000000..18759f0cfb0 --- /dev/null +++ b/config/initializers/7_omniauth.rb @@ -0,0 +1,12 @@ +if Gitlab::LDAP::Config.enabled? + module OmniAuth::Strategies + server = Gitlab.config.ldap.servers.values.first + klass = server['provider_class'] + const_set(klass, Class.new(LDAP)) unless klass == 'LDAP' + end + + OmniauthCallbacksController.class_eval do + server = Gitlab.config.ldap.servers.values.first + alias_method server['provider_name'], :ldap + end +end
\ No newline at end of file diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 34f4f386988..c6eb3e51036 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -204,22 +204,24 @@ Devise.setup do |config| # manager.default_strategies(scope: :user).unshift :some_external_strategy # end - if Gitlab.config.ldap.enabled - if Gitlab.config.ldap.allow_username_or_email_login - email_stripping_proc = ->(name) {name.gsub(/@.*$/,'')} - else - email_stripping_proc = ->(name) {name} + if Gitlab::LDAP::Config.enabled? + Gitlab.config.ldap.servers.values.each do |server| + if server['allow_username_or_email_login'] + email_stripping_proc = ->(name) {name.gsub(/@.*$/,'')} + else + email_stripping_proc = ->(name) {name} + end + + config.omniauth server['provider_name'], + host: server['host'], + base: server['base'], + uid: server['uid'], + port: server['port'], + method: server['method'], + bind_dn: server['bind_dn'], + password: server['password'], + name_proc: email_stripping_proc end - - config.omniauth :ldap, - host: Gitlab.config.ldap['host'], - base: Gitlab.config.ldap['base'], - uid: Gitlab.config.ldap['uid'], - port: Gitlab.config.ldap['port'], - method: Gitlab.config.ldap['method'], - bind_dn: Gitlab.config.ldap['bind_dn'], - password: Gitlab.config.ldap['password'], - name_proc: email_stripping_proc end Gitlab.config.omniauth.providers.each do |provider| diff --git a/config/initializers/disable_email_interceptor.rb b/config/initializers/disable_email_interceptor.rb new file mode 100644 index 00000000000..c76a6b8b19f --- /dev/null +++ b/config/initializers/disable_email_interceptor.rb @@ -0,0 +1,2 @@ +# Interceptor in lib/disable_email_interceptor.rb +ActionMailer::Base.register_interceptor(DisableEmailInterceptor) unless Gitlab.config.gitlab.email_enabled diff --git a/config/initializers/gitlab_shell_secret_token.rb b/config/initializers/gitlab_shell_secret_token.rb new file mode 100644 index 00000000000..8d2b771e535 --- /dev/null +++ b/config/initializers/gitlab_shell_secret_token.rb @@ -0,0 +1,19 @@ +# Be sure to restart your server when you modify this file. + +require 'securerandom' + +# Your secret key for verifying the gitlab_shell. + + +secret_file = Rails.root.join('.gitlab_shell_secret') +gitlab_shell_symlink = File.join(Gitlab.config.gitlab_shell.path, '.gitlab_shell_secret') + +unless File.exist? secret_file + # Generate a new token of 16 random hexadecimal characters and store it in secret_file. + token = SecureRandom.hex(16) + File.write(secret_file, token) +end + +if File.exist?(Gitlab.config.gitlab_shell.path) && !File.exist?(gitlab_shell_symlink) + FileUtils.symlink(secret_file, gitlab_shell_symlink) +end
\ No newline at end of file diff --git a/config/initializers/time_zone.rb b/config/initializers/time_zone.rb new file mode 100644 index 00000000000..ee246e67d66 --- /dev/null +++ b/config/initializers/time_zone.rb @@ -0,0 +1 @@ +Time.zone = Gitlab.config.gitlab.time_zone || Time.zone diff --git a/config/routes.rb b/config/routes.rb index c0a970517b6..b6c5bb5b908 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -137,7 +137,8 @@ Gitlab::Application.routes.draw do end end - match "/u/:username" => "users#show", as: :user, constraints: { username: /.*/ }, via: :get + match "/u/:username" => "users#show", as: :user, + constraints: {username: /(?:[^.]|\.(?!atom$))+/, format: /atom/}, via: :get # # Dashboard Area @@ -181,18 +182,16 @@ Gitlab::Application.routes.draw do resources :projects, constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/ }, except: [:new, :create, :index], path: "/" do member do put :transfer - post :fork post :archive post :unarchive post :upload_image post :toggle_star + get :markdown_preview get :autocomplete_sources - get :import - put :retry_import end scope module: :projects do - resources :blob, only: [:show, :destroy], constraints: { id: /.+/ } do + resources :blob, only: [:show, :destroy], constraints: { id: /.+/, format: false } do get :diff, on: :member end resources :raw, only: [:show], constraints: {id: /.+/} @@ -215,11 +214,11 @@ Gitlab::Application.routes.draw do match "/compare/:from...:to" => "compare#show", as: "compare", via: [:get, :post], constraints: {from: /.+/, to: /.+/} - resources :snippets, constraints: {id: /\d+/} do - member do - get "raw" - end + resources :snippets, constraints: {id: /\d+/} do + member do + get "raw" end + end resources :wikis, only: [:show, :edit, :destroy, :create], constraints: {id: /[a-zA-Z.0-9_\-\/]+/} do collection do @@ -233,7 +232,10 @@ Gitlab::Application.routes.draw do end end - resource :repository, only: [:show] do + resource :fork, only: [:new, :create] + resource :import, only: [:new, :create, :show] + + resource :repository, only: [:show, :create] do member do get "archive", constraints: { format: Gitlab::Regex.archive_formats_regex } end @@ -329,10 +331,6 @@ Gitlab::Application.routes.draw do member do delete :delete_attachment end - - collection do - post :preview - end end end end diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example index 6833082d68b..d8b4f5c7c32 100644 --- a/config/unicorn.rb.example +++ b/config/unicorn.rb.example @@ -13,8 +13,9 @@ # # ENV['RAILS_RELATIVE_URL_ROOT'] = "/gitlab" -# Use at least one worker per core if you're on a dedicated server, -# more will usually help for _short_ waits on databases/caches. +# Read about unicorn workers here: +# http://doc.gitlab.com/ee/install/requirements.html#unicorn-workers +# worker_processes 2 # Since Unicorn is never exposed to outside clients, it does not need to @@ -36,10 +37,10 @@ listen "127.0.0.1:8080", :tcp_nopush => true # nuke workers after 30 seconds instead of 60 seconds (the default) # -# NOTICE: git push over http depends on this value. -# If you want be able to push huge amount of data to git repository over http -# you will have to increase this value too. -# +# NOTICE: git push over http depends on this value. +# If you want be able to push huge amount of data to git repository over http +# you will have to increase this value too. +# # Example of output if you try to push 1GB repo to GitLab over http. # -> git push http://gitlab.... master # diff --git a/db/fixtures/development/12_snippets.rb b/db/fixtures/development/12_snippets.rb index ff91e8430a4..b3a6f39c7d5 100644 --- a/db/fixtures/development/12_snippets.rb +++ b/db/fixtures/development/12_snippets.rb @@ -1,9 +1,26 @@ Gitlab::Seeder.quiet do - contents = [ - `curl https://gist.githubusercontent.com/randx/4275756/raw/da2f262920c96d1a970d48bf2e99147954b1f4bd/glus1204.sh`, - `curl https://gist.githubusercontent.com/randx/3754594/raw/11026a295e6ef3a151c635707a3e1e8e15fc4725/gitlab_setup.sh`, - `curl https://gist.githubusercontent.com/randx/3065552/raw/29fbd09f4605a5ea22a5a9095e35fd1938dea4d6/gistfile1.sh`, - ] + content =<<eos +class Member < ActiveRecord::Base + include Notifiable + include Gitlab::Access + + belongs_to :user + belongs_to :source, polymorphic: true + + validates :user, presence: true + validates :source, presence: true + validates :user_id, uniqueness: { scope: [:source_type, :source_id], message: "already exists in source" } + validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true + + scope :guests, -> { where(access_level: GUEST) } + scope :reporters, -> { where(access_level: REPORTER) } + scope :developers, -> { where(access_level: DEVELOPER) } + scope :masters, -> { where(access_level: MASTER) } + scope :owners, -> { where(access_level: OWNER) } + + delegate :name, :username, :email, to: :user, prefix: true +end +eos (1..50).each do |i| user = User.all.sample @@ -12,10 +29,11 @@ Gitlab::Seeder.quiet do id: i, author_id: user.id, title: Faker::Lorem.sentence(3), - file_name: Faker::Internet.domain_word + '.sh', - private: [true, false].sample, - content: contents.sample, + file_name: Faker::Internet.domain_word + '.rb', + visibility_level: Gitlab::VisibilityLevel.values.sample, + content: content, }]) + print('.') end end diff --git a/db/fixtures/production/001_admin.rb b/db/fixtures/production/001_admin.rb index f84d0903910..0755ac714e1 100644 --- a/db/fixtures/production/001_admin.rb +++ b/db/fixtures/production/001_admin.rb @@ -1,8 +1,10 @@ -password = if ENV['GITLAB_ROOT_PASSWORD'].blank? - "5iveL!fe" - else - ENV['GITLAB_ROOT_PASSWORD'] - end +if ENV['GITLAB_ROOT_PASSWORD'].blank? + password = '5iveL!fe' + expire_time = Time.now +else + password = ENV['GITLAB_ROOT_PASSWORD'] + expire_time = nil +end admin = User.create( email: "admin@example.com", @@ -10,7 +12,7 @@ admin = User.create( username: 'root', password: password, password_confirmation: password, - password_expires_at: Time.now, + password_expires_at: expire_time, theme_id: Gitlab::Theme::MARS ) @@ -21,7 +23,7 @@ admin.save! admin.confirm! if admin.valid? -puts %q[ +puts %Q[ Administrator account created: login.........root diff --git a/db/migrate/20140907220153_serialize_service_properties.rb b/db/migrate/20140907220153_serialize_service_properties.rb index b95f5b82e03..bd75ab1eacb 100644 --- a/db/migrate/20140907220153_serialize_service_properties.rb +++ b/db/migrate/20140907220153_serialize_service_properties.rb @@ -23,7 +23,7 @@ class SerializeServiceProperties < ActiveRecord::Migration associations[service.type.to_sym].each do |attribute| service.send("#{attribute}=", service.attributes[attribute.to_s]) end - service.save! + service.save end remove_column :services, :project_url, :string diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb index 4b62b223cbf..a8e07033a5d 100644 --- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb +++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb @@ -10,7 +10,7 @@ class MoveSlackServiceToWebhook < ActiveRecord::Migration slack_service.properties.delete('subdomain') # Room is configured on the Slack side slack_service.properties.delete('room') - slack_service.save! + slack_service.save end end end diff --git a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb new file mode 100644 index 00000000000..7f125acb5d1 --- /dev/null +++ b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb @@ -0,0 +1,21 @@ +class AddVisibilityLevelToSnippet < ActiveRecord::Migration + def up + add_column :snippets, :visibility_level, :integer, :default => 0, :null => false + + Snippet.where(private: true).update_all(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + Snippet.where(private: false).update_all(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + + add_index :snippets, :visibility_level + + remove_column :snippets, :private + end + + def down + add_column :snippets, :private, :boolean, :default => false, :null => false + + Snippet.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).update_all(private: false) + Snippet.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).update_all(private: true) + + remove_column :snippets, :visibility_level + end +end diff --git a/db/migrate/20141121133009_add_timestamps_to_members.rb b/db/migrate/20141121133009_add_timestamps_to_members.rb new file mode 100644 index 00000000000..ef6d4dedf32 --- /dev/null +++ b/db/migrate/20141121133009_add_timestamps_to_members.rb @@ -0,0 +1,15 @@ +# In 20140914145549_migrate_to_new_members_model.rb we forgot to set the +# created_at and updated_at times for new records in the 'members' table. This +# became a problem after commit c8e78d972a5a628870eefca0f2ccea0199c55bda which +# was added in GitLab 7.5. With this migration we ensure that all rows in +# 'members' have at least some created_at and updated_at timestamp. +class AddTimestampsToMembers < ActiveRecord::Migration + def up + execute "UPDATE members SET created_at = NOW() WHERE created_at is NULL" + execute "UPDATE members SET updated_at = NOW() WHERE updated_at is NULL" + end + + def down + # no change + end +end diff --git a/db/migrate/20141121161704_add_identity_table.rb b/db/migrate/20141121161704_add_identity_table.rb new file mode 100644 index 00000000000..6fe63637dfe --- /dev/null +++ b/db/migrate/20141121161704_add_identity_table.rb @@ -0,0 +1,38 @@ +class AddIdentityTable < ActiveRecord::Migration + def up + create_table :identities do |t| + t.string :extern_uid + t.string :provider + t.references :user + end + + add_index :identities, :user_id + + execute <<eos +INSERT INTO identities (provider, extern_uid, user_id) +SELECT provider, extern_uid, id FROM users +WHERE provider IS NOT NULL +eos + + remove_column :users, :extern_uid + remove_column :users, :provider + end + + def down + add_column :users, :extern_uid, :string + add_column :users, :provider, :string + + if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' + execute <<eos +UPDATE users u +SET provider = i.provider, extern_uid = i.extern_uid +FROM identities i +WHERE i.user_id = u.id +eos + else + execute "UPDATE users u, identities i SET u.provider = i.provider, u.extern_uid = i.extern_uid WHERE u.id = i.user_id" + end + + drop_table :identities + end +end diff --git a/db/migrate/20141205134006_add_locked_at_to_merge_request.rb b/db/migrate/20141205134006_add_locked_at_to_merge_request.rb new file mode 100644 index 00000000000..49651c44a82 --- /dev/null +++ b/db/migrate/20141205134006_add_locked_at_to_merge_request.rb @@ -0,0 +1,5 @@ +class AddLockedAtToMergeRequest < ActiveRecord::Migration + def change + add_column :merge_requests, :locked_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 84fd1256677..b8335c5841b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20141006143943) do +ActiveRecord::Schema.define(version: 20141205134006) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -74,6 +74,14 @@ ActiveRecord::Schema.define(version: 20141006143943) do add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree + create_table "identities", force: true do |t| + t.string "extern_uid" + t.string "provider" + t.integer "user_id" + end + + add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree + create_table "issues", force: true do |t| t.string "title" t.integer "assignee_id" @@ -173,6 +181,7 @@ ActiveRecord::Schema.define(version: 20141006143943) do t.integer "iid" t.text "description" t.integer "position", default: 0 + t.datetime "locked_at" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree @@ -293,20 +302,21 @@ ActiveRecord::Schema.define(version: 20141006143943) do create_table "snippets", force: true do |t| t.string "title" t.text "content" - t.integer "author_id", null: false + t.integer "author_id", null: false t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" t.string "file_name" t.datetime "expires_at" - t.boolean "private", default: true, null: false t.string "type" + t.integer "visibility_level", default: 0, null: false end add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree + add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree create_table "taggings", force: true do |t| t.integer "tag_id" @@ -349,8 +359,6 @@ ActiveRecord::Schema.define(version: 20141006143943) do t.string "bio" t.integer "failed_attempts", default: 0 t.datetime "locked_at" - t.string "extern_uid" - t.string "provider" t.string "username" t.boolean "can_create_group", default: true, null: false t.boolean "can_create_team", default: true, null: false @@ -359,6 +367,7 @@ ActiveRecord::Schema.define(version: 20141006143943) do t.integer "notification_level", default: 1, null: false t.datetime "password_expires_at" t.integer "created_by_id" + t.datetime "last_credential_check_at" t.string "avatar" t.string "confirmation_token" t.datetime "confirmed_at" @@ -366,7 +375,6 @@ ActiveRecord::Schema.define(version: 20141006143943) do t.string "unconfirmed_email" t.boolean "hide_no_ssh_key", default: false t.string "website_url", default: "", null: false - t.datetime "last_credential_check_at" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree @@ -374,7 +382,6 @@ ActiveRecord::Schema.define(version: 20141006143943) do add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree - add_index "users", ["extern_uid", "provider"], name: "index_users_on_extern_uid_and_provider", unique: true, using: :btree add_index "users", ["name"], name: "index_users_on_name", using: :btree add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree add_index "users", ["username"], name: "index_users_on_username", using: :btree diff --git a/doc/README.md b/doc/README.md index 2f90cf14a64..3c8f8ad3d03 100644 --- a/doc/README.md +++ b/doc/README.md @@ -2,24 +2,28 @@ ## User documentation -- [API](api/README.md) Explore how you can access GitLab via a simple and powerful API. -- [Markdown](markdown/markdown.md) Learn what you can do with GitLab's advanced formatting system. +- [API](api/README.md) Automate GitLab via a simple and powerful API. +- [Markdown](markdown/markdown.md) GitLab's advanced formatting system. - [Permissions](permissions/permissions.md) Learn what each role in a project (guest/reporter/developer/master/owner) can do. -- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to a project. +- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat. +- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Web hooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. -- [Workflow](workflow/README.md) Learn how to use Git and GitLab together. +- [Workflow](workflow/README.md) Learn how to get the maximum out of GitLab. ## Administrator documentation - [Install](install/README.md) Requirements, directory structures and manual installation. - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter. -- [Raketasks](raketasks/README.md) Explore what GitLab has in store for you to make administration easier. -- [System hooks](system_hooks/system_hooks.md) Let GitLab notify you when certain management tasks need to be carried out. +- [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects. +- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough. +- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed. - [Security](security/README.md) Learn what you can do to further secure your GitLab instance. - [Update](update/README.md) Update guides to upgrade your installation. - [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page. - [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. +- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. +- [Operations](operations/README.md) Keeping GitLab up and running ## Contributor documentation diff --git a/doc/api/README.md b/doc/api/README.md index f76a253083f..ffe250df3ff 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -21,13 +21,7 @@ ## Clients -- [php-gitlab-api](https://github.com/m4tthumphrey/php-gitlab-api) - PHP -- [Laravel API Wrapper for GitLab CE](https://github.com/adamgoose/gitlab) - PHP / [Laravel](http://laravel.com) -- [Ruby Wrapper](https://github.com/NARKOZ/gitlab) - Ruby -- [python-gitlab](https://github.com/Itxaka/python-gitlab) - Python -- [java-gitlab-api](https://github.com/timols/java-gitlab-api) - Java -- [node-gitlab](https://github.com/moul/node-gitlab) - Node.js -- [NGitLab](https://github.com/Scooletz/NGitLab) - .NET +Find API Clients for GitLab [on our website](https://about.gitlab.com/applications/#api-clients). ## Introduction @@ -158,7 +152,7 @@ When an attribute is missing, you will get something like: HTTP/1.1 400 Bad Request Content-Type: application/json - + { "message":"400 (Bad request) \"title\" not given" } @@ -167,7 +161,7 @@ When a validation error occurs, error messages will be different. They will hold HTTP/1.1 400 Bad Request Content-Type: application/json - + { "message": { "bio": [ diff --git a/doc/api/branches.md b/doc/api/branches.md index 74386615545..319f0b47386 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -211,3 +211,11 @@ Parameters: It return 200 if succeed, 404 if the branch to be deleted does not exist or 400 for other reasons. In case of an error, an explaining message is provided. + +Success response: + +```json +{ + "branch_name": "my-removed-branch" +} +``` diff --git a/doc/api/commits.md b/doc/api/commits.md index 9475ecbaa67..eb8d6a43592 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -93,3 +93,66 @@ Parameters: } ] ``` + +## Get the comments of a commit + +Get the comments of a commit in a project. + +``` +GET /projects/:id/repository/commits/:sha/comments +``` + +Parameters: + +- `id` (required) - The ID of a project +- `sha` (required) - The name of a repository branch or tag or if not given the default branch + +```json +[ + { + "note": "this code is really nice", + "author": { + "id": 11, + "username": "admin", + "email": "admin@local.host", + "name": "Administrator", + "state": "active", + "created_at": "2014-03-06T08:17:35.000Z" + } + } +] +``` + +## Post comment to commit + +Adds a comment to a commit. Optionally you can post comments on a specific line of a commit. Therefor both `path`, `line_new` and `line_old` are required. + +``` +POST /projects/:id/repository/commits/:sha/comments +``` + +Parameters: + +- `id` (required) - The ID of a project +- `sha` (required) - The name of a repository branch or tag or if not given the default branch +- `note` (required) - Text of comment +- `path` (optional) - The file path +- `line` (optional) - The line number +- `line_type` (optional) - The line type (new or old) + +```json +{ + "author": { + "id": 1, + "username": "admin", + "email": "admin@local.host", + "name": "Administrator", + "blocked": false, + "created_at": "2012-04-29T08:46:00Z" + }, + "note": "text1", + "path": "example.rb", + "line": 5, + "line_type": "new" +} +``` diff --git a/doc/api/projects.md b/doc/api/projects.md index dfe3502b6e4..0055e2e476f 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -186,6 +186,7 @@ Parameters: "target_id": 830, "target_type": "Issue", "author_id": 1, + "author_username": "john", "data": null, "target_title": "Public project search field" }, @@ -196,6 +197,7 @@ Parameters: "target_id": null, "target_type": null, "author_id": 1, + "author_username": "john", "data": { "before": "50d4420237a9de7be1304607147aec22e4a14af7", "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", @@ -231,6 +233,7 @@ Parameters: "target_id": 840, "target_type": "Issue", "author_id": 1, + "author_username": "john", "data": null, "target_title": "Finish & merge Code search PR" } diff --git a/doc/api/repositories.md b/doc/api/repositories.md index a412f60c0d9..8acf85d21c8 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -56,6 +56,7 @@ Parameters: [ { "name": "v1.0.0", + "message": "Release 1.0.0", "commit": { "id": "2695effb5807a22ff3d138d593fd856244e155e7", "parents": [], @@ -67,10 +68,11 @@ Parameters: "committed_date": "2012-05-28T04:42:42-07:00", "committer_email": "jack@example.com" }, - "protected": false } ] ``` +The message will be `nil` when creating a lightweight tag otherwise +it will contain the annotation. It returns 200 if the operation succeed. In case of an error, 405 with an explaining error message is returned. diff --git a/doc/api/services.md b/doc/api/services.md new file mode 100644 index 00000000000..ab9f9c00c67 --- /dev/null +++ b/doc/api/services.md @@ -0,0 +1,46 @@ +# Services + +## GitLab CI + +### Edit GitLab CI service + +Set GitLab CI service for a project. + +``` +PUT /projects/:id/services/gitlab-ci +``` + +Parameters: + +- `token` (required) - CI project token +- `project_url` (required) - CI project url + +### Delete GitLab CI service + +Delete GitLab CI service settings for a project. + +``` +DELETE /projects/:id/services/gitlab-ci +``` + +## Hipchat + +### Edit Hipchat service + +Set Hipchat service for project. + +``` +PUT /projects/:id/services/hipchat +``` +Parameters: + +- `token` (required) - Hipchat token +- `room` (required) - Hipchat room name + +### Delete Hipchat service + +Delete Hipchat service for a project. + +``` +DELETE /projects/:id/services/hipchat +``` diff --git a/doc/api/users.md b/doc/api/users.md index 3fdd3a75e88..b30a31deccc 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -78,7 +78,8 @@ GET /users "is_admin": false, "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", "can_create_group": true, - "can_create_project": true + "can_create_project": true, + "projects_limit": 100 } ] ``` @@ -140,7 +141,8 @@ Parameters: "color_scheme_id": 2, "is_admin": false, "can_create_group": true, - "can_create_project": true + "can_create_project": true, + "projects_limit": 100 } ``` @@ -240,7 +242,8 @@ GET /user "color_scheme_id": 2, "is_admin": false, "can_create_group": true, - "can_create_project": true + "can_create_project": true, + "projects_limit": 100 } ``` @@ -257,12 +260,14 @@ GET /user/keys { "id": 1, "title": "Public key", - "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", + "created_at": "2014-08-01T14:47:39.080Z" }, { "id": 3, "title": "Another Public key", - "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", + "created_at": "2014-08-01T14:47:39.080Z" } ] ``` @@ -299,7 +304,8 @@ Parameters: { "id": 1, "title": "Public key", - "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", + "created_at": "2014-08-01T14:47:39.080Z" } ``` diff --git a/doc/customization/libravatar.md b/doc/customization/libravatar.md new file mode 100644 index 00000000000..4dffd3027a9 --- /dev/null +++ b/doc/customization/libravatar.md @@ -0,0 +1,69 @@ +# Use Libravatar service with GitLab + +GitLab by default supports [Gravatar](gravatar.com) avatar service. +Libravatar is a service which delivers your avatar (profile picture) to other websites and their API is +[heavily based on gravatar](http://wiki.libravatar.org/api/). + +This means that it is not complicated to switch to Libravatar avatar service or even self hosted Libravatar server. + +# Configuration + +In [gitlab.yml gravatar section](https://gitlab.com/gitlab-org/gitlab-ce/blob/672bd3902d86b78d730cea809fce312ec49d39d7/config/gitlab.yml.example#L122) set +the configuration options as follows: + +## For HTTP + +```yml + gravatar: + enabled: true + # gravatar urls: possible placeholders: %{hash} %{size} %{email} + plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon" +``` + +## For HTTPS + +```yml + gravatar: + enabled: true + # gravatar urls: possible placeholders: %{hash} %{size} %{email} + ssl_url: "https://seccdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon" +``` + +## Self-hosted + +If you are [running your own libravatar service](http://wiki.libravatar.org/running_your_own/) the url will be different in the configuration +but the important part is to provide the same placeholders so GitLab can parse the url correctly. + +For example, you host a service on `http://libravatar.example.com` the `plain_url` you need to supply in `gitlab.yml` is + +`http://libravatar.example.com/avatar/%{hash}?s=%{size}&d=identicon` + + +## Omnibus-gitlab example + +In `/etc/gitlab/gitlab.rb`: + +#### For http + +```ruby +gitlab_rails['gravatar_enabled'] = true +gitlab_rails['gravatar_plain_url'] = "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon" +``` + +#### For https + +```ruby +gitlab_rails['gravatar_enabled'] = true +gitlab_rails['gravatar_ssl_url'] = "https://seccdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon" +``` + + +Run `sudo gitlab-ctl reconfigure` for changes to take effect. + + +## Default URL for missing images + +[Libravatar supports different sets](http://wiki.libravatar.org/api/) of `missing images` for emails not found on the Libravatar service. + +In order to use a different set other than `identicon`, replace `&d=identicon` portion of the url with another supported set. +For example, you can use `retro` set in which case url would look like: `plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=retro"` diff --git a/doc/development/README.md b/doc/development/README.md index 20db6662aca..c31e5d7ae97 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -4,3 +4,4 @@ - [Shell commands](shell_commands.md) in the GitLab codebase - [Rake tasks](rake_tasks.md) for development - [CI setup](ci_setup.md) for testing GitLab +- [Sidekiq debugging](sidekiq_debugging.md) diff --git a/doc/development/architecture.md b/doc/development/architecture.md index 4624d9f60b6..209182e7742 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -2,12 +2,44 @@ ## Software delivery -There are two editions of GitLab: [Enterprise Edition](https://www.gitlab.com/gitlab-ee/) (EE) and [Community Edition](https://www.gitlab.com/gitlab-ce/) (CE). GitLab CE is delivered via git from the [gitlabhq repository](https://gitlab.com/gitlab-org/gitlab-ce/tree/master). New versions of GitLab are released in stable branches and the master branch is for bleeding edge development. +There are two editions of GitLab: [Enterprise Edition](https://about.gitlab.com/gitlab-ee/) (EE) and [Community Edition](https://about.gitlab.com/gitlab-ce/) (CE). GitLab CE is delivered via git from the [gitlabhq repository](https://gitlab.com/gitlab-org/gitlab-ce/tree/master). New versions of GitLab are released in stable branches and the master branch is for bleeding edge development. EE releases are available not long after CE releases. To obtain the GitLab EE there is a [repository at gitlab.com](https://gitlab.com/subscribers/gitlab-ee). For more information about the release process see the section 'New versions and upgrading' in the readme. Both EE and CE require an add-on component called gitlab-shell. It is obtained from the [gitlab-shell repository](https://gitlab.com/gitlab-org/gitlab-shell/tree/master). New versions are usually tags but staying on the master branch will give you the latest stable version. New releases are generally around the same time as GitLab CE releases with exception for informal security updates deemed critical. +## Physical office analogy + +You can imagine GitLab as a physical office. + +**The repositories** are the goods GitLab handling. +They can be stored in a warehouse. +This can be either a hard disk, or something more complex, such as a NFS filesystem; + +**NginX** acts like the front-desk. +Users come to NginX and request actions to be done by workers in the office; + +**The database** is a series of metal file cabinets with information on: + - The goods in the warehouse (metadata, issues, merge requests etc); + - The users coming to the front desk (permissions) + +**Redis** is a communication board with “cubby holes” that can contain tasks for office workers; + +**Sidekiq** is a worker that primarily handles sending out emails. +It takes tasks from the Redis communication board; + +**A Unicorn worker** is a worker that handles quick/mundane tasks. +They work with the communication board (Redis). +Their job description: + - check permissions by checking the user session stored in a Redis “cubby hole”; + - make tasks for Sidekiq; + - fetch stuff from the warehouse or move things around in there; + +**Gitlab-shell** is a third kind of worker that takes orders from a fax machine (SSH) instead of the front desk (HTTP). +Gitlab-shell communicates with Sidekiq via the “communication board” (Redis), and asks quick questions of the Unicorn workers either directly or via the front desk. + +**GitLab Enterprise Edition (the application)** is the collection of processes and business practices that the office is run by. + ## System Layout When referring to ~git in the pictures it means the home directory of the git user which is typically /home/git. diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md index b3e84183a41..ee16aedafe7 100644 --- a/doc/development/ci_setup.md +++ b/doc/development/ci_setup.md @@ -4,28 +4,30 @@ This document describes what services we use for testing GitLab and GitLab CI. We currently use three CI services to test GitLab: -1. GitLab CI on [GitHost.io](https://gitlab-ce.githost.io/projects/2/) for the [GitLab.com repo](https://gitlab.com/gitlab-org/gitlab-ce) +1. GitLab CI on [GitHost.io](https://gitlab-ce.githost.io/projects/4/) for the [GitLab.com repo](https://gitlab.com/gitlab-org/gitlab-ce) 2. GitLab CI at ci.gitlab.org to test the private GitLab B.V. repo at dev.gitlab.org 3. [Semephore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for [GitHub.com repo](https://github.com/gitlabhq/gitlabhq) | Software @ configuration being tested | GitLab CI (ci.gitlab.org) | GitLab CI (GitHost.io) | Semaphore | -|---------------------------------------|---------------------------|------------------------|-----------| -| GitLab CE @ MySQL | ✓ | ✓ | | -| GitLab CE @ PostgreSQL | | | ✓ | -| GitLab EE @ MySQL | ✓ | | | -| GitLab CI @ MySQL | ✓ | | | -| GitLab CI @ PostgreSQL | | | ✓ | -| GitLab CI Runner | ✓ | | ✓ | -| GitLab Shell | ✓ | | ✓ | -| GitLab Shell | ✓ | | ✓ | +|---------------------------------------|---------------------------|---------------------------------------------------------------------------|-----------| +| GitLab CE @ MySQL | ✓ | ✓ [Core team can trigger builds](https://gitlab-ce.githost.io/projects/4) | | +| GitLab CE @ PostgreSQL | | | ✓ [Core team can trigger builds](https://semaphoreapp.com/gitlabhq/gitlabhq/branches/master) | +| GitLab EE @ MySQL | ✓ | | | +| GitLab CI @ MySQL | ✓ | | | +| GitLab CI @ PostgreSQL | | | ✓ | +| GitLab CI Runner | ✓ | | ✓ | +| GitLab Shell | ✓ | | ✓ | +| GitLab Shell | ✓ | | ✓ | + +Core team has access to trigger builds if needed for GitLab CE. We use [these build scripts](https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/examples/build_script_gitlab_ce.md) for testing with GitLab CI. # Build configuration on [Semaphore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for testing the [GitHub.com repo](https://github.com/gitlabhq/gitlabhq) -Language: Ruby -Ruby verion: 2.1.2 -database.yml: pg +- Language: Ruby +- Ruby verion: 2.1.2 +- database.yml: pg Build commands diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index 6d9ac161e91..53f8095cb13 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -1,6 +1,6 @@ # Rake tasks for developers -## Setup db with developer seeds: +## Setup db with developer seeds Note that if your db user does not have advanced privileges you must create the db manually before running this command. @@ -8,6 +8,10 @@ Note that if your db user does not have advanced privileges you must create the bundle exec rake setup ``` +The `setup` task is a alias for `gitlab:setup`. +This tasks calls `db:setup` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and fianlly it calls `db:seed_fu` to seed the database. +Note: `db:setup` calls `db:seed` but this does nothing. + ## Run tests This runs all test suites present in GitLab. diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md index 23c8365c340..1e51ad73e32 100644 --- a/doc/development/shell_commands.md +++ b/doc/development/shell_commands.md @@ -1,5 +1,8 @@ # Guidelines for shell commands in the GitLab codebase +This document contains guidelines for working with processes and files in the GitLab codebase. +These guidelines are meant to make your code more reliable _and_ secure. + ## References - [Google Ruby Security Reviewer's Guide](https://code.google.com/p/ruby-security/wiki/Guide) @@ -109,3 +112,63 @@ logs = IO.popen(%W(git log), chdir: repo_dir).read ``` Note that unlike `Gitlab::Popen.popen`, `IO.popen` does not capture standard error. + +## Avoid user input at the start of path strings + +Various methods for opening and reading files in Ruby can be used to read the +standard output of a process instead of a file. The following two commands do +roughly the same: + +``` +`touch /tmp/pawned-by-backticks` +File.read('|touch /tmp/pawned-by-file-read') +``` + +The key is to open a 'file' whose name starts with a `|`. +Affected methods include Kernel#open, File::read, File::open, IO::open and IO::read. + +You can protect against this behavior of 'open' and 'read' by ensuring that an +attacker cannot control the start of the filename string you are opening. For +instance, the following is sufficient to protect against accidentally starting +a shell command with `|`: + +``` +# we assume repo_path is not controlled by the attacker (user) +path = File.join(repo_path, user_input) +# path cannot start with '|' now. +File.read(path) +``` + +## Guard against path traversal + +Path traversal is a security where the program (GitLab) tries to restrict user +access to a certain directory on disk, but the user manages to open a file +outside that directory by taking advantage of the `../` path notation. + +``` +# Suppose the user gave us a path and they are trying to trick us +user_input = '../other-repo.git/other-file' + +# We look up the repo path somewhere +repo_path = 'repositories/user-repo.git' + +# The intention of the code below is to open a file under repo_path, but +# because the user used '..' she can 'break out' into +# 'repositories/other-repo.git' +full_path = File.join(repo_path, user_input) +File.open(full_path) do # Oops! +``` + +A good way to protect against this is to compare the full path with its +'absolute path' according to Ruby's `File.absolute_path`. + +``` +full_path = File.join(repo_path, user_input) +if full_path != File.absolute_path(full_path) + raise "Invalid path: #{full_path.inspect}" +end + +File.open(full_path) do # Etc. +``` + +A check like this could have avoided CVE-2013-4583. diff --git a/doc/development/sidekiq_debugging.md b/doc/development/sidekiq_debugging.md new file mode 100644 index 00000000000..cea11e5f126 --- /dev/null +++ b/doc/development/sidekiq_debugging.md @@ -0,0 +1,14 @@ +# Sidekiq debugging + +## Log arguments to Sidekiq jobs + +If you want to see what arguments are being passed to Sidekiq jobs you can set +the SIDEKIQ_LOG_ARGUMENTS environment variable. + +``` +SIDEKIQ_LOG_ARGUMENTS=1 bundle exec foreman start +``` + +It is not recommend to enable this setting in production because some Sidekiq +jobs (such as sending a password reset email) take secret arguments (for +example the password reset token). diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md new file mode 100644 index 00000000000..00867ead80d --- /dev/null +++ b/doc/hooks/custom_hooks.md @@ -0,0 +1,41 @@ +# Custom Git Hooks + +**Note: Custom git hooks must be configured on the filesystem of the GitLab +server. Only GitLab server administrators will be able to complete these tasks. +Please explore webhooks as an option if you do not have filesystem access.** + +Git natively supports hooks that are executed on different actions. +Examples of server-side git hooks include pre-receive, post-receive, and update. +See +[Git SCM Server-Side Hooks](http://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks) +for more information about each hook type. + +As of gitlab-shell version 2.2.0 (which requires GitLab 7.5+), GitLab +administrators can add custom git hooks to any GitLab project. + +## Setup + +Normally, git hooks are placed in the repository or project's `hooks` directory. +GitLab creates a symlink from each project's `hooks` directory to the +gitlab-shell `hooks` directory for ease of maintenance between gitlab-shell +upgrades. As such, custom hooks are implemented a little differently. Behavior +is exactly the same once the hook is created, though. Follow these steps to +set up a custom hook. + +1. Pick a project that needs a custom git hook. +1. On the GitLab server, navigate to the project's repository directory. +For a manual install the path is usually +`/home/git/repositories/<group>/<project>.git`. For Omnibus installs the path is +usually `/var/opt/gitlab/git-data/repositories/<group>/<project>.git`. +1. Create a new directory in this location called `custom_hooks`. +1. Inside the new `custom_hooks` directory, create a file with a name matching +the hook type. For a pre-receive hook the file name should be `pre-receive` with +no extension. +1. Make the hook file executable and make sure it's owned by git. +1. Write the code to make the git hook function as expected. Hooks can be +in any language. Ensure the 'shebang' at the top properly reflects the language +type. For example, if the script is in Ruby the shebang will probably be +`#!/usr/bin/env ruby`. + +That's it! Assuming the hook code is properly implemented the hook will fire +as appropriate. diff --git a/doc/install/installation.md b/doc/install/installation.md index af6e182cfa0..aa04116779e 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -1,18 +1,22 @@ # Installation -## Select Version to Install +## Consider the Omnibus package installation + +Since a manual installation is a lot of work and error prone we strongly recommend the fast and reliable [Omnibus package installation](https://about.gitlab.com/downloads/) (deb/rpm). -Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install. In most cases this should be the highest numbered stable branch (example shown below). +## Select Version to Install -![Select latest branch](https://i.imgur.com/Lrdxk1k.png) +Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the tag (version) of GitLab you would like to install. +In most cases this should be the highest numbered production tag (without rc in it). +You can select the tag in the version dropdown in the top left corner of GitLab (below the menu bar). -If the highest number stable branch is unclear please check the [GitLab Blog](https://www.gitlab.com/blog/) for installation guide links by version. +If the highest number stable branch is unclear please check the [GitLab Blog](https://about.gitlab.com/blog/) for installation guide links by version. ## Important Notes This guide is long because it covers many cases and includes all commands you need, this is [one of the few installation scripts that actually works out of the box](https://twitter.com/robinvdvleuten/status/424163226532986880). -This installation guide was created for and tested on **Debian/Ubuntu** operating systems. Please read [doc/install/requirements.md](./requirements.md) for hardware and operating system requirements. If you want to install on RHEL/CentOS we recommend using the [Omnibus packages](https://www.gitlab.com/downloads/). +This installation guide was created for and tested on **Debian/Ubuntu** operating systems. Please read [doc/install/requirements.md](./requirements.md) for hardware and operating system requirements. If you want to install on RHEL/CentOS we recommend using the [Omnibus packages](https://about.gitlab.com/downloads/). This is the official installation guide to set up a production server. To set up a **development installation** or for many other installation options please see [the installation section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#installation). @@ -50,7 +54,7 @@ up-to-date and install it. Install the required packages (needed to compile Ruby and native extensions to Ruby gems): - sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake + sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake libkrb5-dev Make sure you have the right version of Git installed @@ -70,8 +74,9 @@ Is the system packaged Git too old? Remove it and compile from source. # Download and compile from source cd /tmp - curl -L --progress https://www.kernel.org/pub/software/scm/git/git-2.0.0.tar.gz | tar xz - cd git-2.0.0/ + curl -L --progress https://www.kernel.org/pub/software/scm/git/git-2.1.2.tar.gz | tar xz + cd git-2.1.2/ + ./configure make prefix=/usr/local all # Install into /usr/local/bin @@ -87,7 +92,7 @@ Then select 'Internet Site' and press enter to confirm the hostname. ## 2. Ruby -The use of ruby version managers such as [RVM](http://rvm.io/), [rbenv](https://github.com/sstephenson/rbenv) or [chruby](https://github.com/postmodern/chruby) with GitLab in production frequently leads to hard to diagnose problems. For example, GitLab Shell is called from OpenSSH and having a version manager can prevent pushing and pulling over SSH. Version managers are not supported and we strongly advise everyone to follow the instructions below to use a system ruby. +The use of Ruby version managers such as [RVM](http://rvm.io/), [rbenv](https://github.com/sstephenson/rbenv) or [chruby](https://github.com/postmodern/chruby) with GitLab in production frequently leads to hard to diagnose problems. For example, GitLab Shell is called from OpenSSH and having a version manager can prevent pushing and pulling over SSH. Version managers are not supported and we strongly advise everyone to follow the instructions below to use a system Ruby. Remove the old Ruby 1.8 if present @@ -96,8 +101,8 @@ Remove the old Ruby 1.8 if present Download Ruby and compile it: mkdir /tmp/ruby && cd /tmp/ruby - curl -L --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz - cd ruby-2.1.2 + curl -L --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.5.tar.gz | tar xz + cd ruby-2.1.5 ./configure --disable-install-rdoc make sudo make install @@ -122,7 +127,8 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da # Login to PostgreSQL sudo -u postgres psql -d template1 - # Create a user for GitLab. + # Create a user for GitLab + # Do not type the 'template1=#', this is part of the prompt template1=# CREATE USER git CREATEDB; # Create the GitLab production database & grant all privileges on database @@ -133,6 +139,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da # Try connecting to the new database with the new user sudo -u git -H psql -d gitlabhq_production + + # Quit the database session + gitlabhq_production> \q ## 5. Redis @@ -146,6 +155,17 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da # Enable Redis socket for default Debian / Ubuntu path echo 'unixsocket /var/run/redis/redis.sock' | sudo tee -a /etc/redis/redis.conf + # Grant permission to the socket to all members of the redis group + echo 'unixsocketperm 770' | sudo tee -a /etc/redis/redis.conf + + # Create the directory which contains the socket + mkdir /var/run/redis + chown redis:redis /var/run/redis + chmod 755 /var/run/redis + # Persist the directory which contains the socket, if applicable + if [ -d /etc/tmpfiles.d ]; then + echo 'd /var/run/redis 0755 redis redis 10d -' | sudo tee -a /etc/tmpfiles.d/redis.conf + fi # Activate the changes to redis.conf sudo service redis-server restart @@ -161,9 +181,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-3-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-6-stable gitlab -**Note:** You can change `7-3-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `7-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -179,7 +199,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da # Make sure GitLab can write to the log/ and tmp/ directories sudo chown -R git log/ sudo chown -R git tmp/ - sudo chmod -R u+rwX log/ + sudo chmod -R u+rwX,go-w log/ sudo chmod -R u+rwX tmp/ # Create directory for satellites @@ -258,7 +278,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da GitLab Shell is an SSH access and repository management software developed specially for GitLab. # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install[v2.0.1] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production + sudo -u git -H bundle exec rake gitlab:shell:install[v2.4.0] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production # By default, the gitlab-shell config is generated from your main GitLab config. # You can review (and modify) the gitlab-shell config as follows: @@ -363,15 +383,17 @@ NOTE: Supply `SANITIZE=true` environment variable to `gitlab:check` to omit proj ### Initial Login -Visit YOUR_SERVER in your web browser for your first GitLab login. The setup has created an admin account for you. You can use it to log in: +Visit YOUR_SERVER in your web browser for your first GitLab login. The setup has created a default admin account for you. You can use it to log in: root 5iveL!fe -**Important Note:** Please go over to your profile page and immediately change the password, so nobody can access your GitLab by using this login information later on. +**Important Note:** Please login to the server before exposing it to the public internet. On login you'll be prompted to change the password. **Enjoy!** +You can use `sudo service gitlab start` and `sudo service gitlab stop` to start and stop GitLab. + ## Advanced Setup Tips ### Using HTTPS diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 49edf36f574..8eabb219b1b 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -24,7 +24,7 @@ For the installations options please see [the installation page on the GitLab we On the above unsupported distributions is still possible to install GitLab yourself. Please see the [manual installation guide](https://github.com/gitlabhq/gitlabhq/blob/master/doc/install/installation.md) and the [unofficial installation guides](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Unofficial-Installation-Guides) on the public wiki for more information. -### Non Unix operating systems such as Windows +### Non-Unix operating systems such as Windows GitLab is developed for Unix operating systems. GitLab does **not** run on Windows and we have no plans of supporting it in the near future. @@ -38,6 +38,16 @@ We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab ## Hardware requirements +### Storage + +The necessary hard drive space largely depends on the size of the repos you want to store in GitLab but as a *rule of thumb* you should have at least twice as much free space as all your repos combined take up. You need twice the storage because [GitLab satellites](structure.md) contain an extra copy of each repo. + +If you want to be flexible about growing your hard drive space in the future consider mounting it using LVM so you can add more hard drives when you need them. + +Apart from a local hard drive you can also mount a volume that supports the network file system (NFS) protocol. This volume might be located on a file server, a network attached storage (NAS) device, a storage area network (SAN) or on an Amazon Web Services (AWS) Elastic Block Store (EBS) volume. + +If you have enough RAM memory and a recent CPU the speed of GitLab is mainly limited by hard drive seek times. Having a fast drive (7200 RPM and up) or a solid state drive (SSD) will improve the responsiveness of GitLab. + ### CPU - 1 core works supports up to 100 users but the application can be a bit slower due to having all workers and background jobs running on the same core @@ -50,11 +60,10 @@ We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab ### Memory -- 512MB is the absolute minimum but we do not recommend this amount of memory. -You will either need to configure 512MB or 1.5GB of swap space. -With 512MB of swap space you must configure only one unicorn worker. -With one unicorn worker only git over ssh access will work because the git over http access requires two running workers (one worker to receive the user request and one worker for the authorization check). -If you use SSD storage and configure 1.5GB of swap space you can use two Unicorn workers, this will allow http access but it will still be slow. +You need at least 2GB of addressable memory (RAM + swap) to install and use GitLab! +With less memory GitLab will give strange errors during the reconfigure run and 500 errors during usage. + +- 512MB RAM + 1.5GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advise. - 1GB RAM + 1GB swap supports up to 100 users - **2GB RAM** is the **recommended** memory size and supports up to 500 users - 4GB RAM supports up to 2,000 users @@ -65,15 +74,16 @@ If you use SSD storage and configure 1.5GB of swap space you can use two Unicorn Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. -### Storage - -The necessary hard drive space largely depends on the size of the repos you want to store in GitLab. But as a *rule of thumb* you should have at least twice as much free space as your all repos combined take up. You need twice the storage because [GitLab satellites](structure.md) contain an extra copy of each repo. +## Unicorn Workers -If you want to be flexible about growing your hard drive space in the future consider mounting it using LVM so you can add more hard drives when you need them. - -Apart from a local hard drive you can also mount a volume that supports the network file system (NFS) protocol. This volume might be located on a file server, a network attached storage (NAS) device, a storage area network (SAN) or on an Amazon Web Services (AWS) Elastic Block Store (EBS) volume. +It's possible to increase the amount of unicorn workers and tis will usually help for to reduce the response time of the applications. +For most instances we recommend using: CPU cores + 1 = unicorn workers. +So for a machine with 2 cores, 3 unicorn workers is ideal. -If you have enough RAM memory and a recent CPU the speed of GitLab is mainly limited by hard drive seek times. Having a fast drive (7200 RPM and up) or a solid state drive (SSD) will improve the responsiveness of GitLab. +For all machines that have 1GB and up we recommend a minimum of two unicorn workers. +If you have a 512MB machine with a magnetic (non-SSD) swap drive we recommend to configure only one Unicorn worker to prevent excessive swapping. +With one Unicorn worker only git over ssh access will work because the git over HTTP access requires two running workers (one worker to receive the user request and one worker for the authorization check). +If you have a 512MB machine with a SSD drive you can use two Unicorn workers, this will allow HTTP access although it will be slow due to swapping. ## Database @@ -85,12 +95,12 @@ Redis stores all user sessions and the background task queue. The storage requirements for Redis are minimal, about 25kB per user. Sidekiq processes the background jobs with a multithreaded process. This process starts with the entire Rails stack (200MB+) but it can grow over time due to memory leaks. -On a very active server (10.000 active users) the Sidekiq process can use 1GB+ of memory. +On a very active server (10,000 active users) the Sidekiq process can use 1GB+ of memory. -## Supported webbrowsers +## Supported web browsers - Chrome (Latest stable version) -- Firefox (Latest released version) +- Firefox (Latest released version and [latest ESR version](https://www.mozilla.org/en-US/firefox/organizations/)) - Safari 7+ (known problem: required fields in html5 do not work) - Opera (Latest released version) - IE 10+ diff --git a/doc/integration/gitlab_actions.png b/doc/integration/gitlab_actions.png Binary files differnew file mode 100644 index 00000000000..b08f54d137b --- /dev/null +++ b/doc/integration/gitlab_actions.png diff --git a/doc/integration/gitlab_buttons_in_gmail.md b/doc/integration/gitlab_buttons_in_gmail.md new file mode 100644 index 00000000000..0816509c557 --- /dev/null +++ b/doc/integration/gitlab_buttons_in_gmail.md @@ -0,0 +1,28 @@ +# GitLab buttons in gmail + +GitLab supports [Google actions in email](https://developers.google.com/gmail/markup/actions/actions-overview). + +If correctly setup, emails that require an action will be marked in Gmail. + +![gitlab_actions](gitlab_actions.png) + +To get this functioning, you need to be registered with Google. +[See how to register with google in this document.](https://developers.google.com/gmail/markup/registering-with-google) + +To aid the registering with google, GitLab offers a rake task that will send an email to google whitelisting email address from your GitLab server. + +To check what would be sent to the google email address, run the rake task: + +```bash +bundle exec rake gitlab:mail_google_schema_whitelisting RAILS_ENV=production +``` + +**This will not send the email but give you the output of how the mail will look.** + +Copy the output of the rake task to [google email markup tester](https://www.google.com/webmasters/markup-tester/u/0/) and press "Validate". + +If you receive "No errors detected" message from the tester you can send the email using: + +```bash +bundle exec rake gitlab:mail_google_schema_whitelisting RAILS_ENV=production SEND=true +`` diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index ee472ac3e3b..56b0d826adb 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -6,6 +6,95 @@ The first time a user signs in with LDAP credentials, GitLab will create a new G GitLab user attributes such as nickname and email will be copied from the LDAP user entry. +## Configuring GitLab for LDAP integration + +To enable GitLab LDAP integration you need to add your LDAP server settings in `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. +In GitLab Enterprise Edition you can have multiple LDAP servers connected to one GitLab server. + +Please note that before version 7.4, GitLab used a different syntax for configuring LDAP integration. +The old LDAP integration syntax still works in GitLab 7.4. +If your `gitlab.rb` or `gitlab.yml` file contains LDAP settings in both the old syntax and the new syntax, only the __old__ syntax will be used by GitLab. + +```ruby +# For omnibus packages +gitlab_rails['ldap_enabled'] = true +gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below +main: # 'main' is the GitLab 'provider ID' of this LDAP server + ## label + # + # A human-friendly name for your LDAP server. It is OK to change the label later, + # for instance if you find out it is too large to fit on the web page. + # + # Example: 'Paris' or 'Acme, Ltd.' + label: 'LDAP' + + host: '_your_ldap_server' + port: 636 + uid: 'sAMAccountName' + method: 'ssl' # "tls" or "ssl" or "plain" + bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' + password: '_the_password_of_the_bind_user' + + # This setting specifies if LDAP server is Active Directory LDAP server. + # For non AD servers it skips the AD specific queries. + # If your LDAP server is not AD, set this to false. + active_directory: true + + # If allow_username_or_email_login is enabled, GitLab will ignore everything + # after the first '@' in the LDAP username submitted by the user on login. + # + # Example: + # - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials; + # - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'. + # + # If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to + # disable this setting, because the userPrincipalName contains an '@'. + allow_username_or_email_login: false + + # Base where we can search for users + # + # Ex. ou=People,dc=gitlab,dc=example + # + base: '' + + # Filter LDAP users + # + # Format: RFC 4515 http://tools.ietf.org/search/rfc4515 + # Ex. (employeeType=developer) + # + # Note: GitLab does not support omniauth-ldap's custom filter syntax. + # + user_filter: '' + +# GitLab EE only: add more LDAP servers +# Choose an ID made of a-z and 0-9 . This ID will be stored in the database +# so that GitLab can remember which LDAP server a user belongs to. +# uswest2: +# label: +# host: +# .... +EOS +``` + +If you are using a GitLab installation from source you can find the LDAP settings in `/home/git/gitlab/config/gitlab.yml`: + +``` +production: + # snip... + ldap: + enabled: false + servers: + main: # 'main' is the GitLab 'provider ID' of this LDAP server + ## label + # + # A human-friendly name for your LDAP server. It is OK to change the label later, + # for instance if you find out it is too large to fit on the web page. + # + # Example: 'Paris' or 'Acme, Ltd.' + label: 'LDAP' + # snip... +``` + ## Enabling LDAP sign-in for existing GitLab users When a user signs in to GitLab with LDAP for the first time, and their LDAP email address is the primary email address of an existing GitLab user, then the LDAP DN will be associated with the existing user. @@ -24,15 +113,22 @@ If you want to limit all GitLab access to a subset of the LDAP users on your LDA The filter must comply with [RFC 4515](http://tools.ietf.org/search/rfc4515). ```ruby -# For omnibus-gitlab -gitlab_rails['ldap_user_filter'] = '(employeeType=developer)' +# For omnibus packages; new LDAP server syntax +gitlab_rails['ldap_servers'] = YAML.load <<-EOS +main: + # snip... + user_filter: '(employeeType=developer)' +EOS ``` ```yaml -# For installations from source +# For installations from source; new LDAP server syntax production: ldap: - user_filter: '(employeeType=developer)' + servers: + main: + # snip... + user_filter: '(employeeType=developer)' ``` Tip: if you want to limit access to the nested members of an Active Directory group you can use the following syntax: diff --git a/doc/integration/slack.md b/doc/integration/slack.md index 95cb0c6fae2..f2e73f272ef 100644 --- a/doc/integration/slack.md +++ b/doc/integration/slack.md @@ -4,15 +4,23 @@ To enable Slack integration you must create an Incoming WebHooks integration on Slack; -1. Sign in to [Slack](https://slack.com) (https://YOURSUBDOMAIN.slack.com/services) -1. Click on the Integrations menu at the top of the page. -1. Add a new Integration. +1. [Sign in to Slack](https://slack.com/signin) + +1. Select **Configure Integrations** from the dropdown next to your team name. + +1. Select the **All Services** tab + +1. Click **Add** next to Incoming Webhooks + 1. Pick Incoming WebHooks -1. Choose the channel name you want to send notifications to, in the Settings section -1. Add Integrations. - - Optional step; You can change bot's name and avatar by clicking "change the name of your bot", and "change the icon" after that you have to click "Save settings". -Now, Slack is ready to get external hooks. Before you leave this page don't forget to get the Token that you'll need on GitLab. You can find it by clicking Expand button, located in the "Instructions for creating Incoming WebHooks" section. It's a random alpha-numeric text 24 characters long. +1. Choose the channel name you want to send notifications to + +1. Click **Add Incoming WebHooks Integration**Add Integrations. + - Optional step; You can change bot's name and avatar by clicking modifying the bot name or avatar under **Integration Settings**. + +1. Copy the **Webhook URL**, we'll need this later for GitLab. + ## On GitLab @@ -26,10 +34,8 @@ After Slack is ready we need to setup GitLab. Here are the steps to achieve this 1. Fill in your Slack details - - Mark as active it - - Type your subdomain's prefix (If your subdomain is https://somedomain.slack.com you only have to type the somedomain) - - Type in the token you got from Slack - - Type in the channel name you want to use (eg. #announcements) + - Mark it as active + - Paste in the webhook url you got from Slack Have fun :) diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md index d1b52927d30..b9e501c5ec1 100644 --- a/doc/integration/twitter.md +++ b/doc/integration/twitter.md @@ -13,7 +13,7 @@ To enable the Twitter OmniAuth provider you must register your application with something else descriptive. - Description: Create a description. - Website: The URL to your GitLab installation. 'https://gitlab.example.com' - - Callback URL: 'https://gitlab.example.com/users/auth/github/callback' + - Callback URL: 'https://gitlab.example.com/users/auth/twitter/callback' - Agree to the "Rules of the Road." ![Twitter App Details](twitter_app_details.png) diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 6d96da76ad7..edb7a975503 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -510,6 +510,10 @@ Code above produces next output: | cell 1 | cell 2 | | cell 3 | cell 4 | +**Note** + +The row of dashes between the table header and body must have at least three dashes in each column. + ## References - This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). diff --git a/doc/operations/README.md b/doc/operations/README.md new file mode 100644 index 00000000000..31b1b583b0c --- /dev/null +++ b/doc/operations/README.md @@ -0,0 +1,3 @@ +# GitLab operations + +- [Sidekiq MemoryKiller](sidekiq_memory_killer.md) diff --git a/doc/operations/sidekiq_memory_killer.md b/doc/operations/sidekiq_memory_killer.md new file mode 100644 index 00000000000..867b01b0d5a --- /dev/null +++ b/doc/operations/sidekiq_memory_killer.md @@ -0,0 +1,38 @@ +# Sidekiq MemoryKiller + +The GitLab Rails application code suffers from memory leaks. For web requests +this problem is made manageable using +[unicorn-worker-killer](https://github.com/kzk/unicorn-worker-killer) which +restarts Unicorn worker processes in between requests when needed. The Sidekiq +MemoryKiller applies the same approach to the Sidekiq processes used by GitLab +to process background jobs. + +Unlike unicorn-worker-killer, which is enabled by default for all GitLab +installations since GitLab 6.4, the Sidekiq MemoryKiller is enabled by default +_only_ for Omnibus packages. The reason for this is that the MemoryKiller +relies on Runit to restart Sidekiq after a memory-induced shutdown and GitLab +installations from source do not all use Runit or an equivalent. + +With the default settings, the MemoryKiller will cause a Sidekiq restart no +more often than once every 15 minutes, with the restart causing about one +minute of delay for incoming background jobs. + +## Configuring the MemoryKiller + +The MemoryKiller is controlled using environment variables. + +- `SIDEKIQ_MEMORY_KILLER_MAX_RSS`: if this variable is set, and its value is + greater than 0, then after each Sidekiq job, the MemoryKiller will check the + RSS of the Sidekiq process that executed the job. If the RSS of the Sidekiq + process (expressed in kilobytes) exceeds SIDEKIQ_MEMORY_KILLER_MAX_RSS, a + delayed shutdown is triggered. The default value for Omnibus packages is set + [in the omnibus-gitlab + repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb). +- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults 900 seconds (15 minutes). When + a shutdown is triggered, the Sidekiq process will keep working normally for + another 15 minutes. +- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT`: defaults to 30 seconds. When the grace + time has expired, the MemoryKiller tells Sidekiq to stop accepting new jobs. + Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells + Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must + restart Sidekiq. diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index d561868c8bb..e21384d21dc 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -19,6 +19,7 @@ If a user is a GitLab administrator they receive all permissions. | Create new merge request | | | ✓ | ✓ | ✓ | | Create new branches | | | ✓ | ✓ | ✓ | | Push to non-protected branches | | | ✓ | ✓ | ✓ | +| Force push to non-protected branches | | | ✓ | ✓ | ✓ | | Remove non-protected branches | | | ✓ | ✓ | ✓ | | Add tags | | | ✓ | ✓ | ✓ | | Write a wiki | | | ✓ | ✓ | ✓ | @@ -35,6 +36,8 @@ If a user is a GitLab administrator they receive all permissions. | Switch visibility level | | | | | ✓ | | Transfer project to another namespace | | | | | ✓ | | Remove project | | | | | ✓ | +| Force push to protected branches | | | | | | +| Remove protected branches | | | | | | ## Group diff --git a/doc/project_services/bamboo.md b/doc/project_services/bamboo.md new file mode 100644 index 00000000000..51668128c62 --- /dev/null +++ b/doc/project_services/bamboo.md @@ -0,0 +1,60 @@ +# Atlassian Bamboo CI Service + +GitLab provides integration with Atlassian Bamboo for continuous integration. +When configured, pushes to a project will trigger a build in Bamboo automatically. +Merge requests will also display CI status showing whether the build is pending, +failed, or completed successfully. It also provides a link to the Bamboo build +page for more information. + +Bamboo doesn't quite provide the same features as a traditional build system when +it comes to accepting webhooks and commit data. There are a few things that +need to be configured in a Bamboo build plan before GitLab can integrate. + +## Setup + +### Complete these steps in Bamboo: + +1. Navigate to a Bamboo build plan and choose 'Configure plan' from the 'Actions' +dropdown. +1. Select the 'Triggers' tab. +1. Click 'Add trigger'. +1. Enter a description such as 'GitLab trigger' +1. Choose 'Repository triggers the build when changes are committed' +1. Check one or more repositories checkboxes +1. Enter the GitLab IP address in the 'Trigger IP addresses' box. This is a +whitelist of IP addresses that are allowed to trigger Bamboo builds. +1. Save the trigger. +1. In the left pane, select a build stage. If you have multiple build stages +you want to select the last stage that contains the git checkout task. +1. Select the 'Miscellaneous' tab. +1. Under 'Pattern Match Labelling' put '${bamboo.repository.revision.number}' +in the 'Labels' box. +1. Save + +Bamboo is now ready to accept triggers from GitLab. Next, set up the Bamboo +service in GitLab + +### Complete these steps in GitLab: + +1. Navigate to the project you want to configure to trigger builds. +1. Select 'Settings' in the top navigation. +1. Select 'Services' in the left navigation. +1. Click 'Atlassian Bamboo CI' +1. Select the 'Active' checkbox. +1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com' +1. Enter the build key from your Bamboo build plan. Build keys are a short, +all capital letter, identifier that is unique. It will be something like PR-BLD +1. If necessary, enter username and password for a Bamboo user that has +access to trigger the build plan. Leave these fields blank if you do not require +authentication. +1. Save or optionally click 'Test Settings'. Please note that 'Test Settings' +will actually trigger a build in Bamboo. + +## Troubleshooting + +If builds are not triggered, these are a couple of things to keep in mind. + +1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger +IP addresses'. +1. Remember that GitLab only triggers builds on push events. A commit via the +web interface will not trigger CI currently. diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md new file mode 100644 index 00000000000..20a69a211dd --- /dev/null +++ b/doc/project_services/project_services.md @@ -0,0 +1,18 @@ +# Project Services + +__Project integrations with external services for continuous integration and more.__ + +## Services + +- Assemblia +- [Atlassian Bamboo CI](bamboo.md) An Atlassian product for continous integration. +- Build box +- Campfire +- Emails on push +- Flowdock +- Gemnasium +- GitLab CI +- Hipchat +- PivotalTracker +- Pushover +- Slack diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md index 9e2f697bca6..770b7a70fe0 100644 --- a/doc/raketasks/README.md +++ b/doc/raketasks/README.md @@ -1,5 +1,8 @@ +# Rake tasks + - [Backup restore](backup_restore.md) - [Cleanup](cleanup.md) +- [Features](features.md) - [Maintenance](maintenance.md) and self-checks - [User management](user_management.md) - [Web hooks](web_hooks.md) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index b4581e2a07a..f9d2f5dc4eb 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -14,7 +14,7 @@ You can only restore a backup to exactly the same version of GitLab that you cre sudo gitlab-rake gitlab:backup:create # if you've installed GitLab from source or using the cookbook -bundle exec rake gitlab:backup:create RAILS_ENV=production +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` Example output: @@ -137,7 +137,7 @@ with the name of your bucket: Please be informed that a backup does not store your configuration files. If you use an Omnibus package please see the [instructions in the readme to backup your configuration](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#backup-and-restore-omnibus-gitlab-configuration). If you have a cookbook installation there should be a copy of your configuration in Chef. -If you have a manual installation please consider backing up your gitlab.yml file and any SSL keys and certificates. +If you have a manual installation please consider backing up your `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). ## Restore a previously created backup @@ -203,5 +203,31 @@ Add the following lines at the bottom: ``` # Create a full backup of the GitLab repositories and SQL database every day at 4am -0 4 * * * cd /home/git/gitlab && PATH=/usr/local/bin:/usr/bin:/bin bundle exec rake gitlab:backup:create RAILS_ENV=production +0 4 * * * cd /home/git/gitlab && PATH=/usr/local/bin:/usr/bin:/bin bundle exec rake gitlab:backup:create RAILS_ENV=production CRON=1 ``` + +The `CRON=1` environment setting tells the backup script to suppress all progress output if there are no errors. +This is recommended to reduce cron spam. + +## Alternative backup strategies + +If your GitLab server contains a lot of Git repository data you may find the GitLab backup script to be too slow. +In this case you can consider using filesystem snapshots as part of your backup strategy. + +Example: Amazone EBS + +> A GitLab server using omnibus-gitlab hosted on Amazon AWS. +> An EBS drive containing an ext4 filesystem is mounted at `/var/opt/gitlab`. +> In this case you could make an application backup by taking an EBS snapshot. +> The backup includes all repositories, uploads and Postgres data. + +Example: LVM snapshots + Rsync + +> A GitLab server using omnibus-gitlab, with an LVM logical volume mounted at `/var/opt/gitlab`. +> Replicating the `/var/opt/gitlab` directory usign Rsync would not be reliable because too many files would change while Rsync is running. +> Instead of rsync-ing `/var/opt/gitlab`, we create a temporary LVM snapshot, which we mount as a read-only filesystem at `/mnt/gitlab_backup`. +> Now we can have a longer running Rsync job which will create a consistent replica on the remote server. +> The replica includes all repositories, uploads and Postgres data. + +If you are running GitLab on a virtualized server you can possibly also create VM snapshots of the entire GitLab server. +It is not uncommon however for a VM snapshot to require you to power down the server, so this approach is probably of limited practical use. diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md index 39b1a52a44d..bb229e8acbb 100644 --- a/doc/raketasks/import.md +++ b/doc/raketasks/import.md @@ -1,28 +1,45 @@ -# Import +# Import bare repositories into your GitLab instance -### Import bare repositories into GitLab project instance +## Notes -Notes: +- The owner of the project will be the first admin +- The groups will be created as needed +- The owner of the group will be the first admin +- Existing projects will be skipped -* project owner will be a first admin -* groups will be created as needed -* group owner will be the first admin -* existing projects will be skipped +## How to use -How to use: +### Create a new folder inside the git repositories path. This will be the name of the new group. -1. copy your bare repos under git repos_path (see `config/gitlab.yml` gitlab_shell -> repos_path) -2. run the command below +- For omnibus-gitlab, it is located at: `/var/opt/gitlab/git-data/repositories` by default, unless you changed +it in the `/etc/gitlab/gitlab.rb` file. +- For manual installations, it is usually located at: `/home/git/repositories` or you can see where +your repositories are located by looking at `config/gitlab.yml` under the `gitlab_shell => repos_path` entry. +### Copy your bare repositories inside this newly created folder: + +``` +$ cp -r /old/git/foo.git/ /home/git/repositories/new_group/ +``` + +### Run the command below depending on your type of installation: + +#### Omnibus Installation + +``` +$ sudo gitlab-rake gitlab:import:repos ``` -# omnibus-gitlab -sudo gitlab-rake gitlab:import:repos -# installation from source or cookbook -bundle exec rake gitlab:import:repos RAILS_ENV=production +#### Manual Installation + +Before running this command you need to change the directory to where your GitLab installation is located: + +``` +$ cd /home/git/gitlab +$ sudo -u git -H bundle exec rake gitlab:import:repos RAILS_ENV=production ``` -Example output: +#### Example output ``` Processing abcd.git diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md index f6bd7565799..8bef92e55fe 100644 --- a/doc/raketasks/maintenance.md +++ b/doc/raketasks/maintenance.md @@ -122,3 +122,27 @@ sudo -u git -H mkdir -p /home/git/gitlab-satellites sudo -u git -H bundle exec rake gitlab:satellites:create RAILS_ENV=production sudo chmod u+rwx,g=rx,o-rwx /home/git/gitlab-satellites ``` + +## Rebuild authorized_keys file + +In some case it is necessary to rebuild the `authorized_keys` file. + + +For Omnibus-packages: +``` +sudo gitlab-rake gitlab:shell:setup +``` + +For installations from source: +``` +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production +``` + +``` +This will rebuild an authorized_keys file. +You will lose any data stored in authorized_keys file. +Do you want to continue (yes/no)? yes + +............................ +``` diff --git a/doc/release/monthly.md b/doc/release/monthly.md index c46a3ed9c93..3f1bccb2cf6 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -2,84 +2,92 @@ NOTE: This is a guide for GitLab developers. -# **15th - Code Freeze & Release Manager** +# **7 workdays before release - Code Freeze & Release Manager** -### **1. Stop merging in code, except for important bugfixes** +### **1. Stop merging in code, except for important bug fixes** ### **2. Release Manager** -A release manager is selected that coordinates the entire release of this version. The release manager has to make sure all the steps below are done and delegated where necessary. This person should also make sure this document is kept up to date and issues are created and updated. +A release manager is selected that coordinates all releases the coming month, including the patch releases for previous releases. +The release manager has to make sure all the steps below are done and delegated where necessary. +This person should also make sure this document is kept up to date and issues are created and updated. ### **3. Create an overall issue** -Name it "Release x.x.x" for easier searching. + +Create issue for GitLab CE project(internal). Name it "Release x.x.x" for easier searching. +Replace the dates with actual dates based on the number of workdays before the release. ``` -15th: +Xth: -* Update the changelog (#LINK) -* Triage the omnibus-gitlab milestone +- [ ] Update the CE changelog (#LINK) +- [ ] Update the EE changelog (#LINK) +- [ ] Update the CI changelog (#LINK) +- [ ] Triage the omnibus-gitlab milestone -16th: +Xth: -* Merge CE in to EE (#LINK) -* Close the omnibus-gitlab milestone +- [ ] Merge CE in to EE (#LINK) +- [ ] Close the omnibus-gitlab milestone -17th: +Xth: -* Create x.x.0.rc1 (#LINK) -* Build package for GitLab.com (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#build-a-package) +- [ ] Create x.x.0.rc1 (#LINK) +- [ ] Create x.x.0.rc1-ee (#LINK) +- [ ] Create CI y.y.0.rc1 (#LINK) +- [ ] Build package for GitLab.com (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#build-a-package) -18th: +Xth: -* Update GitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package) -* Regression issue and tweet about rc1 (#LINK) -* Start blog post (#LINK) +- [ ] Update GitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package) +- [ ] Regression issues (CE, CI) and tweet about rc1 (#LINK) +- [ ] Start blog post (#LINK) -21th: +Xth: -* Do QA and fix anything coming out of it (#LINK) +- [ ] Do QA and fix anything coming out of it (#LINK) 22nd: -* Release CE and EE (#LINK) - -23rd: +- [ ] Release CE, EE and CI (#LINK) -* Prepare package for GitLab.com release (#LINK) +Xth: -24th: +- [ ] Deploy to GitLab.com (#LINK) -* Deploy to GitLab.com (#LINK) ``` -### **4. Update Changelog** +### **4. Update changelog** Any changes not yet added to the changelog are added by lead developer and in that merge request the complete team is asked if there is anything missing. +There are three changelogs that need to be updated: CE, EE and CI. + ### **5. Take weekend and vacations into account** Ensure that there is enough time to incorporate the findings of the release candidate, etc. -# **16th - Merge the CE into EE** +# **6 workdays before release- Merge the CE into EE** Do this via a merge request. -# **17th - Create RC1** +# **5 workdays before release - Create RC1** The RC1 release comes with the task to update the installation and upgrade docs. Be mindful that there might already be merge requests for this on GitLab or GitHub. ### **1. Update the installation guide** 1. Check if it references the correct branch `x-x-stable` (doesn't exist yet, but that is okay) -1. Check the [GitLab Shell version](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/tasks/gitlab/check.rake#L782) -1. Check the [Git version](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/tasks/gitlab/check.rake#L794) +1. Check the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782) +1. Check the [Git version](/lib/tasks/gitlab/check.rake#L794) 1. There might be other changes. Ask around. -### **2. Create an update guides** +### **2. Create update guides** -1. Create: CE update guide from previous version. Like `from-6-8-to-6.9` +1. Create: CE update guide from previous version. Like `7.3-to-7.4.md` 1. Create: CE to EE update guide in EE repository for latest version. -1. Update: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/6.0-to-6.x.md to latest version. +1. Update: `6.x-or-7.x-to-7.x.md` to latest version. +1. Create: CI update guide from previous version It's best to copy paste the previous guide and make changes where necessary. The typical steps are listed below with any points you should specifically look at. @@ -92,15 +100,15 @@ List any major changes here, so the user is aware of them before starting to upg - Web server changes - File structure changes -#### 1. Make backup +#### 1. Stop server -#### 2. Stop server +#### 2. Make backup #### 3. Do users need to update dependencies like `git`? -- Check if the [GitLab Shell version](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/tasks/gitlab/check.rake#L782) changed since the last release. +- Check if the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782) changed since the last release. -- Check if the [Git version](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/tasks/gitlab/check.rake#L794) changed since the last release. +- Check if the [Git version](/lib/tasks/gitlab/check.rake#L794) changed since the last release. #### 4. Get latest code @@ -112,19 +120,19 @@ List any major changes here, so the user is aware of them before starting to upg Check if any of these changed since last release: -- <https://gitlab.com/gitlab-org/gitlab-ce/commits/master/lib/support/nginx/gitlab> -- <https://gitlab.com/gitlab-org/gitlab-ce/commits/master/lib/support/nginx/gitlab-ssl> +- [lib/support/nginx/gitlab](/lib/support/nginx/gitlab) +- [lib/support/nginx/gitlab-ssl](/lib/support/nginx/gitlab-ssl) - <https://gitlab.com/gitlab-org/gitlab-shell/commits/master/config.yml.example> -- <https://gitlab.com/gitlab-org/gitlab-ce/commits/master/config/gitlab.yml.example> -- <https://gitlab.com/gitlab-org/gitlab-ce/commits/master/config/unicorn.rb.example> -- <https://gitlab.com/gitlab-org/gitlab-ce/commits/master/config/database.yml.mysql> -- <https://gitlab.com/gitlab-org/gitlab-ce/commits/master/config/database.yml.postgresql> -- <https://gitlab.com/gitlab-org/gitlab-ce/commits/master/config/initializers/rack_attack.rb.example> -- <https://gitlab.com/gitlab-org/gitlab-ce/commits/master/config/resque.yml.example> +- [config/gitlab.yml.example](/config/gitlab.yml.example) +- [config/unicorn.rb.example](/config/unicorn.rb.example) +- [config/database.yml.mysql](/config/database.yml.mysql) +- [config/database.yml.postgresql](/config/database.yml.postgresql) +- [config/initializers/rack_attack.rb.example](/config/initializers/rack_attack.rb.example) +- [config/resque.yml.example](/config/resque.yml.example) #### 8. Need to update init script? -Check if the `init.d/gitlab` script changed since last release: <https://gitlab.com/gitlab-org/gitlab-ce/commits/master/lib/support/init.d/gitlab> +Check if the `init.d/gitlab` script changed since last release: [lib/support/init.d/gitlab](/lib/support/init.d/gitlab) #### 9. Start application @@ -144,38 +152,59 @@ Make sure the code quality indicators are green / good. - [![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq) -### **4. Set VERSION** +### **4. Run release tool** -Change version in VERSION to `x.x.0.rc1`. +**Make sure EE `master` has latest changes from CE `master`** -### **5. Tag** - -Create an annotated tag that points to the version change commit: +Get release tools ``` -git tag -a vx.x.0.rc1 -m 'Version x.x.0.rc1' +git clone git@dev.gitlab.org:gitlab/release-tools.git +cd release-tools ``` -### **6. Create stable branches** +Release candidate creates stable branch from master. +So we need to sync master branch between all CE remotes. Also do same for EE. -For GitLab EE, append `-ee` to the branch. +``` +bundle exec rake sync +``` -`x-x-stable-ee` +Create release candidate and stable branch: ``` -git checkout master -git pull -git checkout -b x-x-stable -git push <remote> x-x-stable +bundle exec rake release["x.x.0.rc1"] ``` Now developers can use master for merging new features. So you should use stable branch for future code chages related to release. -# **18th - Release RC1** +### 5. Release GitLab CI RC1 + +Add to your local `gitlab-ci/.git/config`: + +``` +[remote "public"] + url = none + pushurl = git@dev.gitlab.org:gitlab/gitlab-ci.git + pushurl = git@gitlab.com:gitlab-org/gitlab-ci.git + pushurl = git@github.com:gitlabhq/gitlab-ci.git +``` + +* Create a stable branch `x-y-stable` +* Bump VERSION to `x.y.0.rc1` +* `git tag -a v$(cat VERSION) -m "Version $(cat VERSION)" +* `git push public x-y-stable v$(cat VERSION)` + + +# **4 workdays before release - Release RC1** + +### **1. Determine QA person + +Notify person of QA day. -### **1. Update GitLab.com** +### **2. Update GitLab.com** Merge the RC1 EE code into GitLab.com. Once the build is green, create a package. @@ -183,18 +212,22 @@ If there are big database migrations consider testing them with the production d Try to deploy in the morning. It is important to do this as soon as possible, so we can catch any errors before we release the full version. -### **2. Prepare the blog post** +### **3. Prepare the blog post** - Start with a complete copy of the [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md) and fill it out. - Check the changelog of CE and EE for important changes. +- Also check the CI changelog +- Add a proposed tweet text to the blog post WIP MR description. - Create a WIP MR for the blog post - Ask Dmitriy to add screenshots to the WIP MR. -- Decide with team who will be the MVP user. +- Decide with team who will be the MVP user. +- Create WIP MR for adding MVP to MVP page on website - Add a note if there are security fixes: This release fixes an important security issue and we advise everyone to upgrade as soon as possible. +- Create a merge request on [GitLab.com](https://gitlab.com/gitlab-com/www-gitlab-com/tree/master) - Assign to one reviewer who will fix spelling issues by editing the branch (can use the online editor) - After the reviewer is finished the whole team will be mentioned to give their suggestions via line comments -### **3. Create a regressions issue** +### **4. Create a regressions issue** On [the GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues/) create an issue titled "GitLab X.X regressions" add the following text: @@ -211,7 +244,7 @@ Tweet about the RC release: > GitLab x.x.0.rc1 is out. This release candidate is only suitable for testing. Please link regressions issues from LINK_TO_REGRESSION_ISSUE -# **21st - Preparation** +# **1 workdays before release - Preparation** ### **1. Pre QA merge** @@ -232,93 +265,59 @@ Create an issue with description of a problem, if it is quick fix fix it yoursel **NOTE** If there is a problem that cannot be fixed in a timely manner, reverting the feature is an option! If the feature is reverted, create an issue about it in order to discuss the next steps after the release. -# **22nd - Release CE and EE** - -For GitLab EE, append `-ee` to the branches and tags. - -`x-x-stable-ee` - -`v.x.x.0-ee` - -Note: Merge CE into EE if needed. - -### **1. Set VERSION to x.x.x and push** +# **22nd - Release CE, EE and CI** -- Change the GITLAB_SHELL_VERSION file in `master` of the CE repository if the version changed. -- Change the GITLAB_SHELL_VERSION file in `master` of the EE repository if the version changed. -- Change the VERSION file in `master` branch of the CE repository and commit and push to origin. -- Change the VERSION file in `master` branch of the EE repository and commit and push to origin. +**Make sure EE `x-x-stable-ee` has latest changes from CE `x-x-stable`** -### **2. Update installation.md** -Update [installation.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) to the newest version in master. +### **1. Release code** -### **3. Push latest changes from x-x-stable branch to dev.gitlab.org** +Get release tools ``` -git checkout -b x-x-stable -git push origin x-x-stable +git clone git@dev.gitlab.org:gitlab/release-tools.git +cd release-tools ``` -### **4. Build the Omnibus packages** - -Follow the [release doc in the Omnibus repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md). -This can happen before tagging because Omnibus uses tags in its own repo and SHA1's to refer to the GitLab codebase. - -### **5. Create annotated tag vx.x.x** - -In `x-x-stable` branch check for the SHA-1 of the commit with VERSION file changed. Tag that commit, +Bump version, create release tag and push to remotes: ``` -git tag -a vx.x.0 -m 'Version x.x.0' xxxxx +bundle exec rake release["x.x.0"] ``` -where `xxxxx` is SHA-1. +Also perform these steps for GitLab CI: -### **6. Push the tag and x-x-stable branch to the remotes** +- bump version in the stable branch +- create annotated tag +- push the stable branch and the annotated tag to the public repositories -For GitLab CE, push to dev, GitLab.com and GitHub. +### **2. Update installation.md** -For GitLab EE, push to the subscribers repo. +Update [installation.md](/doc/install/installation.md) to the newest version in master. -Make sure the branch is marked 'protected' on each of the remotes you pushed to. -``` -git push <remote> x-x-stable(-ee) -git push <remote> vx.x.0 -``` +### **3. Build the Omnibus packages** + +Follow the [release doc in the Omnibus repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md). +This can happen before tagging because Omnibus uses tags in its own repo and SHA1's to refer to the GitLab codebase. + -### **7. Publish packages for new release** +### **4. Publish packages for new release** Update `downloads/index.html` and `downloads/archive/index.html` in `www-gitlab-com` repository. -### **8. Publish blog for new release** +### **5. Publish blog for new release** Merge the [blog merge request](#1-prepare-the-blog-post) in `www-gitlab-com` repository. -### **9. Tweet to blog** +### **6. Tweet to blog** Send out a tweet to share the good news with the world. List the most important features and link to the blog post. Proposed tweet for CE "GitLab X.X is released! It brings *** <link-to-blogpost>" -### **10. Send out the newsletter** - -Send out an email to the 'GitLab Newsletter' mailing list on MailChimp. -Replicate the former release newsletter and modify it accordingly. -**Do not forget to edit `Subject line` and regenerate `Plain-Text Email` from HTML source** - -Include a link to the blog post and keep it short. - -Proposed email text: -"We have released a new version of GitLab. See our blog post(<link>) for more information." - - -# **23rd - Optional Patch Release** - -# **24th - Update GitLab.com** - -Merge the stable release into GitLab.com. Once the build is green deploy the next morning. +# **1 workday after release - Update GitLab.com** -# **25th - Release GitLab CI** +- Build a package for gitlab.com based on the official release instead of RC1 +- Deploy the package (should not need downtime because of the small difference with RC1) diff --git a/doc/release/patch.md b/doc/release/patch.md index bcc14568fc8..2bd34b7d822 100644 --- a/doc/release/patch.md +++ b/doc/release/patch.md @@ -10,22 +10,47 @@ Otherwise include it in the monthly release and note there was a regression fix ## Release Procedure +### Preparation + 1. Verify that the issue can be reproduced 1. Note in the 'GitLab X.X regressions' that you will create a patch 1. Create an issue on private GitLab development server 1. Name the issue "Release X.X.X CE and X.X.X EE", this will make searching easier 1. Fix the issue on a feature branch, do this on the private GitLab development server +1. If it is a security issue, then assign it to the release manager and apply a 'security' label +1. Build the package for GitLab.com and do a deploy 1. Consider creating and testing workarounds 1. After the branch is merged into master, cherry pick the commit(s) into the current stable branch +1. Make sure that the build has passed and all tests are passing 1. In a separate commit in the stable branch update the CHANGELOG 1. For EE, update the CHANGELOG-EE if it is EE specific fix. Otherwise, merge the stable CE branch and add to CHANGELOG-EE "Merge community edition changes for version X.X.X" -1. In a separate commit in the stable branch update the VERSION -1. Create an annotated tag vX.X.X for CE and another patch release for EE `git tag -a vx.x.x -m 'Version x.x.x'` -1. Make sure that the build has passed and all tests are passing -1. Push the code and the tags to all the CE and EE repositories + +### Bump version + +Get release tools + +``` +git clone git@dev.gitlab.org:gitlab/release-tools.git +cd release-tools +``` + +Bump version in stable branch, create release tag and push to remotes: + +``` +bundle exec rake release["x.x.x"] +``` + +Or if you need to release only EE: + +``` +CE=false be rake release['x.x.x'] +``` + +### Release + 1. Apply the patch to GitLab Cloud and the private GitLab development server 1. [Build new packages with the latest version](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md) 1. Cherry-pick the changelog update back into master -1. Send tweets about the release from `@gitlabhq`, tweet should include the most important feature that the release is addressing as well as the link to the changelog +1. Create and publish a blog post +1. Send tweets about the release from `@gitlabhq`, tweet should include the most important feature that the release is addressing and link to the blog post 1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only) -1. Send out an email to the 'GitLab Newsletter' mailing list on MailChimp (or the 'Subscribers' list if the patch is EE only) diff --git a/doc/release/security.md b/doc/release/security.md index da442de6ee1..b67e0f37a04 100644 --- a/doc/release/security.md +++ b/doc/release/security.md @@ -8,20 +8,22 @@ Do a security release when there is a critical issue that needs to be addresses ## Security vulnerability disclosure -Please report suspected security vulnerabilities in private to <support@gitlab.com>, also see the [disclosure section on the GitLab.com website](http://www.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities. +Please report suspected security vulnerabilities in private to <support@gitlab.com>, also see the [disclosure section on the GitLab.com website](http://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities. ## Release Procedure 1. Verify that the issue can be reproduced 1. Acknowledge the issue to the researcher that disclosed it +1. Inform the release manager that there needs to be a security release 1. Do the steps from [patch release document](doc/release/patch.md), starting with "Create an issue on private GitLab development server" +1. The MR with the security fix should get a 'security' label and be assigned to the release manager +1. Build the package for GitLab.com and do a deploy 1. Create feature branches for the blog post on GitLab.com and link them from the code branch 1. Merge and publish the blog posts 1. Send tweets about the release from `@gitlabhq` -1. Send out an email to the 'GitLab Newsletter' mailing list on MailChimp (or the 'Subscribers' list if the security fix is for EE only) 1. Send out an email to [the community google mailing list](https://groups.google.com/forum/#!forum/gitlabhq) 1. Post a signed copy of our complete announcement to [oss-security](http://www.openwall.com/lists/oss-security/) and request a CVE number -1. Add the security researcher to the [Security Researcher Acknowledgments list](http://www.gitlab.com/vulnerability-acknowledgements/) +1. Add the security researcher to the [Security Researcher Acknowledgments list](http://about.gitlab.com/vulnerability-acknowledgements/) 1. Thank the security researcher in an email for their cooperation 1. Update the blog post and the CHANGELOG when we receive the CVE number diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md index 897cd0b91fa..cde679598f7 100644 --- a/doc/update/4.2-to-5.0.md +++ b/doc/update/4.2-to-5.0.md @@ -195,6 +195,12 @@ sudo rm -R tmp sudo -u git -H mkdir tmp sudo chmod -R u+rwX tmp/ +# create directory for pids, make sure GitLab can write to it +sudo -u git -H mkdir tmp/pids/ +sudo chmod -R u+rwX tmp/pids/ + +# if you are already running a newer version of GitLab check that installation guide for other tmp folders you need to create + # reboot system sudo reboot diff --git a/doc/update/6.x-or-7.x-to-7.3.md b/doc/update/6.x-or-7.x-to-7.6.md index 171fcb4033a..883a654dcd8 100644 --- a/doc/update/6.x-or-7.x-to-7.3.md +++ b/doc/update/6.x-or-7.x-to-7.6.md @@ -1,6 +1,6 @@ -# From 6.x or 7.x to 7.3 +# From 6.x or 7.x to 7.6 -This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.3. +This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.6. ## Global issue numbers @@ -13,7 +13,11 @@ possible to edit the label text and color. The characters `?`, `&` and `,` are no longer allowed however so those will be removed from your tags during the database migrations for GitLab 7.2. -## 0. Backup +## 0. Stop server + + sudo service gitlab stop + +## 1. Backup It's useful to make a backup just in case things go south: (With MySQL, this may require granting "LOCK TABLES" privileges to the GitLab user on the database version) @@ -23,10 +27,6 @@ cd /home/git/gitlab sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` -## 1. Stop server - - sudo service gitlab stop - ## 2. Update Ruby If you are still using Ruby 1.9.3 or below, you will need to update Ruby. @@ -34,7 +34,7 @@ You can check which version you are running with `ruby -v`. If you are you running Ruby 2.0.x, you do not need to upgrade ruby, but can consider doing so for performance reasons. -If you are running Ruby 2.1.1 consider upgrading to 2.1.2, because of the high memory usage of Ruby 2.1.1. +If you are running Ruby 2.1.1 consider upgrading to 2.1.5, because of the high memory usage of Ruby 2.1.1. Install, update dependencies: @@ -46,8 +46,8 @@ Download and compile Ruby: ```bash mkdir /tmp/ruby && cd /tmp/ruby -curl --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz -cd ruby-2.1.2 +curl --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.5.tar.gz | tar xz +cd ruby-2.1.5 ./configure --disable-install-rdoc make sudo make install @@ -64,13 +64,13 @@ sudo gem install bundler --no-ri --no-rdoc ```bash cd /home/git/gitlab sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically ``` For GitLab Community Edition: ```bash -sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically -sudo -u git -H git checkout 7-3-stable +sudo -u git -H git checkout 7-6-stable ``` OR @@ -78,8 +78,7 @@ OR For GitLab Enterprise Edition: ```bash -sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically -sudo -u git -H git checkout 7-3-stable-ee +sudo -u git -H git checkout 7-6-stable-ee ``` ## 4. Install additional packages @@ -100,6 +99,8 @@ sudo apt-get install pkg-config cmake sed 's/^port .*/port 0/' /etc/redis/redis.conf.orig | sudo tee /etc/redis/redis.conf # Enable Redis socket for default Debian / Ubuntu path echo 'unixsocket /var/run/redis/redis.sock' | sudo tee -a /etc/redis/redis.conf + # Be sure redis group can write to the socket, enable only if supported (>= redis 2.4.0). + sudo sed -i '/# unixsocketperm/ s/^# unixsocketperm.*/unixsocketperm 0775/' /etc/redis/redis.conf # Activate the changes to redis.conf sudo service redis-server restart # Add git to the redis group @@ -118,7 +119,7 @@ sudo apt-get install pkg-config cmake ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.0.1 +sudo -u git -H git checkout v2.4.0 ``` ## 7. Install libs, migrations, etc. @@ -153,14 +154,14 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab TIP: to see what changed in `gitlab.yml.example` in this release use next command: ``` -git diff 6-0-stable:config/gitlab.yml.example 7-3-stable:config/gitlab.yml.example +git diff 6-0-stable:config/gitlab.yml.example 7-6-stable:config/gitlab.yml.example ``` -* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-3-stable/config/gitlab.yml.example but with your settings. -* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-3-stable/config/unicorn.rb.example but with your settings. -* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.0.0/config.yml.example but with your settings. -* HTTP setups: Make `/etc/nginx/sites-available/nginx` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-3-stable/lib/support/nginx/gitlab but with your settings. -* HTTPS setups: Make `/etc/nginx/sites-available/nginx-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-3-stable/lib/support/nginx/gitlab-ssl but with your settings. +* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-6-stable/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-6-stable/config/unicorn.rb.example but with your settings. +* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.4.0/config.yml.example but with your settings. +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-6-stable/lib/support/nginx/gitlab but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-6-stable/lib/support/nginx/gitlab-ssl but with your settings. * Copy rack attack middleware config ```bash @@ -197,6 +198,77 @@ When using Google omniauth login, changes of the Google account required. Ensure that `Contacts API` and the `Google+ API` are enabled in the [Google Developers Console](https://console.developers.google.com/). More details can be found at the [integration documentation](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/google.md). +## 12. Optional optimizations for GitLab setups with MySQL databases + +Only applies if running MySQL database created with GitLab 6.7 or earlier. If you are not experiencing any issues you may not need the following instructions however following them will bring your database in line with the latest recommended installation configuration and help avoid future issues. Be sure to follow these directions exactly. These directions should be safe for any MySQL instance but to be sure make a current MySQL database backup beforehand. + +``` +# Stop GitLab +sudo service gitlab stop + +# Secure your MySQL installation (added in GitLab 6.2) +sudo mysql_secure_installation + +# Login to MySQL +mysql -u root -p + +# do not type the 'mysql>', this is part of the prompt + +# Convert all tables to use the InnoDB storage engine (added in GitLab 6.8) +SELECT CONCAT('ALTER TABLE gitlabhq_production.', table_name, ' ENGINE=InnoDB;') AS 'Copy & run these SQL statements:' FROM information_schema.tables WHERE table_schema = 'gitlabhq_production' AND `ENGINE` <> 'InnoDB' AND `TABLE_TYPE` = 'BASE TABLE'; + +# If previous query returned results, copy & run all outputed SQL statements + +# Convert all tables to correct character set +SET foreign_key_checks = 0; +SELECT CONCAT('ALTER TABLE gitlabhq_production.', table_name, ' CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;') AS 'Copy & run these SQL statements:' FROM information_schema.tables WHERE table_schema = 'gitlabhq_production' AND `TABLE_COLLATION` <> 'utf8_unicode_ci' AND `TABLE_TYPE` = 'BASE TABLE'; + +# If previous query returned results, copy & run all outputed SQL statements + +# turn foreign key checks back on +SET foreign_key_checks = 1; + +# Find MySQL users +mysql> SELECT user FROM mysql.user WHERE user LIKE '%git%'; + +# If git user exists and gitlab user does not exist +# you are done with the database cleanup tasks +mysql> \q + +# If both users exist skip to Delete gitlab user + +# Create new user for GitLab (changed in GitLab 6.4) +# change $password in the command below to a real password you pick +mysql> CREATE USER 'git'@'localhost' IDENTIFIED BY '$password'; + +# Grant the git user necessary permissions on the database +mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, LOCK TABLES ON `gitlabhq_production`.* TO 'git'@'localhost'; + +# Delete the old gitlab user +mysql> DELETE FROM mysql.user WHERE user='gitlab'; + +# Quit the database session +mysql> \q + +# Try connecting to the new database with the new user +sudo -u git -H mysql -u git -p -D gitlabhq_production + +# Type the password you replaced $password with earlier + +# You should now see a 'mysql>' prompt + +# Quit the database session +mysql> \q + +# Update database configuration details +# See config/database.yml.mysql for latest recommended configuration details +# Remove the reaping_frequency setting line if it exists (removed in GitLab 6.8) +# Set production -> pool: 10 (updated in GitLab 5.3) +# Set production -> username: git +# Set production -> password: the password your replaced $password with earlier +sudo -u git -H editor /home/git/gitlab/config/database.yml +``` + ## Things went south? Revert to previous version (6.0) ### 1. Revert the code to the previous version diff --git a/doc/update/7.2-to-7.3.md b/doc/update/7.2-to-7.3.md index 329b763322a..ebdd4ff60fa 100644 --- a/doc/update/7.2-to-7.3.md +++ b/doc/update/7.2-to-7.3.md @@ -18,12 +18,12 @@ sudo service gitlab stop ```bash cd /home/git/gitlab sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically ``` For GitLab Community Edition: ```bash -sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically sudo -u git -H git checkout 7-3-stable ``` @@ -32,7 +32,6 @@ OR For GitLab Enterprise Edition: ```bash -sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically sudo -u git -H git checkout 7-3-stable-ee ``` @@ -75,7 +74,7 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab # Enable Redis socket for default Debian / Ubuntu path echo 'unixsocket /var/run/redis/redis.sock' | sudo tee -a /etc/redis/redis.conf # Be sure redis group can write to the socket, enable only if supported (>= redis 2.4.0). - sed -i '/# unixsocketperm/ s/^# unixsocketperm.*/unixsocketperm 0775/' /etc/redis/redis.conf + sudo sed -i '/# unixsocketperm/ s/^# unixsocketperm.*/unixsocketperm 0775/' /etc/redis/redis.conf # Activate the changes to redis.conf sudo service redis-server restart # Add git to the redis group diff --git a/doc/update/7.3-to-7.4.md b/doc/update/7.3-to-7.4.md new file mode 100644 index 00000000000..2466050ea4c --- /dev/null +++ b/doc/update/7.3-to-7.4.md @@ -0,0 +1,193 @@ +# From 7.3 to 7.4 + +### 0. Stop server + + sudo service gitlab stop + +### 1. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 2. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 7-4-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-4-stable-ee +``` + +### 3. Install libs, migrations, etc. + +```bash +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 4. Update config files + +#### New configuration options for gitlab.yml + +There are new configuration options available for gitlab.yml. View them with the command below and apply them to your current gitlab.yml. + +``` +git diff origin/7-3-stable:config/gitlab.yml.example origin/7-4-stable:config/gitlab.yml.example +``` + +#### Change timeout for unicorn + +``` +# set timeout to 60 +sudo -u git -H editor config/unicorn.rb +``` + +#### Change nginx https settings + +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-4-stable/lib/support/nginx/gitlab-ssl but with your setting + +#### MySQL Databases: Update database.yml config file + +* Add `collation: utf8_general_ci` to config/database.yml as seen in [config/database.yml.mysql](config/database.yml.mysql) + + +### 5. Start application + + sudo service gitlab start + sudo service nginx restart + +### 6. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations upgrade is complete! + + +### 7. Optional optimizations for GitLab setups with MySQL databases + +Only applies if running MySQL database created with GitLab 6.7 or earlier. If you are not experiencing any issues you may not need the following instructions however following them will bring your database in line with the latest recommended installation configuration and help avoid future issues. Be sure to follow these directions exactly. These directions should be safe for any MySQL instance but to be sure make a current MySQL database backup beforehand. + +``` +# Stop GitLab +sudo service gitlab stop + +# Secure your MySQL installation (added in GitLab 6.2) +sudo mysql_secure_installation + +# Login to MySQL +mysql -u root -p + +# do not type the 'mysql>', this is part of the prompt + +# Convert all tables to use the InnoDB storage engine (added in GitLab 6.8) +SELECT CONCAT('ALTER TABLE gitlabhq_production.', table_name, ' ENGINE=InnoDB;') AS 'Copy & run these SQL statements:' FROM information_schema.tables WHERE table_schema = 'gitlabhq_production' AND `ENGINE` <> 'InnoDB' AND `TABLE_TYPE` = 'BASE TABLE'; + +# If previous query returned results, copy & run all outputed SQL statements + +# Convert all tables to correct character set +SET foreign_key_checks = 0; +SELECT CONCAT('ALTER TABLE gitlabhq_production.', table_name, ' CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;') AS 'Copy & run these SQL statements:' FROM information_schema.tables WHERE table_schema = 'gitlabhq_production' AND `TABLE_COLLATION` <> 'utf8_unicode_ci' AND `TABLE_TYPE` = 'BASE TABLE'; + +# If previous query returned results, copy & run all outputed SQL statements + +# turn foreign key checks back on +SET foreign_key_checks = 1; + +# Find MySQL users +mysql> SELECT user FROM mysql.user WHERE user LIKE '%git%'; + +# If git user exists and gitlab user does not exist +# you are done with the database cleanup tasks +mysql> \q + +# If both users exist skip to Delete gitlab user + +# Create new user for GitLab (changed in GitLab 6.4) +# change $password in the command below to a real password you pick +mysql> CREATE USER 'git'@'localhost' IDENTIFIED BY '$password'; + +# Grant the git user necessary permissions on the database +mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, LOCK TABLES ON `gitlabhq_production`.* TO 'git'@'localhost'; + +# Delete the old gitlab user +mysql> DELETE FROM mysql.user WHERE user='gitlab'; + +# Quit the database session +mysql> \q + +# Try connecting to the new database with the new user +sudo -u git -H mysql -u git -p -D gitlabhq_production + +# Type the password you replaced $password with earlier + +# You should now see a 'mysql>' prompt + +# Quit the database session +mysql> \q + +# Update database configuration details +# See config/database.yml.mysql for latest recommended configuration details +# Remove the reaping_frequency setting line if it exists (removed in GitLab 6.8) +# Set production -> pool: 10 (updated in GitLab 5.3) +# Set production -> username: git +# Set production -> password: the password your replaced $password with earlier +sudo -u git -H editor /home/git/gitlab/config/database.yml + +# Start GitLab +sudo service gitlab start +sudo service nginx restart + +# Run thorough check +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + + +## Things went south? Revert to previous version (7.3) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.2 to 7.3](7.2-to-7.3.md), except for the database migration +(The backup is already migrated to the previous version) + +### 2. Restore from the backup: + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` +If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above. + + + + diff --git a/doc/update/7.4-to-7.5.md b/doc/update/7.4-to-7.5.md new file mode 100644 index 00000000000..673eab3c56e --- /dev/null +++ b/doc/update/7.4-to-7.5.md @@ -0,0 +1,108 @@ +# From 7.4 to 7.5 + +### 0. Stop server + + sudo service gitlab stop + +### 1. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 2. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 7-5-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-5-stable-ee +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.2.0 +``` + +### 4. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 5. Update config files + +#### New configuration options for gitlab.yml + +There are new configuration options available for gitlab.yml. View them with the command below and apply them to your current gitlab.yml. + +``` +git diff origin/7-4-stable:config/gitlab.yml.example origin/7-5-stable:config/gitlab.yml.example +``` + +#### Change Nginx settings + +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your setting + +### 6. Start application + + sudo service gitlab start + sudo service nginx restart + +### 7. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations upgrade is complete! + +## Things went south? Revert to previous version (7.4) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.3 to 7.4](7.3-to-7.4.md), except for the database migration +(The backup is already migrated to the previous version) + +### 2. Restore from the backup: + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` +If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/update/7.5-to-7.6.md b/doc/update/7.5-to-7.6.md new file mode 100644 index 00000000000..35cd437fdc4 --- /dev/null +++ b/doc/update/7.5-to-7.6.md @@ -0,0 +1,114 @@ +# From 7.5 to 7.6 + +### 0. Stop server + + sudo service gitlab stop + +### 1. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 2. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 7-6-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-6-stable-ee +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.4.0 +``` + +### 4. Install libs, migrations, etc. + +```bash +sudo apt-get install libkrb5-dev + +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 5. Update config files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`. + +``` +git diff origin/7-5-stable:config/gitlab.yml.example origin/7-6-stable:config/gitlab.yml.example +``` + +#### Change Nginx settings + +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your setting + +#### Setup time zone (optional) + +Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`](config/application.rb) (unlikely), unset it. + +### 6. Start application + + sudo service gitlab start + sudo service nginx restart + +### 7. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations upgrade is complete! + +## Things went south? Revert to previous version (7.5) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.4 to 7.5](7.4-to-7.5.md), except for the database migration +(The backup is already migrated to the previous version) + +### 2. Restore from the backup: + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` +If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index c4a77d12800..629c46ad030 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -26,16 +26,14 @@ sudo -u git -H git checkout LATEST_TAG Replace LATEST_TAG with the latest GitLab tag you want to upgrade to, for example `v6.6.3`. -### 3. Update gitlab-shell if it is not the latest version +### 3. Update gitlab-shell to the corresponding version ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout LATEST_TAG +sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` ``` -Replace LATEST_TAG with the latest GitLab Shell tag you want to upgrade to, for example `v1.7.9`. - ### 4. Install libs, migrations, etc. ```bash diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md index cf59b0e461c..0a9f242d9ab 100644 --- a/doc/update/upgrader.md +++ b/doc/update/upgrader.md @@ -10,6 +10,8 @@ If you have local changes to your GitLab repository the script will stash them a **GitLab Upgrader is available only for GitLab version 6.4.2 or higher.** +**This script does NOT update gitlab-shell, it needs manual update. See step 5 below.** + ## 0. Backup cd /home/git/gitlab @@ -43,28 +45,31 @@ Check if GitLab and its dependencies are configured correctly: If all items are green, then congratulations upgrade is complete! -## 5. Upgrade GitLab Shell (if needed) - -If the `gitlab:check` task reports an outdated version of `gitlab-shell` you should upgrade it. +## 5. Upgrade GitLab Shell -Upgrade it by running the commands below after replacing 2.0.1 with the correct version number: +GitLab Shell might be outdated, running the commands below ensures you're using a compatible version: ``` cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.0.1 +sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` ``` ## One line upgrade command You've read through the entire guide and probably already did all the steps one by one. -Here is a one line command with step 1 to 4 for the next time you upgrade: +Here is a one line command with step 1 to 5 for the next time you upgrade: ```bash -cd /home/git/gitlab; sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production; \ +cd /home/git/gitlab; \ + sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production; \ sudo service gitlab stop; \ if [ -f bin/upgrade.rb ]; then sudo -u git -H ruby bin/upgrade.rb -y; else sudo -u git -H ruby script/upgrade.rb -y; fi; \ + cd /home/git/gitlab-shell; \ + sudo -u git -H git fetch; \ + sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`; \ + cd /home/git/gitlab; \ sudo service gitlab start; \ sudo service nginx restart; sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production ``` diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 31791da8074..e17d21b990d 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -54,6 +54,29 @@ Triggered when you push to the repository except when pushing tags. } ``` +## Tag events + +Triggered when you create (or delete) tags to the repository. + +**Request body:** + +```json +{ + "ref": "refs/tags/v1.0.0", + "before": "0000000000000000000000000000000000000000", + "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", + "user_id": 1, + "user_name": "John Smith", + "project_id": 1, + "repository": { + "name": "jsmith", + "url": "ssh://git@example.com/jsmith/example.git", + "description": "", + "homepage": "http://example.com/jsmith/example" + } +} +``` + ## Issues events Triggered when a new issue is created or an existing issue was updated/closed/reopened. @@ -63,6 +86,11 @@ Triggered when a new issue is created or an existing issue was updated/closed/re ```json { "object_kind": "issue", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, "object_attributes": { "id": 301, "title": "New API: create/update/delete file", @@ -92,6 +120,11 @@ Triggered when a new merge request is created or an existing merge request was u ```json { "object_kind": "merge_request", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, "object_attributes": { "id": 99, "target_branch": "master", diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 323ee48f3bc..c26d85e9955 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -4,3 +4,5 @@ - [Groups](groups.md) - [Labels](labels.md) - [GitLab Flow](gitlab_flow.md) +- [Notifications](notifications.md) +- [Migrating from SVN to GitLab](migrating_from_svn.md) diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 947646c756a..f8fd7c97e2a 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -26,7 +26,7 @@ After getting used to these three steps the branching model becomes the challeng Since many organizations new to git have no conventions how to work with it, it can quickly become a mess. The biggest problem they run into is that many long running branches that each contain part of the changes are around. People have a hard time figuring out which branch they should develop on or deploy to production. -Frequently the reaction to this problem is to adopt a standardized pattern such as [git flow](http://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](https://guides.github.com/introduction/flow/index.html) +Frequently the reaction to this problem is to adopt a standardized pattern such as [git flow](http://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](http://scottchacon.com/2011/08/31/github-flow.html) We think there is still room for improvement and will detail a set of practices we call GitLab flow. # Git flow and its problems @@ -309,3 +309,8 @@ If you need to merge in another branch after starting explain the reason in the If you have not pushed your commits to a shared location yet you can also rebase on master or another feature branch. Do not merge in upstream if your code will work and merge cleanly without doing so, Linus even says that [you should never merge in upstream at random points, only at major releases](http://lwn.net/Articles/328438/). Merging only when needed prevents creating merge commits in your feature branch that later end up littering the master history. + +### References + +- [Sketch file](https://www.dropbox.com/s/58dvsj5votbwrzv/git_flows.sketch?dl=0) with vectors of images in this article +- [Git Flow by Vincent Driessen](http://nvie.com/posts/a-successful-git-branching-model/) diff --git a/doc/workflow/migrating_from_svn.md b/doc/workflow/migrating_from_svn.md new file mode 100644 index 00000000000..207e3641802 --- /dev/null +++ b/doc/workflow/migrating_from_svn.md @@ -0,0 +1,17 @@ +# Migrating from SVN to GitLab + +SVN stands for Subversion and is a version control system (VCS). +Git is a distributed version control system. + +There are some major differences between the two, for more information consult your favourite search engine. + +Git has tools for migrating SVN repositories to git, namely `git svn`. You can read more about this at +[git documentation pages](http://git-scm.com/book/en/Git-and-Other-Systems-Git-and-Subversion). + +Apart from the [official git documentation](http://git-scm.com/book/en/Git-and-Other-Systems-Migrating-to-Git) there is also +user created step by step guide for migrating from SVN to GitLab. + +[Benjamin New](https://github.com/leftclickben) wrote [a guide that shows how to do a migration](https://gist.github.com/leftclickben/322b7a3042cbe97ed2af). Mirrors can be found [here](https://gitlab.com/snippets/2168) and [here](https://gist.github.com/maxlazio/f1b593b0d00aa966e9ca). + +## Contribute to this guide +We welcome all contributions that would expand this guide with instructions on how to migrate from SVN and other version control systems. diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md new file mode 100644 index 00000000000..3c3ce162df5 --- /dev/null +++ b/doc/workflow/notifications.md @@ -0,0 +1,71 @@ +# GitLab Notifications + +GitLab has notifications system in place to notify a user of events important for the workflow. + +## Notification settings + +Under user profile page you can find the notification settings. + +![notification settings](notifications/settings.png) + +Notification settings are divided into three groups: + +* Global Settings +* Group Settings +* Project Settings + +Each of these settings have levels of notification: + +* Disabled - turns off notifications +* Participating - receive notifications from related resources +* Watch - receive notifications from projects or groups user is a member of +* Global - notifications as set at the global settings + +#### Global Settings + +Global Settings are at the bottom of the hierarchy. +Any setting set here will be overriden by a setting at the group or a project level. + +Group or Project settings can use `global` notification setting which will then use +anything that is set at Global Settings. + +#### Group Settings + +Group Settings are taking presedence over Global Settings but are on a level below Project Settings. +This means that you can set a different level of notifications per group while still being able +to have a finer level setting per project. +Organization like this is suitable for users that belong to different groups but don't have the +same need for being notified for every group they are member of. + +#### Project Settings + +Project Settings are at the top level and any setting placed at this level will take presedence of any +other setting. +This is suitable for users that have different needs for notifications per project basis. + +## Notification events + +Below is the table of events users can be notified of: + +| Event | Sent to | Settings level | +|------------------------------|-------------------------------------------------------------------|------------------------------| +| New SSH key added | User | Security email, always sent. | +| New email added | User | Security email, always sent. | +| New user created | User | Sent on user creation, except for omniauth (LDAP)| +| New issue created | Issue assignee [1], project members [2] | [1] not disabled, [2] higher than participating | +| User added to project | User | Sent when user is added to project | +| Project access level changed | User | Sent when user project access level is changed | +| User added to group | User | Sent when user is added to group | +| Project moved | Project members [1] | [1] not disabled | +| Group access level changed | User | Sent when user group access level is changed | +| Close issue | Issue author [1], issue assignee [2], project members [3] | [1] [2] not disabled, [3] higher than participating | +| Reassign issue | New issue assignee [1], old issue assignee [2] | [1] [2] not disabled | +| Reopen issue | Project members [1] | [1] higher than participating | +| New merge request | MR assignee [1] | [1] not disabled | +| Reassign merge request | New MR assignee [1], old MR assignee [2] | [1] [2] not disabled | +| Close merge request | MR author [1], MR assignee [2], project members [3] | [1] [2] not disabled, [3] higher than participating | +| Reopen merge request | Project members [1] | [1] higher than participating | +| Merge merge request | MR author [1], MR assignee [2], project members [3] | [1] [2] not disabled, [3] higher than participating | +| New comment | Mentioned users [1], users participating [2], project members [3] | [1] [2] not disabled, [3] higher than participating | + + diff --git a/doc/workflow/notifications/settings.png b/doc/workflow/notifications/settings.png Binary files differnew file mode 100644 index 00000000000..e5b50ee2494 --- /dev/null +++ b/doc/workflow/notifications/settings.png diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000000..5d0880b8c88 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,34 @@ +FROM ubuntu:14.04 + +# Install required packages +RUN apt-get update -q \ + && DEBIAN_FRONTEND=noninteractive apt-get install -qy --no-install-recommends \ + ca-certificates \ + openssh-server \ + wget + +# Download & Install GitLab +# If the Omnibus package version below is outdated please contribute a merge request to update it. +# If you run GitLab Enterprise Edition point it to a location where you have downloaded it. +RUN TMP_FILE=$(mktemp); \ + wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.5.3-omnibus.5.2.1.ci-1_amd64.deb \ + && dpkg -i $TMP_FILE \ + && rm -f $TMP_FILE + +# Manage SSHD through runit +RUN mkdir -p /opt/gitlab/sv/sshd/supervise \ + && mkfifo /opt/gitlab/sv/sshd/supervise/ok \ + && printf "#!/bin/sh\nexec 2>&1\numask 077\nexec /usr/sbin/sshd -D" > /opt/gitlab/sv/sshd/run \ + && chmod a+x /opt/gitlab/sv/sshd/run \ + && ln -s /opt/gitlab/sv/sshd /opt/gitlab/service \ + && mkdir -p /var/run/sshd + +# Expose web & ssh +EXPOSE 80 22 + +# Volume & configuration +VOLUME ["/var/opt/gitlab", "/var/log/gitlab", "/etc/gitlab"] +ADD gitlab.rb /etc/gitlab/ + +# Default is to run runit & reconfigure +CMD gitlab-ctl reconfigure & /opt/gitlab/embedded/bin/runsvdir-start diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000000..58982a238a8 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,68 @@ +What is GitLab? +=============== + +GitLab offers git repository management, code reviews, issue tracking, activity feeds, wikis. It has LDAP/AD integration, handles 25,000 users on a single server but can also run on a highly available active/active cluster. A subscription gives you access to our support team and to GitLab Enterprise Edition that contains extra features aimed at larger organizations. + +<https://about.gitlab.com> + +![GitLab Logo](https://gitlab.com/uploads/appearance/logo/1/brand_logo-c37eb221b456bb4b472cc1084480991f.png) + + +How to use this image +====================== + +At this moment GitLab doesn't have official Docker images. +Build your own based on the Omnibus packages with the following command (it assumes you're in the GitLab repo root directory): + +```bash +sudo docker build --tag gitlab_image docker/ +``` + +We assume using a data volume container, this will simplify migrations and backups. +This empty container will exist to persist as volumes the 3 directories used by GitLab, so remember not to delete it. + +The directories on data container are: + +- `/var/opt/gitlab` for application data +- `/var/log/gitlab` for logs +- `/etc/gitlab` for configuration + +Create the data container with: + +```bash +sudo docker run --name gitlab_data gitlab_image /bin/true +``` + +After creating this run GitLab: + +```bash +sudo docker run --detach --name gitlab_app --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab_image +``` + +It might take a while before the docker container is responding to queries. You can follow the configuration process with `docker logs -f gitlab_app`. + +You can then go to `http://localhost:8080/` (or `http://192.168.59.103:8080/` if you use boot2docker). +You can login with username `root` and password `5iveL!fe`. +Next time, you can just use `sudo docker start gitlab_app` and `sudo docker stop gitlab_app`. + + +How to configure GitLab +======================== + +This container uses the official Omnibus GitLab distribution, so all configuration is done in the unique configuration file `/etc/gitlab/gitlab.rb`. + +To access GitLab configuration, you can start an interactive command line in a new container using the shared data volume container, you will be able to browse the 3 directories and use your favorite text editor: + +```bash +docker run -ti -e TERM=linux --rm --volumes-from gitlab_data ubuntu +vi /etc/gitlab/gitlab.rb +``` + +**Note** that GitLab will reconfigure itself **at each container start.** You will need to restart the container to reconfigure your GitLab. + +You can find all available options in [Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration). + + +Troubleshooting +========================= +Please see the [troubleshooting](troubleshooting.md) file in this directory. diff --git a/docker/gitlab.rb b/docker/gitlab.rb new file mode 100644 index 00000000000..7fddf309c01 --- /dev/null +++ b/docker/gitlab.rb @@ -0,0 +1,37 @@ +# External URL should be your Docker instance. +# By default, this example is the "standard" boot2docker IP. +# Always use port 80 here to force the internal nginx to bind port 80, +# even if you intend to use another port in Docker. +external_url "http://192.168.59.103/" + +# Prevent Postgres from trying to allocate 25% of total memory +postgresql['shared_buffers'] = '1MB' + +# Configure GitLab to redirect PostgreSQL logs to the data volume +postgresql['log_directory'] = '/var/log/gitlab/postgresql' + +# Some configuration of GitLab +# You can find more at https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration +gitlab_rails['gitlab_email_from'] = 'gitlab@example.com' +gitlab_rails['gitlab_support_email'] = 'support@example.com' +gitlab_rails['time_zone'] = 'Europe/Paris' + +# SMTP settings +# You must use an external server, the Docker container does not install an SMTP server +gitlab_rails['smtp_enable'] = true +gitlab_rails['smtp_address'] = "smtp.example.com" +gitlab_rails['smtp_port'] = 587 +gitlab_rails['smtp_user_name'] = "user" +gitlab_rails['smtp_password'] = "password" +gitlab_rails['smtp_domain'] = "example.com" +gitlab_rails['smtp_authentication'] = "plain" +gitlab_rails['smtp_enable_starttls_auto'] = true + +# Enable LDAP authentication +# gitlab_rails['ldap_enabled'] = true +# gitlab_rails['ldap_host'] = 'ldap.example.com' +# gitlab_rails['ldap_port'] = 389 +# gitlab_rails['ldap_method'] = 'plain' # 'ssl' or 'plain' +# gitlab_rails['ldap_allow_username_or_email_login'] = false +# gitlab_rails['ldap_uid'] = 'uid' +# gitlab_rails['ldap_base'] = 'ou=users,dc=example,dc=com' diff --git a/docker/troubleshooting.md b/docker/troubleshooting.md new file mode 100644 index 00000000000..b1b70de5997 --- /dev/null +++ b/docker/troubleshooting.md @@ -0,0 +1,63 @@ +# Troubleshooting + +This is to troubleshoot https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/245 +But it might contain useful commands for other cases as well. + +The configuration to add the postgres log in vim is: +postgresql['log_directory'] = '/var/log/gitlab/postgresql' + +# Commands + +```bash +sudo docker build --tag gitlab_image docker/ + +sudo docker rm -f gitlab_app +sudo docker rm -f gitlab_data + +sudo docker run --name gitlab_data gitlab_image /bin/true + +sudo docker run -ti --rm --volumes-from gitlab_data ubuntu apt-get update && sudo apt-get install -y vim && sudo vim /etc/gitlab/gitlab.rb + +sudo docker run --detach --name gitlab_app --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab_image + +sudo docker run -t --rm --volumes-from gitlab_data ubuntu tail -f /var/log/gitlab/reconfigure.log + +sudo docker run -t --rm --volumes-from gitlab_data ubuntu tail -f /var/log/gitlab/postgresql/current + +sudo docker run -t --rm --volumes-from gitlab_data ubuntu cat /var/opt/gitlab/postgresql/data/postgresql.conf | grep shared_buffers + +sudo docker run -t --rm --volumes-from gitlab_data ubuntu cat /etc/gitlab/gitlab.rb +``` + +# Interactively + +```bash +# First start a GitLab container without starting GitLab +# This is almost the same as starting the GitLab container except: +# - we run interactively (-t -i) +# - we define TERM=linux because it allows to use arrow keys in vi (!!!) +# - we choose another startup command (bash) +sudo docker run -ti -e TERM=linux --name gitlab_app --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab_image bash + +# Configure GitLab to redirect PostgreSQL logs +echo "postgresql['log_directory'] = '/var/log/gitlab/postgresql'" >> /etc/gitlab/gitlab.rb + +# Prevent Postgres from allocating 25% of total memory +echo "postgresql['shared_buffers'] = '1MB'" >> /etc/gitlab/gitlab.rb + +# You can now start GitLab manually from Bash (in the background) +# Maybe the command below is still missing something to run in the background +gitlab-ctl reconfigure > /var/log/gitlab/reconfigure.log & /opt/gitlab/embedded/bin/runsvdir-start & + +# Inspect PostgreSQL config +cat /var/opt/gitlab/postgresql/data/postgresql.conf | grep shared_buffers + +# And tail the logs (PostgreSQL log may not exist immediately) +tail -f /var/log/gitlab/reconfigure.log /var/log/gitlab/postgresql/current + +# And get the memory +cat /proc/meminfo +head /proc/sys/kernel/shmmax /proc/sys/kernel/shmall +free -m + +``` diff --git a/features/admin/active_tab.feature b/features/admin/active_tab.feature index b28e16f0d6a..5de07e90e28 100644 --- a/features/admin/active_tab.feature +++ b/features/admin/active_tab.feature @@ -1,5 +1,5 @@ @admin -Feature: Admin active tab +Feature: Admin Active Tab Background: Given I sign in as an admin diff --git a/features/admin/groups.feature b/features/admin/groups.feature index 1a465c1be55..aa365a6ea1a 100644 --- a/features/admin/groups.feature +++ b/features/admin/groups.feature @@ -20,3 +20,10 @@ Feature: Admin Groups When I visit admin group page When I select user "John Doe" from user list as "Reporter" Then I should see "John Doe" in team list in every project as "Reporter" + + @javascript + Scenario: Remove user from group + Given we have user "John Doe" in group + When I visit admin group page + And I remove user "John Doe" from group + Then I should not see "John Doe" in team list diff --git a/features/profile/profile.feature b/features/profile/profile.feature index d2125e013bc..d7fa370fe2a 100644 --- a/features/profile/profile.feature +++ b/features/profile/profile.feature @@ -83,3 +83,22 @@ Feature: Profile Given I visit profile design page When I change my code preview theme Then I should receive feedback that the changes were saved + + @javascript + Scenario: I see the password strength indicator + Given I visit profile password page + When I try to set a weak password + Then I should see the input field yellow + + @javascript + Scenario: I see the password strength indicator error + Given I visit profile password page + When I try to set a short password + Then I should see the input field red + And I should see the password error message + + @javascript + Scenario: I see the password strength indicator with success + Given I visit profile password page + When I try to set a strong password + Then I should see the input field green
\ No newline at end of file diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index 8d3e0bd967f..ed548177837 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -110,7 +110,7 @@ Feature: Project Active Tab Scenario: On Project Issues/Browse Given I visit my project's issues page - Then the active sub tab should be Browse Issues + Then the active sub tab should be Issues And no other sub tabs should be active And the active main tab should be Issues diff --git a/features/project/commits/comments.feature b/features/project/commits/comments.feature index e176752cfbf..a45245917e3 100644 --- a/features/project/commits/comments.feature +++ b/features/project/commits/comments.feature @@ -16,12 +16,12 @@ Feature: Project Commits Comments @javascript Scenario: I can't preview without text Given I haven't written any comment text - Then I should not see the comment preview button + Then The comment preview tab should say there is nothing to do @javascript Scenario: I can preview with text - Given I write a comment like "Nice" - Then I should see the comment preview button + Given I write a comment like ":+1: Nice" + Then The comment preview tab should be display rendered Markdown @javascript Scenario: I preview a comment @@ -32,7 +32,7 @@ Feature: Project Commits Comments @javascript Scenario: I can edit after preview Given I preview a comment text like "Bug fixed :smile:" - Then I should see the comment edit button + Then I should see the comment write tab @javascript Scenario: I have a reset form after posting from preview diff --git a/features/project/commits/diff_comments.feature b/features/project/commits/diff_comments.feature index a145ec84b78..9c4cc723d1b 100644 --- a/features/project/commits/diff_comments.feature +++ b/features/project/commits/diff_comments.feature @@ -58,13 +58,13 @@ Feature: Project Commits Diff Comments Scenario: I can't preview without text Given I open a diff comment form And I haven't written any diff comment text - Then I should not see the diff comment preview button + Then The diff comment preview tab should say there is nothing to do @javascript Scenario: I can preview with text Given I open a diff comment form And I write a diff comment like ":-1: I don't like this" - Then I should see the diff comment preview button + Then The diff comment preview tab should display rendered Markdown @javascript Scenario: I preview a diff comment @@ -75,7 +75,7 @@ Feature: Project Commits Diff Comments @javascript Scenario: I can edit after preview Given I preview a diff comment text like "Should fix it :smile:" - Then I should see the diff comment edit button + Then I should see the diff comment write tab @javascript Scenario: The form gets removed after posting diff --git a/features/project/fork.feature b/features/project/fork.feature index d3d1180db04..22f68e5b340 100644 --- a/features/project/fork.feature +++ b/features/project/fork.feature @@ -6,9 +6,11 @@ Feature: Project Fork Scenario: User fork a project Given I click link "Fork" + When I fork to my namespace Then I should see the forked project page Scenario: User already has forked the project Given I already have a project named "Shop" in my namespace And I click link "Fork" + When I fork to my namespace Then I should see a "Name has already been taken" warning diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index e989569ccd4..28ea44530fe 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -144,7 +144,7 @@ Feature: Project Issues Scenario: Issues list should display task status Given project "Shop" has "Tasks-open" open issue with task markdown When I visit project "Shop" issues page - Then I should see the task status for issue "Tasks-open" + Then I should see the task status for the Taskable # Toggling task items @@ -159,3 +159,37 @@ Feature: Project Issues Given project "Shop" has "Tasks-closed" closed issue with task markdown When I visit issue page "Tasks-closed" Then Task checkboxes should be disabled + + # Issue description preview + + @javascript + Scenario: I can't preview without text + Given I click link "New Issue" + And I haven't written any description text + Then The Markdown preview tab should say there is nothing to do + + @javascript + Scenario: I can preview with text + Given I click link "New Issue" + And I write a description like ":+1: Nice" + Then The Markdown preview tab should display rendered Markdown + + @javascript + Scenario: I preview an issue description + Given I click link "New Issue" + And I preview a description text like "Bug fixed :smile:" + Then I should see the Markdown preview + And I should not see the Markdown text field + + @javascript + Scenario: I can edit after preview + Given I click link "New Issue" + And I preview a description text like "Bug fixed :smile:" + Then I should see the Markdown write tab + + @javascript + Scenario: I can preview when editing an existing issue + Given I click link "Release 0.4" + And I click link "Edit" for the issue + And I preview a description text like "Bug fixed :smile:" + Then I should see the Markdown write tab diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index e1e0edd0545..7c029f05d75 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -168,11 +168,10 @@ Feature: Project Merge Requests # Task status in issues list - @now Scenario: Merge requests list should display task status Given project "Shop" has "MR-task-open" open MR with task markdown When I visit project "Shop" merge requests page - Then I should see the task status for merge request "MR-task-open" + Then I should see the task status for the Taskable # Toggling task items @@ -188,3 +187,34 @@ Feature: Project Merge Requests And I visit merge request page "MR-task-open" And I click link "Close" Then Task checkboxes should be disabled + + # Description preview + + @javascript + Scenario: I can't preview without text + Given I visit merge request page "Bug NS-04" + And I click link "Edit" for the merge request + And I haven't written any description text + Then The Markdown preview tab should say there is nothing to do + + @javascript + Scenario: I can preview with text + Given I visit merge request page "Bug NS-04" + And I click link "Edit" for the merge request + And I write a description like ":+1: Nice" + Then The Markdown preview tab should display rendered Markdown + + @javascript + Scenario: I preview a merge request description + Given I visit merge request page "Bug NS-04" + And I click link "Edit" for the merge request + And I preview a description text like "Bug fixed :smile:" + Then I should see the Markdown preview + And I should not see the Markdown text field + + @javascript + Scenario: I can edit after preview + Given I visit merge request page "Bug NS-04" + And I click link "Edit" for the merge request + And I preview a description text like "Bug fixed :smile:" + Then I should see the Markdown write tab diff --git a/features/project/service.feature b/features/project/service.feature index af88eaefa8f..ed9e03b428d 100644 --- a/features/project/service.feature +++ b/features/project/service.feature @@ -19,6 +19,12 @@ Feature: Project Services And I fill hipchat settings Then I should see hipchat service settings saved + Scenario: Activate hipchat service with custom server + When I visit project "Shop" services page + And I click hipchat service link + And I fill hipchat settings with custom server + Then I should see hipchat service settings with custom server saved + Scenario: Activate pivotaltracker service When I visit project "Shop" services page And I click pivotaltracker service link @@ -54,3 +60,9 @@ Feature: Project Services And I click email on push service link And I fill email on push settings Then I should see email on push service settings saved + + Scenario: Activate Atlassian Bamboo CI service + When I visit project "Shop" services page + And I click Atlassian Bamboo CI service link + And I fill Atlassian Bamboo CI settings + Then I should see Atlassian Bamboo CI service settings saved diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 20ef7ac5702..b7d70881d56 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -1,4 +1,4 @@ -Feature: Project Source Browse files +Feature: Project Source Browse Files Background: Given I sign in as a user And I own project "Shop" @@ -30,11 +30,21 @@ Feature: Project Source Browse files And I edit code And I fill the new file name And I fill the commit message - And I click on "Commit changes" + And I click on "Commit Changes" Then I am redirected to the new file And I should see its new content @javascript + Scenario: If I enter an illegal file name I see an error message + Given I click on "new file" link in repo + And I fill the new file name with an illegal name + And I edit code + And I fill the commit message + And I click on "Commit changes" + Then I am on the new file page + And I see a commit error message + + @javascript Scenario: I can edit file Given I click on ".gitignore" file in repo And I click button "Edit" @@ -46,10 +56,20 @@ Feature: Project Source Browse files And I click button "Edit" And I edit code And I fill the commit message - And I click on "Commit changes" + And I click on "Commit Changes" Then I am redirected to the ".gitignore" And I should see its new content + @javascript @wip + Scenario: If I don't change the content of the file I see an error message + Given I click on ".gitignore" file in repo + And I click button "edit" + And I fill the commit message + And I click on "Commit changes" + # Test fails because carriage returns are added to the file. + Then I am on the ".gitignore" edit file page + And I see a commit error message + @javascript Scenario: I can see editing preview Given I click on ".gitignore" file in repo diff --git a/features/snippets/discover.feature b/features/snippets/discover.feature index 5094062c8c3..1a7e132ea25 100644 --- a/features/snippets/discover.feature +++ b/features/snippets/discover.feature @@ -4,8 +4,10 @@ Feature: Snippets Discover Given I sign in as a user And I have public "Personal snippet one" snippet And I have private "Personal snippet private" snippet + And I have internal "Personal snippet internal" snippet Scenario: I should see snippets Given I visit snippets page Then I should see "Personal snippet one" in snippets + And I should see "Personal snippet internal" in snippets And I should not see "Personal snippet private" in snippets diff --git a/features/snippets/public_snippets.feature b/features/snippets/public_snippets.feature new file mode 100644 index 00000000000..c2afb63b6d8 --- /dev/null +++ b/features/snippets/public_snippets.feature @@ -0,0 +1,10 @@ +Feature: Public snippets + Scenario: Unauthenticated user should see public snippets + Given There is public "Personal snippet one" snippet + And I visit snippet page "Personal snippet one" + Then I should see snippet "Personal snippet one" + + Scenario: Unauthenticated user should see raw public snippets + Given There is public "Personal snippet one" snippet + And I visit snippet raw page "Personal snippet one" + Then I should see raw snippet "Personal snippet one" diff --git a/features/snippets/snippets.feature b/features/snippets/snippets.feature index 4c4e3ee2cff..6e8019c326f 100644 --- a/features/snippets/snippets.feature +++ b/features/snippets/snippets.feature @@ -25,4 +25,4 @@ Feature: Snippets Scenario: I destroy "Personal snippet one" Given I visit snippet page "Personal snippet one" And I click link "Destroy" - Then I should not see "Personal snippet one" in snippets + Then I should not see "Personal snippet one" in snippets
\ No newline at end of file diff --git a/features/snippets/user.feature b/features/snippets/user.feature index 424794f73fd..5b5dadb7b39 100644 --- a/features/snippets/user.feature +++ b/features/snippets/user.feature @@ -4,16 +4,19 @@ Feature: Snippets User Given I sign in as a user And I have public "Personal snippet one" snippet And I have private "Personal snippet private" snippet + And I have internal "Personal snippet internal" snippet Scenario: I should see all my snippets Given I visit my snippets page Then I should see "Personal snippet one" in snippets And I should see "Personal snippet private" in snippets + And I should see "Personal snippet internal" in snippets Scenario: I can see only my private snippets Given I visit my snippets page And I click "Private" filter Then I should not see "Personal snippet one" in snippets + And I should not see "Personal snippet internal" in snippets And I should see "Personal snippet private" in snippets Scenario: I can see only my public snippets @@ -21,3 +24,11 @@ Feature: Snippets User And I click "Public" filter Then I should see "Personal snippet one" in snippets And I should not see "Personal snippet private" in snippets + And I should not see "Personal snippet internal" in snippets + + Scenario: I can see only my internal snippets + Given I visit my snippets page + And I click "Internal" filter + Then I should see "Personal snippet internal" in snippets + And I should not see "Personal snippet private" in snippets + And I should not see "Personal snippet one" in snippets diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb index 4f0ba05606d..d69a87cd07e 100644 --- a/features/steps/admin/groups.rb +++ b/features/steps/admin/groups.rb @@ -37,8 +37,7 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps end When 'I select user "John Doe" from user list as "Reporter"' do - user = User.find_by(name: "John Doe") - select2(user.id, from: "#user_ids", multiple: true) + select2(user_john.id, from: "#user_ids", multiple: true) within "#new_team_member" do select "Reporter", from: "access_level" end @@ -58,9 +57,29 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps end end + step 'we have user "John Doe" in group' do + current_group.add_user(user_john, Gitlab::Access::REPORTER) + end + + step 'I remove user "John Doe" from group' do + within "#user_#{user_john.id}" do + click_link 'Remove user from group' + end + end + + step 'I should not see "John Doe" in team list' do + within ".group-users-list" do + page.should_not have_content "John Doe" + end + end + protected def current_group @group ||= Group.first end + + def user_john + @user_john ||= User.find_by(name: "John Doe") + end end diff --git a/features/steps/dashboard/event_filters.rb b/features/steps/dashboard/event_filters.rb index 332bfa95d97..3da3d62d0c0 100644 --- a/features/steps/dashboard/event_filters.rb +++ b/features/steps/dashboard/event_filters.rb @@ -29,7 +29,7 @@ class Spinach::Features::EventFilters < Spinach::FeatureSteps step 'this project has push event' do data = { - before: "0000000000000000000000000000000000000000", + before: Gitlab::Git::BLANK_SHA, after: "0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e", ref: "refs/heads/new_design", user_id: @user.id, diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb index 6b5f88e5895..2a5850d091b 100644 --- a/features/steps/dashboard/issues.rb +++ b/features/steps/dashboard/issues.rb @@ -10,6 +10,7 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps step 'I should see issues authored by me' do should_see(authored_issue) + should_see(authored_issue_on_public_project) should_not_see(assigned_issue) should_not_see(other_issue) end @@ -22,6 +23,7 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps step 'I have authored issues' do authored_issue + authored_issue_on_public_project end step 'I have assigned issues' do @@ -64,6 +66,10 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps @other_issue ||= create :issue, project: project end + def authored_issue_on_public_project + @authored_issue_on_public_project ||= create :issue, author: current_user, project: public_project + end + def project @project ||= begin project =create :project @@ -71,4 +77,8 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps project end end + + def public_project + @public_project ||= create :project, :public + end end diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb index 95c378fa201..75e53173d3f 100644 --- a/features/steps/dashboard/merge_requests.rb +++ b/features/steps/dashboard/merge_requests.rb @@ -4,13 +4,17 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps step 'I should see merge requests assigned to me' do should_see(assigned_merge_request) + should_see(assigned_merge_request_from_fork) should_not_see(authored_merge_request) + should_not_see(authored_merge_request_from_fork) should_not_see(other_merge_request) end step 'I should see merge requests authored by me' do should_see(authored_merge_request) + should_see(authored_merge_request_from_fork) should_not_see(assigned_merge_request) + should_not_see(assigned_merge_request_from_fork) should_not_see(other_merge_request) end @@ -22,10 +26,12 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps step 'I have authored merge requests' do authored_merge_request + authored_merge_request_from_fork end step 'I have assigned merge requests' do assigned_merge_request + assigned_merge_request_from_fork end step 'I have other merge requests' do @@ -53,15 +59,41 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps end def assigned_merge_request - @assigned_merge_request ||= create :merge_request, assignee: current_user, target_project: project, source_project: project + @assigned_merge_request ||= create :merge_request, + assignee: current_user, + target_project: project, + source_project: project end def authored_merge_request - @authored_merge_request ||= create :merge_request, source_branch: 'simple_merge_request', author: current_user, target_project: project, source_project: project + @authored_merge_request ||= create :merge_request, + source_branch: 'simple_merge_request', + author: current_user, + target_project: project, + source_project: project end def other_merge_request - @other_merge_request ||= create :merge_request, source_branch: '2_3_notes_fix', target_project: project, source_project: project + @other_merge_request ||= create :merge_request, + source_branch: '2_3_notes_fix', + target_project: project, + source_project: project + end + + def authored_merge_request_from_fork + @authored_merge_request_from_fork ||= create :merge_request, + source_branch: 'basic_page', + author: current_user, + target_project: public_project, + source_project: forked_project + end + + def assigned_merge_request_from_fork + @assigned_merge_request_from_fork ||= create :merge_request, + source_branch: 'basic_page_fix', + assignee: current_user, + target_project: public_project, + source_project: forked_project end def project @@ -71,4 +103,12 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps project end end + + def public_project + @public_project ||= create :project, :public + end + + def forked_project + @forked_project ||= Projects::ForkService.new(public_project, current_user).execute + end end diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index adfaefb1644..38aaadcd28d 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -58,16 +58,34 @@ class Spinach::Features::Profile < Spinach::FeatureSteps step 'I try change my password w/o old one' do within '.update-password' do - fill_in "user_password", with: "22233344" + fill_in "user_password_profile", with: "22233344" fill_in "user_password_confirmation", with: "22233344" click_button "Save" end end + step 'I try to set a weak password' do + within '.update-password' do + fill_in "user_password_profile", with: "22233344" + end + end + + step 'I try to set a short password' do + within '.update-password' do + fill_in "user_password_profile", with: "short" + end + end + + step 'I try to set a strong password' do + within '.update-password' do + fill_in "user_password_profile", with: "Itulvo9z8uud%$" + end + end + step 'I change my password' do within '.update-password' do fill_in "user_current_password", with: "12345678" - fill_in "user_password", with: "22233344" + fill_in "user_password_profile", with: "22233344" fill_in "user_password_confirmation", with: "22233344" click_button "Save" end @@ -76,7 +94,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps step 'I unsuccessfully change my password' do within '.update-password' do fill_in "user_current_password", with: "12345678" - fill_in "user_password", with: "password" + fill_in "user_password_profile", with: "password" fill_in "user_password_confirmation", with: "confirmation" click_button "Save" end @@ -86,6 +104,22 @@ class Spinach::Features::Profile < Spinach::FeatureSteps page.should have_content "You must provide a valid current password" end + step 'I should see the input field yellow' do + page.should have_css 'div.has-warning' + end + + step 'I should see the input field green' do + page.should have_css 'div.has-success' + end + + step 'I should see the input field red' do + page.should have_css 'div.has-error' + end + + step 'I should see the password error message' do + page.should have_content 'Your password is too short' + end + step "I should see a password error message" do page.should have_content "Password confirmation doesn't match" end @@ -136,7 +170,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step "I am not an ldap user" do - current_user.update_attributes(extern_uid: nil, provider: '') + current_user.identities.delete current_user.ldap_user?.should be_false end @@ -146,7 +180,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps step 'I submit new password' do fill_in :user_current_password, with: '12345678' - fill_in :user_password, with: '12345678' + fill_in :user_password_profile, with: '12345678' fill_in :user_password_confirmation, with: '12345678' click_button "Set new password" end diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 83796b0ba88..bb42d15eae5 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -89,8 +89,8 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps click_link('Labels') end - step 'the active sub tab should be Browse Issues' do - ensure_active_sub_tab('Browse Issues') + step 'the active sub tab should be Issues' do + ensure_active_sub_tab('Issues') end step 'the active sub tab should be Milestones' do diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index c054e0e8282..935f313e298 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -8,7 +8,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps commit = @project.repository.commit page.should have_content(@project.name) page.should have_content(commit.message[0..20]) - page.should have_content(commit.id.to_s[0..5]) + page.should have_content(commit.short_id) end step 'I click atom feed link' do diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index da50ba9ced0..8e58597db20 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -25,4 +25,10 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps step 'I should see a "Name has already been taken" warning' do page.should have_content "Name has already been taken" end + + step 'I fork to my namespace' do + within '.fork-namespaces' do + click_link current_user.name + end + end end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 26c3c7c14bc..c0ae5208541 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -1,5 +1,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps include SharedAuthentication + include SharedIssuable include SharedProject include SharedNote include SharedPaths @@ -154,29 +155,11 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'project "Shop" has "Tasks-open" open issue with task markdown' do - desc_text = <<EOT.gsub(/^ {6}/, '') - * [ ] Task 1 - * [x] Task 2 -EOT - create(:issue, - title: 'Tasks-open', - project: project, - author: project.users.first, - description: desc_text - ) + create_taskable(:issue, 'Tasks-open') end step 'project "Shop" has "Tasks-closed" closed issue with task markdown' do - desc_text = <<EOT.gsub(/^ {6}/, '') - * [ ] Task 1 - * [x] Task 2 -EOT - create(:closed_issue, - title: 'Tasks-closed', - project: project, - author: project.users.first, - description: desc_text - ) + create_taskable(:closed_issue, 'Tasks-closed') end step 'empty project "Empty Project"' do diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb index 89d7af3c9ee..cce87a6d981 100644 --- a/features/steps/project/issues/milestones.rb +++ b/features/steps/project/issues/milestones.rb @@ -8,7 +8,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps milestone = @project.milestones.find_by(title: "v2.2") page.should have_content(milestone.title[0..10]) page.should have_content(milestone.expires_at) - page.should have_content("Browse Issues") + page.should have_content("Issues") end step 'I click link "v2.2"' do @@ -28,7 +28,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps milestone = @project.milestones.find_by(title: "v2.3") page.should have_content(milestone.title[0..10]) page.should have_content(milestone.expires_at) - page.should have_content("Browse Issues") + page.should have_content("Issues") end step 'project "Shop" has milestone "v2.2"' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 0fec2604915..d5e060bdbe8 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -1,5 +1,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps include SharedAuthentication + include SharedIssuable include SharedProject include SharedNote include SharedPaths @@ -98,17 +99,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'project "Shop" has "MR-task-open" open MR with task markdown' do - desc_text = <<EOT.gsub(/^ {6}/, '') - * [ ] Task 1 - * [x] Task 2 -EOT - create(:merge_request, - title: 'MR-task-open', - source_project: project, - target_project: project, - author: project.users.first, - description: desc_text - ) + create_taskable(:merge_request, 'MR-task-open') end step 'I switch to the diff tab' do @@ -121,7 +112,7 @@ EOT step 'I click on the commit in the merge request' do within '.mr-commits' do - click_link sample_commit.id[0..8] + click_link Commit.truncate_sha(sample_commit.id) end end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index f7fff8e64f9..5e7312d90ff 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -4,7 +4,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps include SharedPaths step 'change project settings' do - fill_in 'project_name', with: 'NewName' + fill_in 'project_name_edit', with: 'NewName' uncheck 'project_issues_enabled' end diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 5bd60f99c84..7a0b47a8fe5 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -10,10 +10,11 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps step 'I should see list of available services' do page.should have_content 'Project services' page.should have_content 'Campfire' - page.should have_content 'Hipchat' + page.should have_content 'HipChat' page.should have_content 'GitLab CI' page.should have_content 'Assembla' page.should have_content 'Pushover' + page.should have_content 'Atlassian Bamboo' end step 'I click gitlab-ci service link' do @@ -32,7 +33,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I click hipchat service link' do - click_link 'Hipchat' + click_link 'HipChat' end step 'I fill hipchat settings' do @@ -46,6 +47,17 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps find_field('Room').value.should == 'gitlab' end + step 'I fill hipchat settings with custom server' do + check 'Active' + fill_in 'Room', with: 'gitlab_custom' + fill_in 'Token', with: 'secretCustom' + fill_in 'Server', with: 'https://chat.example.com' + click_button 'Save' + end + + step 'I should see hipchat service settings with custom server saved' do + find_field('Server').value.should == 'https://chat.example.com' + end step 'I click pivotaltracker service link' do click_link 'PivotalTracker' @@ -108,12 +120,12 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps step 'I fill Slack settings' do check 'Active' - fill_in 'Webhook', with: 'https://gitlabhq.slack.com/services/hooks?token=cdIj4r4LfXUOySDUjp0tk3OI' + fill_in 'Webhook', with: 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' click_button 'Save' end step 'I should see Slack service settings saved' do - find_field('Webhook').value.should == 'https://gitlabhq.slack.com/services/hooks?token=cdIj4r4LfXUOySDUjp0tk3OI' + find_field('Webhook').value.should == 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' end step 'I click Pushover service link' do @@ -137,4 +149,23 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps find_field('Priority').find('option[selected]').value.should == '1' find_field('Sound').find('option[selected]').value.should == 'bike' end + + step 'I click Atlassian Bamboo CI service link' do + click_link 'Atlassian Bamboo CI' + end + + step 'I fill Atlassian Bamboo CI settings' do + check 'Active' + fill_in 'Bamboo url', with: 'http://bamboo.example.com' + fill_in 'Build key', with: 'KEY' + fill_in 'Username', with: 'user' + fill_in 'Password', with: 'verySecret' + click_button 'Save' + end + + step 'I should see Atlassian Bamboo CI service settings saved' do + find_field('Bamboo url').value.should == 'http://bamboo.example.com' + find_field('Build key').value.should == 'KEY' + find_field('Username').value.should == 'user' + end end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 0642302e797..ddd501d4f88 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -61,6 +61,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps fill_in :file_name, with: new_file_name end + step 'I fill the new file name with an illegal name' do + fill_in :file_name, with: '.git' + end + step 'I fill the commit message' do fill_in :commit_message, with: 'Not yet a commit message.' end @@ -69,12 +73,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps click_link 'Diff' end - step 'I click on "Commit changes"' do - click_button 'Commit changes' + step 'I click on "Commit Changes"' do + click_button 'Commit Changes' end step 'I click on "Remove"' do - click_link 'Remove' + click_button 'Remove' end step 'I click on "Remove file"' do @@ -151,6 +155,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(page).not_to have_link('permalink') end + step 'I see a commit error message' do + expect(page).to have_content('Your changes could not be committed') + end + private def set_new_content diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 10f3ed90b56..28964d54a8f 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -32,7 +32,7 @@ module SharedDiffNote click_diff_line(sample_commit.line_code) within("#{diff_file_selector} form[rel$='#{sample_commit.line_code}']") do fill_in "note[note]", with: "Should fix it :smile:" - find(".js-note-preview-button").trigger("click") + find('.js-md-preview-button').click end end @@ -41,7 +41,7 @@ module SharedDiffNote within("#{diff_file_selector} form[rel$='#{sample_commit.del_line_code}']") do fill_in "note[note]", with: "DRY this up" - find(".js-note-preview-button").trigger("click") + find('.js-md-preview-button').click end end @@ -71,9 +71,10 @@ module SharedDiffNote end end - step 'I should not see the diff comment preview button' do + step 'The diff comment preview tab should say there is nothing to do' do within(diff_file_selector) do - page.should have_css(".js-note-preview-button", visible: false) + find('.js-md-preview-button').click + expect(find('.js-md-preview')).to have_content('Nothing to preview.') end end @@ -131,27 +132,28 @@ module SharedDiffNote step 'I should see the diff comment preview' do within("#{diff_file_selector} form") do - page.should have_css(".js-note-preview", visible: false) + expect(page).to have_css('.js-md-preview', visible: true) end end - step 'I should see the diff comment edit button' do + step 'I should see the diff comment write tab' do within(diff_file_selector) do - page.should have_css(".js-note-write-button", visible: true) + expect(page).to have_css('.js-md-write-button', visible: true) end end - step 'I should see the diff comment preview button' do + step 'The diff comment preview tab should display rendered Markdown' do within(diff_file_selector) do - page.should have_css(".js-note-preview-button", visible: true) + find('.js-md-preview-button').click + expect(find('.js-md-preview')).to have_css('img.emoji', visible: true) end end step 'I should see two separate previews' do within(diff_file_selector) do - page.should have_css(".js-note-preview", visible: true, count: 2) - page.should have_content("Should fix it") - page.should have_content("DRY this up") + expect(page).to have_css('.js-md-preview', visible: true, count: 2) + expect(page).to have_content('Should fix it') + expect(page).to have_content('DRY this up') end end diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb new file mode 100644 index 00000000000..a0150e90380 --- /dev/null +++ b/features/steps/shared/issuable.rb @@ -0,0 +1,15 @@ +module SharedIssuable + include Spinach::DSL + + def edit_issuable + find('.issue-btn-group').click_link 'Edit' + end + + step 'I click link "Edit" for the merge request' do + edit_issuable + end + + step 'I click link "Edit" for the issue' do + edit_issuable + end +end diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb index 1d9058cf256..e71700880cd 100644 --- a/features/steps/shared/markdown.rb +++ b/features/steps/shared/markdown.rb @@ -6,6 +6,27 @@ module SharedMarkdown find(:css, "#{parent} h#{level}##{id} > :last-child")[:href].should =~ /##{id}$/ end + def create_taskable(type, title) + desc_text = <<EOT.gsub(/^ {6}/, '') + * [ ] Task 1 + * [x] Task 2 +EOT + + case type + when :issue, :closed_issue + options = { project: project } + when :merge_request + options = { source_project: project, target_project: project } + end + + create( + type, + options.merge(title: title, + author: project.users.first, + description: desc_text) + ) + end + step 'Header "Description header" should have correct id and link' do header_should_have_correct_id_and_link(1, 'Description header', 'description-header') end @@ -16,13 +37,7 @@ module SharedMarkdown ) end - step 'I should see the task status for issue "Tasks-open"' do - expect(find(:css, 'span.task-status').text).to eq( - '2 tasks (1 done, 1 unfinished)' - ) - end - - step 'I should see the task status for merge request "MR-task-open"' do + step 'I should see the task status for the Taskable' do expect(find(:css, 'span.task-status').text).to eq( '2 tasks (1 done, 1 unfinished)' ) @@ -39,4 +54,49 @@ module SharedMarkdown 'div.description li.task-list-item input[type="checkbox"]:disabled' ) end + + step 'I should not see the Markdown preview' do + expect(find('.gfm-form .js-md-preview')).not_to be_visible + end + + step 'The Markdown preview tab should say there is nothing to do' do + within('.gfm-form') do + find('.js-md-preview-button').click + expect(find('.js-md-preview')).to have_content('Nothing to preview.') + end + end + + step 'I should not see the Markdown text field' do + expect(find('.gfm-form textarea')).not_to be_visible + end + + step 'I should see the Markdown write tab' do + expect(find('.gfm-form')).to have_css('.js-md-write-button', visible: true) + end + + step 'I should see the Markdown preview' do + expect(find('.gfm-form')).to have_css('.js-md-preview', visible: true) + end + + step 'The Markdown preview tab should display rendered Markdown' do + within('.gfm-form') do + find('.js-md-preview-button').click + expect(find('.js-md-preview')).to have_css('img.emoji', visible: true) + end + end + + step 'I write a description like ":+1: Nice"' do + find('.gfm-form').fill_in 'Description', with: ':+1: Nice' + end + + step 'I preview a description text like "Bug fixed :smile:"' do + within('.gfm-form') do + fill_in 'Description', with: 'Bug fixed :smile:' + find('.js-md-preview-button').click + end + end + + step 'I haven\'t written any description text' do + find('.gfm-form').fill_in 'Description', with: '' + end end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 2b2cb47a715..17adec3eda1 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -23,7 +23,7 @@ module SharedNote step 'I preview a comment text like "Bug fixed :smile:"' do within(".js-main-target-form") do fill_in "note[note]", with: "Bug fixed :smile:" - find(".js-note-preview-button").trigger("click") + find('.js-md-preview-button').click end end @@ -33,9 +33,9 @@ module SharedNote end end - step 'I write a comment like "Nice"' do + step 'I write a comment like ":+1: Nice"' do within(".js-main-target-form") do - fill_in "note[note]", with: "Nice" + fill_in 'note[note]', with: ':+1: Nice' end end @@ -51,13 +51,14 @@ module SharedNote step 'I should not see the comment preview' do within(".js-main-target-form") do - page.should have_css(".js-note-preview", visible: false) + expect(find('.js-md-preview')).not_to be_visible end end - step 'I should not see the comment preview button' do + step 'The comment preview tab should say there is nothing to do' do within(".js-main-target-form") do - page.should have_css(".js-note-preview-button", visible: false) + find('.js-md-preview-button').click + expect(find('.js-md-preview')).to have_content('Nothing to preview.') end end @@ -79,21 +80,22 @@ module SharedNote end end - step 'I should see the comment edit button' do + step 'I should see the comment write tab' do within(".js-main-target-form") do - page.should have_css(".js-note-write-button", visible: true) + expect(page).to have_css('.js-md-write-button', visible: true) end end - step 'I should see the comment preview' do + step 'The comment preview tab should be display rendered Markdown' do within(".js-main-target-form") do - page.should have_css(".js-note-preview", visible: true) + find('.js-md-preview-button').click + expect(find('.js-md-preview')).to have_css('img.emoji', visible: true) end end - step 'I should see the comment preview button' do + step 'I should see the comment preview' do within(".js-main-target-form") do - page.should have_css(".js-note-preview-button", visible: true) + expect(page).to have_css('.js-md-preview', visible: true) end end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 1f238f8befd..5f292255ce1 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -265,6 +265,15 @@ module SharedPaths visit project_blob_path(@project, File.join(root_ref, '.gitignore')) end + step 'I am on the new file page' do + current_path.should eq(project_new_tree_path(@project, root_ref)) + end + + step 'I am on the ".gitignore" edit file page' do + current_path.should eq(project_edit_tree_path( + @project, File.join(root_ref, '.gitignore'))) + end + step 'I visit project source page for "6d39438"' do visit project_tree_path(@project, "6d39438") end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 4b833850a1c..0bd5653538c 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -32,7 +32,7 @@ module SharedProject @project = Project.find_by(name: "Shop") data = { - before: "0000000000000000000000000000000000000000", + before: Gitlab::Git::BLANK_SHA, after: "6d394385cf567f80a8fd85055db1ab4c5295806f", ref: "refs/heads/fix", user_id: @user.id, @@ -131,7 +131,7 @@ module SharedProject end step 'public empty project "Empty Public Project"' do - create :empty_project, :public, name: "Empty Public Project" + create :project_empty_repo, :public, name: "Empty Public Project" end step 'project "Community" has comments' do diff --git a/features/steps/shared/snippet.rb b/features/steps/shared/snippet.rb index 5a27e8750cf..bb596c1620a 100644 --- a/features/steps/shared/snippet.rb +++ b/features/steps/shared/snippet.rb @@ -6,7 +6,7 @@ module SharedSnippet title: "Personal snippet one", content: "Test content", file_name: "snippet.rb", - private: false, + visibility_level: Snippet::PUBLIC, author: current_user) end @@ -15,9 +15,19 @@ module SharedSnippet title: "Personal snippet private", content: "Provate content", file_name: "private_snippet.rb", - private: true, + visibility_level: Snippet::PRIVATE, author: current_user) end + + step 'I have internal "Personal snippet internal" snippet' do + create(:personal_snippet, + title: "Personal snippet internal", + content: "Provate content", + file_name: "internal_snippet.rb", + visibility_level: Snippet::INTERNAL, + author: current_user) + end + step 'I have a public many lined snippet' do create(:personal_snippet, title: 'Many lined snippet', @@ -38,7 +48,16 @@ module SharedSnippet |line fourteen END file_name: 'many_lined_snippet.rb', - private: true, + visibility_level: Snippet::PUBLIC, author: current_user) end + + step 'There is public "Personal snippet one" snippet' do + create(:personal_snippet, + title: "Personal snippet one", + content: "Test content", + file_name: "snippet.rb", + visibility_level: Snippet::PUBLIC, + author: create(:user)) + end end diff --git a/features/steps/snippets/discover.rb b/features/steps/snippets/discover.rb index 42bccafcc84..2667c1e3d44 100644 --- a/features/steps/snippets/discover.rb +++ b/features/steps/snippets/discover.rb @@ -7,6 +7,10 @@ class Spinach::Features::SnippetsDiscover < Spinach::FeatureSteps page.should have_content "Personal snippet one" end + step 'I should see "Personal snippet internal" in snippets' do + page.should have_content "Personal snippet internal" + end + step 'I should not see "Personal snippet private" in snippets' do page.should_not have_content "Personal snippet private" end diff --git a/features/steps/snippets/public_snippets.rb b/features/steps/snippets/public_snippets.rb new file mode 100644 index 00000000000..67669dc0a69 --- /dev/null +++ b/features/steps/snippets/public_snippets.rb @@ -0,0 +1,25 @@ +class Spinach::Features::PublicSnippets < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedSnippet + + step 'I should see snippet "Personal snippet one"' do + page.should have_no_xpath("//i[@class='public-snippet']") + end + + step 'I should see raw snippet "Personal snippet one"' do + page.should have_text(snippet.content) + end + + step 'I visit snippet page "Personal snippet one"' do + visit snippet_path(snippet) + end + + step 'I visit snippet raw page "Personal snippet one"' do + visit raw_snippet_path(snippet) + end + + def snippet + @snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one") + end +end diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb index dedbdd2c4f0..de936db85ee 100644 --- a/features/steps/snippets/snippets.rb +++ b/features/steps/snippets/snippets.rb @@ -46,7 +46,7 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps end step 'I uncheck "Private" checkbox' do - choose "Public" + choose "Internal" click_button "Save" end diff --git a/features/steps/snippets/user.rb b/features/steps/snippets/user.rb index ca9aa64bee6..866f637ab6c 100644 --- a/features/steps/snippets/user.rb +++ b/features/steps/snippets/user.rb @@ -15,6 +15,10 @@ class Spinach::Features::SnippetsUser < Spinach::FeatureSteps page.should have_content "Personal snippet private" end + step 'I should see "Personal snippet internal" in snippets' do + page.should have_content "Personal snippet internal" + end + step 'I should not see "Personal snippet one" in snippets' do page.should_not have_content "Personal snippet one" end @@ -23,9 +27,13 @@ class Spinach::Features::SnippetsUser < Spinach::FeatureSteps page.should_not have_content "Personal snippet private" end - step 'I click "Public" filter' do + step 'I should not see "Personal snippet internal" in snippets' do + page.should_not have_content "Personal snippet internal" + end + + step 'I click "Internal" filter' do within('.nav-stacked') do - click_link "Public" + click_link "Internal" end end @@ -35,6 +43,12 @@ class Spinach::Features::SnippetsUser < Spinach::FeatureSteps end end + step 'I click "Public" filter' do + within('.nav-stacked') do + click_link "Public" + end + end + def snippet @snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one") end diff --git a/lib/api/api.rb b/lib/api/api.rb index 2c7cd9038c3..d26667ba3f7 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -27,6 +27,7 @@ module API helpers APIHelpers mount Groups + mount GroupMembers mount Users mount Projects mount Repositories diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 14f8b20f6b2..6ec1a753a69 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -82,6 +82,7 @@ module API authorize_push_project result = CreateBranchService.new(user_project, current_user). execute(params[:branch_name], params[:ref]) + if result[:status] == :success present result[:branch], with: Entities::RepoObject, @@ -104,7 +105,9 @@ module API execute(params[:branch]) if result[:status] == :success - true + { + branch_name: params[:branch] + } else render_api_error!(result[:message], result[:return_code]) end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 4a67313430a..6c5391b98c8 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -50,6 +50,67 @@ module API not_found! "Commit" unless commit commit.diffs end + + # Get a commit's comments + # + # Parameters: + # id (required) - The ID of a project + # sha (required) - The commit hash + # Examples: + # GET /projects/:id/repository/commits/:sha/comments + get ':id/repository/commits/:sha/comments' do + sha = params[:sha] + commit = user_project.repository.commit(sha) + not_found! 'Commit' unless commit + notes = Note.where(commit_id: commit.id) + present paginate(notes), with: Entities::CommitNote + end + + # Post comment to commit + # + # Parameters: + # id (required) - The ID of a project + # sha (required) - The commit hash + # note (required) - Text of comment + # path (optional) - The file path + # line (optional) - The line number + # line_type (optional) - The type of line (new or old) + # Examples: + # POST /projects/:id/repository/commits/:sha/comments + post ':id/repository/commits/:sha/comments' do + required_attributes! [:note] + + sha = params[:sha] + commit = user_project.repository.commit(sha) + not_found! 'Commit' unless commit + opts = { + note: params[:note], + noteable_type: 'Commit', + commit_id: commit.id + } + + if params[:path] && params[:line] && params[:line_type] + commit.diffs.each do |diff| + next unless diff.new_path == params[:path] + lines = Gitlab::Diff::Parser.new.parse(diff.diff.lines.to_a) + + lines.each do |line| + next unless line.new_pos == params[:line].to_i && line.type == params[:line_type] + break opts[:line_code] = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos) + end + + break if opts[:line_code] + end + end + + note = ::Notes::CreateService.new(user_project, current_user, opts).execute + + if note.save + present note, with: Entities::CommitNote + else + not_found! + end + end end end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 80e9470195e..2fea151aeb3 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -14,9 +14,14 @@ module API expose :bio, :skype, :linkedin, :twitter, :website_url end + class Identity < Grape::Entity + expose :provider, :extern_uid + end + class UserFull < User expose :email - expose :theme_id, :color_scheme_id, :extern_uid, :provider + expose :theme_id, :color_scheme_id, :projects_limit + expose :identities, using: Entities::Identity expose :can_create_group?, as: :can_create_group expose :can_create_project?, as: :can_create_project end @@ -73,6 +78,25 @@ module API end end + class RepoTag < Grape::Entity + expose :name + expose :message do |repo_obj, _options| + if repo_obj.respond_to?(:message) + repo_obj.message + else + nil + end + end + + expose :commit do |repo_obj, options| + if repo_obj.respond_to?(:commit) + repo_obj.commit + elsif options[:project] + options[:project].repository.commit(repo_obj.target) + end + end + end + class RepoObject < Grape::Entity expose :name @@ -159,11 +183,25 @@ module API expose :author, using: Entities::UserBasic end + class CommitNote < Grape::Entity + expose :note + expose(:path) { |note| note.diff_file_name } + expose(:line) { |note| note.diff_new_line } + expose(:line_type) { |note| note.diff_line_type } + expose :author, using: Entities::UserBasic + end + class Event < Grape::Entity expose :title, :project_id, :action_name expose :target_id, :target_type, :author_id expose :data, :target_title expose :created_at + + expose :author_username do |event, options| + if event.author + event.author.username + end + end end class Namespace < Grape::Entity diff --git a/lib/api/files.rb b/lib/api/files.rb index e63e635a4d3..84e1d311781 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -85,7 +85,7 @@ module API branch_name: branch_name } else - render_api_error!(result[:error], 400) + render_api_error!(result[:message], 400) end end @@ -117,7 +117,7 @@ module API branch_name: branch_name } else - render_api_error!(result[:error], 400) + render_api_error!(result[:message], 400) end end @@ -149,7 +149,7 @@ module API branch_name: branch_name } else - render_api_error!(result[:error], 400) + render_api_error!(result[:message], 400) end end end diff --git a/lib/api/group_members.rb b/lib/api/group_members.rb new file mode 100644 index 00000000000..d596517c816 --- /dev/null +++ b/lib/api/group_members.rb @@ -0,0 +1,80 @@ +module API + class GroupMembers < Grape::API + before { authenticate! } + + resource :groups do + helpers do + def find_group(id) + group = Group.find(id) + + if can?(current_user, :read_group, group) + group + else + render_api_error!("403 Forbidden - #{current_user.username} lacks sufficient access to #{group.name}", 403) + end + end + + def validate_access_level?(level) + Gitlab::Access.options_with_owner.values.include? level.to_i + end + end + + # Get a list of group members viewable by the authenticated user. + # + # Example Request: + # GET /groups/:id/members + get ":id/members" do + group = find_group(params[:id]) + members = group.group_members + users = (paginate members).collect(&:user) + present users, with: Entities::GroupMember, group: group + end + + # Add a user to the list of group members + # + # Parameters: + # id (required) - group id + # user_id (required) - the users id + # access_level (required) - Project access level + # Example Request: + # POST /groups/:id/members + post ":id/members" do + group = find_group(params[:id]) + authorize! :manage_group, group + required_attributes! [:user_id, :access_level] + + unless validate_access_level?(params[:access_level]) + render_api_error!("Wrong access level", 422) + end + + if group.group_members.find_by(user_id: params[:user_id]) + render_api_error!("Already exists", 409) + end + + group.add_users([params[:user_id]], params[:access_level]) + member = group.group_members.find_by(user_id: params[:user_id]) + present member.user, with: Entities::GroupMember, group: group + end + + # Remove member. + # + # Parameters: + # id (required) - group id + # user_id (required) - the users id + # + # Example Request: + # DELETE /groups/:id/members/:user_id + delete ":id/members/:user_id" do + group = find_group(params[:id]) + authorize! :manage_group, group + member = group.group_members.find_by(user_id: params[:user_id]) + + if member.nil? + render_api_error!("404 Not Found - user_id:#{params[:user_id]} not a member of group #{group.name}",404) + else + member.destroy + end + end + end + end +end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 4841e04689d..f0ab6938b1c 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -97,57 +97,6 @@ module API not_found! end end - - # Get a list of group members viewable by the authenticated user. - # - # Example Request: - # GET /groups/:id/members - get ":id/members" do - group = find_group(params[:id]) - members = group.group_members - users = (paginate members).collect(&:user) - present users, with: Entities::GroupMember, group: group - end - - # Add a user to the list of group members - # - # Parameters: - # id (required) - group id - # user_id (required) - the users id - # access_level (required) - Project access level - # Example Request: - # POST /groups/:id/members - post ":id/members" do - required_attributes! [:user_id, :access_level] - unless validate_access_level?(params[:access_level]) - render_api_error!("Wrong access level", 422) - end - group = find_group(params[:id]) - if group.group_members.find_by(user_id: params[:user_id]) - render_api_error!("Already exists", 409) - end - group.add_users([params[:user_id]], params[:access_level]) - member = group.group_members.find_by(user_id: params[:user_id]) - present member.user, with: Entities::GroupMember, group: group - end - - # Remove member. - # - # Parameters: - # id (required) - group id - # user_id (required) - the users id - # - # Example Request: - # DELETE /groups/:id/members/:user_id - delete ":id/members/:user_id" do - group = find_group(params[:id]) - member = group.group_members.find_by(user_id: params[:user_id]) - if member.nil? - render_api_error!("404 Not Found - user_id:#{params[:user_id]} not a member of group #{group.name}",404) - else - member.destroy - end - end end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 3262884f6d3..027fb20ec46 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -67,6 +67,10 @@ module API unauthorized! unless current_user end + def authenticate_by_gitlab_shell_token! + unauthorized! unless secret_token == params['secret_token'] + end + def authenticated_as_admin! forbidden! unless current_user.is_admin? end @@ -193,5 +197,9 @@ module API abilities end end + + def secret_token + File.read(Rails.root.join('.gitlab_shell_secret')) + end end end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 5f484f63418..180e50611cf 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -1,6 +1,10 @@ module API # Internal access API class Internal < Grape::API + before { + authenticate_by_gitlab_shell_token! + } + namespace 'internal' do # Check if git command is allowed to project # @@ -14,25 +18,37 @@ module API # post "/allowed" do status 200 + project_path = params[:project] # Check for *.wiki repositories. # Strip out the .wiki from the pathname before finding the # project. This applies the correct project permissions to # the wiki repository as well. - project_path = params[:project] - project_path.gsub!(/\.wiki/,'') if project_path =~ /\.wiki/ + access = + if project_path =~ /\.wiki\Z/ + project_path.sub!(/\.wiki\Z/, '') + Gitlab::GitAccessWiki.new + else + Gitlab::GitAccess.new + end + project = Project.find_with_namespace(project_path) - return false unless project + + unless project + return Gitlab::GitAccessStatus.new(false, 'No such project') + end actor = if params[:key_id] - Key.find(params[:key_id]) + Key.find_by(id: params[:key_id]) elsif params[:user_id] - User.find(params[:user_id]) + User.find_by(id: params[:user_id]) end - return false unless actor + unless actor + return Gitlab::GitAccessStatus.new(false, 'No such user or key') + end - Gitlab::GitAccess.new.allowed?( + access.check( actor, params[:action], project, diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 7f7d2f8e9a8..7fcf97d1ad6 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -178,7 +178,7 @@ module API # DELETE /projects/:id delete ":id" do authorize! :remove_project, user_project - user_project.destroy + ::Projects::DestroyService.new(user_project, current_user, {}).execute end # Mark this project as forked from another diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 626d99c2649..a1a7721b288 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -23,7 +23,8 @@ module API # Example Request: # GET /projects/:id/repository/tags get ":id/repository/tags" do - present user_project.repo.tags.sort_by(&:name).reverse, with: Entities::RepoObject, project: user_project + present user_project.repo.tags.sort_by(&:name).reverse, + with: Entities::RepoTag, project: user_project end # Create tag @@ -43,7 +44,7 @@ module API if result[:status] == :success present result[:tag], - with: Entities::RepoObject, + with: Entities::RepoTag, project: user_project else render_api_error!(result[:message], 400) diff --git a/lib/api/services.rb b/lib/api/services.rb index bde502e32e1..3ad59cf3adf 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -28,7 +28,7 @@ module API # Delete GitLab CI service settings # # Example Request: - # DELETE /projects/:id/keys/:id + # DELETE /projects/:id/services/gitlab-ci delete ":id/services/gitlab-ci" do if user_project.gitlab_ci_service user_project.gitlab_ci_service.update_attributes( @@ -38,7 +38,41 @@ module API ) end end + + # Set Hipchat service for project + # + # Parameters: + # token (required) - Hipchat token + # room (required) - Hipchat room name + # + # Example Request: + # PUT /projects/:id/services/hipchat + put ':id/services/hipchat' do + required_attributes! [:token, :room] + attrs = attributes_for_keys [:token, :room] + user_project.build_missing_services + + if user_project.hipchat_service.update_attributes( + attrs.merge(active: true)) + true + else + not_found! + end + end + + # Delete Hipchat service settings + # + # Example Request: + # DELETE /projects/:id/services/hipchat + delete ':id/services/hipchat' do + if user_project.hipchat_service + user_project.hipchat_service.update_attributes( + active: false, + token: nil, + room: nil + ) + end + end end end end - diff --git a/lib/api/users.rb b/lib/api/users.rb index d07815a8a97..37b36ddcf94 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -59,10 +59,16 @@ module API post do authenticated_as_admin! required_attributes! [:email, :password, :name, :username] - attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :extern_uid, :provider, :bio, :can_create_group, :admin] + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin] user = User.build_user(attrs) admin = attrs.delete(:admin) user.admin = admin unless admin.nil? + + identity_attrs = attributes_for_keys [:provider, :extern_uid] + if identity_attrs.any? + user.identities.build(identity_attrs) + end + if user.save present user, with: Entities::UserFull else @@ -89,8 +95,6 @@ module API # twitter - Twitter account # website_url - Website url # projects_limit - Limit projects each user can create - # extern_uid - External authentication provider UID - # provider - External provider # bio - Bio # admin - User is admin - true or false (default) # can_create_group - User can create groups - true or false @@ -99,7 +103,7 @@ module API put ":id" do authenticated_as_admin! - attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :extern_uid, :provider, :bio, :can_create_group, :admin] + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin] user = User.find(params[:id]) not_found!('User') unless user diff --git a/lib/backup/database.rb b/lib/backup/database.rb index d12d30a9110..9ab6aca276d 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -13,10 +13,10 @@ module Backup def dump success = case config["adapter"] when /^mysql/ then - print "Dumping MySQL database #{config['database']} ... " + $progress.print "Dumping MySQL database #{config['database']} ... " system('mysqldump', *mysql_args, config['database'], out: db_file_name) when "postgresql" then - print "Dumping PostgreSQL database #{config['database']} ... " + $progress.print "Dumping PostgreSQL database #{config['database']} ... " pg_env system('pg_dump', config['database'], out: db_file_name) end @@ -27,13 +27,14 @@ module Backup def restore success = case config["adapter"] when /^mysql/ then - print "Restoring MySQL database #{config['database']} ... " + $progress.print "Restoring MySQL database #{config['database']} ... " system('mysql', *mysql_args, config['database'], in: db_file_name) when "postgresql" then - print "Restoring PostgreSQL database #{config['database']} ... " + $progress.print "Restoring PostgreSQL database #{config['database']} ... " # Drop all tables because PostgreSQL DB dumps do not contain DROP TABLE # statements like MySQL. Rake::Task["gitlab:db:drop_all_tables"].invoke + Rake::Task["gitlab:db:drop_all_postgres_sequences"].invoke pg_env system('psql', config['database'], '-f', db_file_name) end @@ -68,9 +69,9 @@ module Backup def report_success(success) if success - puts '[DONE]'.green + $progress.puts '[DONE]'.green else - puts '[FAILED]'.red + $progress.puts '[FAILED]'.red end end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 03fe0f0b02f..ab8db4e9837 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -18,11 +18,11 @@ module Backup end # create archive - print "Creating backup archive: #{tar_file} ... " + $progress.print "Creating backup archive: #{tar_file} ... " if Kernel.system('tar', '-cf', tar_file, *BACKUP_CONTENTS) - puts "done".green + $progress.puts "done".green else - puts "failed".red + puts "creating archive #{tar_file} failed".red abort 'Backup failed' end @@ -31,37 +31,37 @@ module Backup def upload(tar_file) remote_directory = Gitlab.config.backup.upload.remote_directory - print "Uploading backup archive to remote storage #{remote_directory} ... " + $progress.print "Uploading backup archive to remote storage #{remote_directory} ... " connection_settings = Gitlab.config.backup.upload.connection if connection_settings.blank? - puts "skipped".yellow + $progress.puts "skipped".yellow return end connection = ::Fog::Storage.new(connection_settings) directory = connection.directories.get(remote_directory) if directory.files.create(key: tar_file, body: File.open(tar_file), public: false) - puts "done".green + $progress.puts "done".green else - puts "failed".red + puts "uploading backup to #{remote_directory} failed".red abort 'Backup failed' end end def cleanup - print "Deleting tmp directories ... " + $progress.print "Deleting tmp directories ... " if Kernel.system('rm', '-rf', *BACKUP_CONTENTS) - puts "done".green + $progress.puts "done".green else - puts "failed".red + puts "deleting tmp directory failed".red abort 'Backup failed' end end def remove_old # delete backups - print "Deleting old backups ... " + $progress.print "Deleting old backups ... " keep_time = Gitlab.config.backup.keep_time.to_i path = Gitlab.config.backup.path @@ -76,9 +76,9 @@ module Backup end end end - puts "done. (#{removed} removed)".green + $progress.puts "done. (#{removed} removed)".green else - puts "skipping".yellow + $progress.puts "skipping".yellow end end @@ -101,12 +101,12 @@ module Backup exit 1 end - print "Unpacking backup ... " + $progress.print "Unpacking backup ... " unless Kernel.system(*%W(tar -xf #{tar_file})) - puts "failed".red + puts "unpacking backup failed".red exit 1 else - puts "done".green + $progress.puts "done".green end settings = YAML.load_file("backup_information.yml") diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 4e99d4bbe5c..e18bc804437 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -8,19 +8,21 @@ module Backup prepare Project.find_each(batch_size: 1000) do |project| - print " * #{project.path_with_namespace} ... " + $progress.print " * #{project.path_with_namespace} ... " # Create namespace dir if missing FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace if project.empty_repo? - puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".cyan else - output, status = Gitlab::Popen.popen(%W(git --git-dir=#{path_to_repo(project)} bundle create #{path_to_bundle(project)} --all)) + cmd = %W(git --git-dir=#{path_to_repo(project)} bundle create #{path_to_bundle(project)} --all) + output, status = Gitlab::Popen.popen(cmd) if status.zero? - puts "[DONE]".green + $progress.puts "[DONE]".green else puts "[FAILED]".red + puts "failed: #{cmd.join(' ')}" puts output abort 'Backup failed' end @@ -29,15 +31,17 @@ module Backup wiki = ProjectWiki.new(project) if File.exists?(path_to_repo(wiki)) - print " * #{wiki.path_with_namespace} ... " - if wiki.empty? - puts " [SKIPPED]".cyan + $progress.print " * #{wiki.path_with_namespace} ... " + if wiki.repository.empty? + $progress.puts " [SKIPPED]".cyan else - output, status = Gitlab::Popen.popen(%W(git --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all)) + cmd = %W(git --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all) + output, status = Gitlab::Popen.popen(cmd) if status.zero? - puts " [DONE]".green + $progress.puts " [DONE]".green else puts " [FAILED]".red + puts "failed: #{cmd.join(' ')}" abort 'Backup failed' end end @@ -55,35 +59,52 @@ module Backup FileUtils.mkdir_p(repos_path) Project.find_each(batch_size: 1000) do |project| - print "#{project.path_with_namespace} ... " + $progress.print " * #{project.path_with_namespace} ... " project.namespace.ensure_dir_exist if project.namespace - if system(*%W(git clone --bare #{path_to_bundle(project)} #{path_to_repo(project)}), silent) - puts "[DONE]".green + if File.exists?(path_to_bundle(project)) + cmd = %W(git clone --bare #{path_to_bundle(project)} #{path_to_repo(project)}) + else + cmd = %W(git init --bare #{path_to_repo(project)}) + end + + if system(*cmd, silent) + $progress.puts "[DONE]".green else puts "[FAILED]".red + puts "failed: #{cmd.join(' ')}" abort 'Restore failed' end wiki = ProjectWiki.new(project) if File.exists?(path_to_bundle(wiki)) - print " * #{wiki.path_with_namespace} ... " - if system(*%W(git clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)}), silent) - puts " [DONE]".green + $progress.print " * #{wiki.path_with_namespace} ... " + + # If a wiki bundle exists, first remove the empty repo + # that was initialized with ProjectWiki.new() and then + # try to restore with 'git clone --bare'. + FileUtils.rm_rf(path_to_repo(wiki)) + cmd = %W(git clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)}) + + if system(*cmd, silent) + $progress.puts " [DONE]".green else puts " [FAILED]".red + puts "failed: #{cmd.join(' ')}" abort 'Restore failed' end end end - print 'Put GitLab hooks in repositories dirs'.yellow - if system("#{Gitlab.config.gitlab_shell.path}/bin/create-hooks") - puts " [DONE]".green + $progress.print 'Put GitLab hooks in repositories dirs'.yellow + cmd = "#{Gitlab.config.gitlab_shell.path}/bin/create-hooks" + if system(cmd) + $progress.puts " [DONE]".green else puts " [FAILED]".red + puts "failed: #{cmd}" end end @@ -91,7 +112,7 @@ module Backup protected def path_to_repo(project) - File.join(repos_path, project.path_with_namespace + '.git') + project.repository.path_to_repo end def path_to_bundle(project) diff --git a/lib/disable_email_interceptor.rb b/lib/disable_email_interceptor.rb new file mode 100644 index 00000000000..1b80be112a4 --- /dev/null +++ b/lib/disable_email_interceptor.rb @@ -0,0 +1,8 @@ +# Read about interceptors in http://guides.rubyonrails.org/action_mailer_basics.html#intercepting-emails +class DisableEmailInterceptor + + def self.delivering_email(message) + message.perform_deliveries = false + Rails.logger.info "Emails disabled! Interceptor prevented sending mail #{message.subject}" + end +end diff --git a/lib/gitlab/app_logger.rb b/lib/gitlab/app_logger.rb index 8e4717b46e6..dddcb2538f9 100644 --- a/lib/gitlab/app_logger.rb +++ b/lib/gitlab/app_logger.rb @@ -1,7 +1,7 @@ module Gitlab class AppLogger < Gitlab::Logger - def self.file_name - 'application.log' + def self.file_name_noext + 'application' end def format_message(severity, timestamp, progname, msg) diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 955abc1bedd..30509528b8b 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -1,24 +1,18 @@ module Gitlab class Auth def find(login, password) - user = User.find_by(email: login) || User.find_by(username: login) + user = User.by_login(login) + # If no user is found, or it's an LDAP server, try LDAP. + # LDAP users are only authenticated via LDAP if user.nil? || user.ldap_user? # Second chance - try LDAP authentication - return nil unless ldap_conf.enabled + return nil unless Gitlab::LDAP::Config.enabled? - Gitlab::LDAP::User.authenticate(login, password) + Gitlab::LDAP::Authentication.login(login, password) else user if user.valid_password?(password) end end - - def log - Gitlab::AppLogger - end - - def ldap_conf - @ldap_conf ||= Gitlab.config.ldap - end end end diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index c2f3b851c07..762639414e0 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -80,7 +80,7 @@ module Grack case git_cmd when *Gitlab::GitAccess::DOWNLOAD_COMMANDS if user - Gitlab::GitAccess.new.download_allowed?(user, project) + Gitlab::GitAccess.new.download_access_check(user, project).allowed? elsif project.public? # Allow clone/fetch for public projects true @@ -90,7 +90,7 @@ module Grack when *Gitlab::GitAccess::PUSH_COMMANDS if user # Skip user authorization on upload request. - # It will be serverd by update hook in repository + # It will be done by the pre-receive hook in the repository. true else false diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index f95bbde5b39..aabc7f1e69a 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -8,6 +8,13 @@ module Gitlab end end + class << self + def version_required + @version_required ||= File.read(Rails.root. + join('GITLAB_SHELL_VERSION')).strip + end + end + # Init new repository # # name - project path with namespace @@ -16,7 +23,8 @@ module Gitlab # add_repository("gitlab/gitlab-ci") # def add_repository(name) - system "#{gitlab_shell_path}/bin/gitlab-projects", "add-project", "#{name}.git" + Gitlab::Utils.system_silent([gitlab_shell_projects_path, + 'add-project', "#{name}.git"]) end # Import repository @@ -27,7 +35,8 @@ module Gitlab # import_repository("gitlab/gitlab-ci", "https://github.com/randx/six.git") # def import_repository(name, url) - system "#{gitlab_shell_path}/bin/gitlab-projects", "import-project", "#{name}.git", url, '240' + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'import-project', + "#{name}.git", url, '240']) end # Move repository @@ -39,7 +48,8 @@ module Gitlab # mv_repository("gitlab/gitlab-ci", "randx/gitlab-ci-new.git") # def mv_repository(path, new_path) - system "#{gitlab_shell_path}/bin/gitlab-projects", "mv-project", "#{path}.git", "#{new_path}.git" + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'mv-project', + "#{path}.git", "#{new_path}.git"]) end # Update HEAD for repository @@ -51,7 +61,8 @@ module Gitlab # update_repository_head("gitlab/gitlab-ci", "3-1-stable") # def update_repository_head(path, branch) - system "#{gitlab_shell_path}/bin/gitlab-projects", "update-head", "#{path}.git", branch + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'update-head', + "#{path}.git", branch]) end # Fork repository to new namespace @@ -63,7 +74,8 @@ module Gitlab # fork_repository("gitlab/gitlab-ci", "randx") # def fork_repository(path, fork_namespace) - system "#{gitlab_shell_path}/bin/gitlab-projects", "fork-project", "#{path}.git", fork_namespace + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'fork-project', + "#{path}.git", fork_namespace]) end # Remove repository from file system @@ -74,7 +86,8 @@ module Gitlab # remove_repository("gitlab/gitlab-ci") # def remove_repository(name) - system "#{gitlab_shell_path}/bin/gitlab-projects", "rm-project", "#{name}.git" + Gitlab::Utils.system_silent([gitlab_shell_projects_path, + 'rm-project', "#{name}.git"]) end # Add repository branch from passed ref @@ -87,7 +100,8 @@ module Gitlab # add_branch("gitlab/gitlab-ci", "4-0-stable", "master") # def add_branch(path, branch_name, ref) - system "#{gitlab_shell_path}/bin/gitlab-projects", "create-branch", "#{path}.git", branch_name, ref + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'create-branch', + "#{path}.git", branch_name, ref]) end # Remove repository branch @@ -99,7 +113,8 @@ module Gitlab # rm_branch("gitlab/gitlab-ci", "4-0-stable") # def rm_branch(path, branch_name) - system "#{gitlab_shell_path}/bin/gitlab-projects", "rm-branch", "#{path}.git", branch_name + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'rm-branch', + "#{path}.git", branch_name]) end # Add repository tag from passed ref @@ -117,7 +132,7 @@ module Gitlab cmd = %W(#{gitlab_shell_path}/bin/gitlab-projects create-tag #{path}.git #{tag_name} #{ref}) cmd << message unless message.nil? || message.empty? - system *cmd + Gitlab::Utils.system_silent(cmd) end # Remove repository tag @@ -129,7 +144,8 @@ module Gitlab # rm_tag("gitlab/gitlab-ci", "v4.0") # def rm_tag(path, tag_name) - system "#{gitlab_shell_path}/bin/gitlab-projects", "rm-tag", "#{path}.git", tag_name + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'rm-tag', + "#{path}.git", tag_name]) end # Add new key to gitlab-shell @@ -138,7 +154,8 @@ module Gitlab # add_key("key-42", "sha-rsa ...") # def add_key(key_id, key_content) - system "#{gitlab_shell_path}/bin/gitlab-keys", "add-key", key_id, key_content + Gitlab::Utils.system_silent([gitlab_shell_keys_path, + 'add-key', key_id, key_content]) end # Batch-add keys to authorized_keys @@ -157,7 +174,8 @@ module Gitlab # remove_key("key-342", "sha-rsa ...") # def remove_key(key_id, key_content) - system "#{gitlab_shell_path}/bin/gitlab-keys", "rm-key", key_id, key_content + Gitlab::Utils.system_silent([gitlab_shell_keys_path, + 'rm-key', key_id, key_content]) end # Remove all ssh keys from gitlab shell @@ -166,7 +184,7 @@ module Gitlab # remove_all_keys # def remove_all_keys - system "#{gitlab_shell_path}/bin/gitlab-keys", "clear" + Gitlab::Utils.system_silent([gitlab_shell_keys_path, 'clear']) end # Add empty directory for storing repositories @@ -249,5 +267,13 @@ module Gitlab def exists?(dir_name) File.exists?(full_path(dir_name)) end + + def gitlab_shell_projects_path + File.join(gitlab_shell_path, 'bin', 'gitlab-projects') + end + + def gitlab_shell_keys_path + File.join(gitlab_shell_path, 'bin', 'gitlab-keys') + end end end diff --git a/lib/gitlab/force_push_check.rb b/lib/gitlab/force_push_check.rb new file mode 100644 index 00000000000..6a52cdba608 --- /dev/null +++ b/lib/gitlab/force_push_check.rb @@ -0,0 +1,15 @@ +module Gitlab + class ForcePushCheck + def self.force_push?(project, oldrev, newrev) + return false if project.empty_repo? + + if oldrev != Gitlab::Git::BLANK_SHA && newrev != Gitlab::Git::BLANK_SHA + missed_refs = IO.popen(%W(git --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev})).read + missed_refs.split("\n").size > 0 + else + false + end + end + end +end + diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb new file mode 100644 index 00000000000..67aca5e36e9 --- /dev/null +++ b/lib/gitlab/git.rb @@ -0,0 +1,5 @@ +module Gitlab + module Git + BLANK_SHA = '0' * 40 + end +end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 6247dd59867..875f8d8b3a3 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -5,89 +5,107 @@ module Gitlab attr_reader :params, :project, :git_cmd, :user - def allowed?(actor, cmd, project, changes = nil) + def check(actor, cmd, project, changes = nil) case cmd when *DOWNLOAD_COMMANDS + download_access_check(actor, project) + when *PUSH_COMMANDS if actor.is_a? User - download_allowed?(actor, project) + push_access_check(actor, project, changes) elsif actor.is_a? DeployKey - actor.projects.include?(project) + return build_status_object(false, "Deploy key not allowed to push") elsif actor.is_a? Key - download_allowed?(actor.user, project) + push_access_check(actor.user, project, changes) else raise 'Wrong actor' end - when *PUSH_COMMANDS - if actor.is_a? User - push_allowed?(actor, project, changes) - elsif actor.is_a? DeployKey - # Deploy key not allowed to push - return false - elsif actor.is_a? Key - push_allowed?(actor.user, project, changes) + else + return build_status_object(false, "Wrong command") + end + end + + def download_access_check(actor, project) + if actor.is_a?(User) + user_download_access_check(actor, project) + elsif actor.is_a?(DeployKey) + if actor.projects.include?(project) + build_status_object(true) else - raise 'Wrong actor' + build_status_object(false, "Deploy key not allowed to access this project") end + elsif actor.is_a? Key + user_download_access_check(actor.user, project) else - false + raise 'Wrong actor' end end - def download_allowed?(user, project) - if user && user_allowed?(user) - user.can?(:download_code, project) + def user_download_access_check(user, project) + if user && user_allowed?(user) && user.can?(:download_code, project) + build_status_object(true) else - false + build_status_object(false, "You don't have access") end end - def push_allowed?(user, project, changes) - return false unless user && user_allowed?(user) - return true if changes.blank? + def push_access_check(user, project, changes) + unless user && user_allowed?(user) + return build_status_object(false, "You don't have access") + end + + if changes.blank? + return build_status_object(true) + end + + unless project.repository.exists? + return build_status_object(false, "Repository does not exist") + end changes = changes.lines if changes.kind_of?(String) # Iterate over all changes to find if user allowed all of them to be applied changes.each do |change| - oldrev, newrev, ref = change.split(' ') - - action = if project.protected_branch?(branch_name(ref)) - # we dont allow force push to protected branch - if forced_push?(project, oldrev, newrev) - :force_push_code_to_protected_branches - # and we dont allow remove of protected branch - elsif newrev =~ /0000000/ - :remove_protected_branches - else - :push_code_to_protected_branches - end - elsif project.repository && project.repository.tag_names.include?(tag_name(ref)) - # Prevent any changes to existing git tag unless user has permissions - :admin_project - else - :push_code - end - unless user.can?(action, project) + status = change_access_check(user, project, change) + unless status.allowed? # If user does not have access to make at least one change - cancel all push - return false + return status end end - # If user has access to make all changes - true + return build_status_object(true) end - def forced_push?(project, oldrev, newrev) - return false if project.empty_repo? + def change_access_check(user, project, change) + oldrev, newrev, ref = change.split(' ') + + action = if project.protected_branch?(branch_name(ref)) + # we dont allow force push to protected branch + if forced_push?(project, oldrev, newrev) + :force_push_code_to_protected_branches + # and we dont allow remove of protected branch + elsif newrev == Gitlab::Git::BLANK_SHA + :remove_protected_branches + else + :push_code_to_protected_branches + end + elsif project.repository.tag_names.include?(tag_name(ref)) + # Prevent any changes to existing git tag unless user has permissions + :admin_project + else + :push_code + end - if oldrev !~ /00000000/ && newrev !~ /00000000/ - missed_refs = IO.popen(%W(git --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev})).read - missed_refs.split("\n").size > 0 + if user.can?(action, project) + build_status_object(true) else - false + build_status_object(false, "You don't have permission") end end + def forced_push?(project, oldrev, newrev) + Gitlab::ForcePushCheck.force_push?(project, oldrev, newrev) + end + private def user_allowed?(user) @@ -111,5 +129,11 @@ module Gitlab nil end end + + protected + + def build_status_object(status, message = '') + GitAccessStatus.new(status, message) + end end end diff --git a/lib/gitlab/git_access_status.rb b/lib/gitlab/git_access_status.rb new file mode 100644 index 00000000000..3d451ecebee --- /dev/null +++ b/lib/gitlab/git_access_status.rb @@ -0,0 +1,15 @@ +module Gitlab + class GitAccessStatus + attr_accessor :status, :message + alias_method :allowed?, :status + + def initialize(status, message = '') + @status = status + @message = message + end + + def to_json + {status: @status, message: @message}.to_json + end + end +end
\ No newline at end of file diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb new file mode 100644 index 00000000000..a2177c8d548 --- /dev/null +++ b/lib/gitlab/git_access_wiki.rb @@ -0,0 +1,11 @@ +module Gitlab + class GitAccessWiki < GitAccess + def change_access_check(user, project, change) + if user.can?(:write_wiki, project) + build_status_object(true) + else + build_status_object(false, "You don't have access") + end + end + end +end diff --git a/lib/gitlab/git_logger.rb b/lib/gitlab/git_logger.rb index fbfed205a0f..9e02ccc0f44 100644 --- a/lib/gitlab/git_logger.rb +++ b/lib/gitlab/git_logger.rb @@ -1,7 +1,7 @@ module Gitlab class GitLogger < Gitlab::Logger - def self.file_name - 'githost.log' + def self.file_name_noext + 'githost' end def format_message(severity, timestamp, progname, msg) diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb index 13cb08948bb..39d17def930 100644 --- a/lib/gitlab/git_ref_validator.rb +++ b/lib/gitlab/git_ref_validator.rb @@ -5,7 +5,8 @@ module Gitlab # # Returns true for a valid reference name, false otherwise def validate(ref_name) - system *%W(git check-ref-format refs/#{ref_name}) + Gitlab::Utils.system_silent( + %W(git check-ref-format refs/#{ref_name})) end end end diff --git a/lib/gitlab/issues_labels.rb b/lib/gitlab/issues_labels.rb index 0d34976736f..1bec6088292 100644 --- a/lib/gitlab/issues_labels.rb +++ b/lib/gitlab/issues_labels.rb @@ -15,7 +15,6 @@ module Gitlab { title: "support", color: yellow }, { title: "discussion", color: blue }, { title: "suggestion", color: blue }, - { title: "feature", color: green }, { title: "enhancement", color: green } ] diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index d2235d2e3bc..0c85acf7e69 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -1,18 +1,21 @@ +# LDAP authorization model +# +# * Check if we are allowed access (not blocked) +# module Gitlab module LDAP class Access - attr_reader :adapter + attr_reader :adapter, :provider, :user - def self.open(&block) - Gitlab::LDAP::Adapter.open do |adapter| - block.call(self.new(adapter)) + def self.open(user, &block) + Gitlab::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter| + block.call(self.new(user, adapter)) end end def self.allowed?(user) - self.open do |access| - if access.allowed?(user) - # GitLab EE LDAP code goes here + self.open(user) do |access| + if access.allowed? user.last_credential_check_at = Time.now user.save true @@ -22,21 +25,30 @@ module Gitlab end end - def initialize(adapter=nil) + def initialize(user, adapter=nil) @adapter = adapter + @user = user + @provider = user.ldap_identity.provider end - def allowed?(user) - if Gitlab::LDAP::Person.find_by_dn(user.extern_uid, adapter) - if Gitlab.config.ldap.active_directory - !Gitlab::LDAP::Person.disabled_via_active_directory?(user.extern_uid, adapter) - end + def allowed? + if Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter) + return true unless ldap_config.active_directory + !Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter) else false end rescue false end + + def adapter + @adapter ||= Gitlab::LDAP::Adapter.new(provider) + end + + def ldap_config + Gitlab::LDAP::Config.new(provider) + end end end end diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb index 68ac1b22909..256cdb4c2f1 100644 --- a/lib/gitlab/ldap/adapter.rb +++ b/lib/gitlab/ldap/adapter.rb @@ -1,55 +1,28 @@ module Gitlab module LDAP class Adapter - attr_reader :ldap + attr_reader :provider, :ldap - def self.open(&block) - Net::LDAP.open(adapter_options) do |ldap| - block.call(self.new(ldap)) + def self.open(provider, &block) + Net::LDAP.open(config(provider).adapter_options) do |ldap| + block.call(self.new(provider, ldap)) end end - def self.config - Gitlab.config.ldap + def self.config(provider) + Gitlab::LDAP::Config.new(provider) end - def self.adapter_options - encryption = - case config['method'].to_s - when 'ssl' - :simple_tls - when 'tls' - :start_tls - else - nil - end - - options = { - host: config['host'], - port: config['port'], - encryption: encryption - } - - auth_options = { - auth: { - method: :simple, - username: config['bind_dn'], - password: config['password'] - } - } - - if config['password'] || config['bind_dn'] - options.merge!(auth_options) - end - options + def initialize(provider, ldap=nil) + @provider = provider + @ldap = ldap || Net::LDAP.new(config.adapter_options) end - - def initialize(ldap=nil) - @ldap = ldap || Net::LDAP.new(self.class.adapter_options) + def config + Gitlab::LDAP::Config.new(provider) end - def users(field, value) + def users(field, value, limit = nil) if field.to_sym == :dn options = { base: value, @@ -57,13 +30,13 @@ module Gitlab } else options = { - base: config['base'], + base: config.base, filter: Net::LDAP::Filter.eq(field, value) } end - if config['user_filter'].present? - user_filter = Net::LDAP::Filter.construct(config['user_filter']) + if config.user_filter.present? + user_filter = Net::LDAP::Filter.construct(config.user_filter) options[:filter] = if options[:filter] Net::LDAP::Filter.join(options[:filter], user_filter) @@ -72,12 +45,16 @@ module Gitlab end end + if limit.present? + options.merge!(size: limit) + end + entries = ldap_search(options).select do |entry| entry.respond_to? config.uid end entries.map do |entry| - Gitlab::LDAP::Person.new(entry) + Gitlab::LDAP::Person.new(entry, provider) end end @@ -105,12 +82,6 @@ module Gitlab results end end - - private - - def config - @config ||= self.class.config - end end end end diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb new file mode 100644 index 00000000000..8af2c74e959 --- /dev/null +++ b/lib/gitlab/ldap/authentication.rb @@ -0,0 +1,71 @@ +# This calls helps to authenticate to LDAP by providing username and password +# +# Since multiple LDAP servers are supported, it will loop through all of them +# until a valid bind is found +# + +module Gitlab + module LDAP + class Authentication + def self.login(login, password) + return unless Gitlab::LDAP::Config.enabled? + return unless login.present? && password.present? + + auth = nil + # loop through providers until valid bind + providers.find do |provider| + auth = new(provider) + auth.login(login, password) # true will exit the loop + end + + # If (login, password) was invalid for all providers, the value of auth is now the last + # Gitlab::LDAP::Authentication instance we tried. + auth.user + end + + def self.providers + Gitlab::LDAP::Config.providers + end + + attr_accessor :provider, :ldap_user + + def initialize(provider) + @provider = provider + end + + def login(login, password) + @ldap_user = adapter.bind_as( + filter: user_filter(login), + size: 1, + password: password + ) + end + + def adapter + OmniAuth::LDAP::Adaptor.new(config.options.symbolize_keys) + end + + def config + Gitlab::LDAP::Config.new(provider) + end + + def user_filter(login) + filter = Net::LDAP::Filter.eq(config.uid, login) + + # Apply LDAP user filter if present + if config.user_filter.present? + filter = Net::LDAP::Filter.join( + filter, + Net::LDAP::Filter.construct(config.user_filter) + ) + end + filter + end + + def user + return nil unless ldap_user + Gitlab::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider) + end + end + end +end diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb new file mode 100644 index 00000000000..0cb24d0ccc1 --- /dev/null +++ b/lib/gitlab/ldap/config.rb @@ -0,0 +1,120 @@ +# Load a specific server configuration +module Gitlab + module LDAP + class Config + attr_accessor :provider, :options + + def self.enabled? + Gitlab.config.ldap.enabled + end + + def self.servers + Gitlab.config.ldap.servers.values + end + + def self.providers + servers.map {|server| server['provider_name'] } + end + + def self.valid_provider?(provider) + providers.include?(provider) + end + + def self.invalid_provider(provider) + raise "Unknown provider (#{provider}). Available providers: #{providers}" + end + + def initialize(provider) + if self.class.valid_provider?(provider) + @provider = provider + elsif provider == 'ldap' + @provider = self.class.providers.first + else + self.class.invalid_provider(provider) + end + @options = config_for(@provider) # Use @provider, not provider + end + + def enabled? + base_config.enabled + end + + def adapter_options + { + host: options['host'], + port: options['port'], + encryption: encryption + }.tap do |options| + options.merge!(auth_options) if has_auth? + end + end + + def base + options['base'] + end + + def uid + options['uid'] + end + + def sync_ssh_keys? + sync_ssh_keys.present? + end + + # The LDAP attribute in which the ssh keys are stored + def sync_ssh_keys + options['sync_ssh_keys'] + end + + def user_filter + options['user_filter'] + end + + def group_base + options['group_base'] + end + + def admin_group + options['admin_group'] + end + + def active_directory + options['active_directory'] + end + + protected + def base_config + Gitlab.config.ldap + end + + def config_for(provider) + base_config.servers.values.find { |server| server['provider_name'] == provider } + end + + def encryption + case options['method'].to_s + when 'ssl' + :simple_tls + when 'tls' + :start_tls + else + nil + end + end + + def auth_options + { + auth: { + method: :simple, + username: options['bind_dn'], + password: options['password'] + } + } + end + + def has_auth? + options['password'] || options['bind_dn'] + end + end + end +end diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 87c3d711db4..3e0b3e6cbf8 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -6,24 +6,24 @@ module Gitlab # Source: http://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/ AD_USER_DISABLED = Net::LDAP::Filter.ex("userAccountControl:1.2.840.113556.1.4.803", "2") - def self.find_by_uid(uid, adapter=nil) - adapter ||= Gitlab::LDAP::Adapter.new - adapter.user(config.uid, uid) + attr_accessor :entry, :provider + + def self.find_by_uid(uid, adapter) + adapter.user(adapter.config.uid, uid) end - def self.find_by_dn(dn, adapter=nil) - adapter ||= Gitlab::LDAP::Adapter.new + def self.find_by_dn(dn, adapter) adapter.user('dn', dn) end - def self.disabled_via_active_directory?(dn, adapter=nil) - adapter ||= Gitlab::LDAP::Adapter.new + def self.disabled_via_active_directory?(dn, adapter) adapter.dn_matches_filter?(dn, AD_USER_DISABLED) end - def initialize(entry) + def initialize(entry, provider) Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" } @entry = entry + @provider = provider end def name @@ -38,6 +38,10 @@ module Gitlab uid end + def email + entry.try(:mail) + end + def dn entry.dn end @@ -48,12 +52,8 @@ module Gitlab @entry end - def adapter - @adapter ||= Gitlab::LDAP::Adapter.new - end - def config - @config ||= Gitlab.config.ldap + @config ||= Gitlab::LDAP::Config.new(provider) end end end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 25b5a702f9a..3ef494ba137 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -10,77 +10,51 @@ module Gitlab module LDAP class User < Gitlab::OAuth::User class << self - def find_or_create(auth_hash) - self.auth_hash = auth_hash - find(auth_hash) || find_and_connect_by_email(auth_hash) || create(auth_hash) - end - - def find_and_connect_by_email(auth_hash) - self.auth_hash = auth_hash - user = model.find_by(email: self.auth_hash.email) - - if user - user.update_attributes(extern_uid: auth_hash.uid, provider: auth_hash.provider) - Gitlab::AppLogger.info("(LDAP) Updating legacy LDAP user #{self.auth_hash.email} with extern_uid => #{auth_hash.uid}") - return user - end - end - - def authenticate(login, password) - # Check user against LDAP backend if user is not authenticated - # Only check with valid login and password to prevent anonymous bind results - return nil unless ldap_conf.enabled && login.present? && password.present? - - ldap_user = adapter.bind_as( - filter: user_filter(login), - size: 1, - password: password - ) - - find_by_uid(ldap_user.dn) if ldap_user - end - - def adapter - @adapter ||= OmniAuth::LDAP::Adaptor.new(ldap_conf) + def find_by_uid_and_provider(uid, provider) + # LDAP distinguished name is case-insensitive + identity = ::Identity. + where(provider: [provider, :ldap]). + where('lower(extern_uid) = ?', uid.downcase).last + identity && identity.user end + end - protected - - def find_by_uid_and_provider - find_by_uid(auth_hash.uid) - end + def initialize(auth_hash) + super + update_user_attributes + end - def find_by_uid(uid) - # LDAP distinguished name is case-insensitive - model.where("provider = ? and lower(extern_uid) = ?", provider, uid.downcase).last - end + # instance methods + def gl_user + @gl_user ||= find_by_uid_and_provider || find_by_email || build_new_user + end - def provider - 'ldap' - end + def find_by_uid_and_provider + self.class.find_by_uid_and_provider( + auth_hash.uid.downcase, auth_hash.provider) + end - def raise_error(message) - raise OmniAuth::Error, "(LDAP) " + message - end + def find_by_email + ::User.find_by(email: auth_hash.email) + end - def ldap_conf - Gitlab.config.ldap - end + def update_user_attributes + gl_user.email = auth_hash.email + gl_user.identities.build(provider: auth_hash.provider, extern_uid: auth_hash.uid) + gl_user + end - def user_filter(login) - filter = Net::LDAP::Filter.eq(adapter.uid, login) - # Apply LDAP user filter if present - if ldap_conf['user_filter'].present? - user_filter = Net::LDAP::Filter.construct(ldap_conf['user_filter']) - filter = Net::LDAP::Filter.join(filter, user_filter) - end - filter - end + def changed? + gl_user.changed? end def needs_blocking? false end + + def allowed? + Gitlab::LDAP::Access.allowed?(gl_user) + end end end end diff --git a/lib/gitlab/logger.rb b/lib/gitlab/logger.rb index 8a73ec5038a..59b21149a9a 100644 --- a/lib/gitlab/logger.rb +++ b/lib/gitlab/logger.rb @@ -1,5 +1,9 @@ module Gitlab class Logger < ::Logger + def self.file_name + file_name_noext + '.log' + end + def self.error(message) build.error(message) end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 17512a51658..068c342398b 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -70,14 +70,22 @@ module Gitlab insert_piece($1) end - # Context passed to the markdoqwn pipeline + # Used markdown pipelines in GitLab: + # GitlabEmojiFilter - performs emoji replacement. + # + # see https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters + filters = [ + HTML::Pipeline::Gitlab::GitlabEmojiFilter + ] + markdown_context = { - asset_root: File.join(root_url, - Gitlab::Application.config.assets.prefix) + asset_root: Gitlab.config.gitlab.url, + asset_host: Gitlab::Application.config.asset_host } - result = HTML::Pipeline::Gitlab::MarkdownPipeline.call(text, - markdown_context) + markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline + + result = markdown_pipeline.call(text, markdown_context) text = result[:output].to_html(save_with: 0) allowed_attributes = ActionView::Base.sanitized_allowed_attributes @@ -194,7 +202,7 @@ module Gitlab if identifier == "all" link_to("@all", project_url(project), options) - elsif user = User.find_by(username: identifier) + elsif User.find_by(username: identifier) link_to("@#{identifier}", user_url(identifier), options) end end diff --git a/lib/gitlab/markdown_helper.rb b/lib/gitlab/markdown_helper.rb index abed12fe570..5e3cfc0585b 100644 --- a/lib/gitlab/markdown_helper.rb +++ b/lib/gitlab/markdown_helper.rb @@ -21,5 +21,9 @@ module Gitlab def gitlab_markdown?(filename) filename.downcase.end_with?(*%w(.mdown .md .markdown)) end + + def previewable?(filename) + gitlab_markdown?(filename) || markup?(filename) + end end end diff --git a/lib/gitlab/oauth/auth_hash.rb b/lib/gitlab/oauth/auth_hash.rb index 0198f61f427..ce52beec78e 100644 --- a/lib/gitlab/oauth/auth_hash.rb +++ b/lib/gitlab/oauth/auth_hash.rb @@ -21,7 +21,7 @@ module Gitlab end def name - (info.name || full_name).to_s.force_encoding('utf-8') + (info.try(:name) || full_name).to_s.force_encoding('utf-8') end def full_name diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/oauth/user.rb index b768eda185f..6861427864e 100644 --- a/lib/gitlab/oauth/user.rb +++ b/lib/gitlab/oauth/user.rb @@ -5,67 +5,91 @@ # module Gitlab module OAuth + class ForbiddenAction < StandardError; end + class User - class << self - attr_reader :auth_hash + attr_accessor :auth_hash, :gl_user - def find(auth_hash) - self.auth_hash = auth_hash - find_by_uid_and_provider - end + def initialize(auth_hash) + self.auth_hash = auth_hash + end - def create(auth_hash) - user = new(auth_hash) - user.save_and_trigger_callbacks - end + def persisted? + gl_user.try(:persisted?) + end - def model - ::User - end + def new? + !persisted? + end + + def valid? + gl_user.try(:valid?) + end + + def save + unauthorized_to_create unless gl_user - def auth_hash=(auth_hash) - @auth_hash = AuthHash.new(auth_hash) + if needs_blocking? + gl_user.save! + gl_user.block + else + gl_user.save! end - protected - def find_by_uid_and_provider - model.where(provider: auth_hash.provider, extern_uid: auth_hash.uid).last + log.info "(OAuth) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}" + gl_user + rescue ActiveRecord::RecordInvalid => e + log.info "(OAuth) Error saving user: #{gl_user.errors.full_messages}" + return self, e.record.errors + end + + def gl_user + @user ||= find_by_uid_and_provider + + if signup_enabled? + @user ||= build_new_user end + + @user end - # Instance methods - attr_accessor :auth_hash, :user + protected - def initialize(auth_hash) - self.auth_hash = auth_hash - self.user = self.class.model.new(user_attributes) - user.skip_confirmation! + def needs_blocking? + new? && block_after_signup? + end + + def signup_enabled? + Gitlab.config.omniauth.allow_single_sign_on + end + + def block_after_signup? + Gitlab.config.omniauth.block_auto_created_users end def auth_hash=(auth_hash) @auth_hash = AuthHash.new(auth_hash) end - def save_and_trigger_callbacks - user.save! - log.info "(OAuth) Creating user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}" - user.block if needs_blocking? + def find_by_uid_and_provider + identity = Identity.find_by(provider: auth_hash.provider, extern_uid: auth_hash.uid) + identity && identity.user + end + def build_new_user + user = ::User.new(user_attributes) + user.skip_confirmation! + user.identities.new(extern_uid: auth_hash.uid, provider: auth_hash.provider) user - rescue ActiveRecord::RecordInvalid => e - log.info "(OAuth) Email #{e.record.errors[:email]}. Username #{e.record.errors[:username]}" - return nil, e.record.errors end def user_attributes { - extern_uid: auth_hash.uid, - provider: auth_hash.provider, name: auth_hash.name, username: auth_hash.username, email: auth_hash.email, password: auth_hash.password, - password_confirmation: auth_hash.password, + password_confirmation: auth_hash.password } end @@ -73,12 +97,8 @@ module Gitlab Gitlab::AppLogger end - def raise_error(message) - raise OmniAuth::Error, "(OAuth) " + message - end - - def needs_blocking? - Gitlab.config.omniauth['block_auto_created_users'] + def unauthorized_to_create + raise ForbiddenAction.new("Unauthorized to create user, signup disabled for #{auth_hash.provider}") end end end diff --git a/lib/gitlab/production_logger.rb b/lib/gitlab/production_logger.rb new file mode 100644 index 00000000000..89ce7144b1b --- /dev/null +++ b/lib/gitlab/production_logger.rb @@ -0,0 +1,7 @@ +module Gitlab + class ProductionLogger < Gitlab::Logger + def self.file_name_noext + 'production' + end + end +end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 4b8038843b0..c4d0d85b7f5 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -67,8 +67,7 @@ module Gitlab def default_regex_message "can contain only letters, digits, '_', '-' and '.'. " \ - "It must start with letter, digit or '_', optionally preceeded by '.'. " \ - "It must not end in '.git'." + "Cannot start with '-' or end in '.git'" \ end def default_regex diff --git a/lib/gitlab/sidekiq_logger.rb b/lib/gitlab/sidekiq_logger.rb new file mode 100644 index 00000000000..c1dab87a432 --- /dev/null +++ b/lib/gitlab/sidekiq_logger.rb @@ -0,0 +1,7 @@ +module Gitlab + class SidekiqLogger < Gitlab::Logger + def self.file_name_noext + 'sidekiq' + end + end +end diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb new file mode 100644 index 00000000000..0f2db50e98c --- /dev/null +++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb @@ -0,0 +1,53 @@ +module Gitlab + module SidekiqMiddleware + class MemoryKiller + # Default the RSS limit to 0, meaning the MemoryKiller is disabled + MAX_RSS = (ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] || 0).to_s.to_i + # Give Sidekiq 15 minutes of grace time after exceeding the RSS limit + GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i + # Wait 30 seconds for running jobs to finish during graceful shutdown + SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i + + # Create a mutex used to ensure there will be only one thread waiting to + # shut Sidekiq down + MUTEX = Mutex.new + + def call(worker, job, queue) + yield + current_rss = get_rss + + return unless MAX_RSS > 0 && current_rss > MAX_RSS + + Thread.new do + # Return if another thread is already waiting to shut Sidekiq down + return unless MUTEX.try_lock + + Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\ + "#{MAX_RSS}" + Sidekiq.logger.warn "spawned thread that will shut down PID "\ + "#{Process.pid} in #{GRACE_TIME} seconds" + sleep(GRACE_TIME) + + Sidekiq.logger.warn "sending SIGUSR1 to PID #{Process.pid}" + Process.kill('SIGUSR1', Process.pid) + + Sidekiq.logger.warn "waiting #{SHUTDOWN_WAIT} seconds before sending "\ + "SIGTERM to PID #{Process.pid}" + sleep(SHUTDOWN_WAIT) + + Sidekiq.logger.warn "sending SIGTERM to PID #{Process.pid}" + Process.kill('SIGTERM', Process.pid) + end + end + + private + + def get_rss + output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{Process.pid})) + return 0 unless status.zero? + + output.to_i + end + end + end +end diff --git a/lib/gitlab/theme.rb b/lib/gitlab/theme.rb index b7c50cb734d..a7c83a880f6 100644 --- a/lib/gitlab/theme.rb +++ b/lib/gitlab/theme.rb @@ -19,5 +19,19 @@ module Gitlab return themes[id] end + + def self.type_css_class_by_id(id) + types = { + BASIC => 'light_theme', + MARS => 'dark_theme', + MODERN => 'dark_theme', + GRAY => 'dark_theme', + COLOR => 'dark_theme' + } + + id ||= Gitlab.config.gitlab.default_theme + + types[id] + end end end diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index de7e0404086..877488d8471 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -19,7 +19,7 @@ module Gitlab issue = Issue.find(id) project_issue_url(id: issue.iid, project_id: issue.project, - host: Settings.gitlab['url']) + host: Gitlab.config.gitlab['url']) end end end diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb new file mode 100644 index 00000000000..bd184c27187 --- /dev/null +++ b/lib/gitlab/utils.rb @@ -0,0 +1,13 @@ +module Gitlab + module Utils + extend self + + # Run system command without outputting to stdout. + # + # @param cmd [Array<String>] + # @return [Boolean] + def system_silent(cmd) + Popen::popen(cmd).last.zero? + end + end +end diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index c3378d6a18f..54d740908d5 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -10,6 +10,17 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML super options end + # If project has issue number 39, apostrophe will be linked in + # regular text to the issue as Redcarpet will convert apostrophe to + # #39; + # We replace apostrophe with right single quote before Redcarpet + # does the processing and put the apostrophe back in postprocessing. + # This only influences regular text, code blocks are untouched. + def normal_text(text) + return text unless text.present? + text.gsub("'", "’") + end + def block_code(code, language) # New lines are placed to fix an rendering issue # with code wrapped inside <h1> tag for next case: @@ -44,6 +55,7 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML end def postprocess(full_document) + full_document.gsub!("’", "'") unless @template.instance_variable_get("@project_wiki") || @project.nil? full_document = h.create_relative_links(full_document) end diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index 49a68c62293..c8b769ace8e 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -1,5 +1,5 @@ ## GitLab -## Maintainer: @randx +## Contributors: randx, yin8086, sashkab, orkoden, axilleas, bbodenmiller ## ## Lines starting with two hashes (##) are comments with information. ## Lines starting with one hash (#) are configuration parameters that can be uncommented. @@ -15,7 +15,7 @@ ## - installing an old version of Nginx with the chunkin module [2] compiled in, or ## - using a newer version of Nginx. ## -## At the time of writing we do not know if either of these theoretical solutions works. +## At the time of writing we do not know if either of these theoretical solutions works. ## As a workaround users can use Git over SSH to push large files. ## ## [0] https://git.kernel.org/cgit/git/git.git/tree/Documentation/technical/http-protocol.txt#n99 @@ -26,6 +26,7 @@ ## configuration ## ################################### ## +## See installation.md#using-https for additional HTTPS configuration details. upstream gitlab { server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0; @@ -33,7 +34,8 @@ upstream gitlab { ## Normal HTTP host server { - listen *:80 default_server; + listen 0.0.0.0:80 default_server; + listen [::]:80 default_server; server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com server_tokens off; ## Don't show the nginx version number, a security best practice root /home/git/gitlab/public; @@ -42,6 +44,8 @@ server { ## Or if you want to accept large git objects over http client_max_body_size 20m; + ## See app/controllers/application_controller.rb for headers set + ## Individual nginx logs for this GitLab vhost access_log /var/log/nginx/gitlab_access.log; error_log /var/log/nginx/gitlab_error.log; diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 5f1afe6575c..4e53d5e8b50 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -1,5 +1,5 @@ ## GitLab -## Contributors: randx, yin8086, sashkab, orkoden, axilleas +## Contributors: randx, yin8086, sashkab, orkoden, axilleas, bbodenmiller ## ## Modified from nginx http version ## Modified from http://blog.phusion.nl/2012/04/21/tutorial-setting-up-gitlab-on-debian-6/ @@ -19,16 +19,15 @@ ## - installing an old version of Nginx with the chunkin module [2] compiled in, or ## - using a newer version of Nginx. ## -## At the time of writing we do not know if either of these theoretical solutions works. +## At the time of writing we do not know if either of these theoretical solutions works. ## As a workaround users can use Git over SSH to push large files. ## ## [0] https://git.kernel.org/cgit/git/git.git/tree/Documentation/technical/http-protocol.txt#n99 ## [1] https://github.com/agentzh/chunkin-nginx-module#status ## [2] https://github.com/agentzh/chunkin-nginx-module ## -## ################################### -## SSL configuration ## +## configuration ## ################################### ## ## See installation.md#using-https for additional HTTPS configuration details. @@ -37,22 +36,24 @@ upstream gitlab { server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0; } -## Normal HTTP host +## Redirects all HTTP traffic to the HTTPS host server { - listen *:80 default_server; + listen 0.0.0.0:80; + listen [::]:80 default_server; server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com server_tokens off; ## Don't show the nginx version number, a security best practice - - ## Redirects all traffic to the HTTPS host - root /nowhere; ## root doesn't have to be a valid path since we are redirecting - rewrite ^ https://$server_name$request_uri? permanent; + return 301 https://$server_name$request_uri; + access_log /var/log/nginx/gitlab_access.log; + error_log /var/log/nginx/gitlab_error.log; } + ## HTTPS host server { - listen 443 ssl; + listen 0.0.0.0:443 ssl; + listen [::]:443 ssl default_server; server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com - server_tokens off; + server_tokens off; ## Don't show the nginx version number, a security best practice root /home/git/gitlab/public; ## Increase this if you want to upload large attachments @@ -60,23 +61,19 @@ server { client_max_body_size 20m; ## Strong SSL Security - ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html + ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/ ssl on; ssl_certificate /etc/nginx/ssl/gitlab.crt; ssl_certificate_key /etc/nginx/ssl/gitlab.key; - ssl_ciphers 'AES256+EECDH:AES256+EDH'; - - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_session_cache builtin:1000 shared:SSL:10m; - - ssl_prefer_server_ciphers on; + # GitLab needs backwards compatible ciphers to retain compatibility with Java IDEs + ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 5m; - ## [WARNING] The following header states that the browser should only communicate - ## with your server over a secure connection for the next 24 months. - add_header Strict-Transport-Security max-age=63072000; - add_header X-Frame-Options SAMEORIGIN; - add_header X-Content-Type-Options nosniff; + ## See app/controllers/application_controller.rb for headers set ## [Optional] If your certficate has OCSP, enable OCSP stapling to reduce the overhead and latency of running SSL. ## Replace with your ssl_trusted_certificate. For more info see: @@ -87,11 +84,10 @@ server { # ssl_stapling_verify on; # ssl_trusted_certificate /etc/nginx/ssl/stapling.trusted.crt; # resolver 208.67.222.222 208.67.222.220 valid=300s; # Can change to your DNS resolver if desired - # resolver_timeout 10s; + # resolver_timeout 5s; ## [Optional] Generate a stronger DHE parameter: - ## cd /etc/ssl/certs - ## sudo openssl dhparam -out dhparam.pem 4096 + ## sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096 ## # ssl_dhparam /etc/ssl/certs/dhparam.pem; diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 2eff1260b61..0230fbb010b 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -6,6 +6,7 @@ namespace :gitlab do desc "GITLAB | Create a backup of the GitLab system" task create: :environment do warn_user_is_not_gitlab + configure_cron_mode Rake::Task["gitlab:backup:db:create"].invoke Rake::Task["gitlab:backup:repo:create"].invoke @@ -21,6 +22,7 @@ namespace :gitlab do desc "GITLAB | Restore a previously created backup" task restore: :environment do warn_user_is_not_gitlab + configure_cron_mode backup = Backup::Manager.new backup.unpack @@ -35,43 +37,54 @@ namespace :gitlab do namespace :repo do task create: :environment do - puts "Dumping repositories ...".blue + $progress.puts "Dumping repositories ...".blue Backup::Repository.new.dump - puts "done".green + $progress.puts "done".green end task restore: :environment do - puts "Restoring repositories ...".blue + $progress.puts "Restoring repositories ...".blue Backup::Repository.new.restore - puts "done".green + $progress.puts "done".green end end namespace :db do task create: :environment do - puts "Dumping database ... ".blue + $progress.puts "Dumping database ... ".blue Backup::Database.new.dump - puts "done".green + $progress.puts "done".green end task restore: :environment do - puts "Restoring database ... ".blue + $progress.puts "Restoring database ... ".blue Backup::Database.new.restore - puts "done".green + $progress.puts "done".green end end namespace :uploads do task create: :environment do - puts "Dumping uploads ... ".blue + $progress.puts "Dumping uploads ... ".blue Backup::Uploads.new.dump - puts "done".green + $progress.puts "done".green end task restore: :environment do - puts "Restoring uploads ... ".blue + $progress.puts "Restoring uploads ... ".blue Backup::Uploads.new.restore - puts "done".green + $progress.puts "done".green + end + end + + def configure_cron_mode + if ENV['CRON'] + # We need an object we can say 'puts' and 'print' to; let's use a + # StringIO. + require 'stringio' + $progress = StringIO.new + else + $progress = $stdout end end end # namespace end: backup diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 9ec368254ac..7ff23a7600a 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -574,20 +574,16 @@ namespace :gitlab do Gitlab::Shell.new.version end - def required_gitlab_shell_version - File.read(File.join(Rails.root, "GITLAB_SHELL_VERSION")).strip - end - def gitlab_shell_major_version - required_gitlab_shell_version.split(".")[0].to_i + Gitlab::Shell.version_required.split('.')[0].to_i end def gitlab_shell_minor_version - required_gitlab_shell_version.split(".")[1].to_i + Gitlab::Shell.version_required.split('.')[1].to_i end def gitlab_shell_patch_version - required_gitlab_shell_version.split(".")[2].to_i + Gitlab::Shell.version_required.split('.')[2].to_i end def has_gitlab_shell3? @@ -664,7 +660,7 @@ namespace :gitlab do warn_user_is_not_gitlab start_checking "LDAP" - if ldap_config.enabled + if Gitlab::LDAP::Config.enabled? print_users(args.limit) else puts 'LDAP is disabled in config/gitlab.yml' @@ -675,39 +671,19 @@ namespace :gitlab do def print_users(limit) puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)" - ldap.search(attributes: attributes, filter: filter, size: limit, return_result: false) do |entry| - puts "DN: #{entry.dn}\t#{ldap_config.uid}: #{entry[ldap_config.uid]}" - end - end - def attributes - [ldap_config.uid] - end + servers = Gitlab::LDAP::Config.providers - def filter - uid_filter = Net::LDAP::Filter.present?(ldap_config.uid) - if user_filter - Net::LDAP::Filter.join(uid_filter, user_filter) - else - uid_filter - end - end - - def user_filter - if ldap_config['user_filter'] && ldap_config.user_filter.present? - Net::LDAP::Filter.construct(ldap_config.user_filter) - else - nil + servers.each do |server| + puts "Server: #{server}" + Gitlab::LDAP::Adapter.open(server) do |adapter| + users = adapter.users(adapter.config.uid, '*', 100) + users.each do |user| + puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}" + end + end end end - - def ldap - @ldap ||= OmniAuth::LDAP::Adaptor.new(ldap_config).connection - end - - def ldap_config - @ldap_config ||= Gitlab.config.ldap - end end # Helper methods diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index 63dcdc52370..189ad6090a4 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -92,11 +92,11 @@ namespace :gitlab do User.ldap.each do |ldap_user| print "#{ldap_user.name} (#{ldap_user.extern_uid}) ..." - if Gitlab::LDAP::Access.open { |access| access.allowed?(ldap_user) } + if Gitlab::LDAP::Access.allowed?(ldap_user) puts " [OK]".green else if block_flag - ldap_user.block! + ldap_user.block! unless ldap_user.blocked? puts " [BLOCKED]".red else puts " [NOT IN LDAP]".yellow diff --git a/lib/tasks/gitlab/db/drop_all_postgres_sequences.rake b/lib/tasks/gitlab/db/drop_all_postgres_sequences.rake new file mode 100644 index 00000000000..e9cf0a9b5e8 --- /dev/null +++ b/lib/tasks/gitlab/db/drop_all_postgres_sequences.rake @@ -0,0 +1,10 @@ +namespace :gitlab do + namespace :db do + task drop_all_postgres_sequences: :environment do + connection = ActiveRecord::Base.connection + connection.execute("SELECT c.relname FROM pg_class c WHERE c.relkind = 'S';").each do |sequence| + connection.execute("DROP SEQUENCE #{sequence['relname']}") + end + end + end +end diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index cbfa736c84c..3c693546c09 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -15,23 +15,17 @@ namespace :gitlab do git_base_path = Gitlab.config.gitlab_shell.repos_path repos_to_import = Dir.glob(git_base_path + '/**/*.git') - namespaces = Namespace.pluck(:path) - repos_to_import.each do |repo_path| # strip repo base path repo_path[0..git_base_path.length] = '' path = repo_path.sub(/\.git$/, '') - name = File.basename path - group_name = File.dirname path + group_name, name = File.split(path) group_name = nil if group_name == '.' - # Skip if group or user - next if namespaces.include?(name) - puts "Processing #{repo_path}".yellow - if path =~ /.wiki\Z/ + if path =~ /\.wiki\Z/ puts " * Skipping wiki repo" next end @@ -50,9 +44,9 @@ namespace :gitlab do # find group namespace if group_name - group = Group.find_by(path: group_name) + group = Namespace.find_by(path: group_name) # create group namespace - if !group + unless group group = Group.new(:name => group_name) group.path = group_name group.owner = user diff --git a/lib/tasks/gitlab/mail_google_schema_whitelisting.rake b/lib/tasks/gitlab/mail_google_schema_whitelisting.rake new file mode 100644 index 00000000000..f40bba24da8 --- /dev/null +++ b/lib/tasks/gitlab/mail_google_schema_whitelisting.rake @@ -0,0 +1,73 @@ +require "#{Rails.root}/app/helpers/emails_helper" +require 'action_view/helpers' +extend ActionView::Helpers + +include ActionView::Context +include EmailsHelper + +namespace :gitlab do + desc "Email google whitelisting email with example email for actions in inbox" + task mail_google_schema_whitelisting: :environment do + subject = "Rails | Implemented feature" + url = "#{Gitlab.config.gitlab.url}/base/rails-project/issues/#{rand(1..100)}#note_#{rand(10..1000)}" + schema = email_action(url) + body = email_template(schema, url) + mail = Notify.test_email("schema.whitelisting+sample@gmail.com", subject, body.html_safe) + if send_now + mail.deliver + else + puts "WOULD SEND:" + end + puts mail + end + + def email_template(schema, url) + "<html lang='en'> + <head> + <meta content='text/html; charset=utf-8' http-equiv='Content-Type'> + <title> + GitLab + </title> + </meta> + </head> + <style> + img { + max-width: 100%; + height: auto; + } + p.details { + font-style:italic; + color:#777 + } + .footer p { + font-size:small; + color:#777 + } + </style> + <body> + <div class='content'> + <div> + <p>I like it :+1: </p> + </div> + </div> + + <div class='footer' style='margin-top: 10px;'> + <p> + <br> + You're receiving this notification because you are a member of the Base / Rails Project project team. + <a href=\"#{url}\">View it on GitLab</a> + #{schema} + </p> + </div> + </body> + </html>" + end + + def send_now + if ENV['SEND'] == "true" + true + else + false + end + end +end diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index a8f26a7c029..9af93300e08 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -4,28 +4,32 @@ namespace :gitlab do task :install, [:tag, :repo] => :environment do |t, args| warn_user_is_not_gitlab - default_version = File.read(File.join(Rails.root, "GITLAB_SHELL_VERSION")).strip + default_version = Gitlab::Shell.version_required args.with_defaults(tag: 'v' + default_version, repo: "https://gitlab.com/gitlab-org/gitlab-shell.git") - user = Settings.gitlab.user - home_dir = Rails.env.test? ? Rails.root.join('tmp/tests') : Settings.gitlab.user_home - gitlab_url = Settings.gitlab.url + user = Gitlab.config.gitlab.user + home_dir = Rails.env.test? ? Rails.root.join('tmp/tests') : Gitlab.config.gitlab.user_home + gitlab_url = Gitlab.config.gitlab.url # gitlab-shell requires a / at the end of the url - gitlab_url += "/" unless gitlab_url.match(/\/$/) + gitlab_url += '/' unless gitlab_url.end_with?('/') repos_path = Gitlab.config.gitlab_shell.repos_path target_dir = Gitlab.config.gitlab_shell.path # Clone if needed unless File.directory?(target_dir) - sh "git clone '#{args.repo}' '#{target_dir}'" + system(*%W(git clone -- #{args.repo} #{target_dir})) end # Make sure we're on the right tag Dir.chdir(target_dir) do # First try to checkout without fetching # to avoid stalling tests if the Internet is down. - reset = "git reset --hard $(git describe #{args.tag} || git describe origin/#{args.tag})" - sh "#{reset} || git fetch origin && #{reset}" + reseted = reset_to_commit(args) + + unless reseted + system(*%W(git fetch origin)) + reset_to_commit(args) + end config = { user: user, @@ -54,7 +58,7 @@ namespace :gitlab do File.open("config.yml", "w+") {|f| f.puts config.to_yaml} # Launch installation process - sh "bin/install" + system(*%W(bin/install)) end # Required for debian packaging with PKGR: Setup .ssh/environment with @@ -76,7 +80,7 @@ namespace :gitlab do desc "GITLAB | Build missing projects" task build_missing_projects: :environment do Project.find_each(batch_size: 1000) do |project| - path_to_repo = File.join(Gitlab.config.gitlab_shell.repos_path, "#{project.path_with_namespace}.git") + path_to_repo = project.repository.path_to_repo if File.exists?(path_to_repo) print '-' else @@ -118,5 +122,16 @@ namespace :gitlab do puts "Quitting...".red exit 1 end + + def reset_to_commit(args) + tag, status = Gitlab::Popen.popen(%W(git describe -- #{args.tag})) + + unless status.zero? + tag, status = Gitlab::Popen.popen(%W(git describe -- origin/#{args.tag})) + end + + tag = tag.strip + system(*%W(git reset --hard #{tag})) + end end diff --git a/spec/controllers/branches_controller_spec.rb b/spec/controllers/branches_controller_spec.rb new file mode 100644 index 00000000000..610d7a84e31 --- /dev/null +++ b/spec/controllers/branches_controller_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Projects::BranchesController do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + sign_in(user) + + project.team << [user, :master] + + project.stub(:branches).and_return(['master', 'foo/bar/baz']) + project.stub(:tags).and_return(['v1.0.0', 'v2.0.0']) + controller.instance_variable_set(:@project, project) + end + + describe "POST create" do + render_views + + before { + post :create, + project_id: project.to_param, + branch_name: branch, + ref: ref + } + + context "valid branch name, valid source" do + let(:branch) { "merge_branch" } + let(:ref) { "master" } + it { should redirect_to("/#{project.path_with_namespace}/tree/merge_branch") } + end + + context "invalid branch name, valid ref" do + let(:branch) { "<script>alert('merge');</script>" } + let(:ref) { "master" } + it { should redirect_to("/#{project.path_with_namespace}/tree/alert('merge');") } + end + + context "valid branch name, invalid ref" do + let(:branch) { "merge_branch" } + let(:ref) { "<script>alert('ref');</script>" } + it { should render_template("new") } + end + + context "invalid branch name, invalid ref" do + let(:branch) { "<script>alert('merge');</script>" } + let(:ref) { "<script>alert('ref');</script>" } + it { should render_template("new") } + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index a960571206c..50580cd1334 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -5,10 +5,14 @@ FactoryGirl.define do Faker::Lorem.sentence end - sequence :name, aliases: [:file_name] do + sequence :name do Faker::Name.name end + sequence :file_name do + Faker::Internet.user_name + end + sequence(:url) { Faker::Internet.uri('http') } factory :user, aliases: [:author, :assignee, :owner, :creator] do @@ -24,6 +28,20 @@ FactoryGirl.define do admin true end + factory :omniauth_user do + ignore do + extern_uid '123456' + provider 'ldapmain' + end + + after(:create) do |user, evaluator| + user.identities << create(:identity, + provider: evaluator.provider, + extern_uid: evaluator.extern_uid + ) + end + end + factory :admin, traits: [:admin] end @@ -177,4 +195,9 @@ FactoryGirl.define do deploy_key project end + + factory :identity do + provider 'ldapmain' + extern_uid 'my-ldap-id' + end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 23314b3b1a4..60eb73e4a95 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -27,6 +27,10 @@ # FactoryGirl.define do + # Project without repository + # + # Project does not have bare repository. + # Use this factory if you dont need repository in tests factory :empty_project, class: 'Project' do sequence(:name) { |n| "project#{n}" } path { name.downcase.gsub(/\s/, '_') } @@ -47,6 +51,20 @@ FactoryGirl.define do end end + # Project with empty repository + # + # This is a case when you just created a project + # but not pushed any code there yet + factory :project_empty_repo, parent: :empty_project do + after :create do |project| + project.create_repository + end + end + + # Project with test repository + # + # Test repository source can be found at + # https://gitlab.com/gitlab-org/gitlab-test factory :project, parent: :empty_project do path { 'gitlabhq' } diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb new file mode 100644 index 00000000000..746b6fc1ac9 --- /dev/null +++ b/spec/features/atom/users_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe "User Feed", feature: true do + describe "GET /" do + let!(:user) { create(:user) } + + context "user atom feed via private token" do + it "should render user atom feed" do + visit user_path(user, :atom, private_token: user.private_token) + body.should have_selector("feed title") + end + end + + context 'feed content' do + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project, author: user, description: '') } + let(:note) { create(:note, noteable: issue, author: user, note: 'Bug confirmed', project: project) } + + before do + project.team << [user, :master] + issue_event(issue, user) + note_event(note, user) + visit user_path(user, :atom, private_token: user.private_token) + end + + it "should have issue opened event" do + body.should have_content("#{user.name} opened issue ##{issue.iid}") + end + + it "should have issue comment event" do + body.should have_content("#{user.name} commented on issue ##{issue.iid}") + end + end + end + + def issue_event(issue, user) + EventCreateService.new.open_issue(issue, user) + end + + def note_event(note, user) + EventCreateService.new.leave_note(note, user) + end +end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 92f3a6c0929..cac409b9139 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -19,8 +19,9 @@ describe 'Comments' do it 'should be valid' do should have_css(".js-main-target-form", visible: true, count: 1) find(".js-main-target-form input[type=submit]").value.should == "Add Comment" - within(".js-main-target-form") { should_not have_link("Cancel") } - within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } + within('.js-main-target-form') do + expect(page).not_to have_link('Cancel') + end end describe "with text" do @@ -31,8 +32,10 @@ describe 'Comments' do end it 'should have enable submit button and preview button' do - within(".js-main-target-form") { should_not have_css(".js-comment-button[disabled]") } - within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: true) } + within('.js-main-target-form') do + expect(page).not_to have_css('.js-comment-button[disabled]') + expect(page).to have_css('.js-md-preview-button', visible: true) + end end end end @@ -41,15 +44,17 @@ describe 'Comments' do before do within(".js-main-target-form") do fill_in "note[note]", with: "This is awsome!" - find(".js-note-preview-button").trigger("click") + find('.js-md-preview-button').click click_button "Add Comment" end end it 'should be added and form reset' do should have_content("This is awsome!") - within(".js-main-target-form") { should have_no_field("note[note]", with: "This is awesome!") } - within(".js-main-target-form") { should have_css(".js-note-preview", visible: false) } + within('.js-main-target-form') do + expect(page).to have_no_field('note[note]', with: 'This is awesome!') + expect(page).to have_css('.js-md-preview', visible: :hidden) + end within(".js-main-target-form") { should have_css(".js-note-text", visible: true) } end end @@ -172,11 +177,11 @@ describe 'Comments' do # add two separate texts and trigger previews on both within("tr[id='#{line_code}'] + .js-temp-notes-holder") do fill_in "note[note]", with: "One comment on line 7" - find(".js-note-preview-button").trigger("click") + find('.js-md-preview-button').click end within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do fill_in "note[note]", with: "Another comment on line 10" - find(".js-note-preview-button").trigger("click") + find('.js-md-preview-button').click end end end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 524c4d5fa21..d291621935b 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Projects", feature: true do +describe "Projects", feature: true, js: true do before { login_as :user } describe "DELETE /projects/:id" do @@ -10,8 +10,23 @@ describe "Projects", feature: true do visit edit_project_path(@project) end - it "should be correct path" do - expect { click_link "Remove project" }.to change {Project.count}.by(-1) + it "should remove project" do + expect { remove_project }.to change {Project.count}.by(-1) end + + it 'should delete the project from disk' do + expect(GitlabShellWorker).to( + receive(:perform_async).with(:remove_repository, + /#{@project.path_with_namespace}/) + ).twice + + remove_project + end + end + + def remove_project + click_link "Remove project" + fill_in 'confirm_name_input', with: @project.path + click_button 'Confirm' end end diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index 7b831c48611..a1206989d39 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -11,7 +11,7 @@ describe 'Users', feature: true do fill_in "user_name", with: "Name Surname" fill_in "user_username", with: "Great" fill_in "user_email", with: "name@mail.com" - fill_in "user_password", with: "password1234" + fill_in "user_password_sign_up", with: "password1234" fill_in "user_password_confirmation", with: "password1234" expect { click_button "Sign up" }.to change {User.count}.by(1) end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 7489e56f423..06e247aea61 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -5,9 +5,10 @@ describe IssuesFinder do let(:user2) { create :user } let(:project1) { create(:project) } let(:project2) { create(:project) } - let(:issue1) { create(:issue, assignee: user, project: project1) } - let(:issue2) { create(:issue, assignee: user, project: project2) } - let(:issue3) { create(:issue, assignee: user2, project: project2) } + let(:milestone) { create(:milestone, project: project1) } + let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone) } + let(:issue2) { create(:issue, author: user, assignee: user, project: project2) } + let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) } before do project1.team << [user, :master] @@ -22,37 +23,59 @@ describe IssuesFinder do issue3 end - it 'should filter by all' do - params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new.execute(user, params) - issues.size.should == 3 - end + context 'scope: all' do + it 'should filter by all' do + params = { scope: "all", state: 'opened' } + issues = IssuesFinder.new.execute(user, params) + issues.size.should == 3 + end - it 'should filter by assignee' do - params = { scope: "assigned-to-me", state: 'opened' } - issues = IssuesFinder.new.execute(user, params) - issues.size.should == 2 - end + it 'should filter by assignee id' do + params = { scope: "all", assignee_id: user.id, state: 'opened' } + issues = IssuesFinder.new.execute(user, params) + issues.size.should == 2 + end - it 'should filter by project' do - params = { scope: "assigned-to-me", state: 'opened', project_id: project1.id } - issues = IssuesFinder.new.execute(user, params) - issues.size.should == 1 - end + it 'should filter by author id' do + params = { scope: "all", author_id: user2.id, state: 'opened' } + issues = IssuesFinder.new.execute(user, params) + issues.should == [issue3] + end - it 'should be empty for unauthorized user' do - params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new.execute(nil, params) - issues.size.should be_zero + it 'should filter by milestone id' do + params = { scope: "all", milestone_id: milestone.id, state: 'opened' } + issues = IssuesFinder.new.execute(user, params) + issues.should == [issue1] + end + + it 'should be empty for unauthorized user' do + params = { scope: "all", state: 'opened' } + issues = IssuesFinder.new.execute(nil, params) + issues.size.should be_zero + end + + it 'should not include unauthorized issues' do + params = { scope: "all", state: 'opened' } + issues = IssuesFinder.new.execute(user2, params) + issues.size.should == 2 + issues.should_not include(issue1) + issues.should include(issue2) + issues.should include(issue3) + end end - it 'should not include unauthorized issues' do - params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new.execute(user2, params) - issues.size.should == 2 - issues.should_not include(issue1) - issues.should include(issue2) - issues.should include(issue3) + context 'personal scope' do + it 'should filter by assignee' do + params = { scope: "assigned-to-me", state: 'opened' } + issues = IssuesFinder.new.execute(user, params) + issues.size.should == 2 + end + + it 'should filter by project' do + params = { scope: "assigned-to-me", state: 'opened', project_id: project1.id } + issues = IssuesFinder.new.execute(user, params) + issues.size.should == 1 + end end end end diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb new file mode 100644 index 00000000000..c645cbc964c --- /dev/null +++ b/spec/finders/snippets_finder_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +describe SnippetsFinder do + let(:user) { create :user } + let(:user1) { create :user } + let(:group) { create :group } + + let(:project1) { create(:empty_project, :public, group: group) } + let(:project2) { create(:empty_project, :private, group: group) } + + + context ':all filter' do + before do + @snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE) + @snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL) + @snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC) + end + + it "returns all private and internal snippets" do + snippets = SnippetsFinder.new.execute(user, filter: :all) + snippets.should include(@snippet2, @snippet3) + snippets.should_not include(@snippet1) + end + + it "returns all public snippets" do + snippets = SnippetsFinder.new.execute(nil, filter: :all) + snippets.should include(@snippet3) + snippets.should_not include(@snippet1, @snippet2) + end + end + + context ':by_user filter' do + before do + @snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE, author: user) + @snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL, author: user) + @snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC, author: user) + end + + it "returns all public and internal snippets" do + snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user) + snippets.should include(@snippet2, @snippet3) + snippets.should_not include(@snippet1) + end + + it "returns internal snippets" do + snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal") + snippets.should include(@snippet2) + snippets.should_not include(@snippet1, @snippet3) + end + + it "returns private snippets" do + snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private") + snippets.should include(@snippet1) + snippets.should_not include(@snippet2, @snippet3) + end + + it "returns public snippets" do + snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public") + snippets.should include(@snippet3) + snippets.should_not include(@snippet1, @snippet2) + end + + it "returns all snippets" do + snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user) + snippets.should include(@snippet1, @snippet2, @snippet3) + end + + it "returns only public snippets if unauthenticated user" do + snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user) + snippets.should include(@snippet3) + snippets.should_not include(@snippet2, @snippet1) + end + + end + + context 'by_project filter' do + before do + @snippet1 = create(:project_snippet, visibility_level: Snippet::PRIVATE, project: project1) + @snippet2 = create(:project_snippet, visibility_level: Snippet::INTERNAL, project: project1) + @snippet3 = create(:project_snippet, visibility_level: Snippet::PUBLIC, project: project1) + end + + it "returns public snippets for unauthorized user" do + snippets = SnippetsFinder.new.execute(nil, filter: :by_project, project: project1) + snippets.should include(@snippet3) + snippets.should_not include(@snippet1, @snippet2) + end + + it "returns public and internal snippets for none project members" do + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) + snippets.should include(@snippet2, @snippet3) + snippets.should_not include(@snippet1) + end + + it "returns all snippets for project members" do + project1.team << [user, :developer] + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) + snippets.should include(@snippet1, @snippet2, @snippet3) + end + end +end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 2db67cfdf95..07dd33b211b 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -66,6 +66,16 @@ describe ApplicationHelper do avatar_icon(user.email).to_s.should match("/uploads/user/avatar/#{ user.id }/gitlab_logo.png") end + it "should return an url for the avatar with relative url" do + Gitlab.config.gitlab.stub(relative_url_root: "/gitlab") + Gitlab.config.gitlab.stub(url: Settings.send(:build_gitlab_url)) + + user = create(:user) + user.avatar = File.open(avatar_file_path) + user.save! + avatar_icon(user.email).to_s.should match("/gitlab//uploads/user/avatar/#{ user.id }/gitlab_logo.png") + end + it "should call gravatar_icon when no avatar is present" do user = create(:user, email: 'test@example.com') user.save! diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb new file mode 100644 index 00000000000..4de54d291f2 --- /dev/null +++ b/spec/helpers/events_helper_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe EventsHelper do + include ApplicationHelper + include GitlabMarkdownHelper + + it 'should display one line of plain text without alteration' do + input = 'A short, plain note' + expect(event_note(input)).to match(input) + expect(event_note(input)).not_to match(/\.\.\.\z/) + end + + it 'should display inline code' do + input = 'A note with `inline code`' + expected = 'A note with <code>inline code</code>' + + expect(event_note(input)).to match(expected) + end + + it 'should truncate a note with multiple paragraphs' do + input = "Paragraph 1\n\nParagraph 2" + expected = 'Paragraph 1...' + + expect(event_note(input)).to match(expected) + end + + it 'should display the first line of a code block' do + input = "```\nCode block\nwith two lines\n```" + expected = '<pre><code class="">Code block...</code></pre>' + + expect(event_note(input)).to match(expected) + end + + it 'should truncate a single long line of text' do + text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars + input = "#{text}#{text}#{text}#{text}" # 200 chars + expected = "#{text}#{text}".sub(/.{3}/, '...') + + expect(event_note(input)).to match(expected) + end + + it 'should preserve a link href when link text is truncated' do + text = 'The quick brown fox jumped over the lazy dog' # 44 chars + input = "#{text}#{text}#{text} " # 133 chars + link_url = 'http://example.com/foo/bar/baz' # 30 chars + input << link_url + expected_link_text = 'http://example...</a>' + + expect(event_note(input)).to match(link_url) + expect(event_note(input)).to match(expected_link_text) + end +end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 15033f07432..3c636b747d1 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -60,7 +60,7 @@ describe GitlabMarkdownHelper do end it "should link using a short id" do - actual = "Backported from #{commit.short_id(6)}" + actual = "Backported from #{commit.short_id}" gfm(actual).should match(expected) end @@ -530,6 +530,24 @@ describe GitlabMarkdownHelper do markdown(actual).should match(%r{<li>light by <a.+>@#{member.user.username}</a></li>}) end + it "should not link the apostrophe to issue 39" do + project.team << [user, :master] + project.issues.stub(:where).with(iid: '39').and_return([issue]) + + actual = "Yes, it is @#{member.user.username}'s task." + expected = /Yes, it is <a.+>@#{member.user.username}<\/a>'s task/ + markdown(actual).should match(expected) + end + + it "should not link the apostrophe to issue 39 in code blocks" do + project.team << [user, :master] + project.issues.stub(:where).with(iid: '39').and_return([issue]) + + actual = "Yes, `it is @#{member.user.username}'s task.`" + expected = /Yes, <code>it is @gfm\'s task.<\/code>/ + markdown(actual).should match(expected) + end + it "should handle references in <em>" do actual = "Apply _!#{merge_request.iid}_ ASAP" @@ -576,9 +594,23 @@ describe GitlabMarkdownHelper do end it "should generate absolute urls for emoji" do - markdown(":smile:").should include("src=\"#{url_helper('emoji/smile')}") + markdown(':smile:').should( + include(%(src="#{Gitlab.config.gitlab.url}/assets/emoji/smile.png)) + ) + end + + it "should generate absolute urls for emoji if relative url is present" do + Gitlab.config.gitlab.stub(:url).and_return('http://localhost/gitlab/root') + markdown(":smile:").should include("src=\"http://localhost/gitlab/root/assets/emoji/smile.png") + end + + it "should generate absolute urls for emoji if asset_host is present" do + Gitlab::Application.config.stub(:asset_host).and_return("https://cdn.example.com") + ActionView::Base.any_instance.stub_chain(:config, :asset_host).and_return("https://cdn.example.com") + markdown(":smile:").should include("src=\"https://cdn.example.com/assets/emoji/smile.png") end + it "should handle relative urls for a file in master" do actual = "[GitLab API doc](doc/api/README.md)\n" expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md\">GitLab API doc</a></p>\n" diff --git a/spec/helpers/oauth_helper_spec.rb b/spec/helpers/oauth_helper_spec.rb new file mode 100644 index 00000000000..453699136e9 --- /dev/null +++ b/spec/helpers/oauth_helper_spec.rb @@ -0,0 +1,20 @@ +require "spec_helper" + +describe OauthHelper do + describe "additional_providers" do + it 'returns all enabled providers' do + allow(helper).to receive(:enabled_oauth_providers) { [:twitter, :github] } + helper.additional_providers.should include(*[:twitter, :github]) + end + + it 'does not return ldap provider' do + allow(helper).to receive(:enabled_oauth_providers) { [:twitter, :ldapmain] } + helper.additional_providers.should include(:twitter) + end + + it 'returns empty array' do + allow(helper).to receive(:enabled_oauth_providers) { [] } + helper.additional_providers.should == [] + end + end +end
\ No newline at end of file diff --git a/spec/lib/disable_email_interceptor_spec.rb b/spec/lib/disable_email_interceptor_spec.rb new file mode 100644 index 00000000000..8bf6ee2ed50 --- /dev/null +++ b/spec/lib/disable_email_interceptor_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe DisableEmailInterceptor do + before do + ActionMailer::Base.register_interceptor(DisableEmailInterceptor) + end + + it 'should not send emails' do + Gitlab.config.gitlab.stub(:email_enabled).and_return(false) + expect { + deliver_mail + }.not_to change(ActionMailer::Base.deliveries, :count) + end + + after do + # Removing interceptor from the list because unregister_interceptor is + # implemented in later version of mail gem + # See: https://github.com/mikel/mail/pull/705 + Mail.class_variable_set(:@@delivery_interceptors, []) + end + + def deliver_mail + key = create :personal_key + Notify.new_ssh_key_email(key.id) + end +end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 551fb3fb5f6..95fc7e16a11 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -10,13 +10,21 @@ describe Gitlab::Auth do password: password, password_confirmation: password) end - let(:username) { 'john' } + let(:username) { 'John' } # username isn't lowercase, test this let(:password) { 'my-secret' } it "should find user by valid login/password" do expect( gl_auth.find(username, password) ).to eql user end + it 'should find user by valid email/password with case-insensitive email' do + expect(gl_auth.find(user.email.upcase, password)).to eql user + end + + it 'should find user by valid username/password with case-insensitive username' do + expect(gl_auth.find(username.upcase, password)).to eql user + end + it "should not find user with invalid password" do password = 'wrong' expect( gl_auth.find(username, password) ).to_not eql user @@ -28,17 +36,16 @@ describe Gitlab::Auth do end context "with ldap enabled" do - before { Gitlab.config.ldap['enabled'] = true } - after { Gitlab.config.ldap['enabled'] = false } + before { Gitlab::LDAP::Config.stub(enabled?: true) } it "tries to autheticate with db before ldap" do - expect(Gitlab::LDAP::User).not_to receive(:authenticate) + expect(Gitlab::LDAP::Authentication).not_to receive(:login) gl_auth.find(username, password) end it "uses ldap as fallback to for authentication" do - expect(Gitlab::LDAP::User).to receive(:authenticate) + expect(Gitlab::LDAP::Authentication).to receive(:login) gl_auth.find('ldap_user', 'password') end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 570b03827a8..66e87e57cbc 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -5,14 +5,14 @@ describe Gitlab::GitAccess do let(:project) { create(:project) } let(:user) { create(:user) } - describe 'download_allowed?' do + describe 'download_access_check' do describe 'master permissions' do before { project.team << [user, :master] } context 'pull code' do - subject { access.download_allowed?(user, project) } + subject { access.download_access_check(user, project) } - it { should be_true } + it { subject.allowed?.should be_true } end end @@ -20,9 +20,9 @@ describe Gitlab::GitAccess do before { project.team << [user, :guest] } context 'pull code' do - subject { access.download_allowed?(user, project) } + subject { access.download_access_check(user, project) } - it { should be_false } + it { subject.allowed?.should be_false } end end @@ -33,34 +33,54 @@ describe Gitlab::GitAccess do end context 'pull code' do - subject { access.download_allowed?(user, project) } + subject { access.download_access_check(user, project) } - it { should be_false } + it { subject.allowed?.should be_false } end end describe 'without acccess to project' do context 'pull code' do - subject { access.download_allowed?(user, project) } + subject { access.download_access_check(user, project) } - it { should be_false } + it { subject.allowed?.should be_false } + end + end + + describe 'deploy key permissions' do + let(:key) { create(:deploy_key) } + + context 'pull code' do + context 'allowed' do + before { key.projects << project } + subject { access.download_access_check(key, project) } + + it { subject.allowed?.should be_true } + end + + context 'denied' do + subject { access.download_access_check(key, project) } + + it { subject.allowed?.should be_false } + end end end end - describe 'push_allowed?' do + describe 'push_access_check' do def protect_feature_branch create(:protected_branch, name: 'feature', project: project) end def changes { - push_new_branch: '000000000 570e7b2ab refs/heads/wow', + push_new_branch: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow", push_master: '6f6d7e7ed 570e7b2ab refs/heads/master', push_protected_branch: '6f6d7e7ed 570e7b2ab refs/heads/feature', - push_remove_protected_branch: '570e7b2ab 000000000 refs/heads/feature', + push_remove_protected_branch: "570e7b2ab #{Gitlab::Git::BLANK_SHA} "\ + 'refs/heads/feature', push_tag: '6f6d7e7ed 570e7b2ab refs/tags/v1.0.0', - push_new_tag: '000000000 570e7b2ab refs/tags/v7.8.9', + push_new_tag: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/tags/v7.8.9", push_all: ['6f6d7e7ed 570e7b2ab refs/heads/master', '6f6d7e7ed 570e7b2ab refs/heads/feature'] } end @@ -116,9 +136,9 @@ describe Gitlab::GitAccess do permissions_matrix[role].each do |action, allowed| context action do - subject { access.push_allowed?(user, project, changes[action]) } + subject { access.push_access_check(user, project, changes[action]) } - it { should allowed ? be_true : be_false } + it { subject.allowed?.should allowed ? be_true : be_false } end end end diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb new file mode 100644 index 00000000000..4ff45c0c616 --- /dev/null +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Gitlab::GitAccessWiki do + let(:access) { Gitlab::GitAccessWiki.new } + let(:project) { create(:project) } + let(:user) { create(:user) } + + describe 'push_allowed?' do + before do + create(:protected_branch, name: 'master', project: project) + project.team << [user, :developer] + end + + subject { access.push_access_check(user, project, changes) } + + it { subject.allowed?.should be_true } + end + + def changes + ['6f6d7e7ed 570e7b2ab refs/heads/master'] + end +end diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index d50f605e050..4573b8696c4 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' describe Gitlab::LDAP::Access do - let(:access) { Gitlab::LDAP::Access.new } - let(:user) { create(:user) } + let(:access) { Gitlab::LDAP::Access.new user } + let(:user) { create(:omniauth_user) } describe :allowed? do - subject { access.allowed?(user) } + subject { access.allowed? } context 'when the user cannot be found' do before { Gitlab::LDAP::Person.stub(find_by_dn: nil) } @@ -28,20 +28,14 @@ describe Gitlab::LDAP::Access do it { should be_true } end - context 'and has no disabled flag in active diretory' do - before { - Gitlab::LDAP::Person.stub(disabled_via_active_directory?: false) - Gitlab.config.ldap['enabled'] = true - Gitlab.config.ldap['active_directory'] = false - } - - after { - Gitlab.config.ldap['enabled'] = false - Gitlab.config.ldap['active_directory'] = true - } + context 'without ActiveDirectory enabled' do + before do + Gitlab::LDAP::Config.stub(enabled?: true) + Gitlab::LDAP::Config.any_instance.stub(active_directory: false) + end - it { should be_false } + it { should be_true } end end end -end +end
\ No newline at end of file diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb index c3f07334431..19347e47378 100644 --- a/spec/lib/gitlab/ldap/adapter_spec.rb +++ b/spec/lib/gitlab/ldap/adapter_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::LDAP::Adapter do - let(:adapter) { Gitlab::LDAP::Adapter.new } + let(:adapter) { Gitlab::LDAP::Adapter.new 'ldapmain' } describe :dn_matches_filter? do let(:ldap) { double(:ldap) } diff --git a/spec/lib/gitlab/ldap/authentication_spec.rb b/spec/lib/gitlab/ldap/authentication_spec.rb new file mode 100644 index 00000000000..11fdf108756 --- /dev/null +++ b/spec/lib/gitlab/ldap/authentication_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Gitlab::LDAP::Authentication do + let(:klass) { Gitlab::LDAP::Authentication } + let(:user) { create(:omniauth_user, extern_uid: dn) } + let(:dn) { 'uid=john,ou=people,dc=example,dc=com' } + let(:login) { 'john' } + let(:password) { 'password' } + + describe :login do + let(:adapter) { double :adapter } + before do + Gitlab::LDAP::Config.stub(enabled?: true) + end + + it "finds the user if authentication is successful" do + user + # try only to fake the LDAP call + klass.any_instance.stub(adapter: double(:adapter, + bind_as: double(:ldap_user, dn: dn) + )) + expect(klass.login(login, password)).to be_true + end + + it "is false if the user does not exist" do + # try only to fake the LDAP call + klass.any_instance.stub(adapter: double(:adapter, + bind_as: double(:ldap_user, dn: dn) + )) + expect(klass.login(login, password)).to be_false + end + + it "is false if authentication fails" do + user + # try only to fake the LDAP call + klass.any_instance.stub(adapter: double(:adapter, bind_as: nil)) + expect(klass.login(login, password)).to be_false + end + + it "fails if ldap is disabled" do + Gitlab::LDAP::Config.stub(enabled?: false) + expect(klass.login(login, password)).to be_false + end + + it "fails if no login is supplied" do + expect(klass.login('', password)).to be_false + end + + it "fails if no password is supplied" do + expect(klass.login(login, '')).to be_false + end + end +end
\ No newline at end of file diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb new file mode 100644 index 00000000000..3ebb8aae243 --- /dev/null +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Gitlab::LDAP::Config do + let(:config) { Gitlab::LDAP::Config.new provider } + let(:provider) { 'ldapmain' } + + describe :initalize do + it 'requires a provider' do + expect{ Gitlab::LDAP::Config.new }.to raise_error ArgumentError + end + + it "works" do + expect(config).to be_a described_class + end + + it "raises an error if a unknow provider is used" do + expect{ Gitlab::LDAP::Config.new 'unknown' }.to raise_error + end + + context "if 'ldap' is the provider name" do + let(:provider) { 'ldap' } + + context "and 'ldap' is not in defined as a provider" do + before { Gitlab::LDAP::Config.stub(providers: %w{ldapmain}) } + + it "uses the first provider" do + # Fetch the provider_name attribute from 'options' so that we know + # that the 'options' Hash is not empty/nil. + expect(config.options['provider_name']).to eq('ldapmain') + end + end + end + end +end diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index d232cb20759..f73884e6441 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -1,54 +1,36 @@ require 'spec_helper' describe Gitlab::LDAP::User do - let(:gl_user) { Gitlab::LDAP::User } + let(:gl_user) { Gitlab::LDAP::User.new(auth_hash) } let(:info) do - double( + { name: 'John', email: 'john@example.com', nickname: 'john' - ) + } + end + let(:auth_hash) do + double(uid: 'my-uid', provider: 'ldapmain', info: double(info)) end - before { Gitlab.config.stub(omniauth: {}) } describe :find_or_create do - let(:auth) do - double(info: info, provider: 'ldap', uid: 'my-uid') - end - it "finds the user if already existing" do - existing_user = create(:user, extern_uid: 'my-uid', provider: 'ldap') + existing_user = create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') - expect{ gl_user.find_or_create(auth) }.to_not change{ User.count } + expect{ gl_user.save }.to_not change{ User.count } end it "connects to existing non-ldap user if the email matches" do - existing_user = create(:user, email: 'john@example.com') - expect{ gl_user.find_or_create(auth) }.to_not change{ User.count } + existing_user = create(:omniauth_user, email: 'john@example.com', provider: "twitter") + expect{ gl_user.save }.to_not change{ User.count } existing_user.reload - expect(existing_user.extern_uid).to eql 'my-uid' - expect(existing_user.provider).to eql 'ldap' + expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid' + expect(existing_user.ldap_identity.provider).to eql 'ldapmain' end it "creates a new user if not found" do - expect{ gl_user.find_or_create(auth) }.to change{ User.count }.by(1) - end - end - - describe "authenticate" do - let(:login) { 'john' } - let(:password) { 'my-secret' } - - before { - Gitlab.config.ldap['enabled'] = true - Gitlab.config.ldap['user_filter'] = 'employeeType=developer' - } - after { Gitlab.config.ldap['enabled'] = false } - - it "send an authentication request to ldap" do - expect( Gitlab::LDAP::User.adapter ).to receive(:bind_as) - Gitlab::LDAP::User.authenticate(login, password) + expect{ gl_user.save }.to change{ User.count }.by(1) end end end diff --git a/spec/lib/gitlab/oauth/auth_hash_spec.rb b/spec/lib/gitlab/oauth/auth_hash_spec.rb new file mode 100644 index 00000000000..5eb77b492b2 --- /dev/null +++ b/spec/lib/gitlab/oauth/auth_hash_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Gitlab::OAuth::AuthHash do + let(:auth_hash) do + Gitlab::OAuth::AuthHash.new(double({ + provider: 'twitter', + uid: uid, + info: double(info_hash) + })) + end + let(:uid) { 'my-uid' } + let(:email) { 'my-email@example.com' } + let(:nickname) { 'my-nickname' } + let(:info_hash) { + { + email: email, + nickname: nickname, + name: 'John', + first_name: "John", + last_name: "Who" + } + } + + context "defaults" do + it { expect(auth_hash.provider).to eql 'twitter' } + it { expect(auth_hash.uid).to eql uid } + it { expect(auth_hash.email).to eql email } + it { expect(auth_hash.username).to eql nickname } + it { expect(auth_hash.name).to eql "John" } + it { expect(auth_hash.password).to_not be_empty } + end + + context "email not provided" do + before { info_hash.delete(:email) } + it "generates a temp email" do + expect( auth_hash.email).to start_with('temp-email-for-oauth') + end + end + + context "username not provided" do + before { info_hash.delete(:nickname) } + + it "takes the first part of the email as username" do + expect( auth_hash.username ).to eql "my-email" + end + end + + context "name not provided" do + before { info_hash.delete(:name) } + + it "concats first and lastname as the name" do + expect( auth_hash.name ).to eql "John Who" + end + end +end
\ No newline at end of file diff --git a/spec/lib/gitlab/oauth/user_spec.rb b/spec/lib/gitlab/oauth/user_spec.rb index c241e198609..88307515789 100644 --- a/spec/lib/gitlab/oauth/user_spec.rb +++ b/spec/lib/gitlab/oauth/user_spec.rb @@ -1,83 +1,109 @@ require 'spec_helper' describe Gitlab::OAuth::User do - let(:gl_auth) { Gitlab::OAuth::User } - let(:info) do - double( + let(:oauth_user) { Gitlab::OAuth::User.new(auth_hash) } + let(:gl_user) { oauth_user.gl_user } + let(:uid) { 'my-uid' } + let(:provider) { 'my-provider' } + let(:auth_hash) { double(uid: uid, provider: provider, info: double(info_hash)) } + let(:info_hash) do + { nickname: 'john', name: 'John', email: 'john@mail.com' - ) + } end - before do - Gitlab.config.stub(omniauth: {}) - end - - describe :find do - let!(:existing_user) { create(:user, extern_uid: 'my-uid', provider: 'my-provider') } + describe :persisted? do + let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } it "finds an existing user based on uid and provider (facebook)" do auth = double(info: double(name: 'John'), uid: 'my-uid', provider: 'my-provider') - assert gl_auth.find(auth) + expect( oauth_user.persisted? ).to be_true end - it "finds an existing user based on nested uid and provider" do - auth = double(info: info, uid: 'my-uid', provider: 'my-provider') - assert gl_auth.find(auth) + it "returns false if use is not found in database" do + auth_hash.stub(uid: 'non-existing') + expect( oauth_user.persisted? ).to be_false end end - describe :create do - it "should create user from LDAP" do - auth = double(info: info, uid: 'my-uid', provider: 'ldap') - user = gl_auth.create(auth) + describe :save do + let(:provider) { 'twitter' } - user.should be_valid - user.extern_uid.should == auth.uid - user.provider.should == 'ldap' - end + describe 'signup' do + context "with allow_single_sign_on enabled" do + before { Gitlab.config.omniauth.stub allow_single_sign_on: true } - it "should create user from Omniauth" do - auth = double(info: info, uid: 'my-uid', provider: 'twitter') - user = gl_auth.create(auth) + it "creates a user from Omniauth" do + oauth_user.save - user.should be_valid - user.extern_uid.should == auth.uid - user.provider.should == 'twitter' + expect(gl_user).to be_valid + identity = gl_user.identities.first + expect(identity.extern_uid).to eql uid + expect(identity.provider).to eql 'twitter' + end + end + + context "with allow_single_sign_on disabled (Default)" do + it "throws an error" do + expect{ oauth_user.save }.to raise_error StandardError + end + end end - it "should apply defaults to user" do - auth = double(info: info, uid: 'my-uid', provider: 'ldap') - user = gl_auth.create(auth) + describe 'blocking' do + let(:provider) { 'twitter' } + before { Gitlab.config.omniauth.stub allow_single_sign_on: true } - user.should be_valid - user.projects_limit.should == Gitlab.config.gitlab.default_projects_limit - user.can_create_group.should == Gitlab.config.gitlab.default_can_create_group - end + context 'signup' do + context 'dont block on create' do + before { Gitlab.config.omniauth.stub block_auto_created_users: false } - it "Set a temp email address if not provided (like twitter does)" do - info = double( - uid: 'my-uid', - nickname: 'john', - name: 'John' - ) - auth = double(info: info, uid: 'my-uid', provider: 'my-provider') + it do + oauth_user.save + gl_user.should be_valid + gl_user.should_not be_blocked + end + end - user = gl_auth.create(auth) - expect(user.email).to_not be_empty - end + context 'block on create' do + before { Gitlab.config.omniauth.stub block_auto_created_users: true } + + it do + oauth_user.save + gl_user.should be_valid + gl_user.should be_blocked + end + end + end + + context 'sign-in' do + before do + oauth_user.save + oauth_user.gl_user.activate + end + + context 'dont block on create' do + before { Gitlab.config.omniauth.stub block_auto_created_users: false } + + it do + oauth_user.save + gl_user.should be_valid + gl_user.should_not be_blocked + end + end - it 'generates a username if non provided (google)' do - info = double( - uid: 'my-uid', - name: 'John', - email: 'john@example.com' - ) - auth = double(info: info, uid: 'my-uid', provider: 'my-provider') + context 'block on create' do + before { Gitlab.config.omniauth.stub block_auto_created_users: true } - user = gl_auth.create(auth) - expect(user.username).to eql 'john' + it do + oauth_user.save + gl_user.should be_valid + gl_user.should_not be_blocked + end + end + end end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index e06e8826e5c..a0c37587b23 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -46,7 +46,7 @@ describe Notify do token = 'kETLwRaayvigPq_x3SNM' - subject { Notify.new_user_email(new_user.id, new_user.password, token) } + subject { Notify.new_user_email(new_user.id, token) } it_behaves_like 'an email sent from GitLab' @@ -83,7 +83,7 @@ describe Notify do let(:example_site_path) { root_path } let(:new_user) { create(:user, email: 'newguy@example.com', password: "securePassword") } - subject { Notify.new_user_email(new_user.id, new_user.password) } + subject { Notify.new_user_email(new_user.id) } it_behaves_like 'an email sent from GitLab' diff --git a/spec/models/assembla_service_spec.rb b/spec/models/assembla_service_spec.rb index 0ef475b87c3..4300090eb13 100644 --- a/spec/models/assembla_service_spec.rb +++ b/spec/models/assembla_service_spec.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require 'spec_helper' diff --git a/spec/models/buildbox_service_spec.rb b/spec/models/buildbox_service_spec.rb new file mode 100644 index 00000000000..1d9ca51be16 --- /dev/null +++ b/spec/models/buildbox_service_spec.rb @@ -0,0 +1,73 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + +require 'spec_helper' + +describe BuildboxService do + describe 'Associations' do + it { should belong_to :project } + it { should have_one :service_hook } + end + + describe 'commits methods' do + before do + @project = Project.new + @project.stub( + default_branch: 'default-brancho' + ) + + @service = BuildboxService.new + @service.stub( + project: @project, + service_hook: true, + project_url: 'https://buildbox.io/account-name/example-project', + token: 'secret-sauce-webhook-token:secret-sauce-status-token' + ) + end + + describe :webhook_url do + it 'returns the webhook url' do + @service.webhook_url.should == + 'https://webhook.buildbox.io/deliver/secret-sauce-webhook-token' + end + end + + describe :commit_status_path do + it 'returns the correct status page' do + @service.commit_status_path('2ab7834c').should == + 'https://gitlab.buildbox.io/status/secret-sauce-status-token.json?commit=2ab7834c' + end + end + + describe :build_page do + it 'returns the correct build page' do + @service.build_page('2ab7834c').should == + 'https://buildbox.io/account-name/example-project/builds?commit=2ab7834c' + end + end + + describe :builds_page do + it 'returns the correct path to the builds page' do + @service.builds_path.should == + 'https://buildbox.io/account-name/example-project/builds?branch=default-brancho' + end + end + + describe :status_img_path do + it 'returns the correct path to the status image' do + @service.status_img_path.should == 'https://badge.buildbox.io/secret-sauce-status-token.svg' + end + end + end +end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 6f201adc4e8..a6ec44da4be 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -75,7 +75,7 @@ eos it_behaves_like 'a mentionable' do let(:subject) { commit } let(:mauthor) { create :user, email: commit.author_email } - let(:backref_text) { "commit #{subject.sha[0..5]}" } + let(:backref_text) { "commit #{subject.id}" } let(:set_mentionable_text) { ->(txt){ subject.stub(safe_message: txt) } } # Include the subject in the repository stub. diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb new file mode 100644 index 00000000000..ca6f11b2a4d --- /dev/null +++ b/spec/models/concerns/mentionable_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe Issue, "Mentionable" do + describe :mentioned_users do + let!(:user) { create(:user, username: 'stranger') } + let!(:user2) { create(:user, username: 'john') } + let!(:issue) { create(:issue, description: '@stranger mentioned') } + + subject { issue.mentioned_users } + + it { should include(user) } + it { should_not include(user2) } + end +end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 1fdd959da9d..204ae9da704 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -36,7 +36,7 @@ describe Event do @user = project.owner data = { - before: "0000000000000000000000000000000000000000", + before: Gitlab::Git::BLANK_SHA, after: "0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e", ref: "refs/heads/master", user_id: @user.id, @@ -60,7 +60,6 @@ describe Event do it { @event.push?.should be_true } it { @event.proper?.should be_true } - it { @event.new_branch?.should be_true } it { @event.tag?.should be_false } it { @event.branch_name.should == "master" } it { @event.author.should == @user } diff --git a/spec/models/flowdock_service_spec.rb b/spec/models/flowdock_service_spec.rb index 710b8cba502..5540f0fa988 100644 --- a/spec/models/flowdock_service_spec.rb +++ b/spec/models/flowdock_service_spec.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require 'spec_helper' diff --git a/spec/models/gemnasium_service_spec.rb b/spec/models/gemnasium_service_spec.rb index 5de645cdf33..60ffa6f8b05 100644 --- a/spec/models/gemnasium_service_spec.rb +++ b/spec/models/gemnasium_service_spec.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require 'spec_helper' diff --git a/spec/models/gitlab_ci_service_spec.rb b/spec/models/gitlab_ci_service_spec.rb index e4cd8bb90c3..83277058fbb 100644 --- a/spec/models/gitlab_ci_service_spec.rb +++ b/spec/models/gitlab_ci_service_spec.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require 'spec_helper' @@ -34,11 +34,11 @@ describe GitlabCiService do end describe :commit_status_path do - it { @service.commit_status_path("2ab7834c").should == "http://ci.gitlab.org/projects/2/builds/2ab7834c/status.json?token=verySecret"} + it { @service.commit_status_path("2ab7834c").should == "http://ci.gitlab.org/projects/2/commits/2ab7834c/status.json?token=verySecret"} end describe :build_page do - it { @service.build_page("2ab7834c").should == "http://ci.gitlab.org/projects/2/builds/2ab7834c"} + it { @service.build_page("2ab7834c").should == "http://ci.gitlab.org/projects/2/commits/2ab7834c"} end end end diff --git a/spec/models/group_member_spec.rb b/spec/models/group_member_spec.rb index 6acbc9bb4ae..38657de6793 100644 --- a/spec/models/group_member_spec.rb +++ b/spec/models/group_member_spec.rb @@ -1,14 +1,16 @@ # == Schema Information # -# Table name: group_members +# Table name: members # # id :integer not null, primary key # access_level :integer not null -# group_id :integer not null +# source_id :integer not null +# source_type :string(255) not null # user_id :integer not null +# notification_level :integer not null +# type :string(255) # created_at :datetime # updated_at :datetime -# notification_level :integer default(3), not null # require 'spec_helper' diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index eeecd714a28..6ab7162c15c 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -228,7 +228,7 @@ describe Note do it { should be_valid } its(:noteable) { should == issue } - its(:note) { should == "_mentioned in commit #{commit.sha[0..5]}_" } + its(:note) { should == "_mentioned in commit #{commit.sha}_" } end context 'merge request from an issue' do @@ -249,6 +249,12 @@ describe Note do its(:note) { should == "_mentioned in merge request !#{mergereq.iid}_" } end + context 'commit contained in a merge request' do + subject { Note.create_cross_reference_note(mergereq.commits.first, mergereq, author, project) } + + it { should be_nil } + end + context 'commit from issue' do subject { Note.create_cross_reference_note(commit, issue, author, project) } @@ -267,7 +273,7 @@ describe Note do its(:noteable_type) { should == "Commit" } its(:noteable_id) { should be_nil } its(:commit_id) { should == commit.id } - its(:note) { should == "_mentioned in commit #{parent_commit.id[0...6]}_" } + its(:note) { should == "_mentioned in commit #{parent_commit.id}_" } end end diff --git a/spec/models/project_member_spec.rb b/spec/models/project_member_spec.rb index 0178d065e57..9b5f89b6d7d 100644 --- a/spec/models/project_member_spec.rb +++ b/spec/models/project_member_spec.rb @@ -1,14 +1,16 @@ # == Schema Information # -# Table name: project_members +# Table name: members # # id :integer not null, primary key +# access_level :integer not null +# source_id :integer not null +# source_type :string(255) not null # user_id :integer not null -# project_id :integer not null +# notification_level :integer not null +# type :string(255) # created_at :datetime # updated_at :datetime -# project_access :integer default(0), not null -# notification_level :integer default(3), not null # require 'spec_helper' diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb index e4df934460b..a6e1d9eef50 100644 --- a/spec/models/project_snippet_spec.rb +++ b/spec/models/project_snippet_spec.rb @@ -2,17 +2,17 @@ # # Table name: snippets # -# id :integer not null, primary key -# title :string(255) -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string(255) -# expires_at :datetime -# private :boolean default(TRUE), not null -# type :string(255) +# id :integer not null, primary key +# title :string(255) +# content :text +# author_id :integer not null +# project_id :integer +# created_at :datetime +# updated_at :datetime +# file_name :string(255) +# expires_at :datetime +# type :string(255) +# visibility_level :integer default(0), not null # require 'spec_helper' diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 48b58400a1e..70a15cac1a8 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -145,63 +145,6 @@ describe Project do end end - describe 'comment merge requests with commits' do - before do - @user = create(:user) - group = create(:group) - group.add_owner(@user) - - @project = create(:project, namespace: group) - @fork_project = Projects::ForkService.new(@project, @user).execute - @merge_request = create(:merge_request, source_project: @project, - source_branch: 'master', - target_branch: 'feature', - target_project: @project) - @fork_merge_request = create(:merge_request, source_project: @fork_project, - source_branch: 'master', - target_branch: 'feature', - target_project: @project) - - @commits = @merge_request.commits - end - - context 'push to origin repo source branch' do - before do - @project.comment_mr_with_commits('master', @commits, @user) - end - - it { @merge_request.notes.should_not be_empty } - it { @fork_merge_request.notes.should be_empty } - end - - context 'push to origin repo target branch' do - before do - @project.comment_mr_with_commits('feature', @commits, @user) - end - - it { @merge_request.notes.should be_empty } - it { @fork_merge_request.notes.should be_empty } - end - - context 'push to fork repo source branch' do - before do - @fork_project.comment_mr_with_commits('master', @commits, @user) - end - - it { @merge_request.notes.should be_empty } - it { @fork_merge_request.notes.should_not be_empty } - end - - context 'push to fork repo target branch' do - before do - @fork_project.comment_mr_with_commits('feature', @commits, @user) - end - - it { @merge_request.notes.should be_empty } - it { @fork_merge_request.notes.should be_empty } - end - end - describe :find_with_namespace do context 'with namespace' do before do diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 34c1a686c96..bbf50b654f4 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -27,6 +27,8 @@ describe ProjectTeam do it { project.team.master?(guest).should be_false } it { project.team.master?(reporter).should be_false } it { project.team.master?(nonmember).should be_false } + it { project.team.member?(nonmember).should be_false } + it { project.team.member?(guest).should be_true } end end @@ -60,6 +62,8 @@ describe ProjectTeam do it { project.team.master?(guest).should be_true } it { project.team.master?(reporter).should be_false } it { project.team.master?(nonmember).should be_false } + it { project.team.member?(nonmember).should be_false } + it { project.team.member?(guest).should be_true } end end end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 480aeabf67f..c96f2b20529 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require 'spec_helper' diff --git a/spec/models/slack_message_spec.rb b/spec/models/slack_message_spec.rb index 1cd58534702..c530fad619b 100644 --- a/spec/models/slack_message_spec.rb +++ b/spec/models/slack_message_spec.rb @@ -1,4 +1,4 @@ -require_relative '../../app/models/project_services/slack_message' +require 'spec_helper' describe SlackMessage do subject { SlackMessage.new(args) } @@ -26,11 +26,11 @@ describe SlackMessage do it 'returns a message regarding pushes' do subject.pretext.should == - 'user_name pushed to branch <url/commits/master|master> of ' << + 'user_name pushed to branch <url/commits/master|master> of '\ '<url|project_name> (<url/compare/before...after|Compare changes>)' subject.attachments.should == [ { - text: "<url1|abcdefghi>: message1 - author1\n" << + text: "<url1|abcdefghi>: message1 - author1\n"\ "<url2|123456789>: message2 - author2", color: color, } @@ -45,7 +45,7 @@ describe SlackMessage do it 'returns a message regarding a new branch' do subject.pretext.should == - 'user_name pushed new branch <url/commits/master|master> to ' << + 'user_name pushed new branch <url/commits/master|master> to '\ '<url|project_name>' subject.attachments.should be_empty end diff --git a/spec/models/slack_service_spec.rb b/spec/models/slack_service_spec.rb index 3e555193b32..d4840391967 100644 --- a/spec/models/slack_service_spec.rb +++ b/spec/models/slack_service_spec.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require 'spec_helper' @@ -31,51 +31,27 @@ describe SlackService do end describe "Execute" do - let(:slack) { SlackService.new } - let(:slack_service) { SlackService.new } - let(:user) { create(:user) } + let(:slack) { SlackService.new } + let(:user) { create(:user) } let(:project) { create(:project) } let(:sample_data) { GitPushService.new.sample_data(project, user) } - let(:webhook) { 'https://gitlabhq.slack.com/services/hooks?token=cdIj4r4LfXUOySDUjp0tk3OI' } - let(:new_webhook) { 'https://hooks.gitlabhq.slack.com/services/cdIj4r4LfXUOySDUjp0tk3OI' } - let(:api_url) { - 'https://gitlabhq.slack.com/services/hooks/incoming-webhook?token=cdIj4r4LfXUOySDUjp0tk3OI' - } + let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } before do slack.stub( project: project, project_id: project.id, service_hook: true, - webhook: webhook + webhook: webhook_url ) - WebMock.stub_request(:post, api_url) + WebMock.stub_request(:post, webhook_url) end it "should call Slack API" do slack.execute(sample_data) - WebMock.should have_requested(:post, api_url).once - end - - context 'with new webhook syntax' do - before do - slack_service.stub( - project: project, - project_id: project.id, - service_hook: true, - webhook: new_webhook - ) - - WebMock.stub_request(:post, api_url) - end - - it "should call Slack API" do - slack_service.execute(sample_data) - - WebMock.should have_requested(:post, api_url).once - end + WebMock.should have_requested(:post, webhook_url).once end end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index d179e9516e2..1ef2c512c1f 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -2,17 +2,17 @@ # # Table name: snippets # -# id :integer not null, primary key -# title :string(255) -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string(255) -# expires_at :datetime -# private :boolean default(TRUE), not null -# type :string(255) +# id :integer not null, primary key +# title :string(255) +# content :text +# author_id :integer not null +# project_id :integer +# created_at :datetime +# updated_at :datetime +# file_name :string(255) +# expires_at :datetime +# type :string(255) +# visibility_level :integer default(0), not null # require 'spec_helper' diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0250014bc21..8be7f733a5b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -62,6 +62,7 @@ describe User do it { should have_many(:assigned_issues).dependent(:destroy) } it { should have_many(:merge_requests).dependent(:destroy) } it { should have_many(:assigned_merge_requests).dependent(:destroy) } + it { should have_many(:identities).dependent(:destroy) } end describe "Mass assignment" do @@ -287,6 +288,20 @@ describe User do end end + describe '.by_login' do + let(:username) { 'John' } + let!(:user) { create(:user, username: username) } + + it 'should get the correct user' do + expect(User.by_login(user.email.upcase)).to eq user + expect(User.by_login(user.email)).to eq user + expect(User.by_login(username.downcase)).to eq user + expect(User.by_login(username)).to eq user + expect(User.by_login(nil)).to be_nil + expect(User.by_login('')).to be_nil + end + end + describe 'all_ssh_keys' do it { should have_many(:keys).dependent(:destroy) } @@ -346,6 +361,30 @@ describe User do end end + describe :ldap_user? do + it "is true if provider name starts with ldap" do + user = create(:omniauth_user, provider: 'ldapmain') + expect( user.ldap_user? ).to be_true + end + + it "is false for other providers" do + user = create(:omniauth_user, provider: 'other-provider') + expect( user.ldap_user? ).to be_false + end + + it "is false if no extern_uid is provided" do + user = create(:omniauth_user, extern_uid: nil) + expect( user.ldap_user? ).to be_false + end + end + + describe :ldap_identity do + it "returns ldap identity" do + user = create :omniauth_user + user.ldap_identity.provider.should_not be_empty + end + end + describe '#full_website_url' do let(:user) { create(:user) } @@ -429,4 +468,32 @@ describe User do expect(user.starred?(project)).to be_false end end + + describe "#sort" do + before do + User.delete_all + @user = create :user, created_at: Date.today, last_sign_in_at: Date.today, name: 'Alpha' + @user1 = create :user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, name: 'Omega' + end + + it "sorts users as recently_signed_in" do + User.sort('recent_sign_in').first.should == @user + end + + it "sorts users as late_signed_in" do + User.sort('oldest_sign_in').first.should == @user1 + end + + it "sorts users as recently_created" do + User.sort('recently_created').first.should == @user + end + + it "sorts users as late_created" do + User.sort('late_created').first.should == @user1 + end + + it "sorts users by name when nil is passed" do + User.sort(nil).first.should == @user + end + end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 8834a6cfa83..b45572c39fd 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -146,6 +146,7 @@ describe API::API, api: true do it "should remove branch" do delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user) response.status.should == 200 + json_response['branch_name'].should == branch_name end it 'should return 404 if branch not exists' do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 38e0a284c36..a3f58f50913 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -8,6 +8,7 @@ describe API::API, api: true do let!(:project) { create(:project, creator_id: user.id) } let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } + let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } before { project.team << [user, :reporter] } @@ -81,4 +82,68 @@ describe API::API, api: true do end end end + + describe 'GET /projects:id/repository/commits/:sha/comments' do + context 'authorized user' do + it 'should return merge_request comments' do + get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) + response.status.should == 200 + json_response.should be_an Array + json_response.length.should == 1 + json_response.first['note'].should == 'a comment on a commit' + json_response.first['author']['id'].should == user.id + end + + it 'should return a 404 error if merge_request_id not found' do + get api("/projects/#{project.id}/repository/commits/1234ab/comments", user) + response.status.should == 404 + end + end + + context 'unauthorized user' do + it 'should not return the diff of the selected commit' do + get api("/projects/#{project.id}/repository/commits/1234ab/comments") + response.status.should == 401 + end + end + end + + describe 'POST /projects:id/repository/commits/:sha/comments' do + context 'authorized user' do + it 'should return comment' do + post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment' + response.status.should == 201 + json_response['note'].should == 'My comment' + json_response['path'].should be_nil + json_response['line'].should be_nil + json_response['line_type'].should be_nil + end + + it 'should return the inline comment' do + post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.diffs.first.new_path, line: 7, line_type: 'new' + response.status.should == 201 + json_response['note'].should == 'My comment' + json_response['path'].should == project.repository.commit.diffs.first.new_path + json_response['line'].should == 7 + json_response['line_type'].should == 'new' + end + + it 'should return 400 if note is missing' do + post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) + response.status.should == 400 + end + + it 'should return 404 if note is attached to non existent commit' do + post api("/projects/#{project.id}/repository/commits/1234ab/comments", user), note: 'My comment' + response.status.should == 404 + end + end + + context 'unauthorized user' do + it 'should not return the diff of the selected commit' do + post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments") + response.status.should == 401 + end + end + end end diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb new file mode 100644 index 00000000000..4957186f605 --- /dev/null +++ b/spec/requests/api/group_members_spec.rb @@ -0,0 +1,136 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:owner) { create(:user) } + let(:reporter) { create(:user) } + let(:developer) { create(:user) } + let(:master) { create(:user) } + let(:guest) { create(:user) } + let(:stranger) { create(:user) } + + let!(:group_with_members) do + group = create(:group) + group.add_users([reporter.id], GroupMember::REPORTER) + group.add_users([developer.id], GroupMember::DEVELOPER) + group.add_users([master.id], GroupMember::MASTER) + group.add_users([guest.id], GroupMember::GUEST) + group + end + + let!(:group_no_members) { create(:group) } + + before do + group_with_members.add_owner owner + group_no_members.add_owner owner + end + + describe "GET /groups/:id/members" do + context "when authenticated as user that is part or the group" do + it "each user: should return an array of members groups of group3" do + [owner, master, developer, reporter, guest].each do |user| + get api("/groups/#{group_with_members.id}/members", user) + response.status.should == 200 + json_response.should be_an Array + json_response.size.should == 5 + json_response.find { |e| e['id']==owner.id }['access_level'].should == GroupMember::OWNER + json_response.find { |e| e['id']==reporter.id }['access_level'].should == GroupMember::REPORTER + json_response.find { |e| e['id']==developer.id }['access_level'].should == GroupMember::DEVELOPER + json_response.find { |e| e['id']==master.id }['access_level'].should == GroupMember::MASTER + json_response.find { |e| e['id']==guest.id }['access_level'].should == GroupMember::GUEST + end + end + + it "users not part of the group should get access error" do + get api("/groups/#{group_with_members.id}/members", stranger) + response.status.should == 403 + end + end + end + + describe "POST /groups/:id/members" do + context "when not a member of the group" do + it "should not add guest as member of group_no_members when adding being done by person outside the group" do + post api("/groups/#{group_no_members.id}/members", reporter), user_id: guest.id, access_level: GroupMember::MASTER + response.status.should == 403 + end + end + + context "when a member of the group" do + it "should return ok and add new member" do + new_user = create(:user) + + expect { + post api("/groups/#{group_no_members.id}/members", owner), + user_id: new_user.id, access_level: GroupMember::MASTER + }.to change { group_no_members.members.count }.by(1) + + response.status.should == 201 + json_response['name'].should == new_user.name + json_response['access_level'].should == GroupMember::MASTER + end + + it "should not allow guest to modify group members" do + new_user = create(:user) + + expect { + post api("/groups/#{group_with_members.id}/members", guest), + user_id: new_user.id, access_level: GroupMember::MASTER + }.not_to change { group_with_members.members.count } + + response.status.should == 403 + end + + it "should return error if member already exists" do + post api("/groups/#{group_with_members.id}/members", owner), user_id: master.id, access_level: GroupMember::MASTER + response.status.should == 409 + end + + it "should return a 400 error when user id is not given" do + post api("/groups/#{group_no_members.id}/members", owner), access_level: GroupMember::MASTER + response.status.should == 400 + end + + it "should return a 400 error when access level is not given" do + post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id + response.status.should == 400 + end + + it "should return a 422 error when access level is not known" do + post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234 + response.status.should == 422 + end + end + end + + describe "DELETE /groups/:id/members/:user_id" do + context "when not a member of the group" do + it "should not delete guest's membership of group_with_members" do + random_user = create(:user) + delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user) + response.status.should == 403 + end + end + + context "when a member of the group" do + it "should delete guest's membership of group" do + expect { + delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner) + }.to change { group_with_members.members.count }.by(-1) + + response.status.should == 200 + end + + it "should return a 404 error when user id is not known" do + delete api("/groups/#{group_with_members.id}/members/1328", owner) + response.status.should == 404 + end + + it "should not allow guest to modify group members" do + delete api("/groups/#{group_with_members.id}/members/#{master.id}", guest) + response.status.should == 403 + end + end + end +end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 42ccad71aaf..8dfd2cd650e 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -165,114 +165,4 @@ describe API::API, api: true do end end end - - describe "members" do - let(:owner) { create(:user) } - let(:reporter) { create(:user) } - let(:developer) { create(:user) } - let(:master) { create(:user) } - let(:guest) { create(:user) } - let!(:group_with_members) do - group = create(:group) - group.add_users([reporter.id], GroupMember::REPORTER) - group.add_users([developer.id], GroupMember::DEVELOPER) - group.add_users([master.id], GroupMember::MASTER) - group.add_users([guest.id], GroupMember::GUEST) - group - end - let!(:group_no_members) { create(:group) } - - before do - group_with_members.add_owner owner - group_no_members.add_owner owner - end - - describe "GET /groups/:id/members" do - context "when authenticated as user that is part or the group" do - it "each user: should return an array of members groups of group3" do - [owner, master, developer, reporter, guest].each do |user| - get api("/groups/#{group_with_members.id}/members", user) - response.status.should == 200 - json_response.should be_an Array - json_response.size.should == 5 - json_response.find { |e| e['id']==owner.id }['access_level'].should == GroupMember::OWNER - json_response.find { |e| e['id']==reporter.id }['access_level'].should == GroupMember::REPORTER - json_response.find { |e| e['id']==developer.id }['access_level'].should == GroupMember::DEVELOPER - json_response.find { |e| e['id']==master.id }['access_level'].should == GroupMember::MASTER - json_response.find { |e| e['id']==guest.id }['access_level'].should == GroupMember::GUEST - end - end - - it "users not part of the group should get access error" do - get api("/groups/#{group_with_members.id}/members", user1) - response.status.should == 403 - end - end - end - - describe "POST /groups/:id/members" do - context "when not a member of the group" do - it "should not add guest as member of group_no_members when adding being done by person outside the group" do - post api("/groups/#{group_no_members.id}/members", reporter), user_id: guest.id, access_level: GroupMember::MASTER - response.status.should == 403 - end - end - - context "when a member of the group" do - it "should return ok and add new member" do - count_before=group_no_members.group_members.count - new_user = create(:user) - post api("/groups/#{group_no_members.id}/members", owner), user_id: new_user.id, access_level: GroupMember::MASTER - response.status.should == 201 - json_response['name'].should == new_user.name - json_response['access_level'].should == GroupMember::MASTER - group_no_members.group_members.count.should == count_before + 1 - end - - it "should return error if member already exists" do - post api("/groups/#{group_with_members.id}/members", owner), user_id: master.id, access_level: GroupMember::MASTER - response.status.should == 409 - end - - it "should return a 400 error when user id is not given" do - post api("/groups/#{group_no_members.id}/members", owner), access_level: GroupMember::MASTER - response.status.should == 400 - end - - it "should return a 400 error when access level is not given" do - post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id - response.status.should == 400 - end - - it "should return a 422 error when access level is not known" do - post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234 - response.status.should == 422 - end - end - end - - describe "DELETE /groups/:id/members/:user_id" do - context "when not a member of the group" do - it "should not delete guest's membership of group_with_members" do - random_user = create(:user) - delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user) - response.status.should == 403 - end - end - - context "when a member of the group" do - it "should delete guest's membership of group" do - count_before=group_with_members.group_members.count - delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner) - response.status.should == 200 - group_with_members.group_members.count.should == count_before - 1 - end - - it "should return a 404 error when user id is not known" do - delete api("/groups/#{group_with_members.id}/members/1328", owner) - response.status.should == 404 - end - end - end - end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 6df5ef38961..4faa1f9b964 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -5,10 +5,11 @@ describe API::API, api: true do let(:user) { create(:user) } let(:key) { create(:key, user: user) } let(:project) { create(:project) } + let(:secret_token) { File.read Rails.root.join('.gitlab_shell_secret') } describe "GET /internal/check", no_db: true do it do - get api("/internal/check") + get api("/internal/check"), secret_token: secret_token response.status.should == 200 json_response['api_version'].should == API::API.version @@ -17,7 +18,7 @@ describe API::API, api: true do describe "GET /internal/discover" do it do - get(api("/internal/discover"), key_id: key.id) + get(api("/internal/discover"), key_id: key.id, secret_token: secret_token) response.status.should == 200 @@ -25,7 +26,7 @@ describe API::API, api: true do end end - describe "GET /internal/allowed" do + describe "POST /internal/allowed" do context "access granted" do before do project.team << [user, :developer] @@ -36,7 +37,7 @@ describe API::API, api: true do pull(key, project) response.status.should == 200 - response.body.should == 'true' + JSON.parse(response.body)["status"].should be_true end end @@ -45,7 +46,7 @@ describe API::API, api: true do push(key, project) response.status.should == 200 - response.body.should == 'true' + JSON.parse(response.body)["status"].should be_true end end end @@ -60,7 +61,7 @@ describe API::API, api: true do pull(key, project) response.status.should == 200 - response.body.should == 'false' + JSON.parse(response.body)["status"].should be_false end end @@ -69,7 +70,7 @@ describe API::API, api: true do push(key, project) response.status.should == 200 - response.body.should == 'false' + JSON.parse(response.body)["status"].should be_false end end end @@ -86,7 +87,7 @@ describe API::API, api: true do pull(key, personal_project) response.status.should == 200 - response.body.should == 'false' + JSON.parse(response.body)["status"].should be_false end end @@ -95,7 +96,7 @@ describe API::API, api: true do push(key, personal_project) response.status.should == 200 - response.body.should == 'false' + JSON.parse(response.body)["status"].should be_false end end end @@ -113,7 +114,7 @@ describe API::API, api: true do pull(key, project) response.status.should == 200 - response.body.should == 'true' + JSON.parse(response.body)["status"].should be_true end end @@ -122,7 +123,7 @@ describe API::API, api: true do push(key, project) response.status.should == 200 - response.body.should == 'false' + JSON.parse(response.body)["status"].should be_false end end end @@ -139,7 +140,7 @@ describe API::API, api: true do archive(key, project) response.status.should == 200 - response.body.should == 'true' + JSON.parse(response.body)["status"].should be_true end end @@ -148,10 +149,28 @@ describe API::API, api: true do archive(key, project) response.status.should == 200 - response.body.should == 'false' + JSON.parse(response.body)["status"].should be_false end end end + + context 'project does not exist' do + it do + pull(key, OpenStruct.new(path_with_namespace: 'gitlab/notexists')) + + response.status.should == 200 + JSON.parse(response.body)["status"].should be_false + end + end + + context 'user does not exist' do + it do + pull(OpenStruct.new(id: 0), project) + + response.status.should == 200 + JSON.parse(response.body)["status"].should be_false + end + end end def pull(key, project) @@ -159,7 +178,8 @@ describe API::API, api: true do api("/internal/allowed"), key_id: key.id, project: project.path_with_namespace, - action: 'git-upload-pack' + action: 'git-upload-pack', + secret_token: secret_token ) end @@ -169,7 +189,8 @@ describe API::API, api: true do changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master', key_id: key.id, project: project.path_with_namespace, - action: 'git-receive-pack' + action: 'git-receive-pack', + secret_token: secret_token ) end @@ -179,7 +200,8 @@ describe API::API, api: true do ref: 'master', key_id: key.id, project: project.path_with_namespace, - action: 'git-upload-archive' + action: 'git-upload-archive', + secret_token: secret_token ) end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index aa1437c71aa..2c4b68c10b6 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -203,15 +203,12 @@ describe API::API, api: true do json_response['message']['name'].should == [ 'can\'t be blank', 'is too short (minimum is 0 characters)', - 'can contain only letters, digits, \'_\', \'-\' and \'.\' and '\ - 'space. It must start with letter, digit or \'_\'.' + Gitlab::Regex.project_regex_message ] json_response['message']['path'].should == [ 'can\'t be blank', 'is too short (minimum is 0 characters)', - 'can contain only letters, digits, \'_\', \'-\' and \'.\'. It must '\ - 'start with letter, digit or \'_\', optionally preceeded by \'.\'. '\ - 'It must not end in \'.git\'.' + Gitlab::Regex.send(:default_regex_message) ] end @@ -339,6 +336,7 @@ describe API::API, api: true do json_event['action_name'].should == 'joined' json_event['project_id'].to_i.should == project.id + json_event['author_username'].should == user.username end it "should return a 404 error if not found" do @@ -632,6 +630,11 @@ describe API::API, api: true do describe "DELETE /projects/:id" do context "when authenticated as user" do it "should remove project" do + expect(GitlabShellWorker).to( + receive(:perform_async).with(:remove_repository, + /#{project.path_with_namespace}/) + ).twice + delete api("/projects/#{project.id}", user) response.status.should == 200 end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 6e54839b677..beae71c02d9 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -34,21 +34,23 @@ describe API::API, api: true do end end - # TODO: fix this test for CI - #context 'annotated tag' do - #it 'should create a new annotated tag' do - #post api("/projects/#{project.id}/repository/tags", user), - #tag_name: 'v7.1.0', - #ref: 'master', - #message: 'tag message' - - #response.status.should == 201 - #json_response['name'].should == 'v7.1.0' - # The message is not part of the JSON response. - # Additional changes to the gitlab_git gem may be required. - # json_response['message'].should == 'tag message' - #end - #end + context 'annotated tag' do + it 'should create a new annotated tag' do + # Identity must be set in .gitconfig to create annotated tag. + repo_path = project.repository.path_to_repo + system(*%W(git --git-dir=#{repo_path} config user.name #{user.name})) + system(*%W(git --git-dir=#{repo_path} config user.email #{user.email})) + + post api("/projects/#{project.id}/repository/tags", user), + tag_name: 'v7.1.0', + ref: 'master', + message: 'Release 7.1.0' + + response.status.should == 201 + json_response['name'].should == 'v7.1.0' + json_response['message'].should == 'Release 7.1.0' + end + end it 'should deny for user without push access' do post api("/projects/#{project.id}/repository/tags", user2), diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index f883c9e028a..d8282d0696b 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -27,4 +27,30 @@ describe API::API, api: true do project.gitlab_ci_service.should be_nil end end + + describe 'PUT /projects/:id/services/hipchat' do + it 'should update hipchat settings' do + put api("/projects/#{project.id}/services/hipchat", user), + token: 'secret-token', room: 'test' + + response.status.should == 200 + project.hipchat_service.should_not be_nil + end + + it 'should return if required fields missing' do + put api("/projects/#{project.id}/services/gitlab-ci", user), + token: 'secret-token', active: true + + response.status.should == 400 + end + end + + describe 'DELETE /projects/:id/services/hipchat' do + it 'should delete hipchat settings' do + delete api("/projects/#{project.id}/services/hipchat", user) + + response.status.should == 200 + project.hipchat_service.should be_nil + end + end end diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb index 013f425d6ce..57b2e6cbd6a 100644 --- a/spec/requests/api/session_spec.rb +++ b/spec/requests/api/session_spec.rb @@ -19,6 +19,32 @@ describe API::API, api: true do end end + context 'when email has case-typo and password is valid' do + it 'should return private token' do + post api('/session'), email: user.email.upcase, password: '12345678' + expect(response.status).to eq 201 + + expect(json_response['email']).to eq user.email + expect(json_response['private_token']).to eq user.private_token + expect(json_response['is_admin']).to eq user.is_admin? + expect(json_response['can_create_project']).to eq user.can_create_project? + expect(json_response['can_create_group']).to eq user.can_create_group? + end + end + + context 'when login has case-typo and password is valid' do + it 'should return private token' do + post api('/session'), login: user.username.upcase, password: '12345678' + expect(response.status).to eq 201 + + expect(json_response['email']).to eq user.email + expect(json_response['private_token']).to eq user.private_token + expect(json_response['is_admin']).to eq user.is_admin? + expect(json_response['can_create_project']).to eq user.can_create_project? + expect(json_response['can_create_group']).to eq user.can_create_group? + end + end + context "when invalid password" do it "should return authentication error" do post api("/session"), email: user.email, password: '123' diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index bc1598273be..1ecc79ea7ef 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -33,7 +33,7 @@ describe API::API, api: true do response.status.should == 200 json_response.should be_an Array json_response.first.keys.should include 'email' - json_response.first.keys.should include 'extern_uid' + json_response.first.keys.should include 'identities' json_response.first.keys.should include 'can_create_project' end end @@ -140,9 +140,7 @@ describe API::API, api: true do json_response['message']['projects_limit']. should == ['must be greater than or equal to 0'] json_response['message']['username']. - should == ['can contain only letters, digits, '\ - '\'_\', \'-\' and \'.\'. It must start with letter, digit or '\ - '\'_\', optionally preceeded by \'.\'. It must not end in \'.git\'.'] + should == [Gitlab::Regex.send(:default_regex_message)] end it "shouldn't available for non admin users" do @@ -284,9 +282,7 @@ describe API::API, api: true do json_response['message']['projects_limit']. should == ['must be greater than or equal to 0'] json_response['message']['username']. - should == ['can contain only letters, digits, '\ - '\'_\', \'-\' and \'.\'. It must start with letter, digit or '\ - '\'_\', optionally preceeded by \'.\'. It must not end in \'.git\'.'] + should == [Gitlab::Regex.send(:default_regex_message)] end context "with existing user" do @@ -433,6 +429,7 @@ describe API::API, api: true do json_response['is_admin'].should == user.is_admin? json_response['can_create_project'].should == user.can_create_project? json_response['can_create_group'].should == user.can_create_group? + json_response['projects_limit'].should == user.projects_limit end it "should return 401 error if user is unauthenticated" do diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 8a7d76cc970..f149f3f62a9 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -53,14 +53,14 @@ shared_examples "RESTful project resources" do end end -# projects POST /projects(.:format) projects#create -# new_project GET /projects/new(.:format) projects#new -# fork_project POST /:id/fork(.:format) projects#fork -# files_project GET /:id/files(.:format) projects#files -# edit_project GET /:id/edit(.:format) projects#edit -# project GET /:id(.:format) projects#show -# PUT /:id(.:format) projects#update -# DELETE /:id(.:format) projects#destroy +# projects POST /projects(.:format) projects#create +# new_project GET /projects/new(.:format) projects#new +# files_project GET /:id/files(.:format) projects#files +# edit_project GET /:id/edit(.:format) projects#edit +# project GET /:id(.:format) projects#show +# PUT /:id(.:format) projects#update +# DELETE /:id(.:format) projects#destroy +# markdown_preview_project GET /:id/markdown_preview(.:format) projects#markdown_preview describe ProjectsController, "routing" do it "to #create" do post("/projects").should route_to('projects#create') @@ -70,10 +70,6 @@ describe ProjectsController, "routing" do get("/projects/new").should route_to('projects#new') end - it "to #fork" do - post("/gitlab/gitlabhq/fork").should route_to('projects#fork', id: 'gitlab/gitlabhq') - end - it "to #edit" do get("/gitlab/gitlabhq/edit").should route_to('projects#edit', id: 'gitlab/gitlabhq') end @@ -93,6 +89,12 @@ describe ProjectsController, "routing" do it "to #destroy" do delete("/gitlab/gitlabhq").should route_to('projects#destroy', id: 'gitlab/gitlabhq') end + + it 'to #markdown_preview' do + get('/gitlab/gitlabhq/markdown_preview').should( + route_to('projects#markdown_preview', id: 'gitlab/gitlabhq') + ) + end end # pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages @@ -392,15 +394,10 @@ describe Projects::IssuesController, "routing" do end end -# preview_project_notes POST /:project_id/notes/preview(.:format) notes#preview # project_notes GET /:project_id/notes(.:format) notes#index # POST /:project_id/notes(.:format) notes#create # project_note DELETE /:project_id/notes/:id(.:format) notes#destroy describe Projects::NotesController, "routing" do - it "to #preview" do - post("/gitlab/gitlabhq/notes/preview").should route_to('projects/notes#preview', project_id: 'gitlab/gitlabhq') - end - it_behaves_like "RESTful project resources" do let(:actions) { [:index, :create, :destroy] } let(:controller) { 'notes' } @@ -420,6 +417,7 @@ describe Projects::BlobController, "routing" do it "to #show" do get("/gitlab/gitlabhq/blob/master/app/models/project.rb").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') get("/gitlab/gitlabhq/blob/master/app/models/compare.rb").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/compare.rb') + get("/gitlab/gitlabhq/blob/master/app/models/diff.js").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/diff.js') get("/gitlab/gitlabhq/blob/master/files.scss").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') end end @@ -482,3 +480,13 @@ describe Projects::GraphsController, "routing" do get("/gitlab/gitlabhq/graphs/master").should route_to('projects/graphs#show', project_id: 'gitlab/gitlabhq', id: 'master') end end + +describe Projects::ForksController, "routing" do + it "to #new" do + get("/gitlab/gitlabhq/fork/new").should route_to("projects/forks#new", project_id: 'gitlab/gitlabhq') + end + + it "to #create" do + post("/gitlab/gitlabhq/fork").should route_to("projects/forks#create", project_id: 'gitlab/gitlabhq') + end +end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 4ef053a767f..19b442573f4 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -8,7 +8,7 @@ describe GitPushService do let (:service) { GitPushService.new } before do - @blankrev = '0000000000000000000000000000000000000000' + @blankrev = Gitlab::Git::BLANK_SHA @oldrev = sample_commit.parent_id @newrev = sample_commit.id @ref = 'refs/heads/master' diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb new file mode 100644 index 00000000000..9f294152053 --- /dev/null +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' + +describe MergeRequests::RefreshService do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:service) { MergeRequests::RefreshService } + + describe :execute do + before do + @user = create(:user) + group = create(:group) + group.add_owner(@user) + + @project = create(:project, namespace: group) + @fork_project = Projects::ForkService.new(@project, @user).execute + @merge_request = create(:merge_request, source_project: @project, + source_branch: 'master', + target_branch: 'feature', + target_project: @project) + + @fork_merge_request = create(:merge_request, source_project: @fork_project, + source_branch: 'master', + target_branch: 'feature', + target_project: @project) + + @commits = @merge_request.commits + + @oldrev = @commits.last.id + @newrev = @commits.first.id + end + + context 'push to origin repo source branch' do + before do + service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') + reload_mrs + end + + it { @merge_request.notes.should_not be_empty } + it { @merge_request.should be_open } + it { @fork_merge_request.should be_open } + it { @fork_merge_request.notes.should be_empty } + end + + context 'push to origin repo target branch' do + before do + service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + reload_mrs + end + + it { @merge_request.notes.should be_empty } + it { @merge_request.should be_merged } + it { @fork_merge_request.should be_merged } + it { @fork_merge_request.notes.should be_empty } + end + + context 'push to fork repo source branch' do + before do + service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/master') + reload_mrs + end + + it { @merge_request.notes.should be_empty } + it { @merge_request.should be_open } + it { @fork_merge_request.notes.should_not be_empty } + it { @fork_merge_request.should be_open } + end + + context 'push to fork repo target branch' do + before do + service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + reload_mrs + end + + it { @merge_request.notes.should be_empty } + it { @merge_request.should be_open } + it { @fork_merge_request.notes.should be_empty } + it { @fork_merge_request.should be_open } + end + + context 'push to origin repo target branch after fork project was removed' do + before do + @fork_project.destroy + service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + reload_mrs + end + + it { @merge_request.notes.should be_empty } + it { @merge_request.should be_merged } + it { @fork_merge_request.should be_open } + it { @fork_merge_request.notes.should be_empty } + end + + def reload_mrs + @merge_request.reload + @fork_merge_request.reload + end + end +end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 0edc3a8e807..5c80345c2b3 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -42,10 +42,54 @@ describe Projects::ForkService do end end - def fork_project(from_project, user, fork_success = true) - context = Projects::ForkService.new(from_project, user) - shell = double("gitlab_shell") - shell.stub(fork_repository: fork_success) + describe :fork_to_namespace do + before do + @group_owner = create(:user) + @developer = create(:user) + @project = create(:project, creator_id: @group_owner.id, + star_count: 777, + description: 'Wow, such a cool project!') + @group = create(:group) + @group.add_user(@group_owner, GroupMember::OWNER) + @group.add_user(@developer, GroupMember::DEVELOPER) + @opts = { namespace: @group } + end + + context 'fork project for group' do + it 'group owner successfully forks project into the group' do + to_project = fork_project(@project, @group_owner, true, @opts) + to_project.owner.should == @group + to_project.namespace.should == @group + to_project.name.should == @project.name + to_project.path.should == @project.path + to_project.description.should == @project.description + to_project.star_count.should be_zero + end + end + + context 'fork project for group when user not owner' do + it 'group developer should fail to fork project into the group' do + to_project = fork_project(@project, @developer, true, @opts) + to_project.errors[:namespace].should == ['insufficient access rights'] + end + end + + context 'project already exists in group' do + it 'should fail due to validation, not transaction failure' do + existing_project = create(:project, name: @project.name, + namespace: @group) + to_project = fork_project(@project, @group_owner, true, @opts) + existing_project.persisted?.should be_true + to_project.errors[:base].should == ['Invalid fork destination'] + to_project.errors[:name].should == ['has already been taken'] + to_project.errors[:path].should == ['has already been taken'] + end + end + end + + def fork_project(from_project, user, fork_success = true, params = {}) + context = Projects::ForkService.new(from_project, user, params) + shell = double('gitlab_shell').stub(fork_repository: fork_success) context.stub(gitlab_shell: shell) context.execute end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 2508dfc4565..79d0526ff89 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -3,15 +3,12 @@ require 'spec_helper' describe Projects::TransferService do let(:user) { create(:user) } let(:group) { create(:group) } - let(:group2) { create(:group) } let(:project) { create(:project, namespace: user.namespace) } context 'namespace -> namespace' do before do group.add_owner(user) - @service = Projects::TransferService.new(project, user, namespace_id: group.id) - @service.gitlab_shell.stub(mv_repository: true) - @result = @service.execute + @result = transfer_project(project, user, namespace_id: group.id) end it { @result.should be_true } @@ -20,24 +17,25 @@ describe Projects::TransferService do context 'namespace -> no namespace' do before do - group.add_owner(user) - @service = Projects::TransferService.new(project, user, namespace_id: nil) - @service.gitlab_shell.stub(mv_repository: true) - @result = @service.execute + @result = transfer_project(project, user, namespace_id: nil) end + it { @result.should_not be_nil } # { result.should be_false } passes on nil it { @result.should be_false } it { project.namespace.should == user.namespace } end context 'namespace -> not allowed namespace' do before do - @service = Projects::TransferService.new(project, user, namespace_id: group2.id) - @service.gitlab_shell.stub(mv_repository: true) - @result = @service.execute + @result = transfer_project(project, user, namespace_id: group.id) end + it { @result.should_not be_nil } # { result.should be_false } passes on nil it { @result.should be_false } it { project.namespace.should == user.namespace } end + + def transfer_project(project, user, params) + Projects::TransferService.new(project, user, params).execute + end end diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index 692834c9f29..ebd74206699 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -30,15 +30,15 @@ def common_mentionable_setup "!#{mentioned_mr.iid}, " + "#{ext_proj.path_with_namespace}##{ext_issue.iid}, " + "#{ext_proj.path_with_namespace}!#{ext_mr.iid}, " + - "#{ext_proj.path_with_namespace}@#{ext_commit.id[0..5]}, " + - "#{mentioned_commit.sha[0..5]} and itself as #{backref_text}" + "#{ext_proj.path_with_namespace}@#{ext_commit.short_id}, " + + "#{mentioned_commit.sha[0..10]} and itself as #{backref_text}" end before do # Wire the project's repository to return the mentioned commit, and +nil+ for any # unrecognized commits. - commitmap = { '123456' => mentioned_commit } - extra_commits.each { |c| commitmap[c.sha[0..5]] = c } + commitmap = { '1234567890a' => mentioned_commit } + extra_commits.each { |c| commitmap[c.short_id] = c } mproject.repository.stub(:commit) { |sha| commitmap[sha] } set_mentionable_text.call(ref_string) end @@ -54,7 +54,6 @@ shared_examples 'a mentionable' do it "extracts references from its reference property" do # De-duplicate and omit itself refs = subject.references(mproject) - refs.should have(6).items refs.should include(mentioned_issue) refs.should include(mentioned_mr) @@ -90,7 +89,7 @@ shared_examples 'an editable mentionable' do it 'creates new cross-reference notes when the mentionable text is edited' do new_text = "still mentions ##{mentioned_issue.iid}, " + - "#{mentioned_commit.sha[0..5]}, " + + "#{mentioned_commit.sha[0..10]}, " + "#{ext_issue.iid}, " + "new refs: ##{other_issue.iid}, " + "#{ext_proj.path_with_namespace}##{other_ext_issue.iid}" diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 5f55871dc4a..e6db410fb1c 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -3,6 +3,16 @@ require 'rspec/mocks' module TestEnv extend self + # When developing the seed repository, comment out the branch you will modify. + BRANCH_SHA = { + 'feature' => '0b4bc9a', + 'feature_conflict' => 'bb5206f', + 'fix' => '12d65c8', + 'improve/awesome' => '5937ac0', + 'markdown' => '0ed8c6c', + 'master' => '5937ac0' + } + # Test environment # # See gitlab.yml.example test section for paths @@ -18,13 +28,13 @@ module TestEnv if File.directory?(tmp_test_path) Dir.entries(tmp_test_path).each do |entry| - unless ['.', '..', 'gitlab-shell'].include?(entry) + unless ['.', '..', 'gitlab-shell', factory_repo_name].include?(entry) FileUtils.rm_r(File.join(tmp_test_path, entry)) end end end - FileUtils.mkdir_p(tmp_test_path) + FileUtils.mkdir_p(repos_path) # Setup GitLab shell for test instance setup_gitlab_shell @@ -49,13 +59,32 @@ module TestEnv clone_url = "https://gitlab.com/gitlab-org/#{factory_repo_name}.git" unless File.directory?(factory_repo_path) - git_cmd = %W(git clone --bare #{clone_url} #{factory_repo_path}) - system(*git_cmd) + system(*%W(git clone #{clone_url} #{factory_repo_path})) + end + + Dir.chdir(factory_repo_path) do + BRANCH_SHA.each do |branch, sha| + # Try to reset without fetching to avoid using the network. + reset = %W(git update-ref refs/heads/#{branch} #{sha}) + unless system(*reset) + if system(*%w(git fetch origin)) + unless system(*reset) + raise 'The fetched test seed '\ + 'does not contain the required revision.' + end + else + raise 'Could not fetch test seed repository.' + end + end + end end + + # We must copy bare repositories because we will push to them. + system(*%W(git clone --bare #{factory_repo_path} #{factory_repo_path_bare})) end def copy_repo(project) - base_repo_path = File.expand_path(factory_repo_path) + base_repo_path = File.expand_path(factory_repo_path_bare) target_repo_path = File.expand_path(repos_path + "/#{project.namespace.path}/#{project.path}.git") FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) @@ -69,7 +98,11 @@ module TestEnv private def factory_repo_path - @factory_repo_path ||= repos_path + "/root/#{factory_repo_name}.git" + @factory_repo_path ||= Rails.root.join('tmp', 'tests', factory_repo_name) + end + + def factory_repo_path_bare + factory_repo_path.to_s + '_bare' end def factory_repo_name diff --git a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb new file mode 100644 index 00000000000..45aaf0fc90b --- /dev/null +++ b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb @@ -0,0 +1,27 @@ +require 'spec_helper' +require 'rake' + +describe 'gitlab:mail_google_schema_whitelisting rake task' do + before :all do + Rake.application.rake_require "tasks/gitlab/task_helpers" + Rake.application.rake_require "tasks/gitlab/mail_google_schema_whitelisting" + # empty task as env is already loaded + Rake::Task.define_task :environment + end + + describe 'call' do + before do + # avoid writing task output to spec progress + $stdout.stub :write + end + + let :run_rake_task do + Rake::Task["gitlab:mail_google_schema_whitelisting"].reenable + Rake.application.invoke_task "gitlab:mail_google_schema_whitelisting" + end + + it 'should run the task without errors' do + expect { run_rake_task }.to_not raise_error + end + end +end diff --git a/vendor/assets/javascripts/pwstrength-bootstrap-1.2.2.js b/vendor/assets/javascripts/pwstrength-bootstrap-1.2.2.js new file mode 100644 index 00000000000..ee374a07fab --- /dev/null +++ b/vendor/assets/javascripts/pwstrength-bootstrap-1.2.2.js @@ -0,0 +1,659 @@ +/*! + * jQuery Password Strength plugin for Twitter Bootstrap + * + * Copyright (c) 2008-2013 Tane Piper + * Copyright (c) 2013 Alejandro Blanco + * Dual licensed under the MIT and GPL licenses. + */ + +(function (jQuery) { +// Source: src/rules.js + + var rulesEngine = {}; + + try { + if (!jQuery && module && module.exports) { + var jQuery = require("jquery"), + jsdom = require("jsdom").jsdom; + jQuery = jQuery(jsdom().parentWindow); + } + } catch (ignore) {} + + (function ($, rulesEngine) { + "use strict"; + var validation = {}; + + rulesEngine.forbiddenSequences = [ + "0123456789", "abcdefghijklmnopqrstuvwxyz", "qwertyuiop", "asdfghjkl", + "zxcvbnm", "!@#$%^&*()_+" + ]; + + validation.wordNotEmail = function (options, word, score) { + if (word.match(/^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$/i)) { + return score; + } + return 0; + }; + + validation.wordLength = function (options, word, score) { + var wordlen = word.length, + lenScore = Math.pow(wordlen, options.rules.raisePower); + if (wordlen < options.common.minChar) { + lenScore = (lenScore + score); + } + return lenScore; + }; + + validation.wordSimilarToUsername = function (options, word, score) { + var username = $(options.common.usernameField).val(); + if (username && word.toLowerCase().match(username.toLowerCase())) { + return score; + } + return 0; + }; + + validation.wordTwoCharacterClasses = function (options, word, score) { + if (word.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/) || + (word.match(/([a-zA-Z])/) && word.match(/([0-9])/)) || + (word.match(/(.[!,@,#,$,%,\^,&,*,?,_,~])/) && word.match(/[a-zA-Z0-9_]/))) { + return score; + } + return 0; + }; + + validation.wordRepetitions = function (options, word, score) { + if (word.match(/(.)\1\1/)) { return score; } + return 0; + }; + + validation.wordSequences = function (options, word, score) { + var found = false, + j; + if (word.length > 2) { + $.each(rulesEngine.forbiddenSequences, function (idx, seq) { + var sequences = [seq, seq.split('').reverse().join('')]; + $.each(sequences, function (idx, sequence) { + for (j = 0; j < (word.length - 2); j += 1) { // iterate the word trough a sliding window of size 3: + if (sequence.indexOf(word.toLowerCase().substring(j, j + 3)) > -1) { + found = true; + } + } + }); + }); + if (found) { return score; } + } + return 0; + }; + + validation.wordLowercase = function (options, word, score) { + return word.match(/[a-z]/) && score; + }; + + validation.wordUppercase = function (options, word, score) { + return word.match(/[A-Z]/) && score; + }; + + validation.wordOneNumber = function (options, word, score) { + return word.match(/\d+/) && score; + }; + + validation.wordThreeNumbers = function (options, word, score) { + return word.match(/(.*[0-9].*[0-9].*[0-9])/) && score; + }; + + validation.wordOneSpecialChar = function (options, word, score) { + return word.match(/.[!,@,#,$,%,\^,&,*,?,_,~]/) && score; + }; + + validation.wordTwoSpecialChar = function (options, word, score) { + return word.match(/(.*[!,@,#,$,%,\^,&,*,?,_,~].*[!,@,#,$,%,\^,&,*,?,_,~])/) && score; + }; + + validation.wordUpperLowerCombo = function (options, word, score) { + return word.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/) && score; + }; + + validation.wordLetterNumberCombo = function (options, word, score) { + return word.match(/([a-zA-Z])/) && word.match(/([0-9])/) && score; + }; + + validation.wordLetterNumberCharCombo = function (options, word, score) { + return word.match(/([a-zA-Z0-9].*[!,@,#,$,%,\^,&,*,?,_,~])|([!,@,#,$,%,\^,&,*,?,_,~].*[a-zA-Z0-9])/) && score; + }; + + rulesEngine.validation = validation; + + rulesEngine.executeRules = function (options, word) { + var totalScore = 0; + + $.each(options.rules.activated, function (rule, active) { + if (active) { + var score = options.rules.scores[rule], + funct = rulesEngine.validation[rule], + result, + errorMessage; + + if (!$.isFunction(funct)) { + funct = options.rules.extra[rule]; + } + + if ($.isFunction(funct)) { + result = funct(options, word, score); + if (result) { + totalScore += result; + } + if (result < 0 || (!$.isNumeric(result) && !result)) { + errorMessage = options.ui.spanError(options, rule); + if (errorMessage.length > 0) { + options.instances.errors.push(errorMessage); + } + } + } + } + }); + + return totalScore; + }; + }(jQuery, rulesEngine)); + + try { + if (module && module.exports) { + module.exports = rulesEngine; + } + } catch (ignore) {} + +// Source: src/options.js + + + + + var defaultOptions = {}; + + defaultOptions.common = {}; + defaultOptions.common.minChar = 6; + defaultOptions.common.usernameField = "#username"; + defaultOptions.common.userInputs = [ + // Selectors for input fields with user input + ]; + defaultOptions.common.onLoad = undefined; + defaultOptions.common.onKeyUp = undefined; + defaultOptions.common.zxcvbn = false; + defaultOptions.common.debug = false; + + defaultOptions.rules = {}; + defaultOptions.rules.extra = {}; + defaultOptions.rules.scores = { + wordNotEmail: -100, + wordLength: -50, + wordSimilarToUsername: -100, + wordSequences: -50, + wordTwoCharacterClasses: 2, + wordRepetitions: -25, + wordLowercase: 1, + wordUppercase: 3, + wordOneNumber: 3, + wordThreeNumbers: 5, + wordOneSpecialChar: 3, + wordTwoSpecialChar: 5, + wordUpperLowerCombo: 2, + wordLetterNumberCombo: 2, + wordLetterNumberCharCombo: 2 + }; + defaultOptions.rules.activated = { + wordNotEmail: true, + wordLength: true, + wordSimilarToUsername: true, + wordSequences: true, + wordTwoCharacterClasses: false, + wordRepetitions: false, + wordLowercase: true, + wordUppercase: true, + wordOneNumber: true, + wordThreeNumbers: true, + wordOneSpecialChar: true, + wordTwoSpecialChar: true, + wordUpperLowerCombo: true, + wordLetterNumberCombo: true, + wordLetterNumberCharCombo: true + }; + defaultOptions.rules.raisePower = 1.4; + + defaultOptions.ui = {}; + defaultOptions.ui.bootstrap2 = false; + defaultOptions.ui.showProgressBar = true; + defaultOptions.ui.showPopover = false; + defaultOptions.ui.showStatus = false; + defaultOptions.ui.spanError = function (options, key) { + "use strict"; + var text = options.ui.errorMessages[key]; + if (!text) { return ''; } + return '<span style="color: #d52929">' + text + '</span>'; + }; + defaultOptions.ui.errorMessages = { + wordLength: "Your password is too short", + wordNotEmail: "Do not use your email as your password", + wordSimilarToUsername: "Your password cannot contain your username", + wordTwoCharacterClasses: "Use different character classes", + wordRepetitions: "Too many repetitions", + wordSequences: "Your password contains sequences" + }; + defaultOptions.ui.verdicts = ["Weak", "Normal", "Medium", "Strong", "Very Strong"]; + defaultOptions.ui.showVerdicts = true; + defaultOptions.ui.showVerdictsInsideProgressBar = false; + defaultOptions.ui.showErrors = false; + defaultOptions.ui.container = undefined; + defaultOptions.ui.viewports = { + progress: undefined, + verdict: undefined, + errors: undefined + }; + defaultOptions.ui.scores = [14, 26, 38, 50]; + +// Source: src/ui.js + + + + + var ui = {}; + + (function ($, ui) { + "use strict"; + + var barClasses = ["danger", "warning", "success"], + statusClasses = ["error", "warning", "success"]; + + ui.getContainer = function (options, $el) { + var $container; + + $container = $(options.ui.container); + if (!($container && $container.length === 1)) { + $container = $el.parent(); + } + return $container; + }; + + ui.findElement = function ($container, viewport, cssSelector) { + if (viewport) { + return $container.find(viewport).find(cssSelector); + } + return $container.find(cssSelector); + }; + + ui.getUIElements = function (options, $el) { + var $container, result; + + if (options.instances.viewports) { + return options.instances.viewports; + } + + $container = ui.getContainer(options, $el); + + result = {}; + result.$progressbar = ui.findElement($container, options.ui.viewports.progress, "div.progress"); + if (options.ui.showVerdictsInsideProgressBar) { + result.$verdict = result.$progressbar.find("span.password-verdict"); + } + + if (!options.ui.showPopover) { + if (!options.ui.showVerdictsInsideProgressBar) { + result.$verdict = ui.findElement($container, options.ui.viewports.verdict, "span.password-verdict"); + } + result.$errors = ui.findElement($container, options.ui.viewports.errors, "ul.error-list"); + } + + options.instances.viewports = result; + return result; + }; + + ui.initProgressBar = function (options, $el) { + var $container = ui.getContainer(options, $el), + progressbar = "<div class='progress'><div class='"; + + if (!options.ui.bootstrap2) { + progressbar += "progress-"; + } + progressbar += "bar'>"; + if (options.ui.showVerdictsInsideProgressBar) { + progressbar += "<span class='password-verdict'></span>"; + } + progressbar += "</div></div>"; + + if (options.ui.viewports.progress) { + $container.find(options.ui.viewports.progress).append(progressbar); + } else { + $(progressbar).insertAfter($el); + } + }; + + ui.initHelper = function (options, $el, html, viewport) { + var $container = ui.getContainer(options, $el); + if (viewport) { + $container.find(viewport).append(html); + } else { + $(html).insertAfter($el); + } + }; + + ui.initVerdict = function (options, $el) { + ui.initHelper(options, $el, "<span class='password-verdict'></span>", + options.ui.viewports.verdict); + }; + + ui.initErrorList = function (options, $el) { + ui.initHelper(options, $el, "<ul class='error-list'></ul>", + options.ui.viewports.errors); + }; + + ui.initPopover = function (options, $el) { + $el.popover("destroy"); + $el.popover({ + html: true, + placement: "top", + trigger: "manual", + content: " " + }); + }; + + ui.initUI = function (options, $el) { + if (options.ui.showPopover) { + ui.initPopover(options, $el); + } else { + if (options.ui.showErrors) { ui.initErrorList(options, $el); } + if (options.ui.showVerdicts && !options.ui.showVerdictsInsideProgressBar) { + ui.initVerdict(options, $el); + } + } + if (options.ui.showProgressBar) { + ui.initProgressBar(options, $el); + } + }; + + ui.possibleProgressBarClasses = ["danger", "warning", "success"]; + + ui.updateProgressBar = function (options, $el, cssClass, percentage) { + var $progressbar = ui.getUIElements(options, $el).$progressbar, + $bar = $progressbar.find(".progress-bar"), + cssPrefix = "progress-"; + + if (options.ui.bootstrap2) { + $bar = $progressbar.find(".bar"); + cssPrefix = ""; + } + + $.each(ui.possibleProgressBarClasses, function (idx, value) { + $bar.removeClass(cssPrefix + "bar-" + value); + }); + $bar.addClass(cssPrefix + "bar-" + barClasses[cssClass]); + $bar.css("width", percentage + '%'); + }; + + ui.updateVerdict = function (options, $el, text) { + var $verdict = ui.getUIElements(options, $el).$verdict; + $verdict.text(text); + }; + + ui.updateErrors = function (options, $el) { + var $errors = ui.getUIElements(options, $el).$errors, + html = ""; + $.each(options.instances.errors, function (idx, err) { + html += "<li>" + err + "</li>"; + }); + $errors.html(html); + }; + + ui.updatePopover = function (options, $el, verdictText) { + var popover = $el.data("bs.popover"), + html = "", + hide = true; + + if (options.ui.showVerdicts && + !options.ui.showVerdictsInsideProgressBar && + verdictText.length > 0) { + html = "<h5><span class='password-verdict'>" + verdictText + + "</span></h5>"; + hide = false; + } + if (options.ui.showErrors) { + html += "<div><ul class='error-list' style='margin-bottom: 0; margin-left: -20px'>"; + $.each(options.instances.errors, function (idx, err) { + html += "<li>" + err + "</li>"; + hide = false; + }); + html += "</ul></div>"; + } + + if (hide) { + $el.popover("hide"); + return; + } + + if (options.ui.bootstrap2) { popover = $el.data("popover"); } + + if (popover.$arrow && popover.$arrow.parents("body").length > 0) { + $el.find("+ .popover .popover-content").html(html); + } else { + // It's hidden + popover.options.content = html; + $el.popover("show"); + } + }; + + ui.updateFieldStatus = function (options, $el, cssClass) { + var targetClass = options.ui.bootstrap2 ? ".control-group" : ".form-group", + $container = $el.parents(targetClass).first(); + + $.each(statusClasses, function (idx, css) { + if (!options.ui.bootstrap2) { css = "has-" + css; } + $container.removeClass(css); + }); + + cssClass = statusClasses[cssClass]; + if (!options.ui.bootstrap2) { cssClass = "has-" + cssClass; } + $container.addClass(cssClass); + }; + + ui.percentage = function (score, maximun) { + var result = Math.floor(100 * score / maximun); + result = result < 0 ? 0 : result; + result = result > 100 ? 100 : result; + return result; + }; + + ui.getVerdictAndCssClass = function (options, score) { + var cssClass, verdictText, level; + + if (score <= 0) { + cssClass = 0; + level = -1; + verdictText = options.ui.verdicts[0]; + } else if (score < options.ui.scores[0]) { + cssClass = 0; + level = 0; + verdictText = options.ui.verdicts[0]; + } else if (score < options.ui.scores[1]) { + cssClass = 0; + level = 1; + verdictText = options.ui.verdicts[1]; + } else if (score < options.ui.scores[2]) { + cssClass = 1; + level = 2; + verdictText = options.ui.verdicts[2]; + } else if (score < options.ui.scores[3]) { + cssClass = 1; + level = 3; + verdictText = options.ui.verdicts[3]; + } else { + cssClass = 2; + level = 4; + verdictText = options.ui.verdicts[4]; + } + + return [verdictText, cssClass, level]; + }; + + ui.updateUI = function (options, $el, score) { + var cssClass, barPercentage, verdictText; + + cssClass = ui.getVerdictAndCssClass(options, score); + verdictText = cssClass[0]; + cssClass = cssClass[1]; + + if (options.ui.showProgressBar) { + barPercentage = ui.percentage(score, options.ui.scores[3]); + ui.updateProgressBar(options, $el, cssClass, barPercentage); + if (options.ui.showVerdictsInsideProgressBar) { + ui.updateVerdict(options, $el, verdictText); + } + } + + if (options.ui.showStatus) { + ui.updateFieldStatus(options, $el, cssClass); + } + + if (options.ui.showPopover) { + ui.updatePopover(options, $el, verdictText); + } else { + if (options.ui.showVerdicts && !options.ui.showVerdictsInsideProgressBar) { + ui.updateVerdict(options, $el, verdictText); + } + if (options.ui.showErrors) { + ui.updateErrors(options, $el); + } + } + }; + }(jQuery, ui)); + +// Source: src/methods.js + + + + + var methods = {}; + + (function ($, methods) { + "use strict"; + var onKeyUp, applyToAll; + + onKeyUp = function (event) { + var $el = $(event.target), + options = $el.data("pwstrength-bootstrap"), + word = $el.val(), + userInputs, + verdictText, + verdictLevel, + score; + + if (options === undefined) { return; } + + options.instances.errors = []; + if (options.common.zxcvbn) { + userInputs = []; + $.each(options.common.userInputs, function (idx, selector) { + userInputs.push($(selector).val()); + }); + userInputs.push($(options.common.usernameField).val()); + score = zxcvbn(word, userInputs).entropy; + } else { + score = rulesEngine.executeRules(options, word); + } + ui.updateUI(options, $el, score); + verdictText = ui.getVerdictAndCssClass(options, score); + verdictLevel = verdictText[2]; + verdictText = verdictText[0]; + + if (options.common.debug) { console.log(score + ' - ' + verdictText); } + + if ($.isFunction(options.common.onKeyUp)) { + options.common.onKeyUp(event, { + score: score, + verdictText: verdictText, + verdictLevel: verdictLevel + }); + } + }; + + methods.init = function (settings) { + this.each(function (idx, el) { + // Make it deep extend (first param) so it extends too the + // rules and other inside objects + var clonedDefaults = $.extend(true, {}, defaultOptions), + localOptions = $.extend(true, clonedDefaults, settings), + $el = $(el); + + localOptions.instances = {}; + $el.data("pwstrength-bootstrap", localOptions); + $el.on("keyup", onKeyUp); + $el.on("change", onKeyUp); + $el.on("onpaste", onKeyUp); + + ui.initUI(localOptions, $el); + if ($.trim($el.val())) { // Not empty, calculate the strength + $el.trigger("keyup"); + } + + if ($.isFunction(localOptions.common.onLoad)) { + localOptions.common.onLoad(); + } + }); + + return this; + }; + + methods.destroy = function () { + this.each(function (idx, el) { + var $el = $(el), + options = $el.data("pwstrength-bootstrap"), + elements = ui.getUIElements(options, $el); + elements.$progressbar.remove(); + elements.$verdict.remove(); + elements.$errors.remove(); + $el.removeData("pwstrength-bootstrap"); + }); + }; + + methods.forceUpdate = function () { + this.each(function (idx, el) { + var event = { target: el }; + onKeyUp(event); + }); + }; + + methods.addRule = function (name, method, score, active) { + this.each(function (idx, el) { + var options = $(el).data("pwstrength-bootstrap"); + + options.rules.activated[name] = active; + options.rules.scores[name] = score; + options.rules.extra[name] = method; + }); + }; + + applyToAll = function (rule, prop, value) { + this.each(function (idx, el) { + $(el).data("pwstrength-bootstrap").rules[prop][rule] = value; + }); + }; + + methods.changeScore = function (rule, score) { + applyToAll.call(this, rule, "scores", score); + }; + + methods.ruleActive = function (rule, active) { + applyToAll.call(this, rule, "activated", active); + }; + + $.fn.pwstrength = function (method) { + var result; + + if (methods[method]) { + result = methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === "object" || !method) { + result = methods.init.apply(this, arguments); + } else { + $.error("Method " + method + " does not exist on jQuery.pwstrength-bootstrap"); + } + + return result; + }; + }(jQuery, methods)); +}(jQuery));
\ No newline at end of file |