diff options
1216 files changed, 32975 insertions, 17605 deletions
diff --git a/.gitignore b/.gitignore index 912d61a8f0a..683e6c45a2b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,13 +2,15 @@ .rbx/ db/*.sqlite3 db/*.sqlite3-journal -log/*.log +log/*.log* tmp/ .sass-cache/ coverage/* backups/* *.swp public/uploads/ +.ruby-version +.ruby-gemset .rvmrc .rbenv-version .directory @@ -20,6 +22,7 @@ config/database.yml config/initializers/omniauth.rb config/unicorn.rb config/resque.yml +config/aws.yml db/data.yml .idea .DS_Store @@ -27,3 +30,6 @@ db/data.yml vendor/bundle/* rails_best_practices_output.html doc/code/* +.secret +*.log +public/uploads.* @@ -1 +1 @@ ---colour +--color --drb diff --git a/.travis.yml b/.travis.yml index 6d39488df27..3ed3dec1425 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: ruby env: - - DB=postgresql - - DB=mysql + - DB=mysql TRAVIS=true before_install: - sudo apt-get install libicu-dev -y - gem install charlock_holmes -v="0.6.9" @@ -9,7 +8,7 @@ branches: only: - 'master' rvm: - - 1.9.3-p327 + - 2.0.0 services: - mysql - postgresql diff --git a/CHANGELOG b/CHANGELOG index a692bbfa8f2..3db03f76ed7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,10 +1,197 @@ +v 6.2.0 + - Public projects are visible from the outside + +v 6.1.0 + - Project specific IDs for issues, mr, milestones + Above items will get a new id and for example all bookmarked issue urls will change. + Old issue urls are redirected to the new one if the issue id is too high for an internal id. + - Description field added to Merge Request + - API: Sudo api calls (Izaak Alpert) + - API: Group membership api (Izaak Alpert) + - Improved commit diff + - Improved large commit handling (Boyan Tabakov) + - Rewrite: Init script now less prone to errors and keeps better track of the service (Rovanion Luckey) + - Link issues, merge requests, and commits when they reference each other with GFM (Ash Wilson) + - Close issues automatically when pushing commits with a special message + - Improve user removal from admin area + - Invalidate events cache when project was moved + - Remove deprecated classes and rake tasks + - Add event filter for group and project show pages + - Add links to create branch/tag from project home page + - Add public-project? checkbox to new-project view + - Improved compare page. Added link to proceed into Merge Request + - Send email to user when he was added to group + - New landing page when you have 0 projects + +v 6.0.0 + - Feature: Replace teams with group membership + We introduce group membership in 6.0 as a replacement for teams. + The old combination of groups and teams was confusing for a lot of people. + And when the members of a team where changed this wasn't reflected in the project permissions. + In GitLab 6.0 you will be able to add members to a group with a permission level for each member. + These group members will have access to the projects in that group. + Any changes to group members will immediately be reflected in the project permissions. + You can even have multiple owners for a group, greatly simplifying administration. + - Feature: Ability to have multiple owners for group + - Feature: Merge Requests between fork and project (Izaak Alpert) + - Feature: Generate fingerprint for ssh keys + - Feature: Ability to create and remove branches with UI + - Feature: Ability to create and remove git tags with UI + - Feature: Groups page in profile. You can leave group there + - API: Allow login with LDAP credentials + - Redesign: project settings navigation + - Redesign: snippets area + - Redesign: ssh keys page + - Redesign: buttons, blocks and other ui elements + - Add comment title to rss feed + - You can use arrows to navigate at tree view + - Add project filter on dashboard + - Cache project graph + - Drop support of root namespaces + - Default theme is classic now + - Cache result of methods like authorize_projects, project.team.members etc + - Remove $.ready events + - Fix onclick events being double binded + - Add notification level to group membership + - Move all project controllers/views under Projects:: module + - Move all profile controllers/views under Profiles:: module + - Apply user project limit only for personal projects + - Unicorn is default web server again + - Store satellites lock files inside satellites dir + - Disabled threadsafety mode in rails + - Fixed bug with loosing MR comments + - Improved MR comments logic + - Render readme file for projects in public area + +v 5.4.0 + - Ability to edit own comments + - Documentation improvements + - Improve dashboard projects page + - Fixed nav for empty repos + - GitLab Markdown help page + - Misspelling fixes + - Added support of unicorn and fog gems + - Added client list to API doc + - Fix PostgreSQL database restoration problem + - Increase snippet content column size + - allow project import via git:// url + - Show participants on issues, including mentions + - Notify mentioned users with email + +v 5.3.0 + - Refactored services + - Campfire service added + - HipChat service added + - Fixed bug with LDAP + git over http + - Fixed bug with google analytics code being ignored + - Improve sign-in page if ldap enabled + - Respect newlines in wall messages + - Generate the Rails secret token on first run + - Rename repo feature + - Init.d: remove gitlab.socket on service start + - Api: added teams api + - Api: Prevent blob content being escaped + - Api: Smart deploy key add behaviour + - Api: projects/owned.json return user owned project + - Fix bug with team assignation on project from #4109 + - Advanced snippets: public/private, project/personal (Andrew Kulakov) + - Repository Graphs (Karlo Nicholas T. Soriano) + - Fix dashboard lost if comment on commit + - Update gitlab-grack. Fixes issue with --depth option + - Fix project events duplicate on project page + - Fix postgres error when displaying network graph. + - Fix dashboard event filter when navigate via turbolinks + - init.d: Ensure socket is removed before starting service + - Admin area: Style teams:index, group:show pages + - Own page for failed forking + - Scrum view for milestone + +v 5.2.0 + - Turbolinks + - Git over http with ldap credentials + - Diff with better colors and some spacing on the corners + - Default values for project features + - Fixed huge_commit view + - Restyle project clone panel + - Move Gitlab::Git code to gitlab_git gem + - Move update docs in repo + - Requires gitlab-shell v1.4.0 + - Fixed submodules listing under file tab + - Fork feature (Angus MacArthur) + - git version check in gitlab:check + - Shared deploy keys feature + - Ability to generate default labels set for issues + - Improve gfm autocomplete (Harold Luo) + - Added support for Google Analytics + - Code search feature (Javier Castro) + +v 5.1.0 + - You can login with email or username now + - Corrected project transfer rollback when repository cannot be moved + - Move both repo and wiki when project transfer requested + - Admin area: project editing was removed from admin namespace + - Access: admin user has now access to any project. + - Notification settings + - Gitlab::Git set of objects to abstract from grit library + - Replace Unicorn web server with Puma + - Backup/Restore refactored. Backup dump project wiki too now + - Restyled Issues list. Show milestone version in issue row + - Restyled Merge Request list + - Backup now dump/restore uploads + - Improved performance of dashboard (Andrew Kumanyaev) + - File history now tracks renames (Akzhan Abdulin) + - Drop wiki migration tools + - Drop sqlite migration tools + - project tagging + - Paginate users in API + - Restyled network graph (Hiroyuki Sato) + +v 5.0.1 + - Fixed issue with gitlab-grit being overridden by grit + v 5.0.0 - Replaced gitolite with gitlab-shell + - Removed gitolite-related libraries + - State machine added + - Setup gitlab as git user + - Internal API + - Show team tab for empty projects + - Import repository feature + - Updated rails + - Use lambda for scopes + - Redesign admin area -> users + - Redesign admin area -> user + - Secure link to file attachments + - Add validations for Group and Team names + - Restyle team page for project + - Update capybara, rspec-rails, poltergeist to recent versions + - Wiki on git using Gollum + - Added Solarized Dark theme for code review + - Don't show user emails in autocomplete lists, profile pages + - Added settings tab for group, team, project + - Replace user popup with icons in header + - Handle project moving with gitlab-shell + - Added select2-rails for selectboxes with ajax data load + - Fixed search field on projects page + - Added teams to search autocomplete + - Move groups and teams on dashboard sidebar to sub-tabs + - API: improved return codes and docs. (Felix Gilcher, Sebastian Ziebell) + - Redesign wall to be more like chat + - Snippets, Wall features are disabled by default for new projects v 4.2.0 - Teams - User show page. Via /u/username - Show help contents on pages for better navigation + - Async gitolite calls + - added satellites logs + - can_create_group, can_create_team booleans for User + - Process web hooks async + - GFM: Fix images escaped inside links + - Network graph improved + - Switchable branches for network graph + - API: Groups + - Fixed project download v 4.1.0 - Optional Sign-Up @@ -20,7 +207,7 @@ v 4.1.0 - cleanup rake tasks - fix backup/restore - scss cleanup - - show preview for note images + - show preview for note images - improved network-graph - get rid of app/roles/ - added new classes Team, Repository @@ -33,8 +220,8 @@ v 4.1.0 v 4.0.0 - Remove project code and path from API. Use id instead - - Return valid clonable url to repo for web hook - - Fixed backup issue + - Return valid cloneable url to repo for web hook + - Fixed backup issue - Reorganized settings - Fixed commits compare - Refactored scss @@ -98,15 +285,15 @@ v 3.0.0 - Web Editor - Fixed bug with gitolite keys - UI improved - - Increased perfomance of application + - Increased performance of application - Show user avatar in last commit when browsing Files - Refactored Gitlab::Merge - - Use Font Awsome for icons - - Separate observing of Note and MergeRequestsa + - Use Font Awesome for icons + - Separate observing of Note and MergeRequests - Milestone "All Issues" filter - Fix issue close and reopen button text and styles - Fix forward/back while browsing Tree hierarchy - - Show numer of notes for commits and merge requests + - Show number of notes for commits and merge requests - Added support pg from box and update installation doc - Reject ssh keys that break gitolite - [API] list one project hook @@ -125,7 +312,7 @@ v 2.9.0 - added factory_girl - restyled projects list on dashboard - ssh keys validation to prevent gitolite crash - - send notifications if changed premission in project + - send notifications if changed permission in project - scss refactoring. gitlab_bootstrap/ dir - fix git push http body bigger than 112k problem - list of labels page under issues tab @@ -159,7 +346,7 @@ v 2.7.0 - System hooks - UI improved - Dashboard events endless scroll - - Source perfomance increased + - Source performance increased v 2.6.0 - UI polished @@ -167,7 +354,7 @@ v 2.6.0 - Handle huge commits - Last Push widget - Bugfix - - Better perfomance + - Better performance - Email in resque - Increased test coverage - Ability to remove branch with MR accept @@ -188,7 +375,7 @@ v 2.4.0 - Bootstrap 2.0 - Responsive layout - Big commits handling - - Perfomance improved + - Performance improved - Milestones v 2.3.1 @@ -268,11 +455,11 @@ v 1.0.2 - added adv validation for project path & code - feature: issues can be sortable - bugfix - - username dispalyed on top panel + - username displayed on top panel v 1.0.1 - fixed: with invalid source code for commit - - fixed: lose branch/tag selection when use tree navigateion + - fixed: lose branch/tag selection when use tree navigation - when history clicked - display path - bug fix & code cleaning @@ -289,11 +476,11 @@ v 0.9.4 - authorization improved - html escaping - bug fix - - increassed test coverage + - increased test coverage - design improvements v 0.9.1 - - increassed test coverage + - increased test coverage - design improvements - new issue email notification - updated app name @@ -301,7 +488,7 @@ v 0.9.1 - issue can be edit v 0.8.0 - - sytax highlight for main file types + - syntax highlight for main file types - redesign - stability - security fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00304dd3d64..9d9be5bdc21 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,26 +1,75 @@ -# Contact & support +# Contribute to GitLab -If you want quick help, head over to our [Support Forum](https://groups.google.com/forum/#!forum/gitlabhq). -Otherwise you can follow our [Issue Submission Guide](https://github.com/gitlabhq/gitlabhq/wiki/Issue-Submission-Guide) for a more systematic and thorough guide to solving your issues. +This guide details how to use issues and pull requests to improve GitLab. +- [Closing policy for issues and pull requests](#closing-policy-for-issues-and-pull-requests) +- [Issue tracker](#issue-tracker) +- [Pull requests](#pull-requests) +If you want to know how the GitLab team handles contributions have a look at [the GitLab contributing process](PROCESS.md). -# Contribute to GitLab +## Closing policy for issues and pull requests + +GitLab is a popular open source project and the capacity to deal with issues and pull requests is limited. Out of respect for our volunteers, issues and pull requests not in line with the guidelines listed in this document may be closed without notice. + +Please treat our volunteers with courtesy and respect, it will go a long way towards getting your issue resolved. + +Issues and pull requests should be in English and contain appropriate language for audiences of all ages. + +## Issue tracker + +To get support for your particular problem please use the channels as detailed in [the getting help section of the readme](https://github.com/gitlabhq/gitlabhq#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/). + +The [issue tracker](https://github.com/gitlabhq/gitlabhq/issues) is only for obvious bugs or misbehavior in the latest [stable or development release of GitLab](MAINTENANCE.md). 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 pull request which partially or fully addresses the issue. + +Do not use the issue tracker for feature requests. We have a specific [feedback and suggestions forum](http://feedback.gitlab.com) for this purpose. + +Please send a pull request with a tested solution or a pull request with a failing test instead of opening an issue if you can. If you're unsure where to post, post to the [mailing list](https://groups.google.com/forum/#!forum/gitlabhq) or [Stack Overflow](http://stackoverflow.com/questions/tagged/gitlab) first. There are a lot of helpful GitLab users there who may be able to help you quickly. If your particular issue turns out to be a bug, it will find its way from there. + +### Issue tracker guidelines + +**[Search](https://github.com/gitlabhq/gitlabhq/search?q=&ref=cmdform&type=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): -## Recipes +1. **Summary:** Summarize your issue in one sentence (what goes wrong, what did you expect to happen) +2. **Steps to reproduce:** How can we reproduce the issue, preferably on the [GitLab Vagrant virtual machine](https://github.com/gitlabhq/gitlab-vagrant-vm) (start with: `vagrant destroy && vagrant up && vagrant ssh`) +3. **Expected behavior:** Describe your issue in detail +4. **Observed behavior** +5. **Relevant logs and/or screenshots:** Please use code blocks (\`\`\`) to format console output, logs, and code as it's very hard to read otherwise. +6. **Output of checks** + * Results of GitLab [Application Check](doc/install/installation.md#check-application-status) (`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production`); we will only investigate if the tests are passing + * Version of GitLab you are running; we will only investigate issues in the latest stable and development releases as per the [maintenance policy](MAINTENANCE.md) + * Add the last commit sha1 of the GitLab version you used to replicate the issue (obtainable from the help page) + * Describe your setup (use relevant parts from `sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`) +7. **Possible fixes**: If you can, link to the line of code that might be responsible for the problem -We collect user submitted installation scripts and config file templates for platforms we don't support officially. -We believe there is merit in allowing a certain amount of diversity. -You can get and submit your solution to running/configuring GitLab with your favorite OS/distro, database, web server, cloud hoster, configuration management tool, etc. +## Pull requests -Help us improve the collection of [GitLab Recipes](https://github.com/gitlabhq/gitlab-recipes/) +We welcome pull requests with fixes and improvements to GitLab code, tests, and/or documentation. The features we would really like a pull request for are listed with the [status 'accepting merge/pull requests' on our feedback forum](http://feedback.gitlab.com/forums/176466-general/status/796455) but other improvements are also welcome. +### Pull request guidelines -## Feature suggestions +If you can, please submit a pull 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 pull request is as follows: -Follow the [Issue Submission Guide](https://github.com/gitlabhq/gitlabhq/wiki/Issue-Submission-Guide) and support other peoples ideas or propose your own. +1. Fork the project on GitHub +1. Create a feature branch +1. Write [tests](README.md#run-the-tests) and code +1. Add your changes to the [CHANGELOG](CHANGELOG) +1. If you have multiple commits please combine them into one commit by [squashing them](http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) +1. Push the commit to your fork +1. Submit a pull request +2. [Search for issues](https://github.com/gitlabhq/gitlabhq/search?q=&ref=cmdform&type=Issues) related to your pull request and mention them in the pull request description +We will accept pull requests if: -## Code +* The code has proper tests and all tests pass (or it is a test exposing a failure in existing code) +* It can be merged without problems (if not please use: `git rebase master`) +* It does not break any existing functionality +* It's quality code that conforms to the [Ruby](https://github.com/bbatsov/ruby-style-guide) and [Rails](https://github.com/bbatsov/rails-style-guide) style guides and best practices +* The description includes a motive for your change and the method you used to achieve it +* It is not a catch all pull request but rather fixes a specific issue or implements a specific feature +* It keeps the GitLab code base clean and well structured +* We think other users will benefit from the same functionality +* If it makes changes to the UI the pull request should include screenshots +* It is a single commit (please use `git rebase -i` to squash commits) -Follow our [Developer Guide](https://github.com/gitlabhq/gitlabhq/wiki/Developer-Guide) to set you up for hacking on GitLab. +For examples of feedback on pull requests please look at already [closed pull requests](https://github.com/gitlabhq/gitlabhq/pulls?direction=desc&page=1&sort=created&state=closed). @@ -1,4 +1,4 @@ -source "http://rubygems.org" +source "https://rubygems.org" def darwin_only(require_as) RUBY_PLATFORM.include?('darwin') && require_as @@ -8,58 +8,64 @@ def linux_only(require_as) RUBY_PLATFORM.include?('linux') && require_as end -gem "rails", "3.2.12" +gem "rails", "3.2.13" # Supported DBs gem "mysql2", group: :mysql gem "pg", group: :postgres # Auth -gem "devise", "~> 2.1.0" -gem 'omniauth', "~> 1.1.1" +gem "devise", '~> 2.2' +gem 'omniauth', "~> 1.1.3" gem 'omniauth-google-oauth2' gem 'omniauth-twitter' gem 'omniauth-github' -# GITLAB patched libs -gem "grit", git: "https://github.com/gitlabhq/grit.git", ref: '7f35cb98ff17d534a07e3ce6ec3d580f67402837' -gem 'grack', git: "https://github.com/gitlabhq/grack.git", ref: 'ba46f3b0845c6a09d488ae6abdce6ede37e227e8' -gem 'grit_ext', git: "https://github.com/gitlabhq/grit_ext.git", ref: '8e6afc2da821354774aa4d1ee8a1aa2082f84a3e' +# Extracting information from a git repository +# Provide access to Gitlab::Git library +gem "gitlab_git", '2.3.1' -# LDAP Auth -gem 'gitlab_omniauth-ldap', '1.0.2', require: "omniauth-ldap" +# Ruby/Rack Git Smart-HTTP Server Handler +gem 'gitlab-grack', '~> 1.0.1', require: 'grack' -# Dump db to yml file. Mostly used to migrate from sqlite to mysql -gem 'gitlab_yaml_db', '1.0.0', require: "yaml_db" +# LDAP Auth +gem 'gitlab_omniauth-ldap', '1.0.3', require: "omniauth-ldap" # Syntax highlighter -gem "pygments.rb", git: "https://github.com/gitlabhq/pygments.rb.git", branch: "master" +gem "gitlab-pygments.rb", '~> 0.3.2', require: 'pygments.rb' + +# Git Wiki +gem "gitlab-gollum-lib", "~> 1.0.1", require: 'gollum-lib' # Language detection -gem "github-linguist", "~> 2.3.4" , require: "linguist" +gem "github-linguist", require: "linguist" # API -gem "grape", "~> 0.2.1" +gem "grape", "~> 0.4.1" +gem "grape-entity", "~> 0.3.0" # Format dates and times # based on human-friendly examples gem "stamp" +# Enumeration fields +gem 'enumerize' + # Pagination gem "kaminari", "~> 0.14.1" # HAML -gem "haml-rails", "~> 0.3.5" +gem "haml-rails" # Files attachments -gem "carrierwave", "~> 0.7.1" +gem "carrierwave" + +# for aws storage +gem "fog", "~> 1.3.1", group: :aws # Authorization gem "six" -# Generate Fake data -gem "ffaker" - # Seed data gem "seed-fu" @@ -67,19 +73,22 @@ gem "seed-fu" gem "redcarpet", "~> 2.2.2" gem "github-markup", "~> 0.7.4", require: 'github/markup' -# Servers -gem "unicorn", "~> 4.4.0" +# Asciidoc to HTML +gem "asciidoctor" -# Issue tags -gem "acts-as-taggable-on", "2.3.3" +# Application server +gem "unicorn", '~> 4.6.3', group: :unicorn -# Decorators -gem "draper", "~> 0.18.0" +# State machine +gem "state_machine" + +# Issue tags +gem "acts-as-taggable-on" # Background jobs gem 'slim' -gem 'sinatra', :require => nil -gem 'sidekiq', '2.6.4' +gem 'sinatra', require: nil +gem 'sidekiq' # HTTP requests gem "httparty" @@ -92,23 +101,44 @@ gem 'settingslogic' # Misc gem "foreman" -gem "git" + +# Cache +gem "redis-rails" + +# Campfire integration +gem 'tinder', '~> 1.9.2' + +# HipChat integration +gem "hipchat", "~> 0.9.0" + +# d3 +gem "d3_rails", "~> 3.1.4" + +# underscore-rails +gem "underscore-rails", "~> 1.4.4" + +# Sanitize user input +gem "sanitize" group :assets do - gem "sass-rails", "~> 3.2.5" - gem "coffee-rails", "~> 3.2.2" - gem "uglifier", "~> 1.3.0" + gem "sass-rails" + gem "coffee-rails" + gem "uglifier" gem "therubyracer" + gem 'turbolinks' + gem 'jquery-turbolinks' - gem 'chosen-rails', "0.9.8" - gem 'jquery-atwho-rails', "0.1.7" + gem 'chosen-rails', "1.0.0" + gem 'select2-rails' + gem 'jquery-atwho-rails', "0.3.0" gem "jquery-rails", "2.1.3" gem "jquery-ui-rails", "2.0.2" gem "modernizr", "2.6.2" - gem "raphael-rails", git: "https://github.com/gitlabhq/raphael-rails.git" - gem 'bootstrap-sass', "2.2.1.1" - gem "font-awesome-sass-rails", "~> 3.0.0" + gem "raphael-rails", "~> 2.1.2" + gem 'bootstrap-sass' + gem "font-awesome-rails" gem "gemoji", "~> 1.2.1", require: 'emoji/railtie' + gem "gon" end group :development do @@ -130,16 +160,23 @@ group :development do end group :development, :test do + gem 'coveralls', require: false gem 'rails-dev-tweaks' gem 'spinach-rails' gem "rspec-rails" gem "capybara" gem "pry" gem "awesome_print" - gem "database_cleaner", ref: "f89c34300e114be99532f14c115b2799a3380ac6", git: "https://github.com/bmabey/database_cleaner.git" + gem "database_cleaner" gem "launchy" gem 'factory_girl_rails' + # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) + gem 'minitest', '~> 4.7.0' + + # Generate Fake data + gem "ffaker" + # Guard gem 'guard-rspec' gem 'guard-spinach' @@ -150,17 +187,20 @@ group :development, :test do gem 'rb-inotify', require: linux_only('rb-inotify') # PhantomJS driver for Capybara - gem 'poltergeist', git: 'https://github.com/jonleighton/poltergeist.git', ref: '5c2e092001074a8cf09f332d3714e9ba150bc8ca' + gem 'poltergeist', '~> 1.4.1' + + gem 'spork', '~> 1.0rc' + gem 'jasmine' end group :test do gem "simplecov", require: false - gem "shoulda-matchers", "1.3.0" + gem "shoulda-matchers", "~> 2.1.0" gem 'email_spec' gem "webmock" gem 'test_after_commit' end group :production do - gem "gitlab_meta", '5.0' + gem "gitlab_meta", '6.0' end diff --git a/Gemfile.lock b/Gemfile.lock index 1bc7124fff4..9de7a0f876b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,80 +1,20 @@ GIT - remote: https://github.com/bmabey/database_cleaner.git - revision: f89c34300e114be99532f14c115b2799a3380ac6 - ref: f89c34300e114be99532f14c115b2799a3380ac6 - specs: - database_cleaner (0.9.1) - -GIT remote: https://github.com/ctran/annotate_models.git - revision: be4e26825b521f0b2d86b181e2dff89901aa9b1e + revision: 18a4e2eb77c8f3ef695b563e4a7ca45dfede819b specs: - annotate (2.6.0.beta1) + annotate (2.6.0.beta2) activerecord (>= 2.3.0) rake (>= 0.8.7) -GIT - remote: https://github.com/gitlabhq/grack.git - revision: ba46f3b0845c6a09d488ae6abdce6ede37e227e8 - ref: ba46f3b0845c6a09d488ae6abdce6ede37e227e8 - specs: - grack (1.0.0) - rack (~> 1.4.1) - -GIT - remote: https://github.com/gitlabhq/grit.git - revision: 7f35cb98ff17d534a07e3ce6ec3d580f67402837 - ref: 7f35cb98ff17d534a07e3ce6ec3d580f67402837 - specs: - grit (2.5.0) - diff-lcs (~> 1.1) - mime-types (~> 1.15) - posix-spawn (~> 0.3.6) - -GIT - remote: https://github.com/gitlabhq/grit_ext.git - revision: 8e6afc2da821354774aa4d1ee8a1aa2082f84a3e - ref: 8e6afc2da821354774aa4d1ee8a1aa2082f84a3e - specs: - grit_ext (0.6.1) - charlock_holmes (~> 0.6.9) - -GIT - remote: https://github.com/gitlabhq/pygments.rb.git - revision: db1da0343adf86b49bdc3add04d02d2e80438d38 - branch: master - specs: - pygments.rb (0.3.2) - posix-spawn (~> 0.3.6) - yajl-ruby (~> 1.1.0) - -GIT - remote: https://github.com/gitlabhq/raphael-rails.git - revision: cb2c92a040b9b941a5f1aa1ea866cc26e944fe58 - specs: - raphael-rails (2.1.0) - -GIT - remote: https://github.com/jonleighton/poltergeist.git - revision: 5c2e092001074a8cf09f332d3714e9ba150bc8ca - ref: 5c2e092001074a8cf09f332d3714e9ba150bc8ca - specs: - poltergeist (1.0.2) - capybara (~> 1.1) - childprocess (~> 0.3) - faye-websocket (~> 0.4, >= 0.4.4) - http_parser.rb (~> 0.5.3) - multi_json (~> 1.0) - GEM - remote: http://rubygems.org/ + remote: https://rubygems.org/ specs: - actionmailer (3.2.12) - actionpack (= 3.2.12) - mail (~> 2.4.4) - actionpack (3.2.12) - activemodel (= 3.2.12) - activesupport (= 3.2.12) + actionmailer (3.2.13) + actionpack (= 3.2.13) + mail (~> 2.5.3) + actionpack (3.2.13) + activemodel (= 3.2.13) + activesupport (= 3.2.13) builder (~> 3.0.0) erubis (~> 2.7.0) journey (~> 1.0.4) @@ -82,205 +22,285 @@ GEM rack-cache (~> 1.2) rack-test (~> 0.6.1) sprockets (~> 2.2.1) - activemodel (3.2.12) - activesupport (= 3.2.12) + activemodel (3.2.13) + activesupport (= 3.2.13) builder (~> 3.0.0) - activerecord (3.2.12) - activemodel (= 3.2.12) - activesupport (= 3.2.12) + activerecord (3.2.13) + activemodel (= 3.2.13) + activesupport (= 3.2.13) arel (~> 3.0.2) tzinfo (~> 0.3.29) - activeresource (3.2.12) - activemodel (= 3.2.12) - activesupport (= 3.2.12) - activesupport (3.2.12) - i18n (~> 0.6) + activeresource (3.2.13) + activemodel (= 3.2.13) + activesupport (= 3.2.13) + activesupport (3.2.13) + i18n (= 0.6.1) multi_json (~> 1.0) - acts-as-taggable-on (2.3.3) - rails (~> 3.0) - addressable (2.3.2) + acts-as-taggable-on (2.4.1) + rails (>= 3, < 5) + addressable (2.3.4) arel (3.0.2) + asciidoctor (0.1.3) awesome_print (1.1.0) - backports (2.6.5) - bcrypt-ruby (3.0.1) - better_errors (0.3.2) + backports (3.3.2) + bcrypt-ruby (3.1.1) + better_errors (0.9.0) coderay (>= 1.0.0) - erubis (>= 2.7.0) - binding_of_caller (0.6.8) - bootstrap-sass (2.2.1.1) + erubis (>= 2.6.6) + binding_of_caller (0.7.2) + debug_inspector (>= 0.0.1) + bootstrap-sass (2.3.2.0) sass (~> 3.2) builder (3.0.4) - capybara (1.1.3) + capybara (2.1.0) mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) - selenium-webdriver (~> 2.0) - xpath (~> 0.1.4) - carrierwave (0.7.1) + xpath (~> 2.0) + carrierwave (0.8.0) activemodel (>= 3.2.0) activesupport (>= 3.2.0) - celluloid (0.12.4) - facter (>= 1.6.12) + celluloid (0.14.1) timers (>= 1.0.0) - charlock_holmes (0.6.9) - childprocess (0.3.6) - ffi (~> 1.0, >= 1.0.6) - chosen-rails (0.9.8) - railties (~> 3.0) - thor (~> 0.14) - code_analyzer (0.3.1) + charlock_holmes (0.6.9.4) + childprocess (0.3.9) + ffi (~> 1.0, >= 1.0.11) + chosen-rails (1.0.0) + coffee-rails (>= 3.2) + compass-rails (>= 1.0) + railties (>= 3.0) + sass-rails (>= 3.2) + chunky_png (1.2.8) + cliver (0.2.1) + code_analyzer (0.3.2) sexp_processor - coderay (1.0.8) + coderay (1.0.9) coffee-rails (3.2.2) coffee-script (>= 2.2.0) railties (~> 3.2.0) coffee-script (2.2.0) coffee-script-source execjs - coffee-script-source (1.4.0) + coffee-script-source (1.6.2) colored (1.2) colorize (0.5.8) - connection_pool (1.0.0) - crack (0.3.1) + compass (0.12.2) + chunky_png (~> 1.2) + fssm (>= 0.2.7) + sass (~> 3.1) + compass-rails (1.0.3) + compass (>= 0.12.2, < 0.14) + connection_pool (1.1.0) + coveralls (0.6.7) + colorize + multi_json (~> 1.3) + rest-client + simplecov (>= 0.7) + thor + crack (0.4.0) + safe_yaml (~> 0.9.0) + d3_rails (3.1.10) + railties (>= 3.1.0) daemons (1.1.9) - devise (2.1.2) + database_cleaner (1.1.1) + debug_inspector (0.0.2) + descendants_tracker (0.0.1) + devise (2.2.5) bcrypt-ruby (~> 3.0) orm_adapter (~> 0.1) railties (~> 3.1) warden (~> 1.2.1) - diff-lcs (1.1.3) - draper (0.18.0) - actionpack (~> 3.2) - activesupport (~> 3.2) + diff-lcs (1.2.4) + dotenv (0.8.0) email_spec (1.4.0) launchy (~> 2.1) mail (~> 2.2) + enumerize (0.6.1) + activesupport (>= 3.2) erubis (2.7.0) escape_utils (0.2.4) - eventmachine (1.0.0) + eventmachine (1.0.3) + excon (0.13.4) execjs (1.4.0) multi_json (~> 1.0) - facter (1.6.17) - factory_girl (4.1.0) + factory_girl (4.2.0) activesupport (>= 3.0.0) - factory_girl_rails (4.1.0) - factory_girl (~> 4.1.0) + factory_girl_rails (4.2.1) + factory_girl (~> 4.2.0) railties (>= 3.0.0) - faraday (0.8.4) + faraday (0.8.7) multipart-post (~> 1.1) - faye-websocket (0.4.6) - eventmachine (>= 0.12.0) - ffaker (1.15.0) - ffi (1.1.5) - font-awesome-sass-rails (3.0.0.1) - railties (>= 3.1.1) - sass-rails (>= 3.1.1) - foreman (0.60.2) + faraday_middleware (0.9.0) + faraday (>= 0.7.4, < 0.9) + ffaker (1.16.1) + ffi (1.9.0) + fog (1.3.1) + builder + excon (~> 0.13.0) + formatador (~> 0.2.0) + mime-types + multi_json (~> 1.0) + net-scp (~> 1.0.4) + net-ssh (>= 2.1.3) + nokogiri (~> 1.5.0) + ruby-hmac + font-awesome-rails (3.2.1.2) + railties (>= 3.2, < 5.0) + foreman (0.63.0) + dotenv (>= 0.7) thor (>= 0.13.6) + formatador (0.2.4) + fssm (0.2.10) gemoji (1.2.1) - gherkin-ruby (0.2.1) - git (1.2.5) + gherkin-ruby (0.3.0) github-linguist (2.3.4) charlock_holmes (~> 0.6.6) escape_utils (~> 0.2.3) mime-types (~> 1.19) pygments.rb (>= 0.2.13) - github-markup (0.7.4) - gitlab_meta (5.0) - gitlab_omniauth-ldap (1.0.2) - net-ldap (~> 0.2.2) + github-markdown (0.5.3) + github-markup (0.7.5) + gitlab-gollum-lib (1.0.1) + github-markdown (~> 0.5.3) + github-markup (>= 0.7.5, < 1.0.0) + gitlab-grit (>= 2.5.1) + nokogiri (~> 1.5.9) + pygments.rb (~> 0.4.2) + sanitize (~> 2.0.3) + stringex (~> 1.5.1) + gitlab-grack (1.0.1) + rack (~> 1.4.1) + gitlab-grit (2.6.0) + charlock_holmes (~> 0.6.9) + diff-lcs (~> 1.1) + mime-types (~> 1.15) + posix-spawn (~> 0.3.6) + gitlab-pygments.rb (0.3.2) + posix-spawn (~> 0.3.6) + yajl-ruby (~> 1.1.0) + gitlab_git (2.3.1) + activesupport (~> 3.2.13) + github-linguist (~> 2.3.4) + gitlab-grit (~> 2.6.0) + gitlab_meta (6.0) + gitlab_omniauth-ldap (1.0.3) + net-ldap (~> 0.3.1) omniauth (~> 1.0) pyu-ruby-sasl (~> 0.0.3.1) rubyntlm (~> 0.1.1) - gitlab_yaml_db (1.0.0) - grape (0.2.2) + gon (4.1.1) + actionpack (>= 2.3.0) + json + grape (0.4.1) activesupport - hashie (~> 1.2) + builder + hashie (>= 1.2.0) multi_json (>= 1.3.2) - multi_xml - rack + multi_xml (>= 0.5.2) + rack (>= 1.3.0) rack-accept rack-mount virtus + grape-entity (0.3.0) + activesupport + multi_json (>= 1.3.2) growl (1.0.3) - guard (1.5.4) - listen (>= 0.4.2) + guard (1.8.1) + formatador (>= 0.2.4) + listen (>= 1.0.0) lumberjack (>= 1.0.2) pry (>= 0.9.10) thor (>= 0.14.6) - guard-rspec (2.1.2) - guard (>= 1.1) - rspec (~> 2.11) + guard-rspec (3.0.2) + guard (>= 1.8) + rspec (~> 2.13) guard-spinach (0.0.2) guard (>= 1.1) spinach - haml (3.1.7) - haml-rails (0.3.5) + haml (4.0.3) + tilt + haml-rails (0.4) actionpack (>= 3.1, < 4.1) activesupport (>= 3.1, < 4.1) - haml (~> 3.1) + haml (>= 3.1, < 4.1) railties (>= 3.1, < 4.1) hashie (1.2.0) - hike (1.2.1) + hike (1.2.3) + hipchat (0.9.0) + httparty + httparty http_parser.rb (0.5.3) - httparty (0.9.0) + httparty (0.11.0) multi_json (~> 1.0) - multi_xml + multi_xml (>= 0.5.2) httpauth (0.2.0) i18n (0.6.1) + jasmine (1.3.2) + jasmine-core (~> 1.3.1) + rack (~> 1.0) + rspec (>= 1.3.1) + selenium-webdriver (>= 0.1.3) + jasmine-core (1.3.1) journey (1.0.4) - jquery-atwho-rails (0.1.7) + jquery-atwho-rails (0.3.0) jquery-rails (2.1.3) railties (>= 3.1.0, < 5.0) thor (~> 0.14) + jquery-turbolinks (1.0.0) + railties (>= 3.1.0) + turbolinks jquery-ui-rails (2.0.2) jquery-rails railties (>= 3.1.0) json (1.7.7) - jwt (0.1.5) - multi_json (>= 1.0) + jwt (0.1.8) + multi_json (>= 1.5) kaminari (0.14.1) actionpack (>= 3.0.0) activesupport (>= 3.0.0) - kgio (2.7.4) - launchy (2.1.2) + kgio (2.8.0) + launchy (2.3.0) addressable (~> 2.3) - letter_opener (1.0.0) - launchy (>= 2.0.4) - libv8 (3.3.10.4) - libwebsocket (0.1.6) - websocket - listen (0.5.3) - lumberjack (1.0.2) - mail (2.4.4) - i18n (>= 0.4.0) + letter_opener (1.1.1) + launchy (~> 2.2) + libv8 (3.11.8.17) + listen (1.2.2) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + rb-kqueue (>= 0.2) + lumberjack (1.0.3) + mail (2.5.4) mime-types (~> 1.16) treetop (~> 1.4.8) method_source (0.8.1) - mime-types (1.21) + mime-types (1.25) + minitest (4.7.4) modernizr (2.6.2) sprockets (~> 2.0) - multi_json (1.5.1) - multi_xml (0.5.1) - multipart-post (1.1.5) + multi_json (1.7.9) + multi_xml (0.5.4) + multipart-post (1.2.0) mysql2 (0.3.11) - net-ldap (0.2.2) - nokogiri (1.5.5) + net-ldap (0.3.1) + net-scp (1.0.4) + net-ssh (>= 1.99.1) + net-ssh (2.6.8) + nokogiri (1.5.10) oauth (0.4.7) - oauth2 (0.8.0) + oauth2 (0.8.1) faraday (~> 0.8) httpauth (~> 0.1) jwt (~> 0.1.4) multi_json (~> 1.0) rack (~> 1.2) - omniauth (1.1.1) - hashie (~> 1.2) + omniauth (1.1.4) + hashie (>= 1.2, < 3) rack - omniauth-github (1.0.3) + omniauth-github (1.1.0) omniauth (~> 1.0) omniauth-oauth2 (~> 1.1) - omniauth-google-oauth2 (0.1.13) + omniauth-google-oauth2 (0.1.19) omniauth (~> 1.0) omniauth-oauth2 omniauth-oauth (1.0.1) @@ -289,92 +309,126 @@ GEM omniauth-oauth2 (1.1.1) oauth2 (~> 0.8.0) omniauth (~> 1.0) - omniauth-twitter (0.0.14) + omniauth-twitter (0.0.17) multi_json (~> 1.3) omniauth-oauth (~> 1.0) orm_adapter (0.4.0) - pg (0.14.1) + pg (0.15.1) + poltergeist (1.4.1) + capybara (~> 2.1.0) + cliver (~> 0.2.1) + multi_json (~> 1.0) + websocket-driver (>= 0.2.0) polyglot (0.3.3) posix-spawn (0.3.6) - progressbar (0.12.0) - pry (0.9.10) + pry (0.9.12.2) coderay (~> 1.0.5) method_source (~> 0.8) - slop (~> 3.3.1) + slop (~> 3.4) + pygments.rb (0.4.2) + posix-spawn (~> 0.3.6) + yajl-ruby (~> 1.1.0) pyu-ruby-sasl (0.0.3.3) - quiet_assets (1.0.1) - railties (~> 3.1) + quiet_assets (1.0.2) + railties (>= 3.1, < 5.0) rack (1.4.5) rack-accept (0.4.5) rack (>= 0.4) rack-cache (1.2) rack (>= 0.4) - rack-mini-profiler (0.1.23) + rack-mini-profiler (0.1.26) rack (>= 1.1.3) rack-mount (0.8.3) rack (>= 1.0.0) - rack-protection (1.3.2) + rack-protection (1.5.0) rack rack-ssl (1.3.3) rack rack-test (0.6.2) rack (>= 1.0) - rails (3.2.12) - actionmailer (= 3.2.12) - actionpack (= 3.2.12) - activerecord (= 3.2.12) - activeresource (= 3.2.12) - activesupport (= 3.2.12) + rails (3.2.13) + actionmailer (= 3.2.13) + actionpack (= 3.2.13) + activerecord (= 3.2.13) + activeresource (= 3.2.13) + activesupport (= 3.2.13) bundler (~> 1.0) - railties (= 3.2.12) + railties (= 3.2.13) rails-dev-tweaks (0.6.1) actionpack (~> 3.1) railties (~> 3.1) - rails_best_practices (1.13.2) + rails_best_practices (1.13.8) activesupport awesome_print code_analyzer colored erubis i18n - progressbar - railties (3.2.12) - actionpack (= 3.2.12) - activesupport (= 3.2.12) + ruby-progressbar + railties (3.2.13) + actionpack (= 3.2.13) + activesupport (= 3.2.13) rack-ssl (~> 1.3.2) rake (>= 0.8.7) rdoc (~> 3.4) thor (>= 0.14.6, < 2.0) - raindrops (0.10.0) - rake (10.0.3) - rb-fsevent (0.9.2) - rb-inotify (0.8.8) + raindrops (0.11.0) + rake (10.1.0) + raphael-rails (2.1.2) + rb-fsevent (0.9.3) + rb-inotify (0.9.0) + ffi (>= 0.5.0) + rb-kqueue (0.2.0) ffi (>= 0.5.0) - rdoc (3.12.1) + rdoc (3.12.2) json (~> 1.4) redcarpet (2.2.2) - redis (3.0.2) - redis-namespace (1.2.1) + redis (3.0.4) + redis-actionpack (3.2.4) + actionpack (~> 3.2.0) + redis-rack (~> 1.4.4) + redis-store (~> 1.1.4) + redis-activesupport (3.2.4) + activesupport (~> 3.2.0) + redis-store (~> 1.1.0) + redis-namespace (1.3.1) redis (~> 3.0.0) - rspec (2.12.0) - rspec-core (~> 2.12.0) - rspec-expectations (~> 2.12.0) - rspec-mocks (~> 2.12.0) - rspec-core (2.12.0) - rspec-expectations (2.12.0) - diff-lcs (~> 1.1.3) - rspec-mocks (2.12.0) - rspec-rails (2.12.0) + redis-rack (1.4.4) + rack (~> 1.4.0) + redis-store (~> 1.1.4) + redis-rails (3.2.4) + redis-actionpack (~> 3.2.4) + redis-activesupport (~> 3.2.4) + redis-store (~> 1.1.4) + redis-store (1.1.4) + redis (>= 2.2) + ref (1.0.5) + rest-client (1.6.7) + mime-types (>= 1.16) + rspec (2.13.0) + rspec-core (~> 2.13.0) + rspec-expectations (~> 2.13.0) + rspec-mocks (~> 2.13.0) + rspec-core (2.13.1) + rspec-expectations (2.13.0) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.13.1) + rspec-rails (2.13.2) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 2.12.0) - rspec-expectations (~> 2.12.0) - rspec-mocks (~> 2.12.0) + rspec-core (~> 2.13.0) + rspec-expectations (~> 2.13.0) + rspec-mocks (~> 2.13.0) + ruby-hmac (0.4.0) + ruby-progressbar (1.2.0) rubyntlm (0.1.1) rubyzip (0.9.9) - sass (3.2.5) - sass-rails (3.2.5) + safe_yaml (0.9.3) + sanitize (2.0.3) + nokogiri (>= 1.4.4, < 1.6) + sass (3.2.9) + sass-rails (3.2.6) railties (~> 3.2.0) sass (>= 3.1.10) tilt (~> 1.3) @@ -384,78 +438,104 @@ GEM seed-fu (2.2.0) activerecord (~> 3.1) activesupport (~> 3.1) - selenium-webdriver (2.26.0) + select2-rails (3.4.2) + sass-rails + thor (~> 0.14) + selenium-webdriver (2.33.0) childprocess (>= 0.2.5) - libwebsocket (~> 0.1.3) multi_json (~> 1.0) rubyzip - settingslogic (2.0.8) - sexp_processor (4.1.3) - shoulda-matchers (1.3.0) + websocket (~> 1.0.4) + settingslogic (2.0.9) + sexp_processor (4.2.1) + shoulda-matchers (2.1.0) activesupport (>= 3.0.0) - sidekiq (2.6.4) - celluloid (~> 0.12.0) - connection_pool (~> 1.0) - multi_json (~> 1) - redis (~> 3) + sidekiq (2.14.0) + celluloid (>= 0.14.1) + connection_pool (>= 1.0.0) + json + redis (>= 3.0.4) redis-namespace + simple_oauth (0.1.9) simplecov (0.7.1) multi_json (~> 1.0) simplecov-html (~> 0.7.1) simplecov-html (0.7.1) - sinatra (1.3.3) - rack (~> 1.3, >= 1.3.6) - rack-protection (~> 1.2) - tilt (~> 1.3, >= 1.3.3) + sinatra (1.4.3) + rack (~> 1.4) + rack-protection (~> 1.4) + tilt (~> 1.3, >= 1.3.4) six (0.2.0) - slim (1.3.6) - temple (~> 0.5.5) - tilt (~> 1.3.3) - slop (3.3.3) - spinach (0.5.2) - colorize - gherkin-ruby (~> 0.2.0) - spinach-rails (0.1.8) - capybara (~> 1) + slim (2.0.0) + temple (~> 0.6.5) + tilt (~> 1.3, >= 1.3.3) + slop (3.4.5) + spinach (0.8.3) + colorize (= 0.5.8) + gherkin-ruby (~> 0.3.0) + spinach-rails (0.2.1) + capybara (>= 2.0.0) railties (>= 3) spinach (>= 0.4) + spork (1.0.0rc2) sprockets (2.2.2) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - stamp (0.3.0) - temple (0.5.5) - test_after_commit (0.0.1) - therubyracer (0.10.2) - libv8 (~> 3.3.10) - thin (1.5.0) + stamp (0.5.0) + state_machine (1.2.0) + stringex (1.5.1) + temple (0.6.5) + test_after_commit (0.2.0) + therubyracer (0.11.4) + libv8 (~> 3.11.8.12) + ref + thin (1.5.1) daemons (>= 1.0.9) eventmachine (>= 0.12.6) rack (>= 1.0.0) - thor (0.17.0) - tilt (1.3.3) - timers (1.0.2) - treetop (1.4.12) + thor (0.18.1) + tilt (1.4.1) + timers (1.1.0) + tinder (1.9.2) + eventmachine (~> 1.0) + faraday (~> 0.8) + faraday_middleware (~> 0.9) + hashie (~> 1.0) + json (~> 1.7.5) + mime-types (~> 1.19) + multi_json (~> 1.5) + twitter-stream (~> 0.1) + treetop (1.4.14) polyglot polyglot (>= 0.3.1) - tzinfo (0.3.35) - uglifier (1.3.0) + turbolinks (1.2.0) + coffee-rails + twitter-stream (0.1.16) + eventmachine (>= 0.12.8) + http_parser.rb (~> 0.5.1) + simple_oauth (~> 0.1.4) + tzinfo (0.3.37) + uglifier (2.1.1) execjs (>= 0.3.0) multi_json (~> 1.0, >= 1.0.2) - unicorn (4.4.0) + underscore-rails (1.4.4) + unicorn (4.6.3) kgio (~> 2.6) rack raindrops (~> 0.7) - virtus (0.5.2) - backports (~> 2.6.1) - warden (1.2.1) + virtus (0.5.5) + backports (~> 3.3) + descendants_tracker (~> 0.0.1) + warden (1.2.3) rack (>= 1.0) - webmock (1.9.0) + webmock (1.11.0) addressable (>= 2.2.7) - crack (>= 0.1.7) - websocket (1.0.2) - xpath (0.1.4) + crack (>= 0.3.2) + websocket (1.0.7) + websocket-driver (0.3.0) + xpath (2.0.0) nokogiri (~> 1.3) yajl-ruby (1.1.0) @@ -463,82 +543,98 @@ PLATFORMS ruby DEPENDENCIES - acts-as-taggable-on (= 2.3.3) + acts-as-taggable-on annotate! + asciidoctor awesome_print better_errors binding_of_caller - bootstrap-sass (= 2.2.1.1) + bootstrap-sass capybara - carrierwave (~> 0.7.1) - chosen-rails (= 0.9.8) - coffee-rails (~> 3.2.2) + carrierwave + chosen-rails (= 1.0.0) + coffee-rails colored - database_cleaner! - devise (~> 2.1.0) - draper (~> 0.18.0) + coveralls + d3_rails (~> 3.1.4) + database_cleaner + devise (~> 2.2) email_spec + enumerize factory_girl_rails ffaker - font-awesome-sass-rails (~> 3.0.0) + fog (~> 1.3.1) + font-awesome-rails foreman gemoji (~> 1.2.1) - git - github-linguist (~> 2.3.4) + github-linguist github-markup (~> 0.7.4) - gitlab_meta (= 5.0) - gitlab_omniauth-ldap (= 1.0.2) - gitlab_yaml_db (= 1.0.0) - grack! - grape (~> 0.2.1) - grit! - grit_ext! + gitlab-gollum-lib (~> 1.0.1) + gitlab-grack (~> 1.0.1) + gitlab-pygments.rb (~> 0.3.2) + gitlab_git (= 2.3.1) + gitlab_meta (= 6.0) + gitlab_omniauth-ldap (= 1.0.3) + gon + grape (~> 0.4.1) + grape-entity (~> 0.3.0) growl guard-rspec guard-spinach - haml-rails (~> 0.3.5) + haml-rails + hipchat (~> 0.9.0) httparty - jquery-atwho-rails (= 0.1.7) + jasmine + jquery-atwho-rails (= 0.3.0) jquery-rails (= 2.1.3) + jquery-turbolinks jquery-ui-rails (= 2.0.2) kaminari (~> 0.14.1) launchy letter_opener + minitest (~> 4.7.0) modernizr (= 2.6.2) mysql2 - omniauth (~> 1.1.1) + omniauth (~> 1.1.3) omniauth-github omniauth-google-oauth2 omniauth-twitter pg - poltergeist! + poltergeist (~> 1.4.1) pry - pygments.rb! quiet_assets (~> 1.0.1) rack-mini-profiler - rails (= 3.2.12) + rails (= 3.2.13) rails-dev-tweaks rails_best_practices - raphael-rails! + raphael-rails (~> 2.1.2) rb-fsevent rb-inotify redcarpet (~> 2.2.2) + redis-rails rspec-rails - sass-rails (~> 3.2.5) + sanitize + sass-rails sdoc seed-fu + select2-rails settingslogic - shoulda-matchers (= 1.3.0) - sidekiq (= 2.6.4) + shoulda-matchers (~> 2.1.0) + sidekiq simplecov sinatra six slim spinach-rails + spork (~> 1.0rc) stamp + state_machine test_after_commit therubyracer thin - uglifier (~> 1.3.0) - unicorn (~> 4.4.0) + tinder (~> 1.9.2) + turbolinks + uglifier + underscore-rails (~> 1.4.4) + unicorn (~> 4.6.3) webmock diff --git a/MAINTENANCE.md b/MAINTENANCE.md new file mode 100644 index 00000000000..0dca62974c3 --- /dev/null +++ b/MAINTENANCE.md @@ -0,0 +1,23 @@ +# GitLab Maintenance Policy + +GitLab is a fast moving and evolving project. We currently don't have the +resources to support many releases concurrently. We support exactly one stable +release at any given time. + +GitLab follows the [Semantic Versioning](http://semver.org/) for its releases: +`(Major).(Minor).(Patch)`. + +* **Major version**: Whenever there is something significant or any backwards + incompatible changes are introduced to the public API. +* **Minor version**: When new, backwards compatible functionality is introduced + to the public API or a minor feature is introduced, or when a set of smaller + features is rolled out. +* **Patch number**: When backwards compatible bug fixes are introduced that fix + incorrect behavior. + +The current stable release will receive security patches and bug fixes +(eg. `5.0` -> `5.0.1`). Feature releases will mark the next supported stable +release where the minor version is increased numerically by increments of one +(eg. `5.0 -> 5.1`). + +We encourage everyone to run the latest stable release to ensure that you can easily upgrade to the most secure and feature rich GitLab experience. In order to make sure you can easily run the most recent stable release, we are working hard to keep the update process simple and reliable. diff --git a/PROCESS.md b/PROCESS.md new file mode 100644 index 00000000000..668cacc870a --- /dev/null +++ b/PROCESS.md @@ -0,0 +1,105 @@ +# GitLab Contributing Process + +## Purpose of describing the contributing process + +Below we describe the contributing process to GitLab for two reasons. So that contributors know what to expect from maintainers (possible responses, friendly treatment, etc.). And so that maintainers know what to expect from contributors (use the latest version, ensure that the issue is addressed, friendly treatment, etc.). + +## Common actions + +### Issue team +- Looks for issues without workflow labels and triages issue +- Monitors pull requests +- Closes invalid issues and pull requests with a comment (duplicates, [feature requests](#feature-requests), [fixed in newer version](#issue-fixed-in-newer-version), [issue report for old version](#issue-report-for-old-version), not a problem in GitLab, etc.) +- Assigns appropriate [labels](#how-we-handle-issues) +- Asks for feedback from issue reporter/pull request initiator ([invalid issue reports](#improperly-formatted-issue), [format code](#code-format), etc.) +- Asks for feedback from the relevant developer(s) based on the [list of members and their specialities](http://gitlab.org/team/) +- Monitors all issues/pull requests for feedback (but especially ones commented on since automatically watching them): +- Closes issues with no feedback from the reporter for two weeks +- Closes stale pull requests + +### Development team + +- Responds to issues and pull requests the issue team mentions them in +- Monitors for new issues in _Awaiting developer action/feedback_ with no developer activity (once a week) +- Monitors for new pull requests (at least once a week) +- Manages their work queue by looking at issues and pull requests assigned to them +- Close fixed issues (via commit messages or manually) +- Codes [new features](http://feedback.gitlab.com/forums/176466-general/filters/top)! +- Response guidelines +- Be kind to people trying to contribute. Be aware that people can be a non-native or a native English speaker, they might not understand thing or they might be very sensitive to how your word things. Use emoji to express your feelings (heart, star, smile, etc.). Some good tips about giving feedback to pull requests is in the [Thoughtbot code review guide](https://github.com/thoughtbot/guides/tree/master/code-review). + +## Priorities of the issue team + +1. Mentioning people (critical) +2. Workflow labels (normal) +3. Functional labels (minor) +4. Assigning issues (avoid if possible) + +## 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](http://gitlab.org/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 + +Workflow labels are purposely not very detailed since that would be hard to keep updated as you would need to reevaluate them after every comment. We optionally use functional labels on demand when want to group related issues to get an overview (for example all issues related to RVM, to tackle them in one go) and to add details to the issue. + +- _Awaiting feedback_: Feedback pending from the reporter +- _Awaiting confirmation of fix_: The issue should already be solved in **master** (generally you can avoid this workflow item and just close the issue right away) +- _Attached PR_: There is a PR attached and the discussion should happen there + - We need to let issues stay in sync with the PR's. We can do this with a "Closing #XXXX" or "Fixes #XXXX" comment in the PR. We can't close the issue when there is a pull request because sometimes a PR is not good and we just close the PR, then the issue must stay. +- _Awaiting developer action/feedback_: Issue needs to be fixed or clarified by a developer + +## Functional labels + +These labels describe what development specialities are involved such as: PostgreSQL, UX, LDAP. + +## Assigning issues + +If an issue is complex and needs the attention of a specific person, assignment is a good option but assigning issues might discourage other people from contributing to that issue. We need all the contributions we can get so this should never be discouraged. Also, an assigned person might not have time for a few weeks, so others should feel free to takeover. + +## Label colors +- Light orange `#fef2c0`: workflow labels for issue team members (awaiting feedback, awaiting confirmation of fix) +- Bright orange `#eb6420`: workflow labels for core team members (attached PR, awaiting developer action/feedback) +- Light blue `#82C5FF`: functional labels +- Green labels `#009800`: issues that can generally be ignored. For example, issues given the following labels normally can be closed immediately: + - Feature request (see copy & paste response: [Feature requests](#feature-requests)) + - Support (see copy & paste response: [Support requests and configuration questions](#support-requests-and-configuration-questions) + +## Copy & paste responses + +### Improperly formatted issue + +Thanks for the issue report. Please reformat your issue to conform to the issue tracker guidelines found in our \[contributing guidelines\]\(https://github.com/gitlabhq/gitlabhq/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). + +### Feature requests + +Thanks for your interest in GitLab. We don't use the GitHub issue tracker for feature requests. Please use http://feedback.gitlab.com/ for this purpose or create a pull request implementing this feature. Have a look at the \[contribution guidelines\]\(https://github.com/gitlabhq/gitlabhq/blob/master/CONTRIBUTING.md) for more information. + +### Issue report for old version + +Thanks for the issue report but we only support issues for the latest stable version of GitLab. I'm closing this issue but if you still experience this problem in the latest stable version, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://github.com/gitlabhq/gitlabhq/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). + +### Support requests and configuration questions + +Thanks for your interest in GitLab. We don't use the GitHub 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 unofficial #gitlab IRC channel on Freenode or the http://www.gitlab.com paid services for this purpose. Have a look at the \[contribution guidelines\]\(https://github.com/gitlabhq/gitlabhq/blob/master/CONTRIBUTING.md) for more information. + +### Code format + +Please use ``` to format console output, logs, and code as it's very hard to read otherwise. + +### Issue fixed in newer version + +Thanks for the issue report. This issue has already been fixed in newer versions of GitLab. Due to the size of this project and our limited resources we are only able to support the latest stable release as outlined in our \[contributing guidelines\]\(https://github.com/gitlabhq/gitlabhq/blob/master/CONTRIBUTING.md#issue-tracker). In order to get this bug fix and enjoy many new features please \[upgrade\]\(http://blog.gitlab.org/). If you still experience issues at that time please open a new issue following our issue tracker guidelines found in the \[contributing guidelines\]\(https://github.com/gitlabhq/gitlabhq/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). + +### Improperly formatted pull request + +Thanks for your interest in improving the GitLab codebase! Please update your pull request according to the \[contributing guidelines\]\(https://github.com/gitlabhq/gitlabhq/blob/master/CONTRIBUTING.md#pull-request-guidelines). + +### Inactivity close of an issue + +It's been at least 2 weeks (and a new release) since we heard from you. I'm closing this issue but if you still experience this problem, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://github.com/gitlabhq/gitlabhq/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). + +### Inactivity close of a pull request + +This pull request has been closed because a request for more information has not been reacted to for more than 2 weeks. If you respond and conform to the pull request guidelines in our \[contributing guidelines\]\(https://github.com/gitlabhq/gitlabhq/blob/master/CONTRIBUTING.md#pull-requests) we will reopen this pull request. + @@ -1,2 +1,2 @@ -web: bundle exec unicorn_rails -p $PORT +web: bundle exec unicorn_rails -p $PORT -E development worker: bundle exec sidekiq -q post_receive,mailer,system_hook,project_web_hook,common,default,gitlab_shell diff --git a/README.md b/README.md index ee029f9bfcb..ce0f4a8a1c5 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,166 @@ -# Welcome to GitLab [](https://travis-ci.org/gitlabhq/gitlabhq) [](https://travis-ci.org/gitlabhq/grit) [](https://codeclimate.com/github/gitlabhq/gitlabhq) [](https://gemnasium.com/gitlabhq/gitlabhq) +## GitLab: self hosted Git management software -GitLab is a free project and repository management application + -[](http://ci.gitlab.org/projects/1?ref=master) + -## Application details +### GitLab allows you to + * keep your code secure on your own server + * manage repositories, users and access permissions + * communicate through issues, line-comments and wiki pages + * perform code review with merge requests -* based on Ruby on Rails -* distributed under the MIT License -* works with gitolite +### GitLab is -## Requirements +* powered by Ruby on Rails +* completely free and open source (MIT license) +* used by more than 25.000 organizations to keep their code secure -* Ubuntu/Debian +### Code status + +* [](http://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch) + +* [](https://codeclimate.com/github/gitlabhq/gitlabhq) + +* [](https://gemnasium.com/gitlabhq/gitlabhq) this button can be yellow (small updates are available) but must not be red (a security fix or an important update is available), gems are updated in major releases of GitLab. + +* [](https://coveralls.io/r/gitlabhq/gitlabhq) + +### Resources + +* GitLab.org community site: [Homepage](http://gitlab.org) | [Screenshots](http://gitlab.org/screenshots/) | [Blog](http://blog.gitlab.org/) | [Demo](http://demo.gitlabhq.com/users/sign_in) + +* GitLab.com commercial services: [Homepage](http://www.gitlab.com/) | [Subscription](http://www.gitlab.com/subscription/) | [Consultancy](http://www.gitlab.com/consultancy/) | [GitLab Cloud](http://www.gitlab.com/cloud/) | [Blog](http://blog.gitlab.com/) + +* GitLab CI: [Readme](https://github.com/gitlabhq/gitlab-ci/blob/master/README.md) of the GitLab open-source continuous integration server + +### Requirements + +* Ubuntu/Debian** * ruby 1.9.3+ -* MySQL -* git -* gitolite -* redis +* git 1.7.10+ +* redis 2.0+ +* MySQL or PostgreSQL + +** More details are in the [requirements doc](doc/install/requirements.md) + +### Installation + +#### Official production installation + +* [Installation guide for a production server](doc/install/installation.md) + + +#### Official development installation + +If you want to contribute, please first read our [Contributing Guidelines](https://github.com/gitlabhq/gitlabhq/blob/master/CONTRIBUTING.md) and then we suggest you to use the Vagrant virtual machine project to get an environment working with all dependencies. + +* [Vagrant virtual machine for development](https://github.com/gitlabhq/gitlab-vagrant-vm) + + +#### Unofficial production installations + +* [GitLab recipes](https://github.com/gitlabhq/gitlab-recipes) repository with unofficial guides for using GitLab with different software (operating systems, webservers, etc.) than the official version. + +* [Installation guides](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Unofficial-Installation-Guides) public wiki with unofficial guides to install GitLab on different operating systems. + +* [BitNami one-click installers](http://bitnami.com/stack/gitlab) + +* [TurnKey Linux virtual appliance](http://www.turnkeylinux.org/gitlab) + + +### New versions and upgrading + +Since 2011 GitLab is released on the 22nd of every month. Every new release includes an upgrade guide. + +* [Upgrade guides](doc/update) + +* [Changelog](CHANGELOG) + +* Features that will be in the next releases are listed on [the feedback and suggestions 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). + + +### 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 + +### 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 + bundle exec rake sidekiq:start + +### Run the tests + +* Seed the database + + bundle exec rake db:setup RAILS_ENV=test + bundle exec rake db:seed_fu RAILS_ENV=test + +* Run all tests + + bundle exec rake gitlab: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 + + +### GitLab interfaces + +* [GitLab API](doc/api/README.md) + +* [Rake tasks](doc/raketasks) + +* [Directory structure](doc/install/structure.md) + +* [Databases](doc/install/databases.md) + + +### Getting help + +* [Maintenance policy](MAINTENANCE.md) specifies what versions are supported. + +* [Troubleshooting guide](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Trouble-Shooting-Guide) contains solutions to common problems. + +* [Mailing list](https://groups.google.com/forum/#!forum/gitlabhq) and [Stack Overflow](http://stackoverflow.com/questions/tagged/gitlab) are the best places to ask questions. For example you can use it if you have questions about: permission denied errors, invisible repos, can't clone/pull/push or with web hooks that don't fire. Please search for similar issues before posting your own, there's a good chance somebody else had the same issue you have now and has resolved it. There are a lot of helpful GitLab users there who may be able to help you quickly. If your particular issue turns out to be a bug, it will find its way from there to a fix. -## Install +* [Unofficial #gitlab IRC on Freenode](http://www.freenode.net/) is another way to get in touch with other GitLab users who may be able to help you. -Checkout wiki pages for installation information, migration, etc. +* [Feedback and suggestions forum](http://feedback.gitlab.com) is the place to propose and discuss new features for GitLab. -## Community +* [Contributing guide](https://github.com/gitlabhq/gitlabhq/blob/master/CONTRIBUTING.md) describes how to submit pull requests and issues. Pull requests and issues not in line with the guidelines in this document will be closed. -[Google Group](https://groups.google.com/group/gitlabhq) +* [Support subscription](http://www.gitlab.com/subscription/) connects you to the knowledge of GitLab experts that will resolve your issues and answer your questions. -## Contacts +* [Consultancy](http://www.gitlab.com/consultancy/) allows you hire GitLab experts for installations, upgrades and customizations. -Twitter: - * @gitlabhq - * @dzaporozhets +### Getting in touch -Email +* [Core team](https://github.com/gitlabhq?tab=members) - * m@gitlabhq.com +* [Contributors](https://github.com/gitlabhq/gitlabhq/graphs/contributors) -## Contribute +* [Leader](https://github.com/randx) -[Developer Guide](https://github.com/gitlabhq/gitlabhq/wiki/Developer-Guide) -Want to help - send a pull request. -We'll accept good pull requests. +* [Contact page](http://gitlab.org/contact/) diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index d148b518b0e..00000000000 --- a/ROADMAP.md +++ /dev/null @@ -1,12 +0,0 @@ -## GitLab Roadmap - -### v5.0 March 22 - -* Replace gitolite with gitlab-shell -* Usability improvements -* Notification improvements - -### v4.2 February 22 - -* Teams - @@ -1 +1 @@ -5.0.0pre +6.2.0.pre diff --git a/app/assets/fonts/OFL.txt b/app/assets/fonts/OFL.txt deleted file mode 100644 index 3ce219f0126..00000000000 --- a/app/assets/fonts/OFL.txt +++ /dev/null @@ -1,92 +0,0 @@ -Copyright (c) 2010, Jan Gerner (post@yanone.de)
-This Font Software is licensed under the SIL Open Font License, Version 1.1.
-This license is copied below, and is also available with a FAQ at:
-http://scripts.sil.org/OFL
-
-
------------------------------------------------------------
-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
------------------------------------------------------------
-
-PREAMBLE
-The goals of the Open Font License (OFL) are to stimulate worldwide
-development of collaborative font projects, to support the font creation
-efforts of academic and linguistic communities, and to provide a free and
-open framework in which fonts may be shared and improved in partnership
-with others.
-
-The OFL allows the licensed fonts to be used, studied, modified and
-redistributed freely as long as they are not sold by themselves. The
-fonts, including any derivative works, can be bundled, embedded,
-redistributed and/or sold with any software provided that any reserved
-names are not used by derivative works. The fonts and derivatives,
-however, cannot be released under any other type of license. The
-requirement for fonts to remain under this license does not apply
-to any document created using the fonts or their derivatives.
-
-DEFINITIONS
-"Font Software" refers to the set of files released by the Copyright
-Holder(s) under this license and clearly marked as such. This may
-include source files, build scripts and documentation.
-
-"Reserved Font Name" refers to any names specified as such after the
-copyright statement(s).
-
-"Original Version" refers to the collection of Font Software components as
-distributed by the Copyright Holder(s).
-
-"Modified Version" refers to any derivative made by adding to, deleting,
-or substituting -- in part or in whole -- any of the components of the
-Original Version, by changing formats or by porting the Font Software to a
-new environment.
-
-"Author" refers to any designer, engineer, programmer, technical
-writer or other person who contributed to the Font Software.
-
-PERMISSION & CONDITIONS
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of the Font Software, to use, study, copy, merge, embed, modify,
-redistribute, and sell modified and unmodified copies of the Font
-Software, subject to the following conditions:
-
-1) Neither the Font Software nor any of its individual components,
-in Original or Modified Versions, may be sold by itself.
-
-2) Original or Modified Versions of the Font Software may be bundled,
-redistributed and/or sold with any software, provided that each copy
-contains the above copyright notice and this license. These can be
-included either as stand-alone text files, human-readable headers or
-in the appropriate machine-readable metadata fields within text or
-binary files as long as those fields can be easily viewed by the user.
-
-3) No Modified Version of the Font Software may use the Reserved Font
-Name(s) unless explicit written permission is granted by the corresponding
-Copyright Holder. This restriction only applies to the primary font name as
-presented to the users.
-
-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
-Software shall not be used to promote, endorse or advertise any
-Modified Version, except to acknowledge the contribution(s) of the
-Copyright Holder(s) and the Author(s) or with their explicit written
-permission.
-
-5) The Font Software, modified or unmodified, in part or in whole,
-must be distributed entirely under this license, and must not be
-distributed under any other license. The requirement for fonts to
-remain under this license does not apply to any document created
-using the Font Software.
-
-TERMINATION
-This license becomes null and void if any of the above conditions are
-not met.
-
-DISCLAIMER
-THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
-OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/app/assets/fonts/YanoneKaffeesatz-Light.ttf b/app/assets/fonts/YanoneKaffeesatz-Light.ttf Binary files differdeleted file mode 100644 index 5026d3bdbe2..00000000000 --- a/app/assets/fonts/YanoneKaffeesatz-Light.ttf +++ /dev/null diff --git a/app/assets/images/dark.png b/app/assets/images/dark-scheme-preview.png Binary files differindex 055a9069b63..055a9069b63 100644 --- a/app/assets/images/dark.png +++ b/app/assets/images/dark-scheme-preview.png diff --git a/app/assets/images/login-logo.png b/app/assets/images/login-logo.png Binary files differdeleted file mode 100644 index 8c064b12dd0..00000000000 --- a/app/assets/images/login-logo.png +++ /dev/null diff --git a/app/assets/images/logo-black.png b/app/assets/images/logo-black.png Binary files differnew file mode 100644 index 00000000000..6567f2e5463 --- /dev/null +++ b/app/assets/images/logo-black.png diff --git a/app/assets/images/logo-white.png b/app/assets/images/logo-white.png Binary files differnew file mode 100644 index 00000000000..a63fb1c9c0a --- /dev/null +++ b/app/assets/images/logo-white.png diff --git a/app/assets/images/logo_dark.png b/app/assets/images/logo_dark.png Binary files differdeleted file mode 100644 index 4a3e3391599..00000000000 --- a/app/assets/images/logo_dark.png +++ /dev/null diff --git a/app/assets/images/logo_white.png b/app/assets/images/logo_white.png Binary files differdeleted file mode 100644 index e3415816558..00000000000 --- a/app/assets/images/logo_white.png +++ /dev/null diff --git a/app/assets/images/merge.png b/app/assets/images/merge.png Binary files differdeleted file mode 100644 index 4a6bb2e154d..00000000000 --- a/app/assets/images/merge.png +++ /dev/null diff --git a/app/assets/images/monokai-scheme-preview.png b/app/assets/images/monokai-scheme-preview.png Binary files differnew file mode 100644 index 00000000000..9477941778e --- /dev/null +++ b/app/assets/images/monokai-scheme-preview.png diff --git a/app/assets/images/solarized-dark-scheme-preview.png b/app/assets/images/solarized-dark-scheme-preview.png Binary files differnew file mode 100644 index 00000000000..728964bc4c8 --- /dev/null +++ b/app/assets/images/solarized-dark-scheme-preview.png diff --git a/app/assets/images/white.png b/app/assets/images/white-scheme-preview.png Binary files differindex 67eb8763044..67eb8763044 100644 --- a/app/assets/images/white.png +++ b/app/assets/images/white-scheme-preview.png diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee new file mode 100644 index 00000000000..fdefbfb92bd --- /dev/null +++ b/app/assets/javascripts/activities.js.coffee @@ -0,0 +1,31 @@ +class Activities + constructor: -> + Pager.init 20, true + $(".event_filter_link").bind "click", (event) => + event.preventDefault() + @toggleFilter($(event.currentTarget)) + @reloadActivities() + + reloadActivities: -> + $(".content_list").html '' + Pager.init 20, true + + + toggleFilter: (sender) -> + sender.parent().toggleClass "inactive" + event_filters = $.cookie("event_filter") + filter = sender.attr("id").split("_")[0] + if event_filters + event_filters = event_filters.split(",") + else + event_filters = new Array() + + index = event_filters.indexOf(filter) + if index is -1 + event_filters.push filter + else + 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 1dafdf4bd8b..6230fe7f93f 100644 --- a/app/assets/javascripts/admin.js.coffee +++ b/app/assets/javascripts/admin.js.coffee @@ -1,17 +1,38 @@ -$ -> - $('input#user_force_random_password').on 'change', (elem) -> - elems = $('#user_password, #user_password_confirmation') - - if $(@).attr 'checked' - elems.val('').attr 'disabled', true - else - elems.removeAttr 'disabled' - - $('.log-tabs a').click (e) -> - e.preventDefault() - $(this).tab('show') - - $('.log-bottom').click (e) -> - e.preventDefault() - visible_log = $(".file_content:visible") - visible_log.animate({ scrollTop: visible_log.find('ol').height() }, "fast") +class Admin + constructor: -> + $('input#user_force_random_password').on 'change', (elem) -> + elems = $('#user_password, #user_password_confirmation') + + if $(@).attr 'checked' + elems.val('').attr 'disabled', true + else + elems.removeAttr 'disabled' + + $('.log-tabs a').click (e) -> + e.preventDefault() + $(this).tab('show') + + $('.log-bottom').click (e) -> + e.preventDefault() + visible_log = $(".file-content:visible") + visible_log.animate({ scrollTop: visible_log.find('ol').height() }, "fast") + + modal = $('.change-owner-holder') + + $('.change-owner-link').bind "click", (e) -> + e.preventDefault() + $(this).hide() + modal.show() + + $('.change-owner-cancel-link').bind "click", (e) -> + e.preventDefault() + modal.hide() + $('.change-owner-link').show() + + $('li.users_project').bind 'ajax:success', -> + Turbolinks.visit(location.href) + + $('li.users_group').bind 'ajax:success', -> + Turbolinks.visit(location.href) + +@Admin = Admin diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee new file mode 100644 index 00000000000..db80e7b0f3c --- /dev/null +++ b/app/assets/javascripts/api.js.coffee @@ -0,0 +1,54 @@ +@Api = + users_path: "/api/:version/users.json" + user_path: "/api/:version/users/:id.json" + notes_path: "/api/:version/projects/:id/notes.json" + + # Get 20 (depends on api) recent notes + # and sort the ascending from oldest to newest + notes: (project_id, callback) -> + url = Api.buildUrl(Api.notes_path) + url = url.replace(':id', project_id) + + $.ajax( + url: url, + data: + private_token: gon.api_token + gfm: true + recent: true + dataType: "json" + ).done (notes) -> + notes.sort (a, b) -> + return a.id - b.id + callback(notes) + + user: (user_id, callback) -> + url = Api.buildUrl(Api.user_path) + url = url.replace(':id', user_id) + + $.ajax( + url: url + data: + private_token: gon.api_token + dataType: "json" + ).done (user) -> + callback(user) + + # Return users list. Filtered by query + # Only active users retrieved + users: (query, callback) -> + url = Api.buildUrl(Api.users_path) + + $.ajax( + url: url + data: + private_token: gon.api_token + search: query + per_page: 20 + active: true + dataType: "json" + ).done (users) -> + callback(users) + + buildUrl: (url) -> + url = gon.relative_url_root + url if gon.relative_url_root? + return url.replace(':version', gon.api_version) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 49effdf9c15..0767b82032d 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,12 +14,18 @@ //= require jquery.waitforimages //= require jquery.atwho //= require jquery.scrollto +//= require jquery.blockUI +//= require turbolinks +//= require jquery.turbolinks //= require bootstrap //= require modernizr //= require chosen-jquery +//= require select2 //= require raphael //= require g.raphael-min //= require g.bar-min //= require branch-graph //= require ace-src-noconflict/ace //= require_tree . +//= require d3 +//= require underscore diff --git a/app/assets/javascripts/behaviors/toggler_behavior.coffee b/app/assets/javascripts/behaviors/toggler_behavior.coffee index 3fefbf8e121..7e438c51c1c 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.coffee +++ b/app/assets/javascripts/behaviors/toggler_behavior.coffee @@ -3,3 +3,15 @@ $ -> container = $(@).closest(".js-toggler-container") container.toggleClass("on") + + $("body").on "click", ".js-toggle-visibility-link", (e) -> + $(@).find('i'). + toggleClass('icon-chevron-down'). + toggleClass('icon-chevron-up') + container = $(".js-toggle-visibility-container") + container.toggleClass("hide") + e.preventDefault() + + $("body").on "click", ".js-toggle-button", (e) -> + $(@).closest(".js-toggle-container").find(".js-toggle-content").toggle() + e.preventDefault() diff --git a/app/assets/javascripts/blob.js.coffee b/app/assets/javascripts/blob.js.coffee new file mode 100644 index 00000000000..03e280f8976 --- /dev/null +++ b/app/assets/javascripts/blob.js.coffee @@ -0,0 +1,24 @@ +class BlobView + constructor: -> + # See if there are lines selected + # "#L12" and "#L34-56" supported + highlightBlobLines = -> + if window.location.hash isnt "" + matches = window.location.hash.match(/\#L(\d+)(\-(\d+))?/) + first_line = parseInt(matches?[1]) + last_line = parseInt(matches?[3]) + + unless isNaN first_line + last_line = first_line if isNaN(last_line) + $("#tree-content-holder .highlight .line").removeClass("hll") + $("#LC#{line}").addClass("hll") for line in [first_line..last_line] + $("#L#{first_line}").ScrollTo() + + # Highlight the correct lines on load + highlightBlobLines() + + # Highlight the correct lines when the hash part of the URL changes + $(window).on 'hashchange', highlightBlobLines + + +@BlobView = BlobView diff --git a/app/assets/javascripts/branch-graph.js.coffee b/app/assets/javascripts/branch-graph.js.coffee new file mode 100644 index 00000000000..318538509a5 --- /dev/null +++ b/app/assets/javascripts/branch-graph.js.coffee @@ -0,0 +1,326 @@ +class BranchGraph + constructor: (@element, @options) -> + @preparedCommits = {} + @mtime = 0 + @mspace = 0 + @parents = {} + @colors = ["#000"] + @offsetX = 150 + @offsetY = 20 + @unitTime = 30 + @unitSpace = 10 + @prev_start = -1 + @load() + + load: -> + $.ajax + url: @options.url + method: "get" + dataType: "json" + success: $.proxy((data) -> + $(".loading", @element).hide() + @prepareData data.days, data.commits + @buildGraph() + , this) + + prepareData: (@days, @commits) -> + @collectParents() + @graphHeight = $(@element).height() + @graphWidth = $(@element).width() + ch = Math.max(@graphHeight, @offsetY + @unitTime * @mtime + 150) + cw = Math.max(@graphWidth, @offsetX + @unitSpace * @mspace + 300) + @r = Raphael(@element.get(0), cw, ch) + @top = @r.set() + @barHeight = Math.max(@graphHeight, @unitTime * @days.length + 320) + + for c in @commits + c.isParent = true if c.id of @parents + @preparedCommits[c.id] = c + @markCommit(c) + + @collectColors() + + collectParents: -> + for c in @commits + @mtime = Math.max(@mtime, c.time) + @mspace = Math.max(@mspace, c.space) + for p in c.parents + @parents[p[0]] = true + @mspace = Math.max(@mspace, p[1]) + + collectColors: -> + k = 0 + while k < @mspace + @colors.push Raphael.getColor(.8) + # Skipping a few colors in the spectrum to get more contrast between colors + Raphael.getColor() + Raphael.getColor() + k++ + + buildGraph: -> + r = @r + cuday = 0 + cumonth = "" + + r.rect(0, 0, 40, @barHeight).attr fill: "#222" + r.rect(40, 0, 30, @barHeight).attr fill: "#444" + + for day, mm in @days + if cuday isnt day[0] + # Dates + r.text(55, @offsetY + @unitTime * mm, day[0]) + .attr( + font: "12px Monaco, monospace" + fill: "#BBB" + ) + cuday = day[0] + + if cumonth isnt day[1] + # Months + r.text(20, @offsetY + @unitTime * mm, day[1]) + .attr( + font: "12px Monaco, monospace" + fill: "#EEE" + ) + cumonth = day[1] + + @renderPartialGraph() + + @bindEvents() + + renderPartialGraph: -> + start = Math.floor((@element.scrollTop() - @offsetY) / @unitTime) - 10 + start = 0 if start < 0 + end = start + 40 + end = @commits.length if @commits.length < end + + if @prev_start == -1 or Math.abs(@prev_start - start) > 10 + i = start + + @prev_start = start + + while i < end + commit = @commits[i] + i += 1 + + if commit.hasDrawn isnt true + x = @offsetX + @unitSpace * (@mspace - commit.space) + y = @offsetY + @unitTime * commit.time + + @drawDot(x, y, commit) + + @drawLines(x, y, commit) + + @appendLabel(x, y, commit) + + @appendAnchor(x, y, commit) + + commit.hasDrawn = true + + @top.toFront() + + bindEvents: -> + drag = {} + element = @element + + $(element).scroll (event) => + @renderPartialGraph() + + $(window).on + keydown: (event) => + # left + element.scrollLeft element.scrollLeft() - 50 if event.keyCode is 37 + # top + element.scrollTop element.scrollTop() - 50 if event.keyCode is 38 + # right + element.scrollLeft element.scrollLeft() + 50 if event.keyCode is 39 + # bottom + element.scrollTop element.scrollTop() + 50 if event.keyCode is 40 + @renderPartialGraph() + + appendLabel: (x, y, commit) -> + return unless commit.refs + + r = @r + shortrefs = commit.refs + # Truncate if longer than 15 chars + shortrefs = shortrefs.substr(0, 15) + "…" if shortrefs.length > 17 + text = r.text(x + 4, y, shortrefs).attr( + "text-anchor": "start" + font: "10px Monaco, monospace" + fill: "#FFF" + title: commit.refs + ) + textbox = text.getBBox() + # Create rectangle based on the size of the textbox + rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr( + fill: "#000" + "fill-opacity": .5 + stroke: "none" + ) + triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr( + fill: "#000" + "fill-opacity": .5 + stroke: "none" + ) + + label = r.set(rect, text) + label.transform(["t", -rect.getBBox().width - 15, 0]) + + # Set text to front + text.toFront() + + appendAnchor: (x, y, commit) -> + r = @r + top = @top + options = @options + anchor = r.circle(x, y, 10).attr( + fill: "#000" + opacity: 0 + cursor: "pointer" + ).click(-> + window.open options.commit_url.replace("%s", commit.id), "_blank" + ).hover(-> + @tooltip = r.commitTooltip(x + 5, y, commit) + top.push @tooltip.insertBefore(this) + , -> + @tooltip and @tooltip.remove() and delete @tooltip + ) + top.push anchor + + drawDot: (x, y, commit) -> + r = @r + r.circle(x, y, 3).attr( + fill: @colors[commit.space] + stroke: "none" + ) + r.rect(@offsetX + @unitSpace * @mspace + 10, y - 10, 20, 20).attr( + fill: "url(#{commit.author.icon})" + stroke: @colors[commit.space] + "stroke-width": 2 + ) + r.text(@offsetX + @unitSpace * @mspace + 35, y, commit.message.split("\n")[0]).attr( + "text-anchor": "start" + font: "14px Monaco, monospace" + ) + + drawLines: (x, y, commit) -> + r = @r + for parent, i in commit.parents + parentCommit = @preparedCommits[parent[0]] + parentY = @offsetY + @unitTime * parentCommit.time + parentX1 = @offsetX + @unitSpace * (@mspace - parentCommit.space) + parentX2 = @offsetX + @unitSpace * (@mspace - parent[1]) + + # Set line color + if parentCommit.space <= commit.space + color = @colors[commit.space] + + else + color = @colors[parentCommit.space] + + # Build line shape + if parent[1] is commit.space + offset = [0, 5] + arrow = "l-2,5,4,0,-2,-5,0,5" + + else if parent[1] < commit.space + offset = [3, 3] + arrow = "l5,0,-2,4,-3,-4,4,2" + + else + offset = [-3, 3] + arrow = "l-5,0,2,4,3,-4,-4,2" + + # Start point + route = ["M", x + offset[0], y + offset[1]] + + # Add arrow if not first parent + if i > 0 + route.push(arrow) + + # Circumvent if overlap + if commit.space isnt parentCommit.space or commit.space isnt parent[1] + route.push( + "L", parentX2, y + 10, + "L", parentX2, parentY - 5, + ) + + # End point + route.push("L", parentX1, parentY) + + r + .path(route) + .attr( + stroke: color + "stroke-width": 2) + + markCommit: (commit) -> + if commit.id is @options.commit_id + r = @r + x = @offsetX + @unitSpace * (@mspace - commit.space) + y = @offsetY + @unitTime * commit.time + r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr( + fill: "#000" + "fill-opacity": .5 + stroke: "none" + ) + # Displayed in the center + @element.scrollTop(y - @graphHeight / 2) + +Raphael::commitTooltip = (x, y, commit) -> + boxWidth = 300 + boxHeight = 200 + icon = @image(commit.author.icon, x, y, 20, 20) + nameText = @text(x + 25, y + 10, commit.author.name) + idText = @text(x, y + 35, commit.id) + messageText = @text(x, y + 50, commit.message) + textSet = @set(icon, nameText, idText, messageText).attr( + "text-anchor": "start" + font: "12px Monaco, monospace" + ) + nameText.attr( + font: "14px Arial" + "font-weight": "bold" + ) + + idText.attr fill: "#AAA" + @textWrap messageText, boxWidth - 50 + rect = @rect(x - 10, y - 10, boxWidth, 100, 4).attr( + fill: "#FFF" + stroke: "#000" + "stroke-linecap": "round" + "stroke-width": 2 + ) + tooltip = @set(rect, textSet) + rect.attr( + height: tooltip.getBBox().height + 10 + width: tooltip.getBBox().width + 10 + ) + + tooltip.transform ["t", 20, 20] + tooltip + +Raphael::textWrap = (t, width) -> + content = t.attr("text") + abc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + t.attr text: abc + letterWidth = t.getBBox().width / abc.length + t.attr text: content + words = content.split(" ") + x = 0 + s = [] + + for word in words + if x + (word.length * letterWidth) > width + s.push "\n" + x = 0 + x += word.length * letterWidth + s.push word + " " + + t.attr text: s.join("") + b = t.getBBox() + h = Math.abs(b.y2) - Math.abs(b.y) + 1 + t.attr y: b.y + h + +@BranchGraph = BranchGraph diff --git a/app/assets/javascripts/chart.js.coffee b/app/assets/javascripts/chart.js.coffee new file mode 100644 index 00000000000..989f48e5e75 --- /dev/null +++ b/app/assets/javascripts/chart.js.coffee @@ -0,0 +1,21 @@ +@Chart = + labels: [] + values: [] + + init: (labels, values, title) -> + r = Raphael('activity-chart') + + fin = -> + @flag = r.popup(@bar.x, @bar.y, @bar.value or "0").insertBefore(this) + + fout = -> + @flag.animate + opacity: 0, 300, -> @remove() + + r.text(160, 10, title).attr font: "13px sans-serif" + r.barchart( + 10, 20, 560, 200, + [values], + {colors:["#456"]} + ).label(labels, true) + .hover(fin, fout) diff --git a/app/assets/javascripts/commit.js.coffee b/app/assets/javascripts/commit.js.coffee new file mode 100644 index 00000000000..9f55a1e6368 --- /dev/null +++ b/app/assets/javascripts/commit.js.coffee @@ -0,0 +1,6 @@ +class Commit + constructor: -> + $('.files .file').each -> + new CommitFile(this) + +@Commit = Commit diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee index 47d6fcf8089..de4c06a2728 100644 --- a/app/assets/javascripts/commits.js.coffee +++ b/app/assets/javascripts/commits.js.coffee @@ -23,7 +23,7 @@ class CommitsList @data.offset = limit this.initLoadMore() - this.showProgress(); + this.showProgress() @getOld: -> this.showProgress() @@ -41,7 +41,8 @@ class CommitsList else @disable = true - @initLoadMore: -> + @initLoadMore: -> + $(document).unbind('scroll') $(document).endlessScroll bottomPixels: 400 fireDelay: 1000 @@ -51,4 +52,4 @@ class CommitsList callback: => this.getOld() -this.CommitsList = CommitsList
\ No newline at end of file +this.CommitsList = CommitsList diff --git a/app/assets/javascripts/dashboard.js.coffee b/app/assets/javascripts/dashboard.js.coffee index 6171e0d50fd..d2bd9e7362b 100644 --- a/app/assets/javascripts/dashboard.js.coffee +++ b/app/assets/javascripts/dashboard.js.coffee @@ -1,27 +1,33 @@ -window.dashboardPage = -> - Pager.init 20, true - $(".event_filter_link").bind "click", (event) -> - event.preventDefault() - toggleFilter $(this) - reloadActivities() - -reloadActivities = -> - $(".content_list").html '' - Pager.init 20, true - -toggleFilter = (sender) -> - sender.parent().toggleClass "inactive" - event_filters = $.cookie("event_filter") - filter = sender.attr("id").split("_")[0] - if event_filters - event_filters = event_filters.split(",") - else - event_filters = new Array() - - index = event_filters.indexOf(filter) - if index is -1 - event_filters.push filter - else - event_filters.splice index, 1 - - $.cookie "event_filter", event_filters.join(",") +class Dashboard + constructor: -> + @initSidebarTab() + + $(".dash-filter").keyup -> + terms = $(this).val() + uiBox = $(this).parents('.ui-box').first() + if terms == "" || terms == undefined + uiBox.find(".dash-list li").show() + else + uiBox.find(".dash-list li").each (index) -> + name = $(this).find(".filter-title").text() + + if name.toLowerCase().search(terms.toLowerCase()) == -1 + $(this).hide() + else + $(this).show() + + + + initSidebarTab: -> + key = "dashboard_sidebar_filter" + + # store selection in cookie + $('.dash-sidebar-tabs a').on 'click', (e) -> + $.cookie(key, $(e.target).attr('id')) + + # show tab from cookie + sidebar_filter = $.cookie(key) + $("#" + sidebar_filter).tab('show') if sidebar_filter + + +@Dashboard = Dashboard diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee new file mode 100644 index 00000000000..e264e281309 --- /dev/null +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -0,0 +1,51 @@ +$ -> + new Dispatcher() + +class Dispatcher + constructor: () -> + @initSearch() + @initPageScripts() + + initPageScripts: -> + page = $('body').attr('data-page') + project_id = $('body').attr('data-project-id') + + unless page + return false + + path = page.split(':') + + switch page + when 'projects:issues:index' + Issues.init() + when 'projects:issues:new', 'projects:merge_requests:new' + GitLab.GfmAutoComplete.setup() + when 'dashboard:show' + new Dashboard() + new Activities() + when 'projects:commit:show' + new Commit() + when 'groups:show', 'projects:show' + new Activities() + when 'projects:new', 'projects:edit' + new Project() + when 'projects:walls:show' + new Wall(project_id) + when 'projects:teams:members:index' + new TeamMembers() + when 'groups:members' + new GroupMembers() + when 'projects:tree:show' + new TreeView() + when 'projects:blob:show' + new BlobView() + + switch path.first() + when 'admin' then new Admin() + when 'projects' + new Wikis() if path[1] == 'wikis' + + + initSearch: -> + autocomplete_json = $('.search-autocomplete-json').data('autocomplete-opts') + new SearchAutocomplete(autocomplete_json) diff --git a/app/assets/javascripts/extensions/jquery.js.coffee b/app/assets/javascripts/extensions/jquery.js.coffee new file mode 100644 index 00000000000..40fb6cb9fc3 --- /dev/null +++ b/app/assets/javascripts/extensions/jquery.js.coffee @@ -0,0 +1,13 @@ +$.fn.showAndHide = -> + $(@).show(). + delay(3000). + fadeOut() + +$.fn.enableButton = -> + $(@).removeAttr('disabled'). + removeClass('disabled') + +$.fn.disableButton = -> + $(@).attr('disabled', 'disabled'). + addClass('disabled') + diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee new file mode 100644 index 00000000000..f8b7789884f --- /dev/null +++ b/app/assets/javascripts/flash.js.coffee @@ -0,0 +1,15 @@ +class Flash + constructor: (message, type)-> + flash = $(".flash-container") + flash.html("") + + $('<div/>', + class: "flash-#{type}", + text: message + ).appendTo(".flash-container") + + flash.click -> $(@).fadeOut() + flash.show() + setTimeout (-> flash.fadeOut()), 5000 + +@Flash = Flash diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index 1cc9d34dd80..77091da8f61 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -2,37 +2,55 @@ window.GitLab ?= {} GitLab.GfmAutoComplete = + # private_token: '' + dataSource: '' # Emoji Emoji: - data: [] + assetBase: '' template: '<li data-value="${insert}">${name} <img alt="${name}" height="20" src="${image}" width="20" /></li>' # Team Members Members: - data: [] - url: '' - params: - private_token: '' template: '<li data-value="${username}">${username} <small>${name}</small></li>' + Issues: + template: '<li data-value="${id}"><small>${id}</small> ${title} </li>' + # Add GFM auto-completion to all input fields, that accept GFM input. setup: -> input = $('.js-gfm-input') # Emoji - input.atWho ':', - data: @Emoji.data + input.atwho + at: ':' tpl: @Emoji.template + callbacks: + before_save: (emojis) => + $.map emojis, (em) => name: em, insert: em+ ':', image: "#{@Emoji.assetBase}/#{em}.png" # Team Members - input.atWho '@', + input.atwho + at: '@' tpl: @Members.template - callback: (query, callback) => - request_params = $.extend({}, @Members.params, query: query) - $.getJSON(@Members.url, request_params).done (members) => - new_members_data = $.map(members, (m) -> - username: m.username, - name: m.name - ) - callback(new_members_data) + search_key: 'search' + callbacks: + before_save: (members) => + $.map members, (m) => name: m.name, username: m.username, search: "#{m.username} #{m.name}" + + input.atwho + at: '#' + alias: 'issues' + search_key: 'search' + tpl: @Issues.template + callbacks: + before_save: (issues) -> + $.map issues, (i) -> id: i.iid, title: sanitize(i.title), search: "#{i.iid} #{i.title}" + input.one "focus", => + $.getJSON(@dataSource).done (data) -> + # load members + input.atwho 'load', "@", data.members + # load issues + input.atwho 'load', "issues", data.issues + # load emojis + input.atwho 'load', ":", data.emojis diff --git a/app/assets/javascripts/groups.js.coffee b/app/assets/javascripts/groups.js.coffee new file mode 100644 index 00000000000..c0ffccd8f70 --- /dev/null +++ b/app/assets/javascripts/groups.js.coffee @@ -0,0 +1,6 @@ +class GroupMembers + constructor: -> + $('li.users_group').bind 'ajax:success', -> + $(this).fadeOut() + +@GroupMembers = GroupMembers diff --git a/app/assets/javascripts/issues.js b/app/assets/javascripts/issues.js deleted file mode 100644 index 9ba1a3f1bba..00000000000 --- a/app/assets/javascripts/issues.js +++ /dev/null @@ -1,80 +0,0 @@ -function initIssuesSearch() { - var href = $('#issue_search_form').attr('action'); - var last_terms = ''; - - $('#issue_search').keyup(function() { - var terms = $(this).val(); - var milestone_id = $('#milestone_id').val(); - var status = $('#status').val(); - - if (terms != last_terms) { - last_terms = terms; - - if (terms.length >= 2 || terms.length == 0) { - $.get(href, { 'status': status, 'terms': terms, 'milestone_id': milestone_id }, function(response) { - $('#issues-table').html(response); - }); - } - } - }); -} - -/** - * Init issues page - * - */ -function issuesPage(){ - initIssuesSearch(); - $("#update_status").chosen(); - $("#update_assignee_id").chosen(); - $("#update_milestone_id").chosen(); - - $("#label_name").chosen(); - $("#assignee_id").chosen(); - $("#milestone_id").chosen(); - $("#milestone_id, #assignee_id, #label_name").on("change", function(){ - $(this).closest("form").submit(); - }); - - $('body').on('ajax:success', '.close_issue, .reopen_issue', function(){ - var t = $(this), - totalIssues, - reopen = t.hasClass('reopen_issue'); - $('.issue_counter').each(function(){ - var issue = $(this); - totalIssues = parseInt( $(this).html(), 10 ); - - if( reopen && issue.closest('.main_menu').length ){ - $(this).html( totalIssues+1 ); - }else { - $(this).html( totalIssues-1 ); - } - }); - - }); - - $(".check_all_issues").click(function () { - $('.selected_issue').attr('checked', this.checked); - issuesCheckChanged(); - }); - - $('.selected_issue').bind('change', issuesCheckChanged); -} - -function issuesCheckChanged() { - var checked_issues = $('.selected_issue:checked'); - - if(checked_issues.length > 0) { - var ids = [] - $.each(checked_issues, function(index, value) { - ids.push($(value).attr("data-id")); - }) - $('#update_issues_ids').val(ids); - $('.issues_filters').hide(); - $('.issues_bulk_update').show(); - } else { - $('#update_issues_ids').val([]); - $('.issues_bulk_update').hide(); - $('.issues_filters').show(); - } -} diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee new file mode 100644 index 00000000000..67d9498c50a --- /dev/null +++ b/app/assets/javascripts/issues.js.coffee @@ -0,0 +1,72 @@ +@Issues = + init: -> + Issues.initSearch() + Issues.initSelects() + Issues.initChecks() + + $("body").on "ajax:success", ".close_issue, .reopen_issue", -> + t = $(this) + totalIssues = undefined + reopen = t.hasClass("reopen_issue") + $(".issue_counter").each -> + issue = $(this) + totalIssues = parseInt($(this).html(), 10) + if reopen and issue.closest(".main_menu").length + $(this).html totalIssues + 1 + else + $(this).html totalIssues - 1 + $("body").on "click", ".issues-filters .dropdown-menu a", -> + $('.issues-list').block( + message: null, + overlayCSS: + backgroundColor: '#DDD' + opacity: .4 + ) + + reload: -> + Issues.initSelects() + Issues.initChecks() + $('#filter_issue_search').val($('#issue_search').val()) + + initSelects: -> + $("#update_status").chosen() + $("#update_assignee_id").chosen() + $("#update_milestone_id").chosen() + $("#label_name").chosen() + $("#assignee_id").chosen() + $("#milestone_id").chosen() + $("#milestone_id, #assignee_id, #label_name").on "change", -> + $(this).closest("form").submit() + + initChecks: -> + $(".check_all_issues").click -> + $(".selected_issue").attr "checked", @checked + Issues.checkChanged() + + $(".selected_issue").bind "change", Issues.checkChanged + + + initSearch: -> + form = $("#issue_search_form") + last_terms = "" + $("#issue_search").keyup -> + terms = $(this).val() + unless terms is last_terms + last_terms = terms + if terms.length >= 2 or terms.length is 0 + form.submit() + + checkChanged: -> + checked_issues = $(".selected_issue:checked") + if checked_issues.length > 0 + ids = [] + $.each checked_issues, (index, value) -> + ids.push $(value).attr("data-id") + + $("#update_issues_ids").val ids + $(".issues-filters").hide() + $(".issues_bulk_update").show() + else + $("#update_issues_ids").val [] + $(".issues_bulk_update").hide() + $(".issues-filters").show() diff --git a/app/assets/javascripts/lib/jquery.timeago.js b/app/assets/javascripts/lib/jquery.timeago.js new file mode 100644 index 00000000000..cc17aa7d3d1 --- /dev/null +++ b/app/assets/javascripts/lib/jquery.timeago.js @@ -0,0 +1,181 @@ +/** + * Timeago is a jQuery plugin that makes it easy to support automatically + * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). + * + * @name timeago + * @version 1.1.0 + * @requires jQuery v1.2.3+ + * @author Ryan McGeary + * @license MIT License - http://www.opensource.org/licenses/mit-license.php + * + * For usage and examples, visit: + * http://timeago.yarp.com/ + * + * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org) + */ + +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + $.timeago = function(timestamp) { + if (timestamp instanceof Date) { + return inWords(timestamp); + } else if (typeof timestamp === "string") { + return inWords($.timeago.parse(timestamp)); + } else if (typeof timestamp === "number") { + return inWords(new Date(timestamp)); + } else { + return inWords($.timeago.datetime(timestamp)); + } + }; + var $t = $.timeago; + + $.extend($.timeago, { + settings: { + refreshMillis: 60000, + allowFuture: false, + strings: { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "ago", + suffixFromNow: "from now", + seconds: "less than a minute", + minute: "about a minute", + minutes: "%d minutes", + hour: "about an hour", + hours: "about %d hours", + day: "a day", + days: "%d days", + month: "about a month", + months: "%d months", + year: "about a year", + years: "%d years", + wordSeparator: " ", + numbers: [] + } + }, + inWords: function(distanceMillis) { + var $l = this.settings.strings; + var prefix = $l.prefixAgo; + var suffix = $l.suffixAgo; + if (this.settings.allowFuture) { + if (distanceMillis < 0) { + prefix = $l.prefixFromNow; + suffix = $l.suffixFromNow; + } + } + + var seconds = Math.abs(distanceMillis) / 1000; + var minutes = seconds / 60; + var hours = minutes / 60; + var days = hours / 24; + var years = days / 365; + + function substitute(stringOrFunction, number) { + var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; + var value = ($l.numbers && $l.numbers[number]) || number; + return string.replace(/%d/i, value); + } + + var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || + seconds < 90 && substitute($l.minute, 1) || + minutes < 45 && substitute($l.minutes, Math.round(minutes)) || + minutes < 90 && substitute($l.hour, 1) || + hours < 24 && substitute($l.hours, Math.round(hours)) || + hours < 42 && substitute($l.day, 1) || + days < 30 && substitute($l.days, Math.round(days)) || + days < 45 && substitute($l.month, 1) || + days < 365 && substitute($l.months, Math.round(days / 30)) || + years < 1.5 && substitute($l.year, 1) || + substitute($l.years, Math.round(years)); + + var separator = $l.wordSeparator || ""; + if ($l.wordSeparator === undefined) { separator = " "; } + return $.trim([prefix, words, suffix].join(separator)); + }, + parse: function(iso8601) { + var s = $.trim(iso8601); + s = s.replace(/\.\d+/,""); // remove milliseconds + s = s.replace(/-/,"/").replace(/-/,"/"); + s = s.replace(/T/," ").replace(/Z/," UTC"); + s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 + return new Date(s); + }, + datetime: function(elem) { + var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title"); + return $t.parse(iso8601); + }, + isTime: function(elem) { + // jQuery's `is()` doesn't play well with HTML5 in IE + return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); + } + }); + + // functions that can be called via $(el).timeago('action') + // init is default when no action is given + // functions are called with context of a single element + var functions = { + init: function(){ + var refresh_el = $.proxy(refresh, this); + refresh_el(); + var $s = $t.settings; + if ($s.refreshMillis > 0) { + setInterval(refresh_el, $s.refreshMillis); + } + }, + update: function(time){ + $(this).data('timeago', { datetime: $t.parse(time) }); + refresh.apply(this); + } + }; + + $.fn.timeago = function(action, options) { + var fn = action ? functions[action] : functions.init; + if(!fn){ + throw new Error("Unknown function name '"+ action +"' for timeago"); + } + // each over objects here and call the requested function + this.each(function(){ + fn.call(this, options); + }); + return this; + }; + + function refresh() { + var data = prepareData(this); + if (!isNaN(data.datetime)) { + $(this).text(inWords(data.datetime)); + } + return this; + } + + function prepareData(element) { + element = $(element); + if (!element.data("timeago")) { + element.data("timeago", { datetime: $t.datetime(element) }); + var text = $.trim(element.text()); + if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) { + element.attr("title", text); + } + } + return element.data("timeago"); + } + + function inWords(date) { + return $t.inWords(distance(date)); + } + + function distance(date) { + return (new Date().getTime() - date.getTime()); + } + + // fix for IE6 suckage + document.createElement("abbr"); + document.createElement("time"); +})); diff --git a/app/assets/javascripts/lib/md5.js b/app/assets/javascripts/lib/md5.js new file mode 100644 index 00000000000..b63716eaad2 --- /dev/null +++ b/app/assets/javascripts/lib/md5.js @@ -0,0 +1,211 @@ +function md5 (str) { + // http://kevin.vanzonneveld.net + // + original by: Webtoolkit.info (http://www.webtoolkit.info/) + // + namespaced by: Michael White (http://getsprink.com) + // + tweaked by: Jack + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // - depends on: utf8_encode + // * example 1: md5('Kevin van Zonneveld'); + // * returns 1: '6e658d4bfcb59cc13f96c14450ac40b9' + var xl; + + var rotateLeft = function (lValue, iShiftBits) { + return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)); + }; + + var addUnsigned = function (lX, lY) { + var lX4, lY4, lX8, lY8, lResult; + lX8 = (lX & 0x80000000); + lY8 = (lY & 0x80000000); + lX4 = (lX & 0x40000000); + lY4 = (lY & 0x40000000); + lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF); + if (lX4 & lY4) { + return (lResult ^ 0x80000000 ^ lX8 ^ lY8); + } + if (lX4 | lY4) { + if (lResult & 0x40000000) { + return (lResult ^ 0xC0000000 ^ lX8 ^ lY8); + } else { + return (lResult ^ 0x40000000 ^ lX8 ^ lY8); + } + } else { + return (lResult ^ lX8 ^ lY8); + } + }; + + var _F = function (x, y, z) { + return (x & y) | ((~x) & z); + }; + var _G = function (x, y, z) { + return (x & z) | (y & (~z)); + }; + var _H = function (x, y, z) { + return (x ^ y ^ z); + }; + var _I = function (x, y, z) { + return (y ^ (x | (~z))); + }; + + var _FF = function (a, b, c, d, x, s, ac) { + a = addUnsigned(a, addUnsigned(addUnsigned(_F(b, c, d), x), ac)); + return addUnsigned(rotateLeft(a, s), b); + }; + + var _GG = function (a, b, c, d, x, s, ac) { + a = addUnsigned(a, addUnsigned(addUnsigned(_G(b, c, d), x), ac)); + return addUnsigned(rotateLeft(a, s), b); + }; + + var _HH = function (a, b, c, d, x, s, ac) { + a = addUnsigned(a, addUnsigned(addUnsigned(_H(b, c, d), x), ac)); + return addUnsigned(rotateLeft(a, s), b); + }; + + var _II = function (a, b, c, d, x, s, ac) { + a = addUnsigned(a, addUnsigned(addUnsigned(_I(b, c, d), x), ac)); + return addUnsigned(rotateLeft(a, s), b); + }; + + var convertToWordArray = function (str) { + var lWordCount; + var lMessageLength = str.length; + var lNumberOfWords_temp1 = lMessageLength + 8; + var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64; + var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16; + var lWordArray = new Array(lNumberOfWords - 1); + var lBytePosition = 0; + var lByteCount = 0; + while (lByteCount < lMessageLength) { + lWordCount = (lByteCount - (lByteCount % 4)) / 4; + lBytePosition = (lByteCount % 4) * 8; + lWordArray[lWordCount] = (lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition)); + lByteCount++; + } + lWordCount = (lByteCount - (lByteCount % 4)) / 4; + lBytePosition = (lByteCount % 4) * 8; + lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition); + lWordArray[lNumberOfWords - 2] = lMessageLength << 3; + lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29; + return lWordArray; + }; + + var wordToHex = function (lValue) { + var wordToHexValue = "", + wordToHexValue_temp = "", + lByte, lCount; + for (lCount = 0; lCount <= 3; lCount++) { + lByte = (lValue >>> (lCount * 8)) & 255; + wordToHexValue_temp = "0" + lByte.toString(16); + wordToHexValue = wordToHexValue + wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2); + } + return wordToHexValue; + }; + + var x = [], + k, AA, BB, CC, DD, a, b, c, d, S11 = 7, + S12 = 12, + S13 = 17, + S14 = 22, + S21 = 5, + S22 = 9, + S23 = 14, + S24 = 20, + S31 = 4, + S32 = 11, + S33 = 16, + S34 = 23, + S41 = 6, + S42 = 10, + S43 = 15, + S44 = 21; + + str = this.utf8_encode(str); + x = convertToWordArray(str); + a = 0x67452301; + b = 0xEFCDAB89; + c = 0x98BADCFE; + d = 0x10325476; + + xl = x.length; + for (k = 0; k < xl; k += 16) { + AA = a; + BB = b; + CC = c; + DD = d; + a = _FF(a, b, c, d, x[k + 0], S11, 0xD76AA478); + d = _FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756); + c = _FF(c, d, a, b, x[k + 2], S13, 0x242070DB); + b = _FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE); + a = _FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF); + d = _FF(d, a, b, c, x[k + 5], S12, 0x4787C62A); + c = _FF(c, d, a, b, x[k + 6], S13, 0xA8304613); + b = _FF(b, c, d, a, x[k + 7], S14, 0xFD469501); + a = _FF(a, b, c, d, x[k + 8], S11, 0x698098D8); + d = _FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF); + c = _FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1); + b = _FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE); + a = _FF(a, b, c, d, x[k + 12], S11, 0x6B901122); + d = _FF(d, a, b, c, x[k + 13], S12, 0xFD987193); + c = _FF(c, d, a, b, x[k + 14], S13, 0xA679438E); + b = _FF(b, c, d, a, x[k + 15], S14, 0x49B40821); + a = _GG(a, b, c, d, x[k + 1], S21, 0xF61E2562); + d = _GG(d, a, b, c, x[k + 6], S22, 0xC040B340); + c = _GG(c, d, a, b, x[k + 11], S23, 0x265E5A51); + b = _GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA); + a = _GG(a, b, c, d, x[k + 5], S21, 0xD62F105D); + d = _GG(d, a, b, c, x[k + 10], S22, 0x2441453); + c = _GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681); + b = _GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8); + a = _GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6); + d = _GG(d, a, b, c, x[k + 14], S22, 0xC33707D6); + c = _GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87); + b = _GG(b, c, d, a, x[k + 8], S24, 0x455A14ED); + a = _GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905); + d = _GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8); + c = _GG(c, d, a, b, x[k + 7], S23, 0x676F02D9); + b = _GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A); + a = _HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942); + d = _HH(d, a, b, c, x[k + 8], S32, 0x8771F681); + c = _HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122); + b = _HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C); + a = _HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44); + d = _HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9); + c = _HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60); + b = _HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70); + a = _HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6); + d = _HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA); + c = _HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085); + b = _HH(b, c, d, a, x[k + 6], S34, 0x4881D05); + a = _HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039); + d = _HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5); + c = _HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8); + b = _HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665); + a = _II(a, b, c, d, x[k + 0], S41, 0xF4292244); + d = _II(d, a, b, c, x[k + 7], S42, 0x432AFF97); + c = _II(c, d, a, b, x[k + 14], S43, 0xAB9423A7); + b = _II(b, c, d, a, x[k + 5], S44, 0xFC93A039); + a = _II(a, b, c, d, x[k + 12], S41, 0x655B59C3); + d = _II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92); + c = _II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D); + b = _II(b, c, d, a, x[k + 1], S44, 0x85845DD1); + a = _II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F); + d = _II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0); + c = _II(c, d, a, b, x[k + 6], S43, 0xA3014314); + b = _II(b, c, d, a, x[k + 13], S44, 0x4E0811A1); + a = _II(a, b, c, d, x[k + 4], S41, 0xF7537E82); + d = _II(d, a, b, c, x[k + 11], S42, 0xBD3AF235); + c = _II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB); + b = _II(b, c, d, a, x[k + 9], S44, 0xEB86D391); + a = addUnsigned(a, AA); + b = addUnsigned(b, BB); + c = addUnsigned(c, CC); + d = addUnsigned(d, DD); + } + + var temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d); + + return temp.toLowerCase(); +} diff --git a/app/assets/javascripts/lib/utf8_encode.js b/app/assets/javascripts/lib/utf8_encode.js new file mode 100644 index 00000000000..39ffe44dae0 --- /dev/null +++ b/app/assets/javascripts/lib/utf8_encode.js @@ -0,0 +1,70 @@ +function utf8_encode (argString) {
+ // http://kevin.vanzonneveld.net
+ // + original by: Webtoolkit.info (http://www.webtoolkit.info/)
+ // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // + improved by: sowberry
+ // + tweaked by: Jack
+ // + bugfixed by: Onno Marsman
+ // + improved by: Yves Sucaet
+ // + bugfixed by: Onno Marsman
+ // + bugfixed by: Ulrich
+ // + bugfixed by: Rafal Kukawski
+ // + improved by: kirilloid
+ // + bugfixed by: kirilloid
+ // * example 1: utf8_encode('Kevin van Zonneveld');
+ // * returns 1: 'Kevin van Zonneveld'
+
+ if (argString === null || typeof argString === "undefined") {
+ return "";
+ }
+
+ var string = (argString + ''); // .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
+ var utftext = '',
+ start, end, stringl = 0;
+
+ start = end = 0;
+ stringl = string.length;
+ for (var n = 0; n < stringl; n++) {
+ var c1 = string.charCodeAt(n);
+ var enc = null;
+
+ if (c1 < 128) {
+ end++;
+ } else if (c1 > 127 && c1 < 2048) {
+ enc = String.fromCharCode(
+ (c1 >> 6) | 192,
+ ( c1 & 63) | 128
+ );
+ } else if (c1 & 0xF800 != 0xD800) {
+ enc = String.fromCharCode(
+ (c1 >> 12) | 224,
+ ((c1 >> 6) & 63) | 128,
+ ( c1 & 63) | 128
+ );
+ } else { // surrogate pairs
+ if (c1 & 0xFC00 != 0xD800) { throw new RangeError("Unmatched trail surrogate at " + n); }
+ var c2 = string.charCodeAt(++n);
+ if (c2 & 0xFC00 != 0xDC00) { throw new RangeError("Unmatched lead surrogate at " + (n-1)); }
+ c1 = ((c1 & 0x3FF) << 10) + (c2 & 0x3FF) + 0x10000;
+ enc = String.fromCharCode(
+ (c1 >> 18) | 240,
+ ((c1 >> 12) & 63) | 128,
+ ((c1 >> 6) & 63) | 128,
+ ( c1 & 63) | 128
+ );
+ }
+ if (enc !== null) {
+ if (end > start) {
+ utftext += string.slice(start, end);
+ }
+ utftext += enc;
+ start = end = n + 1;
+ }
+ }
+
+ if (end > start) {
+ utftext += string.slice(start, stringl);
+ }
+
+ return utftext;
+}
diff --git a/app/assets/javascripts/main.js.coffee b/app/assets/javascripts/main.js.coffee index 5aaea50cf21..011244a5868 100644 --- a/app/assets/javascripts/main.js.coffee +++ b/app/assets/javascripts/main.js.coffee @@ -7,6 +7,8 @@ window.slugify = (text) -> window.ajaxGet = (url) -> $.ajax({type: "GET", url: url, dataType: "script"}) +window.showAndHide = (selector) -> + window.errorMessage = (message) -> ehtml = $("<p>") ehtml.addClass("error_message") @@ -32,13 +34,41 @@ window.disableButtonIfEmptyField = (field_selector, button_selector) -> else closest_submit.enable() +window.sanitize = (str) -> + return str.replace(/<(?:.|\n)*?>/gm, '') + +window.linkify = (str) -> + exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig + return str.replace(exp,"<a href='$1'>$1</a>") + +window.simpleFormat = (str) -> + linkify(sanitize(str).replace(/\n/g, '<br />')) + +window.startSpinner = -> + $('.turbolink-spinner').fadeIn() + +window.stopSpinner = -> + $('.turbolink-spinner').fadeOut() + +window.unbindEvents = -> + $(document).unbind('scroll') + $(document).off('scroll') + +document.addEventListener("page:fetch", startSpinner) +document.addEventListener("page:fetch", unbindEvents) +document.addEventListener("page:receive", stopSpinner) + $ -> # Click a .one_click_select field, select the contents $(".one_click_select").on 'click', -> $(@).select() + $('.remove-row').bind 'ajax:success', -> + $(this).closest('li').fadeOut() + # Click a .appear-link, appear-data fadeout - $(".appear-link").on 'click', -> + $(".appear-link").on 'click', (e) -> $('.appear-data').fadeIn() + e.preventDefault() # Initialize chosen selects $('select.chosen').chosen() @@ -49,11 +79,17 @@ $ -> # Bottom tooltip $('.has_bottom_tooltip').tooltip(placement: 'bottom') + # Form submitter + $('.trigger-submit').on 'change', -> + $(@).parents('form').submit() + + $("abbr.timeago").timeago() + # Flash - if (flash = $("#flash-container")).length > 0 - flash.click -> $(@).slideUp("slow") - flash.slideDown "slow" - setTimeout (-> flash.slideUp("slow")), 3000 + if (flash = $(".flash-container")).length > 0 + flash.click -> $(@).fadeOut() + flash.show() + setTimeout (-> flash.fadeOut()), 5000 # Disable form buttons while a form is submitting $('body').on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) -> @@ -77,9 +113,13 @@ $ -> when 115 $("#search").focus() e.preventDefault() + when 63 + new Shortcuts() + e.preventDefault() + # Commit show suppressed diff - $(".supp_diff_link").bind "click", -> + $(".content").on "click", ".supp_diff_link", -> $(@).next('table').show() $(@).remove() diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee index 65ed817c7c6..5400bc5c1ad 100644 --- a/app/assets/javascripts/merge_requests.js.coffee +++ b/app/assets/javascripts/merge_requests.js.coffee @@ -11,7 +11,7 @@ class MergeRequest constructor: (@opts) -> this.$el = $('.merge-request') - @diffs_loaded = false + @diffs_loaded = if @opts.action == 'diffs' then true else false @commits_loaded = false this.activateTab(@opts.action) @@ -21,17 +21,19 @@ class MergeRequest this.initMergeWidget() this.$('.show-all-commits').on 'click', => this.showAllCommits() + + modal = $('#modal_merge_info').modal(show: false) # Local jQuery finder $: (selector) -> this.$el.find(selector) initMergeWidget: -> - this.showState( @opts.current_state ) + this.showState( @opts.current_status ) if this.$('.automerge_widget').length and @opts.check_enable $.get @opts.url_to_automerge_check, (data) => - this.showState( data.state ) + this.showState( data.merge_status ) , 'json' if @opts.ci_enable @@ -76,7 +78,6 @@ class MergeRequest $('.ci_widget.ci-' + state).show() loadDiff: (event) -> - $('.dashboard-loader').show() $.ajax type: 'GET' url: this.$('.nav-tabs .diffs-tab a').attr('href') diff --git a/app/assets/javascripts/milestones.js.coffee b/app/assets/javascripts/milestones.js.coffee deleted file mode 100644 index 99a52bf4d3f..00000000000 --- a/app/assets/javascripts/milestones.js.coffee +++ /dev/null @@ -1,14 +0,0 @@ -$ -> - $('.milestone-issue-filter li[data-closed]').addClass('hide') - - $('.milestone-issue-filter ul.nav li a').click -> - $('.milestone-issue-filter li').toggleClass('active') - $('.milestone-issue-filter li[data-closed]').toggleClass('hide') - false - - $('.milestone-merge-requests-filter li[data-closed]').addClass('hide') - - $('.milestone-merge-requests-filter ul.nav li a').click -> - $('.milestone-merge-requests-filter li').toggleClass('active') - $('.milestone-merge-requests-filter li[data-closed]').toggleClass('hide') - false diff --git a/app/assets/javascripts/network.js.coffee b/app/assets/javascripts/network.js.coffee new file mode 100644 index 00000000000..cea5986f45a --- /dev/null +++ b/app/assets/javascripts/network.js.coffee @@ -0,0 +1,11 @@ +class Network + constructor: (opts) -> + $("#filter_ref").click -> + $(this).closest('form').submit() + + branch_graph = new BranchGraph($(".network-graph"), opts) + + vph = $(window).height() - 250 + $('.network-graph').css 'height': (vph + 'px') + +@Network = Network diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 919c6b7f4a2..5225623c1f0 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,37 +1,37 @@ var NoteList = { - + id: null, notes_path: null, target_params: null, target_id: 0, target_type: null, - top_id: 0, - bottom_id: 0, - loading_more_disabled: false, - reversed: false, init: function(tid, tt, path) { NoteList.notes_path = path + ".js"; NoteList.target_id = tid; NoteList.target_type = tt; - NoteList.reversed = $("#notes-list").is(".reversed"); NoteList.target_params = "target_type=" + NoteList.target_type + "&target_id=" + NoteList.target_id; NoteList.setupMainTargetNoteForm(); - if(NoteList.reversed) { - var form = $(".js-main-target-form"); - form.find(".note-form-actions").hide(); - var textarea = form.find(".js-note-text"); - textarea.css("height", "40px"); - textarea.on("focus", function(){ - textarea.css("height", "80px"); - form.find(".note-form-actions").show(); - }); - } - // get initial set of notes NoteList.getContent(); + // Unbind events to prevent firing twice + $(document).off("click", ".js-add-diff-note-button"); + $(document).off("click", ".js-discussion-reply-button"); + $(document).off("click", ".js-note-preview-button"); + $(document).off("click", ".js-note-attachment-input"); + $(document).off("click", ".js-close-discussion-note-form"); + $(document).off("click", ".js-note-delete"); + $(document).off("click", ".js-note-edit"); + $(document).off("click", ".js-note-edit-cancel"); + $(document).off("click", ".js-note-attachment-delete"); + $(document).off("click", ".js-choose-note-attachment-button"); + $(document).off("click", ".js-show-outdated-discussion"); + + $(document).off("ajax:complete", ".js-main-target-form"); + + // add a new diff note $(document).on("click", ".js-add-diff-note-button", @@ -62,6 +62,26 @@ var NoteList = { ".js-note-delete", NoteList.removeNote); + // show the edit note form + $(document).on("click", + ".js-note-edit", + NoteList.showEditNoteForm); + + // cancel note editing + $(document).on("click", + ".note-edit-cancel", + NoteList.cancelNoteEdit); + + // delete note attachment + $(document).on("click", + ".js-note-attachment-delete", + NoteList.deleteNoteAttachment); + + // update the note after editing + $(document).on("ajax:complete", + "form.edit_note", + NoteList.updateNote); + // reset main target form after submit $(document).on("ajax:complete", ".js-main-target-form", @@ -69,12 +89,12 @@ var NoteList = { $(document).on("click", - ".js-choose-note-attachment-button", - NoteList.chooseNoteAttachment); + ".js-choose-note-attachment-button", + NoteList.chooseNoteAttachment); $(document).on("click", - ".js-show-outdated-discussion", - function(e) { $(this).next('.outdated-discussion').show(); e.preventDefault() }); + ".js-show-outdated-discussion", + function(e) { $(this).next('.outdated-discussion').show(); e.preventDefault() }); }, @@ -113,8 +133,8 @@ var NoteList = { /** * Called when clicking the "Choose File" button. - * - * Opesn the file selection dialog. + * + * Opens the file selection dialog. */ chooseNoteAttachment: function() { var form = $(this).closest("form"); @@ -149,7 +169,7 @@ var NoteList = { /** * Called in response to "cancel" on a diff note form. - * + * * Shows the reply button again. * Removes the form and if necessary it's temporary row. */ @@ -157,7 +177,7 @@ var NoteList = { var form = $(this).closest("form"); var row = form.closest("tr"); - // show the reply button (will only work for replys) + // show the reply button (will only work for replies) form.prev(".js-discussion-reply-button").show(); if (row.is(".js-temp-notes-holder")) { @@ -193,6 +213,60 @@ var NoteList = { }, /** + * Called in response to clicking the edit note link + * + * Replaces the note text with the note edit form + * Adds a hidden div with the original content of the note to fill the edit note form with + * if the user cancels + */ + showEditNoteForm: function(e) { + e.preventDefault(); + var note = $(this).closest(".note"); + note.find(".note-text").hide(); + + // Show the attachment delete link + note.find(".js-note-attachment-delete").show(); + + GitLab.GfmAutoComplete.setup(); + + var form = note.find(".note-edit-form"); + form.show(); + + var textarea = form.find("textarea"); + var p = $("<p></p>").text(textarea.val()); + var hidden_div = $('<div class="note-original-content"></div>').append(p); + form.append(hidden_div); + hidden_div.hide(); + textarea.focus(); + }, + + /** + * Called in response to clicking the cancel button when editing a note + * + * Resets and hides the note editing form + */ + cancelNoteEdit: function(e) { + e.preventDefault(); + var note = $(this).closest(".note"); + NoteList.resetNoteEditing(note); + }, + + + /** + * Called in response to clicking the delete attachment link + * + * Removes the attachment wrapper view, including image tag if it exists + * Resets the note editing form + */ + deleteNoteAttachment: function() { + var note = $(this).closest(".note"); + note.find(".note-attachment").remove(); + NoteList.resetNoteEditing(note); + NoteList.rewriteTimestamp(note.find(".note-last-update")); + }, + + + /** * Called when clicking on the "reply" button for a diff line. * * Shows the note form below the notes. @@ -327,7 +401,7 @@ var NoteList = { /** - * Gets an inital set of notes. + * Gets an initial set of notes. */ getContent: function() { $.ajax({ @@ -344,128 +418,11 @@ var NoteList = { * Replaces the content of #notes-list with the given html. */ setContent: function(newNoteIds, html) { - NoteList.top_id = newNoteIds.first(); - NoteList.bottom_id = newNoteIds.last(); $("#notes-list").html(html); - - // for the wall - if (NoteList.reversed) { - // init infinite scrolling - NoteList.initLoadMore(); - - // init getting new notes - NoteList.initRefreshNew(); - } - }, - - - /** - * Handle loading more notes when scrolling to the bottom of the page. - * The id of the last note in the list is in NoteList.bottom_id. - * - * Set up refreshing only new notes after all notes have been loaded. - */ - - - /** - * Initializes loading more notes when scrolling to the bottom of the page. - */ - initLoadMore: function() { - $(document).endlessScroll({ - bottomPixels: 400, - fireDelay: 1000, - fireOnce:true, - ceaseFire: function() { - return NoteList.loading_more_disabled; - }, - callback: function(i) { - NoteList.getMore(); - } - }); - }, - - /** - * Gets an additional set of notes. - */ - getMore: function() { - // only load more notes if there are no "new" notes - $('.loading').show(); - $.ajax({ - url: NoteList.notes_path, - data: NoteList.target_params + "&loading_more=1&" + (NoteList.reversed ? "before_id" : "after_id") + "=" + NoteList.bottom_id, - complete: function(){ $('.js-notes-busy').removeClass("loading")}, - beforeSend: function() { $('.js-notes-busy').addClass("loading") }, - dataType: "script" - }); - }, - - /** - * Called in response to getMore(). - * Append notes to #notes-list. - */ - appendMoreNotes: function(newNoteIds, html) { - var lastNewNoteId = newNoteIds.last(); - if(lastNewNoteId != NoteList.bottom_id) { - NoteList.bottom_id = lastNewNoteId; - $("#notes-list").append(html); - } - }, - - /** - * Called in response to getMore(). - * Disables loading more notes when scrolling to the bottom of the page. - */ - finishedLoadingMore: function() { - NoteList.loading_more_disabled = true; - - // make sure we are up to date - NoteList.updateVotes(); }, /** - * Handle refreshing and adding of new notes. - * - * New notes are all notes that are created after the site has been loaded. - * The "old" notes are in #notes-list the "new" ones will be in #new-notes-list. - * The id of the last "old" note is in NoteList.bottom_id. - */ - - - /** - * Initializes getting new notes every n seconds. - * - * Note: only used on wall. - */ - initRefreshNew: function() { - setInterval("NoteList.getNew()", 10000); - }, - - /** - * Gets the new set of notes. - * - * Note: only used on wall. - */ - getNew: function() { - $.ajax({ - url: NoteList.notes_path, - data: NoteList.target_params + "&loading_new=1&after_id=" + (NoteList.reversed ? NoteList.top_id : NoteList.bottom_id), - dataType: "script" - }); - }, - - /** - * Called in response to getNew(). - * Replaces the content of #new-notes-list with the given html. - * - * Note: only used on wall. - */ - replaceNewNotes: function(newNoteIds, html) { - $("#new-notes-list").html(html); - NoteList.updateVotes(); - }, - - /** * Adds a single common note to #notes-list. */ appendNewNote: function(id, html) { @@ -498,15 +455,6 @@ var NoteList = { }, /** - * Adds a single wall note to #new-notes-list. - * - * Note: only used on wall. - */ - appendNewWallNote: function(id, html) { - $("#new-notes-list").prepend(html); - }, - - /** * Called in response the main target form has been successfully submitted. * * Removes any errors. @@ -568,5 +516,65 @@ var NoteList = { votes.find(".upvotes").text(votes.find(".upvotes").text().replace(/\d+/, upvotes)); votes.find(".downvotes").text(votes.find(".downvotes").text().replace(/\d+/, downvotes)); } + }, + + /** + * Called in response to the edit note form being submitted + * + * Updates the current note field. + * Hides the edit note form + */ + updateNote: function(e, xhr, settings) { + response = JSON.parse(xhr.responseText); + if (response.success) { + var note_li = $("#note_" + response.id); + var note_text = note_li.find(".note-text"); + note_text.html(response.note).show(); + + var note_form = note_li.find(".note-edit-form"); + note_form.hide(); + note_form.find(".btn-save").enableButton(); + + // Update the "Edited at xxx label" on the note to show it's just been updated + NoteList.rewriteTimestamp(note_li.find(".note-last-update")); + } + }, + + /** + * Called in response to the 'cancel note' link clicked, or after deleting a note attachment + * + * Hides the edit note form and shows the note + * Resets the edit note form textarea with the original content of the note + */ + resetNoteEditing: function(note) { + note.find(".note-text").show(); + + // Hide the attachment delete link + note.find(".js-note-attachment-delete").hide(); + + // Put the original content of the note back into the edit form textarea + var form = note.find(".note-edit-form"); + var original_content = form.find(".note-original-content"); + form.find("textarea").val(original_content.text()); + original_content.remove(); + + note.find(".note-edit-form").hide(); + }, + + /** + * Utility function to generate new timestamp text for a note + * + */ + rewriteTimestamp: function(element) { + // Strip all newlines from the existing timestamp + var ts = element.text().replace(/\n/g, ' ').trim(); + + // If the timestamp already has '(Edited xxx ago)' text, remove it + ts = ts.replace(new RegExp("\\(Edited [A-Za-z0-9 ]+\\)$", "gi"), ""); + + // Append "(Edited just now)" + ts = (ts + " <small>(Edited just now)</small>"); + + element.html(ts); } }; diff --git a/app/assets/javascripts/pager.js.coffee b/app/assets/javascripts/pager.js.coffee index 5f606acdf9c..5bd11d273a7 100644 --- a/app/assets/javascripts/pager.js.coffee +++ b/app/assets/javascripts/pager.js.coffee @@ -30,6 +30,7 @@ @disable = true initLoadMore: -> + $(document).unbind('scroll') $(document).endlessScroll bottomPixels: 400 fireDelay: 1000 diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index 42207a390b3..e7974611cbe 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -1,13 +1,9 @@ $ -> $('.edit_user .application-theme input, .edit_user .code-preview-theme input').click -> - # Hide any previous submission feedback - $('.edit_user .update-feedback').hide() - # Submit the form $('.edit_user').submit() - # Go up the hierarchy and show the corresponding submission feedback element - $(@).closest('fieldset').find('.update-feedback').show('highlight', {color: '#DFF0D8'}, 500) + new Flash("Appearance settings saved", "notice") $('.update-username form').on 'ajax:before', -> $('.loading-gif').show() @@ -15,6 +11,8 @@ $ -> $(this).find('.update-failed').hide() $('.update-username form').on 'ajax:complete', -> - $(this).find('.save-btn').removeAttr('disabled') - $(this).find('.save-btn').removeClass('disabled') + $(this).find('.btn-save').enableButton() $(this).find('.loading-gif').hide() + + $('.update-notifications').on 'ajax:complete', -> + $(this).find('.btn-save').enableButton() diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee new file mode 100644 index 00000000000..83236b34814 --- /dev/null +++ b/app/assets/javascripts/project.js.coffee @@ -0,0 +1,42 @@ +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 = $ '.project_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 'git remote add origin ' + $(@).data 'clone' + + # Ref switcher + $('.project-refs-select').on 'change', -> + $(@).parents('form').submit() diff --git a/app/assets/javascripts/project_import.js.coffee b/app/assets/javascripts/project_import.js.coffee new file mode 100644 index 00000000000..7cf44da99fe --- /dev/null +++ b/app/assets/javascripts/project_import.js.coffee @@ -0,0 +1,7 @@ +class ProjectImport + constructor: -> + setTimeout -> + Turbolinks.visit(location.href) + , 5000 + +@ProjectImport = ProjectImport diff --git a/app/assets/javascripts/projects.js.coffee b/app/assets/javascripts/projects.js.coffee deleted file mode 100644 index d03a487c453..00000000000 --- a/app/assets/javascripts/projects.js.coffee +++ /dev/null @@ -1,20 +0,0 @@ -window.Projects = -> - $('.new_project, .edit_project').on 'ajax:before', -> - $('.project_new_holder, .project_edit_holder').hide() - $('.save-project-loader').show() - - $('form #project_default_branch').chosen() - disableButtonIfEmptyField '#project_name', '.project-submit' - -$ -> - # Git clone panel switcher - scope = $ '.project_clone_holder' - if scope.length > 0 - $('a, button', scope).click -> - $('a, button', scope).removeClass 'active' - $(@).addClass 'active' - $('#project_clone', scope).val $(@).data 'clone' - - # Ref switcher - $('.project-refs-select').on 'change', -> - $(@).parents('form').submit() diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee new file mode 100644 index 00000000000..3418690e109 --- /dev/null +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -0,0 +1,8 @@ +class SearchAutocomplete + constructor: (json) -> + $("#search").autocomplete + source: json + select: (event, ui) -> + location.href = ui.item.url + +@SearchAutocomplete = SearchAutocomplete diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee new file mode 100644 index 00000000000..e7e40a066ec --- /dev/null +++ b/app/assets/javascripts/shortcuts.js.coffee @@ -0,0 +1,11 @@ +class Shortcuts + constructor: -> + if $('#modal-shortcuts').length > 0 + $('#modal-shortcuts').modal('show') + else + $.ajax( + url: '/help/shortcuts', + dataType: "script" + ) + +@Shortcuts = Shortcuts diff --git a/app/assets/javascripts/stat_graph.js.coffee b/app/assets/javascripts/stat_graph.js.coffee new file mode 100644 index 00000000000..b129619696f --- /dev/null +++ b/app/assets/javascripts/stat_graph.js.coffee @@ -0,0 +1,6 @@ +class window.StatGraph + @log: {} + @get_log: -> + @log + @set_log: (data) -> + @log = data diff --git a/app/assets/javascripts/stat_graph_contributors.js.coffee b/app/assets/javascripts/stat_graph_contributors.js.coffee new file mode 100644 index 00000000000..168b7337041 --- /dev/null +++ b/app/assets/javascripts/stat_graph_contributors.js.coffee @@ -0,0 +1,84 @@ +class window.ContributorsStatGraph + init: (log) -> + @parsed_log = ContributorsStatGraphUtil.parse_log(log) + @set_current_field("commits") + total_commits = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field) + author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field) + @add_master_graph(total_commits) + @add_authors_graph(author_commits) + @change_date_header() + add_master_graph: (total_data) -> + @master_graph = new ContributorsMasterGraph(total_data) + @master_graph.draw() + add_authors_graph: (author_data) -> + @authors = [] + limited_author_data = author_data.slice(0, 100) + _.each(limited_author_data, (d) => + author_header = @create_author_header(d) + $(".contributors-list").append(author_header) + @authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates) + author_graph.draw() + ) + format_author_commit_info: (author) -> + commits = $('<span/>', { + class: 'graph-author-commits-count' + }) + commits.text(author.commits + " commits") + + additions = $('<span/>', { + class: 'graph-additions' + }) + additions.text(author.additions + " ++") + + deletions = $('<span/>', { + class: 'graph-deletions' + }) + deletions.text(author.deletions + " --") + + $('<span/>').append(commits) + .append(" / ") + .append(additions) + .append(" / ") + .append(deletions) + + create_author_header: (author) -> + list_item = $('<li/>', { + class: 'person' + style: 'display: block;' + }) + author_name = $('<h4>' + author.author_name + '</h4>') + author_email = $('<p class="graph-author-email">' + author.author_email + '</p>') + author_commit_info_span = $('<span/>', { + class: 'commits' + }) + author_commit_info = @format_author_commit_info(author) + author_commit_info_span.html(author_commit_info) + list_item.append(author_name) + list_item.append(author_email) + list_item.append(author_commit_info_span) + list_item + redraw_master: -> + total_data = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field) + @master_graph.set_data(total_data) + @master_graph.redraw() + redraw_authors: -> + $("ol").html("") + x_domain = ContributorsGraph.prototype.x_domain + author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field, x_domain) + _.each(author_commits, (d) => + @redraw_author_commit_info(d) + $(@authors[d.author_name].list_item).appendTo("ol") + @authors[d.author_name].set_data(d.dates) + @authors[d.author_name].redraw() + ) + set_current_field: (field) -> + @field = field + change_date_header: -> + x_domain = ContributorsGraph.prototype.x_domain + print_date_format = d3.time.format("%B %e %Y") + print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]) + $("#date_header").text(print) + redraw_author_commit_info: (author) -> + author_list_item = $(@authors[author.author_name].list_item) + author_commit_info = @format_author_commit_info(author) + author_list_item.find("span").html(author_commit_info) diff --git a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee new file mode 100644 index 00000000000..48443644169 --- /dev/null +++ b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee @@ -0,0 +1,176 @@ +class window.ContributorsGraph + MARGIN: + top: 20 + right: 20 + bottom: 30 + left: 50 + x_domain: null + y_domain: null + dates: [] + @set_x_domain: (data) => + @prototype.x_domain = data + @set_y_domain: (data) => + @prototype.y_domain = [0, d3.max(data, (d) -> + d.commits = d.commits ? d.additions ? d.deletions + )] + @init_x_domain: (data) => + @prototype.x_domain = d3.extent(data, (d) -> + d.date + ) + @init_y_domain: (data) => + @prototype.y_domain = [0, d3.max(data, (d) -> + d.commits = d.commits ? d.additions ? d.deletions + )] + @init_domain: (data) => + @init_x_domain(data) + @init_y_domain(data) + @set_dates: (data) => + @prototype.dates = data + set_x_domain: -> + @x.domain(@x_domain) + set_y_domain: -> + @y.domain(@y_domain) + set_domain: -> + @set_x_domain() + @set_y_domain() + create_scale: (width, height) -> + @x = d3.time.scale().range([0, width]).clamp(true) + @y = d3.scale.linear().range([height, 0]).nice() + draw_x_axis: -> + @svg.append("g").attr("class", "x axis").attr("transform", "translate(0, #{@height})") + .call(@x_axis) + draw_y_axis: -> + @svg.append("g").attr("class", "y axis").call(@y_axis) + set_data: (data) -> + @data = data + +class window.ContributorsMasterGraph extends ContributorsGraph + constructor: (@data) -> + if $(window).width() > 1214 + @width = 1100 + else + @width = 870 + + @height = 200 + @x = null + @y = null + @x_axis = null + @y_axis = null + @area = null + @svg = null + @brush = null + @x_max_domain = null + process_dates: (data) -> + dates = @get_dates(data) + @parse_dates(data) + ContributorsGraph.set_dates(dates) + get_dates: (data) -> + _.pluck(data, 'date') + parse_dates: (data) -> + parseDate = d3.time.format("%Y-%m-%d").parse + data.forEach((d) -> + d.date = parseDate(d.date) + ) + create_scale: -> + super @width, @height + create_axes: -> + @x_axis = d3.svg.axis().scale(@x).orient("bottom") + @y_axis = d3.svg.axis().scale(@y).orient("left").ticks(5) + create_svg: -> + @svg = d3.select("#contributors-master").append("svg") + .attr("width", @width + @MARGIN.left + @MARGIN.right) + .attr("height", @height + @MARGIN.top + @MARGIN.bottom) + .attr("class", "tint-box") + .append("g") + .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")") + create_area: (x, y) -> + @area = d3.svg.area().x((d) -> + x(d.date) + ).y0(@height).y1((d) -> + xa = d.commits = d.commits ? d.additions ? d.deletions + console.log(xa) + y(xa) + ).interpolate("basis") + create_brush: -> + @brush = d3.svg.brush().x(@x).on("brushend", @update_content) + draw_path: (data) -> + @svg.append("path").datum(data).attr("class", "area").attr("d", @area) + add_brush: -> + @svg.append("g").attr("class", "selection").call(@brush).selectAll("rect").attr("height", @height) + update_content: => + ContributorsGraph.set_x_domain(if @brush.empty() then @x_max_domain else @brush.extent()) + $("#brush_change").trigger('change') + draw: -> + @process_dates(@data) + @create_scale() + @create_axes() + ContributorsGraph.init_domain(@data) + @x_max_domain = @x_domain + @set_domain() + @create_area(@x, @y) + @create_svg() + @create_brush() + @draw_path(@data) + @draw_x_axis() + @draw_y_axis() + @add_brush() + redraw: -> + @process_dates(@data) + ContributorsGraph.set_y_domain(@data) + @set_y_domain() + @svg.select("path").datum(@data) + @svg.select("path").attr("d", @area) + @svg.select(".y.axis").call(@y_axis) + +class window.ContributorsAuthorGraph extends ContributorsGraph + constructor: (@data) -> + if $(window).width() > 1214 + @width = 490 + else + @width = 380 + + @height = 200 + @x = null + @y = null + @x_axis = null + @y_axis = null + @area = null + @svg = null + @list_item = null + create_scale: -> + super @width, @height + create_axes: -> + @x_axis = d3.svg.axis().scale(@x).orient("bottom").ticks(8) + @y_axis = d3.svg.axis().scale(@y).orient("left").ticks(5) + create_area: (x, y) -> + @area = d3.svg.area().x((d) -> + parseDate = d3.time.format("%Y-%m-%d").parse + x(parseDate(d)) + ).y0(@height).y1((d) => + if @data[d]? then y(@data[d]) else y(0) + ).interpolate("basis") + create_svg: -> + @list_item = d3.selectAll(".person")[0].pop() + @svg = d3.select(@list_item).append("svg") + .attr("width", @width + @MARGIN.left + @MARGIN.right) + .attr("height", @height + @MARGIN.top + @MARGIN.bottom) + .attr("class", "spark") + .append("g") + .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")") + draw_path: (data) -> + @svg.append("path").datum(data).attr("class", "area-contributor").attr("d", @area) + draw: -> + @create_scale() + @create_axes() + @set_domain() + @create_area(@x, @y) + @create_svg() + @draw_path(@dates) + @draw_x_axis() + @draw_y_axis() + redraw: -> + @set_domain() + @svg.select("path").datum(@dates) + @svg.select("path").attr("d", @area) + @svg.select(".x.axis").call(@x_axis) + @svg.select(".y.axis").call(@y_axis) diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/stat_graph_contributors_util.js.coffee new file mode 100644 index 00000000000..364cab18377 --- /dev/null +++ b/app/assets/javascripts/stat_graph_contributors_util.js.coffee @@ -0,0 +1,93 @@ +window.ContributorsStatGraphUtil = + parse_log: (log) -> + total = {} + by_author = {} + for entry in log + @add_date(entry.date, total) unless total[entry.date]? + @add_author(entry, by_author) unless by_author[entry.author_name]? + @add_date(entry.date, by_author[entry.author_name]) unless by_author[entry.author_name][entry.date] + @store_data(entry, total[entry.date], by_author[entry.author_name][entry.date]) + total = _.toArray(total) + by_author = _.toArray(by_author) + total: total, by_author: by_author + + add_date: (date, collection) -> + collection[date] = {} + collection[date].date = date + + add_author: (author, by_author) -> + by_author[author.author_name] = {} + by_author[author.author_name].author_name = author.author_name + by_author[author.author_name].author_email = author.author_email + + store_data: (entry, total, by_author) -> + @store_commits(total, by_author) + @store_additions(entry, total, by_author) + @store_deletions(entry, total, by_author) + + store_commits: (total, by_author) -> + @add(total, "commits", 1) + @add(by_author, "commits", 1) + + add: (collection, field, value) -> + collection[field] ?= 0 + collection[field] += value + + store_additions: (entry, total, by_author) -> + entry.additions ?= 0 + @add(total, "additions", entry.additions) + @add(by_author, "additions", entry.additions) + + store_deletions: (entry, total, by_author) -> + entry.deletions ?= 0 + @add(total, "deletions", entry.deletions) + @add(by_author, "deletions", entry.deletions) + + get_total_data: (parsed_log, field) -> + log = parsed_log.total + total_data = @pick_field(log, field) + _.sortBy(total_data, (d) -> + d.date + ) + pick_field: (log, field) -> + total_data = [] + _.each(log, (d) -> + total_data.push(_.pick(d, [field, 'date'])) + ) + total_data + + get_author_data: (parsed_log, field, date_range = null) -> + log = parsed_log.by_author + author_data = [] + + _.each(log, (log_entry) => + parsed_log_entry = @parse_log_entry(log_entry, field, date_range) + if not _.isEmpty(parsed_log_entry.dates) + author_data.push(parsed_log_entry) + ) + + _.sortBy(author_data, (d) -> + d[field] + ).reverse() + + parse_log_entry: (log_entry, field, date_range) -> + parsed_entry = {} + parsed_entry.author_name = log_entry.author_name + parsed_entry.author_email = log_entry.author_email + parsed_entry.dates = {} + parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0 + _.each(_.omit(log_entry, 'author_name', 'author_email'), (value, key) => + if @in_range(value.date, date_range) + parsed_entry.dates[value.date] = value[field] + parsed_entry.commits += value.commits + parsed_entry.additions += value.additions + parsed_entry.deletions += value.deletions + ) + return parsed_entry + + in_range: (date, date_range) -> + if date_range is null || date_range[0] <= new Date(date) <= date_range[1] + true + else + false + diff --git a/app/assets/javascripts/team_members.js.coffee b/app/assets/javascripts/team_members.js.coffee new file mode 100644 index 00000000000..5eaa8ad4ff9 --- /dev/null +++ b/app/assets/javascripts/team_members.js.coffee @@ -0,0 +1,6 @@ +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 5003f9b00c7..4852e879b68 100644 --- a/app/assets/javascripts/tree.js.coffee +++ b/app/assets/javascripts/tree.js.coffee @@ -1,56 +1,43 @@ -# Code browser tree slider - -$ -> - if $('#tree-slider').length > 0 - # Show the "Loading commit data" for only the first element - $('span.log_loading:first').removeClass('hide') - - $('#tree-slider .tree-item-file-name a, .breadcrumb li > a').live "click", -> - $("#tree-content-holder").hide("slide", { direction: "left" }, 150) +class TreeView + constructor: -> + @initKeyNav() + # Code browser tree slider # Make the entire tree-item row clickable, but not if clicking another link (like a commit message) - $("#tree-slider .tree-item").live 'click', (e) -> - $('.tree-item-file-name a', this).trigger('click') if (e.target.nodeName != "A") - - # Show/Hide the loading spinner - $('#tree-slider .tree-item-file-name a, .breadcrumb a, .project-refs-form').live - "ajax:beforeSend": -> $('.tree_progress').addClass("loading") - "ajax:complete": -> $('.tree_progress').removeClass("loading") - - # Maintain forward/back history while browsing the file tree - ((window) -> - History = window.History - $ = window.jQuery - document = window.document + $(".tree-content-holder .tree-item").on 'click', (e) -> + if (e.target.nodeName != "A") + path = $('.tree-item-file-name a', this).attr('href') + Turbolinks.visit(path) - # Check to see if History.js is enabled for our Browser - unless History.enabled - return false - - $('#tree-slider .tree-item-file-name a, .breadcrumb li > a').live 'click', (e) -> - History.pushState(null, null, decodeURIComponent($(@).attr('href'))) - return false - - History.Adapter.bind window, 'statechange', -> - state = History.getState() - window.ajaxGet(state.url) - )(window) - - # See if there are lines selected - # "#L12" and "#L34-56" supported - highlightBlobLines = -> - if window.location.hash isnt "" - matches = window.location.hash.match(/\#L(\d+)(\-(\d+))?/) - first_line = parseInt(matches?[1]) - last_line = parseInt(matches?[3]) - - unless isNaN first_line - last_line = first_line if isNaN(last_line) - $("#tree-content-holder .highlight .line").removeClass("hll") - $("#LC#{line}").addClass("hll") for line in [first_line..last_line] - $("#L#{first_line}").ScrollTo() + # Show the "Loading commit data" for only the first element + $('span.log_loading:first').removeClass('hide') - # Highlight the correct lines on load - highlightBlobLines() - # Highlight the correct lines when the hash part of the URL changes - $(window).on 'hashchange', highlightBlobLines + initKeyNav: -> + li = $("tr.tree-item") + liSelected = null + $('body').keydown (e) -> + if e.which is 40 + if liSelected + next = liSelected.next() + if next.length > 0 + liSelected.removeClass "selected" + liSelected = next.addClass("selected") + else + liSelected = li.eq(0).addClass("selected") + + $(liSelected).focus() + else if e.which is 38 + if liSelected + next = liSelected.prev() + if next.length > 0 + liSelected.removeClass "selected" + liSelected = next.addClass("selected") + else + liSelected = li.last().addClass("selected") + + $(liSelected).focus() + 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/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee new file mode 100644 index 00000000000..8286ca2f0c1 --- /dev/null +++ b/app/assets/javascripts/users_select.js.coffee @@ -0,0 +1,37 @@ +$ -> + userFormatResult = (user) -> + avatar = gon.gravatar_url + avatar = avatar.replace('%{hash}', md5(user.email)) + avatar = avatar.replace('%{size}', '24') + + markup = "<div class='user-result'>" + markup += "<div class='user-image'><img class='avatar s24' src='" + avatar + "'></div>" + markup += "<div class='user-name'>" + user.name + "</div>" + markup += "<div class='user-username'>" + user.username + "</div>" + markup += "</div>" + markup + + userFormatSelection = (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/wall.js.coffee b/app/assets/javascripts/wall.js.coffee new file mode 100644 index 00000000000..4cc11331aca --- /dev/null +++ b/app/assets/javascripts/wall.js.coffee @@ -0,0 +1,85 @@ +class Wall + constructor: (project_id) -> + @project_id = project_id + @note_ids = [] + @getContent() + @initRefresh() + @initForm() + + # + # Gets an initial set of notes. + # + getContent: -> + Api.notes @project_id, (notes) => + $.each notes, (i, note) => + # render note if it not present in loaded list + # or skip if rendered + if $.inArray(note.id, @note_ids) == -1 + @note_ids.push(note.id) + @renderNote(note) + @scrollDown() + $("abbr.timeago").timeago() + + initRefresh: -> + setInterval => + @refresh() + , 10000 + + refresh: -> + @getContent() + + scrollDown: -> + notes = $('ul.notes') + $('body, html').scrollTop(notes.height()) + + initForm: -> + form = $('.wall-note-form') + form.find("#target_type").val('wall') + + form.on 'ajax:success', => + @refresh() + form.find(".js-note-text").val("").trigger("input") + + form.on 'ajax:complete', -> + form.find(".js-comment-button").removeAttr('disabled') + form.find(".js-comment-button").removeClass('disabled') + + form.on "click", ".js-choose-note-attachment-button", -> + form.find(".js-note-attachment-input").click() + + form.on "change", ".js-note-attachment-input", -> + filename = $(this).val().replace(/^.*[\\\/]/, '') + form.find(".js-attachment-filename").text(filename) + + form.find('.note_text').keydown (e) -> + if e.ctrlKey && e.keyCode == 13 + form.find('.js-comment-button').submit() + + form.show() + + renderNote: (note) -> + template = @noteTemplate() + template = template.replace('{{author_name}}', note.author.name) + template = template.replace(/{{created_at}}/g, note.created_at) + template = template.replace('{{text}}', simpleFormat(note.body)) + + if note.attachment + file = '<i class="icon-paper-clip"/><a href="' + gon.relative_url_root + '/files/note/' + note.id + '/' + note.attachment + '">' + note.attachment + '</a>' + else + file = '' + template = template.replace('{{file}}', file) + + + $('ul.notes').append(template) + + noteTemplate: -> + return '<li> + <strong class="wall-author">{{author_name}}</strong> + <span class="wall-text"> + {{text}} + <span class="wall-file">{{file}}</span> + </span> + <abbr class="timeago" title="{{created_at}}">{{created_at}}</abbr> + </li>' + +@Wall = Wall diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee new file mode 100644 index 00000000000..17e790e5b7c --- /dev/null +++ b/app/assets/javascripts/wikis.js.coffee @@ -0,0 +1,12 @@ +class Wikis + constructor: -> + $('.build-new-wiki').bind "click", -> + field = $('#new_wiki_path') + slug = field.val() + path = field.attr('data-wikis-path') + + if(slug.length > 0) + location.href = path + "/" + slug + + +@Wikis = Wikis diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 6b500b88823..b1a23427add 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -5,6 +5,7 @@ *= require jquery.ui.gitlab *= require jquery.atwho *= require chosen + *= require select2 *= require_self */ @@ -14,7 +15,7 @@ @import "gitlab_bootstrap.scss"; @import "common.scss"; -@import "ref_select.scss"; +@import "selects.scss"; @import "sections/header.scss"; @import "sections/nav.scss"; @@ -33,9 +34,15 @@ @import "sections/login.scss"; @import "sections/editor.scss"; @import "sections/admin.scss"; +@import "sections/wiki.scss"; +@import "sections/wall.scss"; +@import "sections/dashboard.scss"; +@import "sections/stat_graph.scss"; @import "highlight/white.scss"; @import "highlight/dark.scss"; +@import "highlight/solarized_dark.scss"; +@import "highlight/monokai.scss"; /** * UI themes: diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index 7ac8c2dd91c..1572227ec3a 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -17,33 +17,14 @@ body { margin: 0 0; } -.visible_link, .author_link { color: $link_color; } .help li { color:$style_color; } -.back_link { - text-decoration: underline; +.back-link { font-size: 14px; - font-weight: bold; - padding: 10px 0; - padding-bottom: 0; -} - -.info_link { - margin-right: 5px; - float: left; - - img { - width: 20px; - } -} - -.download_repo_link { - background: url("images.png") no-repeat 0 -48px; - padding-left: 20px; } table a code { @@ -52,10 +33,6 @@ table a code { margin-right: 3px; } -.span12 hr{ - margin-top: 5px; -} - .loading { margin: 20px auto; background: url(ajax_loader.gif) no-repeat center center; @@ -67,32 +44,30 @@ table a code { } /** FLASH message **/ -#flash-container { - height: 50px; - position: fixed; - z-index: 10001; - top: 0px; - width: 100%; - margin-bottom: 15px; - overflow: hidden; - background: white; +.flash-container { + display: none; cursor: pointer; - border-bottom: 1px solid #ccc; + margin: 0; text-align: center; - display: none; + color: #fff; + font-size: 14px; + position: fixed; + bottom: 0; + width: 100%; + opacity: 0.8; + z-index: 100; - h4 { - color: #666; - font-size: 18px; - line-height: 38px; - padding-top: 5px; - margin: 2px; - font-weight: normal; + .flash-notice { + background: #49C; + padding: 10px; + text-shadow: 0 1px 1px #178; } -} -.git_url_wrapper { - margin-right:50px + .flash-alert { + background: #C67; + text-shadow: 0 1px 1px #945; + padding: 10px; + } } span.update-author { @@ -106,66 +81,18 @@ span.update-author { } } -.dashboard-loader { - float: left; - margin: 10px; - display: none; -} .user-mention { color: #2FA0BB; font-weight: bold; } -.neib { - margin-right: 10px; -} - .label { - padding: 0px 4px; - font-size: 10px; + padding: 1px 4px; + font-size: 12px; font-style: normal; - background-color: $link_color; - - &.label-success { - background-color: #8D8; - color: #333; - text-shadow: 0 1px 1px white; - } - - &.label-error { - background-color: #D88; - color: #333; - text-shadow: 0 1px 1px white; - } -} - -form { - @extend .form-horizontal; - - .actions { - @extend .form-actions; - } - - .clearfix { - @extend .control-group; - } - - .input { - @extend .controls; - } - - label { - @extend .control-label; - } - .xlarge { - @extend .input-xlarge; - } - .xxlarge { - @extend .input-xxlarge; - } + font-weight: normal; } - .field_with_errors { display: inline; } @@ -179,55 +106,10 @@ ul.breadcrumb { } a { - color: #474D57; - font-weight: bold; - font-size: 14px; - } - - .arrow { - background: url("images.png") no-repeat -85px -77px; - width: 19px; - height: 16px; - float: left; - position: relative; - left: -10px; - padding: 0; - margin: 0; - } -} - -input[type=text] { - &.large_text { - padding: 6px; font-size: 16px; } } -input.git_clone_url { - width: 325px; -} - -.merge-request-form-holder { - select { - width: 300px; - } -} - -/** Issues **/ -#issue_assignee_id { - width: 300px; -} - -#new_issue_dialog textarea{ - height: 100px; -} - -.project_list_url { - width: 250px; - background:#fff !important; -} - - .line_holder { &:hover { td { @@ -236,47 +118,12 @@ input.git_clone_url { } } -li.commit { - .avatar { - width: 24px; - top:-5px; - margin-right: 10px; - margin-left: 10px; - } - - code { - padding: 2px 2px 0; - margin-top: -2px; - &:hover { - color: black; - border: 1px solid #ccc; - } - } -} p.time { color: #999; font-size: 90%; margin: 30px 3px 3px 2px; } - -.styled_image { - border: 2px solid #ddd; -} - - - -/* Fix for readme code (stopped it from being yellow) */ -.readme { - pre { - background: white !important; - - code { - background: none !important; - } - } -} - .search-holder { label, input { height: 30px; @@ -303,23 +150,17 @@ p.time { top: -5px; @include border-radius(4px); + &.success { + background: #4A4; + color: #FFF; + } + &.error { background: #DA4E49; color: #FFF; } } -.arrow{ - background: #E3E5EA; - padding: 5px; - margin-top: 5px; - @include border-radius(5px); - text-shadow: none; - color: #999; - line-height: 16px; - font-weight: bold; -} - .thin_area{ height: 150px; } @@ -357,38 +198,6 @@ li.note { } } -.remember_me { - text-align: left; - - input { - margin: 0; - } - - span { - padding-left: 5px; - } -} - - - -/** - * Admin area - * - */ -.admin_dash { - .data { - a { - h1 { - line-height: 48px; - font-size: 48px; - padding: 20px; - text-align: center; - font-weight: normal; - } - } - } -} - .rss-icon { img { width: 24px; @@ -400,25 +209,12 @@ li.note { } } - - -/* CHZN reset few styles */ -.chzn-container-single .chzn-single { - background: #FFF; - border: 1px solid #bbb; - box-shadow: none; -} -.chzn-container-active .chzn-single { - background: #fff; -} - - .supp_diff_link, .show-all-commits { cursor: pointer; } -.merge_request, +.merge-request, .issue { &.today{ background: #EFE; @@ -445,14 +241,17 @@ li.note { } } -.error_message { - @extend .cred; - border-left: 4px solid #E99; +.error-message { padding: 10px; - margin-bottom: 10px; - background: #FEE; + background: #C67; padding-left: 20px; + margin: 0; + color: #FFF; + a { + color: #fff; + text-decoration: underline; + } &.centered { text-align: center; } @@ -518,27 +317,6 @@ pre { } } -.float-link { - float: left; - margin-right: 15px; - .s16 { - margin-right: 5px; - } -} - -.dashboard-search-filter { - padding:5px; - - .search-text-input { - float:left; - @extend .span2; - } - .btn { - margin-left: 5px; - float:left; - } -} - h1.http_status_code { font-size: 56px; line-height: 100px; @@ -568,3 +346,44 @@ img.emoji { .appear-data { display: none; } + +.chart { + overflow: hidden; + height: 220px; +} + +.navless-container { + margin-top: 20px; +} + +.description-block { + @extend .light-well; + @extend .light; + margin-bottom: 10px; +} + +.group-name { + font-size: 14px; + line-height: 24px; +} + +table { + td.permission-x { + background: #D9EDF7 !important; + text-align: center; + } +} + +.dashboard-intro-icon { + float: left; + font-size: 32px; + color: #AAA; + padding: 5px 0; + width: 50px; + min-height: 100px; +} + +.navbar-gitlab .navbar-inner .nav > li .btn-sign-in { + @extend .btn-new; + padding: 5px 15px; +} diff --git a/app/assets/stylesheets/gitlab_bootstrap.scss b/app/assets/stylesheets/gitlab_bootstrap.scss index 2ad1bf944a9..faf36b702c0 100644 --- a/app/assets/stylesheets/gitlab_bootstrap.scss +++ b/app/assets/stylesheets/gitlab_bootstrap.scss @@ -2,11 +2,49 @@ $baseFontSize: 13px !default; $baseLineHeight: 18px !default; -// BOOTSTRAP -@import "bootstrap"; +/** + * BOOTSTRAP + */ +@import "bootstrap/variables"; +@import "bootstrap/mixins"; +@import "bootstrap/reset"; +@import "bootstrap/scaffolding"; +@import "bootstrap/grid"; +@import "bootstrap/layouts"; +@import "bootstrap/type"; +@import "bootstrap/code"; +@import "bootstrap/forms"; +@import "bootstrap/tables"; +@import "bootstrap/sprites"; +@import "bootstrap/dropdowns"; +@import "bootstrap/wells"; +@import "bootstrap/component-animations"; +@import "bootstrap/close"; +@import "bootstrap/button-groups"; +@import "bootstrap/alerts"; +@import "bootstrap/navs"; +@import "bootstrap/navbar"; +@import "bootstrap/breadcrumbs"; +@import "bootstrap/pagination"; +@import "bootstrap/pager"; +@import "bootstrap/modals"; +@import "bootstrap/tooltip"; +@import "bootstrap/popovers"; +@import "bootstrap/thumbnails"; +@import "bootstrap/media"; +@import "bootstrap/labels-badges"; +@import "bootstrap/progress-bars"; +@import "bootstrap/accordion"; +@import "bootstrap/carousel"; +@import "bootstrap/hero-unit"; +@import "bootstrap/utilities"; @import "bootstrap/responsive-utilities"; @import "bootstrap/responsive-1200px-min"; +/** + * Font icons + * + */ @import "font-awesome"; /** @@ -24,5 +62,5 @@ $baseLineHeight: 18px !default; @import "gitlab_bootstrap/buttons.scss"; @import "gitlab_bootstrap/blocks.scss"; @import "gitlab_bootstrap/files.scss"; -@import "gitlab_bootstrap/tables.scss"; @import "gitlab_bootstrap/lists.scss"; +@import "gitlab_bootstrap/forms.scss"; diff --git a/app/assets/stylesheets/gitlab_bootstrap/avatar.scss b/app/assets/stylesheets/gitlab_bootstrap/avatar.scss index de1fb1551bf..c23970c13eb 100644 --- a/app/assets/stylesheets/gitlab_bootstrap/avatar.scss +++ b/app/assets/stylesheets/gitlab_bootstrap/avatar.scss @@ -1,8 +1,22 @@ -/** AVATARS **/ -img.avatar { float: left; margin-right: 12px; width: 40px; border: 1px solid #ddd; padding: 1px; } -img.avatar.s16 { width: 16px; height: 16px; margin-right: 6px; } -img.avatar.s24 { width: 24px; height: 24px; margin-right: 8px; } -img.avatar.s32 { width: 32px; height: 32px; margin-right: 10px; } -img.avatar.s90 { width: 90px; height: 90px; margin-right: 15px; } -img.lil_av { padding-left: 4px; padding-right: 3px; } -img.small { width: 80px; } +.avatar { + float: left; + margin-right: 12px; + width: 40px; + border: 1px solid #ddd; + padding: 1px; + + &.avatar-inline { + float: none; + margin-left: 3px; + + &.s16 { margin-right: 2px; } + &.s24 { margin-right: 2px; } + } + + &.s16 { width: 16px; height: 16px; margin-right: 6px; } + &.s24 { width: 24px; height: 24px; margin-right: 8px; } + &.s26 { width: 26px; height: 26px; margin-right: 8px; } + &.s32 { width: 32px; height: 32px; margin-right: 10px; } + &.s60 { width: 60px; height: 60px; margin-right: 12px; } + &.s90 { width: 90px; height: 90px; margin-right: 15px; } +} diff --git a/app/assets/stylesheets/gitlab_bootstrap/blocks.scss b/app/assets/stylesheets/gitlab_bootstrap/blocks.scss index 4d1b6446362..fcf1159cd33 100644 --- a/app/assets/stylesheets/gitlab_bootstrap/blocks.scss +++ b/app/assets/stylesheets/gitlab_bootstrap/blocks.scss @@ -10,14 +10,43 @@ * */ .ui-box { - background: #F9F9F9; - margin-bottom: 25px; + background: #FFF; + margin-bottom: 20px; border: 1px solid #CCC; - @include solid-shade; + word-wrap: break-word; + + &.small-box { + margin-bottom: 10px; + + .title { + font-size: 13px; + line-height: 30px; + + a { + color: #666; + &:hover { + text-decoration: underline; + } + } + } + } &.ui-box-show { margin:20px 0; background: #FFF; + + .control-group { + margin-bottom: 0; + } + } + + &.ui-box-danger { + .title { + @include linear-gradient(#F26E5E, #bd362f); + color: #fff; + text-shadow: 0 1px 1px #900; + font-weight: bold; + } } img { max-width: 100%; } @@ -32,14 +61,6 @@ .ui-box-body, .ui-box-bottom { padding: 15px; - word-wrap: break-word; - - pre { - background: none !important; - margin: 0; - border: none; - padding: 0; - } .clearfix { margin: 0; @@ -52,6 +73,7 @@ font-size: 18px; font-weight: normal; line-height: 28px; + margin: 0; } h3 { margin: 0; @@ -60,7 +82,6 @@ .ui-box-body { border: none; - font-size: 12px; background-color: #f5f5f5; border: none; border-top: 1px solid #eee; @@ -70,10 +91,6 @@ border-top: 1px solid #eee; } - &.white { - background: #fff; - } - ul { margin: 0; } @@ -84,10 +101,11 @@ color: #456; font-size: 16px; text-shadow: 0 1px 1px #fff; - padding: 0px 10px; - line-height: 36px; + padding: 0 10px; font-size: 14px; + line-height: 40px; font-weight: normal; + margin: 0; > a { text-shadow: 0 1px 1px #fff; @@ -98,8 +116,10 @@ margin-top: 0; } - .btn-tiny { - @include box-shadow(0 0px 0px 1px #f1f1f1); + .btn { + vertical-align: middle; + padding: 4px 12px; + @include box-shadow(0 0px 1px 1px #f2f2f2); } .nav-pills { @@ -129,15 +149,6 @@ margin-bottom: 0; padding: 5px 20px; } - .middle_title { - background: #f5f5f5; - margin:20px -20px; - padding: 0 20px; - border-top: 1px solid #eee; - border-bottom: 1px solid #eee; - font-size: 14px; - color: #777; - } } .row_title { @@ -148,4 +159,28 @@ text-decoration: underline; } } + + .form-holder { + padding-top: 20px; + form { + margin-bottom: 0; + legend { + text-indent: 10px; + } + .form-actions { + margin-bottom: 0; + } + } + } +} + +.tab-pane { + .ui-box { + margin: 3px 3px 25px 3px; + } +} + +.light-well { + background: #f9f9f9; + padding: 15px; } diff --git a/app/assets/stylesheets/gitlab_bootstrap/buttons.scss b/app/assets/stylesheets/gitlab_bootstrap/buttons.scss index 03497e32d26..9eb32ca95e6 100644 --- a/app/assets/stylesheets/gitlab_bootstrap/buttons.scss +++ b/app/assets/stylesheets/gitlab_bootstrap/buttons.scss @@ -1,65 +1,106 @@ .btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 13px; + line-height: $baseLineHeight; + text-align: center; + vertical-align: middle; + cursor: pointer; + border: 1px solid #BBB; + color: $style_color; + @include border-radius($baseBorderRadius); + @include box-shadow(inset 0 1px 0 rgba(255,255,255,.2)); @include linear-gradient(#f1f1f1, #e1e1e1); text-shadow: 0 1px 1px #FFF; - border-color: #BBB; + text-decoration: none; + &.hover, &:hover { + color: $style_color; background: #f1f1f1; - @include linear-gradient(#fAfAfA, #f1f1f1); border-color: #AAA; - color: #333; + text-decoration: none; + @include linear-gradient(#fAfAfA, #f1f1f1); } - &.btn-primary { - background: #2a79A3; - @include linear-gradient(#47A7b7, #2585b5); - border-color: #2A79A3; - color: #fff; - text-shadow: 0 1px 1px #268; - &:hover { - background: $primary_color; - color: #fff; - } + &.focus, + &:focus { + text-decoration: none; + @include box-shadow(inset 0 2px 4px rgba(0,0,0,.15)); + } - &.disabled { - color: #fff; - background: #29B; - } + &.active, + &:active { + background-image: none; + outline: 0; + text-decoration: none; + @include box-shadow(inset 0 2px 4px rgba(0,0,0,.15)); } - &.btn-info { - background: #5aB9C3; - border-color: $primary_color; - color: #fff; - text-shadow: 0 1px 1px #268; - &:hover { - background: $primary_color; - color: #fff; - } + &.disabled, + &[disabled] { + cursor: default; + background-image: none; + @include opacity(65); + @include box-shadow(none); + } - &.disabled { - color: #fff; - background: #29B; + &.btn-primary { + color: #FFF; + border-color: #189; + text-shadow: 0 1px 1px #189; + @include linear-gradient(#4AC, #289); + + &.hover, + &:hover, + &.disabled, + &[disabled] { + color: #FFF; + background: #389; } } - &.success { - @extend .btn-success; + &.btn-success { + color: #FFF; + border-color: #1A1; + text-shadow: 0 1px 1px #FFF; + text-shadow: 0 1px 1px #181; + @include linear-gradient(#62C452, #51a351); - &:hover { - @extend .btn-success; - background: #51a351; + + &.hover, + &:hover, + &.disabled, + &[disabled] { + color: #FFF; + background: #2A2; } + } + + &.btn-danger { + color: #FFF; + text-shadow: 0 1px 1px #811; + border-color: #BD362F; + @include linear-gradient(#EE5F5B, #BD362F); + - &.disabled { - color: #fff; - background: #2b2; + &.hover, + &:hover, + &.disabled, + &[disabled] { + color: #FFF; + background: #A22; } } + &.btn-new { + @extend .btn-success; + } + &.btn-create { @extend .wide; - @extend .success; + @extend .btn-success; } &.btn-save { @@ -70,12 +111,6 @@ &.btn-close, &.btn-remove { @extend .btn-danger; - border-color: #BD362F; - - &:hover { - color: #fff; - background: #EE4E49; - } } &.btn-cancel { @@ -87,13 +122,9 @@ padding-right: 20px; } - &.small { - @extend .btn-small; - } - - &.active { - border-color: #aaa; - background-color: #ccc; + &.btn-small { + padding: 2px 10px; + font-size: 12px; } &.btn-tiny { @@ -107,9 +138,4 @@ margin-right: 7px; float: left; } - - &.padded { - margin-right: 3px; - padding: 4px 10px 4px; - } } diff --git a/app/assets/stylesheets/gitlab_bootstrap/common.scss b/app/assets/stylesheets/gitlab_bootstrap/common.scss index dcfd610e2c4..bc6c786da50 100644 --- a/app/assets/stylesheets/gitlab_bootstrap/common.scss +++ b/app/assets/stylesheets/gitlab_bootstrap/common.scss @@ -1,18 +1,26 @@ /** COLORS **/ -.cgray { color:gray } -.cred { color:#D12F19 } -.cgreen { color:#4a2 } -.cblack { color:#111 } -.cdark { color:#444 } -.cwhite { color:#fff!important } -.bgred { background:#F2DEDE!important } +.cgray { color: gray } +.cred { color: #D12F19 } +.cgreen { color: #4a2 } +.cblue { color: #29A } +.cblack { color: #111 } +.cdark { color: #444 } +.cwhite { color: #fff!important } +.bgred { background: #F2DEDE!important } /** COMMON CLASSES **/ .left { float:left } -.append-bottom-10 { margin-bottom:10px } -.append-bottom-20 { margin-bottom:20px } + .prepend-top-10 { margin-top:10px } .prepend-top-20 { margin-top:20px } +.prepend-left-10 { margin-left:10px } +.prepend-left-20 { margin-left:20px } +.append-right-10 { margin-right:10px } +.append-right-20 { margin-right:20px } +.append-bottom-10 { margin-bottom:10px } +.append-bottom-20 { margin-bottom:20px } +.inline { display: inline-block } + .padded { padding:20px } .ipadded { padding:20px!important } .lborder { border-left:1px solid #eee } @@ -20,7 +28,7 @@ .hint { font-style: italic; color: #999; } .light { color: #888 } .tiny { font-weight: normal } -.vtop { vertical-align: top; } +.vtop { vertical-align: top !important; } /** ALERT MESSAGES **/ @@ -40,33 +48,42 @@ line-height: 36px; } -p.slead { color: #456; font-size: 16px; margin-bottom: 12px; font-weight: 200; line-height: 24px; } +.slead { + color: #666; + font-size: 14px; + margin-bottom: 12px; + font-weight: normal; + line-height: 24px; +} + + +.tab-content { + overflow: visible; +} -/** FORMS **/ -input[type='search'].search-text-input { - background-image: url("icon-search.png"); - background-repeat: no-repeat; - background-position: 10px; - padding-left: 25px; - @include border-radius(4px); - border: 1px solid #ccc; +@media (max-width: 1200px) { + .only-wide { + display: none; + } } -input[type='text'].danger { - background: #F2DEDE!important; - border-color: #D66; - text-shadow: 0 1px 1px #fff +.pagination ul > li > a, .pagination ul > li >span { + @include linear-gradient(#f1f1f1, #e1e1e1); + color: #333; + text-shadow: 0 1px 1px #FFF; } -fieldset legend { font-size: 17px; } +pre.well-pre { + border: 1px solid #EEE; + background: #f9f9f9; + border-radius: 0; + color: #555; +} -/** PAGINATION **/ -.gitlab_pagination { - span a { color: $link_color; } - .prev, .next, .current, .page a { - padding: 10px; - } - .current { - border-bottom: 2px solid $style_color; - } +.input-append .btn.active, .input-prepend .btn.active { + background: #CCC; + border-color: #BBB; + text-shadow: 0 1px 1px #fff; + font-weight: bold; + @include box-shadow(inset 0 2px 4px rgba(0,0,0,.15)); } diff --git a/app/assets/stylesheets/gitlab_bootstrap/files.scss b/app/assets/stylesheets/gitlab_bootstrap/files.scss index 279cfcd2103..8ba8c93e3d6 100644 --- a/app/assets/stylesheets/gitlab_bootstrap/files.scss +++ b/app/assets/stylesheets/gitlab_bootstrap/files.scss @@ -2,19 +2,23 @@ * File content holder * */ -.file_holder { - border: 1px solid #BBB; +.file-holder { + border: 1px solid #CCC; margin-bottom: 1em; - @include solid-shade; - .file_title { + table { + @extend .table; + } + + .file-title { border-bottom: 1px solid #bbb; @include bg-dark-gray-gradient; + text-shadow: 0 1px 1px #fff; margin: 0; font-weight: normal; font-weight: bold; text-align: left; - color: #666; + color: $style_color; padding: 9px 10px; height: 18px; @@ -33,7 +37,7 @@ } } } - .file_content { + .file-content { background: #fff; font-size: 11px; @@ -48,7 +52,17 @@ &.wiki { padding: 20px; - font-size: 13px; + font-size: 14px; + line-height: 1.6; + + .highlight { + margin-bottom: 9px; + @include border-radius(4px); + + > pre { + margin: 0; + } + } } &.blob_file { @@ -162,6 +176,7 @@ color: #666; padding: 10px 6px 10px 0; text-align: right; + background: #EEE; a { color: #666; diff --git a/app/assets/stylesheets/gitlab_bootstrap/fonts.scss b/app/assets/stylesheets/gitlab_bootstrap/fonts.scss index a0c9a6c7b8a..8cc9986415c 100644 --- a/app/assets/stylesheets/gitlab_bootstrap/fonts.scss +++ b/app/assets/stylesheets/gitlab_bootstrap/fonts.scss @@ -1,7 +1,2 @@ -@font-face{ - font-family: Yanone; - src: font-url('YanoneKaffeesatz-Light.ttf'); -} - /** Typo **/ $monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'Courier New', 'andale mono', 'lucida console', monospace; diff --git a/app/assets/stylesheets/gitlab_bootstrap/forms.scss b/app/assets/stylesheets/gitlab_bootstrap/forms.scss new file mode 100644 index 00000000000..a2612166c78 --- /dev/null +++ b/app/assets/stylesheets/gitlab_bootstrap/forms.scss @@ -0,0 +1,51 @@ +form { + @extend .form-horizontal; + + label { + @extend .control-label; + } +} + +input.input-xpadding, +.add-on.input-xpadding { + padding: 6px 10px; +} + +.control-group { + .control-label { + padding-top: 6px; + } + .controls { + input, textarea { + padding: 6px 10px; + } + + input[type="radio"], input[type="checkbox"] { + margin-top: 6px; + } + + .add-on { + padding: 6px; + } + } +} + +input[type='search'].search-text-input { + background-image: url("icon-search.png"); + background-repeat: no-repeat; + background-position: 10px; + padding-left: 25px; + @include border-radius(4px); + border: 1px solid #ccc; +} + +input[type='text'].danger { + background: #F2DEDE!important; + border-color: #D66; + text-shadow: 0 1px 1px #fff +} + +fieldset legend { + font-size: 16px; + margin-bottom: 10px; +} diff --git a/app/assets/stylesheets/gitlab_bootstrap/lists.scss b/app/assets/stylesheets/gitlab_bootstrap/lists.scss index 0f893a553ee..83066b5beec 100644 --- a/app/assets/stylesheets/gitlab_bootstrap/lists.scss +++ b/app/assets/stylesheets/gitlab_bootstrap/lists.scss @@ -6,7 +6,6 @@ margin: 0; list-style: none; li { - background-color: #FFF; padding: 10px; min-height: 20px; border-bottom: 1px solid #eee; @@ -16,6 +15,12 @@ color: #888; } + &.unstyled { + &:hover { + background: none; + } + } + &.smoke { background-color: #f5f5f5; } &:hover { @@ -69,5 +74,22 @@ ul.bordered-list { display: block; margin: 0px; &:last-child { border:none } + &.active { + background: #f9f9f9; + a { font-weight: bold; } + } + + &.light { + a { color: #777; } + } + } + + &.top-list { + li:first-child { + padding-top: 0; + h4, h5 { + margin-top: 0; + } + } } } diff --git a/app/assets/stylesheets/gitlab_bootstrap/mixins.scss b/app/assets/stylesheets/gitlab_bootstrap/mixins.scss index 9b1e2f2c728..8b975a12cf7 100644 --- a/app/assets/stylesheets/gitlab_bootstrap/mixins.scss +++ b/app/assets/stylesheets/gitlab_bootstrap/mixins.scss @@ -17,6 +17,10 @@ border-radius: $radius; } +@mixin border-radius-left($radius) { + @include border-radius($radius 0 0 $radius) +} + @mixin linear-gradient($from, $to) { background-image: -webkit-gradient(linear, 0 0, 0 100%, from($from), to($to)); background-image: -webkit-linear-gradient($from, $to); @@ -24,6 +28,14 @@ background-image: -o-linear-gradient($from, $to); } +@mixin transition($transition) { + -webkit-transition: $transition; + -moz-transition: $transition; + -ms-transition: $transition; + -o-transition: $transition; + transition: $transition; +} + /** * Prefilled mixins * Mixins with fixed values @@ -62,8 +74,41 @@ @mixin header-font { color: $style_color; text-shadow: 0 1px 1px #FFF; - font-family: 'Yanone', sans-serif; - font-size: 26px; - line-height: 42px; + font-size: 16px; + line-height: 40px; font-weight: normal; } + +@mixin md-typography { + *:first-child { + margin-top: 0; + } + + code { padding: 0 4px; } + h1 { margin-top: 30px;} + h2 { margin-top: 25px;} + h3 { margin-top: 20px;} + h4 { margin-top: 15px;} + + blockquote p { + color: #888; + font-size: 14px; + line-height: 1.5; + } + + table { + @extend .table; + @extend .table-bordered; + th { + background: #EEE; + } + } +} + +@mixin page-title { + color: $style_color; + font-size: 20px; + line-height: 1.5; + margin-top: 0px; + margin-bottom: 15px; +} diff --git a/app/assets/stylesheets/gitlab_bootstrap/nav.scss b/app/assets/stylesheets/gitlab_bootstrap/nav.scss index 2eaef61ca33..aa4cb1ed5fd 100644 --- a/app/assets/stylesheets/gitlab_bootstrap/nav.scss +++ b/app/assets/stylesheets/gitlab_bootstrap/nav.scss @@ -10,17 +10,32 @@ > li > a { @include border-radius(0); } + &.nav-stacked { > li > a { border-left: 4px solid #EEE; padding: 12px; } > .active > a { - border-color: #29B; + border-color: $primary_color; border-radius: 0; background: #F1F1F1; color: $style_color; font-weight: bold; + text-shadow: 0 1px 1px #fff; + } + + &.nav-stacked-menu { + background: #FAFAFA; + li > a { + padding: 16px; + } + } + } + + &.nav-pills-small { + > li > a { + padding: 8px 12px; } } } @@ -57,9 +72,21 @@ border-color: #CCC; border-bottom: 1px solid #fff; color: #333; + font-weight: bold; } } } &.nav-small-tabs > li > a { padding: 6px 9px; } } + + + +/** + * fix to keep tooltips position in top navigation bar + * + */ +.navbar .nav > li { + position: relative; + white-space: nowrap; +} diff --git a/app/assets/stylesheets/gitlab_bootstrap/tables.scss b/app/assets/stylesheets/gitlab_bootstrap/tables.scss deleted file mode 100644 index 7a9eac82566..00000000000 --- a/app/assets/stylesheets/gitlab_bootstrap/tables.scss +++ /dev/null @@ -1,63 +0,0 @@ -table { - @extend .table; - @extend .table-striped; - @include solid-shade; - border: 1px solid #bbb; - width: 100%; - - &.low { - td { - line-height: 18px; - } - } - - th { - font-weight: bold; - vertical-align: middle; - border-bottom: 1px solid #bbb; - text-shadow: 0 1px 1px #fff; - @include bg-dark-gray-gradient; - - ul.nav { - text-shadow: none; - margin: 0; - } - } - - th, td { - padding: 10px; - line-height: 18px; - text-align: left; - } - - td { - border-color: #f1f1f1; - line-height: 28px; - - .s16 { - margin-top: 5px; - margin-right: 5px; - } - - &:first-child { - border-left: 1px solid #bbb; - } - - &:last-child { - border-right: 1px solid #bbb; - } - } - - &.bordered { - @extend .table-bordered; - } - - &.lite { - border: none; - box-shadow: none; - tr, td { - border: none; - background:none !important; - } - } -} diff --git a/app/assets/stylesheets/gitlab_bootstrap/typography.scss b/app/assets/stylesheets/gitlab_bootstrap/typography.scss index 781577c2147..d3986556376 100644 --- a/app/assets/stylesheets/gitlab_bootstrap/typography.scss +++ b/app/assets/stylesheets/gitlab_bootstrap/typography.scss @@ -2,16 +2,23 @@ * Headers * */ +h1, h2, h3, h4, h5, h6 { + font-weight: 500; + line-height: 1.1; +} + +h1.page-title { + @include page-title; + font-size: 28px; +} -h1, h2, h3, h4, h5, h6 { margin: 0; } -h3, h4, h5, h6 { line-height: 36px; } -h5 { font-size: 14px; } +h2.page-title { + @include page-title; + font-size: 24px; +} -h3.page_title { - color: #456; - font-size: 20px; - font-weight: normal; - line-height: 28px; +h3.page-title { + @include page-title; } h6 { @@ -41,11 +48,8 @@ a { color: $primary_color; } - &.btn { - color: $style_color; - &:hover { - color: $style_color; - } + &:focus { + text-decoration: underline; } &.dark { @@ -87,16 +91,18 @@ a:focus { * */ .wiki { - font-size: 13px; - - code { padding: 0 4px; } - p { font-size: 13px; } - h1 { font-size: 32px; line-height: 40px; margin: 10px 0;} - h2 { font-size: 26px; line-height: 40px; margin: 10px 0;} - h3 { font-size: 22px; line-height: 40px; margin: 10px 0;} - h4 { font-size: 18px; line-height: 20px; margin: 10px 0;} - h5 { font-size: 14px; line-height: 20px; margin: 10px 0;} - h6 { font-size: 12px; line-height: 20px; margin: 10px 0;} - .white .highlight pre { background: #f5f5f5; } - ul { margin: 0 0 9px 25px !important; } + @include md-typography; + + font-size: 14px; + line-height: 1.6; + .white .highlight pre { + background: #f5f5f5; + } + ul { + margin: 0 0 9px 25px !important; + } +} + +.md { + @include md-typography; } diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 6018ff7074d..129d33dcac3 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -1,8 +1,10 @@ -.black .highlight { +.dark .highlight { + background-color: #333; + pre { + background-color: #333; color: #eee; - background: inherit; } .hll { display: block; background-color: darken($hover, 65%) } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss new file mode 100644 index 00000000000..c9709fa7f12 --- /dev/null +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -0,0 +1,89 @@ +$monokai-fg: #f8f8f2; +$monokai-comment: #75715e; +$monokai-pink: #f92672; +$monokai-blue: #66d9ef; +$monokai-green: #a6e22e; +$monokai-gold: #e6db74; +$monokai-dark: #3b3a32; +$monokai-purple: #ae81ff; + +.monokai .highlight { + + background-color: #272822; + + pre { + background-color: #272822; + color: $monokai-fg; + } + + .hll { background-color: darken($hover, 65%) } + .c { color: $monokai-comment } /* Comment */ + .err { color: $monokai-fg } /* Error */ + .g { color: $monokai-fg } /* Generic */ + .k { color: $monokai-pink } /* Keyword */ + .l { color: $monokai-fg } /* Literal */ + .n { color: $monokai-blue } /* Name */ + .o { color: $monokai-fg } /* Operator */ + .x { color: $monokai-fg } /* Other */ + .p { color: $monokai-fg } /* Punctuation */ + .cm { color: $monokai-comment } /* Comment.Multiline */ + .cp { color: $monokai-comment } /* Comment.Preproc */ + .c1 { color: $monokai-comment } /* Comment.Single */ + .cs { color: $monokai-comment } /* Comment.Special */ + .gd { color: #8b0807 } /* Generic.Deleted */ + .ge { color: $monokai-fg; text-decoration: underline } /* Generic.Emph */ + .gr { color: $monokai-fg } /* Generic.Error */ + .gh { color: $monokai-fg; font-weight: bold } /* Generic.Heading */ + .gi { color: $monokai-fg; font-weight: bold; background-color: #46830c } /* Generic.Inserted */ + .go { color: $monokai-dark; background-color: #31322c } /* Generic.Output */ + .gp { color: $monokai-fg } /* Generic.Prompt */ + .gs { color: $monokai-fg } /* Generic.Strong */ + .gu { color: $monokai-fg; font-weight: bold } /* Generic.Subheading */ + .gt { color: #f8f8f0; background-color: $monokai-pink } /* Generic.Traceback */ + .kc { color: $monokai-purple } /* Keyword.Constant */ + .kd { color: $monokai-pink } /* Keyword.Declaration */ + .kn { color: $monokai-pink } /* Keyword.Namespace */ + .kp { color: $monokai-pink } /* Keyword.Pseudo */ + .kr { color: $monokai-pink } /* Keyword.Reserved */ + .kt { color: $monokai-fg } /* Keyword.Type */ + .ld { color: $monokai-fg } /* Literal.Date */ + .m { color: $monokai-purple } /* Literal.Number */ + .s { color: $monokai-gold } /* Literal.String */ + .na { color: $monokai-purple } /* Name.Attribute */ + .nb { color: $monokai-blue } /* Name.Builtin */ + .nc { color: $monokai-fg } /* Name.Class */ + .no { color: $monokai-fg } /* Name.Constant */ + .nd { color: $monokai-fg } /* Name.Decorator */ + .ni { color: $monokai-fg } /* Name.Entity */ + .ne { color: $monokai-fg } /* Name.Exception */ + .nf { color: $monokai-green } /* Name.Function */ + .nl { color: $monokai-gold } /* Name.Label */ + .nn { color: $monokai-fg } /* Name.Namespace */ + .nx { color: $monokai-fg } /* Name.Other */ + .nt { color: $monokai-pink } /* Name.Tag */ + .nv { color: $monokai-blue; font-style: italic } /* Name.Variable */ + .py { color: $monokai-fg } /* Name.Property */ + .ow { color: $monokai-pink } /* Operator.Word */ + .w { color: $monokai-fg } /* Text.Whitespace */ + .mf { color: $monokai-purple } /* Literal.Number.Float */ + .mh { color: $monokai-purple } /* Literal.Number.Hex */ + .mi { color: $monokai-purple } /* Literal.Number.Integer */ + .mo { color: $monokai-purple } /* Literal.Number.Oct */ + .sb { color: $monokai-gold } /* Literal.String.Backtick */ + .sc { color: $monokai-gold } /* Literal.String.Char */ + .sd { color: $monokai-gold } /* Literal.String.Doc */ + .s2 { color: $monokai-gold } /* Literal.String.Double */ + .se { color: $monokai-gold } /* Literal.String.Escape */ + .sh { color: $monokai-gold } /* Literal.String.Heredoc */ + .si { color: $monokai-gold } /* Literal.String.Interpol */ + .sx { color: $monokai-gold } /* Literal.String.Other */ + .sr { color: $monokai-gold } /* Literal.String.Regex */ + .s1 { color: $monokai-gold } /* Literal.String.Single */ + .ss { color: $monokai-gold } /* Literal.String.Symbol */ + .bp { color: $monokai-fg } /* Name.Builtin.Pseudo */ + .vc { color: $monokai-blue; font-style: italic } /* Name.Variable.Class */ + .vg { color: $monokai-blue; font-style: italic } /* Name.Variable.Global */ + .vi { color: $monokai-blue; font-style: italic } /* Name.Variable.Instance */ + .il { color: $monokai-purple } /* Literal.Number.Integer.Long */ +} + diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss new file mode 100644 index 00000000000..cc82f39ac93 --- /dev/null +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -0,0 +1,80 @@ +.solarized-dark .highlight { + + background-color: #002B36; + + pre { + background-color: #002B36; + color: #eee; + } + + .hll { background-color: #073642 } + .c { color: #586E75 } /* Comment */ + .err { color: #93A1A1 } /* Error */ + .g { color: #93A1A1 } /* Generic */ + .k { color: #859900 } /* Keyword */ + .l { color: #93A1A1 } /* Literal */ + .n { color: #93A1A1 } /* Name */ + .o { color: #859900 } /* Operator */ + .x { color: #CB4B16 } /* Other */ + .p { color: #93A1A1 } /* Punctuation */ + .cm { color: #586E75 } /* Comment.Multiline */ + .cp { color: #859900 } /* Comment.Preproc */ + .c1 { color: #586E75 } /* Comment.Single */ + .cs { color: #859900 } /* Comment.Special */ + .gd { color: #2AA198 } /* Generic.Deleted */ + .ge { color: #93A1A1; font-style: italic } /* Generic.Emph */ + .gr { color: #DC322F } /* Generic.Error */ + .gh { color: #CB4B16 } /* Generic.Heading */ + .gi { color: #859900 } /* Generic.Inserted */ + .go { color: #93A1A1 } /* Generic.Output */ + .gp { color: #93A1A1 } /* Generic.Prompt */ + .gs { color: #93A1A1; font-weight: bold } /* Generic.Strong */ + .gu { color: #CB4B16 } /* Generic.Subheading */ + .gt { color: #93A1A1 } /* Generic.Traceback */ + .kc { color: #CB4B16 } /* Keyword.Constant */ + .kd { color: #268BD2 } /* Keyword.Declaration */ + .kn { color: #859900 } /* Keyword.Namespace */ + .kp { color: #859900 } /* Keyword.Pseudo */ + .kr { color: #268BD2 } /* Keyword.Reserved */ + .kt { color: #DC322F } /* Keyword.Type */ + .ld { color: #93A1A1 } /* Literal.Date */ + .m { color: #2AA198 } /* Literal.Number */ + .s { color: #2AA198 } /* Literal.String */ + .na { color: #93A1A1 } /* Name.Attribute */ + .nb { color: #B58900 } /* Name.Builtin */ + .nc { color: #268BD2 } /* Name.Class */ + .no { color: #CB4B16 } /* Name.Constant */ + .nd { color: #268BD2 } /* Name.Decorator */ + .ni { color: #CB4B16 } /* Name.Entity */ + .ne { color: #CB4B16 } /* Name.Exception */ + .nf { color: #268BD2 } /* Name.Function */ + .nl { color: #93A1A1 } /* Name.Label */ + .nn { color: #93A1A1 } /* Name.Namespace */ + .nx { color: #93A1A1 } /* Name.Other */ + .py { color: #93A1A1 } /* Name.Property */ + .nt { color: #268BD2 } /* Name.Tag */ + .nv { color: #268BD2 } /* Name.Variable */ + .ow { color: #859900 } /* Operator.Word */ + .w { color: #93A1A1 } /* Text.Whitespace */ + .mf { color: #2AA198 } /* Literal.Number.Float */ + .mh { color: #2AA198 } /* Literal.Number.Hex */ + .mi { color: #2AA198 } /* Literal.Number.Integer */ + .mo { color: #2AA198 } /* Literal.Number.Oct */ + .sb { color: #586E75 } /* Literal.String.Backtick */ + .sc { color: #2AA198 } /* Literal.String.Char */ + .sd { color: #93A1A1 } /* Literal.String.Doc */ + .s2 { color: #2AA198 } /* Literal.String.Double */ + .se { color: #CB4B16 } /* Literal.String.Escape */ + .sh { color: #93A1A1 } /* Literal.String.Heredoc */ + .si { color: #2AA198 } /* Literal.String.Interpol */ + .sx { color: #2AA198 } /* Literal.String.Other */ + .sr { color: #DC322F } /* Literal.String.Regex */ + .s1 { color: #2AA198 } /* Literal.String.Single */ + .ss { color: #2AA198 } /* Literal.String.Symbol */ + .bp { color: #268BD2 } /* Name.Builtin.Pseudo */ + .vc { color: #268BD2 } /* Name.Variable.Class */ + .vg { color: #268BD2 } /* Name.Variable.Global */ + .vi { color: #268BD2 } /* Name.Variable.Instance */ + .il { color: #2AA198 } /* Literal.Number.Integer.Long */ +} + diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index f200e1d7b60..df127a7c491 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -1,4 +1,7 @@ .white .highlight { + + background-color: #fff; + pre { background-color: #fff; color: #333; diff --git a/app/assets/stylesheets/ref_select.scss b/app/assets/stylesheets/ref_select.scss deleted file mode 100644 index 284d1c324e5..00000000000 --- a/app/assets/stylesheets/ref_select.scss +++ /dev/null @@ -1,90 +0,0 @@ -/** Branch/tag selector **/ -.project-refs-form { - margin: 0; - span { - background:none !important; - position:static !important; - width:auto !important; - height:auto !important; - } -} -.project-refs-select { - width: 120px; -} - -.project-refs-form .chzn-container { - position: relative; - top: 0; - left: 0; - margin-right: 10px; - - .chzn-drop { - min-width: 400px; - .chzn-results { - max-height: 300px; - } - .chzn-search input { - min-width: 365px; - } - } -} - -/** Fix for Search Dropdown Border **/ -.chzn-container { - .chzn-search { - input:focus { - @include box-shadow(none); - } - } - - .chzn-drop { - margin: 7px 0; - min-width: 200px; - border: 1px solid #bbb; - @include border-radius(0); - - .chzn-results { - margin-top: 5px; - max-height: 300px; - - .group-result { - color: $style_color; - border-bottom: 1px solid #EEE; - padding: 8px; - } - .active-result { - @include border-radius(0); - - &.highlighted { - background: $hover; - color: $style_color; - } - &.result-selected { - background: #EEE; - border-left: 4px solid #CCC; - } - } - } - - .chzn-search { - @include bg-gray-gradient; - input { - min-width: 165px; - border-color: #CCC; - } - } - } - - .chzn-single { - @include bg-light-gray-gradient; - - div { - background: transparent; - border-left: none; - } - - span { - font-weight: normal; - } - } -} diff --git a/app/assets/stylesheets/sections/admin.scss b/app/assets/stylesheets/sections/admin.scss index 18b102d7022..e189fd27ac6 100644 --- a/app/assets/stylesheets/sections/admin.scss +++ b/app/assets/stylesheets/sections/admin.scss @@ -1,3 +1,21 @@ +/** + * Admin area + * + */ +.admin_dash { + .data { + a { + h1 { + line-height: 48px; + font-size: 48px; + padding: 20px; + text-align: center; + font-weight: normal; + } + } + } +} + .admin-filter form { label { width: 110px; } .controls { margin-left: 130px; } diff --git a/app/assets/stylesheets/sections/commits.scss b/app/assets/stylesheets/sections/commits.scss index 8b93287ed1e..be6fb29c817 100644 --- a/app/assets/stylesheets/sections/commits.scss +++ b/app/assets/stylesheets/sections/commits.scss @@ -29,7 +29,7 @@ a{ color: $style_color; } - + > span { font-family: $monospace_font; font-size: 14px; @@ -67,6 +67,7 @@ } table { + width: 100%; font-family: $monospace_font; border: none; margin: 0px; @@ -99,11 +100,24 @@ } } } + .line_holder { + &.old .old_line, + &.old .new_line { + background: #FCC; + border-color: #E7BABA; + } + &.new .old_line, + &.new .new_line { + background: #CFC; + border-color: #B9ECB9; + } + } .line_content { + display: block; white-space: pre; - height: 14px; + height: 18px; margin: 0px; - padding: 0px; + padding: 0px 0.5em; border: none; &.new { background: #CFD; @@ -124,7 +138,7 @@ .wrap{ display: inline-block; } - + .frame { display: inline-block; background-color: #fff; @@ -149,7 +163,7 @@ .view.swipe{ position: relative; - + .swipe-frame{ display: block; margin: auto; @@ -228,7 +242,7 @@ bottom: 0px; left: 50%; margin-left: -150px; - + .drag-track{ display: block; position: absolute; @@ -237,7 +251,7 @@ width: 276px; background: url('onion_skin_sprites.gif') -4px -20px repeat-x; } - + .dragger { display: block; position: absolute; @@ -248,7 +262,7 @@ background: url('onion_skin_sprites.gif') 0px -34px repeat-x; cursor: pointer; } - + .transparent { display: block; position: absolute; @@ -258,7 +272,7 @@ width: 10px; background: url('onion_skin_sprites.gif') -2px 0px no-repeat; } - + .opaque { display: block; position: absolute; @@ -275,19 +289,19 @@ padding: 10px; text-align: center; - + background-image: -webkit-gradient(linear, 0 0, 0 30, color-stop(0.066, #eee), to(#dfdfdf)); background-image: -webkit-linear-gradient(#eee 6.6%, #dfdfdf); background-image: -moz-linear-gradient(#eee 6.6%, #dfdfdf); background-image: -o-linear-gradient(#eee 6.6%, #dfdfdf); - + ul, li{ list-style: none; margin: 0; padding: 0; display: inline-block; } - + li{ color: grey; border-left: 1px solid #c1c1c1; @@ -322,66 +336,40 @@ } .commit-author, .commit-committer{ display: block; - color: #999; - font-weight: normal; + color: #999; + font-weight: normal; font-style: italic; } .commit-author strong, .commit-committer strong{ - font-weight: bold; + font-weight: bold; font-style: normal; } -/** - * COMMIT ROW - */ -.commit { - .browse_code_link_holder { - @extend .span2; - float: right; - } - - .committed_ago { - float: right; - @extend .cgray; - } - - .notes_count { - float: right; - margin: -6px 8px 6px; - } - - code { - background: #FCEEC1; - color: $style_color; - } - - .commit_short_id { - float: left; - @extend .lined; - min-width: 65px; - font-family: $monospace_font; - } -} - .file-stats a { color: $style_color; } .file-stats { - .new-file{ - i{ + .new-file { + a { + color: #090; + } + i { color: #1BCF00; } } - .renamed-file{ - i{ + .renamed-file { + i { color: #FE9300; } } - .deleted-file{ - i{ - color: #FF0000; + .deleted-file { + a { + color: #B00; + } + i { + color: #EE0000; } } .edit-file{ @@ -403,8 +391,8 @@ .commits-compare-switch{ background: url("switch_icon.png") no-repeat center center; - width: 16px; - height: 18px; + width: 22px; + height: 22px; text-indent: -9999px; float: left; margin-right: 9px; @@ -413,3 +401,108 @@ padding: 4px; background-color: #EEE; } + +.commit-description { + background: none; + border: none; + margin: 0; + padding: 0; + margin-top: 10px; +} + +.commit-box { + margin: 10px 0; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + padding: 20px 0; + + .commit-title { + margin: 0; + font-size: 20px; + font-weight: bold; + } + + .commit-description { + margin-top: 15px; + } +} + + +.commit-stat-summary { + color: #666; + line-height: 2; +} + +.commit-breadcrumb { + padding: 0; +} + +.commit-info-row { + margin-bottom: 10px; + .avatar { + @extend .avatar-inline; + } + .commit-committer-link, + .commit-author-link { + color: #444; + font-weight: bold; + } +} + +.lists-separator { + margin: 10px 0; + border-top: 1px dashed #CCC; +} + +/** + * COMMIT ROW + */ +li.commit { + padding: 8px; + + .commit-row-title { + font-size: 14px; + margin-bottom: 2px; + + .notes_count { + float: right; + margin-right: 10px; + } + + .commit_short_id { + min-width: 65px; + font-family: $monospace_font; + } + + .commit-row-message { + color: #555; + font-weight: bolder; + &:hover { + color: #444; + text-decoration: underline; + } + } + } + + .commit-row-info { + a { + color: #777; + } + + .committed_ago { + float: right; + @extend .cgray; + } + } + + &.inline-commit { + .commit-row-title { + font-size: 13px; + } + + .committed_ago { + float: right; + @extend .cgray; + } + } +}
\ No newline at end of file diff --git a/app/assets/stylesheets/sections/dashboard.scss b/app/assets/stylesheets/sections/dashboard.scss new file mode 100644 index 00000000000..3f7825d71ce --- /dev/null +++ b/app/assets/stylesheets/sections/dashboard.scss @@ -0,0 +1,102 @@ +.dashboard { + @extend .row; + .activities { + } + + .side { + @extend .pull-right; + + .ui-box { + margin: 0px; + box-shadow: none; + + .nav-projects-tabs li { padding: 0; } + } + } +} + +.dashboard-search-filter { + padding:5px; + + .search-text-input { + float:left; + @extend .span2; + } + .btn { + margin-left: 5px; + float:left; + } +} + +.dashboard { + .dash-filter { + margin: 7px 0; + padding: 4px 6px; + width: 202px; + float: left; + } +} + +@media (max-width: 1200px) { + .dashboard .dash-filter { + width: 132px; + } +} + +.dash-sidebar-tabs { + margin-bottom: 2px; + border: none; + margin: 0; + + li { + &.active { + a { + @include linear-gradient(#f5f5f5, #eee); + border-bottom: 1px solid #EEE !important; + &:hover { + background: #eee; + } + } + } + + a { + border-color: #CCC !important; + } + } +} + +.project-row, .group-row { + padding: 10px 15px !important; + + .namespace-name { + color: #666; + font-weight: bold; + } + + .project-name, .group-name { + font-size: 16px; + } + + .arrow { + float: right; + padding: 10px 5px; + margin: 0; + font-size: 20px; + color: #666; + } + + .last-activity { + color: #AAA; + display: block; + margin-top: 5px; + .date { + color: #777; + } + } +} + +.group-row { + .arrow { + padding: 2px 5px; + } +} diff --git a/app/assets/stylesheets/sections/events.scss b/app/assets/stylesheets/sections/events.scss index df8fd8d6458..39b2ad7a09c 100644 --- a/app/assets/stylesheets/sections/events.scss +++ b/app/assets/stylesheets/sections/events.scss @@ -31,49 +31,64 @@ * */ .event-item { + &:first-child { + padding-top: 0; + } + + &.event-inline { + .avatar { + width: 16px; + height: 16px; + } + } + + padding: 14px 0px; border-bottom: 1px solid #eee; .event-title { color: #333; - font-weight: bold; + font-weight: normal; + font-size: 15px; .author_name { color: #333; } } .event-body { - .commit p { - color: #555; - padding-top: 5px; - } + margin-left: 35px; + margin-right: 100px; + .event-info { color: #666; } .event-note { - padding-top: 5px; - padding-left: 5px; - display: inline-block; color: #555; + margin-top: 5px; - .note-file-attach { - margin-left: -25px; - float: left; - .note-image-attach { - margin-left: 0px; - max-width: 200px; - } + pre { + border: none; + background: #f9f9f9; + border-radius: 0; + color: #555; + margin: 0 20px; + } + + .note-image-attach { + margin-top: 4px; + margin-left: 0px; + max-width: 200px; + } + + p:last-child { + margin-bottom: 0; } } .event-note-icon { color: #777; float: left; font-size: 16px; - line-height: 18px; - margin: 5px; + line-height: 16px; + margin-right: 5px; } } - .avatar { - position: relative; - top: -3px; - } .event_icon { position: relative; float: right; @@ -87,16 +102,7 @@ width: 20px; } } - ul { - margin-left: 50px; - margin-bottom: 5px; - .avatar { - width: 18px; - margin-top: 3px; - } - } - padding: 16px 5px; &:last-child { border:none } .event_commits { @@ -107,41 +113,24 @@ background: transparent; padding: 3px; border: none; - font-size: 12px; + color: #666; + .commit-row-title { + font-size: 12px; + } } &.commits-stat { display: block; - margin-top: 5px; + padding: 3px; + + &:hover { + background: none; + } } } } } /** - * Push event widget - * - */ -.event_lp { - color: #777; - padding: 10px; - min-height: 22px; - border-left: 5px solid #5AB9C3; - margin-bottom: 20px; - background: #f9f9f9; - - .avatar { - width: 24px; - } - - .btn-new-mr { - @extend .btn-info; - @extend .small; - @extend .pull-right; - margin: -3px; - } -} - -/** * Event filter * */ @@ -153,7 +142,7 @@ .filter_icon { a { text-align:center; - border-left: 3px solid #29B; + border-left: 3px solid $primary_color; background: #f9f9f9; margin-bottom: 10px; float: left; diff --git a/app/assets/stylesheets/sections/graph.scss b/app/assets/stylesheets/sections/graph.scss index 5800098ade4..1e22d161bfc 100644 --- a/app/assets/stylesheets/sections/graph.scss +++ b/app/assets/stylesheets/sections/graph.scss @@ -1,19 +1,38 @@ -.graph_holder { +.project-network { border: 1px solid #aaa; padding: 1px; - - h4 { - padding: 0 10px; + .tip { + color: #888; + font-size: 14px; + padding: 10px; border-bottom: 1px solid #bbb; @include bg-gray-gradient; } - .graph { + .network-graph { background: #f1f1f1; - cursor: move; - height: 70%; - overflow: hidden; + height: 500px; + overflow-y: scroll; + overflow-x: hidden; + } +} + +.graphs { + .graph-author-commits-count { + } + + .graph-author-email { + float: right; + color: #777; + } + + .graph-additions { + color: #4a2; + } + + .graph-deletions { + color: #d12f19; } } diff --git a/app/assets/stylesheets/sections/header.scss b/app/assets/stylesheets/sections/header.scss index 5fe18131828..bd72d08295a 100644 --- a/app/assets/stylesheets/sections/header.scss +++ b/app/assets/stylesheets/sections/header.scss @@ -5,15 +5,16 @@ header { &.navbar-gitlab { .navbar-inner { - height: 45px; - padding: 5px; + height: 40px; + padding: 3px; background: #F1F1F1; + filter: none; .nav > li > a { color: $style_color; text-shadow: 0 1px 0 #fff; - font-size: 18px; - padding: 12px; + font-size: 14px; + padding: 10px; } /** NAV block with links and profile **/ @@ -25,7 +26,6 @@ header { } z-index: 10; - /*height: 60px;*/ /** * @@ -34,7 +34,7 @@ header { */ .app_logo { float: left; - margin-right: 15px; + margin-right: 9px; position: relative; top: -5px; padding-top: 5px; @@ -42,10 +42,12 @@ header { a { float: left; padding: 0px; - margin: 0 10px; + margin: 0 6px; h1 { - background: url('logo_dark.png') no-repeat 0px 2px; + margin: 0; + background: url('logo-black.png') no-repeat center 1px; + background-size: 38px; float: left; height: 40px; width: 40px; @@ -67,22 +69,31 @@ header { position: relative; float: left; margin: 0; - margin-left: 15px; + margin-left: 5px; @include header-font; } + .profile-pic { + position: relative; + top: -4px; + img { + width: 26px; + height: 26px; + @include border-radius(4px); + } + } + /** * * Search box * */ .search { - margin-right: 45px; + margin-right: 10px; margin-left: 10px; - margin-top: 2px; .search-input { - @extend .span2; + @extend .span3; background-image: url("icon-search.png"); background-repeat: no-repeat; background-position: 10px; @@ -91,121 +102,13 @@ header { @include border-radius(3px); border: 1px solid #c6c6c6; box-shadow: none; + @include transition(all 0.15s ease-in 0s); &:focus { - @extend .span3; + @extend .span4; } } } - /** - * - * Account box - * - */ - .account-box { - position: absolute; - right: 0; - top: 6px; - z-index: 10000; - width: 128px; - font-size: 11px; - float: right; - display: block; - cursor: pointer; - img { - @include border-radius(3px); - right: 5px; - position: absolute; - width: 28px; - height: 28px; - display: block; - top: 1px; - &:after { - content: " "; - display: block; - position: absolute; - top: 0; - right: 0; - left: 0; - bottom: 0; - float: right; - @include border-radius(5px); - border: 1px solid rgba(255, 255, 255, 0.1); - border-bottom: 0; - background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(255, 255, 255, 0.15)), to(rgba(0, 0, 0, 0.25))), - -webkit-gradient(linear, left top, right bottom, color-stop(0, rgba(255, 255, 255, 0)), color-stop(0.5, rgba(255, 255, 255, 0.1)), color-stop(0.501, rgba(255, 255, 255, 0)), color-stop(1, rgba(255, 255, 255, 0))); - background: -moz-linear-gradient(top, rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.25)), - -moz-linear-gradient(left top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.1) 50%, rgba(255, 255, 255, 0) 50%, rgba(255, 255, 255, 0)); - background: linear-gradient(top, rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.25)), - linear-gradient(left top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.1) 50%, rgba(255, 255, 255, 0) 50%, rgba(255, 255, 255, 0)); - -webkit-background-origin: border-box; - -moz-background-origin: border; - background-origin: border-box; } } } - - .account-box { - &.hover { - height: 138px; } - &:hover > .account-links { - display: block; } } - - .account-links { - @include border-radius(5px); - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); - position: relative; - &:before { - content: "."; - width: 0; - height: 0; - position: absolute; - border: 5px solid transparent; - border-color: rgba(255, 255, 255, 0); - border-bottom-color: #555; - text-indent: -9999px; - top: -10px; - line-height: 0; - right: 10px; - z-index: 10; } - background: #555; - display: none; - z-index: 100000; - @include border-radius(4px); - width: 130px; - position: absolute; - right: 5px; - top: 38px; - margin-top: 0; - float: right; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); - a { - color: #fff; - padding: 12px 15px; - display: block; - text-shadow: none; - border-bottom: 1px solid #666; - font-size: 12px; - &:hover { - color: #fff; - background: #333; - } - } - } - - .account-box.hover .arrow-up { - top: 41px; - right: 6px; - position: absolute; } - - .account-links a { - &:first-child { - @include border-radius(5px 5px 0 0); - } - &:last-child { - @include border-radius(0 0 5px 5px); - border-bottom: 0; - } - } - - /* * Dark header @@ -218,16 +121,25 @@ header { border-bottom: 1px solid #AAA; .nav > li > a { - color: #fff; - text-shadow: 0 1px 0 #111; + color: #AAA; + text-shadow: 0 1px 0 #444; + + &:hover { + color: #FFF; + } } } } + .turbolink-spinner { + color: #FFF; + } + .search { .search-input { background-color: #D2D5DA; background-color: rgba(255, 255, 255, 0.5); + border: 1px solid #AAA; &:focus { background-color: white; @@ -240,15 +152,22 @@ header { .app_logo { a { h1 { - background: url('logo_white.png') no-repeat center center; + background: url('logo-white.png') no-repeat center 1px; + background-size: 38px; color: #fff; - text-shadow: 0 1px 1px #111; + text-shadow: 0 1px 1px #444; } } } .project_name { + a { + color: #BBB; + &:hover { + color: #FFF; + } + } color: #fff; - text-shadow: 0 1px 1px #111; + text-shadow: 0 1px 1px #444; } } @@ -261,11 +180,11 @@ header { .separator { float: left; - height: 60px; + height: 46px; width: 1px; background: white; border-left: 1px solid #DDD; - margin-top: -10px; + margin-top: -3px; margin-left: 10px; margin-right: 10px; } diff --git a/app/assets/stylesheets/sections/issues.scss b/app/assets/stylesheets/sections/issues.scss index 351f2404492..e384aebc76c 100644 --- a/app/assets/stylesheets/sections/issues.scss +++ b/app/assets/stylesheets/sections/issues.scss @@ -1,22 +1,39 @@ -.issues_table { +.issues-list { .issue { padding: 10px; + position: relative; - .issue_check { + .issue-title { + margin-bottom: 5px; + font-size: 14px; + } + + .issue-info { + color: #999; + } + + .issue-check { float: left; padding: 8px 0; padding-right: 8px; min-width: 15px; } - p { - padding-top: 0; - padding-bottom: 2px; + .issue-labels { + display: inline-block; } - img.avatar { - width: 32px; - margin-top: 1px; + .issue-actions { + display: none; + position: absolute; + top: 10px; + right: 2px; + } + + &:hover { + .issue-actions { + display: block; + } } } } @@ -27,14 +44,17 @@ input.check_all_issues { margin: 0; margin-right: 10px; position: relative; - top: 8px; - height: 22px; + top: 13px; } .issues_content { .title { height: 40px; } + + form { + margin: 0; + } } .btn.close_issue { @@ -60,7 +80,7 @@ input.check_all_issues { @media (min-width: 800px) { .issues_bulk_update select { width: 120px; } } @media (min-width: 1200px) { .issues_bulk_update select { width: 160px; } } -#issues-table-holder { +.issues-holder { .issues_filters { } @@ -71,17 +91,7 @@ input.check_all_issues { } .update_selected_issues { - position: relative; - top:5px; margin-left: 4px; - float: left; - } - - .update_issues_text { - padding: 3px; - line-height: 28px; - float: left; - color: #479; } } } @@ -89,3 +99,7 @@ input.check_all_issues { #update_status { width: 100px; } + +.participants { + margin-bottom: 10px; +} diff --git a/app/assets/stylesheets/sections/login.scss b/app/assets/stylesheets/sections/login.scss index 89b8f1c0055..33bef59c089 100644 --- a/app/assets/stylesheets/sections/login.scss +++ b/app/assets/stylesheets/sections/login.scss @@ -1,7 +1,8 @@ /* Login Page */ body.login-page{ - padding-top: 7%; - background: #666; + .container > .content { + padding-top: 20px; + } } .login-box{ @@ -37,3 +38,11 @@ body.login-page{ } .login-box a.forgot{float: right; padding-top: 6px} + +.remember_me { + text-align: left; + + input { + margin: 2px; + } +} diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index ff715c0fd18..aa61cae4b9a 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -53,57 +53,66 @@ } } -li.merge_request { - padding: 10px; - img.avatar { - width: 32px; - margin-top: 1px; - } - p { - padding: 0px; - padding-bottom: 2px; - } -} - .merge-in-progress { @extend .padded; @extend .append-bottom-10; } -.label_branch { +.mr_source_commit, +.mr_target_commit { + .commit { + margin: 0; + padding: 0; + padding: 5px 0; + list-style: none; + &:hover { + background: none; + } + } +} + +.label-branch { @include border-radius(4px); - padding: 2px 4px; + padding: 3px 4px; border: none; font-size: 14px; background: #474D57; color: #fff; font-family: $monospace_font; + font-weight: normal; + overflow: hidden; + + .label-project { + @include border-radius-left(4px); + padding: 3px 4px; + background: #279; + position: relative; + left: -4px; + letter-spacing: -1px; + } } -.mr_source_commit, -.mr_target_commit { - .commit { - margin: 0; - padding: 0; - padding: 5px; - margin-bottom: 5px; - .avatar { position:relative } - .row_title { - color: #444; - } - .commit-author-name, - .dash, - .committed_ago, - .browse_code_link_holder { - display: none; +.mr-list { + .merge-request { + padding: 10px; + position: relative; + + .merge-request-title { + margin-bottom: 5px; + font-size: 14px; } - list-style: none; - &:hover { - background: none; + + .merge-request-info { + color: #999; } } } -.mr_direction_tip { - margin-top:40px +.merge-request-angle { + text-align: center; + margin: 0; +} + +.merge-request-form-info { + padding: 15px 0; } diff --git a/app/assets/stylesheets/sections/nav.scss b/app/assets/stylesheets/sections/nav.scss index 50091cd7365..54263523e85 100644 --- a/app/assets/stylesheets/sections/nav.scss +++ b/app/assets/stylesheets/sections/nav.scss @@ -1,68 +1,85 @@ -/* - * Main Menu of Application - * - */ -ul.main_menu { - margin: auto; - margin: 30px 0; - margin-top: 10px; - height: 38px; - position: relative; - overflow: hidden; - .count { - position: relative; - top: -1px; - display: inline-block; - height: 15px; - margin: 0 0 0 5px; - padding: 0 8px 1px 8px; - height: auto; - font-size: 0.82em; - line-height: 14px; - text-align: center; - color: #777; - } - .label { - background: $hover; - text-shadow: none; - color: $style_color; - } - li { - list-style-type: none; - margin: 0; - display: table-cell; - width: 1%; - border-bottom: 2px solid #EEE; - &.active { - border-bottom: 2px solid #474D57; - a { - color: $style_color; - } +.main-nav { + background: #f5f5f5; + margin: 20px 0; + margin-top: 0; + padding-top: 4px; + border-bottom: 1px solid #E1E1E1; + + ul { + margin: auto; + height: 40px; + overflow: hidden; + .count { + font-weight: normal; + display: inline-block; + height: 15px; + padding: 1px 6px; + height: auto; + font-size: 0.82em; + line-height: 14px; + text-align: center; + color: #777; + background: #eee; + @include border-radius(8px); } + .label { + background: $hover; + text-shadow: none; + color: $style_color; + } + li { + list-style-type: none; + margin: 0; + display: table-cell; + width: 1%; + &.active { + a { + color: $style_color; + font-weight: bolder; + + &:after { + content: ''; + display: block; + position: relative; + bottom: 8px; + left: 50%; + width: 0; + height: 0; + border-color: transparent transparent #777 transparent; + border-style: solid; + border-width: 6px; + margin-left: -6px; + } + } + } + + &:hover { + a { + color: $style_color; + } + } - &.home { - a { - i { - font-size: 20px; - position: relative; - top: 4px; + &.home { + a { + i { + font-size: 20px; + position: relative; + top: 4px; + } } } } - } - a { - display: block; - text-align: center; - font-weight: normal; - height: 36px; - line-height: 36px; - color: #777; - text-shadow: 0 1px 1px white; - padding: 0 10px; + a { + display: block; + text-align: center; + font-weight: normal; + height: 38px; + line-height: 34px; + color: #777; + text-shadow: 0 1px 1px white; + padding: 0 10px; + text-decoration: none; + padding-top: 2px; + } } } -/* - * End of Main Menu - * - */ - diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/sections/notes.scss index 1f92a3a835d..bae1ac3aa9a 100644 --- a/app/assets/stylesheets/sections/notes.scss +++ b/app/assets/stylesheets/sections/notes.scss @@ -1,6 +1,13 @@ /** * Notes */ + +@-webkit-keyframes target-note { + from { background:#fffff0; } + 50% { background:#ffffd3; } + to { background:#fffff0; } +} + ul.notes { display: block; list-style: none; @@ -83,13 +90,23 @@ ul.notes { margin-top: -20px; } .note-body { + @include md-typography; margin-left: 45px; + + .highlight { + @include border-radius(4px); + } } .note-header { padding-bottom: 5px; } } + .note:target { + -webkit-animation:target-note 2s linear; + background: #fffff0; + } + // paint top or bottom borders depending on notes direction &:not(.reversed) .note, &:not(.reversed) .discussion { @@ -191,7 +208,7 @@ ul.notes { } } - // "show" the icon also if we just hover somwhere over the line + // "show" the icon also if we just hover somewhere over the line &:hover > td { background: $hover !important; @@ -212,7 +229,17 @@ ul.notes { .reply-btn { @extend .btn-primary; } -.file .content tr.line_holder:hover > td { background: $hover !important; } +.file .content tr.line_holder:hover { + &> td.line_content { + background: $hover !important; + border-color: darken($hover, 10%) !important; + } + &> td.new_line, + &> td.old_line { + background: darken($hover, 4%) !important; + border-color: darken($hover, 10%) !important; + } +} .file .content tr.line_holder:hover > td .line_note_link { opacity: 1.0; filter: alpha(opacity=100); @@ -273,6 +300,15 @@ ul.notes { } +.common-note-form { + margin: 0; + height: 140px; + background: #F9F9F9; + padding: 3px; + padding-bottom: 25px; + border: 1px solid #DDD; +} + .note-form-actions { background: #F9F9F9; @@ -280,9 +316,41 @@ ul.notes { padding: 0 5px; .note-form-option { - margin-top: 8px; - margin-left: 15px; + margin-top: 10px; + margin-left: 30px; @extend .pull-left; - @extend .span4; } + + .js-notify-commit-author { + float: left; + } +} + +.note-edit-form { + display: none; + + .note_text { + border: 1px solid #DDD; + box-shadow: none; + font-size: 14px; + height: 80px; + width: 98.6%; + } + + .form-actions { + padding-left: 20px; + + .btn-save { + float: left; + } + + .note-form-option { + float: left; + padding: 2px 0 0 25px; + } + } +} + +.js-note-attachment-delete { + display: none; } diff --git a/app/assets/stylesheets/sections/profile.scss b/app/assets/stylesheets/sections/profile.scss index 607daf7a97e..5c7516ce6f9 100644 --- a/app/assets/stylesheets/sections/profile.scss +++ b/app/assets/stylesheets/sections/profile.scss @@ -1,22 +1,6 @@ -.profile_history { - .event_feed { - min-height: 20px; - .avatar { - width: 20px; - } - } -} - -.profile_avatar_holder { - float: left; - width: 60px; - height: 60px; - margin-right: 20px; - img { - width: 60px; - height: 60px; - background: #fff; - padding: 1px; - border: 1px solid #ddd; +.update-notifications { + margin-bottom: 0; + label { + margin-bottom: 0; } } diff --git a/app/assets/stylesheets/sections/projects.scss b/app/assets/stylesheets/sections/projects.scss index 28df1b5ac28..0491b68db57 100644 --- a/app/assets/stylesheets/sections/projects.scss +++ b/app/assets/stylesheets/sections/projects.scss @@ -1,56 +1,5 @@ -.projects { - @extend .row; - .activities { - } - - .side { - @extend .pull-right; - - .projects_box { - > .title { - padding: 2px 15px; - } - .nav-projects-tabs li { padding: 0; } - .well-list { - li { padding: 15px; } - .arrow { - float: right; - padding: 10px; - margin: 0; - } - .last_activity { - padding-top: 5px; - display: block; - span, strong { - font-size: 12px; - color: #666; - } - } - } - @extend .ui-box; - } - } -} - .new_project, .edit_project { - .project_name_holder { - input, - label { - font-size: 16px; - line-height: 20px; - padding: 8px; - } - .btn { - padding: 6px 10px; - margin-left: 10px; - margin-bottom: 8px; - } - } - .adv_settings { - h6 { margin-left: 40px; } - } - fieldset.features { .control-label { font-weight: bold; @@ -58,28 +7,38 @@ } } +.project-name-holder { + .help-inline { + vertical-align: top; + padding: 7px; + } +} + .project_clone_panel { @include border-radius(4px); @include bg-gray-gradient; padding: 4px 7px; border: 1px solid #CCC; margin-bottom: 20px; -} -.project_clone_holder { - input[type="text"], .btn { - font-size: 12px; - line-height: 18px; - margin: 0; - padding: 3px 10px; + padding: 4px 12px; } +} +.project_clone_holder { input[type="text"] { @extend .monospace; border: 1px solid #BBB; box-shadow: none; margin-left: -1px; + background: #FFF; + } +} + +.project-public-holder { + .help-inline { + padding-top: 7px; } } @@ -89,7 +48,7 @@ margin-bottom: 50px; } h3 { - @extend .page_title; + @extend .page-title; } } @@ -115,3 +74,44 @@ ul.nav.nav-projects-tabs { } } } + +.team_member_row form { + margin: 0px; +} + +.my-projects { + li { + .project-title { + font-size: 14px; + } + + .project-info { + margin-bottom: 10px; + } + + .access-icon i { + color: #AAA; + } + } +} + +.public-clone { + background: #333; + color: #f5f5f5; + padding: 6px 10px; + margin: 1px; + font-weight: normal; +} + +.new-tag-btn { + position: relative; + top: -5px; +} + +.public-projects .repo-info { + color: #777; + + a { + color: #777; + } +} diff --git a/app/assets/stylesheets/sections/snippets.scss b/app/assets/stylesheets/sections/snippets.scss index 3944814fc3e..e67ca794f25 100644 --- a/app/assets/stylesheets/sections/snippets.scss +++ b/app/assets/stylesheets/sections/snippets.scss @@ -1,9 +1,16 @@ -.snippet.file_holder { - .file_title { +.snippet.file-holder { + .file-title { .snippet-file-name { + padding: 4px 10px; position: relative; top: -4px; left: -4px; } } } + +.my-snippets li:first-child { + h4 { margin-top: 0; } + padding-top: 0; +} + diff --git a/app/assets/stylesheets/sections/stat_graph.scss b/app/assets/stylesheets/sections/stat_graph.scss new file mode 100644 index 00000000000..b9be47e7700 --- /dev/null +++ b/app/assets/stylesheets/sections/stat_graph.scss @@ -0,0 +1,50 @@ +.tint-box { + background: #f3f3f3; + position: relative; + margin-bottom: 10px; +} + +.area { + fill: #1db34f; + fill-opacity: 0.5; +} + +.axis { + fill: #aaa; + font-size: 10px; +} + +#contributors { + .contributors-list { + margin: 0 0 10px 0; + list-style: none; + padding: 0; + } + + .person { + &:nth-child(even) { + float: right; + } + float: left; + margin-top: 10px; + } + + .person .spark { + display: block; + background: #f3f3f3; + } + + .person .area-contributor { + fill: #f17f49; + } +} + +.selection rect { + fill: #333; + fill-opacity: 0.1; + stroke: #333; + stroke-width: 1px; + stroke-opacity: 0.4; + shape-rendering: crispedges; + stroke-dasharray: 3 3; +} diff --git a/app/assets/stylesheets/sections/themes.scss b/app/assets/stylesheets/sections/themes.scss index 4e5eaf575ae..cd1aa2b011a 100644 --- a/app/assets/stylesheets/sections/themes.scss +++ b/app/assets/stylesheets/sections/themes.scss @@ -1,10 +1,3 @@ -.application-theme, .code-preview-theme { - .update-feedback { - color: #468847; - float: right; - } -} - .themes_opts { padding-left: 20px; @@ -27,15 +20,15 @@ } &.modern { - background: #567; + background: #345; } &.gray { - background: #708090; + background: #373737; } &.violet { - background: #657; + background: #547; } } } diff --git a/app/assets/stylesheets/sections/tree.scss b/app/assets/stylesheets/sections/tree.scss index 0ba68e5010c..2a84741f0d6 100644 --- a/app/assets/stylesheets/sections/tree.scss +++ b/app/assets/stylesheets/sections/tree.scss @@ -13,8 +13,22 @@ } .tree-table { + @extend .table; @include border-radius(0); - .tree-item { + + tr { + td, th { + padding: 8px 10px; + line-height: 20px; + } + th { + font-weight: normal; + font-size: 15px; + border-bottom: 1px solid #CCC; + } + td { + border-color: #F1F1F1; + } &:hover { td { background: $hover; @@ -23,6 +37,13 @@ } cursor: pointer; } + &.selected { + td { + background: #f5f5f5; + border-top: 1px solid #EEE; + border-bottom: 1px solid #EEE; + } + } } } @@ -42,17 +63,6 @@ } } - .tree-table { - th .btn { - margin: -2px -1px; - padding: 2px 10px; - } - td { - line-height: 20px; - background: #fafafa; - } - } - .tree_author { padding-right: 8px; @@ -96,9 +106,15 @@ } .tree-btn-group { + top: 2px; + .btn { - margin-right:-3px; + margin-right: 0px; padding: 2px 10px; } } +.tree-ref-holder { + float: left; + margin-top: 5px; +} diff --git a/app/assets/stylesheets/sections/votes.scss b/app/assets/stylesheets/sections/votes.scss index 4686f5422dc..49489babab7 100644 --- a/app/assets/stylesheets/sections/votes.scss +++ b/app/assets/stylesheets/sections/votes.scss @@ -27,7 +27,7 @@ } } .votes-block { - margin: 14px 6px 6px 0; + margin: 6px; .downvotes { float: right; } @@ -35,9 +35,4 @@ .votes-inline { display: inline-block; margin: 0 8px; - .progress { - display: inline-block; - padding: 0 0 2px; - width: 45px; - } } diff --git a/app/assets/stylesheets/sections/wall.scss b/app/assets/stylesheets/sections/wall.scss new file mode 100644 index 00000000000..d6ac08fcf6f --- /dev/null +++ b/app/assets/stylesheets/sections/wall.scss @@ -0,0 +1,55 @@ +.wall-page { + .wall-note-form { + @extend .span12; + + margin: 0; + height: 140px; + background: #F9F9F9; + position: fixed; + bottom: 0px; + padding: 3px; + padding-bottom: 25px; + border: 1px solid #DDD; + } + + .notes { + margin-bottom: 160px; + background: #FFE; + border: 1px solid #EED; + + > li { + @extend .clearfix; + border-bottom: 1px solid #EED; + padding: 10px; + } + + .wall-author { + color: #666; + float: left; + font-size: 12px; + width: 120px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .wall-text { + border-left: 1px solid #CCC; + margin-left: 10px; + padding-left: 10px; + float: left; + width: 75%; + } + + .wall-file { + margin-left: 8px; + background: #EEE; + } + + abbr { + float: right; + color: #AAA; + border: none; + } + } +} diff --git a/app/assets/stylesheets/sections/wiki.scss b/app/assets/stylesheets/sections/wiki.scss new file mode 100644 index 00000000000..ed3a432ded0 --- /dev/null +++ b/app/assets/stylesheets/sections/wiki.scss @@ -0,0 +1,6 @@ +h3.page-title .edit-wiki-header { + width: 780px; + margin-left: auto; + margin-right: auto; + padding-right: 7px; +} diff --git a/app/assets/stylesheets/selects.scss b/app/assets/stylesheets/selects.scss new file mode 100644 index 00000000000..09ae57b6692 --- /dev/null +++ b/app/assets/stylesheets/selects.scss @@ -0,0 +1,153 @@ +/* CHZN reset few styles */ +.chosen-container-single .chosen-single { + background: #FFF; + border: 1px solid #bbb; + box-shadow: none; +} +.chosen-container-active .chosen-single { + background: #fff; +} + +.ajax-users-select { + width: 400px; + + &.input-large { + width: 210px; + } +} + +.user-result { + .user-image { + float: left; + } + .user-name { + } + .user-username { + color: #999; + } +} + +/** Branch/tag selector **/ +.project-refs-form { + margin: 0; + span { + background:none !important; + position:static !important; + width:auto !important; + height:auto !important; + } +} +.project-refs-select { + width: 120px; +} + +.project-refs-form .chosen-container { + position: relative; + top: 0; + left: 0; + margin-right: 10px; + + .chosen-drop { + min-width: 400px; + .chosen-results { + max-height: 300px; + } + .chosen-search input { + min-width: 365px; + } + } +} + +/** Fix for Search Dropdown Border **/ +.chosen-container { + min-width: 100px; + + .chosen-search { + input:focus { + @include box-shadow(none); + } + } + + .chosen-drop { + margin: 7px 0; + min-width: 200px; + border: 1px solid #bbb; + @include border-radius(0); + + .chosen-results { + margin-top: 5px; + max-height: 300px; + + .group-result { + color: $style_color; + border-bottom: 1px solid #EEE; + padding: 8px; + } + .active-result { + @include border-radius(0); + + &.highlighted { + background: $hover; + color: $style_color; + } + &.result-selected { + background: #EEE; + border-left: 4px solid #CCC; + } + } + } + + .chosen-search { + @include bg-gray-gradient; + input { + min-width: 165px; + border-color: #CCC; + } + } + } +} + +.chosen-container .chosen-single, +.chosen-container.chosen-with-drop .chosen-single { + @include bg-light-gray-gradient; + + div { + background: transparent; + border-left: none; + } + + span { + font-weight: normal; + } +} + +/** Select2 styling **/ +.select2-container .select2-choice { + background: #f1f1f1; + background-image: -webkit-gradient(linear, 0 0, 0 30, color-stop(0.066, whitesmoke), to(#e1e1e1)); + background-image: -webkit-linear-gradient(whitesmoke 6.6%, #e1e1e1); + background-image: -moz-linear-gradient(whitesmoke 6.6%, #e1e1e1); + background-image: -o-linear-gradient(whitesmoke 6.6%, #e1e1e1); +} + +.select2-container .select2-choice div { + border: none; + background: none; +} + +.select2-drop { + padding-top: 8px; +} + +.select2-no-results, .select2-searching { + padding: 7px; + color: #666; +} + +.chosen-container .chosen-single div b { + background-position-y: 0px !important; +} + +.chosen-container .chosen-drop .chosen-search input { + background-position-y: -24px !important; +} diff --git a/app/assets/stylesheets/themes/ui_basic.scss b/app/assets/stylesheets/themes/ui_basic.scss index 4e34e8b1b6b..30f0bbaf4c8 100644 --- a/app/assets/stylesheets/themes/ui_basic.scss +++ b/app/assets/stylesheets/themes/ui_basic.scss @@ -5,7 +5,11 @@ */ .ui_basic { .separator { - background: white; + background: #F9F9F9; border-left: 1px solid #DDD; } + + .main-nav { + background: #FFF; + } } diff --git a/app/assets/stylesheets/themes/ui_color.scss b/app/assets/stylesheets/themes/ui_color.scss index d7a554ff9e5..1fd54ff18a6 100644 --- a/app/assets/stylesheets/themes/ui_color.scss +++ b/app/assets/stylesheets/themes/ui_color.scss @@ -16,15 +16,16 @@ @extend .header-dark; &.navbar-gitlab { .navbar-inner { - background: #657; + background: #547; + border-bottom: 1px solid #435; .app_logo { &:hover { - background-color: #6A5A7A; + background-color: #435; } } .separator { - background: #546; - border-left: 1px solid #706080; + background: #435; + border-left: 1px solid #658; } } } diff --git a/app/assets/stylesheets/themes/ui_gray.scss b/app/assets/stylesheets/themes/ui_gray.scss index f0547c72157..41c08c840e2 100644 --- a/app/assets/stylesheets/themes/ui_gray.scss +++ b/app/assets/stylesheets/themes/ui_gray.scss @@ -16,15 +16,16 @@ @extend .header-dark; &.navbar-gitlab { .navbar-inner { - background: #708090; + background: #373737; + border-bottom: 1px solid #272727; .app_logo { &:hover { - background-color: #6A7A8A; + background-color: #272727; } } .separator { - background: #607080; - border-left: 1px solid #8090A0; + background: #272727; + border-left: 1px solid #474747; } } } diff --git a/app/assets/stylesheets/themes/ui_mars.scss b/app/assets/stylesheets/themes/ui_mars.scss index 0a78c5c09f5..a2b8c21ea11 100644 --- a/app/assets/stylesheets/themes/ui_mars.scss +++ b/app/assets/stylesheets/themes/ui_mars.scss @@ -8,66 +8,27 @@ * */ .ui_mars { - /* * Application Header * */ header { - + @extend .header-dark; &.navbar-gitlab { .navbar-inner { - background: #474D57 url('bg-header.png') repeat-x bottom; - border-bottom: 1px solid #444; - - .nav > li > a { - color: #eee; - text-shadow: 0 1px 0 #444; + background: #474D57; + border-bottom: 1px solid #373D47; + .app_logo { + &:hover { + background-color: #373D47; + } } } } - .search { - float: right; - margin-right: 45px; - .search-input { - border: 1px solid rgba(0, 0, 0, 0.7); - background-color: #D2D5DA; - background-color: rgba(255, 255, 255, 0.5); - - &:focus { - background-color: white; - } - } - } - .search-input::-webkit-input-placeholder { - color: #666; - } - .app_logo { - a { - h1 { - background: url('logo_white.png') no-repeat center center; - color: #eee; - text-shadow: 0 1px 1px #111; - } - } - &:hover { - background-color: #41464e; - } - } - .project_name { - color: #eee; - text-shadow: 0 1px 1px #111; + .separator { + background: #31363E; + border-left: 1px solid #666; } } - - .separator { - background: #31363E; - border-left: 1px solid #666; - } - - /* - * End of Application Header - * - */ } diff --git a/app/assets/stylesheets/themes/ui_modern.scss b/app/assets/stylesheets/themes/ui_modern.scss index a5bf414c443..6173757082e 100644 --- a/app/assets/stylesheets/themes/ui_modern.scss +++ b/app/assets/stylesheets/themes/ui_modern.scss @@ -16,15 +16,16 @@ @extend .header-dark; &.navbar-gitlab { .navbar-inner { - background: #567; + background: #345; + border-bottom: 1px solid #234; .app_logo { &:hover { - background-color: #516171; + background-color: #234; } } .separator { - background: #456; - border-left: 1px solid #678; + background: #234; + border-left: 1px solid #456; } } } diff --git a/app/contexts/commit_load_context.rb b/app/contexts/commit_load_context.rb index 1f23f633af3..2930c5b1668 100644 --- a/app/contexts/commit_load_context.rb +++ b/app/contexts/commit_load_context.rb @@ -12,7 +12,6 @@ class CommitLoadContext < BaseContext commit = project.repository.commit(params[:id]) if commit - commit = CommitDecorator.decorate(commit) line_notes = project.notes.for_commit_id(commit.id).inline result[:commit] = commit @@ -21,7 +20,8 @@ class CommitLoadContext < BaseContext result[:notes_count] = project.notes.for_commit_id(commit.id).count begin - result[:suppress_diff] = true if commit.diffs.size > Commit::DIFF_SAFE_SIZE && !params[:force_show_diff] + result[:suppress_diff] = true if commit.diff_suppress? && !params[:force_show_diff] + result[:force_suppress_diff] = commit.diff_force_suppress? rescue Grit::Git::GitTimeout result[:suppress_diff] = true result[:status] = :huge_commit diff --git a/app/contexts/filter_context.rb b/app/contexts/filter_context.rb index 401d19b31c8..2607b12b4ae 100644 --- a/app/contexts/filter_context.rb +++ b/app/contexts/filter_context.rb @@ -11,8 +11,8 @@ class FilterContext end def apply_filter items - if params[:project_id] - items = items.where(project_id: params[:project_id]) + if params[:project_id].present? + items = items.of_projects(params[:project_id]) end if params[:search].present? diff --git a/app/contexts/issues/bulk_update_context.rb b/app/contexts/issues/bulk_update_context.rb new file mode 100644 index 00000000000..73a3c353523 --- /dev/null +++ b/app/contexts/issues/bulk_update_context.rb @@ -0,0 +1,39 @@ +module Issues + class BulkUpdateContext < BaseContext + def execute + update_data = params[:update] + + issues_ids = update_data[:issues_ids].split(",") + milestone_id = update_data[:milestone_id] + assignee_id = update_data[:assignee_id] + status = update_data[:status] + + new_state = nil + + if status.present? + if status == 'closed' + new_state = :close + else + new_state = :reopen + end + end + + opts = {} + opts[:milestone_id] = milestone_id if milestone_id.present? + opts[:assignee_id] = assignee_id if assignee_id.present? + + issues = Issue.where(id: issues_ids).all + issues = issues.select { |issue| can?(current_user, :modify_issue, issue) } + + issues.each do |issue| + issue.update_attributes(opts) + issue.send new_state if new_state + end + + { + count: issues.count, + success: !issues.count.zero? + } + end + end +end diff --git a/app/contexts/issues/list_context.rb b/app/contexts/issues/list_context.rb new file mode 100644 index 00000000000..da2eed0e259 --- /dev/null +++ b/app/contexts/issues/list_context.rb @@ -0,0 +1,36 @@ +module Issues + class ListContext < BaseContext + attr_accessor :issues + + def execute + @issues = @project.issues + + @issues = case params[:state] + when 'all' then @issues + when 'closed' then @issues.closed + else @issues.opened + end + + @issues = case params[:scope] + when 'assigned-to-me' then @issues.assigned_to(current_user) + when 'created-by-me' then @issues.authored(current_user) + else @issues + end + + @issues = @issues.tagged_with(params[:label_name]) if params[:label_name].present? + @issues = @issues.includes(:author, :project) + + # Filter by specific assignee_id (or lack thereof)? + if params[:assignee_id].present? + @issues = @issues.where(assignee_id: (params[:assignee_id] == '0' ? nil : params[:assignee_id])) + end + + # Filter by specific milestone_id (or lack thereof)? + if params[:milestone_id].present? + @issues = @issues.where(milestone_id: (params[:milestone_id] == '0' ? nil : params[:milestone_id])) + end + + @issues + end + end +end diff --git a/app/contexts/issues_bulk_update_context.rb b/app/contexts/issues_bulk_update_context.rb deleted file mode 100644 index 7c3c1d4f7c3..00000000000 --- a/app/contexts/issues_bulk_update_context.rb +++ /dev/null @@ -1,24 +0,0 @@ -class IssuesBulkUpdateContext < BaseContext - def execute - update_data = params[:update] - - issues_ids = update_data[:issues_ids].split(",") - milestone_id = update_data[:milestone_id] - assignee_id = update_data[:assignee_id] - status = update_data[:status] - - opts = {} - opts[:milestone_id] = milestone_id if milestone_id.present? - opts[:assignee_id] = assignee_id if assignee_id.present? - opts[:closed] = (status == "closed") if status.present? - - issues = Issue.where(id: issues_ids).all - issues = issues.select { |issue| can?(current_user, :modify_issue, issue) } - issues.each { |issue| issue.update_attributes(opts) } - { - count: issues.count, - success: !issues.count.zero? - } - end -end - diff --git a/app/contexts/issues_list_context.rb b/app/contexts/issues_list_context.rb deleted file mode 100644 index 0cc73f99535..00000000000 --- a/app/contexts/issues_list_context.rb +++ /dev/null @@ -1,29 +0,0 @@ -class IssuesListContext < BaseContext - include IssuesHelper - - attr_accessor :issues - - def execute - @issues = case params[:status] - when issues_filter[:all] then @project.issues - when issues_filter[:closed] then @project.issues.closed - when issues_filter[:to_me] then @project.issues.opened.assigned(current_user) - else @project.issues.opened - end - - @issues = @issues.tagged_with(params[:label_name]) if params[:label_name].present? - @issues = @issues.includes(:author, :project).order("updated_at") - - # Filter by specific assignee_id (or lack thereof)? - if params[:assignee_id].present? - @issues = @issues.where(assignee_id: (params[:assignee_id] == '0' ? nil : params[:assignee_id])) - end - - # Filter by specific milestone_id (or lack thereof)? - if params[:milestone_id].present? - @issues = @issues.where(milestone_id: (params[:milestone_id] == '0' ? nil : params[:milestone_id])) - end - - @issues - end -end diff --git a/app/contexts/merge_requests_load_context.rb b/app/contexts/merge_requests_load_context.rb index 4ec66cd9b78..c3408db6e11 100644 --- a/app/contexts/merge_requests_load_context.rb +++ b/app/contexts/merge_requests_load_context.rb @@ -2,19 +2,23 @@ # based on filtering passed via params for @project class MergeRequestsLoadContext < BaseContext def execute - type = params[:f] + merge_requests = @project.merge_requests - merge_requests = project.merge_requests - - merge_requests = case type + merge_requests = case params[:state] when 'all' then merge_requests when 'closed' then merge_requests.closed - when 'assigned-to-me' then merge_requests.opened.assigned(current_user) else merge_requests.opened end + merge_requests = case params[:scope] + when 'assigned-to-me' then merge_requests.assigned_to(current_user) + when 'created-by-me' then merge_requests.authored(current_user) + else merge_requests + end + + merge_requests = merge_requests.page(params[:page]).per(20) - merge_requests = merge_requests.includes(:author, :project).order("closed, created_at desc") + merge_requests = merge_requests.includes(:author, :source_project, :target_project).order("created_at desc") # Filter by specific assignee_id (or lack thereof)? if params[:assignee_id].present? diff --git a/app/contexts/notes/create_context.rb b/app/contexts/notes/create_context.rb index 1367dff4699..36ea76ff949 100644 --- a/app/contexts/notes/create_context.rb +++ b/app/contexts/notes/create_context.rb @@ -3,8 +3,6 @@ module Notes def execute note = project.notes.new(params[:note]) note.author = current_user - note.notify = params[:notify].present? - note.notify_author = params[:notify_author].present? note.save note end diff --git a/app/contexts/notes/load_context.rb b/app/contexts/notes/load_context.rb index e3875e1d4e2..234e9ac3cdd 100644 --- a/app/contexts/notes/load_context.rb +++ b/app/contexts/notes/load_context.rb @@ -3,8 +3,6 @@ module Notes def execute target_type = params[:target_type] target_id = params[:target_id] - after_id = params[:after_id] - before_id = params[:before_id] @notes = case target_type @@ -16,17 +14,6 @@ module Notes project.merge_requests.find(target_id).mr_and_commit_notes.inc_author.fresh when "snippet" project.snippets.find(target_id).notes.fresh - when "wall" - # this is the only case, where the order is DESC - project.notes.common.inc_author_project.order("created_at DESC, id DESC").limit(50) - end - - @notes = if after_id - @notes.where("id > ?", after_id) - elsif before_id - @notes.where("id < ?", before_id) - else - @notes end end end diff --git a/app/contexts/projects/create_context.rb b/app/contexts/projects/create_context.rb index 629c5294754..1c60a5de141 100644 --- a/app/contexts/projects/create_context.rb +++ b/app/contexts/projects/create_context.rb @@ -8,21 +8,33 @@ module Projects # get namespace id namespace_id = params.delete(:namespace_id) - @project = Project.new(params) + # Load default feature settings + default_features = Gitlab.config.gitlab.default_projects_features + + default_opts = { + issues_enabled: default_features.issues, + wiki_enabled: default_features.wiki, + wall_enabled: default_features.wall, + snippets_enabled: default_features.snippets, + merge_requests_enabled: default_features.merge_requests, + public: default_features.public + }.stringify_keys + + @project = Project.new(default_opts.merge(params)) # Parametrize path for project # # Ex. # 'GitLab HQ'.parameterize => "gitlab-hq" # - @project.path = @project.name.dup.parameterize + @project.path = @project.name.dup.parameterize unless @project.path.present? if namespace_id # Find matching namespace and check if it allowed # for current user if namespace_id passed. if allowed_namespace?(current_user, namespace_id) - @project.namespace_id = namespace_id unless namespace_id == Namespace.global_id + @project.namespace_id = namespace_id else deny_namespace return @project @@ -34,18 +46,15 @@ module Projects @project.creator = current_user - # Import project from cloneable resource - if @project.valid? && @project.import_url.present? - shell = Gitlab::Shell.new - if shell.import_repository(@project.path_with_namespace, @project.import_url) - true - else - @project.errors.add(:import_url, 'cannot clone repo') - end - end - if @project.save - @project.users_projects.create(project_access: UsersProject::MASTER, user: current_user) + @project.discover_default_branch + + unless @project.group + @project.users_projects.create( + project_access: UsersProject::MASTER, + user: current_user + ) + end end @project @@ -61,12 +70,8 @@ module Projects end def allowed_namespace?(user, namespace_id) - if namespace_id == Namespace.global_id - return user.admin - else - namespace = Namespace.find_by_id(namespace_id) - current_user.can?(:manage_namespace, namespace) - end + namespace = Namespace.find_by_id(namespace_id) + current_user.can?(:manage_namespace, namespace) end end end diff --git a/app/contexts/projects/fork_context.rb b/app/contexts/projects/fork_context.rb new file mode 100644 index 00000000000..fbc67220d5d --- /dev/null +++ b/app/contexts/projects/fork_context.rb @@ -0,0 +1,44 @@ +module Projects + class ForkContext < BaseContext + include Gitlab::ShellAdapter + + def initialize(project, user) + @from_project, @current_user = project, user + end + + def execute + project = @from_project.dup + project.name = @from_project.name + project.path = @from_project.path + project.namespace = current_user.namespace + project.creator = current_user + + # 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 + # project with the same name and namespace + if project.valid? + begin + Project.transaction do + #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.users_projects.create(project_access: UsersProject::MASTER, user: current_user) + end + #Now fork the repo + unless gitlab_shell.fork_repository(@from_project.path_with_namespace, project.namespace.path) + raise "forking failed in gitlab-shell" + end + project.ensure_satellite_exists + end + rescue => ex + project.errors.add(:base, "Fork transaction failed.") + project.destroy + end + else + project.errors.add(:base, "Invalid fork destination") + end + project + + end + end +end diff --git a/app/contexts/projects/transfer_context.rb b/app/contexts/projects/transfer_context.rb new file mode 100644 index 00000000000..3011984e3f8 --- /dev/null +++ b/app/contexts/projects/transfer_context.rb @@ -0,0 +1,22 @@ +module Projects + class TransferContext < BaseContext + def execute(role = :default) + namespace_id = params[:project].delete(:namespace_id) + allowed_transfer = can?(current_user, :change_namespace, project) || role == :admin + + if allowed_transfer && namespace_id.present? + if namespace_id.to_i != project.namespace_id + # Transfer to someone namespace + namespace = Namespace.find(namespace_id) + project.transfer(namespace) + end + end + + rescue ProjectTransferService::TransferError => ex + project.reload + project.errors.add(:namespace_id, ex.message) + false + end + end +end + diff --git a/app/contexts/projects/update_context.rb b/app/contexts/projects/update_context.rb index e5d09b7df7f..40385fa65b0 100644 --- a/app/contexts/projects/update_context.rb +++ b/app/contexts/projects/update_context.rb @@ -1,24 +1,8 @@ module Projects class UpdateContext < BaseContext def execute(role = :default) - namespace_id = params[:project].delete(:namespace_id) + params[:project].delete(:namespace_id) params[:project].delete(:public) unless can?(current_user, :change_public_mode, project) - - allowed_transfer = can?(current_user, :change_namespace, project) || role == :admin - - if allowed_transfer && namespace_id.present? - if namespace_id == Namespace.global_id - if project.namespace.present? - # Transfer to global namespace from anyone - project.transfer(nil) - end - elsif namespace_id.to_i != project.namespace_id - # Transfer to someone namespace - namespace = Namespace.find(namespace_id) - project.transfer(namespace) - end - end - project.update_attributes(params[:project], as: role) end end diff --git a/app/contexts/search_context.rb b/app/contexts/search_context.rb index 9becb8d674f..48def0784fd 100644 --- a/app/contexts/search_context.rb +++ b/app/contexts/search_context.rb @@ -10,10 +10,19 @@ class SearchContext return result unless query.present? - result[:projects] = Project.where(id: project_ids).search(query).limit(10) - result[:merge_requests] = MergeRequest.where(project_id: project_ids).search(query).limit(10) - result[:issues] = Issue.where(project_id: project_ids).search(query).limit(10) - result[:wiki_pages] = Wiki.where(project_id: project_ids).search(query).limit(10) + projects = Project.where(id: project_ids) + result[:projects] = projects.search(query).limit(20) + + # Search inside single project + project = projects.first if projects.length == 1 + + if params[:search_code].present? + result[:blobs] = project.repository.search_files(query, params[:repository_ref]) unless project.empty_repo? + else + result[:merge_requests] = MergeRequest.in_projects(project_ids).search(query).order('updated_at DESC').limit(20) + result[:issues] = Issue.where(project_id: project_ids).search(query).order('updated_at DESC').limit(20) + result[:wiki_pages] = [] + end result end @@ -22,8 +31,8 @@ class SearchContext projects: [], merge_requests: [], issues: [], - wiki_pages: [] + wiki_pages: [], + blobs: [] } end end - diff --git a/app/contexts/test_hook_context.rb b/app/contexts/test_hook_context.rb index d2d82a52cf5..63eda6c7d06 100644 --- a/app/contexts/test_hook_context.rb +++ b/app/contexts/test_hook_context.rb @@ -1,8 +1,7 @@ class TestHookContext < BaseContext def execute hook = project.hooks.find(params[:id]) - commits = project.repository.commits(project.default_branch, nil, 3) - data = project.post_receive_data(commits.last.id, commits.first.id, "refs/heads/#{project.default_branch}", current_user) + data = GitPushService.new.sample_data(project, current_user) hook.execute(data) end end diff --git a/app/controllers/admin/background_jobs_controller.rb b/app/controllers/admin/background_jobs_controller.rb new file mode 100644 index 00000000000..994e707965a --- /dev/null +++ b/app/controllers/admin/background_jobs_controller.rb @@ -0,0 +1,4 @@ +class Admin::BackgroundJobsController < Admin::ApplicationController + def show + end +end diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 3c27b86180b..3c80b6503fa 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -2,8 +2,5 @@ class Admin::DashboardController < Admin::ApplicationController def index @projects = Project.order("created_at DESC").limit(10) @users = User.order("created_at DESC").limit(10) - - rescue Redis::InheritedError - @resque_accessible = false end end diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index f552fb595b8..89b395786b3 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -8,12 +8,6 @@ class Admin::GroupsController < Admin::ApplicationController end def show - @projects = Project.scoped - @projects = @projects.not_in_group(@group) if @group.projects.present? - @projects = @projects.all - @projects.reject!(&:empty_repo?) - - @users = User.active end def new @@ -26,55 +20,30 @@ class Admin::GroupsController < Admin::ApplicationController def create @group = Group.new(params[:group]) @group.path = @group.name.dup.parameterize if @group.name - @group.owner = current_user if @group.save + @group.add_owner(current_user) redirect_to [:admin, @group], notice: 'Group was successfully created.' else - render action: "new" + render "new" end end def update - group_params = params[:group].dup - owner_id =group_params.delete(:owner_id) - - if owner_id - @group.owner = User.find(owner_id) - end - - if @group.update_attributes(group_params) + if @group.update_attributes(params[:group]) redirect_to [:admin, @group], notice: 'Group was successfully updated.' else - render action: "edit" - end - end - - def project_update - project_ids = params[:project_ids] - - Project.where(id: project_ids).each do |project| - project.transfer(@group) + render "edit" end - - redirect_to :back, notice: 'Group was successfully updated.' - end - - def remove_project - @project = Project.find(params[:project_id]) - @project.transfer(nil) - - redirect_to :back, notice: 'Group was successfully updated.' end def project_teams_update - @group.add_users_to_project_teams(params[:user_ids], params[:project_access]) - redirect_to [:admin, @group], notice: 'Users was successfully added.' + @group.add_users(params[:user_ids].split(','), params[:group_access]) + + redirect_to [:admin, @group], notice: 'Users were successfully added.' end def destroy - @group.truncate_teams - @group.destroy redirect_to admin_groups_path, notice: 'Group was successfully deleted.' diff --git a/app/controllers/admin/projects/application_controller.rb b/app/controllers/admin/projects/application_controller.rb deleted file mode 100644 index b3f1539f387..00000000000 --- a/app/controllers/admin/projects/application_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -# Provides a base class for Admin controllers to subclass -# -# Automatically sets the layout and ensures an administrator is logged in -class Admin::Projects::ApplicationController < Admin::ApplicationController - - protected - - def project - @project ||= Project.find_with_namespace(params[:project_id]) - end -end diff --git a/app/controllers/admin/projects/members_controller.rb b/app/controllers/admin/projects/members_controller.rb deleted file mode 100644 index d9c0d572bb1..00000000000 --- a/app/controllers/admin/projects/members_controller.rb +++ /dev/null @@ -1,32 +0,0 @@ -class Admin::Projects::MembersController < Admin::Projects::ApplicationController - def edit - @member = team_member - @project = project - @team_member_relation = team_member_relation - end - - def update - if team_member_relation.update_attributes(params[:team_member]) - redirect_to [:admin, project], notice: 'Project Access was successfully updated.' - else - render action: "edit" - end - end - - def destroy - team_member_relation.destroy - - redirect_to :back - end - - private - - def team_member - @member ||= project.users.find_by_username(params[:id]) - end - - def team_member_relation - team_member.users_projects.find_by_project_id(project) - end - -end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 8ae0bba9a2d..088174fd3b8 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -2,49 +2,20 @@ class Admin::ProjectsController < Admin::ApplicationController before_filter :project, only: [:edit, :show, :update, :destroy, :team_update] def index - @projects = Project.scoped - @projects = @projects.where(namespace_id: params[:namespace_id]) if params[:namespace_id].present? + owner_id = params[:owner_id] + user = User.find_by_id(owner_id) + + @projects = user ? user.owned_projects : Project.scoped @projects = @projects.where(public: true) if params[:public_only].present? @projects = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? - @projects = @projects.where(namespace_id: nil) if params[:namespace_id] == Namespace.global_id @projects = @projects.search(params[:name]) if params[:name].present? @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]).per(20) end def show @repository = @project.repository - @users = User.active - @users = @users.not_in_project(@project) if @project.users.present? - @users = @users.all - end - - def edit - end - - def team_update - @project.team.add_users_ids(params[:user_ids], params[:project_access]) - - redirect_to [:admin, @project], notice: 'Project was successfully updated.' - end - - def update - project.creator = current_user unless project.creator - - status = ::Projects::UpdateContext.new(project, current_user, params).execute(:admin) - - if status - redirect_to [:admin, @project], notice: 'Project was successfully updated.' - else - render action: "edit" - end - end - - def destroy - @project.team.truncate - @project.destroy - - redirect_to admin_projects_path, notice: 'Project was successfully deleted.' + @group = @project.group end protected diff --git a/app/controllers/admin/resque_controller.rb b/app/controllers/admin/resque_controller.rb deleted file mode 100644 index 7d489ab4876..00000000000 --- a/app/controllers/admin/resque_controller.rb +++ /dev/null @@ -1,4 +0,0 @@ -class Admin::ResqueController < Admin::ApplicationController - def show - end -end diff --git a/app/controllers/admin/teams/application_controller.rb b/app/controllers/admin/teams/application_controller.rb deleted file mode 100644 index 8710821454e..00000000000 --- a/app/controllers/admin/teams/application_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -# Provides a base class for Admin controllers to subclass -# -# Automatically sets the layout and ensures an administrator is logged in -class Admin::Teams::ApplicationController < Admin::ApplicationController - - private - - def user_team - @team = UserTeam.find_by_path(params[:team_id]) - end -end diff --git a/app/controllers/admin/teams/members_controller.rb b/app/controllers/admin/teams/members_controller.rb deleted file mode 100644 index e7dbcad568f..00000000000 --- a/app/controllers/admin/teams/members_controller.rb +++ /dev/null @@ -1,41 +0,0 @@ -class Admin::Teams::MembersController < Admin::Teams::ApplicationController - def new - @users = User.potential_team_members(user_team) - @users = UserDecorator.decorate @users - end - - def create - unless params[:user_ids].blank? - user_ids = params[:user_ids] - access = params[:default_project_access] - is_admin = params[:group_admin] - user_team.add_members(user_ids, access, is_admin) - end - - redirect_to admin_team_path(user_team), notice: 'Members was successfully added into Team of users.' - end - - def edit - team_member - end - - def update - options = {default_projects_access: params[:default_project_access], group_admin: params[:group_admin]} - if user_team.update_membership(team_member, options) - redirect_to admin_team_path(user_team), notice: "Membership for #{team_member.name} was successfully updated in Team of users." - else - render :edit - end - end - - def destroy - user_team.remove_member(team_member) - redirect_to admin_team_path(user_team), notice: "Member #{team_member.name} was successfully removed from Team of users." - end - - protected - - def team_member - @member ||= user_team.members.find_by_username(params[:id]) - end -end diff --git a/app/controllers/admin/teams/projects_controller.rb b/app/controllers/admin/teams/projects_controller.rb deleted file mode 100644 index 8584a188b20..00000000000 --- a/app/controllers/admin/teams/projects_controller.rb +++ /dev/null @@ -1,41 +0,0 @@ -class Admin::Teams::ProjectsController < Admin::Teams::ApplicationController - def new - @projects = Project.scoped - @projects = @projects.without_team(user_team) if user_team.projects.any? - #@projects.reject!(&:empty_repo?) - end - - def create - unless params[:project_ids].blank? - project_ids = params[:project_ids] - access = params[:greatest_project_access] - user_team.assign_to_projects(project_ids, access) - end - - redirect_to admin_team_path(user_team), notice: 'Team of users was successfully assgned to projects.' - end - - def edit - team_project - end - - def update - if user_team.update_project_access(team_project, params[:greatest_project_access]) - redirect_to admin_team_path(user_team), notice: 'Access was successfully updated.' - else - render :edit - end - end - - def destroy - user_team.resign_from_project(team_project) - redirect_to admin_team_path(user_team), notice: 'Team of users was successfully reassigned from project.' - end - - protected - - def team_project - @project ||= user_team.projects.find_with_namespace(params[:id]) - end - -end diff --git a/app/controllers/admin/teams_controller.rb b/app/controllers/admin/teams_controller.rb deleted file mode 100644 index 786957cbc59..00000000000 --- a/app/controllers/admin/teams_controller.rb +++ /dev/null @@ -1,59 +0,0 @@ -class Admin::TeamsController < Admin::ApplicationController - def index - @teams = UserTeam.order('name ASC') - @teams = @teams.search(params[:name]) if params[:name].present? - @teams = @teams.page(params[:page]).per(20) - end - - def show - user_team - end - - def new - @team = UserTeam.new - end - - def edit - user_team - end - - def create - @team = UserTeam.new(params[:user_team]) - @team.path = @team.name.dup.parameterize if @team.name - @team.owner = current_user - - if @team.save - redirect_to admin_team_path(@team), notice: 'Team of users was successfully created.' - else - render action: "new" - end - end - - def update - user_team_params = params[:user_team].dup - owner_id = user_team_params.delete(:owner_id) - - if owner_id - user_team.owner = User.find(owner_id) - end - - if user_team.update_attributes(user_team_params) - redirect_to admin_team_path(user_team), notice: 'Team of users was successfully updated.' - else - render action: "edit" - end - end - - def destroy - user_team.destroy - - redirect_to admin_teams_path, notice: 'Team of users was successfully deleted.' - end - - protected - - def user_team - @team ||= UserTeam.find_by_path(params[:id]) - end - -end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 2e7114e10a9..70bbe306562 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -1,70 +1,60 @@ class Admin::UsersController < Admin::ApplicationController - before_filter :admin_user, only: [:show, :edit, :update, :destroy] + before_filter :user, only: [:show, :edit, :update, :destroy] def index - @admin_users = User.scoped - @admin_users = @admin_users.filter(params[:filter]) - @admin_users = @admin_users.search(params[:name]) if params[:name].present? - @admin_users = @admin_users.alphabetically.page(params[:page]) + @users = User.scoped + @users = @users.filter(params[:filter]) + @users = @users.search(params[:name]) if params[:name].present? + @users = @users.alphabetically.page(params[:page]) end def show - # Projects user can be added to - @not_in_projects = Project.scoped - @not_in_projects = @not_in_projects.without_user(admin_user) if admin_user.authorized_projects.present? - - # Projects he already own or joined - @projects = admin_user.authorized_projects.where('projects.id in (?)', admin_user.authorized_projects.map(&:id)) - end - - def team_update - UsersProject.add_users_into_projects( - params[:project_ids], - [admin_user.id], - params[:project_access] - ) - - redirect_to [:admin, admin_user], notice: 'Teams were successfully updated.' + @projects = user.authorized_projects end - def new - @admin_user = User.new({ projects_limit: Gitlab.config.gitlab.default_projects_limit }, as: :admin) + @user = User.build_user end def edit - admin_user + user end def block - if admin_user.block + if user.block redirect_to :back, alert: "Successfully blocked" else - redirect_to :back, alert: "Error occured. User was not blocked" + redirect_to :back, alert: "Error occurred. User was not blocked" end end def unblock - if admin_user.update_attribute(:blocked, false) + if user.activate redirect_to :back, alert: "Successfully unblocked" else - redirect_to :back, alert: "Error occured. User was not unblocked" + redirect_to :back, alert: "Error occurred. User was not unblocked" end end def create admin = params[:user].delete("admin") - @admin_user = User.new(params[:user], as: :admin) - @admin_user.admin = (admin && admin.to_i > 0) + opts = { + force_random_password: true, + password_expires_at: Time.now + } + + @user = User.build_user(params[:user].merge(opts), as: :admin) + @user.admin = (admin && admin.to_i > 0) + @user.created_by_id = current_user.id respond_to do |format| - if @admin_user.save - format.html { redirect_to [:admin, @admin_user], notice: 'User was successfully created.' } - format.json { render json: @admin_user, status: :created, location: @admin_user } + if @user.save + format.html { redirect_to [:admin, @user], notice: 'User was successfully created.' } + format.json { render json: @user, status: :created, location: @user } else - format.html { render action: "new" } - format.json { render json: @admin_user.errors, status: :unprocessable_entity } + format.html { render "new" } + format.json { render json: @user.errors, status: :unprocessable_entity } end end end @@ -77,24 +67,27 @@ class Admin::UsersController < Admin::ApplicationController params[:user].delete(:password_confirmation) end - admin_user.admin = (admin && admin.to_i > 0) + user.admin = (admin && admin.to_i > 0) respond_to do |format| - if admin_user.update_attributes(params[:user], as: :admin) - format.html { redirect_to [:admin, admin_user], notice: 'User was successfully updated.' } + if user.update_attributes(params[:user], as: :admin) + format.html { redirect_to [:admin, user], notice: 'User was successfully updated.' } format.json { head :ok } else - format.html { render action: "edit" } - format.json { render json: admin_user.errors, status: :unprocessable_entity } + # restore username to keep form action url. + user.username = params[:id] + format.html { render "edit" } + format.json { render json: user.errors, status: :unprocessable_entity } end end end def destroy - if admin_user.personal_projects.count > 0 - redirect_to admin_users_path, alert: "User is a project owner and can't be removed." and return - end - admin_user.destroy + # 1. Remove groups where user is the only owner + user.solo_owned_groups.map(&:destroy) + + # 2. Remove user with all authored content including personal projects + user.destroy respond_to do |format| format.html { redirect_to admin_users_path } @@ -104,7 +97,7 @@ class Admin::UsersController < Admin::ApplicationController protected - def admin_user - @admin_user ||= User.find_by_username!(params[:id]) + def user + @user ||= User.find_by_username!(params[:id]) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1f211bac9c2..d974600dcc1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,10 +1,12 @@ class ApplicationController < ActionController::Base before_filter :authenticate_user! before_filter :reject_blocked! - before_filter :set_current_user_for_observers + before_filter :check_password_expiration + before_filter :set_current_user_for_thread before_filter :add_abilities before_filter :dev_tools if Rails.env == 'development' before_filter :default_headers + before_filter :add_gon_variables protect_from_forgery @@ -29,26 +31,25 @@ class ApplicationController < ActionController::Base end def reject_blocked! - if current_user && current_user.blocked + if current_user && current_user.blocked? sign_out current_user - flash[:alert] = "Your account is blocked. Retry when an admin unblock it." + flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it." redirect_to new_user_session_path end end def after_sign_in_path_for resource - if resource.is_a?(User) && resource.respond_to?(:blocked) && resource.blocked + if resource.is_a?(User) && resource.respond_to?(:blocked?) && resource.blocked? sign_out resource - flash[:alert] = "Your account is blocked. Retry when an admin unblock it." + flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it." new_user_session_path else super end end - def set_current_user_for_observers - MergeRequestObserver.current_user = current_user - IssueObserver.current_user = current_user + def set_current_user_for_thread + Thread.current[:current_user] = current_user end def abilities @@ -68,7 +69,7 @@ class ApplicationController < ActionController::Base @project else @project = nil - render_404 + render_404 and return end end @@ -87,19 +88,11 @@ class ApplicationController < ActionController::Base end def authorize_code_access! - return access_denied! unless can?(current_user, :download_code, project) + return access_denied! unless can?(current_user, :download_code, project) or project.public? end - def authorize_create_team! - return access_denied! unless can?(current_user, :create_team, nil) - end - - def authorize_manage_user_team! - return access_denied! unless user_team.present? && can?(current_user, :manage_user_team, user_team) - end - - def authorize_admin_user_team! - return access_denied! unless user_team.present? && can?(current_user, :admin_user_team, user_team) + def authorize_push! + return access_denied! unless can?(current_user, :push_code, project) end def access_denied! @@ -148,4 +141,23 @@ class ApplicationController < ActionController::Base headers['X-Frame-Options'] = 'DENY' headers['X-XSS-Protection'] = '1; mode=block' end + + def add_gon_variables + gon.default_issues_tracker = Project.issues_tracker.default_value + gon.api_version = API::API.version + gon.api_token = current_user.private_token if current_user + gon.gravatar_url = request.ssl? || Gitlab.config.gitlab.https ? Gitlab.config.gravatar.ssl_url : Gitlab.config.gravatar.plain_url + gon.relative_url_root = Gitlab.config.gitlab.relative_url_root + end + + def check_password_expiration + if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? + redirect_to new_profile_password_path and return + end + end + + def event_filter + filters = cookies['event_filter'].split(',') if cookies['event_filter'].present? + @event_filter ||= EventFilter.new(filters) + end end diff --git a/app/controllers/blob_controller.rb b/app/controllers/blob_controller.rb deleted file mode 100644 index d4a45d9508e..00000000000 --- a/app/controllers/blob_controller.rb +++ /dev/null @@ -1,24 +0,0 @@ -# Controller for viewing a file's blame -class BlobController < ProjectResourceController - include ExtractsPath - - # Authorize - before_filter :authorize_read_project! - before_filter :authorize_code_access! - before_filter :require_non_empty_project - - before_filter :assign_ref_vars - - def show - if @tree.is_blob? - send_data( - @tree.data, - type: @tree.mime_type, - disposition: 'inline', - filename: @tree.name - ) - else - not_found! - end - end -end diff --git a/app/controllers/compare_controller.rb b/app/controllers/compare_controller.rb deleted file mode 100644 index ae20f9c0ba6..00000000000 --- a/app/controllers/compare_controller.rb +++ /dev/null @@ -1,25 +0,0 @@ -class CompareController < ProjectResourceController - # Authorize - before_filter :authorize_read_project! - before_filter :authorize_code_access! - before_filter :require_non_empty_project - - def index - end - - def show - result = Commit.compare(project, params[:from], params[:to]) - - @commits = result[:commits] - @commit = result[:commit] - @diffs = result[:diffs] - @refs_are_same = result[:same] - @line_notes = [] - - @commits = CommitDecorator.decorate(@commits) - end - - def create - redirect_to project_compare_path(@project, params[:from], params[:to]) - end -end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index f320e819e26..ac319384434 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,15 +1,18 @@ class DashboardController < ApplicationController respond_to :html - before_filter :load_projects + before_filter :load_projects, except: [:projects] before_filter :event_filter, only: :show def show - @groups = current_user.authorized_groups + # Fetch only 30 projects. + # If user needs more - point to Dashboard#projects page + @projects_limit = 30 + + @groups = current_user.authorized_groups.sort_by(&:human_name) @has_authorized_projects = @projects.count > 0 - @teams = current_user.authorized_teams @projects_count = @projects.count - @projects = @projects.limit(20) + @projects = @projects.limit(@projects_limit) @events = Event.in_projects(current_user.authorized_projects.pluck(:id)) @events = @event_filter.apply_filter(@events) @@ -27,13 +30,22 @@ class DashboardController < ApplicationController def projects @projects = case params[:scope] when 'personal' then - @projects.personal(current_user) + current_user.namespace.projects when 'joined' then - @projects.joined(current_user) + current_user.authorized_projects.joined(current_user) + when 'owned' then + current_user.owned_projects else - @projects + current_user.authorized_projects end + @projects = @projects.where(namespace_id: Group.find_by_name(params[:group])) if params[:group].present? + @projects = @projects.includes(:namespace).sorted_by_activity + + @labels = current_user.authorized_projects.tags_on(:labels) + @groups = current_user.authorized_groups + + @projects = @projects.tagged_with(params[:label]) if params[:label].present? @projects = @projects.page(params[:page]).per(30) end @@ -62,9 +74,4 @@ class DashboardController < ApplicationController def load_projects @projects = current_user.authorized_projects.sorted_by_activity end - - def event_filter - filters = cookies['event_filter'].split(',') if cookies['event_filter'] - @event_filter ||= EventFilter.new(filters) - end end diff --git a/app/controllers/deploy_keys_controller.rb b/app/controllers/deploy_keys_controller.rb deleted file mode 100644 index a89ebbcb8d5..00000000000 --- a/app/controllers/deploy_keys_controller.rb +++ /dev/null @@ -1,39 +0,0 @@ -class DeployKeysController < ProjectResourceController - respond_to :html - - # Authorize - before_filter :authorize_admin_project! - - def index - @keys = @project.deploy_keys.all - end - - def show - @key = @project.deploy_keys.find(params[:id]) - end - - def new - @key = @project.deploy_keys.new - - respond_with(@key) - end - - def create - @key = @project.deploy_keys.new(params[:key]) - if @key.save - redirect_to project_deploy_keys_path(@project) - else - render "new" - end - end - - def destroy - @key = @project.deploy_keys.find(params[:id]) - @key.destroy - - respond_to do |format| - format.html { redirect_to project_deploy_keys_url } - format.js { render nothing: true } - end - end -end diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb new file mode 100644 index 00000000000..bf30de565ed --- /dev/null +++ b/app/controllers/files_controller.rb @@ -0,0 +1,17 @@ +class FilesController < ApplicationController + def download + note = Note.find(params[:id]) + uploader = note.attachment + + if uploader.file_storage? + if can?(current_user, :read_project, note.project) + send_file uploader.file.path, disposition: 'attachment' + else + not_found! + end + else + redirect_to uploader.url + end + end +end + diff --git a/app/controllers/graph_controller.rb b/app/controllers/graph_controller.rb deleted file mode 100644 index c370433e500..00000000000 --- a/app/controllers/graph_controller.rb +++ /dev/null @@ -1,28 +0,0 @@ -class GraphController < ProjectResourceController - include ExtractsPath - - # Authorize - before_filter :authorize_read_project! - before_filter :authorize_code_access! - before_filter :require_non_empty_project - - def show - if params.has_key?(:q) && params[:q].blank? - redirect_to project_graph_path(@project, params[:id]) - return - end - - if params.has_key?(:q) - @q = params[:q] - @commit = @project.repository.commit(@q) || @commit - end - - respond_to do |format| - format.html - format.json do - graph = Gitlab::Graph::JsonBuilder.new(project, @ref, @commit) - render :json => graph.to_json - end - end - end -end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 7b8649a6bdf..f80167da4cb 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,7 +1,5 @@ class GroupsController < ApplicationController respond_to :html - layout 'group', except: [:new, :create] - before_filter :group, except: [:new, :create] # Authorize @@ -12,6 +10,10 @@ class GroupsController < ApplicationController # Load group projects before_filter :projects, except: [:new, :create] + layout :determine_layout + + before_filter :set_title, only: [:new, :create] + def new @group = Group.new end @@ -19,9 +21,9 @@ class GroupsController < ApplicationController def create @group = Group.new(params[:group]) @group.path = @group.name.dup.parameterize if @group.name - @group.owner = current_user if @group.save + @group.add_owner(current_user) redirect_to @group, notice: 'Group was successfully created.' else render action: "new" @@ -29,7 +31,9 @@ class GroupsController < ApplicationController end def show - @events = Event.in_projects(project_ids).limit(20).offset(params[:offset] || 0) + @events = Event.in_projects(project_ids) + @events = event_filter.apply_filter(@events) + @events = @events.limit(20).offset(params[:offset] || 0) @last_push = current_user.recent_push respond_to do |format| @@ -59,44 +63,17 @@ class GroupsController < ApplicationController end end - def search - result = SearchContext.new(project_ids, params).execute - - @projects = result[:projects] - @merge_requests = result[:merge_requests] - @issues = result[:issues] - @wiki_pages = result[:wiki_pages] - end - - def people + def members @project = group.projects.find(params[:project_id]) if params[:project_id] - @users = @project ? @project.users : group.users - @users.sort_by!(&:name) - - if @project - @team_member = @project.users_projects.new - else - @team_member = UsersProject.new - end - end - - def team_members - @group.add_users_to_project_teams(params[:user_ids], params[:project_access]) - redirect_to people_group_path(@group), notice: 'Users was successfully added.' + @members = group.users_groups.order('group_access DESC') + @users_group = UsersGroup.new end def edit end def update - group_params = params[:group].dup - owner_id =group_params.delete(:owner_id) - - if owner_id - @group.owner = User.find(owner_id) - end - - if @group.update_attributes(group_params) + if @group.update_attributes(params[:group]) redirect_to @group, notice: 'Group was successfully updated.' else render action: "edit" @@ -104,7 +81,6 @@ class GroupsController < ApplicationController end def destroy - @group.truncate_teams @group.destroy redirect_to root_path, notice: 'Group was removed.' @@ -126,7 +102,7 @@ class GroupsController < ApplicationController # Dont allow unauthorized access to group def authorize_read_group! - unless projects.present? or can?(current_user, :manage_group, @group) + unless projects.present? or can?(current_user, :read_group, @group) return render_404 end end @@ -142,4 +118,16 @@ class GroupsController < ApplicationController return render_404 end end + + def set_title + @title = 'New Group' + end + + def determine_layout + if [:new, :create].include?(action_name.to_sym) + 'navless' + else + 'group' + end + end end diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index b22280d2fd2..051cbdfaf05 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -1,4 +1,18 @@ class HelpController < ApplicationController def index end + + def api + @category = params[:category] + @category = "README" if @category.blank? + + if File.exists?(Rails.root.join('doc', 'api', @category + '.md')) + render 'api' + else + not_found! + end + end + + def shortcuts + end end diff --git a/app/controllers/merge_requests_controller.rb b/app/controllers/merge_requests_controller.rb deleted file mode 100644 index ab6bf595982..00000000000 --- a/app/controllers/merge_requests_controller.rb +++ /dev/null @@ -1,145 +0,0 @@ -class MergeRequestsController < ProjectResourceController - before_filter :module_enabled - before_filter :merge_request, only: [:edit, :update, :show, :commits, :diffs, :automerge, :automerge_check, :ci_status] - before_filter :validates_merge_request, only: [:show, :diffs] - before_filter :define_show_vars, only: [:show, :diffs] - - # Allow read any merge_request - before_filter :authorize_read_merge_request! - - # Allow write(create) merge_request - before_filter :authorize_write_merge_request!, only: [:new, :create] - - # Allow modify merge_request - before_filter :authorize_modify_merge_request!, only: [:close, :edit, :update, :sort] - - def index - @merge_requests = MergeRequestsLoadContext.new(project, current_user, params).execute - end - - def show - @target_type = :merge_request - @target_id = @merge_request.id - - respond_to do |format| - format.html - format.js - - format.diff { render text: @merge_request.to_diff } - format.patch { render text: @merge_request.to_patch } - end - end - - def diffs - @diffs = @merge_request.diffs - @commit = @merge_request.last_commit - - @comments_allowed = @reply_allowed = true - @comments_target = { noteable_type: 'MergeRequest', - noteable_id: @merge_request.id } - @line_notes = @merge_request.notes.where("line_code is not null") - end - - def new - @merge_request = @project.merge_requests.new(params[:merge_request]) - end - - def edit - end - - def create - @merge_request = @project.merge_requests.new(params[:merge_request]) - @merge_request.author = current_user - - if @merge_request.save - @merge_request.reload_code - redirect_to [@project, @merge_request], notice: 'Merge request was successfully created.' - else - render action: "new" - end - end - - def update - if @merge_request.update_attributes(params[:merge_request].merge(author_id_of_changes: current_user.id)) - @merge_request.reload_code - @merge_request.mark_as_unchecked - redirect_to [@project, @merge_request], notice: 'Merge request was successfully updated.' - else - render action: "edit" - end - end - - def automerge_check - if @merge_request.unchecked? - @merge_request.check_if_can_be_merged - end - render json: {state: @merge_request.human_state} - rescue Gitlab::SatelliteNotExistError - render json: {state: :no_satellite} - end - - def automerge - return access_denied! unless can?(current_user, :accept_mr, @project) - if @merge_request.open? && @merge_request.can_be_merged? - @merge_request.should_remove_source_branch = params[:should_remove_source_branch] - @merge_request.automerge!(current_user) - @status = true - else - @status = false - end - end - - def branch_from - @commit = @repository.commit(params[:ref]) - @commit = CommitDecorator.decorate(@commit) - end - - def branch_to - @commit = @repository.commit(params[:ref]) - @commit = CommitDecorator.decorate(@commit) - end - - def ci_status - status = project.gitlab_ci_service.commit_status(merge_request.last_commit.sha) - response = { status: status } - - render json: response - end - - protected - - def merge_request - @merge_request ||= @project.merge_requests.find(params[:id]) - end - - def authorize_modify_merge_request! - return render_404 unless can?(current_user, :modify_merge_request, @merge_request) - end - - def authorize_admin_merge_request! - return render_404 unless can?(current_user, :admin_merge_request, @merge_request) - end - - def module_enabled - return render_404 unless @project.merge_requests_enabled - end - - def validates_merge_request - # Show git not found page if target branch doesnt exist - return git_not_found! unless @project.repo.heads.map(&:name).include?(@merge_request.target_branch) - - # Show git not found page if source branch doesnt exist - # and there is no saved commits between source & target branch - return git_not_found! if !@project.repo.heads.map(&:name).include?(@merge_request.source_branch) && @merge_request.commits.blank? - end - - def define_show_vars - # Build a note object for comment form - @note = @project.notes.new(noteable: @merge_request) - - # Get commits from repository - # or from cache if already merged - @commits = @merge_request.commits - @commits = CommitDecorator.decorate(@commits) - end -end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index c4ebf0e4889..7131e0fe181 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -16,35 +16,41 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end def ldap - # We only find ourselves here if the authentication to LDAP was successful. - @user = User.find_for_ldap_auth(request.env["omniauth.auth"], current_user) - if @user.persisted? - @user.remember_me = true - end - sign_in_and_redirect @user + # 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? + sign_in_and_redirect(@user) end private def handle_omniauth - oauth = request.env['omniauth.auth'] - provider, uid = oauth['provider'], oauth['uid'] - if current_user # Change a logged-in user's authentication method: - current_user.extern_uid = uid - current_user.provider = provider + current_user.extern_uid = oauth['uid'] + current_user.provider = oauth['provider'] current_user.save redirect_to profile_path else - @user = User.find_or_new_for_omniauth(oauth) + @user = Gitlab::OAuth::User.find(oauth) + + # Create user if does not exist + # and allow_single_sign_on is true + if Gitlab.config.omniauth['allow_single_sign_on'] + @user ||= Gitlab::OAuth::User.create(oauth) + end if @user - sign_in_and_redirect @user + sign_in_and_redirect(@user) else flash[:notice] = "There's no such user!" redirect_to new_user_session_path end end end + + def oauth + @oauth ||= request.env['omniauth.auth'] + end end diff --git a/app/controllers/profiles/groups_controller.rb b/app/controllers/profiles/groups_controller.rb new file mode 100644 index 00000000000..378ff6bcf34 --- /dev/null +++ b/app/controllers/profiles/groups_controller.rb @@ -0,0 +1,24 @@ +class Profiles::GroupsController < ApplicationController + layout "profile" + + def index + @user_groups = current_user.users_groups.page(params[:page]).per(20) + end + + def leave + @users_group = group.users_groups.where(user_id: current_user.id).first + + if group.last_owner?(current_user) + redirect_to(profile_groups_path, alert: "You can't leave group. You must add at least one more owner to it.") + else + @users_group.destroy + redirect_to(profile_groups_path, info: "You left #{group.name} group.") + end + end + + private + + def group + @group ||= Group.find_by_path(params[:id]) + end +end diff --git a/app/controllers/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index 1a25d834e12..c36dae2abd3 100644 --- a/app/controllers/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -1,9 +1,8 @@ -class KeysController < ApplicationController +class Profiles::KeysController < ApplicationController layout "profile" - respond_to :js, :html def index - @keys = current_user.keys.all + @keys = current_user.keys.order('id DESC').all end def show @@ -12,15 +11,16 @@ class KeysController < ApplicationController def new @key = current_user.keys.new - - respond_with(@key) end def create @key = current_user.keys.new(params[:key]) - @key.save - respond_with(@key) + if @key.save + redirect_to profile_key_path(@key) + else + render 'new' + end end def destroy @@ -28,7 +28,7 @@ class KeysController < ApplicationController @key.destroy respond_to do |format| - format.html { redirect_to keys_url } + format.html { redirect_to profile_keys_url } format.js { render nothing: true } end end diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb new file mode 100644 index 00000000000..5c492aeb49d --- /dev/null +++ b/app/controllers/profiles/notifications_controller.rb @@ -0,0 +1,26 @@ +class Profiles::NotificationsController < ApplicationController + layout 'profile' + + def show + @notification = current_user.notification + @users_projects = current_user.users_projects + @users_groups = current_user.users_groups + end + + def update + type = params[:notification_type] + + @saved = if type == 'global' + current_user.notification_level = params[:notification_level] + current_user.save + elsif type == 'group' + users_group = current_user.users_groups.find(params[:notification_id]) + users_group.notification_level = params[:notification_level] + users_group.save + else + users_project = current_user.users_projects.find(params[:notification_id]) + users_project.notification_level = params[:notification_level] + users_project.save + end + end +end diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb new file mode 100644 index 00000000000..432899f857d --- /dev/null +++ b/app/controllers/profiles/passwords_controller.rb @@ -0,0 +1,38 @@ +class Profiles::PasswordsController < ApplicationController + layout 'navless' + + skip_before_filter :check_password_expiration + + before_filter :set_user + before_filter :set_title + + def new + end + + def create + new_password = params[:user][:password] + new_password_confirmation = params[:user][:password_confirmation] + + result = @user.update_attributes( + password: new_password, + password_confirmation: new_password_confirmation + ) + + if result + @user.update_attributes(password_expires_at: nil) + redirect_to root_path, notice: 'Password successfully changed' + else + render :new + end + end + + private + + def set_user + @user = current_user + end + + def set_title + @title = "New password" + end +end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 1d1efb16f04..75f12f8a6af 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -1,5 +1,10 @@ class ProfilesController < ApplicationController + include ActionView::Helpers::SanitizeHelper + before_filter :user + before_filter :authorize_change_password!, only: :update_password + before_filter :authorize_change_username!, only: :update_username + layout 'profile' def show @@ -28,9 +33,16 @@ class ProfilesController < ApplicationController end def update_password - params[:user].reject!{ |k, v| k != "password" && k != "password_confirmation"} + password_attributes = params[:user].select do |key, value| + %w(password password_confirmation).include?(key.to_s) + end - if @user.update_attributes(params[:user]) + unless @user.valid_password?(params[:user][:current_password]) + redirect_to account_profile_path, alert: 'You must provide a valid current password' + return + end + + if @user.update_attributes(password_attributes) flash[:notice] = "Password was successfully updated. Please login with it" redirect_to new_user_session_path else @@ -63,4 +75,12 @@ class ProfilesController < ApplicationController def user @user = current_user end + + def authorize_change_password! + return render_404 if @user.ldap_user? + end + + def authorize_change_username! + return render_404 unless @user.can_change_username? + end end diff --git a/app/controllers/project_resource_controller.rb b/app/controllers/project_resource_controller.rb deleted file mode 100644 index ea78b3ff7c4..00000000000 --- a/app/controllers/project_resource_controller.rb +++ /dev/null @@ -1,4 +0,0 @@ -class ProjectResourceController < ApplicationController - before_filter :project - before_filter :repository -end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 7e4776d2d75..8fd4565f367 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -1,11 +1,26 @@ class Projects::ApplicationController < ApplicationController + before_filter :project + before_filter :repository + layout :determine_layout - before_filter :authorize_admin_team_member! + def authenticate_user! + # Restrict access to Projects area only + # for non-signed users + if !current_user + id = params[:project_id] || params[:id] + @project = Project.find_with_namespace(id) - protected + return if @project && @project.public + end - def user_team - @team ||= UserTeam.find_by_path(params[:id]) + super end + def determine_layout + if current_user + 'projects' + else + 'public_projects' + end + end end diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb new file mode 100644 index 00000000000..e58b4507202 --- /dev/null +++ b/app/controllers/projects/blame_controller.rb @@ -0,0 +1,14 @@ +# Controller for viewing a file's blame +class Projects::BlameController < Projects::ApplicationController + include ExtractsPath + + # Authorize + before_filter :authorize_read_project! + before_filter :authorize_code_access! + before_filter :require_non_empty_project + + def show + @blob = Gitlab::Git::Blob.new(@repository, @commit.id, @ref, @path) + @blame = Gitlab::Git::Blame.new(project.repository, @commit.id, @path) + end +end diff --git a/app/controllers/blame_controller.rb b/app/controllers/projects/blob_controller.rb index 37d7245ccb4..b1329c01ce7 100644 --- a/app/controllers/blame_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -1,5 +1,5 @@ # Controller for viewing a file's blame -class BlameController < ProjectResourceController +class Projects::BlobController < Projects::ApplicationController include ExtractsPath # Authorize @@ -7,10 +7,7 @@ class BlameController < ProjectResourceController before_filter :authorize_code_access! before_filter :require_non_empty_project - before_filter :assign_ref_vars - def show - @repo = @project.repo - @blame = Grit::Blob.blame(@repo, @commit.id, @path) + @blob = Gitlab::Git::Blob.new(@repository, @commit.id, @ref, @path) end end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb new file mode 100644 index 00000000000..aa6914414ce --- /dev/null +++ b/app/controllers/projects/branches_controller.rb @@ -0,0 +1,40 @@ +class Projects::BranchesController < 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_admin_project!, only: [:destroy] + + def index + @branches = Kaminari.paginate_array(@repository.branches).page(params[:page]).per(30) + end + + def recent + @branches = @repository.recent_branches + end + + def create + @repository.add_branch(params[:branch_name], params[:ref]) + + if new_branch = @repository.find_branch(params[:branch_name]) + Event.create_ref_event(@project, current_user, new_branch, 'add') + end + + redirect_to project_branches_path(@project) + end + + def destroy + branch = @repository.find_branch(params[:id]) + + if branch && @repository.rm_branch(branch.name) + Event.create_ref_event(@project, current_user, branch, 'rm') + end + + respond_to do |format| + format.html { redirect_to project_branches_path(@project) } + format.js { render nothing: true } + end + end +end diff --git a/app/controllers/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 1329410891d..bdc501d73bb 100644 --- a/app/controllers/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -1,7 +1,7 @@ # Controller for a specific Commit # # Not to be confused with CommitsController, plural. -class CommitController < ProjectResourceController +class Projects::CommitController < Projects::ApplicationController # Authorize before_filter :authorize_read_project! before_filter :authorize_code_access! @@ -11,9 +11,14 @@ class CommitController < ProjectResourceController result = CommitLoadContext.new(project, current_user, params).execute @commit = result[:commit] - git_not_found! unless @commit + + if @commit.nil? + git_not_found! + return + end @suppress_diff = result[:suppress_diff] + @force_suppress_diff = result[:force_suppress_diff] @note = result[:note] @line_notes = result[:line_notes] diff --git a/app/controllers/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 534ae1edd31..bdffc940ea5 100644 --- a/app/controllers/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -1,6 +1,6 @@ require "base64" -class CommitsController < ProjectResourceController +class Projects::CommitsController < Projects::ApplicationController include ExtractsPath # Authorize @@ -13,7 +13,6 @@ class CommitsController < ProjectResourceController @limit, @offset = (params[:limit] || 40), (params[:offset] || 0) @commits = @repo.commits(@ref, @path, @limit, @offset) - @commits = CommitDecorator.decorate(@commits) respond_to do |format| format.html # index.html.erb diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb new file mode 100644 index 00000000000..b7531e2cefb --- /dev/null +++ b/app/controllers/projects/compare_controller.rb @@ -0,0 +1,27 @@ +class Projects::CompareController < Projects::ApplicationController + # Authorize + before_filter :authorize_read_project! + before_filter :authorize_code_access! + before_filter :require_non_empty_project + + def index + end + + def show + compare = Gitlab::Git::Compare.new(@repository.raw_repository, params[:from], params[:to]) + + @commits = compare.commits + @commit = compare.commit + @diffs = compare.diffs + @refs_are_same = compare.same + @line_notes = [] + + diff_line_count = Commit::diff_line_count(@diffs) + @suppress_diff = Commit::diff_suppress?(@diffs, diff_line_count) && !params[:force_show_diff] + @force_suppress_diff = Commit::diff_force_suppress?(@diffs, diff_line_count) + end + + def create + redirect_to project_compare_path(@project, params[:from], params[:to]) + end +end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb new file mode 100644 index 00000000000..0750e0a146f --- /dev/null +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -0,0 +1,61 @@ +class Projects::DeployKeysController < Projects::ApplicationController + respond_to :html + + # Authorize + before_filter :authorize_admin_project! + + layout "project_settings" + + def index + @enabled_keys = @project.deploy_keys.all + @available_keys = available_keys - @enabled_keys + end + + def show + @key = @project.deploy_keys.find(params[:id]) + end + + def new + @key = @project.deploy_keys.new + + respond_with(@key) + end + + def create + @key = DeployKey.new(params[:deploy_key]) + + if @key.valid? && @project.deploy_keys << @key + redirect_to project_deploy_keys_path(@project) + else + render "new" + end + end + + def destroy + @key = @project.deploy_keys.find(params[:id]) + @key.destroy + + respond_to do |format| + format.html { redirect_to project_deploy_keys_url } + format.js { render nothing: true } + end + end + + def enable + project.deploy_keys << available_keys.find(params[:id]) + + redirect_to project_deploy_keys_path(@project) + end + + def disable + @project.deploy_keys_projects.where(deploy_key_id: params[:id]).last.destroy + + redirect_to project_deploy_keys_path(@project) + end + + protected + + def available_keys + @available_keys ||= current_user.accessible_deploy_keys + end +end diff --git a/app/controllers/tree_controller.rb b/app/controllers/projects/edit_tree_controller.rb index 2151bd7cbbd..3b945fc7126 100644 --- a/app/controllers/tree_controller.rb +++ b/app/controllers/projects/edit_tree_controller.rb @@ -1,5 +1,5 @@ -# Controller for viewing a repository's file structure -class TreeController < ProjectResourceController +# Controller for edit a repository's file +class Projects::EditTreeController < Projects::ApplicationController include ExtractsPath # Authorize @@ -7,22 +7,10 @@ class TreeController < ProjectResourceController before_filter :authorize_code_access! before_filter :require_non_empty_project - before_filter :assign_ref_vars - before_filter :edit_requirements, only: [:edit, :update] + before_filter :edit_requirements, only: [:show, :update] def show - @hex_path = Digest::SHA1.hexdigest(@path) - @logs_path = logs_file_project_ref_path(@project, @ref, @path) - - respond_to do |format| - format.html - # Disable cache so browser history works - format.js { no_cache_headers } - end - end - - def edit - @last_commit = @project.repository.last_commit_for(@ref, @path).sha + @last_commit = Gitlab::Git::Commit.last_for_path(@project.repository, @ref, @path).sha end def update @@ -34,18 +22,20 @@ class TreeController < ProjectResourceController ) if updated_successfully - redirect_to project_tree_path(@project, @id), notice: "Your changes have been successfully commited" + redirect_to project_blob_path(@project, @id), notice: "Your changes have been successfully commited" else flash[:notice] = "Your changes could not be commited, because the file has been changed" - render :edit + render :show end end private def edit_requirements - unless @tree.is_blob? && @tree.text? - redirect_to project_tree_path(@project, @id), notice: "You can only edit text files" + @blob = Gitlab::Git::Blob.new(@repository, @commit.id, @ref, @path) + + unless @blob.exists? && @blob.text? + redirect_to project_blob_path(@project, @id), notice: "You can only edit text files" end allowed = if project.protected_branch? @ref diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb new file mode 100644 index 00000000000..252d47d939e --- /dev/null +++ b/app/controllers/projects/graphs_controller.rb @@ -0,0 +1,25 @@ +class Projects::GraphsController < Projects::ApplicationController + # Authorize + before_filter :authorize_read_project! + before_filter :authorize_code_access! + before_filter :require_non_empty_project + + def show + respond_to do |format| + format.html + format.js do + fetch_graph + end + end + end + + private + + def fetch_graph + @log = @project.repository.graph_log.to_json + @success = true + rescue => ex + @log = [] + @success = false + end +end diff --git a/app/controllers/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index b7d25e36252..1a94dbab5ea 100644 --- a/app/controllers/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -1,10 +1,11 @@ -class HooksController < ProjectResourceController +class Projects::HooksController < Projects::ApplicationController # Authorize - before_filter :authorize_read_project! - before_filter :authorize_admin_project!, only: [:new, :create, :destroy] + before_filter :authorize_admin_project! respond_to :html + layout "project_settings" + def index @hooks = @project.hooks.all @hook = ProjectHook.new diff --git a/app/controllers/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 9917d198cbf..e8f845b2d17 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -1,4 +1,4 @@ -class IssuesController < ProjectResourceController +class Projects::IssuesController < Projects::ApplicationController before_filter :module_enabled before_filter :issue, only: [:edit, :update, :show] @@ -14,9 +14,16 @@ class IssuesController < ProjectResourceController respond_to :js, :html def index + terms = params['issue_search'] + @issues = issues_filtered + @issues = @issues.where("title LIKE ?", "%#{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? + respond_to do |format| format.html # index.html.erb format.js @@ -76,37 +83,19 @@ class IssuesController < ProjectResourceController end end - def sort - return render_404 unless can?(current_user, :admin_issue, @project) - - @issues = @project.issues.where(id: params['issue']) - @issues.each do |issue| - issue.position = params['issue'].index(issue.id.to_s) + 1 - issue.save - end - - render nothing: true - end - - def search - terms = params['terms'] - - @issues = issues_filtered - @issues = @issues.where("title LIKE ?", "%#{terms}%") unless terms.blank? - @issues = @issues.page(params[:page]).per(100) - - render partial: 'issues' - end - def bulk_update - result = IssuesBulkUpdateContext.new(project, current_user, params).execute + result = Issues::BulkUpdateContext.new(project, current_user, params).execute redirect_to :back, notice: "#{result[:count]} issues updated" end protected def issue - @issue ||= @project.issues.find(params[:id]) + @issue ||= begin + @project.issues.find_by_iid!(params[:id]) + rescue ActiveRecord::RecordNotFound + redirect_old + end end def authorize_modify_issue! @@ -122,6 +111,22 @@ class IssuesController < ProjectResourceController end def issues_filtered - @issues = IssuesListContext.new(project, current_user, params).execute + @issues = Issues::ListContext.new(project, current_user, params).execute + end + + # Since iids are implemented only in 6.1 + # user may navigate to issue page using old global ids. + # + # To prevent 404 errors we provide a redirect to correct iids until 7.0 release + # + def redirect_old + issue = @project.issues.find_by_id(params[:id]) + + if issue + redirect_to project_issue_path(@project, issue) + return + else + raise ActiveRecord::RecordNotFound.new + end end end diff --git a/app/controllers/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 999351e22df..65f9e2b9d57 100644 --- a/app/controllers/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -1,4 +1,4 @@ -class LabelsController < ProjectResourceController +class Projects::LabelsController < Projects::ApplicationController before_filter :module_enabled # Allow read any issue @@ -7,7 +7,13 @@ class LabelsController < ProjectResourceController respond_to :js, :html def index - @labels = @project.issues_labels.order('count DESC') + @labels = @project.issues_labels + end + + def generate + Gitlab::IssuesLabels.generate(@project) + + redirect_to project_labels_path(@project) end protected diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb new file mode 100644 index 00000000000..0cc09caf1d2 --- /dev/null +++ b/app/controllers/projects/merge_requests_controller.rb @@ -0,0 +1,200 @@ +require 'gitlab/satellite/satellite' + +class Projects::MergeRequestsController < Projects::ApplicationController + before_filter :module_enabled + before_filter :merge_request, only: [:edit, :update, :show, :commits, :diffs, :automerge, :automerge_check, :ci_status] + before_filter :closes_issues, only: [:edit, :update, :show, :commits, :diffs] + before_filter :validates_merge_request, only: [:show, :diffs] + before_filter :define_show_vars, only: [:show, :diffs] + + # Allow read any merge_request + before_filter :authorize_read_merge_request! + + # Allow write(create) merge_request + before_filter :authorize_write_merge_request!, only: [:new, :create] + + # Allow modify merge_request + before_filter :authorize_modify_merge_request!, only: [:close, :edit, :update, :sort] + + def index + @merge_requests = MergeRequestsLoadContext.new(project, current_user, params).execute + 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? + end + + def show + respond_to do |format| + format.html + format.js + + format.diff { render text: @merge_request.to_diff(current_user) } + format.patch { render text: @merge_request.to_patch(current_user) } + end + end + + def diffs + @commit = @merge_request.last_commit + + @comments_allowed = @reply_allowed = true + @comments_target = {noteable_type: 'MergeRequest', + noteable_id: @merge_request.id} + @line_notes = @merge_request.notes.where("line_code is not null") + + diff_line_count = Commit::diff_line_count(@merge_request.diffs) + @suppress_diff = Commit::diff_suppress?(@merge_request.diffs, diff_line_count) && !params[:force_show_diff] + @force_suppress_diff = Commit::diff_force_suppress?(@merge_request.diffs, diff_line_count) + end + + def new + @merge_request = MergeRequest.new(params[:merge_request]) + @merge_request.source_project = @project unless @merge_request.source_project + @merge_request.target_project = @project unless @merge_request.target_project + @target_branches = @merge_request.target_project.nil? ? [] : @merge_request.target_project.repository.branch_names + @source_project = @merge_request.source_project + @merge_request + end + + def edit + @source_project = @merge_request.source_project + @target_project = @merge_request.target_project + @target_branches = @merge_request.target_project.repository.branch_names + end + + def create + @merge_request = MergeRequest.new(params[:merge_request]) + @merge_request.author = current_user + @target_branches ||= [] + if @merge_request.save + @merge_request.reload_code + redirect_to [@merge_request.target_project, @merge_request], notice: 'Merge request was successfully created.' + else + @source_project = @merge_request.source_project + @target_project = @merge_request.target_project + render action: "new" + end + end + + def update + if @merge_request.update_attributes(params[:merge_request].merge(author_id_of_changes: current_user.id)) + @merge_request.reload_code + @merge_request.mark_as_unchecked + redirect_to [@merge_request.target_project, @merge_request], notice: 'Merge request was successfully updated.' + else + render "edit" + end + end + + def automerge_check + if @merge_request.unchecked? + @merge_request.check_if_can_be_merged + end + render json: {merge_status: @merge_request.merge_status_name} + rescue Gitlab::SatelliteNotExistError + render json: {merge_status: :no_satellite} + end + + def automerge + return access_denied! unless allowed_to_merge? + + if @merge_request.opened? && @merge_request.can_be_merged? + @merge_request.should_remove_source_branch = params[:should_remove_source_branch] + @merge_request.automerge!(current_user) + @status = true + else + @status = false + end + end + + def branch_from + #This is always source + @source_project = @merge_request.nil? ? @project : @merge_request.source_project + @commit = @repository.commit(params[:ref]) if params[:ref].present? + end + + def branch_to + @target_project = selected_target_project + @commit = @target_project.repository.commit(params[:ref]) if params[:ref].present? + end + + def update_branches + @target_project = selected_target_project + @target_branches = @target_project.repository.branch_names + @target_branches + end + + def ci_status + status = project.gitlab_ci_service.commit_status(merge_request.last_commit.sha) + response = {status: status} + + render json: response + end + + protected + + def selected_target_project + ((@project.id.to_s == params[:target_project_id]) || @project.forked_project_link.nil?) ? @project : @project.forked_project_link.forked_from_project + end + + def merge_request + @merge_request ||= @project.merge_requests.find_by_iid!(params[:id]) + end + + def closes_issues + @closes_issues ||= @merge_request.closes_issues + end + + def authorize_modify_merge_request! + return render_404 unless can?(current_user, :modify_merge_request, @merge_request) + end + + def authorize_admin_merge_request! + return render_404 unless can?(current_user, :admin_merge_request, @merge_request) + end + + def module_enabled + return render_404 unless @project.merge_requests_enabled + end + + def validates_merge_request + # Show git not found page + # if there is no saved commits between source & target branch + if @merge_request.commits.blank? + # and if source target doesn't exist + return invalid_mr unless @merge_request.target_project.repository.branch_names.include?(@merge_request.target_branch) + + # or if source branch doesn't exist + return invalid_mr unless @merge_request.source_project.repository.branch_names.include?(@merge_request.source_branch) + end + end + + def define_show_vars + # Build a note object for comment form + @note = @project.notes.new(noteable: @merge_request) + + # Get commits from repository + # or from cache if already merged + @commits = @merge_request.commits + + @allowed_to_merge = allowed_to_merge? + @show_merge_controls = @merge_request.opened? && @commits.any? && @allowed_to_merge + + @target_type = :merge_request + @target_id = @merge_request.id + end + + def allowed_to_merge? + action = if project.protected_branch?(@merge_request.target_branch) + :push_code_to_protected_branches + else + :push_code + end + + can?(current_user, action, @project) + end + + def invalid_mr + # Render special view for MR with removed source or target branch + render 'invalid' + end +end diff --git a/app/controllers/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index a0c824e8abb..39cd579cce5 100644 --- a/app/controllers/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -1,4 +1,4 @@ -class MilestonesController < ProjectResourceController +class Projects::MilestonesController < Projects::ApplicationController before_filter :module_enabled before_filter :milestone, only: [:edit, :update, :destroy, :show] @@ -12,9 +12,9 @@ class MilestonesController < ProjectResourceController def index @milestones = case params[:f] - when 'all'; @project.milestones.order("closed, due_date DESC") + when 'all'; @project.milestones.order("state, due_date DESC") when 'closed'; @project.milestones.closed.order("due_date DESC") - else @project.milestones.active.order("due_date ASC") + else @project.milestones.active.order("due_date DESC") end @milestones = @milestones.includes(:project) @@ -32,7 +32,7 @@ class MilestonesController < ProjectResourceController def show @issues = @milestone.issues - @users = UserDecorator.decorate(@milestone.participants) + @users = @milestone.participants.uniq @merge_requests = @milestone.merge_requests respond_to do |format| @@ -81,7 +81,7 @@ class MilestonesController < ProjectResourceController protected def milestone - @milestone ||= @project.milestones.find(params[:id]) + @milestone ||= @project.milestones.find_by_iid!(params[:id]) end def authorize_admin_milestone! diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb new file mode 100644 index 00000000000..9832495c64f --- /dev/null +++ b/app/controllers/projects/network_controller.rb @@ -0,0 +1,19 @@ +class Projects::NetworkController < Projects::ApplicationController + include ExtractsPath + include ApplicationHelper + + # Authorize + before_filter :authorize_read_project! + before_filter :authorize_code_access! + before_filter :require_non_empty_project + + def show + respond_to do |format| + format.html + + format.json do + @graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref]) + end + end + end +end diff --git a/app/controllers/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 000c7bbb641..8214163c315 100644 --- a/app/controllers/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -1,4 +1,4 @@ -class NotesController < ProjectResourceController +class Projects::NotesController < Projects::ApplicationController # Authorize before_filter :authorize_read_note! before_filter :authorize_write_note!, only: [:create] @@ -38,6 +38,32 @@ class NotesController < ProjectResourceController end end + def update + @note = @project.notes.find(params[:id]) + return access_denied! unless can?(current_user, :admin_note, @note) + + @note.update_attributes(params[:note]) + + respond_to do |format| + format.js do + render js: { success: @note.valid?, id: @note.id, note: view_context.markdown(@note.note) }.to_json + end + format.html do + redirect_to :back + end + end + end + + def delete_attachment + @note = @project.notes.find(params[:id]) + @note.remove_attachment! + @note.update_attribute(:attachment, nil) + + respond_to do |format| + format.js { render nothing: true } + end + end + def preview render text: view_context.markdown(params[:note]) end @@ -71,7 +97,6 @@ class NotesController < ProjectResourceController # Helps to distinguish e.g. commit notes in mr notes list def note_for_main_target?(note) - note.for_wall? || - (@target_type.camelize == note.noteable_type && !note.for_diff_line?) + (@target_type.camelize == note.noteable_type && !note.for_diff_line?) end end diff --git a/app/controllers/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index fd2734eff84..81531bb0ac0 100644 --- a/app/controllers/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -1,4 +1,4 @@ -class ProtectedBranchesController < ProjectResourceController +class Projects::ProtectedBranchesController < Projects::ApplicationController # Authorize before_filter :authorize_read_project! before_filter :require_non_empty_project diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb new file mode 100644 index 00000000000..0c23d411f4c --- /dev/null +++ b/app/controllers/projects/raw_controller.rb @@ -0,0 +1,33 @@ +# Controller for viewing a file's raw +class Projects::RawController < Projects::ApplicationController + include ExtractsPath + + # Authorize + before_filter :authorize_read_project! + before_filter :authorize_code_access! + before_filter :require_non_empty_project + + def show + @blob = Gitlab::Git::Blob.new(@repository, @commit.id, @ref, @path) + + if @blob.exists? + type = if @blob.mime_type =~ /html|javascript/ + 'text/plain; charset=utf-8' + else + @blob.mime_type + end + + headers['X-Content-Type-Options'] = 'nosniff' + + send_data( + @blob.data, + type: type, + disposition: 'inline', + filename: @blob.name + ) + else + not_found! + end + end +end + diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb new file mode 100644 index 00000000000..e5c090e1f4d --- /dev/null +++ b/app/controllers/projects/refs_controller.rb @@ -0,0 +1,43 @@ +class Projects::RefsController < Projects::ApplicationController + include ExtractsPath + + # Authorize + before_filter :authorize_read_project! + before_filter :authorize_code_access! + before_filter :require_non_empty_project + + def switch + respond_to do |format| + format.html do + new_path = if params[:destination] == "tree" + project_tree_path(@project, (@id)) + elsif params[:destination] == "blob" + project_blob_path(@project, (@id)) + elsif params[:destination] == "graph" + project_network_path(@project, @id, @options) + else + project_commits_path(@project, @id) + end + + redirect_to new_path + end + format.js do + @ref = params[:ref] + define_tree_vars + render "tree" + end + end + end + + def logs_tree + contents = @tree.entries + @logs = contents.map do |content| + file = params[:path] ? File.join(params[:path], content.name) : content.name + last_commit = @repo.commits(@commit.id, file, 1).last + { + file_name: content.name, + commit: last_commit + } + end + end +end diff --git a/app/controllers/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index 229cb36949b..20e2a9311ee 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -1,23 +1,11 @@ -class RepositoriesController < ProjectResourceController +class Projects::RepositoriesController < Projects::ApplicationController # Authorize before_filter :authorize_read_project! before_filter :authorize_code_access! before_filter :require_non_empty_project - def show - @activities = @repository.commits_with_refs(20) - end - - def branches - @branches = @repository.branches - end - - def tags - @tags = @repository.tags - end - def stats - @stats = Gitlab::GitStats.new(@repository.raw, @repository.root_ref) + @stats = Gitlab::Git::Stats.new(@repository.raw, @repository.root_ref) @graph = @stats.graph end @@ -26,8 +14,9 @@ class RepositoriesController < ProjectResourceController render_404 and return end + storage_path = Rails.root.join("tmp", "repositories") - file_path = @repository.archive_repo(params[:ref]) + file_path = @repository.archive_repo(params[:ref], storage_path) if file_path # Send file to user diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb new file mode 100644 index 00000000000..6db22186c14 --- /dev/null +++ b/app/controllers/projects/services_controller.rb @@ -0,0 +1,39 @@ +class Projects::ServicesController < Projects::ApplicationController + # Authorize + before_filter :authorize_admin_project! + before_filter :service, only: [:edit, :update, :test] + + respond_to :html + + layout "project_settings" + + def index + @project.build_missing_services + @services = @project.services.reload + end + + def edit + end + + def update + if @service.update_attributes(params[:service]) + redirect_to edit_project_service_path(@project, @service.to_param) + else + render 'edit' + end + end + + def test + data = GitPushService.new.sample_data(project, current_user) + + @service.execute(data) + + redirect_to :back + end + + private + + def service + @service ||= @project.services.find { |service| service.to_param == params[:id] } + end +end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb new file mode 100644 index 00000000000..dd0c1a57089 --- /dev/null +++ b/app/controllers/projects/snippets_controller.rb @@ -0,0 +1,89 @@ +class Projects::SnippetsController < Projects::ApplicationController + before_filter :module_enabled + before_filter :snippet, only: [:show, :edit, :destroy, :update, :raw] + + # Allow read any snippet + before_filter :authorize_read_project_snippet! + + # Allow write(create) snippet + before_filter :authorize_write_project_snippet!, only: [:new, :create] + + # Allow modify snippet + before_filter :authorize_modify_project_snippet!, only: [:edit, :update] + + # Allow destroy snippet + before_filter :authorize_admin_project_snippet!, only: [:destroy] + + respond_to :html + + def index + @snippets = @project.snippets.fresh.non_expired + end + + def new + @snippet = @project.snippets.build + end + + def create + @snippet = @project.snippets.build(params[:project_snippet]) + @snippet.author = current_user + + if @snippet.save + redirect_to project_snippet_path(@project, @snippet) + else + respond_with(@snippet) + end + end + + def edit + end + + def update + if @snippet.update_attributes(params[:project_snippet]) + redirect_to project_snippet_path(@project, @snippet) + else + respond_with(@snippet) + end + end + + def show + @note = @project.notes.new(noteable: @snippet) + @target_type = :snippet + @target_id = @snippet.id + end + + def destroy + return access_denied! unless can?(current_user, :admin_project_snippet, @snippet) + + @snippet.destroy + + redirect_to project_snippets_path(@project) + end + + def raw + send_data( + @snippet.content, + type: "text/plain", + disposition: 'inline', + filename: @snippet.file_name + ) + end + + protected + + def snippet + @snippet ||= @project.snippets.find(params[:id]) + end + + def authorize_modify_project_snippet! + return render_404 unless can?(current_user, :modify_project_snippet, @snippet) + end + + def authorize_admin_project_snippet! + return render_404 unless can?(current_user, :admin_project_snippet, @snippet) + end + + def module_enabled + return render_404 unless @project.snippets_enabled + end +end diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb new file mode 100644 index 00000000000..9dbb0d81888 --- /dev/null +++ b/app/controllers/projects/tags_controller.rb @@ -0,0 +1,36 @@ +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_admin_project!, only: [:destroy] + + def index + @tags = Kaminari.paginate_array(@repository.tags).page(params[:page]).per(30) + end + + def create + @repository.add_tag(params[:tag_name], params[:ref]) + + if new_tag = @repository.find_tag(params[:tag_name]) + Event.create_ref_event(@project, current_user, new_tag, 'add', 'refs/tags') + end + + redirect_to project_tags_path(@project) + end + + def destroy + tag = @repository.find_tag(params[:id]) + + if tag && @repository.rm_tag(tag.name) + Event.create_ref_event(@project, current_user, tag, 'rm', 'refs/tags') + end + + respond_to do |format| + format.html { redirect_to project_tags_path } + format.js { render nothing: true } + end + end +end diff --git a/app/controllers/team_members_controller.rb b/app/controllers/projects/team_members_controller.rb index 18d4ae3ac96..b4b318fa59e 100644 --- a/app/controllers/team_members_controller.rb +++ b/app/controllers/projects/team_members_controller.rb @@ -1,15 +1,12 @@ -class TeamMembersController < ProjectResourceController +class Projects::TeamMembersController < Projects::ApplicationController # Authorize - before_filter :authorize_read_project! - before_filter :authorize_admin_project!, except: [:index, :show] + before_filter :authorize_admin_project! - def index - @teams = UserTeam.scoped - end + layout "project_settings" - def show - @user_project_relation = project.users_projects.find_by_user_id(member) - @events = member.recent_events.in_projects(project).limit(7) + def index + @group = @project.group + @users_projects = @project.users_projects.order('project_access DESC') end def new @@ -17,7 +14,7 @@ class TeamMembersController < ProjectResourceController end def create - users = User.where(id: params[:user_ids]) + users = User.where(id: params[:user_ids].split(',')) @project.team << [users, params[:project_access]] @@ -51,9 +48,9 @@ class TeamMembersController < ProjectResourceController def apply_import giver = Project.find(params[:source_project_id]) status = @project.team.import(giver) - notice = status ? "Succesfully imported" : "Import failed" + notice = status ? "Successfully imported" : "Import failed" - redirect_to project_team_members_path(project), notice: notice + redirect_to project_team_index_path(project), notice: notice end protected diff --git a/app/controllers/projects/teams_controller.rb b/app/controllers/projects/teams_controller.rb deleted file mode 100644 index 3ca724aaf4d..00000000000 --- a/app/controllers/projects/teams_controller.rb +++ /dev/null @@ -1,27 +0,0 @@ -class Projects::TeamsController < Projects::ApplicationController - - def available - @teams = current_user.is_admin? ? UserTeam.scoped : current_user.user_teams - @teams = @teams.without_project(project) - unless @teams.any? - redirect_to project_team_index_path(project), notice: "No avaliable teams for assigment." - end - end - - def assign - unless params[:team_id].blank? - team = UserTeam.find(params[:team_id]) - access = params[:greatest_project_access] - team.assign_to_project(project, access) - end - redirect_to project_team_index_path(project) - end - - def resign - team = project.user_teams.find_by_path(params[:id]) - team.resign_from_project(project) - - redirect_to project_team_index_path(project) - end - -end diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb new file mode 100644 index 00000000000..5d543f35665 --- /dev/null +++ b/app/controllers/projects/tree_controller.rb @@ -0,0 +1,17 @@ +# Controller for viewing a repository's file structure +class Projects::TreeController < Projects::ApplicationController + include ExtractsPath + + # Authorize + before_filter :authorize_read_project! + before_filter :authorize_code_access! + before_filter :require_non_empty_project + + def show + respond_to do |format| + format.html + # Disable cache so browser history works + format.js { no_cache_headers } + end + end +end diff --git a/app/controllers/projects/walls_controller.rb b/app/controllers/projects/walls_controller.rb new file mode 100644 index 00000000000..834215a1473 --- /dev/null +++ b/app/controllers/projects/walls_controller.rb @@ -0,0 +1,20 @@ +class Projects::WallsController < Projects::ApplicationController + before_filter :module_enabled + + respond_to :js, :html + + def show + @note = @project.notes.new + + respond_to do |format| + format.html + end + end + + protected + + def module_enabled + return render_404 unless @project.wall_enabled + end +end + diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb new file mode 100644 index 00000000000..797f3d3dd41 --- /dev/null +++ b/app/controllers/projects/wikis_controller.rb @@ -0,0 +1,95 @@ +class Projects::WikisController < Projects::ApplicationController + before_filter :authorize_read_wiki! + before_filter :authorize_write_wiki!, only: [:edit, :create, :history] + before_filter :authorize_admin_wiki!, only: :destroy + before_filter :load_gollum_wiki + + def pages + @wiki_pages = @gollum_wiki.pages + end + + def show + @wiki = @gollum_wiki.find_page(params[:id], params[:version_id]) + + if @wiki + render 'show' + else + return render('empty') unless can?(current_user, :write_wiki, @project) + @wiki = WikiPage.new(@gollum_wiki) + @wiki.title = params[:id] + + render 'edit' + end + end + + def edit + @wiki = @gollum_wiki.find_page(params[:id]) + end + + def update + @wiki = @gollum_wiki.find_page(params[:id]) + + return render('empty') unless can?(current_user, :write_wiki, @project) + + if @wiki.update(content, format, message) + redirect_to [@project, @wiki], notice: 'Wiki was successfully updated.' + else + render 'edit' + end + end + + def create + @wiki = WikiPage.new(@gollum_wiki) + + if @wiki.create(wiki_params) + redirect_to project_wiki_path(@project, @wiki), notice: 'Wiki was successfully updated.' + else + render action: "edit" + end + end + + def history + @wiki = @gollum_wiki.find_page(params[:id]) + + redirect_to(project_wiki_path(@project, :home), notice: "Page not found") unless @wiki + end + + def destroy + @wiki = @gollum_wiki.find_page(params[:id]) + @wiki.delete if @wiki + redirect_to project_wiki_path(@project, :home), notice: "Page was successfully deleted" + end + + def git_access + end + + private + + def load_gollum_wiki + @gollum_wiki = GollumWiki.new(@project, current_user) + + # Call #wiki to make sure the Wiki Repo is initialized + @gollum_wiki.wiki + rescue GollumWiki::CouldNotCreateWikiError => ex + flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." + redirect_to @project + return false + end + + def wiki_params + params[:wiki].slice(:title, :content, :format, :message) + end + + def content + params[:wiki][:content] + end + + def format + params[:wiki][:format] + end + + def message + params[:wiki][:message] + end + +end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 5da3fbf591c..7264128691e 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,21 +1,22 @@ -require Rails.root.join('lib', 'gitlab', 'graph', 'json_builder') - -class ProjectsController < ProjectResourceController - skip_before_filter :project, only: [:new, :create] - skip_before_filter :repository, only: [:new, :create] +class ProjectsController < ApplicationController + skip_before_filter :authenticate_user!, only: [:show] + before_filter :project, except: [:new, :create] + 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] + before_filter :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer] before_filter :require_non_empty_project, only: [:blob, :tree, :graph] - layout 'application', only: [:new, :create] + layout 'navless', only: [:new, :create, :fork] + before_filter :set_title, only: [:new, :create] def new @project = Project.new end def edit + render 'edit', layout: "project_settings" end def create @@ -27,7 +28,7 @@ class ProjectsController < ProjectResourceController if @project.saved? redirect_to @project else - render action: "new" + render "new" end end format.js @@ -35,69 +36,97 @@ class ProjectsController < ProjectResourceController end def update - status = ::Projects::UpdateContext.new(project, current_user, params).execute + status = ::Projects::UpdateContext.new(@project, current_user, params).execute respond_to do |format| if status flash[:notice] = 'Project was successfully updated.' - format.html { redirect_to edit_project_path(project), notice: 'Project was successfully updated.' } + format.html { redirect_to edit_project_path(@project), notice: 'Project was successfully updated.' } format.js else - format.html { render action: "edit" } + format.html { render "edit", layout: "project_settings" } format.js end end + end - rescue Project::TransferError => ex - @error = ex - render :update_failed + def transfer + ::Projects::TransferContext.new(project, current_user, params).execute end def show + return authenticate_user! unless @project.public || current_user + limit = (params[:limit] || 20).to_i - @events = @project.events.recent.limit(limit).offset(params[:offset] || 0) + @events = @project.events.recent + @events = event_filter.apply_filter(@events) + @events = @events.limit(limit).offset(params[:offset] || 0) + + # Ensure project default branch is set if it possible + # Normally it defined on push or during creation + @project.discover_default_branch respond_to do |format| format.html do - if @project.repository && !@project.repository.empty? - @last_push = current_user.recent_push(@project.id) - render :show + if @project.empty_repo? + render "projects/empty", layout: user_layout else - render "projects/empty" + if current_user + @last_push = current_user.recent_push(@project.id) + end + render :show, layout: user_layout end end format.js end end - def files - @notes = @project.notes.where("attachment != 'NULL'").order("created_at DESC").limit(100) - end + def destroy + return access_denied! unless can?(current_user, :remove_project, project) - # - # Wall - # + project.team.truncate + project.destroy - def wall - return render_404 unless @project.wall_enabled + respond_to do |format| + format.html { redirect_to root_path } + end + end - @target_type = :wall - @target_id = nil - @note = @project.notes.new + def fork + @forked_project = ::Projects::ForkContext.new(project, current_user).execute respond_to do |format| - format.html + 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 destroy - return access_denied! unless can?(current_user, :remove_project, project) - - project.team.truncate - project.destroy + def autocomplete_sources + @suggestions = { + emojis: Emoji.names, + issues: @project.issues.select([:iid, :title, :description]), + members: @project.team.members.sort_by(&:username).map { |user| { username: user.username, name: user.name } } + } respond_to do |format| - format.html { redirect_to root_path } + format.json { render :json => @suggestions } end end + + private + + def set_title + @title = 'New Project' + end + + def user_layout + current_user ? "projects" : "public_projects" + end end diff --git a/app/controllers/public/projects_controller.rb b/app/controllers/public/projects_controller.rb index b929b23e68c..87e903a1d2d 100644 --- a/app/controllers/public/projects_controller.rb +++ b/app/controllers/public/projects_controller.rb @@ -1,12 +1,13 @@ class Public::ProjectsController < ApplicationController skip_before_filter :authenticate_user!, - :reject_blocked, :set_current_user_for_observers, - :add_abilities + :reject_blocked, :set_current_user_for_observers, + :add_abilities layout 'public' def index @projects = Project.public_only + @projects = @projects.search(params[:search]) if params[:search].present? @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]).per(20) end end diff --git a/app/controllers/refs_controller.rb b/app/controllers/refs_controller.rb deleted file mode 100644 index 0e4dba3dc4b..00000000000 --- a/app/controllers/refs_controller.rb +++ /dev/null @@ -1,69 +0,0 @@ -class RefsController < ProjectResourceController - - # Authorize - before_filter :authorize_read_project! - before_filter :authorize_code_access! - before_filter :require_non_empty_project - - before_filter :ref - before_filter :define_tree_vars, only: [:blob, :logs_tree] - - def switch - respond_to do |format| - format.html do - new_path = if params[:destination] == "tree" - project_tree_path(@project, (@ref + "/" + params[:path])) - elsif params[:destination] == "graph" - project_graph_path(@project, @ref) - else - project_commits_path(@project, @ref) - end - - redirect_to new_path - end - format.js do - @ref = params[:ref] - define_tree_vars - render "tree" - end - end - end - - def logs_tree - contents = @tree.contents - @logs = contents.map do |content| - file = params[:path] ? File.join(params[:path], content.name) : content.name - last_commit = @repo.commits(@commit.id, file, 1).last - last_commit = CommitDecorator.decorate(last_commit) - { - file_name: content.name, - commit: last_commit - } - end - end - - protected - - def define_tree_vars - params[:path] = nil if params[:path].blank? - - @repo = project.repository - @commit = @repo.commit(@ref) - @commit = CommitDecorator.decorate(@commit) - @tree = Tree.new(@commit.tree, @ref, params[:path]) - @tree = TreeDecorator.new(@tree) - @hex_path = Digest::SHA1.hexdigest(params[:path] || "") - - if params[:path] - @logs_path = logs_file_project_ref_path(@project, @ref, params[:path]) - else - @logs_path = logs_tree_project_ref_path(@project, @ref) - end - rescue - return render_404 - end - - def ref - @ref = params[:id] || params[:ref] - end -end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index b7ee75619cb..5f18bac82ed 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -2,9 +2,6 @@ class RegistrationsController < Devise::RegistrationsController before_filter :signup_enabled? def destroy - if current_user.owned_projects.count > 0 - redirect_to account_profile_path, alert: "Remove projects and groups before removing account." and return - end current_user.destroy respond_to do |format| @@ -12,9 +9,16 @@ class RegistrationsController < Devise::RegistrationsController end end + protected + + def build_resource(hash=nil) + super + self.resource.with_defaults + end + private def signup_enabled? redirect_to new_user_session_path unless Gitlab.config.gitlab.signup_enabled end -end
\ No newline at end of file +end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index bbd67df6c70..f5c3bb133ed 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -6,9 +6,11 @@ class SearchController < ApplicationController project_ids = current_user.authorized_projects.map(&:id) if group_id.present? - group_project_ids = Group.find(group_id).projects.map(&:id) + @group = Group.find(group_id) + group_project_ids = @group.projects.map(&:id) project_ids.select! { |id| group_project_ids.include?(id)} elsif project_id.present? + @project = Project.find(params[:project_id]) project_ids.select! { |id| id == project_id.to_i} end @@ -18,5 +20,7 @@ class SearchController < ApplicationController @merge_requests = result[:merge_requests] @issues = result[:issues] @wiki_pages = result[:wiki_pages] + @blobs = Kaminari.paginate_array(result[:blobs]).page(params[:page]).per(20) + @total_results = @projects.count + @merge_requests.count + @issues.count + @wiki_pages.count + @blobs.total_count end end diff --git a/app/controllers/services_controller.rb b/app/controllers/services_controller.rb deleted file mode 100644 index d0df469b967..00000000000 --- a/app/controllers/services_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -class ServicesController < ProjectResourceController - # Authorize - before_filter :authorize_admin_project! - - respond_to :html - - def index - @gitlab_ci_service = @project.gitlab_ci_service - end - - def edit - @service = @project.gitlab_ci_service - - # Create if missing - @service = @project.create_gitlab_ci_service unless @service - end - - def update - @service = @project.gitlab_ci_service - - if @service.update_attributes(params[:service]) - redirect_to edit_project_service_path(@project, :gitlab_ci) - else - render 'edit' - end - end - - def test - commits = project.repository.commits(project.default_branch, nil, 3) - data = project.post_receive_data(commits.last.id, commits.first.id, "refs/heads/#{project.default_branch}", current_user) - - @service = project.gitlab_ci_service - @service.execute(data) - - redirect_to :back - end -end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 26898abfa82..b91f68aab5e 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,37 +1,60 @@ -class SnippetsController < ProjectResourceController +class SnippetsController < ApplicationController before_filter :snippet, only: [:show, :edit, :destroy, :update, :raw] - # Allow read any snippet - before_filter :authorize_read_snippet! - - # Allow write(create) snippet - before_filter :authorize_write_snippet!, only: [:new, :create] - # Allow modify snippet before_filter :authorize_modify_snippet!, only: [:edit, :update] # Allow destroy snippet before_filter :authorize_admin_snippet!, only: [:destroy] + before_filter :set_title + respond_to :html + layout 'navless' + def index - @snippets = @project.snippets.fresh.non_expired + @snippets = Snippet.public.fresh.non_expired.page(params[:page]).per(20) + end + + def user_index + @user = User.find_by_username(params[:username]) + @snippets = @user.snippets.fresh.non_expired + + if @user == current_user + @snippets = case params[:scope] + when 'public' then + @snippets.public + when 'private' then + @snippets.private + else + @snippets + end + else + @snippets = @snippets.public + end + + @snippets = @snippets.page(params[:page]).per(20) + + if @user == current_user + render 'current_user_index' + else + render 'user_index' + end end def new - @snippet = @project.snippets.new + @snippet = PersonalSnippet.new end def create - @snippet = @project.snippets.new(params[:snippet]) + @snippet = PersonalSnippet.new(params[:personal_snippet]) @snippet.author = current_user - @snippet.save - if @snippet.valid? - redirect_to [@project, @snippet] + if @snippet.save + redirect_to snippet_path(@snippet) else - respond_with(@snippet) + respond_with @snippet end end @@ -39,27 +62,22 @@ class SnippetsController < ProjectResourceController end def update - @snippet.update_attributes(params[:snippet]) - - if @snippet.valid? - redirect_to [@project, @snippet] + if @snippet.update_attributes(params[:personal_snippet]) + redirect_to snippet_path(@snippet) else - respond_with(@snippet) + respond_with @snippet end end def show - @note = @project.notes.new(noteable: @snippet) - @target_type = :snippet - @target_id = @snippet.id end def destroy - return access_denied! unless can?(current_user, :admin_snippet, @snippet) + return access_denied! unless can?(current_user, :admin_personal_snippet, @snippet) @snippet.destroy - redirect_to project_snippets_path(@project) + redirect_to snippets_path end def raw @@ -74,14 +92,18 @@ class SnippetsController < ProjectResourceController protected def snippet - @snippet ||= @project.snippets.find(params[:id]) + @snippet ||= PersonalSnippet.where('author_id = :user_id or private is false', user_id: current_user.id).find(params[:id]) end def authorize_modify_snippet! - return render_404 unless can?(current_user, :modify_snippet, @snippet) + return render_404 unless can?(current_user, :modify_personal_snippet, @snippet) end def authorize_admin_snippet! - return render_404 unless can?(current_user, :admin_snippet, @snippet) + return render_404 unless can?(current_user, :admin_personal_snippet, @snippet) + end + + def set_title + @title = 'Snippets' end end diff --git a/app/controllers/teams/application_controller.rb b/app/controllers/teams/application_controller.rb deleted file mode 100644 index fc23202610c..00000000000 --- a/app/controllers/teams/application_controller.rb +++ /dev/null @@ -1,13 +0,0 @@ -class Teams::ApplicationController < ApplicationController - - layout 'user_team' - - before_filter :authorize_manage_user_team! - - protected - - def user_team - @team ||= UserTeam.find_by_path(params[:team_id]) - end - -end diff --git a/app/controllers/teams/members_controller.rb b/app/controllers/teams/members_controller.rb deleted file mode 100644 index db218b8ca5e..00000000000 --- a/app/controllers/teams/members_controller.rb +++ /dev/null @@ -1,49 +0,0 @@ -class Teams::MembersController < Teams::ApplicationController - - skip_before_filter :authorize_manage_user_team!, only: [:index] - - def index - @members = user_team.members - end - - def new - @users = User.potential_team_members(user_team) - @users = UserDecorator.decorate @users - end - - def create - unless params[:user_ids].blank? - user_ids = params[:user_ids] - access = params[:default_project_access] - is_admin = params[:group_admin] - user_team.add_members(user_ids, access, is_admin) - end - - redirect_to team_members_path(user_team), notice: 'Members was successfully added into Team of users.' - end - - def edit - team_member - end - - def update - options = {default_projects_access: params[:default_project_access], group_admin: params[:group_admin]} - if user_team.update_membership(team_member, options) - redirect_to team_members_path(user_team), notice: "Membership for #{team_member.name} was successfully updated in Team of users." - else - render :edit - end - end - - def destroy - user_team.remove_member(team_member) - redirect_to team_path(user_team), notice: "Member #{team_member.name} was successfully removed from Team of users." - end - - protected - - def team_member - @member ||= user_team.members.find_by_username(params[:id]) - end - -end diff --git a/app/controllers/teams/projects_controller.rb b/app/controllers/teams/projects_controller.rb deleted file mode 100644 index e87889b4046..00000000000 --- a/app/controllers/teams/projects_controller.rb +++ /dev/null @@ -1,57 +0,0 @@ -class Teams::ProjectsController < Teams::ApplicationController - - skip_before_filter :authorize_manage_user_team!, only: [:index] - - def index - @projects = user_team.projects - @avaliable_projects = current_user.admin? ? Project.without_team(user_team) : current_user.owned_projects.without_team(user_team) - end - - def new - user_team - @avaliable_projects = current_user.owned_projects.scoped - @avaliable_projects = @avaliable_projects.without_team(user_team) if user_team.projects.any? - - redirect_to team_projects_path(user_team), notice: "No avalible projects." unless @avaliable_projects.any? - end - - def create - redirect_to :back if params[:project_ids].blank? - - project_ids = params[:project_ids] - access = params[:greatest_project_access] - - # Reject non-allowed projects - allowed_project_ids = current_user.owned_projects.map(&:id) - project_ids.select! { |id| allowed_project_ids.include?(id.to_i) } - - # Assign projects to team - user_team.assign_to_projects(project_ids, access) - - redirect_to team_projects_path(user_team), notice: 'Team of users was successfully assigned to projects.' - end - - def edit - team_project - end - - def update - if user_team.update_project_access(team_project, params[:greatest_project_access]) - redirect_to team_projects_path(user_team), notice: 'Access was successfully updated.' - else - render :edit - end - end - - def destroy - user_team.resign_from_project(team_project) - redirect_to team_projects_path(user_team), notice: 'Team of users was successfully reassigned from project.' - end - - private - - def team_project - @project ||= user_team.projects.find_with_namespace(params[:id]) - end - -end diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb deleted file mode 100644 index ef66b77e232..00000000000 --- a/app/controllers/teams_controller.rb +++ /dev/null @@ -1,76 +0,0 @@ -class TeamsController < ApplicationController - # Authorize - before_filter :authorize_create_team!, only: [:new, :create] - before_filter :authorize_manage_user_team!, only: [:edit, :update] - before_filter :authorize_admin_user_team!, only: [:destroy] - - before_filter :user_team, except: [:new, :create] - - layout 'user_team', except: [:new, :create] - - def show - user_team - projects - @events = Event.in_projects(user_team.project_ids).limit(20).offset(params[:offset] || 0) - end - - def edit - user_team - end - - def update - if user_team.update_attributes(params[:user_team]) - redirect_to team_path(user_team) - else - render action: :edit - end - end - - def destroy - user_team.destroy - redirect_to dashboard_path - end - - def new - @team = UserTeam.new - end - - def create - @team = UserTeam.new(params[:user_team]) - @team.owner = current_user unless params[:owner] - @team.path = @team.name.dup.parameterize if @team.name - - if @team.save - redirect_to team_path(@team) - else - render action: :new - end - end - - # Get authored or assigned open merge requests - def merge_requests - projects - @merge_requests = MergeRequest.of_user_team(user_team) - @merge_requests = FilterContext.new(@merge_requests, params).execute - @merge_requests = @merge_requests.recent.page(params[:page]).per(20) - end - - # Get only assigned issues - def issues - projects - @issues = Issue.of_user_team(user_team) - @issues = FilterContext.new(@issues, params).execute - @issues = @issues.recent.page(params[:page]).per(20) - @issues = @issues.includes(:author, :project) - end - - protected - - def projects - @projects ||= user_team.projects.sorted_by_activity - end - - def user_team - @team ||= current_user.authorized_teams.find_by_path(params[:id]) - end -end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index e027057fe65..4947c33f959 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,7 +1,11 @@ class UsersController < ApplicationController + layout 'navless' + def show @user = User.find_by_username!(params[:username]) @projects = @user.authorized_projects.where('projects.id in (?)', current_user.authorized_projects.map(&:id)) @events = @user.recent_events.where(project_id: @projects.map(&:id)).limit(20) + + @title = @user.name end end diff --git a/app/controllers/users_groups_controller.rb b/app/controllers/users_groups_controller.rb new file mode 100644 index 00000000000..749da1e1413 --- /dev/null +++ b/app/controllers/users_groups_controller.rb @@ -0,0 +1,41 @@ +class UsersGroupsController < ApplicationController + before_filter :group + + # Authorize + before_filter :authorize_admin_group! + + layout 'group' + + def create + @group.add_users(params[:user_ids].split(','), params[:group_access]) + + redirect_to members_group_path(@group), notice: 'Users were successfully added.' + end + + def update + @member = @group.users_groups.find(params[:id]) + @member.update_attributes(params[:users_group]) + end + + def destroy + @users_group = @group.users_groups.find(params[:id]) + @users_group.destroy + + respond_to do |format| + format.html { redirect_to members_group_path(@group), notice: 'User was successfully removed from group.' } + format.js { render nothing: true } + end + end + + protected + + def group + @group ||= Group.find_by_path(params[:group_id]) + end + + def authorize_admin_group! + unless can?(current_user, :manage_group, group) + return render_404 + end + end +end diff --git a/app/controllers/wikis_controller.rb b/app/controllers/wikis_controller.rb deleted file mode 100644 index 69280291003..00000000000 --- a/app/controllers/wikis_controller.rb +++ /dev/null @@ -1,59 +0,0 @@ -class WikisController < ProjectResourceController - before_filter :authorize_read_wiki! - before_filter :authorize_write_wiki!, only: [:edit, :create, :history] - before_filter :authorize_admin_wiki!, only: :destroy - - def pages - @wiki_pages = @project.wikis.group(:slug).ordered - end - - def show - @most_recent_wiki = @project.wikis.where(slug: params[:id]).ordered.first - if params[:version_id] - @wiki = @project.wikis.find(params[:version_id]) - else - @wiki = @most_recent_wiki - end - - if @wiki - render 'show' - else - if can?(current_user, :write_wiki, @project) - @wiki = @project.wikis.new(slug: params[:id]) - render 'edit' - else - render 'empty' - end - end - end - - def edit - @wiki = @project.wikis.where(slug: params[:id]).ordered.first - @wiki = Wiki.regenerate_from @wiki - end - - def create - @wiki = @project.wikis.new(params[:wiki]) - @wiki.user = current_user - - respond_to do |format| - if @wiki.save - format.html { redirect_to [@project, @wiki], notice: 'Wiki was successfully updated.' } - else - format.html { render action: "edit" } - end - end - end - - def history - @wiki_pages = @project.wikis.where(slug: params[:id]).ordered - end - - def destroy - @wikis = @project.wikis.where(slug: params[:id]).delete_all - - respond_to do |format| - format.html { redirect_to project_wiki_path(@project, :index), notice: "Page was successfully deleted" } - end - end -end diff --git a/app/decorators/application_decorator.rb b/app/decorators/application_decorator.rb deleted file mode 100644 index 3023699e700..00000000000 --- a/app/decorators/application_decorator.rb +++ /dev/null @@ -1,28 +0,0 @@ -class ApplicationDecorator < Draper::Base - # Lazy Helpers - # PRO: Call Rails helpers without the h. proxy - # ex: number_to_currency(model.price) - # CON: Add a bazillion methods into your decorator's namespace - # and probably sacrifice performance/memory - # - # Enable them by uncommenting this line: - # lazy_helpers - - # Shared Decorations - # Consider defining shared methods common to all your models. - # - # Example: standardize the formatting of timestamps - # - # def formatted_timestamp(time) - # h.content_tag :span, time.strftime("%a %m/%d/%y"), - # class: 'timestamp' - # end - # - # def created_at - # formatted_timestamp(model.created_at) - # end - # - # def updated_at - # formatted_timestamp(model.updated_at) - # end -end diff --git a/app/decorators/commit_decorator.rb b/app/decorators/commit_decorator.rb deleted file mode 100644 index a066b2e4a26..00000000000 --- a/app/decorators/commit_decorator.rb +++ /dev/null @@ -1,92 +0,0 @@ -class CommitDecorator < ApplicationDecorator - decorates :commit - - # Returns a string describing the commit for use in a link title - # - # Example - # - # "Commit: Alex Denisov - Project git clone panel" - def link_title - "Commit: #{author_name} - #{title}" - end - - # Returns the commits title. - # - # Usually, the commit title is the first line of the commit message. - # In case this first line is longer than 80 characters, it is cut off - # after 70 characters and ellipses (`&hellp;`) are appended. - def title - title = safe_message - - return no_commit_message if title.blank? - - title_end = title.index(/\n/) - if (!title_end && title.length > 80) || (title_end && title_end > 80) - title[0..69] << "…".html_safe - else - title.split(/\n/, 2).first - end - end - - # Returns the commits description - # - # cut off, ellipses (`&hellp;`) are prepended to the commit message. - def description - description = safe_message - - title_end = description.index(/\n/) - if (!title_end && description.length > 80) || (title_end && title_end > 80) - "…".html_safe << description[70..-1] - else - description.split(/\n/, 2)[1].try(:chomp) - end - end - - # Returns a link to the commit author. If the author has a matching user and - # is a member of the current @project it will link to the team member page. - # Otherwise it will link to the author email as specified in the commit. - # - # options: - # avatar: true will prepend the avatar image - # size: size of the avatar image in px - def author_link(options = {}) - person_link(options.merge source: :author) - end - - # Just like #author_link but for the committer. - def committer_link(options = {}) - person_link(options.merge source: :committer) - end - - protected - - def no_commit_message - "--no commit message" - end - - # Private: Returns a link to a person. If the person has a matching user and - # is a member of the current @project it will link to the team member page. - # Otherwise it will link to the person email as specified in the commit. - # - # options: - # source: one of :author or :committer - # avatar: true will prepend the avatar image - # size: size of the avatar image in px - def person_link(options = {}) - source_name = send "#{options[:source]}_name".to_sym - source_email = send "#{options[:source]}_email".to_sym - text = if options[:avatar] - avatar = h.image_tag h.gravatar_icon(source_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "" - %Q{#{avatar} <span class="commit-#{options[:source]}-name">#{source_name}</span>} - else - source_name - end - team_member = @project.try(:team_member_by_name_or_email, source_name, source_email) - - if team_member.nil? - h.mail_to source_email, text.html_safe, class: "commit-#{options[:source]}-link" - else - h.link_to text, h.project_team_member_path(@project, team_member), class: "commit-#{options[:source]}-link" - end - end -end diff --git a/app/decorators/event_decorator.rb b/app/decorators/event_decorator.rb deleted file mode 100644 index 1b0ad0da28f..00000000000 --- a/app/decorators/event_decorator.rb +++ /dev/null @@ -1,44 +0,0 @@ -class EventDecorator < ApplicationDecorator - decorates :event - - def feed_title - if self.issue? - "#{self.author_name} #{self.action_name} issue ##{self.target_id}: #{self.issue_title} at #{self.project.name}" - elsif self.merge_request? - "#{self.author_name} #{self.action_name} MR ##{self.target_id}: #{self.merge_request_title} at #{self.project.name}" - elsif self.push? - "#{self.author_name} #{self.push_action_name} #{self.ref_type} #{self.ref_name} at #{self.project.name}" - elsif self.membership_changed? - "#{self.author_name} #{self.action_name} #{self.project.name}" - else - "" - end - end - - def feed_url - if self.issue? - h.project_issue_url(self.project, self.issue) - elsif self.merge_request? - h.project_merge_request_url(self.project, self.merge_request) - - elsif self.push? - if self.push_with_commits? - if self.commits_count > 1 - h.project_compare_url(self.project, :from => self.parent_commit.id, :to => self.last_commit.id) - else - h.project_commit_url(self.project, :id => self.last_commit.id) - end - else - h.project_commits_url(self.project, self.ref_name) - end - end - end - - def feed_summary - if self.issue? - h.render "events/event_issue", issue: self.issue - elsif self.push? - h.render "events/event_push", event: self - end - end -end diff --git a/app/decorators/tree_decorator.rb b/app/decorators/tree_decorator.rb deleted file mode 100644 index 0e760f97dee..00000000000 --- a/app/decorators/tree_decorator.rb +++ /dev/null @@ -1,33 +0,0 @@ -class TreeDecorator < ApplicationDecorator - decorates :tree - - def breadcrumbs(max_links = 2) - if path - part_path = "" - parts = path.split("\/") - - yield('..', nil) if parts.count > max_links - - parts.each do |part| - part_path = File.join(part_path, part) unless part_path.empty? - part_path = part if part_path.empty? - - next unless parts.last(2).include?(part) if parts.count > max_links - yield(part, h.tree_join(ref, part_path)) - end - end - end - - def up_dir? - path.present? - end - - def up_dir_path - file = File.join(path, "..") - h.tree_join(ref, file) - end - - def readme - @readme ||= contents.find { |c| c.is_a?(Grit::Blob) and c.name =~ /^readme/i } - end -end diff --git a/app/decorators/user_decorator.rb b/app/decorators/user_decorator.rb deleted file mode 100644 index b781f237352..00000000000 --- a/app/decorators/user_decorator.rb +++ /dev/null @@ -1,15 +0,0 @@ -class UserDecorator < ApplicationDecorator - decorates :user - - def avatar_image size = 16 - h.image_tag h.gravatar_icon(self.email, size), class: "avatar #{"s#{size}"}", width: size - end - - def tm_of(project) - project.team_member_by_id(self.id) - end - - def name_with_email - "#{name} (#{email})" - end -end diff --git a/app/helpers/admin/teams/members_helper.rb b/app/helpers/admin/teams/members_helper.rb deleted file mode 100644 index 58b9f1896c4..00000000000 --- a/app/helpers/admin/teams/members_helper.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Admin::Teams::MembersHelper - def member_since(team, member) - team.user_team_user_relationships.find_by_user_id(member).created_at - end -end diff --git a/app/helpers/admin/teams/projects_helper.rb b/app/helpers/admin/teams/projects_helper.rb deleted file mode 100644 index b97cc403337..00000000000 --- a/app/helpers/admin/teams/projects_helper.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Admin::Teams::ProjectsHelper - def assigned_since(team, project) - team.user_team_project_relationships.find_by_project_id(project).created_at - end -end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 196105f0119..7e5c10fee05 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,6 +2,24 @@ require 'digest/md5' require 'uri' module ApplicationHelper + COLOR_SCHEMES = { + 1 => 'white', + 2 => 'dark', + 3 => 'solarized-dark', + 4 => 'monokai', + } + COLOR_SCHEMES.default = 'white' + + # Helper method to access the COLOR_SCHEMES + # + # The keys are the `color_scheme_ids` + # The values are the `name` of the scheme. + # + # The preview images are `name-scheme-preview.png` + # The stylesheets should use the css class `.name` + def color_schemes + COLOR_SCHEMES.freeze + end # Check if a particular controller is the current one # @@ -17,7 +35,7 @@ module ApplicationHelper args.any? { |v| v.to_s.downcase == controller.controller_name } end - # Check if a partcular action is the current one + # Check if a particular action is the current one # # args - One or more action names to check # @@ -37,7 +55,7 @@ module ApplicationHelper if !Gitlab.config.gravatar.enabled || user_email.blank? 'no_avatar.png' else - gravatar_url = request.ssl? ? Gitlab.config.gravatar.ssl_url : Gitlab.config.gravatar.plain_url + gravatar_url = request.ssl? || gitlab_config.https ? Gitlab.config.gravatar.ssl_url : Gitlab.config.gravatar.plain_url user_email.strip! sprintf gravatar_url, hash: Digest::MD5.hexdigest(user_email.downcase), size: size end @@ -72,13 +90,14 @@ module ApplicationHelper end def search_autocomplete_source - projects = current_user.authorized_projects.map { |p| { label: "project: #{p.name_with_namespace}", url: project_path(p) } } - groups = current_user.authorized_groups.map { |group| { label: "group: #{group.name}", url: group_path(group) } } - teams = current_user.authorized_teams.map { |team| { label: "team: #{team.name}", url: team_path(team) } } + return unless current_user + + projects = current_user.authorized_projects.map { |p| { label: "project: #{simple_sanitize(p.name_with_namespace)}", url: project_path(p) } } + groups = current_user.authorized_groups.map { |group| { label: "group: #{simple_sanitize(group.name)}", url: group_path(group) } } default_nav = [ { label: "My Profile", url: profile_path }, - { label: "My SSH Keys", url: keys_path }, + { label: "My SSH Keys", url: profile_keys_path }, { label: "My Dashboard", url: root_path }, { label: "Admin Section", url: admin_root_path }, ] @@ -96,17 +115,19 @@ module ApplicationHelper ] project_nav = [] - if @project && @project.repository && @project.repository.root_ref + if @project && @project.repository.exists? && @project.repository.root_ref project_nav = [ - { label: "#{@project.name_with_namespace} - Issues", url: project_issues_path(@project) }, - { label: "#{@project.name_with_namespace} - Commits", url: project_commits_path(@project, @ref || @project.repository.root_ref) }, - { label: "#{@project.name_with_namespace} - Merge Requests", url: project_merge_requests_path(@project) }, - { label: "#{@project.name_with_namespace} - Milestones", url: project_milestones_path(@project) }, - { label: "#{@project.name_with_namespace} - Snippets", url: project_snippets_path(@project) }, - { label: "#{@project.name_with_namespace} - Team", url: project_team_index_path(@project) }, - { label: "#{@project.name_with_namespace} - Tree", url: project_tree_path(@project, @ref || @project.repository.root_ref) }, - { label: "#{@project.name_with_namespace} - Wall", url: wall_project_path(@project) }, - { label: "#{@project.name_with_namespace} - Wiki", url: project_wikis_path(@project) }, + { label: "#{simple_sanitize(@project.name_with_namespace)} - Files", url: project_tree_path(@project, @ref || @project.repository.root_ref) }, + { label: "#{simple_sanitize(@project.name_with_namespace)} - Commits", url: project_commits_path(@project, @ref || @project.repository.root_ref) }, + { label: "#{simple_sanitize(@project.name_with_namespace)} - Network", url: project_network_path(@project, @ref || @project.repository.root_ref) }, + { label: "#{simple_sanitize(@project.name_with_namespace)} - Graph", url: project_graph_path(@project, @ref || @project.repository.root_ref) }, + { label: "#{simple_sanitize(@project.name_with_namespace)} - Issues", url: project_issues_path(@project) }, + { label: "#{simple_sanitize(@project.name_with_namespace)} - Merge Requests", url: project_merge_requests_path(@project) }, + { label: "#{simple_sanitize(@project.name_with_namespace)} - Milestones", url: project_milestones_path(@project) }, + { label: "#{simple_sanitize(@project.name_with_namespace)} - Snippets", url: project_snippets_path(@project) }, + { label: "#{simple_sanitize(@project.name_with_namespace)} - Team", url: project_team_index_path(@project) }, + { label: "#{simple_sanitize(@project.name_with_namespace)} - Wall", url: project_wall_path(@project) }, + { label: "#{simple_sanitize(@project.name_with_namespace)} - Wiki", url: project_wikis_path(@project) }, ] end @@ -119,25 +140,29 @@ module ApplicationHelper Emoji.names.to_s end - def ldap_enable? - Devise.omniauth_providers.include?(:ldap) - end - def app_theme Gitlab::Theme.css_class_by_id(current_user.try(:theme_id)) end def user_color_scheme_class - current_user.dark_scheme ? :black : :white + COLOR_SCHEMES[current_user.try(:color_scheme_id)] if defined?(current_user) end + # Define whenever show last push event + # with suggestion to create MR def show_last_push_widget?(event) - event && - event.last_push_to_non_root? && - !event.rm_ref? && - event.project && - event.project.repository && - event.project.merge_requests_enabled + # Skip if event is not about added or modified non-master branch + return false unless event && event.last_push_to_non_root? && !event.rm_ref? + + project = event.project + + # Skip if project repo is empty or MR disabled + return false unless project && !project.empty_repo? && project.merge_requests_enabled + + # Skip if user already created appropriate MR + return false if project.merge_requests.where(source_branch: event.branch_name).opened.any? + + true end def hexdigest(string) @@ -145,9 +170,8 @@ module ApplicationHelper end def project_last_activity project - activity = project.last_activity - if activity && activity.created_at - time_ago_in_words(activity.created_at) + " ago" + if project.last_activity_at + time_ago_in_words(project.last_activity_at) + " ago" else "Never" end @@ -159,8 +183,65 @@ module ApplicationHelper alt: "Sign in with #{provider.to_s.titleize}") end + def simple_sanitize str + sanitize(str, tags: %w(a span)) + end + def image_url(source) - root_url + path_to_image(source) + # prevent relative_root_path being added twice (it's part of root_url and path_to_image) + root_url.sub(/#{root_path}$/, path_to_image(source)) end + alias_method :url_to_image, :image_url + + def users_select_tag(id, opts = {}) + css_class = "ajax-users-select " + css_class << "multiselect " if opts[:multiple] + css_class << (opts[:class] || '') + value = opts[:selected] || '' + + hidden_field_tag(id, value, class: css_class) + end + + def body_data_page + path = controller.controller_path.split('/') + namespace = path.first if path.second + + [namespace, controller.controller_name, controller.action_name].compact.join(":") + end + + # shortcut for gitlab config + def gitlab_config + Gitlab.config.gitlab + end + + # shortcut for gitlab extra config + def extra_config + Gitlab.config.extra + end + + def public_icon + content_tag :i, nil, class: 'icon-globe cblue' + end + + def private_icon + content_tag :i, nil, class: 'icon-lock cgreen' + end + + def search_placeholder + if @project && @project.persisted? + "Search in this project" + elsif @group && @group.persisted? + "Search in this group" + else + "Search" + end + end + + def first_line(str) + lines = str.split("\n") + line = lines.first + line += "..." if lines.size > 1 + line + end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 6d2ce2feea3..f8f84ff8b62 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -1,58 +1,47 @@ module CommitsHelper - def identification_type(line) - if line[0] == "+" - "new" - elsif line[0] == "-" - "old" - else - nil - end + # Returns a link to the commit author. If the author has a matching user and + # is a member of the current @project it will link to the team member page. + # Otherwise it will link to the author email as specified in the commit. + # + # options: + # avatar: true will prepend the avatar image + # size: size of the avatar image in px + def commit_author_link(commit, options = {}) + commit_person_link(commit, options.merge(source: :author)) end - def build_line_anchor(diff, line_new, line_old) - "#{hexdigest(diff.new_path)}_#{line_old}_#{line_new}" + # Just like #author_link but for the committer. + def commit_committer_link(commit, options = {}) + commit_person_link(commit, options.merge(source: :committer)) end def each_diff_line(diff, index) - diff_arr = diff.diff.lines.to_a - - line_old = 1 - line_new = 1 - type = nil - - lines_arr = ::Gitlab::InlineDiff.processing diff_arr - lines_arr.each do |line| - next if line.match(/^\-\-\- \/dev\/null/) - next if line.match(/^\+\+\+ \/dev\/null/) - next if line.match(/^\-\-\- a/) - next if line.match(/^\+\+\+ b/) - - full_line = html_escape(line.gsub(/\n/, '')) - full_line = ::Gitlab::InlineDiff.replace_markers full_line - - if line.match(/^@@ -/) - type = "match" - - line_old = line.match(/\-[0-9]*/)[0].to_i.abs rescue 0 - line_new = line.match(/\+[0-9]*/)[0].to_i.abs rescue 0 + Gitlab::DiffParser.new(diff).each do |full_line, type, line_code, line_new, line_old| + yield(full_line, type, line_code, line_new, line_old) + end + end - next if line_old == 1 && line_new == 1 #top of file - yield(full_line, type, nil, nil, nil) - next - else - type = identification_type(line) - line_code = build_line_anchor(diff, line_new, line_old) - yield(full_line, type, line_code, line_new, line_old) - end + def each_diff_line_near(diff, index, expected_line_code) + max_number_of_lines = 16 + prev_match_line = nil + prev_lines = [] - if line[0] == "+" - line_new += 1 - elsif line[0] == "-" - line_old += 1 + each_diff_line(diff, index) do |full_line, type, line_code, line_new, line_old| + line = [full_line, type, line_code, line_new, line_old] + if line_code != expected_line_code + if type == "match" + prev_lines.clear + prev_match_line = line + else + prev_lines.push(line) + prev_lines.shift if prev_lines.length >= max_number_of_lines + end else - line_new += 1 - line_old += 1 + yield(prev_match_line) if !prev_match_line.nil? + prev_lines.each { |ln| yield(ln) } + yield(line) + break end end end @@ -67,10 +56,9 @@ module CommitsHelper end end - def commit_to_html commit - if commit.model - escape_javascript(render 'commits/commit', commit: commit) - end + def commit_to_html(commit, project, inline = true) + template = inline ? "inline_commit" : "commit" + escape_javascript(render "projects/commits/#{template}", commit: commit, project: project) unless commit.nil? end def diff_line_content(line) @@ -80,4 +68,63 @@ module CommitsHelper line end end + + # Breadcrumb links for a Project and, if applicable, a tree path + def commits_breadcrumbs + return unless @project && @ref + + # Add the root project link and the arrow icon + crumbs = content_tag(:li) do + content_tag(:span, nil, class: 'arrow') + + link_to(@project.name, project_commits_path(@project, @ref)) + end + + if @path + parts = @path.split('/') + + parts.each_with_index do |part, i| + crumbs += content_tag(:span, ' / ', class: 'divider') + crumbs += content_tag(:li) do + # The text is just the individual part, but the link needs all the parts before it + link_to part, project_commits_path(@project, tree_join(@ref, parts[0..i].join('/'))) + end + end + end + + crumbs.html_safe + end + + protected + + # Private: Returns a link to a person. If the person has a matching user and + # is a member of the current @project it will link to the team member page. + # Otherwise it will link to the person email as specified in the commit. + # + # options: + # source: one of :author or :committer + # 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 + text = if options[:avatar] + avatar = image_tag(gravatar_icon(source_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "") + %Q{#{avatar} <span class="commit-#{options[:source]}-name">#{source_name}</span>} + else + source_name + end + + user = User.where('name like ? or email like ?', source_name, source_email).first + + options = { + class: "commit-#{options[:source]}-link has_tooltip", + data: { :'original-title' => sanitize(source_email) } + } + + if user.nil? + mail_to(source_email, text.html_safe, options) + else + link_to(text.html_safe, user_path(user), options) + end + end end diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb new file mode 100644 index 00000000000..ea2540bf385 --- /dev/null +++ b/app/helpers/compare_helper.rb @@ -0,0 +1,13 @@ +module CompareHelper + def compare_to_mr_button? + params[:from].present? && params[:to].present? && + @repository.branch_names.include?(params[:from]) && + @repository.branch_names.include?(params[:to]) && + params[:from] != params[:to] && + !@refs_are_same + end + + def compare_mr_path + new_project_merge_request_path(@project, merge_request: {source_branch: params[:to], target_branch: params[:from]}) + end +end diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index c759dffa16e..35c7bcbd2cf 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -1,5 +1,5 @@ module DashboardHelper - def dashboard_filter_path(entity, options={}) + def filter_path(entity, options={}) exist_opts = { status: params[:status], project_id: params[:project_id], @@ -7,12 +7,9 @@ module DashboardHelper options = exist_opts.merge(options) - case entity - when 'issue' then - issues_dashboard_path(options) - when 'merge_request' - merge_requests_dashboard_path(options) - end + path = request.path + path << "?#{options.to_param}" + path end def entities_per_project project, entity @@ -27,6 +24,6 @@ module DashboardHelper items.opened end - items.where(assignee_id: current_user.id).count + items.cared(current_user).count end end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 38374e3394d..cd8761a6113 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -28,7 +28,7 @@ module EventsHelper end content_tag :div, class: "filter_icon #{inactive}" do - link_to dashboard_path, class: 'has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => tooltip do + link_to request.path, class: 'has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => tooltip do content_tag :i, nil, class: icon_for_event[key] end end @@ -42,4 +42,89 @@ module EventsHelper EventFilter.team => "icon-user", } end + + def event_feed_title(event) + if event.issue? + "#{event.author_name} #{event.action_name} issue ##{event.target_id}: #{event.issue_title} at #{event.project_name}" + elsif event.merge_request? + "#{event.author_name} #{event.action_name} MR ##{event.target_id}: #{event.merge_request_title} at #{event.project_name}" + elsif event.push? + "#{event.author_name} #{event.push_action_name} #{event.ref_type} #{event.ref_name} at #{event.project_name}" + elsif event.membership_changed? + "#{event.author_name} #{event.action_name} #{event.project_name}" + elsif event.note? + "#{event.author_name} commented on #{event.note_target_type} ##{truncate event.note_target_id} at #{event.project_name}" + else + "" + end + end + + def event_feed_url(event) + if event.issue? + project_issue_url(event.project, event.issue) + elsif event.merge_request? + project_merge_request_url(event.project, event.merge_request) + + elsif event.push? + if event.push_with_commits? + if event.commits_count > 1 + project_compare_url(event.project, from: event.commit_from, to: event.commit_to) + else + project_commit_url(event.project, id: event.commit_to) + end + else + project_commits_url(event.project, event.ref_name) + end + end + end + + def event_feed_summary(event) + if event.issue? + render "events/event_issue", issue: event.issue + elsif event.push? + render "events/event_push", event: event + end + end + + def event_note_target_path(event) + if event.note? && event.note_commit? + project_commit_path(event.project, event.note_target) + else + url_for([event.project, event.note_target]) + end + end + + def event_note_title_html(event) + if event.note_target + if event.note_commit? + link_to project_commit_path(event.project, event.note_commit_id), class: "commit_short_id" do + "#{event.note_target_type} #{event.note_short_commit_id}" + end + elsif event.note_project_snippet? + link_to(project_snippet_path(event.project, event.note_target)) do + content_tag :strong do + "#{event.note_target_type} ##{truncate event.note_target_id}" + end + end + else + link_to event_note_target_path(event) do + content_tag :strong do + "#{event.note_target_type} ##{truncate event.note_target_iid}" + end + end + end + elsif event.wall_note? + link_to 'wall', project_wall_path(event.project) + else + content_tag :strong do + "(deleted)" + end + end + end + + def event_note(text) + text = first_line(text) + text = truncate(text, length: 150) + sanitize(markdown(text), tags: %w(a img b pre p)) + end end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 1a3d34eb886..375f8861dae 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -49,4 +49,12 @@ module GitlabMarkdownHelper @markdown.render(text).html_safe end + + def render_wiki_content(wiki_page) + if wiki_page.format == :markdown + markdown(wiki_page.content) + else + wiki_page.formatted_content.html_safe + end + end end diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb new file mode 100644 index 00000000000..7cb1b6f8d1a --- /dev/null +++ b/app/helpers/graph_helper.rb @@ -0,0 +1,16 @@ +module GraphHelper + def get_refs(repo, commit) + refs = "" + refs += commit.ref_names(repo).join(" ") + + # append note count + refs += "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0 + + refs + end + + def parents_zip_spaces(parents, parent_spaces) + ids = parents.map { |p| p.id } + ids.zip(parent_spaces) + end +end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 283119bc24c..8573c59dc94 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,17 +1,5 @@ module GroupsHelper - def group_filter_path(entity, options={}) - exist_opts = { - status: params[:status], - project_id: params[:project_id], - } - - options = exist_opts.merge(options) - - case entity - when 'issue' then - issues_group_path(@group, options) - when 'merge_request' - merge_requests_group_path(@group, options) - end + def remove_user_from_group_message(group, user) + "You are going to remove #{user.name} from #{group.name} Group. Are you sure?" end end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 2825787fd2f..5977c9cbae2 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -1,43 +1,62 @@ module IssuesHelper - def project_issues_filter_path project, params = {} - params[:f] ||= cookies['issue_filter'] - project_issues_path project, params - end - def issue_css_classes issue classes = "issue" - classes << " closed" if issue.closed + classes << " closed" if issue.closed? classes << " today" if issue.today? classes end - def issue_tags - @project.issues.tag_counts_on(:labels).map(&:name) - end - # Returns an OpenStruct object suitable for use by <tt>options_from_collection_for_select</tt> # to allow filtering issues by an unassigned User or Milestone def unassigned_filter # Milestone uses :title, Issue uses :name - OpenStruct.new(id: 0, title: 'Unspecified', name: 'Unassigned') + OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned') + end + + def url_for_project_issues + return "" if @project.nil? + + if @project.used_default_issues_tracker? + project_issues_path(@project) + else + url = Gitlab.config.issues_tracker[@project.issues_tracker]["project_url"] + url.gsub(':project_id', @project.id.to_s) + .gsub(':issues_tracker_id', @project.issues_tracker_id.to_s) + end end - def issues_filter - { - all: "all", - closed: "closed", - to_me: "assigned-to-me", - open: "open" - } + def url_for_new_issue + return "" if @project.nil? + + if @project.used_default_issues_tracker? + url = new_project_issue_path project_id: @project + else + url = Gitlab.config.issues_tracker[@project.issues_tracker]["new_issue_url"] + url.gsub(':project_id', @project.id.to_s) + .gsub(':issues_tracker_id', @project.issues_tracker_id.to_s) + end end - def labels_autocomplete_source - labels = @project.issues_labels.order('count DESC') - labels = labels.map{ |l| { label: l.name, value: l.name } } - labels.to_json + def url_for_issue(issue_iid) + return "" if @project.nil? + + if @project.used_default_issues_tracker? + url = project_issue_url project_id: @project, id: issue_iid + else + url = Gitlab.config.issues_tracker[@project.issues_tracker]["issues_url"] + url.gsub(':id', issue_iid.to_s) + .gsub(':project_id', @project.id.to_s) + .gsub(':issues_tracker_id', @project.issues_tracker_id.to_s) + end end - def issues_active_milestones - @project.milestones.active.order("id desc").all + def title_for_issue(issue_iid) + return "" if @project.nil? + + if @project.used_default_issues_tracker? && issue = @project.issues.where(iid: issue_iid).first + issue.title + else + "" + end end end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb new file mode 100644 index 00000000000..9a6aea85ee9 --- /dev/null +++ b/app/helpers/labels_helper.rb @@ -0,0 +1,28 @@ +module LabelsHelper + def issue_label_names + @project.issues_labels.map(&:name) + end + + def labels_autocomplete_source + labels = @project.issues_labels + labels = labels.map{ |l| { label: l.name, value: l.name } } + labels.to_json + end + + def label_css_class(name) + klass = Gitlab::IssuesLabels + + case name + when *klass.warning_labels + 'label-warning' + when *klass.neutral_labels + 'label-inverse' + when *klass.positive_labels + 'label-success' + when *klass.important_labels + 'label-important' + else + 'label-info' + end + end +end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index ca0a89c3749..4ba48aa4339 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -2,22 +2,43 @@ module MergeRequestsHelper def new_mr_path_from_push_event(event) new_project_merge_request_path( event.project, - merge_request: { - source_branch: event.branch_name, - target_branch: event.project.repository.root_ref, - title: event.branch_name.titleize - } + new_mr_from_push_event(event, event.project) ) end + def new_mr_path_for_fork_from_push_event(event) + new_project_merge_request_path( + event.project, + new_mr_from_push_event(event, event.project.forked_from_project) + ) + end + + def new_mr_from_push_event(event, target_project) + return :merge_request => { + source_project_id: event.project.id, + target_project_id: target_project.id, + source_branch: event.branch_name, + target_branch: target_project.repository.root_ref, + title: event.branch_name.titleize + } + end + def mr_css_classes mr - classes = "merge_request" - classes << " closed" if mr.closed + classes = "merge-request" + classes << " closed" if mr.closed? classes << " merged" if mr.merged? classes end def ci_build_details_path merge_request - merge_request.project.gitlab_ci_service.build_page(merge_request.last_commit.sha) + merge_request.source_project.gitlab_ci_service.build_page(merge_request.last_commit.sha) + end + + def merge_path_description(merge_request, separator) + if merge_request.for_fork? + "Project:Branches: #{@merge_request.source_project.path_with_namespace}:#{@merge_request.source_branch} #{separator} #{@merge_request.target_project.path_with_namespace}:#{@merge_request.target_branch}" + else + "Branches: #{@merge_request.source_branch} #{separator} #{@merge_request.target_branch}" + end end end diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index 6d0c6c98191..dc88e178360 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -1,20 +1,12 @@ module NamespacesHelper def namespaces_options(selected = :current_user, scope = :default) - if current_user.admin - groups = Group.all - users = Namespace.root - else - groups = current_user.owned_groups.select {|n| n.type == 'Group'} - users = current_user.namespaces.reject {|n| n.type == 'Group'} - end - + groups = current_user.owned_groups.select {|n| n.type == 'Group'} + users = current_user.namespaces.reject {|n| n.type == 'Group'} - global_opts = ["Global", [['/', Namespace.global_id]] ] - group_opts = ["Groups", groups.map {|g| [g.human_name, g.id]} ] - users_opts = [ "Users", users.map {|u| [u.human_name, u.id]} ] + group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [g.human_name, g.id]} ] + users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [u.human_name, u.id]} ] options = [] - options << global_opts if current_user.admin options << group_opts options << users_opts diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 7a0ed251aa8..a3ec4cca59d 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -1,8 +1,7 @@ module NotesHelper # Helps to distinguish e.g. commit notes in mr notes list def note_for_main_target?(note) - note.for_wall? || - (@target_type.camelize == note.noteable_type && !note.for_diff_line?) + (@target_type.camelize == note.noteable_type && !note.for_diff_line?) end def note_target_fields @@ -29,4 +28,11 @@ module NotesHelper def loading_new_notes? params[:loading_new].present? end + + def note_timestamp(note) + # Shows the created at time and the updated at time if different + ts = "#{time_ago_in_words(note.created_at)} ago" + ts << content_tag(:small, " (Edited #{time_ago_in_words(note.updated_at)} ago)") if note.updated_at != note.created_at + ts.html_safe + end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb new file mode 100644 index 00000000000..ae3402b2617 --- /dev/null +++ b/app/helpers/notifications_helper.rb @@ -0,0 +1,13 @@ +module NotificationsHelper + def notification_icon(notification) + if notification.disabled? + content_tag :i, nil, class: 'icon-circle cred' + elsif notification.participating? + content_tag :i, nil, class: 'icon-circle cblue' + elsif notification.watch? + content_tag :i, nil, class: 'icon-circle cgreen' + else + content_tag :i, nil, class: 'icon-circle-blank cblue' + end + end +end diff --git a/app/helpers/oauth_helper.rb b/app/helpers/oauth_helper.rb new file mode 100644 index 00000000000..c0177dacbf8 --- /dev/null +++ b/app/helpers/oauth_helper.rb @@ -0,0 +1,19 @@ +module OauthHelper + def ldap_enabled? + Devise.omniauth_providers.include?(:ldap) + end + + def default_providers + [:twitter, :github, :google_oauth2, :ldap] + end + + def enabled_oauth_providers + Devise.omniauth_providers + end + + def enabled_social_providers + enabled_oauth_providers.select do |name| + [:twitter, :github, :google_oauth2].include?(name.to_sym) + end + end +end diff --git a/app/helpers/profile_helper.rb b/app/helpers/profile_helper.rb index 80d67009f59..88d9f184d0e 100644 --- a/app/helpers/profile_helper.rb +++ b/app/helpers/profile_helper.rb @@ -4,4 +4,16 @@ module ProfileHelper 'active' end end + + def show_profile_username_tab? + current_user.can_change_username? + end + + def show_profile_social_tab? + Gitlab.config.omniauth.enabled && !current_user.ldap_user? + end + + def show_profile_remove_tab? + Gitlab.config.gitlab.signup_enabled && !current_user.ldap_user? + end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 05303e86ae8..9071c688df1 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -1,12 +1,4 @@ module ProjectsHelper - def grouper_project_members(project) - @project.users_projects.sort_by(&:project_access).reverse.group_by(&:project_access) - end - - def grouper_project_teams(project) - @project.user_team_project_relationships.sort_by(&:greatest_access).reverse.group_by(&:greatest_access) - end - def remove_from_project_team_message(project, user) "You are going to remove #{user.name} from #{project.name} project team. Are you sure?" end @@ -25,7 +17,7 @@ module ProjectsHelper end def link_to_member(project, author, opts = {}) - default_opts = { avatar: true } + default_opts = { avatar: true, name: true, size: 16 } opts = default_opts.merge(opts) return "(deleted)" unless author @@ -33,33 +25,98 @@ module ProjectsHelper author_html = "" # Build avatar image tag - author_html << image_tag(gravatar_icon(author.try(:email)), width: 16, class: "lil_av") if opts[:avatar] + author_html << image_tag(gravatar_icon(author.try(:email), opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar] # Build name span tag - author_html << content_tag(:span, sanitize(author.name), class: 'author') + author_html << content_tag(:span, sanitize(author.name), class: 'author') if opts[:name] author_html = author_html.html_safe - tm = project.team_member_by_id(author) - - if tm - link_to author_html, project_team_member_path(project, tm.user_username), class: "author_link" + if opts[:name] + link_to(author_html, user_path(author), class: "author_link").html_safe else - author_html - end.html_safe - end - - def tm_path team_member - project_team_member_path(@project, team_member) + link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => sanitize(author.name) } ).html_safe + end end def project_title project if project.group content_tag :span do - link_to(project.group.name, group_path(project.group)) + " / " + project.name + link_to(simple_sanitize(project.group.name), group_path(project.group)) + " / " + project.name end else project.name end end + + def remove_project_message(project) + "You are going to remove #{project.name_with_namespace}.\n Removed project CANNOT be restored!\n Are you ABSOLUTELY sure?" + end + + def project_nav_tabs + @nav_tabs ||= get_project_nav_tabs(@project, current_user) + end + + def project_nav_tab?(name) + project_nav_tabs.include? name + end + + def project_filter_path(options={}) + exist_opts = { + state: params[:state], + scope: params[:scope], + label_name: params[:label_name], + milestone_id: params[:milestone_id], + } + + options = exist_opts.merge(options) + + path = request.path + path << "?#{options.to_param}" + path + end + + def project_active_milestones + @project.milestones.active.order("id desc").all + end + + private + + def get_project_nav_tabs(project, current_user) + nav_tabs = [:home] + + if !project.empty_repo? && can?(current_user, :download_code, project) + nav_tabs << [:files, :commits, :network, :graphs] + end + + if project.repo_exists? && project.merge_requests_enabled + nav_tabs << :merge_requests + end + + if can?(current_user, :admin_project, project) + nav_tabs << :settings + end + + [:issues, :wiki, :wall, :snippets].each do |feature| + nav_tabs << feature if project.send :"#{feature}_enabled" + end + + nav_tabs.flatten + end + + def git_user_name + if current_user + current_user.name + else + "Your name" + end + end + + def git_user_email + if current_user + current_user.email + else + "your@email.com" + end + end end diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 6a91d616722..b0abc2cae33 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -8,4 +8,12 @@ module SnippetsHelper ] options_for_select(options) end + + def reliable_snippet_path(snippet) + if snippet.project_id? + project_snippet_path(snippet.project, snippet) + else + snippet_path(snippet) + end + end end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 5bd6de896ee..ce675872264 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -73,18 +73,16 @@ module TabHelper end def project_tab_class - [:show, :files, :edit, :update].each do |action| - return "active" if current_page?(controller: "projects", action: action, id: @project) - end + return "active" if current_page?(controller: "/projects", action: :edit, id: @project) - if ['snippets', 'services', 'hooks', 'deploy_keys', 'team_members'].include? controller.controller_name + if ['services', 'hooks', 'deploy_keys', 'team_members'].include? controller.controller_name "active" end end def branches_tab_class - if current_page?(branches_project_repository_path(@project)) || - current_controller?(:protected_branches) || + if current_controller?(:protected_branches) || + current_controller?(:branches) || current_page?(project_repository_path(@project)) 'active' end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 0f2b695e0ad..73d36d0801c 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -3,24 +3,20 @@ module TreeHelper # their corresponding partials # # contents - A Grit::Tree object for the current tree - def render_tree(contents) + def render_tree(tree) # Render Folders before Files/Submodules - folders, files = contents.partition { |v| v.kind_of?(Grit::Tree) } + folders, files, submodules = tree.trees, tree.blobs, tree.submodules tree = "" # Render folders if we have any - tree += render partial: 'tree/tree_item', collection: folders, locals: {type: 'folder'} if folders.present? - - files.each do |f| - if f.respond_to?(:url) - # Object is a Submodule - tree += render partial: 'tree/submodule_item', object: f - else - # Object is a Blob - tree += render partial: 'tree/tree_item', object: f, locals: {type: 'file'} - end - end + tree += render partial: 'projects/tree/tree_item', collection: folders, locals: {type: 'folder'} if folders.present? + + # Render files if we have any + tree += render partial: 'projects/tree/blob_item', collection: files, locals: {type: 'file'} if files.present? + + # Render submodules if we have any + tree += render partial: 'projects/tree/submodule_item', collection: submodules if submodules.present? tree.html_safe end @@ -43,16 +39,16 @@ module TreeHelper # # Returns boolean def markup?(filename) - filename.end_with?(*%w(.textile .rdoc .org .creole - .mediawiki .rst .asciidoc .pod)) + filename.downcase.end_with?(*%w(.textile .rdoc .org .creole + .mediawiki .rst .asciidoc .pod)) end def gitlab_markdown?(filename) - filename.end_with?(*%w(.mdown .md .markdown)) + filename.downcase.end_with?(*%w(.mdown .md .markdown)) end def plain_text_readme? filename - filename == 'README' + filename =~ /^README(.txt)?$/i end # Simple shortcut to File.join @@ -61,6 +57,8 @@ module TreeHelper end def allowed_tree_edit? + return false unless @repository.branch_names.include?(@ref) + if @project.protected_branch? @ref can?(current_user, :push_code_to_protected_branches, @project) else @@ -68,28 +66,29 @@ module TreeHelper end end - # Breadcrumb links for a Project and, if applicable, a tree path - def breadcrumbs - return unless @project && @ref + def tree_breadcrumbs(tree, max_links = 2) + if tree.path + part_path = "" + parts = tree.path.split("\/") - # Add the root project link and the arrow icon - crumbs = content_tag(:li) do - content_tag(:span, nil, class: 'arrow') + - link_to(@project.name, project_commits_path(@project, @ref)) - end + yield('..', nil) if parts.count > max_links - if @path - parts = @path.split('/') + parts.each do |part| + part_path = File.join(part_path, part) unless part_path.empty? + part_path = part if part_path.empty? - parts.each_with_index do |part, i| - crumbs += content_tag(:span, '/', class: 'divider') - crumbs += content_tag(:li) do - # The text is just the individual part, but the link needs all the parts before it - link_to part, project_commits_path(@project, tree_join(@ref, parts[0..i].join('/'))) - end + next unless parts.last(2).include?(part) if parts.count > max_links + yield(part, tree_join(tree.ref, part_path)) end end + end + + def up_dir_path tree + file = File.join(tree.path, "..") + tree_join(tree.ref, file) + end - crumbs.html_safe + def leave_edit_message + "Leave edit mode?\nAll unsaved changes will be lost." end end diff --git a/app/helpers/user_teams_helper.rb b/app/helpers/user_teams_helper.rb deleted file mode 100644 index 2055bb3c8bc..00000000000 --- a/app/helpers/user_teams_helper.rb +++ /dev/null @@ -1,26 +0,0 @@ -module UserTeamsHelper - def team_filter_path(entity, options={}) - exist_opts = { - status: params[:status], - project_id: params[:project_id], - } - - options = exist_opts.merge(options) - - case entity - when 'issue' then - issues_team_path(@team, options) - when 'merge_request' - merge_requests_team_path(@team, options) - end - end - - def grouped_user_team_members(team) - team.user_team_user_relationships.sort_by(&:permission).reverse.group_by(&:permission) - end - - def remove_from_user_team_message(team, member) - "You are going to remove #{member.name} from #{team.name}. Are you sure?" - end - -end diff --git a/app/mailers/emails/groups.rb b/app/mailers/emails/groups.rb new file mode 100644 index 00000000000..2e9d28981e3 --- /dev/null +++ b/app/mailers/emails/groups.rb @@ -0,0 +1,11 @@ +module Emails + module Groups + def group_access_granted_email(user_group_id) + @membership = UsersGroup.find(user_group_id) + @group = @membership.group + + mail(to: @membership.user.email, + subject: subject("access to group was granted")) + end + end +end diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb new file mode 100644 index 00000000000..6eda88c7921 --- /dev/null +++ b/app/mailers/emails/issues.rb @@ -0,0 +1,33 @@ +module Emails + module Issues + def new_issue_email(recipient_id, issue_id) + @issue = Issue.find(issue_id) + @project = @issue.project + mail(to: recipient(recipient_id), subject: subject("new issue ##{@issue.iid}", @issue.title)) + end + + def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id) + @issue = Issue.find(issue_id) + @previous_assignee = User.find_by_id(previous_assignee_id) if previous_assignee_id + @project = @issue.project + mail(to: recipient(recipient_id), subject: subject("changed issue ##{@issue.iid}", @issue.title)) + end + + def closed_issue_email(recipient_id, issue_id, updated_by_user_id) + @issue = Issue.find issue_id + @project = @issue.project + @updated_by = User.find updated_by_user_id + mail(to: recipient(recipient_id), + subject: subject("Closed issue ##{@issue.iid}", @issue.title)) + end + + def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id) + @issue = Issue.find issue_id + @issue_status = status + @project = @issue.project + @updated_by = User.find updated_by_user_id + mail(to: recipient(recipient_id), + subject: subject("changed issue ##{@issue.iid}", @issue.title)) + end + end +end diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb new file mode 100644 index 00000000000..57c1925fa47 --- /dev/null +++ b/app/mailers/emails/merge_requests.rb @@ -0,0 +1,66 @@ +module Emails + module MergeRequests + def new_merge_request_email(recipient_id, merge_request_id) + @merge_request = MergeRequest.find(merge_request_id) + mail(to: recipient(recipient_id), subject: subject("new merge request !#{@merge_request.iid}", @merge_request.title)) + end + + def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id) + @merge_request = MergeRequest.find(merge_request_id) + @previous_assignee = User.find_by_id(previous_assignee_id) if previous_assignee_id + mail(to: recipient(recipient_id), subject: subject("changed merge request !#{@merge_request.iid}", @merge_request.title)) + end + + def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) + @merge_request = MergeRequest.find(merge_request_id) + @updated_by = User.find updated_by_user_id + mail(to: recipient(recipient_id), subject: subject("Closed merge request !#{@merge_request.iid}", @merge_request.title)) + end + + def merged_merge_request_email(recipient_id, merge_request_id) + @merge_request = MergeRequest.find(merge_request_id) + mail(to: recipient(recipient_id), subject: subject("Accepted merge request !#{@merge_request.iid}", @merge_request.title)) + end + end + + # Over rides default behavour to show source/target + # Formats arguments into a String suitable for use as an email subject + # + # extra - Extra Strings to be inserted into the subject + # + # Examples + # + # >> subject('Lorem ipsum') + # => "GitLab Merge Request | Lorem ipsum" + # + # # Automatically inserts Project name: + # Forked MR + # => source project => <Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...> + # => target project => <Project id: 2, name: "My Ror", path: "ruby_on_rails", ...> + # => source branch => source + # => target branch => target + # >> subject('Lorem ipsum') + # => "GitLab Merge Request | Ruby on Rails:source >> My Ror:target | Lorem ipsum " + # + # Non Forked MR + # => source project => <Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...> + # => target project => <Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...> + # => source branch => source + # => target branch => target + # >> subject('Lorem ipsum') + # => "GitLab Merge Request | Ruby on Rails | source >> target | Lorem ipsum " + # # Accepts multiple arguments + # >> subject('Lorem ipsum', 'Dolor sit amet') + # => "GitLab Merge Request | Lorem ipsum | Dolor sit amet" + def subject(*extra) + subject = "GitLab Merge Request |" + if @merge_request.for_fork? + subject << "#{@merge_request.source_project.name_with_namespace}:#{merge_request.source_branch} >> #{@merge_request.target_project.name_with_namespace}:#{merge_request.target_branch}" + else + subject << "#{@merge_request.source_project.name_with_namespace} | #{merge_request.source_branch} >> #{merge_request.target_branch}" + end + subject << " | " + extra.join(' | ') if extra.present? + subject + end + +end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb new file mode 100644 index 00000000000..761b4c8161f --- /dev/null +++ b/app/mailers/emails/notes.rb @@ -0,0 +1,30 @@ +module Emails + module Notes + def note_commit_email(recipient_id, note_id) + @note = Note.find(note_id) + @commit = @note.noteable + @project = @note.project + mail(to: recipient(recipient_id), subject: subject("note for commit #{@commit.short_id}", @commit.title)) + end + + def note_issue_email(recipient_id, note_id) + @note = Note.find(note_id) + @issue = @note.noteable + @project = @note.project + mail(to: recipient(recipient_id), subject: subject("note for issue ##{@issue.iid}")) + end + + def note_merge_request_email(recipient_id, note_id) + @note = Note.find(note_id) + @merge_request = @note.noteable + @project = @note.project + mail(to: recipient(recipient_id), subject: subject("note for merge request !#{@merge_request.iid}")) + end + + def note_wall_email(recipient_id, note_id) + @note = Note.find(note_id) + @project = @note.project + mail(to: recipient(recipient_id), subject: subject("note on wall")) + end + end +end diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb new file mode 100644 index 00000000000..bcd44f9476c --- /dev/null +++ b/app/mailers/emails/profile.rb @@ -0,0 +1,15 @@ +module Emails + module Profile + def new_user_email(user_id, password) + @user = User.find(user_id) + @password = password + mail(to: @user.email, subject: subject("Account was created for you")) + end + + def new_ssh_key_email(key_id) + @key = Key.find(key_id) + @user = @key.user + mail(to: @user.email, subject: subject("SSH key was added to your account")) + end + end +end diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb new file mode 100644 index 00000000000..4d5fe9ef614 --- /dev/null +++ b/app/mailers/emails/projects.rb @@ -0,0 +1,17 @@ +module Emails + module Projects + def project_access_granted_email(user_project_id) + @users_project = UsersProject.find user_project_id + @project = @users_project.project + mail(to: @users_project.user.email, + subject: subject("access to project was granted")) + end + + def project_was_moved_email(project_id, user_id) + @user = User.find user_id + @project = Project.find project_id + mail(to: @user.email, + subject: subject("project was moved")) + end + end +end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 08f7e01aab1..2f7be00c33e 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -1,7 +1,14 @@ class Notify < ActionMailer::Base + include Emails::Issues + include Emails::MergeRequests + include Emails::Notes + include Emails::Projects + include Emails::Profile + include Emails::Groups add_template_helper ApplicationHelper add_template_helper GitlabMarkdownHelper + add_template_helper MergeRequestsHelper default_url_options[:host] = Gitlab.config.gitlab.host default_url_options[:protocol] = Gitlab.config.gitlab.protocol @@ -15,117 +22,6 @@ class Notify < ActionMailer::Base delay_for(2.seconds) end - - # - # Issue - # - - def new_issue_email(issue_id) - @issue = Issue.find(issue_id) - @project = @issue.project - mail(to: @issue.assignee_email, subject: subject("new issue ##{@issue.id}", @issue.title)) - end - - def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id) - @issue = Issue.find(issue_id) - @previous_assignee ||= User.find(previous_assignee_id) - @project = @issue.project - mail(to: recipient(recipient_id), subject: subject("changed issue ##{@issue.id}", @issue.title)) - end - - def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id) - @issue = Issue.find issue_id - @issue_status = status - @project = @issue.project - @updated_by = User.find updated_by_user_id - mail(to: recipient(recipient_id), - subject: subject("changed issue ##{@issue.id}", @issue.title)) - end - - - - # - # Merge Request - # - - def new_merge_request_email(merge_request_id) - @merge_request = MergeRequest.find(merge_request_id) - @project = @merge_request.project - mail(to: @merge_request.assignee_email, subject: subject("new merge request !#{@merge_request.id}", @merge_request.title)) - end - - def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id) - @merge_request = MergeRequest.find(merge_request_id) - @previous_assignee ||= User.find(previous_assignee_id) - @project = @merge_request.project - mail(to: recipient(recipient_id), subject: subject("changed merge request !#{@merge_request.id}", @merge_request.title)) - end - - - - # - # Note - # - - def note_commit_email(recipient_id, note_id) - @note = Note.find(note_id) - @commit = @note.noteable - @commit = CommitDecorator.decorate(@commit) - @project = @note.project - mail(to: recipient(recipient_id), subject: subject("note for commit #{@commit.short_id}", @commit.title)) - end - - def note_issue_email(recipient_id, note_id) - @note = Note.find(note_id) - @issue = @note.noteable - @project = @note.project - mail(to: recipient(recipient_id), subject: subject("note for issue ##{@issue.id}")) - end - - def note_merge_request_email(recipient_id, note_id) - @note = Note.find(note_id) - @merge_request = @note.noteable - @project = @note.project - mail(to: recipient(recipient_id), subject: subject("note for merge request !#{@merge_request.id}")) - end - - def note_wall_email(recipient_id, note_id) - @note = Note.find(note_id) - @project = @note.project - mail(to: recipient(recipient_id), subject: subject("note on wall")) - end - - - # - # Project - # - - def project_access_granted_email(user_project_id) - @users_project = UsersProject.find user_project_id - @project = @users_project.project - mail(to: @users_project.user.email, - subject: subject("access to project was granted")) - end - - - def project_was_moved_email(user_project_id) - @users_project = UsersProject.find user_project_id - @project = @users_project.project - mail(to: @users_project.user.email, - subject: subject("project was moved")) - end - - # - # User - # - - def new_user_email(user_id, password) - @user = User.find(user_id) - @password = password - mail(to: @user.email, subject: subject("Account was created for you")) - end - - private # Look up a User by their ID and return their email address diff --git a/app/models/ability.rb b/app/models/ability.rb index 6d087a959a9..85476089145 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,24 +1,54 @@ class Ability class << self def allowed(user, subject) + return not_auth_abilities(user, subject) if user.nil? return [] unless user.kind_of?(User) + return [] if user.blocked? case subject.class.name when "Project" then project_abilities(user, subject) when "Issue" then issue_abilities(user, subject) when "Note" then note_abilities(user, subject) - when "Snippet" then snippet_abilities(user, subject) + when "ProjectSnippet" then project_snippet_abilities(user, subject) + when "PersonalSnippet" then personal_snippet_abilities(user, subject) when "MergeRequest" then merge_request_abilities(user, subject) - when "Group", "Namespace" then group_abilities(user, subject) - when "UserTeam" then user_team_abilities(user, subject) + when "Group" then group_abilities(user, subject) + when "Namespace" then namespace_abilities(user, subject) else [] end.concat(global_abilities(user)) end + # List of possible abilities + # for non-authenticated user + def not_auth_abilities(user, subject) + project = if subject.kind_of?(Project) + subject + elsif subject.respond_to?(:project) + subject.project + else + nil + end + + if project && project.public + [ + :read_project, + :read_wiki, + :read_issue, + :read_milestone, + :read_project_snippet, + :read_team_member, + :read_merge_request, + :read_note, + :download_code + ] + else + [] + end + end + def global_abilities(user) rules = [] rules << :create_group if user.can_create_group - rules << :create_team if user.can_create_team rules end @@ -41,20 +71,35 @@ class Ability rules << project_guest_rules end - if project.owner == user + if project.public? + rules << public_project_rules + end + + if project.owner == user || user.admin? + rules << project_admin_rules + end + + if project.group && project.group.has_owner?(user) rules << project_admin_rules end rules.flatten end + def public_project_rules + project_guest_rules + [ + :download_code, + :fork_project, + ] + end + def project_guest_rules [ :read_project, :read_wiki, :read_issue, :read_milestone, - :read_snippet, + :read_project_snippet, :read_team_member, :read_merge_request, :read_note, @@ -67,7 +112,8 @@ class Ability def project_report_rules project_guest_rules + [ :download_code, - :write_snippet + :fork_project, + :write_project_snippet ] end @@ -83,15 +129,14 @@ class Ability project_dev_rules + [ :push_code_to_protected_branches, :modify_issue, - :modify_snippet, + :modify_project_snippet, :modify_merge_request, :admin_issue, :admin_milestone, - :admin_snippet, + :admin_project_snippet, :admin_team_member, :admin_merge_request, :admin_note, - :accept_mr, :admin_wiki, :admin_project ] @@ -109,8 +154,12 @@ class Ability def group_abilities user, group rules = [] + if group.users.include?(user) || user.admin? + rules << :read_group + end + # Only group owner and administrators can manage group - if group.owner == user || user.admin? + if group.has_owner?(user) || user.admin? rules << [ :manage_group, :manage_namespace @@ -120,23 +169,20 @@ class Ability rules.flatten end - def user_team_abilities user, team + def namespace_abilities user, namespace rules = [] - # Only group owner and administrators can manage group - if team.owner == user || team.admin?(user) || user.admin? - rules << [ :manage_user_team ] - end - - if team.owner == user || user.admin? - rules << [ :admin_user_team ] + # Only namespace owner and administrators can manage it + if namespace.owner == user || user.admin? + rules << [ + :manage_namespace + ] end rules.flatten end - - [:issue, :note, :snippet, :merge_request].each do |name| + [:issue, :note, :project_snippet, :personal_snippet, :merge_request].each do |name| define_method "#{name}_abilities" do |user, subject| if subject.author == user [ diff --git a/app/models/campfire_service.rb b/app/models/campfire_service.rb new file mode 100644 index 00000000000..fb2a49fd586 --- /dev/null +++ b/app/models/campfire_service.rb @@ -0,0 +1,78 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# token :string(255) +# project_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean default(FALSE), not null +# project_url :string(255) +# subdomain :string(255) +# room :string(255) +# + +class CampfireService < Service + attr_accessible :subdomain, :room + + validates :token, presence: true, if: :activated? + + def title + 'Campfire' + end + + def description + 'Simple web-based real-time group chat' + end + + def to_param + 'campfire' + end + + def fields + [ + { type: 'text', name: 'token', placeholder: '' }, + { type: 'text', name: 'subdomain', placeholder: '' }, + { type: 'text', name: 'room', placeholder: '' } + ] + end + + def execute(push_data) + room = gate.find_room_by_name(self.room) + return true unless room + + message = build_message(push_data) + + room.speak(message) + end + + private + + def gate + @gate ||= Tinder::Campfire.new(subdomain, token: token) + end + + def build_message(push) + ref = push[:ref].gsub("refs/heads/", "") + before = push[:before] + after = push[:after] + + message = "" + message << "[#{project.name_with_namespace}] " + message << "#{push[:user_name]} " + + if before =~ /000000/ + message << "pushed new branch #{ref} \n" + elsif after =~ /000000/ + message << "removed branch #{ref} \n" + else + message << "pushed #{push[:total_commits_count]} commits to #{ref}. " + message << "#{project.web_url}/compare/#{before}...#{after}" + end + + message + end +end diff --git a/app/models/commit.rb b/app/models/commit.rb index 17d41f27f34..dd1f9801878 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -2,167 +2,141 @@ class Commit include ActiveModel::Conversion include StaticModel extend ActiveModel::Naming + include Mentionable - # Safe amount of files with diffs in one commit to render + attr_mentionable :safe_message + + # Safe amount of changes (files and lines) in one commit to render # Used to prevent 500 error on huge commits by suppressing diff # - DIFF_SAFE_SIZE = 100 - - attr_accessor :commit, :head, :refs - - delegate :message, :authored_date, :committed_date, :parents, :sha, - :date, :committer, :author, :diffs, :tree, :id, :stats, - :to_patch, to: :commit - - class << self - def find_or_first(repo, commit_id = nil, root_ref) - commit = if commit_id - repo.commit(commit_id) - else - repo.commits(root_ref).first - end - - Commit.new(commit) if commit - end - - def fresh_commits(repo, n = 10) - commits = repo.heads.map do |h| - repo.commits(h.name, n).map { |c| Commit.new(c, h) } - end.flatten.uniq { |c| c.id } - - commits.sort! do |x, y| - y.committed_date <=> x.committed_date - end - - commits[0...n] - end - - def commits_with_refs(repo, n = 20) - commits = repo.branches.map { |ref| Commit.new(ref.commit, ref) } - - commits.sort! do |x, y| - y.committed_date <=> x.committed_date - end - - commits[0..n] - end - - def commits_since(repo, date) - commits = repo.heads.map do |h| - repo.log(h.name, nil, since: date).each { |c| Commit.new(c, h) } - end.flatten.uniq { |c| c.id } + # User can force display of diff above this size + DIFF_SAFE_FILES = 100 + DIFF_SAFE_LINES = 5000 + # Commits above this size will not be rendered in HTML + DIFF_HARD_LIMIT_FILES = 500 + DIFF_HARD_LIMIT_LINES = 10000 + + def self.decorate(commits) + commits.map { |c| self.new(c) } + end - commits.sort! do |x, y| - y.committed_date <=> x.committed_date - end + # Calculate number of lines to render for diffs + def self.diff_line_count(diffs) + diffs.reduce(0){|sum, d| sum + d.diff.lines.count} + end - commits - end + def self.diff_suppress?(diffs, line_count = nil) + # optimize - check file count first + return true if diffs.size > DIFF_SAFE_FILES - def commits(repo, ref, path = nil, limit = nil, offset = nil) - if path - repo.log(ref, path, max_count: limit, skip: offset) - elsif limit && offset - repo.commits(ref, limit, offset) - else - repo.commits(ref) - end.map{ |c| Commit.new(c) } - end - - def commits_between(repo, from, to) - repo.commits_between(from, to).map { |c| Commit.new(c) } - end + line_count ||= Commit::diff_line_count(diffs) + line_count > DIFF_SAFE_LINES + end - def compare(project, from, to) - result = { - commits: [], - diffs: [], - commit: nil, - same: false - } + def self.diff_force_suppress?(diffs, line_count = nil) + # optimize - check file count first + return true if diffs.size > DIFF_HARD_LIMIT_FILES - return result unless from && to + line_count ||= Commit::diff_line_count(diffs) + line_count > DIFF_HARD_LIMIT_LINES + end - first = project.repository.commit(to.try(:strip)) - last = project.repository.commit(from.try(:strip)) + attr_accessor :raw - if first && last - result[:same] = (first.id == last.id) - result[:commits] = project.repo.commits_between(last.id, first.id).map {|c| Commit.new(c)} - result[:diffs] = project.repo.diff(last.id, first.id) rescue [] - result[:commit] = Commit.new(first) - end + def initialize(raw_commit) + raise "Nil as raw commit passed" unless raw_commit - result - end + @raw = raw_commit end - def initialize(raw_commit, head = nil) - raise "Nil as raw commit passed" unless raw_commit - - @commit = raw_commit - @head = head + def id + @raw.id end - def short_id(length = 10) - id.to_s[0..length] + def diff_line_count + @diff_line_count ||= Commit::diff_line_count(self.diffs) + @diff_line_count end - def safe_message - @safe_message ||= message + def diff_suppress? + Commit::diff_suppress?(self.diffs, diff_line_count) end - def created_at - committed_date + def diff_force_suppress? + Commit::diff_force_suppress?(self.diffs, diff_line_count) end - def author_email - author.email + # Returns a string describing the commit for use in a link title + # + # Example + # + # "Commit: Alex Denisov - Project git clone panel" + def link_title + "Commit: #{author_name} - #{title}" end - def author_name - author.name + # Returns the commits title. + # + # Usually, the commit title is the first line of the commit message. + # In case this first line is longer than 100 characters, it is cut off + # after 80 characters and ellipses (`&hellp;`) are appended. + def title + title = safe_message + + return no_commit_message if title.blank? + + title_end = title.index(/\n/) + if (!title_end && title.length > 100) || (title_end && title_end > 100) + title[0..79] << "…".html_safe + else + title.split(/\n/, 2).first + end end - # Was this commit committed by a different person than the original author? - def different_committer? - author_name != committer_name || author_email != committer_email + # Returns the commits description + # + # cut off, ellipses (`&hellp;`) are prepended to the commit message. + def description + description = safe_message + + title_end = description.index(/\n/) + if (!title_end && description.length > 100) || (title_end && title_end > 100) + "…".html_safe << description[80..-1] + else + description.split(/\n/, 2)[1].try(:chomp) + end end - def committer_name - committer.name + # Regular expression that identifies commit message clauses that trigger issue closing. + def issue_closing_regex + @issue_closing_regex ||= Regexp.new(Gitlab.config.gitlab.issue_closing_pattern) end - def committer_email - committer.email + # Discover issues should be closed when this commit is pushed to a project's + # default branch. + def closes_issues project + md = issue_closing_regex.match(safe_message) + if md + extractor = Gitlab::ReferenceExtractor.new + extractor.analyze(md[0]) + extractor.issues_for(project) + else + [] + end end - def prev_commit - @prev_commit ||= if parents.present? - Commit.new(parents.first) - else - nil - end + # Mentionable override. + def gfm_reference + "commit #{sha[0..5]}" end - def prev_commit_id - prev_commit.try :id + def method_missing(m, *args, &block) + @raw.send(m, *args, &block) end - # Shows the diff between the commit's parent and the commit. - # - # Cuts out the header and stats from #to_patch and returns only the diff. - def to_diff - # see Grit::Commit#show - patch = to_patch - - # discard lines before the diff - lines = patch.split("\n") - while !lines.first.start_with?("diff --git") do - lines.shift - end - lines.pop if lines.last =~ /^[\d.]+$/ # Git version - lines.pop if lines.last == "-- " # end of diff - lines.join("\n") + def respond_to?(method) + return true if @raw.respond_to?(method) + + super end end diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/internal_id.rb new file mode 100644 index 00000000000..821ed54fb98 --- /dev/null +++ b/app/models/concerns/internal_id.rb @@ -0,0 +1,17 @@ +module InternalId + extend ActiveSupport::Concern + + included do + validate :set_iid, on: :create + validates :iid, presence: true, numericality: true + end + + def set_iid + max_iid = project.send(self.class.name.tableize).maximum(:iid) + self.iid = max_iid.to_i + 1 + end + + def to_param + iid.to_s + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 645b35ec660..7f820f950b0 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -6,25 +6,24 @@ # module Issuable extend ActiveSupport::Concern + include Mentionable included do - belongs_to :project belongs_to :author, class_name: "User" belongs_to :assignee, class_name: "User" belongs_to :milestone has_many :notes, as: :noteable, dependent: :destroy - validates :project, presence: true validates :author, presence: true validates :title, presence: true, length: { within: 0..255 } - validates :closed, inclusion: { in: [true, false] } - scope :opened, -> { where(closed: false) } - scope :closed, -> { where(closed: true) } - scope :of_group, ->(group) { where(project_id: group.project_ids) } - scope :of_user_team, ->(team) { where(project_id: team.project_ids, assignee_id: team.member_ids) } - scope :assigned, ->(u) { where(assignee_id: u.id)} + scope :authored, ->(user) { where(author_id: user) } + scope :assigned_to, ->(u) { where(assignee_id: u.id)} scope :recent, -> { order("created_at DESC") } + scope :assigned, -> { where("assignee_id IS NOT NULL") } + scope :unassigned, -> { where("assignee_id IS NULL") } + scope :of_projects, ->(ids) { where(project_id: ids) } + delegate :name, :email, @@ -38,6 +37,8 @@ module Issuable prefix: true attr_accessor :author_id_of_changes + + attr_mentionable :title, :description end module ClassMethods @@ -62,14 +63,6 @@ module Issuable assignee_id_changed? end - def is_being_closed? - closed_changed? && closed - end - - def is_being_reopened? - closed_changed? && !closed - end - # # Votes # @@ -104,4 +97,18 @@ module Issuable def votes_count upvotes + downvotes end + + # Return all users participating on the discussion + def participants + users = [] + users << author + users << assignee if is_assigned? + mentions = [] + mentions << self.mentioned_users + notes.each do |note| + users << note.author + mentions << note.mentioned_users + end + users.concat(mentions.reduce([], :|)).uniq + end end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb new file mode 100644 index 00000000000..5858fe1bb6f --- /dev/null +++ b/app/models/concerns/mentionable.rb @@ -0,0 +1,97 @@ +# == Mentionable concern +# +# Contains functionality related to objects that can mention Users, Issues, MergeRequests, or Commits by +# GFM references. +# +# Used by Issue, Note, MergeRequest, and Commit. +# +module Mentionable + extend ActiveSupport::Concern + + module ClassMethods + # Indicate which attributes of the Mentionable to search for GFM references. + def attr_mentionable *attrs + mentionable_attrs.concat(attrs.map(&:to_s)) + end + + # Accessor for attributes marked mentionable. + def mentionable_attrs + @mentionable_attrs ||= [] + end + end + + # Generate a GFM back-reference that will construct a link back to this Mentionable when rendered. Must + # be overridden if this model object can be referenced directly by GFM notation. + def gfm_reference + raise NotImplementedError.new("#{self.class} does not implement #gfm_reference") + end + + # Construct a String that contains possible GFM references. + def mentionable_text + self.class.mentionable_attrs.map { |attr| send(attr) || '' }.join + end + + # The GFM reference to this Mentionable, which shouldn't be included in its #references. + def local_reference + self + end + + # Determine whether or not a cross-reference Note has already been created between this Mentionable and + # the specified target. + def has_mentioned? target + Note.cross_reference_exists?(target, local_reference) + end + + def mentioned_users + users = [] + return users if mentionable_text.blank? + has_project = self.respond_to? :project + matches = mentionable_text.scan(/@[a-zA-Z][a-zA-Z0-9_\-\.]*/) + matches.each do |match| + identifier = match.delete "@" + if has_project + id = project.team.members.find { |u| u.username == identifier }.try(:id) + else + id = User.where(username: identifier).pluck(:id).first + end + users << User.find(id) unless id.blank? + end + users.uniq + end + + # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. + def references p = project, text = mentionable_text + return [] if text.blank? + ext = Gitlab::ReferenceExtractor.new + ext.analyze(text) + (ext.issues_for(p) + ext.merge_requests_for(p) + ext.commits_for(p)).uniq - [local_reference] + end + + # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. + def create_cross_references! p = project, a = author, without = [] + refs = references(p) - without + refs.each do |ref| + Note.create_cross_reference_note(ref, local_reference, a, p) + end + end + + # If the mentionable_text field is about to change, locate any *added* references and create cross references for + # them. Invoke from an observer's #before_save implementation. + def notice_added_references p = project, a = author + ch = changed_attributes + original, mentionable_changed = "", false + self.class.mentionable_attrs.each do |attr| + if ch[attr] + original << ch[attr] + mentionable_changed = true + end + end + + # Only proceed if the saved changes actually include a chance to an attr_mentionable field. + return unless mentionable_changed + + preexisting = references(p, original) + create_cross_references!(p, a, preexisting) + end + +end diff --git a/app/models/concerns/notifiable.rb b/app/models/concerns/notifiable.rb new file mode 100644 index 00000000000..722f375e71d --- /dev/null +++ b/app/models/concerns/notifiable.rb @@ -0,0 +1,15 @@ +# == Notifiable concern +# +# Contains notification functionality shared between UsersProject and UsersGroup +# +module Notifiable + extend ActiveSupport::Concern + + included do + validates :notification_level, inclusion: { in: Notification.project_notification_levels }, presence: true + end + + def notification + @notification ||= Notification.new(self) + end +end diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb new file mode 100644 index 00000000000..47aeb93a419 --- /dev/null +++ b/app/models/deploy_key.rb @@ -0,0 +1,20 @@ +# == Schema Information +# +# Table name: keys +# +# id :integer not null, primary key +# user_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# key :text +# title :string(255) +# type :string(255) +# fingerprint :string(255) +# + +class DeployKey < Key + has_many :deploy_keys_projects, dependent: :destroy + has_many :projects, through: :deploy_keys_projects + + scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where('deploy_keys_projects.project_id in (?)', projects) } +end diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb new file mode 100644 index 00000000000..6f109e48314 --- /dev/null +++ b/app/models/deploy_keys_project.rb @@ -0,0 +1,22 @@ +# == Schema Information +# +# Table name: deploy_keys_projects +# +# id :integer not null, primary key +# deploy_key_id :integer not null +# project_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class DeployKeysProject < ActiveRecord::Base + attr_accessible :key_id, :project_id + + belongs_to :project + belongs_to :deploy_key + + validates :deploy_key_id, presence: true + validates :deploy_key_id, uniqueness: { scope: [:project_id], message: "already exists in project" } + + validates :project_id, presence: true +end diff --git a/app/models/event.rb b/app/models/event.rb index 18422e192a4..095a06c956b 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -54,6 +54,27 @@ class Event < ActiveRecord::Base Event::COMMENTED end end + + def create_ref_event(project, user, ref, action = 'add', prefix = 'refs/heads') + if action.to_s == 'add' + before = '00000000' + after = ref.commit.id + else + before = ref.commit.id + after = '00000000' + end + + Event.create( + project: project, + action: Event::PUSHED, + data: { + ref: "#{prefix}/#{ref.name}", + before: before, + after: after + }, + author_id: user.id + ) + end end def proper? @@ -68,14 +89,16 @@ class Event < ActiveRecord::Base def project_name if project - project.name + project.name_with_namespace else "(deleted project)" end end def target_title - target.try :title + if target && target.respond_to?(:title) + target.title + end end def push? @@ -130,15 +153,11 @@ class Event < ActiveRecord::Base target if target_type == "MergeRequest" end - def author - @author ||= User.find(author_id) - end - def action_name if closed? "closed" elsif merged? - "merged" + "accepted" elsif joined? 'joined' elsif left? @@ -204,7 +223,7 @@ class Event < ActiveRecord::Base # Max 20 commits from push DESC def commits - @commits ||= data[:commits].map { |commit| repository.commit(commit[:id]) }.reverse + @commits ||= data[:commits].reverse end def commits_count @@ -225,26 +244,8 @@ class Event < ActiveRecord::Base end end - def repository - project.repository - end - - def parent_commit - repository.commit(commit_from) - rescue => ex - nil - end - - def last_commit - repository.commit(commit_to) - rescue => ex - nil - end - def push_with_commits? - md_ref? && commits.any? && parent_commit && last_commit - rescue Grit::NoSuchPathError - false + md_ref? && commits.any? && commit_from && commit_to end def last_push_to_non_root? @@ -255,6 +256,10 @@ class Event < ActiveRecord::Base target.commit_id end + def target_iid + target.respond_to?(:iid) ? target.iid : target_id + end + def note_short_commit_id note_commit_id[0..8] end @@ -263,6 +268,10 @@ class Event < ActiveRecord::Base target.noteable_type == "Commit" end + def note_project_snippet? + target.noteable_type == "Snippet" + end + def note_target target.noteable end @@ -275,6 +284,14 @@ class Event < ActiveRecord::Base end end + def note_target_iid + if note_target.respond_to?(:iid) + note_target.iid + else + note_target_id + end.to_s + end + def wall_note? target.noteable_type.blank? end @@ -286,4 +303,14 @@ class Event < ActiveRecord::Base "Wall" end.downcase end + + def body? + if push? + push_with_commits? + elsif note? + true + else + target.respond_to? :title + end + end end diff --git a/app/models/forked_project_link.rb b/app/models/forked_project_link.rb new file mode 100644 index 00000000000..aaa527a1145 --- /dev/null +++ b/app/models/forked_project_link.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: forked_project_links +# +# id :integer not null, primary key +# forked_to_project_id :integer not null +# forked_from_project_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class ForkedProjectLink < ActiveRecord::Base + attr_accessible :forked_from_project_id, :forked_to_project_id + + # Relations + belongs_to :forked_to_project, class_name: Project + belongs_to :forked_from_project, class_name: Project + +end diff --git a/app/models/gitlab_ci_service.rb b/app/models/gitlab_ci_service.rb index 4eb39c7ef4d..7f5380a4551 100644 --- a/app/models/gitlab_ci_service.rb +++ b/app/models/gitlab_ci_service.rb @@ -11,6 +11,8 @@ # updated_at :datetime not null # active :boolean default(FALSE), not null # project_url :string(255) +# subdomain :string(255) +# room :string(255) # class GitlabCiService < Service @@ -46,4 +48,31 @@ class GitlabCiService < Service def build_page sha project_url + "/builds/#{sha}" end + + def builds_path + project_url + "?ref=" + project.default_branch + end + + def status_img_path + project_url + "/status.png?ref=" + project.default_branch + end + + def title + 'GitLab CI' + end + + def description + 'Continuous integration server from GitLab' + end + + def to_param + 'gitlab_ci' + end + + def fields + [ + { type: 'text', name: 'token', placeholder: 'GitLab CI project specific token' }, + { type: 'text', name: 'project_url', placeholder: 'http://ci.gitlabhq.com/projects/3'} + ] + end end diff --git a/app/models/gollum_wiki.rb b/app/models/gollum_wiki.rb new file mode 100644 index 00000000000..d1edbab4533 --- /dev/null +++ b/app/models/gollum_wiki.rb @@ -0,0 +1,119 @@ +class GollumWiki + + MARKUPS = { + "Markdown" => :markdown, + "RDoc" => :rdoc + } + + class CouldNotCreateWikiError < StandardError; end + + # Returns a string describing what went wrong after + # an operation fails. + attr_reader :error_message + + def initialize(project, user = nil) + @project = project + @user = user + end + + def path + @project.path + '.wiki' + end + + def path_with_namespace + @project.path_with_namespace + ".wiki" + end + + def url_to_repo + gitlab_shell.url_to_repo(path_with_namespace) + end + + def ssh_url_to_repo + url_to_repo + end + + def http_url_to_repo + http_url = [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('') + end + + # Returns the Gollum::Wiki object. + def wiki + @wiki ||= begin + Gollum::Wiki.new(path_to_repo) + rescue Grit::NoSuchPathError + create_repo! + end + end + + # Returns an Array of Gitlab WikiPage instances or an + # empty Array if this Wiki has no pages. + def pages + wiki.pages.map { |page| WikiPage.new(self, page, true) } + end + + # Finds a page within the repository based on a tile + # or slug. + # + # title - The human readable or parameterized title of + # the page. + # + # Returns an initialized WikiPage instance or nil + def find_page(title, version = nil) + if page = wiki.page(title, version) + WikiPage.new(self, page, true) + else + nil + end + end + + def create_page(title, content, format = :markdown, message = nil) + commit = commit_details(:created, message, title) + + wiki.write_page(title, format, content, commit) + rescue Gollum::DuplicatePageError => e + @error_message = "Duplicate page: #{e.message}" + return false + end + + def update_page(page, content, format = :markdown, message = nil) + commit = commit_details(:updated, message, page.title) + + wiki.update_page(page, page.name, format, content, commit) + end + + def delete_page(page, message = nil) + wiki.delete_page(page, commit_details(:deleted, message, page.title)) + end + + private + + def create_repo! + if init_repo(path_with_namespace) + Gollum::Wiki.new(path_to_repo) + else + raise CouldNotCreateWikiError + end + end + + def init_repo(path_with_namespace) + gitlab_shell.add_repository(path_with_namespace) + end + + def commit_details(action, message = nil, title = nil) + commit_message = message || default_message(action, title) + + {email: @user.email, name: @user.name, message: commit_message} + end + + def default_message(action, title) + "#{@user.username} #{action} page: #{title}" + end + + def gitlab_shell + @gitlab_shell ||= Gitlab::Shell.new + end + + def path_to_repo + @path_to_repo ||= File.join(Gitlab.config.gitlab_shell.repos_path, "#{path_with_namespace}.git") + end +end diff --git a/app/models/group.rb b/app/models/group.rb index 8ba92980a9b..0cc3a3a0f41 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -2,35 +2,51 @@ # # Table name: namespaces # -# id :integer not null, primary key -# name :string(255) not null -# path :string(255) not null -# owner_id :integer not null -# created_at :datetime not null -# updated_at :datetime not null -# type :string(255) +# id :integer not null, primary key +# name :string(255) not null +# path :string(255) not null +# owner_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# type :string(255) +# description :string(255) default(""), not null # class Group < Namespace - def add_users_to_project_teams(user_ids, project_access) - UsersProject.add_users_into_projects( - projects.map(&:id), - user_ids, - project_access - ) + has_many :users_groups, dependent: :destroy + has_many :users, through: :users_groups + + def human_name + name end - def users - users = User.joins(:users_projects).where(users_projects: {project_id: project_ids}) - users = users << owner - users.uniq + def owners + @owners ||= users_groups.owners.map(&:user) end - def human_name - name + def add_users(user_ids, group_access) + user_ids.compact.each do |user_id| + self.users_groups.create(user_id: user_id, group_access: group_access) + end + end + + def add_user(user, group_access) + self.users_groups.create(user_id: user.id, group_access: group_access) + end + + def add_owner(user) + self.add_user(user, UsersGroup::OWNER) + end + + def has_owner?(user) + owners.include?(user) + end + + def last_owner?(user) + has_owner?(user) && owners.size == 1 end - def truncate_teams - UsersProject.truncate_teams(project_ids) + def members + users_groups end end diff --git a/app/models/hipchat_service.rb b/app/models/hipchat_service.rb new file mode 100644 index 00000000000..c3fb4826334 --- /dev/null +++ b/app/models/hipchat_service.rb @@ -0,0 +1,75 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# token :string(255) +# project_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean default(FALSE), not null +# project_url :string(255) +# subdomain :string(255) +# room :string(255) +# + +class HipchatService < Service + attr_accessible :room + + validates :token, presence: true, if: :activated? + + def title + 'Hipchat' + end + + def description + 'Simple web-based real-time group chat' + end + + def to_param + 'hipchat' + end + + def fields + [ + { type: 'text', name: 'token', placeholder: '' }, + { type: 'text', name: 'room', placeholder: '' } + ] + end + + def execute(push_data) + gate[room].send('Gitlab', create_message(push_data)) + end + + private + + def gate + @gate ||= HipChat::Client.new(token) + end + + def create_message(push) + ref = push[:ref].gsub("refs/heads/", "") + before = push[:before] + after = push[:after] + + message = "" + message << "#{push[:user_name]} " + if before =~ /000000/ + message << "pushed new branch <a href=\"#{project.web_url}/commits/#{ref}\">#{ref}</a> to <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a>\n" + elsif after =~ /000000/ + message << "removed branch #{ref} from <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a> \n" + else + message << "#pushed to branch <a href=\"#{project.web_url}/commits/#{ref}\">#{ref}</a> " + message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a> " + message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)" + for commit in push[:commits] do + message << "<br /> - #{commit[:message]} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)" + end + end + + message + end + +end diff --git a/app/models/issue.rb b/app/models/issue.rb index 07c0401143c..f3ec322126f 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -9,22 +9,57 @@ # project_id :integer # created_at :datetime not null # updated_at :datetime not null -# closed :boolean default(FALSE), not null # position :integer default(0) # branch_name :string(255) # description :text # milestone_id :integer +# state :string(255) +# iid :integer # class Issue < ActiveRecord::Base include Issuable + include InternalId - attr_accessible :title, :assignee_id, :closed, :position, :description, - :milestone_id, :label_list, :author_id_of_changes + belongs_to :project + validates :project, presence: true + + scope :of_group, ->(group) { where(project_id: group.project_ids) } + scope :of_user_team, ->(team) { where(project_id: team.project_ids, assignee_id: team.member_ids) } + scope :opened, -> { with_state(:opened) } + scope :closed, -> { with_state(:closed) } + + attr_accessible :title, :assignee_id, :position, :description, + :milestone_id, :label_list, :author_id_of_changes, + :state_event acts_as_taggable_on :labels - def self.open_for(user) - opened.assigned(user) + scope :cared, ->(user) { where(assignee_id: user) } + scope :open_for, ->(user) { opened.assigned_to(user) } + + state_machine :state, initial: :opened do + event :close do + transition [:reopened, :opened] => :closed + end + + event :reopen do + transition closed: :reopened + end + + state :opened + + state :reopened + + state :closed + end + + # Both open and reopened issues should be listed as opened + scope :opened, -> { with_state(:opened, :reopened) } + + # Mentionable overrides. + + def gfm_reference + "issue ##{iid}" end end diff --git a/app/models/key.rb b/app/models/key.rb index 64441ea54eb..79f7bbd2590 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -2,74 +2,64 @@ # # Table name: keys # -# id :integer not null, primary key -# user_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# key :text -# title :string(255) -# identifier :string(255) -# project_id :integer +# id :integer not null, primary key +# user_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# key :text +# title :string(255) +# type :string(255) +# fingerprint :string(255) # require 'digest/md5' class Key < ActiveRecord::Base + include Gitlab::Popen + belongs_to :user - belongs_to :project attr_accessible :key, :title - before_validation :strip_white_space - before_save :set_identifier + before_validation :strip_white_space, :generate_fingerpint validates :title, presence: true, length: { within: 0..255 } - validates :key, presence: true, length: { within: 0..5000 }, format: { :with => /ssh-.{3} / }, uniqueness: true - validate :fingerprintable_key + validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }, uniqueness: true + validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' } delegate :name, :email, to: :user, prefix: true def strip_white_space - self.key = self.key.strip unless self.key.blank? + self.key = key.strip unless key.blank? end - def fingerprintable_key - return true unless key # Don't test if there is no key. - - file = Tempfile.new('key_file') - begin - file.puts key - file.rewind - fingerprint_output = `ssh-keygen -lf #{file.path} 2>&1` # Catch stderr. - ensure - file.close - file.unlink # deletes the temp file - end - errors.add(:key, "can't be fingerprinted") if fingerprint_output.match("failed") + # projects that has this key + def projects + user.authorized_projects end - def set_identifier - if is_deploy_key - self.identifier = "deploy_#{Digest::MD5.hexdigest(key)}" - else - self.identifier = "#{user.identifier}_#{Time.now.to_i}" - end + def shell_id + "key-#{id}" end - def is_deploy_key - !!project_id - end + private - # projects that has this key - def projects - if is_deploy_key - [project] - else - user.authorized_projects + def generate_fingerpint + self.fingerprint = nil + return unless key.present? + + cmd_status = 0 + cmd_output = '' + Tempfile.open('gitlab_key_file') do |file| + file.puts key + file.rewind + cmd_output, cmd_status = popen("ssh-keygen -lf #{file.path}", '/tmp') end - end - def shell_id - "key-#{self.id}" + if cmd_status.zero? + cmd_output.gsub /([\d\h]{2}:)+[\d\h]{2}/ do |match| + self.fingerprint = match + end + end end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 345b8d6e07d..7f367588b23 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -2,21 +2,22 @@ # # Table name: merge_requests # -# id :integer not null, primary key -# target_branch :string(255) not null -# source_branch :string(255) not null -# project_id :integer not null -# author_id :integer -# assignee_id :integer -# title :string(255) -# closed :boolean default(FALSE), not null -# created_at :datetime not null -# updated_at :datetime not null -# st_commits :text(2147483647) -# st_diffs :text(2147483647) -# merged :boolean default(FALSE), not null -# state :integer default(1), not null -# milestone_id :integer +# id :integer not null, primary key +# target_branch :string(255) not null +# source_branch :string(255) not null +# source_project_id :integer not null +# author_id :integer +# assignee_id :integer +# title :string(255) +# created_at :datetime not null +# updated_at :datetime not null +# st_commits :text(2147483647) +# st_diffs :text(2147483647) +# milestone_id :integer +# state :string(255) +# merge_status :string(255) +# target_project_id :integer not null +# iid :integer # require Rails.root.join("app/models/commit") @@ -24,45 +25,92 @@ require Rails.root.join("lib/static_model") class MergeRequest < ActiveRecord::Base include Issuable + include InternalId - attr_accessible :title, :assignee_id, :closed, :target_branch, :source_branch, :milestone_id, - :author_id_of_changes + belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" + belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" + + attr_accessible :title, :assignee_id, :source_project_id, :source_branch, :target_project_id, :target_branch, :milestone_id, :author_id_of_changes, :state_event, :description attr_accessor :should_remove_source_branch - BROKEN_DIFF = "--broken-diff" + state_machine :state, initial: :opened do + event :close do + transition [:reopened, :opened] => :closed + end + + event :merge do + transition [:reopened, :opened] => :merged + end + + event :reopen do + transition closed: :reopened + end + + state :opened + + state :reopened + + state :closed + + state :merged + end + + state_machine :merge_status, initial: :unchecked do + event :mark_as_unchecked do + transition [:can_be_merged, :cannot_be_merged] => :unchecked + end + + event :mark_as_mergeable do + transition unchecked: :can_be_merged + end + + event :mark_as_unmergeable do + transition unchecked: :cannot_be_merged + end + + state :unchecked - UNCHECKED = 1 - CAN_BE_MERGED = 2 - CANNOT_BE_MERGED = 3 + state :can_be_merged + + state :cannot_be_merged + end serialize :st_commits serialize :st_diffs + validates :source_project, presence: true validates :source_branch, presence: true + validates :target_project, presence: true validates :target_branch, presence: true validate :validate_branches - def self.find_all_by_branch(branch_name) - where("source_branch LIKE :branch OR target_branch LIKE :branch", branch: branch_name) - end + scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.project_ids) } + scope :of_user_team, ->(team) { where("(source_project_id in (:team_project_ids) OR target_project_id in (:team_project_ids) AND assignee_id in (:team_member_ids))", team_project_ids: team.project_ids, team_member_ids: team.member_ids) } + scope :opened, -> { with_state(:opened) } + scope :closed, -> { with_state(:closed) } + scope :merged, -> { with_state(:merged) } + scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } + scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } + scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } + scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) } + scope :of_projects, ->(ids) { where(target_project_id: ids) } + # Closed scope for merge request should return + # both merged and closed mr's + scope :closed, -> { with_states(:closed, :merged) } - def self.find_all_by_milestone(milestone) - where("milestone_id = :milestone_id", milestone_id: milestone) - end + def validate_branches + if target_project==source_project && target_branch == source_branch + errors.add :branch_conflict, "You can not use same project/branch for source and target" + end - def human_state - states = { - CAN_BE_MERGED => "can_be_merged", - CANNOT_BE_MERGED => "cannot_be_merged", - UNCHECKED => "unchecked" - } - states[self.state] - end + if opened? || reopened? + similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened + similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id - def validate_branches - if target_branch == source_branch - errors.add :base, "You can not use same branch for source and target branches" + if similar_mrs.any? + errors.add :base, "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}" + end end end @@ -71,45 +119,29 @@ class MergeRequest < ActiveRecord::Base self.reloaded_diffs end - def unchecked? - state == UNCHECKED - end - - def mark_as_unchecked - self.state = UNCHECKED - self.save - end - - def can_be_merged? - state == CAN_BE_MERGED - end - def check_if_can_be_merged - self.state = if Gitlab::Satellite::MergeAction.new(self.author, self).can_be_merged? - CAN_BE_MERGED - else - CANNOT_BE_MERGED - end - self.save + if Gitlab::Satellite::MergeAction.new(self.author, self).can_be_merged? + mark_as_mergeable + else + mark_as_unmergeable + end end def diffs - st_diffs || [] + @diffs ||= (load_diffs(st_diffs) || []) end def reloaded_diffs - if open? && unmerged_diffs.any? - self.st_diffs = unmerged_diffs + if opened? && unmerged_diffs.any? + self.st_diffs = dump_diffs(unmerged_diffs) self.save end - - rescue Grit::Git::GitTimeout - self.st_diffs = [BROKEN_DIFF] - self.save end def broken_diffs? - diffs == [BROKEN_DIFF] + diffs == broken_diffs + rescue + true end def valid_diffs? @@ -117,78 +149,64 @@ class MergeRequest < ActiveRecord::Base end def unmerged_diffs - # Only show what is new in the source branch compared to the target branch, not the other way around. - # The linex below with merge_base is equivalent to diff with three dots (git diff branch1...branch2) - # From the git documentation: "git diff A...B" is equivalent to "git diff $(git-merge-base A B) B" - common_commit = project.repo.git.native(:merge_base, {}, [target_branch, source_branch]).strip - diffs = project.repo.diff(common_commit, source_branch) + diffs = if for_fork? + Gitlab::Satellite::MergeAction.new(author, self).diffs_between_satellite + else + Gitlab::Git::Diff.between(target_project.repository, source_branch, target_branch) + end + + diffs ||= [] + diffs end def last_commit commits.first end - def merged? - merged && merge_event - end - def merge_event - self.project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last + self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last end def closed_event - self.project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last + self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last end def commits - st_commits || [] + load_commits(st_commits || []) end def probably_merged? unmerged_commits.empty? && - commits.any? && open? - end - - def open? - !closed - end - - def mark_as_merged! - self.merged = true - self.closed = true - save - end - - def mark_as_unmergable - self.state = CANNOT_BE_MERGED - self.save + commits.any? && opened? end def reloaded_commits - if open? && unmerged_commits.any? - self.st_commits = unmerged_commits + if opened? && unmerged_commits.any? + self.st_commits = dump_commits(unmerged_commits) save + end commits end def unmerged_commits - self.project.repo. - commits_between(self.target_branch, self.source_branch). - map {|c| Commit.new(c)}. + if for_fork? + commits = Gitlab::Satellite::MergeAction.new(self.author, self).commits_between + else + commits = target_project.repository.commits_between(self.target_branch, self.source_branch) + end + + if commits.present? + commits = Commit.decorate(commits). sort_by(&:created_at). reverse + end + commits end def merge!(user_id) - self.mark_as_merged! - Event.create( - project: self.project, - action: Event::MERGED, - target_id: self.id, - target_type: "MergeRequest", - author_id: user_id - ) + self.author_id_of_changes = user_id + self.merge end def automerge!(current_user) @@ -197,7 +215,7 @@ class MergeRequest < ActiveRecord::Base true end rescue - self.mark_as_unmergable + mark_as_unmergeable false end @@ -209,18 +227,74 @@ class MergeRequest < ActiveRecord::Base # Returns the raw diff for this merge request # # see "git diff" - def to_diff - project.repo.git.native(:diff, {timeout: 30, raise: true}, "#{target_branch}...#{source_branch}") + def to_diff(current_user) + Gitlab::Satellite::MergeAction.new(current_user, self).diff_in_satellite end # Returns the commit as a series of email patches. # # see "git format-patch" - def to_patch - project.repo.git.format_patch({timeout: 30, raise: true, stdout: true}, "#{target_branch}..#{source_branch}") + def to_patch(current_user) + Gitlab::Satellite::MergeAction.new(current_user, self).format_patch end def last_commit_short_sha @last_commit_short_sha ||= last_commit.sha[0..10] end + + def for_fork? + target_project != source_project + end + + def disallow_source_branch_removal? + (source_project.root_ref? source_branch) || for_fork? + end + + def project + target_project + end + + # Return the set of issues that will be closed if this merge request is accepted. + def closes_issues + if target_branch == project.default_branch + unmerged_commits.map { |c| c.closes_issues(project) }.flatten.uniq.sort_by(&:id) + else + [] + end + end + + # Mentionable override. + def gfm_reference + "merge request !#{iid}" + end + + private + + def dump_commits(commits) + commits.map(&:to_hash) + end + + def load_commits(array) + array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash)) } + end + + def dump_diffs(diffs) + if diffs == broken_diffs + broken_diffs + elsif diffs.respond_to?(:map) + diffs.map(&:to_hash) + end + end + + def load_diffs(raw) + if raw == broken_diffs + broken_diffs + elsif raw.respond_to?(:map) + raw.map { |hash| Gitlab::Git::Diff.new(hash) } + end + end + + def broken_diffs + [Gitlab::Git::Diff::BROKEN_DIFF] + end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 457fe18f35b..1a73fa71e48 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -7,25 +7,42 @@ # project_id :integer not null # description :text # due_date :date -# closed :boolean default(FALSE), not null # created_at :datetime not null # updated_at :datetime not null +# state :string(255) +# iid :integer # class Milestone < ActiveRecord::Base - attr_accessible :title, :description, :due_date, :closed, :author_id_of_changes + include InternalId + + attr_accessible :title, :description, :due_date, :state_event, :author_id_of_changes attr_accessor :author_id_of_changes belongs_to :project has_many :issues has_many :merge_requests + has_many :participants, through: :issues, source: :assignee - scope :active, -> { where(closed: false) } - scope :closed, -> { where(closed: true) } + scope :active, -> { with_state(:active) } + scope :closed, -> { with_state(:closed) } validates :title, presence: true validates :project, presence: true - validates :closed, inclusion: { in: [true, false] } + + state_machine :state, initial: :active do + event :close do + transition active: :closed + end + + event :activate do + transition closed: :active + end + + state :closed + + state :active + end def expired? if due_date @@ -35,10 +52,6 @@ class Milestone < ActiveRecord::Base end end - def participants - User.where(id: issues.pluck(:assignee_id)) - end - def open_items_count self.issues.opened.count + self.merge_requests.opened.count end @@ -62,23 +75,19 @@ class Milestone < ActiveRecord::Base if due_date.past? "expired at #{due_date.stamp("Aug 21, 2011")}" else - "expires at #{due_date.stamp("Aug 21, 2011")}" + "expires at #{due_date.stamp("Aug 21, 2011")}" end - end + end end def can_be_closed? - open? && issues.opened.count.zero? + active? && issues.opened.count.zero? end def is_empty? total_items_count.zero? end - def open? - !closed - end - def author_id author_id_of_changes end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 4e157839369..6ac94c06604 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -2,31 +2,39 @@ # # Table name: namespaces # -# id :integer not null, primary key -# name :string(255) not null -# path :string(255) not null -# owner_id :integer not null -# created_at :datetime not null -# updated_at :datetime not null -# type :string(255) +# id :integer not null, primary key +# name :string(255) not null +# path :string(255) not null +# owner_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# type :string(255) +# description :string(255) default(""), not null # class Namespace < ActiveRecord::Base - attr_accessible :name, :path + include Gitlab::ShellAdapter + + attr_accessible :name, :description, :path has_many :projects, dependent: :destroy belongs_to :owner, class_name: "User" - validates :name, presence: true, uniqueness: true + validates :owner, presence: true, unless: ->(n) { n.type == "Group" } + validates :name, presence: true, uniqueness: true, + length: { within: 0..255 }, + format: { with: Gitlab::Regex.name_regex, + message: "only letters, digits, spaces & '_' '-' '.' allowed." } + validates :description, length: { within: 0..255 } validates :path, uniqueness: true, presence: true, length: { within: 1..255 }, + exclusion: { in: Gitlab::Blacklist.path }, format: { with: Gitlab::Regex.path_regex, message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" } - validates :owner, presence: true delegate :name, to: :owner, allow_nil: true, prefix: true after_create :ensure_dir_exist - after_update :move_dir + after_update :move_dir, if: :path_changed? after_destroy :rm_dir scope :root, -> { where('type IS NULL') } @@ -48,48 +56,34 @@ class Namespace < ActiveRecord::Base end def ensure_dir_exist - unless dir_exists? - FileUtils.mkdir( namespace_full_path, mode: 0770 ) - end - end - - def dir_exists? - File.exists?(namespace_full_path) + gitlab_shell.add_namespace(path) end - def namespace_full_path - @namespace_full_path ||= File.join(Gitlab.config.gitlab_shell.repos_path, path) + def rm_dir + gitlab_shell.rm_namespace(path) end def move_dir - if path_changed? - old_path = File.join(Gitlab.config.gitlab_shell.repos_path, path_was) - new_path = File.join(Gitlab.config.gitlab_shell.repos_path, path) - if File.exists?(new_path) - raise "Already exists" - end - - + if gitlab_shell.mv_namespace(path_was, path) + # If repositories moved successfully we need to remove old satellites + # and send update instructions to users. + # However we cannot allow rollback since we moved namespace dir + # So we basically we mute exceptions in next actions begin - # Remove satellite when moving repo - if path_was.present? - satellites_path = File.join(Gitlab.config.satellites.path, path_was) - FileUtils.rm_r( satellites_path, force: true ) - end - - FileUtils.mv( old_path, new_path ) + gitlab_shell.rm_satellites(path_was) send_update_instructions - rescue Exception => e - raise "Namespace move error #{old_path} #{new_path}" + rescue + # Returning false does not rollback after_* transaction but gives + # us information about failing some of tasks + false end + else + # if we cannot move namespace directory we should rollback + # db changes in order to prevent out of sync between db and fs + raise Exception.new('namespace directory cannot be moved') end end - def rm_dir - dir_path = File.join(Gitlab.config.gitlab_shell.repos_path, path) - FileUtils.rm_r( dir_path, force: true ) - end - def send_update_instructions projects.each(&:send_move_instructions) end diff --git a/app/models/network/commit.rb b/app/models/network/commit.rb new file mode 100644 index 00000000000..e31adcebbe7 --- /dev/null +++ b/app/models/network/commit.rb @@ -0,0 +1,37 @@ +require "grit" + +module Network + class Commit + include ActionView::Helpers::TagHelper + + attr_accessor :time, :spaces, :parent_spaces + + def initialize(raw_commit) + @commit = raw_commit + @time = -1 + @spaces = [] + @parent_spaces = [] + end + + def method_missing(m, *args, &block) + @commit.send(m, *args, &block) + end + + def space + if @spaces.size > 0 + @spaces.first + else + 0 + end + end + + def parents(map) + @commit.parents.map do |p| + if map.include?(p.id) + map[p.id] + end + end + .compact + end + end +end diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb new file mode 100644 index 00000000000..424819f3501 --- /dev/null +++ b/app/models/network/graph.rb @@ -0,0 +1,273 @@ +module Network + class Graph + attr_reader :days, :commits, :map, :notes, :repo + + def self.max_count + @max_count ||= 650 + end + + def initialize project, ref, commit, filter_ref + @project = project + @ref = ref + @commit = commit + @filter_ref = filter_ref + @repo = project.repository + + @commits = collect_commits + @days = index_commits + @notes = collect_notes + end + + protected + + def collect_notes + h = Hash.new(0) + @project.notes.where('noteable_type = ?' ,"Commit").group('notes.commit_id').select('notes.commit_id, count(notes.id) as note_count').each do |item| + h[item.commit_id] = item.note_count.to_i + end + h + end + + # Get commits from repository + # + def collect_commits + find_commits(count_to_display_commit_in_center).map do |commit| + # Decorate with app/model/network/commit.rb + Network::Commit.new(commit) + end + end + + # Method is adding time and space on the + # list of commits. As well as returns date list + # correlated with time set on commits. + # + # @return [Array<TimeDate>] list of commit dates correlated with time on commits + def index_commits + days = [] + @map = {} + @reserved = {} + + @commits.each_with_index do |c,i| + c.time = i + days[i] = c.committed_date + @map[c.id] = c + @reserved[i] = [] + end + + commits_sort_by_ref.each do |commit| + place_chain(commit) + end + + # find parent spaces for not overlap lines + @commits.each do |c| + c.parent_spaces.concat(find_free_parent_spaces(c)) + end + + days + end + + # Skip count that the target commit is displayed in center. + def count_to_display_commit_in_center + offset = -1 + skip = 0 + while offset == -1 + tmp_commits = find_commits(skip) + if tmp_commits.size > 0 + index = tmp_commits.index do |c| + c.id == @commit.id + end + + if index + # Find the target commit + offset = index + skip + else + skip += self.class.max_count + end + else + # Cant't find the target commit in the repo. + offset = 0 + end + end + + if self.class.max_count / 2 < offset then + # get max index that commit is displayed in the center. + offset - self.class.max_count / 2 + else + 0 + end + end + + def find_commits(skip = 0) + opts = { + max_count: self.class.max_count, + skip: skip + } + + opts[:ref] = @commit.id if @filter_ref + + @repo.find_commits(opts) + end + + def commits_sort_by_ref + @commits.sort do |a,b| + if include_ref?(a) + -1 + elsif include_ref?(b) + 1 + else + b.committed_date <=> a.committed_date + end + end + end + + def include_ref?(commit) + commit.ref_names(@repo).include?(@ref) + end + + def find_free_parent_spaces(commit) + spaces = [] + + commit.parents(@map).each do |parent| + range = commit.time..parent.time + + space = if commit.space >= parent.space then + find_free_parent_space(range, parent.space, -1, commit.space) + else + find_free_parent_space(range, commit.space, -1, parent.space) + end + + mark_reserved(range, space) + spaces << space + end + + spaces + end + + def find_free_parent_space(range, space_base, space_step, space_default) + if is_overlap?(range, space_default) then + find_free_space(range, space_step, space_base, space_default) + else + space_default + end + end + + def is_overlap?(range, overlap_space) + range.each do |i| + if i != range.first && + i != range.last && + @commits[i].spaces.include?(overlap_space) then + + return true; + end + end + + false + end + + # Add space mark on commit and its parents + # + # @param [::Commit] the commit object. + def place_chain(commit, parent_time = nil) + leaves = take_left_leaves(commit) + if leaves.empty? + return + end + + time_range = leaves.first.time..leaves.last.time + space_base = get_space_base(leaves) + space = find_free_space(time_range, 2, space_base) + leaves.each do |l| + l.spaces << space + # Also add space to parent + l.parents(@map).each do |parent| + if 0 < parent.space && parent.space < space + parent.spaces << space + end + end + end + + # and mark it as reserved + if parent_time.nil? + min_time = leaves.first.time + else + min_time = parent_time + 1 + end + + max_time = leaves.last.time + leaves.last.parents(@map).each do |parent| + if max_time < parent.time + max_time = parent.time + end + end + mark_reserved(min_time..max_time, space) + + # Visit branching chains + leaves.each do |l| + parents = l.parents(@map).select{|p| p.space.zero?} + for p in parents + place_chain(p, l.time) + end + end + end + + def get_space_base(leaves) + space_base = 1 + parents = leaves.last.parents(@map) + if parents.size > 0 + if parents.first.space > 0 + space_base = parents.first.space + end + end + space_base + end + + def mark_reserved(time_range, space) + for day in time_range + @reserved[day].push(space) + end + end + + def find_free_space(time_range, space_step, space_base = 1, space_default = nil) + space_default ||= space_base + + reserved = [] + for day in time_range + reserved += @reserved[day] + end + reserved.uniq! + + space = space_default + while reserved.include?(space) do + space += space_step + if space < space_base then + space_step *= -1 + space = space_base + space_step + end + end + + space + end + + # Takes most left subtree branch of commits + # which don't have space mark yet. + # + # @param [::Commit] the commit object. + # + # @return [Array<Network::Commit>] list of branch commits + def take_left_leaves(raw_commit) + commit = @map[raw_commit.id] + leaves = [] + leaves.push(commit) if commit.space.zero? + + while true + return leaves if commit.parents(@map).count.zero? + + commit = commit.parents(@map).first + + return leaves unless commit.space.zero? + + leaves.push(commit) + end + end + end +end diff --git a/app/models/note.rb b/app/models/note.rb index 97f6bf6e3a7..e819a5516b5 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -13,17 +13,18 @@ # line_code :string(255) # commit_id :string(255) # noteable_id :integer +# st_diff :text # require 'carrierwave/orm/activerecord' require 'file_size_validator' class Note < ActiveRecord::Base + include Mentionable + attr_accessible :note, :noteable, :noteable_id, :noteable_type, :project_id, :attachment, :line_code, :commit_id - - attr_accessor :notify - attr_accessor :notify_author + attr_mentionable :note belongs_to :project belongs_to :noteable, polymorphic: true @@ -43,55 +44,112 @@ class Note < ActiveRecord::Base # Scopes scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } - scope :inline, -> { where("line_code IS NOT NULL") } - scope :not_inline, -> { where("line_code IS NULL") } + scope :inline, ->{ where("line_code IS NOT NULL") } + scope :not_inline, ->{ where(line_code: [nil, '']) } scope :common, ->{ where(noteable_type: ["", nil]) } scope :fresh, ->{ order("created_at ASC, id ASC") } scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author, ->{ includes(:author) } - def self.create_status_change_note(noteable, author, status) + serialize :st_diff + before_create :set_diff, if: ->(n) { n.line_code.present? } + + def self.create_status_change_note(noteable, project, author, status, source) + body = "_Status changed to #{status}#{' by ' + source.gfm_reference if source}_" + + create({ + noteable: noteable, + project: project, + author: author, + note: body, + system: true + }, without_protection: true) + end + + # +noteable+ was referenced from +mentioner+, by including GFM in either +mentioner+'s description or an associated Note. + # Create a system Note associated with +noteable+ with a GFM back-reference to +mentioner+. + def self.create_cross_reference_note(noteable, mentioner, author, project) create({ noteable: noteable, - project: noteable.project, + commit_id: (noteable.sha if noteable.respond_to? :sha), + project: project, author: author, - note: "_Status changed to #{status}_" + note: "_mentioned in #{mentioner.gfm_reference}_", + system: true }, without_protection: true) end + # Determine whether or not a cross-reference note already exists. + def self.cross_reference_exists?(noteable, mentioner) + where(noteable_id: noteable.id, system: true, note: "_mentioned in #{mentioner.gfm_reference}_").any? + end + def commit_author @commit_author ||= project.users.find_by_email(noteable.author_email) || - project.users.find_by_name(noteable.author_name) + project.users.find_by_name(noteable.author_name) rescue nil end - def diff - if noteable.diffs.present? - noteable.diffs.select do |d| - if d.b_path - Digest::SHA1.hexdigest(d.b_path) == diff_file_index - end - end.first + def find_diff + return nil unless noteable && noteable.diffs.present? + + @diff ||= noteable.diffs.find do |d| + Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path end end + def set_diff + # First lets find notes with same diff + # before iterating over all mr diffs + diff = Note.where(noteable_id: self.noteable_id, noteable_type: self.noteable_type, line_code: self.line_code).last.try(:diff) + diff ||= find_diff + + self.st_diff = diff.to_hash if diff + end + + def diff + @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) + end + + def active? + # TODO: determine if discussion is outdated + # according to recent MR diff or not + true + end + def diff_file_index line_code.split('_')[0] end def diff_file_name - diff.b_path + diff.new_path if diff + end + + def diff_old_line + line_code.split('_')[1].to_i end def diff_new_line line_code.split('_')[2].to_i end + def diff_line + return @diff_line if @diff_line + + if diff + Gitlab::DiffParser.new(diff).each do |full_line, type, line_code, line_new, line_old| + @diff_line = full_line if line_code == self.line_code + end + end + + @diff_line + end + def discussion_id - @discussion_id ||= [:discussion, noteable_type.try(:underscore), noteable_id, line_code].join("-").to_sym + @discussion_id ||= [:discussion, noteable_type.try(:underscore), noteable_id || commit_id, line_code].join("-").to_sym end # Returns true if this is a downvote note, @@ -138,19 +196,11 @@ class Note < ActiveRecord::Base super end # Temp fix to prevent app crash - # if note commit id doesnt exist + # if note commit id doesn't exist rescue nil end - def notify - @notify ||= false - end - - def notify_author - @notify_author ||= false - end - # Returns true if this is an upvote note, # otherwise false is returned def upvote? @@ -163,6 +213,16 @@ class Note < ActiveRecord::Base for_issue? || (for_merge_request? && !for_diff_line?) end + # Mentionable override. + def gfm_reference + noteable.gfm_reference + end + + # Mentionable override. + def local_reference + noteable + end + def noteable_type_name if noteable_type.present? noteable_type.downcase @@ -170,4 +230,10 @@ class Note < ActiveRecord::Base "wall" end end + + # FIXME: Hack for polymorphic associations with STI + # For more information wisit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations + def noteable_type=(sType) + super(sType.to_s.classify.constantize.base_class.to_s) + end end diff --git a/app/models/notification.rb b/app/models/notification.rb new file mode 100644 index 00000000000..ff6a18d6a51 --- /dev/null +++ b/app/models/notification.rb @@ -0,0 +1,39 @@ +class Notification + # + # Notification levels + # + N_DISABLED = 0 + N_PARTICIPATING = 1 + N_WATCH = 2 + N_GLOBAL = 3 + + attr_accessor :target + + def self.notification_levels + [N_DISABLED, N_PARTICIPATING, N_WATCH] + end + + def self.project_notification_levels + [N_DISABLED, N_PARTICIPATING, N_WATCH, N_GLOBAL] + end + + def initialize(target) + @target = target + end + + def disabled? + target.notification_level == N_DISABLED + end + + def participating? + target.notification_level == N_PARTICIPATING + end + + def watch? + target.notification_level == N_WATCH + end + + def global? + target.notification_level == N_GLOBAL + end +end diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb new file mode 100644 index 00000000000..ef2000ad05e --- /dev/null +++ b/app/models/personal_snippet.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: snippets +# +# id :integer not null, primary key +# title :string(255) +# content :text(2147483647) +# author_id :integer not null +# project_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# file_name :string(255) +# expires_at :datetime +# private :boolean default(TRUE), not null +# type :string(255) +# + +class PersonalSnippet < Snippet +end diff --git a/app/models/pivotaltracker_service.rb b/app/models/pivotaltracker_service.rb new file mode 100644 index 00000000000..f28e142da77 --- /dev/null +++ b/app/models/pivotaltracker_service.rb @@ -0,0 +1,59 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# token :string(255) +# project_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean default(FALSE), not null +# + +class PivotaltrackerService < Service + include HTTParty + + validates :token, presence: true, if: :activated? + + def title + 'PivotalTracker' + end + + def description + 'Project Management Software (Source Commits Endpoint)' + end + + def to_param + 'pivotaltracker' + end + + def fields + [ + { type: 'text', name: 'token', placeholder: '' } + ] + end + + def execute(push) + url = 'https://www.pivotaltracker.com/services/v5/source_commits' + push[:commits].each do |commit| + message = { + 'source_commit' => { + 'commit_id' => commit[:id], + 'author' => commit[:author][:name], + 'url' => commit[:url], + 'message' => commit[:message] + } + } + PivotaltrackerService.post( + url, + body: message.to_json, + headers: { + 'Content-Type' => 'application/json', + 'X-TrackerToken' => token + } + ) + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 15b2d858b62..c4f4b06713d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -16,22 +16,27 @@ # wiki_enabled :boolean default(TRUE), not null # namespace_id :integer # public :boolean default(FALSE), not null +# issues_tracker :string(255) default("gitlab"), not null +# issues_tracker_id :string(255) +# snippets_enabled :boolean default(TRUE), not null +# last_activity_at :datetime +# imported :boolean default(FALSE), not null +# import_url :string(255) # require "grit" class Project < ActiveRecord::Base - include Gitolited + include Gitlab::ShellAdapter + extend Enumerize - class TransferError < StandardError; end - - attr_accessible :name, :path, :description, :default_branch, - :issues_enabled, :wall_enabled, :merge_requests_enabled, - :wiki_enabled, :public, :import_url, as: [:default, :admin] + attr_accessible :name, :path, :description, :default_branch, :issues_tracker, :label_list, + :issues_enabled, :wall_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, + :wiki_enabled, :public, :import_url, :last_activity_at, as: [:default, :admin] attr_accessible :namespace_id, :creator_id, as: :admin - attr_accessor :import_url + acts_as_taggable_on :labels, :issues_default_labels # Relations belongs_to :creator, foreign_key: "creator_id", class_name: "User" @@ -40,66 +45,72 @@ class Project < ActiveRecord::Base has_one :last_event, class_name: 'Event', order: 'events.created_at DESC', foreign_key: 'project_id' has_one :gitlab_ci_service, dependent: :destroy + has_one :campfire_service, dependent: :destroy + has_one :pivotaltracker_service, dependent: :destroy + has_one :hipchat_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 + has_many :services, dependent: :destroy has_many :events, dependent: :destroy - has_many :merge_requests, dependent: :destroy - has_many :issues, dependent: :destroy, order: "closed, created_at DESC" + has_many :merge_requests, dependent: :destroy, foreign_key: "target_project_id" + has_many :issues, dependent: :destroy, order: "state DESC, created_at DESC" has_many :milestones, dependent: :destroy - has_many :users_projects, dependent: :destroy has_many :notes, dependent: :destroy - has_many :snippets, dependent: :destroy - has_many :deploy_keys, dependent: :destroy, class_name: "Key", foreign_key: "project_id" + has_many :snippets, dependent: :destroy, class_name: "ProjectSnippet" has_many :hooks, dependent: :destroy, class_name: "ProjectHook" - has_many :wikis, dependent: :destroy has_many :protected_branches, dependent: :destroy - has_many :user_team_project_relationships, dependent: :destroy - has_many :users, through: :users_projects - has_many :user_teams, through: :user_team_project_relationships - has_many :user_team_user_relationships, through: :user_teams - has_many :user_teams_members, through: :user_team_user_relationships + has_many :users_projects, dependent: :destroy + has_many :users, through: :users_projects + + has_many :deploy_keys_projects, dependent: :destroy + has_many :deploy_keys, through: :deploy_keys_projects delegate :name, to: :owner, allow_nil: true, prefix: true + delegate :members, to: :team, prefix: true # Validations validates :creator, presence: true validates :description, length: { within: 0..2000 } validates :name, presence: true, length: { within: 0..255 }, format: { with: Gitlab::Regex.project_name_regex, - message: "only letters, digits, spaces & '_' '-' '.' allowed. Letter should be first" } + message: "only letters, digits, spaces & '_' '-' '.' allowed. Letter or digit should be first" } validates :path, presence: true, length: { within: 0..255 }, + exclusion: { in: Gitlab::Blacklist.path }, format: { with: Gitlab::Regex.path_regex, - message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" } + message: "only letters, digits & '_' '-' '.' allowed. Letter or digit should be first" } validates :issues_enabled, :wall_enabled, :merge_requests_enabled, :wiki_enabled, inclusion: { in: [true, false] } + validates :issues_tracker_id, length: { within: 0..255 } + validates :namespace, presence: true validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :path, scope: :namespace_id validates :import_url, - format: { with: URI::regexp(%w(http https)), message: "should be a valid url" }, + format: { with: URI::regexp(%w(git http https)), message: "should be a valid url" }, if: :import? - validate :check_limit, :repo_name + validate :check_limit, on: :create # Scopes - scope :without_user, ->(user) { where("id NOT IN (:ids)", ids: user.authorized_projects.map(&:id) ) } - scope :not_in_group, ->(group) { where("id NOT IN (:ids)", ids: group.project_ids ) } - scope :without_team, ->(team) { team.projects.present? ? where("id NOT IN (:ids)", ids: team.projects.map(&:id)) : scoped } - scope :in_team, ->(team) { where("id IN (:ids)", ids: team.projects.map(&:id)) } + scope :without_user, ->(user) { where("projects.id NOT IN (:ids)", ids: user.authorized_projects.map(&:id) ) } + scope :without_team, ->(team) { team.projects.present? ? where("projects.id NOT IN (:ids)", ids: team.projects.map(&:id)) : scoped } + scope :not_in_group, ->(group) { where("projects.id NOT IN (:ids)", ids: group.project_ids ) } + scope :in_team, ->(team) { where("projects.id IN (:ids)", ids: team.projects.map(&:id)) } scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) } - scope :sorted_by_activity, ->() { order("(SELECT max(events.created_at) FROM events WHERE events.project_id = projects.id) DESC") } + scope :in_group_namespace, -> { joins(:group) } + scope :sorted_by_activity, -> { reorder("projects.last_activity_at DESC") } scope :personal, ->(user) { where(namespace_id: user.namespace_id) } scope :joined, ->(user) { where("namespace_id != ?", user.namespace_id) } scope :public_only, -> { where(public: true) } + enumerize :issues_tracker, in: (Gitlab.config.issues_tracker.keys).append(:gitlab), default: :gitlab + class << self def abandoned - project_ids = Event.select('max(created_at) as latest_date, project_id'). - group('project_id'). - having('latest_date < ?', 6.months.ago).map(&:project_id) - - where(id: project_ids) + where('projects.last_activity_at < ?', 6.months.ago) end def with_push @@ -125,10 +136,6 @@ class Project < ActiveRecord::Base where(path: id, namespace_id: nil).last end end - - def access_options - UsersProject.access_roles - end end def team @@ -136,45 +143,31 @@ class Project < ActiveRecord::Base end def repository - if path - @repository ||= Repository.new(path_with_namespace, default_branch) - else - nil - end - rescue Grit::NoSuchPathError - nil + @repository ||= Repository.new(path_with_namespace, default_branch) end def saved? - id && valid? + id && persisted? end def import? import_url.present? end + def imported? + imported + end + def check_limit unless creator.can_create_project? - errors[:base] << ("Your own projects limit is #{creator.projects_limit}! Please contact administrator to increase it") + errors[:limit_reached] << ("Your own projects limit is #{creator.projects_limit}! Please contact administrator to increase it") end rescue errors[:base] << ("Can't check your ability to create project") end - def repo_name - denied_paths = %w(admin dashboard groups help profile projects search) - - if denied_paths.include?(path) - errors.add(:path, "like #{path} is not allowed") - end - end - def to_param - if namespace - namespace.path + "/" + path - else - path - end + namespace.path + "/" + path end def web_url @@ -190,7 +183,7 @@ class Project < ActiveRecord::Base end def last_activity_date - last_event.try(:created_at) || updated_at + last_activity_at || updated_at end def project_id @@ -198,11 +191,37 @@ class Project < ActiveRecord::Base end def issues_labels - issues.tag_counts_on(:labels) + @issues_labels ||= (issues_default_labels + issues.tags_on(:labels)).uniq.sort_by(&:name) + end + + def issue_exists?(issue_id) + if used_default_issues_tracker? + self.issues.where(iid: issue_id).first.present? + else + true + end + end + + def used_default_issues_tracker? + self.issues_tracker == Project.issues_tracker.default_value end - def services - [gitlab_ci_service].compact + def can_have_issues_tracker_id? + self.issues_enabled && !self.used_default_issues_tracker? + end + + def build_missing_services + available_services_names.each do |service_name| + service = services.find { |service| service.to_param == service_name } + + # If service is available but missing in db + # we should create an instance. Ex `create_gitlab_ci_service` + service = self.send :"create_#{service_name}_service" if service.nil? + end + end + + def available_services_names + %w(gitlab_ci campfire hipchat pivotaltracker) end def gitlab_ci? @@ -224,16 +243,16 @@ class Project < ActiveRecord::Base end def send_move_instructions - self.users_projects.each do |member| - Notify.delay.project_was_moved_email(member.id) + team.members.each do |user| + Notify.delay.project_was_moved_email(self.id, user.id) end end def owner - if namespace - namespace_owner + if group + group else - creator + namespace.try(:owner) end end @@ -247,32 +266,6 @@ class Project < ActiveRecord::Base users_projects.find_by_user_id(user_id) end - def transfer(new_namespace) - Project.transaction do - old_namespace = namespace - self.namespace = new_namespace - - old_dir = old_namespace.try(:path) || '' - new_dir = new_namespace.try(:path) || '' - - old_repo = if old_dir.present? - File.join(old_dir, self.path) - else - self.path - end - - if Project.where(path: self.path, namespace_id: new_namespace.try(:id)).present? - raise TransferError.new("Project with same path in target namespace already exists") - end - - Gitlab::ProjectMover.new(self, old_dir, new_dir).execute - - save! - end - rescue Gitlab::ProjectMover::ProjectMoveError => ex - raise Project::TransferError.new(ex.message) - end - def name_with_namespace @name_with_namespace ||= begin if namespace @@ -283,10 +276,6 @@ class Project < ActiveRecord::Base end end - def namespace_owner - namespace.try(:owner) - end - def path_with_namespace if namespace namespace.path + '/' + path @@ -295,51 +284,8 @@ class Project < ActiveRecord::Base end end - # This method will be called after each post receive and only if the provided - # user is present in GitLab. - # - # All callbacks for post receive should be placed here. - def trigger_post_receive(oldrev, newrev, ref, user) - data = post_receive_data(oldrev, newrev, ref, user) - - # Create satellite - self.satellite.create unless self.satellite.exists? - - # Create push event - self.observe_push(data) - - if push_to_branch? ref, oldrev - # Close merged MR - self.update_merge_requests(oldrev, newrev, ref, user) - - # Execute web hooks - self.execute_hooks(data.dup) - - # Execute project services - self.execute_services(data.dup) - end - - # Discover the default branch, but only if it hasn't already been set to - # something else - if repository && default_branch.nil? - update_attributes(default_branch: self.repository.discover_default_branch) - end - end - - def push_to_branch? ref, oldrev - ref_parts = ref.split('/') - - # Return if this is not a push to a branch (e.g. new commits) - !(ref_parts[1] !~ /heads/ || oldrev == "00000000000000000000000000000000") - end - - def observe_push(data) - Event.create( - project: self, - action: Event::PUSHED, - data: data, - author_id: data[:user_id] - ) + def transfer(new_namespace) + ProjectTransferService.new.transfer(self, new_namespace) end def execute_hooks(data) @@ -354,68 +300,12 @@ class Project < ActiveRecord::Base end end - # Produce a hash of post-receive data - # - # data = { - # before: String, - # after: String, - # ref: String, - # user_id: String, - # user_name: String, - # repository: { - # name: String, - # url: String, - # description: String, - # homepage: String, - # }, - # commits: Array, - # total_commits_count: Fixnum - # } - # - def post_receive_data(oldrev, newrev, ref, user) - - push_commits = repository.commits_between(oldrev, newrev) - - # Total commits count - push_commits_count = push_commits.size - - # Get latest 20 commits ASC - push_commits_limited = push_commits.last(20) - - # Hash to be passed as post_receive_data - data = { - before: oldrev, - after: newrev, - ref: ref, - user_id: user.id, - user_name: user.name, - repository: { - name: name, - url: url_to_repo, - description: description, - homepage: web_url, - }, - commits: [], - total_commits_count: push_commits_count - } - - # For perfomance purposes maximum 20 latest commits - # will be passed as post receive hook data. - # - push_commits_limited.each do |commit| - data[:commits] << { - id: commit.id, - message: commit.safe_message, - timestamp: commit.date.xmlschema, - url: "#{Gitlab.config.gitlab.url}/#{path_with_namespace}/commit/#{commit.id}", - author: { - name: commit.author_name, - email: commit.author_email - } - } + def discover_default_branch + # Discover the default branch, but only if it hasn't already been set to + # something else + if repository.exists? && default_branch.nil? + update_attributes(default_branch: self.repository.discover_default_branch) end - - data end def update_merge_requests(oldrev, newrev, ref, user) @@ -424,7 +314,7 @@ class Project < ActiveRecord::Base c_ids = self.repository.commits_between(oldrev, newrev).map(&:id) # Update code for merge requests - mrs = self.merge_requests.opened.find_all_by_branch(branch_name).all + mrs = self.merge_requests.opened.by_branch(branch_name).all mrs.each { |merge_request| merge_request.reload_code; merge_request.mark_as_unchecked } # Close merge requests @@ -436,14 +326,18 @@ class Project < ActiveRecord::Base end def valid_repo? - repo + repository.exists? rescue errors.add(:path, "Invalid repository path") false end def empty_repo? - !repository || repository.empty? + !repository.exists? || repository.empty? + end + + def ensure_satellite_exists + self.satellite.create unless self.satellite.exists? end def satellite @@ -463,18 +357,25 @@ class Project < ActiveRecord::Base end def repo_exists? - @repo_exists ||= (repository && repository.branches.present?) + @repo_exists ||= repository.exists? rescue @repo_exists = false end def open_branches - if protected_branches.empty? - self.repo.heads - else - pnames = protected_branches.map(&:name) - self.repo.heads.reject { |h| pnames.include?(h.name) } - end.sort_by(&:name) + all_branches = repository.branches + + if protected_branches.present? + all_branches.reject! do |branch| + protected_branches_names.include?(branch.name) + end + end + + all_branches + end + + def protected_branches_names + @protected_branches_names ||= protected_branches.map(&:name) end def root_ref?(branch) @@ -489,13 +390,58 @@ class Project < ActiveRecord::Base http_url = [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('') end - def project_access_human(member) - project_user_relation = self.users_projects.find_by_user_id(member.id) - self.class.access_options.key(project_user_relation.project_access) - end - # Check if current branch name is marked as protected in the system def protected_branch? branch_name - protected_branches.map(&:name).include?(branch_name) + protected_branches_names.include?(branch_name) + end + + def forked? + !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?) + end + + def personal? + !group + end + + def rename_repo + old_path_with_namespace = File.join(namespace_dir, path_was) + new_path_with_namespace = File.join(namespace_dir, path) + + if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace) + # If repository moved successfully we need to remove old satellite + # and send update instructions to users. + # However we cannot allow rollback since we moved repository + # So we basically we mute exceptions in next actions + begin + gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki") + gitlab_shell.rm_satellites(old_path_with_namespace) + ensure_satellite_exists + send_move_instructions + reset_events_cache + rescue + # Returning false does not rollback after_* transaction but gives + # us information about failing some of tasks + false + end + else + # if we cannot move namespace directory we should rollback + # db changes in order to prevent out of sync between db and fs + raise Exception.new('repository cannot be renamed') + end + end + + # Reset events cache related to this project + # + # Since we do cache @event we need to reset cache in special cases: + # * when project was moved + # * when project was renamed + # Events cache stored like events/23-20130109142513. + # The cache key includes updated_at timestamp. + # Thus it will automatically generate a new fragment + # when the event is updated because the key changes. + def reset_events_cache + Event.where(project_id: self.id). + order('id DESC').limit(100). + update_all(updated_at: Time.now) end end diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb new file mode 100644 index 00000000000..f38aa07059c --- /dev/null +++ b/app/models/project_snippet.rb @@ -0,0 +1,28 @@ +# == Schema Information +# +# Table name: snippets +# +# id :integer not null, primary key +# title :string(255) +# content :text(2147483647) +# author_id :integer not null +# project_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# file_name :string(255) +# expires_at :datetime +# private :boolean default(TRUE), not null +# type :string(255) +# + +class ProjectSnippet < Snippet + belongs_to :project + belongs_to :author, class_name: "User" + + validates :project, presence: true + + # Scopes + scope :fresh, -> { order("created_at DESC") } + scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } + scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } +end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index c2cf83c0ca8..bc35c4041ba 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -21,8 +21,26 @@ class ProjectTeam end end - def get_tm user_id - project.users_projects.find_by_user_id(user_id) + def find(user_id) + user = project.users.find_by_id(user_id) + + if group + user ||= group.users.find_by_id(user_id) + end + + user + end + + def find_tm(user_id) + tm = project.users_projects.find_by_user_id(user_id) + + # If user is not in project members + # we should check for group membership + if group && !tm + tm = group.users_groups.find_by_user_id(user_id) + end + + tm end def add_user(user, access) @@ -47,45 +65,23 @@ class ProjectTeam end def members - project.users_projects + @members ||= fetch_members end def guests - members.guests.map(&:user) + @guests ||= fetch_members(:guests) end def reporters - members.reporters.map(&:user) + @reporters ||= fetch_members(:reporters) end def developers - members.developers.map(&:user) + @developers ||= fetch_members(:developers) end def masters - members.masters.map(&:user) - end - - def repository_readers - repository_members[UsersProject::REPORTER] - end - - def repository_writers - repository_members[UsersProject::DEVELOPER] - end - - def repository_masters - repository_members[UsersProject::MASTER] - end - - def repository_members - keys = Hash.new {|h,k| h[k] = [] } - UsersProject.select("keys.identifier, project_access"). - joins(user: :keys).where(project_id: project.id). - each {|row| keys[row.project_access] << [row.identifier] } - - keys[UsersProject::REPORTER] += project.deploy_keys.pluck(:identifier) - keys + @masters ||= fetch_members(:masters) end def import(source_project) @@ -104,7 +100,6 @@ class ProjectTeam new_tm = tm.dup new_tm.id = nil new_tm.project_id = target_project.id - new_tm.skip_git = true new_tm end @@ -118,4 +113,22 @@ class ProjectTeam rescue false end + + private + + def fetch_members(level = nil) + project_members = project.users_projects + group_members = group ? group.users_groups : [] + + if level + project_members = project_members.send(level) + group_members = group_members.send(level) if group + end + + (project_members + group_members).map(&:user).uniq + end + + def group + project.group + end end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 57229d50759..16379720e59 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -10,7 +10,7 @@ # class ProtectedBranch < ActiveRecord::Base - include Gitolited + include Gitlab::ShellAdapter attr_accessible :name diff --git a/app/models/repository.rb b/app/models/repository.rb index 8bcafbacda1..aeec48ee5cc 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1,150 +1,153 @@ class Repository - # Repository directory name with namespace direcotry - # Examples: - # gitlab/gitolite - # diaspora - # - attr_accessor :path_with_namespace + include Gitlab::ShellAdapter - # Grit repo object - attr_accessor :repo + attr_accessor :raw_repository - # Default branch in the repository - attr_accessor :root_ref + def initialize(path_with_namespace, default_branch) + @raw_repository = Gitlab::Git::Repository.new(path_with_namespace, default_branch) + rescue Gitlab::Git::Repository::NoRepository + nil + end - def initialize(path_with_namespace, root_ref = 'master') - @root_ref = root_ref || "master" - @path_with_namespace = path_with_namespace + def exists? + raw_repository + end - # Init grit repo object - repo + def empty? + raw_repository.empty? end - def raw - repo + def commit(id = nil) + return nil unless raw_repository + commit = Gitlab::Git::Commit.find(raw_repository, id) + commit = Commit.new(commit) if commit + commit end - def path_to_repo - @path_to_repo ||= File.join(Gitlab.config.gitlab_shell.repos_path, "#{path_with_namespace}.git") + def commits(ref, path = nil, limit = nil, offset = nil) + commits = Gitlab::Git::Commit.where( + repo: raw_repository, + ref: ref, + path: path, + limit: limit, + offset: offset, + ) + commits = Commit.decorate(commits) if commits.present? + commits end - def repo - @repo ||= Grit::Repo.new(path_to_repo) + def commits_between(from, to) + commits = Gitlab::Git::Commit.between(raw_repository, from, to) + commits = Commit.decorate(commits) if commits.present? + commits end - def commit(commit_id = nil) - Commit.find_or_first(repo, commit_id, root_ref) + def find_branch(name) + branches.find { |branch| branch.name == name } end - def fresh_commits(n = 10) - Commit.fresh_commits(repo, n) + def find_tag(name) + tags.find { |tag| tag.name == name } end - def commits_with_refs(n = 20) - Commit.commits_with_refs(repo, n) + def recent_branches(limit = 20) + branches.sort do |a, b| + a.commit.committed_date <=> b.commit.committed_date + end[0..limit] end - def commits_since(date) - Commit.commits_since(repo, date) + def add_branch(branch_name, ref) + Rails.cache.delete(cache_key(:branch_names)) + + gitlab_shell.add_branch(path_with_namespace, branch_name, ref) end - def commits(ref, path = nil, limit = nil, offset = nil) - Commit.commits(repo, ref, path, limit, offset) + def add_tag(tag_name, ref) + Rails.cache.delete(cache_key(:tag_names)) + + gitlab_shell.add_tag(path_with_namespace, tag_name, ref) end - def last_commit_for(ref, path = nil) - commits(ref, path, 1).first + def rm_branch(branch_name) + Rails.cache.delete(cache_key(:branch_names)) + + gitlab_shell.rm_branch(path_with_namespace, branch_name) end - def commits_between(from, to) - Commit.commits_between(repo, from, to) + def rm_tag(tag_name) + Rails.cache.delete(cache_key(:tag_names)) + + gitlab_shell.rm_tag(path_with_namespace, tag_name) end - # Returns an Array of branch names - def branch_names - repo.branches.collect(&:name).sort + def round_commit_count + if commit_count > 10000 + '10000+' + elsif commit_count > 5000 + '5000+' + elsif commit_count > 1000 + '1000+' + else + commit_count + end end - # Returns an Array of Branches - def branches - repo.branches.sort_by(&:name) + def branch_names + Rails.cache.fetch(cache_key(:branch_names)) do + raw_repository.branch_names + end end - # Returns an Array of tag names def tag_names - repo.tags.collect(&:name).sort.reverse + Rails.cache.fetch(cache_key(:tag_names)) do + raw_repository.tag_names + end end - # Returns an Array of Tags - def tags - repo.tags.sort_by(&:name).reverse + def commit_count + Rails.cache.fetch(cache_key(:commit_count)) do + begin + raw_repository.raw.commit_count + rescue + 0 + end + end end - # Returns an Array of branch and tag names - def ref_names - [branch_names + tag_names].flatten + # Return repo size in megabytes + # Cached in redis + def size + Rails.cache.fetch(cache_key(:size)) do + raw_repository.size + end end - def heads - @heads ||= repo.heads + def expire_cache + Rails.cache.delete(cache_key(:size)) + Rails.cache.delete(cache_key(:branch_names)) + Rails.cache.delete(cache_key(:tag_names)) + Rails.cache.delete(cache_key(:commit_count)) + Rails.cache.delete(cache_key(:graph_log)) end - def tree(fcommit, path = nil) - fcommit = commit if fcommit == :head - tree = fcommit.tree - path ? (tree / path) : tree + def graph_log + Rails.cache.fetch(cache_key(:graph_log)) do + stats = Gitlab::Git::GitStats.new(raw, root_ref) + stats.parsed_log + end end - def has_commits? - !!commit - rescue Grit::NoSuchPathError - false + def cache_key(type) + "#{type}:#{path_with_namespace}" end - def empty? - !has_commits? - end - - # Discovers the default branch based on the repository's available branches - # - # - If no branches are present, returns nil - # - If one branch is present, returns its name - # - If two or more branches are present, returns the one that has a name - # matching root_ref (default_branch or 'master' if default_branch is nil) - def discover_default_branch - if branch_names.length == 0 - nil - elsif branch_names.length == 1 - branch_names.first - else - branch_names.select { |v| v == root_ref }.first - end + def method_missing(m, *args, &block) + raw_repository.send(m, *args, &block) end - # Archive Project to .tar.gz - # - # Already packed repo archives stored at - # app_root/tmp/repositories/project_name/project_name-commit-id.tag.gz - # - def archive_repo(ref) - ref = ref || self.root_ref - commit = self.commit(ref) - return nil unless commit - - # Build file path - file_name = self.path_with_namespace + "-" + commit.id.to_s + ".tar.gz" - storage_path = Rails.root.join("tmp", "repositories") - file_path = File.join(storage_path, file_name) - - # Put files into a directory before archiving - prefix = self.path_with_namespace + "/" - - # Create file if not exists - unless File.exists?(file_path) - FileUtils.mkdir_p File.dirname(file_path) - file = self.repo.archive_to_file(ref, prefix, file_path) - end + def respond_to?(method) + return true if raw_repository.respond_to?(method) - file_path + super end end diff --git a/app/models/service.rb b/app/models/service.rb index d3486d29200..540aaad1ce5 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -11,8 +11,12 @@ # updated_at :datetime not null # active :boolean default(FALSE), not null # project_url :string(255) +# subdomain :string(255) +# room :string(255) # +# To add new service you should build a class inherited from Service +# and implement a set of methods class Service < ActiveRecord::Base attr_accessible :title, :token, :type, :active @@ -24,4 +28,29 @@ class Service < ActiveRecord::Base def activated? active end + + def title + # implement inside child + end + + def description + # implement inside child + end + + def to_param + # implement inside child + end + + def fields + # implement inside child + [] + end + + def execute + # implement inside child + end + + def can_test? + !project.empty_repo? + end end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index c4ee35e0556..edc179b20fd 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -4,36 +4,39 @@ # # id :integer not null, primary key # title :string(255) -# content :text +# content :text(2147483647) # author_id :integer not null -# project_id :integer not null +# project_id :integer # created_at :datetime not null # updated_at :datetime not null # file_name :string(255) # expires_at :datetime +# private :boolean default(TRUE), not null +# type :string(255) # class Snippet < ActiveRecord::Base include Linguist::BlobHelper - attr_accessible :title, :content, :file_name, :expires_at + attr_accessible :title, :content, :file_name, :expires_at, :private - belongs_to :project belongs_to :author, class_name: "User" + has_many :notes, as: :noteable, dependent: :destroy delegate :name, :email, to: :author, prefix: true, allow_nil: true validates :author, presence: true - validates :project, presence: true validates :title, presence: true, length: { within: 0..255 } validates :file_name, presence: true, length: { within: 0..255 } validates :content, presence: true # Scopes - scope :fresh, -> { order("created_at DESC") } - scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } + scope :public, -> { where(private: false) } + scope :private, -> { where(private: true) } + 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]) } def self.content_types [ diff --git a/app/models/system_hook.rb b/app/models/system_hook.rb index 5f1bd6477c4..5cdf046644f 100644 --- a/app/models/system_hook.rb +++ b/app/models/system_hook.rb @@ -12,13 +12,4 @@ # class SystemHook < WebHook - def self.all_hooks_fire(data) - SystemHook.all.each do |sh| - sh.async_execute data - end - end - - def async_execute(data) - Sidekiq::Client.enqueue(SystemHookWorker, id, data) - end end diff --git a/app/models/tree.rb b/app/models/tree.rb index 96395a42394..042050527c1 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -1,29 +1,17 @@ class Tree - include Linguist::BlobHelper + attr_accessor :raw - attr_accessor :path, :tree, :ref - - delegate :contents, :basename, :name, :data, :mime_type, - :mode, :size, :text?, :colorize, to: :tree - - def initialize(raw_tree, ref = nil, path = nil) - @ref, @path = ref, path - @tree = if path.present? - raw_tree / path - else - raw_tree - end + def initialize(repository, sha, ref = nil, path = nil) + @raw = Gitlab::Git::Tree.new(repository, sha, ref, path) end - def is_blob? - tree.is_a?(Grit::Blob) + def method_missing(m, *args, &block) + @raw.send(m, *args, &block) end - def invalid? - tree.nil? - end + def respond_to?(method) + return true if @raw.respond_to?(method) - def empty? - data.blank? + super end end diff --git a/app/models/user.rb b/app/models/user.rb index 10af9b8c165..f1f93eadc1a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -22,10 +22,8 @@ # linkedin :string(255) default(""), not null # twitter :string(255) default(""), not null # authentication_token :string(255) -# dark_scheme :boolean default(FALSE), not null # theme_id :integer default(1), not null # bio :string(255) -# blocked :boolean default(FALSE), not null # failed_attempts :integer default(0) # locked_at :datetime # extern_uid :string(255) @@ -33,6 +31,11 @@ # username :string(255) # can_create_group :boolean default(TRUE), not null # can_create_team :boolean default(TRUE), not null +# state :string(255) +# color_scheme_id :integer default(1), not null +# notification_level :integer default(1), not null +# password_expires_at :datetime +# created_by_id :integer # class User < ActiveRecord::Base @@ -40,65 +43,140 @@ class User < ActiveRecord::Base :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :registerable attr_accessible :email, :password, :password_confirmation, :remember_me, :bio, :name, :username, - :skype, :linkedin, :twitter, :dark_scheme, :theme_id, :force_random_password, - :extern_uid, :provider, as: [:default, :admin] - attr_accessible :projects_limit, :can_create_team, :can_create_group, as: :admin + :skype, :linkedin, :twitter, :color_scheme_id, :theme_id, :force_random_password, + :extern_uid, :provider, :password_expires_at, + as: [:default, :admin] + + attr_accessible :projects_limit, :can_create_group, + as: :admin attr_accessor :force_random_password + # Virtual attribute for authenticating by either username or email + attr_accessor :login + + # Add login to attr_accessible + attr_accessible :login + + + # + # Relations + # + # Namespace for personal projects - has_one :namespace, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace", conditions: 'type IS NULL' + has_one :namespace, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace", conditions: 'type IS NULL' + + # Namespaces (owned groups and own namespace) + has_many :namespaces, foreign_key: :owner_id + + # Profile + has_many :keys, dependent: :destroy - has_many :keys, dependent: :destroy + # Groups + has_many :own_groups, class_name: "Group", foreign_key: :owner_id + has_many :owned_groups, through: :users_groups, source: :group, conditions: { users_groups: { group_access: UsersGroup::OWNER } } + + has_many :users_groups, dependent: :destroy + has_many :groups, through: :users_groups + + # Projects + has_many :snippets, dependent: :destroy, foreign_key: :author_id, class_name: "Snippet" has_many :users_projects, dependent: :destroy has_many :issues, dependent: :destroy, foreign_key: :author_id has_many :notes, dependent: :destroy, foreign_key: :author_id has_many :merge_requests, dependent: :destroy, foreign_key: :author_id has_many :events, dependent: :destroy, foreign_key: :author_id, class_name: "Event" + has_many :recent_events, foreign_key: :author_id, class_name: "Event", order: "id DESC" has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue" has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest" - has_many :groups, class_name: "Group", foreign_key: :owner_id - has_many :recent_events, class_name: "Event", foreign_key: :author_id, order: "id DESC" - - has_many :projects, through: :users_projects - - has_many :user_team_user_relationships, dependent: :destroy - - has_many :user_teams, through: :user_team_user_relationships - has_many :user_team_project_relationships, through: :user_teams - has_many :team_projects, through: :user_team_project_relationships + has_many :groups_projects, through: :groups, source: :projects + has_many :personal_projects, through: :namespace, source: :projects + has_many :projects, through: :users_projects + has_many :own_projects, foreign_key: :creator_id, class_name: 'Project' + has_many :owned_projects, through: :namespaces, source: :projects + # + # Validations + # validates :name, presence: true + validates :email, presence: true, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/ } validates :bio, length: { within: 0..255 } 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: true, + exclusion: { in: Gitlab::Blacklist.path }, format: { with: Gitlab::Regex.username_regex, message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" } + validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true validate :namespace_uniq, if: ->(user) { user.username_changed? } before_validation :generate_password, on: :create + before_validation :sanitize_attrs + before_save :ensure_authentication_token + alias_attribute :private_token, :authentication_token delegate :path, to: :namespace, allow_nil: true, prefix: true + state_machine :state, initial: :active do + after_transition any => :blocked do |user, transition| + # Remove user from all projects and + user.users_projects.find_each do |membership| + # skip owned resources + next if membership.project.owner == user + + return false unless membership.destroy + end + + # Remove user from all groups + user.users_groups.find_each do |membership| + # skip owned resources + next if membership.group.last_owner?(user) + + return false unless membership.destroy + end + end + + event :block do + transition active: :blocked + end + + event :activate do + transition blocked: :active + end + end + # Scopes scope :admins, -> { where(admin: true) } - scope :blocked, -> { where(blocked: true) } - scope :active, -> { where(blocked: false) } + scope :blocked, -> { with_state(:blocked) } + scope :active, -> { with_state(:active) } scope :alphabetically, -> { order('name ASC') } scope :in_team, ->(team){ where(id: team.member_ids) } 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) ) : scoped } + scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM users_projects)') } + scope :ldap, -> { where(provider: 'ldap') } + scope :potential_team_members, ->(team) { team.members.any? ? active.not_in_team(team) : active } # # Class methods # class << self + # Devise method overridden to allow sing in with email or username + def find_for_database_authentication(warden_conditions) + conditions = warden_conditions.dup + if login = conditions.delete(:login) + where(conditions).where(["lower(username) = :value OR lower(email) = :value", { value: login.downcase }]).first + else + where(conditions).first + end + end + def filter filter_name case filter_name when "admins"; self.admins @@ -109,36 +187,32 @@ class User < ActiveRecord::Base end end - def not_in_project(project) - if project.users.present? - where("id not in (:ids)", ids: project.users.map(&:id) ) - else - scoped - end - end - - def without_projects - where('id NOT IN (SELECT DISTINCT(user_id) FROM users_projects)') - end - - def create_from_omniauth(auth, ldap = false) - gitlab_auth.create_from_omniauth(auth, ldap) - end - - def find_or_new_for_omniauth(auth) - gitlab_auth.find_or_new_for_omniauth(auth) + def search query + where("name LIKE :query OR email LIKE :query OR username LIKE :query", query: "%#{query}%") end - def find_for_ldap_auth(auth, signed_in_resource = nil) - gitlab_auth.find_for_ldap_auth(auth, signed_in_resource) + def by_username_or_id(name_or_id) + if (name_or_id.is_a?(Integer)) + User.find_by_id(name_or_id) + else + User.find_by_username(name_or_id) + end end - def gitlab_auth - Gitlab::Auth.new + def build_user(attrs = {}, options= {}) + if options[:as] == :admin + User.new(defaults.merge(attrs.symbolize_keys), options) + else + User.new(attrs, options).with_defaults + end end - def search query - where("name LIKE :query OR email LIKE :query OR username LIKE :query", query: "%#{query}%") + def defaults + { + projects_limit: Gitlab.config.gitlab.default_projects_limit, + can_create_group: Gitlab.config.gitlab.default_can_create_group, + theme_id: Gitlab.config.gitlab.default_theme + } end end @@ -150,6 +224,10 @@ class User < ActiveRecord::Base username end + def notification + @notification ||= Notification.new(self) + end + def generate_password if self.force_random_password self.password = self.password_confirmation = Devise.friendly_token.first(8) @@ -163,67 +241,26 @@ class User < ActiveRecord::Base end end - # Namespaces user has access to - def namespaces - namespaces = [] - - # Add user account namespace - namespaces << self.namespace if self.namespace - - # Add groups you can manage - namespaces += groups.all - - namespaces - end - - # Groups where user is an owner - def owned_groups - groups - end - # Groups user has access to def authorized_groups @authorized_groups ||= begin - groups = Group.where(id: self.authorized_projects.pluck(:namespace_id)).all - groups = groups + self.groups - groups.uniq - end + group_ids = (groups.pluck(:id) + own_groups.pluck(:id) + authorized_projects.pluck(:namespace_id)) + Group.where(id: group_ids).order('namespaces.name ASC') + end end # Projects user has access to def authorized_projects - project_ids = users_projects.pluck(:project_id) - project_ids = project_ids | owned_projects.pluck(:id) - Project.where(id: project_ids) - end - - # Projects in user namespace - def personal_projects - Project.personal(self) - end - - # Projects where user is an owner - def owned_projects - Project.where("(projects.namespace_id IN (:namespaces)) OR - (projects.namespace_id IS NULL AND projects.creator_id = :user_id)", - namespaces: namespaces.map(&:id), user_id: self.id) + @authorized_projects ||= begin + project_ids = (owned_projects.pluck(:id) + groups_projects.pluck(:id) + projects.pluck(:id)).uniq + Project.where(id: project_ids).joins(:namespace).order('namespaces.name ASC') + end end # Team membership in authorized projects def tm_in_authorized_projects - UsersProject.where(project_id: authorized_projects.map(&:id), user_id: self.id) - end - - # Returns a string for use as a Gitolite user identifier - # - # Note that Gitolite 2.x requires the following pattern for users: - # - # ^@?[0-9a-zA-Z][0-9a-zA-Z._\@+-]*$ - def identifier - # Replace non-word chars with underscores, then make sure it starts with - # valid chars - email.gsub(/\W/, '_').gsub(/\A([\W\_])+/, '') + UsersProject.where(project_id: authorized_projects.map(&:id), user_id: self.id) end def is_admin? @@ -234,8 +271,12 @@ class User < ActiveRecord::Base keys.count == 0 end + def can_change_username? + Gitlab.config.gitlab.username_changing_enabled + end + def can_create_project? - projects_limit > personal_projects.count + projects_limit_left > 0 end def can_create_group? @@ -263,18 +304,11 @@ class User < ActiveRecord::Base end def cared_merge_requests - MergeRequest.where("author_id = :id or assignee_id = :id", id: self.id) + MergeRequest.cared(self) end - # Remove user from all projects and - # set blocked attribute to true - def block - users_projects.find_each do |membership| - return false unless membership.destroy - end - - self.blocked = true - save + def projects_limit_left + projects_limit - personal_projects.count end def projects_limit_percent @@ -296,25 +330,65 @@ class User < ActiveRecord::Base end def several_namespaces? - namespaces.size > 1 + namespaces.many? || owned_groups.any? end def namespace_id namespace.try :id end - def authorized_teams - @authorized_teams ||= begin - ids = [] - ids << UserTeam.with_member(self).pluck('user_teams.id') - ids << UserTeam.created_by(self).pluck('user_teams.id') - ids.flatten + def name_with_username + "#{name} (#{username})" + end + + def tm_of(project) + project.team_member_by_id(self.id) + end + + def already_forked? project + !!fork_of(project) + end + + def fork_of project + links = ForkedProjectLink.where(forked_from_project_id: project, forked_to_project_id: personal_projects) + + if links.any? + links.first.forked_to_project + else + nil + end + end - UserTeam.where(id: ids) - end + def ldap_user? + extern_uid && provider == 'ldap' end - def owned_teams - UserTeam.where(owner_id: self.id) + def accessible_deploy_keys + DeployKey.in_projects(self.authorized_projects).uniq + end + + def created_by + User.find_by_id(created_by_id) if created_by_id + end + + def sanitize_attrs + %w(name username skype linkedin twitter bio).each do |attr| + value = self.send(attr) + self.send("#{attr}=", Sanitize.clean(value)) if value.present? + end + end + + def solo_owned_groups + @solo_owned_groups ||= owned_groups.select do |group| + group.owners == [self] + end + end + + def with_defaults + User.defaults.each do |k, v| + self.send("#{k}=", v) + end + + self end end diff --git a/app/models/user_team.rb b/app/models/user_team.rb deleted file mode 100644 index dc8cf9eeb22..00000000000 --- a/app/models/user_team.rb +++ /dev/null @@ -1,109 +0,0 @@ -# == Schema Information -# -# Table name: user_teams -# -# id :integer not null, primary key -# name :string(255) -# path :string(255) -# owner_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# - -class UserTeam < ActiveRecord::Base - attr_accessible :name, :owner_id, :path - - belongs_to :owner, class_name: User - - has_many :user_team_project_relationships, dependent: :destroy - has_many :user_team_user_relationships, dependent: :destroy - - has_many :projects, through: :user_team_project_relationships - has_many :members, through: :user_team_user_relationships, source: :user - - validates :name, presence: true, uniqueness: true - validates :owner, presence: true - validates :path, uniqueness: true, presence: true, length: { within: 1..255 }, - format: { with: Gitlab::Regex.path_regex, - message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" } - - scope :with_member, ->(user){ joins(:user_team_user_relationships).where(user_team_user_relationships: {user_id: user.id}) } - scope :with_project, ->(project){ joins(:user_team_project_relationships).where(user_team_project_relationships: {project_id: project})} - scope :without_project, ->(project){ where("user_teams.id NOT IN (:ids)", ids: (a = with_project(project); a.blank? ? 0 : a))} - scope :created_by, ->(user){ where(owner_id: user) } - - class << self - def search query - where("name LIKE :query OR path LIKE :query", query: "%#{query}%") - end - - def global_id - 'GLN' - end - - def access_roles - UsersProject.access_roles - end - end - - def to_param - path - end - - def assign_to_projects(projects, access) - projects.each do |project| - assign_to_project(project, access) - end - end - - def assign_to_project(project, access) - Gitlab::UserTeamManager.assign(self, project, access) - end - - def resign_from_project(project) - Gitlab::UserTeamManager.resign(self, project) - end - - def add_members(users, access, group_admin) - users.each do |user| - add_member(user, access, group_admin) - end - end - - def add_member(user, access, group_admin) - Gitlab::UserTeamManager.add_member_into_team(self, user, access, group_admin) - end - - def remove_member(user) - Gitlab::UserTeamManager.remove_member_from_team(self, user) - end - - def update_membership(user, options) - Gitlab::UserTeamManager.update_team_user_membership(self, user, options) - end - - def update_project_access(project, permission) - Gitlab::UserTeamManager.update_project_greates_access(self, project, permission) - end - - def max_project_access(project) - user_team_project_relationships.find_by_project_id(project).greatest_access - end - - def human_max_project_access(project) - self.class.access_roles.invert[max_project_access(project)] - end - - def default_projects_access(member) - user_team_user_relationships.find_by_user_id(member).permission - end - - def human_default_projects_access(member) - self.class.access_roles.invert[default_projects_access(member)] - end - - def admin?(member) - user_team_user_relationships.with_user(member).first.group_admin? - end - -end diff --git a/app/models/user_team_project_relationship.rb b/app/models/user_team_project_relationship.rb deleted file mode 100644 index a7aa88970c7..00000000000 --- a/app/models/user_team_project_relationship.rb +++ /dev/null @@ -1,40 +0,0 @@ -# == Schema Information -# -# Table name: user_team_project_relationships -# -# id :integer not null, primary key -# project_id :integer -# user_team_id :integer -# greatest_access :integer -# created_at :datetime not null -# updated_at :datetime not null -# - -class UserTeamProjectRelationship < ActiveRecord::Base - attr_accessible :greatest_access, :project_id, :user_team_id - - belongs_to :user_team - belongs_to :project - - validates :project, presence: true - validates :user_team, presence: true - validate :check_greatest_access - - scope :with_project, ->(project){ where(project_id: project.id) } - - def team_name - user_team.name - end - - private - - def check_greatest_access - errors.add(:base, :incorrect_access_code) unless correct_access? - end - - def correct_access? - return false if greatest_access.blank? - return true if UsersProject.access_roles.has_value?(greatest_access) - false - end -end diff --git a/app/models/user_team_user_relationship.rb b/app/models/user_team_user_relationship.rb deleted file mode 100644 index 1f7e2625f5f..00000000000 --- a/app/models/user_team_user_relationship.rb +++ /dev/null @@ -1,32 +0,0 @@ -# == Schema Information -# -# Table name: user_team_user_relationships -# -# id :integer not null, primary key -# user_id :integer -# user_team_id :integer -# group_admin :boolean -# permission :integer -# created_at :datetime not null -# updated_at :datetime not null -# - -class UserTeamUserRelationship < ActiveRecord::Base - attr_accessible :group_admin, :permission, :user_id, :user_team_id - - belongs_to :user_team - belongs_to :user - - validates :user_team, presence: true - validates :user, presence: true - - scope :with_user, ->(user) { where(user_id: user.id) } - - def user_name - user.name - end - - def access_human - UsersProject.access_roles.invert[permission] - end -end diff --git a/app/models/users_group.rb b/app/models/users_group.rb new file mode 100644 index 00000000000..181bf322283 --- /dev/null +++ b/app/models/users_group.rb @@ -0,0 +1,46 @@ +# == Schema Information +# +# Table name: users_groups +# +# id :integer not null, primary key +# group_access :integer not null +# group_id :integer not null +# user_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# notification_level :integer default(3), not null +# + +class UsersGroup < ActiveRecord::Base + include Notifiable + include Gitlab::Access + + def self.group_access_roles + Gitlab::Access.options_with_owner + end + + attr_accessible :group_access, :user_id + + belongs_to :user + belongs_to :group + + scope :guests, -> { where(group_access: GUEST) } + scope :reporters, -> { where(group_access: REPORTER) } + scope :developers, -> { where(group_access: DEVELOPER) } + scope :masters, -> { where(group_access: MASTER) } + scope :owners, -> { where(group_access: OWNER) } + + scope :with_group, ->(group) { where(group_id: group.id) } + scope :with_user, ->(user) { where(user_id: user.id) } + + validates :group_access, inclusion: { in: UsersGroup.group_access_roles.values }, presence: true + validates :user_id, presence: true + validates :group_id, presence: true + validates :user_id, uniqueness: { scope: [:group_id], message: "already exists in group" } + + delegate :name, :username, :email, to: :user, prefix: true + + def access_field + group_access + end +end diff --git a/app/models/users_project.rb b/app/models/users_project.rb index 486aaa6966a..6f147859a5c 100644 --- a/app/models/users_project.rb +++ b/app/models/users_project.rb @@ -2,32 +2,28 @@ # # Table name: users_projects # -# id :integer not null, primary key -# user_id :integer not null -# project_id :integer not null -# created_at :datetime not null -# updated_at :datetime not null -# project_access :integer default(0), not null +# id :integer not null, primary key +# user_id :integer not null +# project_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# project_access :integer default(0), not null +# notification_level :integer default(3), not null # class UsersProject < ActiveRecord::Base - include Gitolited - - GUEST = 10 - REPORTER = 20 - DEVELOPER = 30 - MASTER = 40 + include Gitlab::ShellAdapter + include Notifiable + include Gitlab::Access attr_accessible :user, :user_id, :project_access belongs_to :user belongs_to :project - attr_accessor :skip_git - validates :user, presence: true validates :user_id, uniqueness: { scope: [:project_id], message: "already exists in project" } - validates :project_access, inclusion: { in: [GUEST, REPORTER, DEVELOPER, MASTER] }, presence: true + validates :project_access, inclusion: { in: Gitlab::Access.values }, presence: true validates :project, presence: true delegate :name, :username, :email, to: :user, prefix: true @@ -38,7 +34,7 @@ class UsersProject < ActiveRecord::Base scope :masters, -> { where(project_access: MASTER) } scope :in_project, ->(project) { where(project_id: project.id) } - scope :in_projects, ->(projects) { where(project_id: project_ids) } + scope :in_projects, ->(projects) { where(project_id: projects.map { |p| p.id }) } scope :with_user, ->(user) { where(user_id: user.id) } class << self @@ -75,7 +71,6 @@ class UsersProject < ActiveRecord::Base user_ids.each do |user_id| users_project = UsersProject.new(project_access: project_access, user_id: user_id) users_project.project_id = project_id - users_project.skip_git = true users_project.save end end @@ -90,7 +85,6 @@ class UsersProject < ActiveRecord::Base UsersProject.transaction do users_projects = UsersProject.where(project_id: project_ids) users_projects.each do |users_project| - users_project.skip_git = true users_project.destroy end end @@ -105,33 +99,19 @@ class UsersProject < ActiveRecord::Base end def roles_hash - { - guest: GUEST, - reporter: REPORTER, - developer: DEVELOPER, - master: MASTER - } + Gitlab::Access.sym_options end def access_roles - { - "Guest" => GUEST, - "Reporter" => REPORTER, - "Developer" => DEVELOPER, - "Master" => MASTER - } + Gitlab::Access.options end end - def project_access_human - Project.access_options.key(self.project_access) - end - - def repo_access_human - self.class.access_roles.invert[self.project_access] + def access_field + project_access end - def skip_git? - !!@skip_git + def owner? + project.owner == user end end diff --git a/app/models/web_hook.rb b/app/models/web_hook.rb index efa27f31982..3f22b1082fb 100644 --- a/app/models/web_hook.rb +++ b/app/models/web_hook.rb @@ -28,10 +28,14 @@ class WebHook < ActiveRecord::Base WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" }) else post_url = url.gsub("#{parsed_url.userinfo}@", "") + auth = { + username: URI.decode(parsed_url.user), + password: URI.decode(parsed_url.password), + } WebHook.post(post_url, body: data.to_json, headers: {"Content-Type" => "application/json"}, - basic_auth: {username: parsed_url.user, password: parsed_url.password}) + basic_auth: auth) end end diff --git a/app/models/wiki.rb b/app/models/wiki.rb deleted file mode 100644 index 7f488ca7625..00000000000 --- a/app/models/wiki.rb +++ /dev/null @@ -1,55 +0,0 @@ -# == Schema Information -# -# Table name: wikis -# -# id :integer not null, primary key -# title :string(255) -# content :text -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# slug :string(255) -# user_id :integer -# - -class Wiki < ActiveRecord::Base - attr_accessible :title, :content, :slug - - belongs_to :project - belongs_to :user - has_many :notes, as: :noteable, dependent: :destroy - - validates :content, presence: true - validates :user, presence: true - validates :title, presence: true, length: 1..250 - - before_update :set_slug - - scope :ordered, order("created_at DESC") - - def to_param - slug - end - - class << self - def search(query) - where("title like :query OR content like :query", query: "%#{query}%") - end - end - - protected - - def self.regenerate_from wiki - regenerated_field = [:slug, :content, :title] - - new_wiki = Wiki.new - regenerated_field.each do |field| - new_wiki.send("#{field}=", wiki.send(field)) - end - new_wiki - end - - def set_slug - self.slug = self.title.parameterize - end -end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb new file mode 100644 index 00000000000..a7e6fea5ad0 --- /dev/null +++ b/app/models/wiki_page.rb @@ -0,0 +1,185 @@ +class WikiPage + include ActiveModel::Validations + include ActiveModel::Conversion + include StaticModel + extend ActiveModel::Naming + + def self.primary_key + 'slug' + end + + def self.model_name + ActiveModel::Name.new(self, nil, 'wiki') + end + + def to_key + [:slug] + end + + validates :title, presence: true + validates :content, presence: true + + # The Gitlab GollumWiki instance. + attr_reader :wiki + + # The raw Gollum::Page instance. + attr_reader :page + + # The attributes Hash used for storing and validating + # new Page values before writing to the Gollum repository. + attr_accessor :attributes + + def initialize(wiki, page = nil, persisted = false) + @wiki = wiki + @page = page + @persisted = persisted + @attributes = {}.with_indifferent_access + + set_attributes if persisted? + end + + # The escaped URL path of this page. + def slug + @attributes[:slug] + end + + alias :to_param :slug + + # The formatted title of this page. + def title + @attributes[:title] || "" + end + + # Sets the title of this page. + def title=(new_title) + @attributes[:title] = new_title + end + + # The raw content of this page. + def content + @attributes[:content] + end + + # The processed/formatted content of this page. + def formatted_content + @attributes[:formatted_content] + end + + # The markup format for the page. + def format + @attributes[:format] || :markdown + end + + # The commit message for this page version. + def message + version.try(:message) + end + + # The Gitlab Commit instance for this page. + def version + return nil unless persisted? + + @version ||= Commit.new(Gitlab::Git::Commit.new(@page.version)) + end + + # Returns an array of Gitlab Commit instances. + def versions + return [] unless persisted? + + @page.versions.map { |v| Commit.new(Gitlab::Git::Commit.new(v)) } + end + + def commit + versions.first + end + + # Returns the Date that this latest version was + # created on. + def created_at + @page.version.date + end + + # Returns boolean True or False if this instance + # is an old version of the page. + def historical? + @page.historical? + end + + # Returns boolean True or False if this instance + # has been fully saved to disk or not. + def persisted? + @persisted == true + end + + # Creates a new Wiki Page. + # + # attr - Hash of attributes to set on the new page. + # :title - The title for the new page. + # :content - The raw markup content. + # :format - Optional symbol representing the + # content format. Can be any type + # listed in the GollumWiki::MARKUPS + # Hash. + # :message - Optional commit message to set on + # the new page. + # + # Returns the String SHA1 of the newly created page + # or False if the save was unsuccessful. + def create(attr = {}) + @attributes.merge!(attr) + + save :create_page, title, content, format, message + end + + # Updates an existing Wiki Page, creating a new version. + # + # new_content - The raw markup content to replace the existing. + # format - Optional symbol representing the content format. + # See GollumWiki::MARKUPS Hash for available formats. + # message - Optional commit message to set on the new version. + # + # Returns the String SHA1 of the newly created page + # or False if the save was unsuccessful. + def update(new_content = "", format = :markdown, message = nil) + @attributes[:content] = new_content + @attributes[:format] = format + + save :update_page, @page, content, format, message + end + + # Destroys the Wiki Page. + # + # Returns boolean True or False. + def delete + if wiki.delete_page(@page) + true + else + false + end + end + + private + + def set_attributes + attributes[:slug] = @page.escaped_url_path + attributes[:title] = @page.title + attributes[:content] = @page.raw_data + attributes[:formatted_content] = @page.formatted_data + attributes[:format] = @page.format + end + + def save(method, *args) + if valid? && wiki.send(method, *args) + @page = wiki.wiki.paged(title) + + set_attributes + + @persisted = true + else + errors.add(:base, wiki.error_message) if wiki.error_message + @persisted = false + end + @persisted + end + +end diff --git a/app/observers/activity_observer.rb b/app/observers/activity_observer.rb index b568bb6b763..d754ac8185a 100644 --- a/app/observers/activity_observer.rb +++ b/app/observers/activity_observer.rb @@ -1,34 +1,39 @@ -class ActivityObserver < ActiveRecord::Observer - observe :issue, :merge_request, :note, :milestone +class ActivityObserver < BaseObserver + observe :issue, :note, :milestone def after_create(record) event_author_id = record.author_id - # Skip status notes - if record.kind_of?(Note) && record.note.include?("_Status changed to ") - return true + if record.kind_of?(Note) + # Skip system notes, like status changes and cross-references. + return true if record.system? + + # Skip wall notes to prevent spamming of dashboard + return true if record.noteable_type.blank? end if event_author_id - Event.create( - project: record.project, - target_id: record.id, - target_type: record.class.name, - action: Event.determine_action(record), - author_id: event_author_id - ) + create_event(record, Event.determine_action(record)) end end - def after_save(record) - if record.changed.include?("closed") && record.author_id_of_changes - Event.create( - project: record.project, - target_id: record.id, - target_type: record.class.name, - action: (record.closed ? Event::CLOSED : Event::REOPENED), - author_id: record.author_id_of_changes - ) - end + def after_close(record, transition) + create_event(record, Event::CLOSED) + end + + def after_reopen(record, transition) + create_event(record, Event::REOPENED) + end + + protected + + def create_event(record, status) + Event.create( + project: record.project, + target_id: record.id, + target_type: record.class.name, + action: status, + author_id: current_user.id + ) end end diff --git a/app/observers/base_observer.rb b/app/observers/base_observer.rb new file mode 100644 index 00000000000..f9a0242ce77 --- /dev/null +++ b/app/observers/base_observer.rb @@ -0,0 +1,17 @@ +class BaseObserver < ActiveRecord::Observer + def notification + NotificationService.new + end + + def log_info message + Gitlab::AppLogger.info message + end + + def current_user + Thread.current[:current_user] + end + + def current_commit + Thread.current[:current_commit] + end +end diff --git a/app/observers/issue_observer.rb b/app/observers/issue_observer.rb index 262d0f892c4..886d8b776fb 100644 --- a/app/observers/issue_observer.rb +++ b/app/observers/issue_observer.rb @@ -1,33 +1,32 @@ -class IssueObserver < ActiveRecord::Observer - cattr_accessor :current_user - +class IssueObserver < BaseObserver def after_create(issue) - if issue.assignee && issue.assignee != current_user - Notify.delay.new_issue_email(issue.id) - end + notification.new_issue(issue, current_user) + + issue.create_cross_references!(issue.project, current_user) + end + + def after_close(issue, transition) + notification.close_issue(issue, current_user) + + create_note(issue) + end + + def after_reopen(issue, transition) + create_note(issue) end def after_update(issue) - send_reassigned_email(issue) if issue.is_being_reassigned? - - status = nil - status = 'closed' if issue.is_being_closed? - status = 'reopened' if issue.is_being_reopened? - if status - Note.create_status_change_note(issue, current_user, status) - [issue.author, issue.assignee].compact.each do |recipient| - Notify.delay.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) - end + if issue.is_being_reassigned? + notification.reassigned_issue(issue, current_user) end + + issue.notice_added_references(issue.project, current_user) end protected - def send_reassigned_email(issue) - recipient_ids = [issue.assignee_id, issue.assignee_id_was].keep_if {|id| id && id != current_user.id } - - recipient_ids.each do |recipient_id| - Notify.delay.reassigned_issue_email(recipient_id, issue.id, issue.assignee_id_was) - end + # Create issue note with service comment like 'Status changed to closed' + def create_note(issue) + Note.create_status_change_note(issue, issue.project, current_user, issue.state, current_commit) end end diff --git a/app/observers/key_observer.rb b/app/observers/key_observer.rb index 664cbdfd234..c013594e787 100644 --- a/app/observers/key_observer.rb +++ b/app/observers/key_observer.rb @@ -1,12 +1,12 @@ -class KeyObserver < ActiveRecord::Observer - include Gitolited - - def after_save(key) +class KeyObserver < BaseObserver + def after_create(key) GitlabShellWorker.perform_async( :add_key, key.shell_id, key.key ) + + notification.new_key(key) end def after_destroy(key) diff --git a/app/observers/merge_request_observer.rb b/app/observers/merge_request_observer.rb index 6d3c2bdd186..d70da514cd2 100644 --- a/app/observers/merge_request_observer.rb +++ b/app/observers/merge_request_observer.rb @@ -1,31 +1,56 @@ -class MergeRequestObserver < ActiveRecord::Observer - cattr_accessor :current_user +class MergeRequestObserver < ActivityObserver + observe :merge_request def after_create(merge_request) - if merge_request.assignee && merge_request.assignee != current_user - Notify.delay.new_merge_request_email(merge_request.id) + if merge_request.author_id + create_event(merge_request, Event.determine_action(merge_request)) end + + notification.new_merge_request(merge_request, current_user) + + merge_request.create_cross_references!(merge_request.project, current_user) end - def after_update(merge_request) - send_reassigned_email(merge_request) if merge_request.is_being_reassigned? + def after_close(merge_request, transition) + create_event(merge_request, Event::CLOSED) + Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil) - status = nil - status = 'closed' if merge_request.is_being_closed? - status = 'reopened' if merge_request.is_being_reopened? - if status - Note.create_status_change_note(merge_request, current_user, status) - end + notification.close_mr(merge_request, current_user) end - protected + def after_merge(merge_request, transition) + notification.merge_mr(merge_request) + # Since MR can be merged via sidekiq + # to prevent event duplication do this check + return true if merge_request.merge_event - def send_reassigned_email(merge_request) - recipients_ids = merge_request.assignee_id_was, merge_request.assignee_id - recipients_ids.delete current_user.id + Event.create( + project: merge_request.target_project, + target_id: merge_request.id, + target_type: merge_request.class.name, + action: Event::MERGED, + author_id: merge_request.author_id_of_changes + ) + end - recipients_ids.each do |recipient_id| - Notify.delay.reassigned_merge_request_email(recipient_id, merge_request.id, merge_request.assignee_id_was) - end + def after_reopen(merge_request, transition) + create_event(merge_request, Event::REOPENED) + Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil) + end + + def after_update(merge_request) + notification.reassigned_merge_request(merge_request, current_user) if merge_request.is_being_reassigned? + + merge_request.notice_added_references(merge_request.project, current_user) + end + + def create_event(record, status) + Event.create( + project: record.target_project, + target_id: record.id, + target_type: record.class.name, + action: status, + author_id: current_user.id + ) end end diff --git a/app/observers/note_observer.rb b/app/observers/note_observer.rb index 4ee9fadf4da..d31b6e8d7ce 100644 --- a/app/observers/note_observer.rb +++ b/app/observers/note_observer.rb @@ -1,39 +1,17 @@ -class NoteObserver < ActiveRecord::Observer - +class NoteObserver < BaseObserver def after_create(note) - send_notify_mails(note) - end - - protected - - def send_notify_mails(note) - if note.notify - notify_team(note) - elsif note.notify_author - # Notify only author of resource - if note.commit_author - Notify.delay.note_commit_email(note.commit_author.id, note.id) - end - else - # Otherwise ignore it - nil - end - end - - # Notifies the whole team except the author of note - def notify_team(note) - # Note: wall posts are not "attached" to anything, so fall back to "Wall" - noteable_type = note.noteable_type.presence || "Wall" - notify_method = "note_#{noteable_type.underscore}_email".to_sym + notification.new_note(note) - if Notify.respond_to? notify_method - team_without_note_author(note).map do |u| - Notify.delay.send(notify_method, u.id, note.id) + unless note.system? + # Create a cross-reference note if this Note contains GFM that names an + # issue, merge request, or commit. + note.references.each do |mentioned| + Note.create_cross_reference_note(mentioned, note.noteable, note.author, note.project) end end end - def team_without_note_author(note) - note.project.users.reject { |u| u.id == note.author.id } + def after_update(note) + note.notice_added_references(note.project, current_user) end end diff --git a/app/observers/project_activity_cache_observer.rb b/app/observers/project_activity_cache_observer.rb new file mode 100644 index 00000000000..96ced90db80 --- /dev/null +++ b/app/observers/project_activity_cache_observer.rb @@ -0,0 +1,8 @@ +class ProjectActivityCacheObserver < BaseObserver + observe :event + + def after_create(event) + event.project.update_column(:last_activity_at, event.created_at) if event.project + end +end + diff --git a/app/observers/project_observer.rb b/app/observers/project_observer.rb index 4b1f8295dc2..acbb719b69a 100644 --- a/app/observers/project_observer.rb +++ b/app/observers/project_observer.rb @@ -1,15 +1,34 @@ -class ProjectObserver < ActiveRecord::Observer +class ProjectObserver < BaseObserver def after_create(project) - GitlabShellWorker.perform_async( - :add_repository, - project.path_with_namespace - ) + project.update_column(:last_activity_at, project.created_at) + + return true if project.forked? - log_info("#{project.owner.name} created a new project \"#{project.name_with_namespace}\"") + if project.import? + RepositoryImportWorker.perform_in(5.seconds, project.id) + else + GitlabShellWorker.perform_async( + :add_repository, + project.path_with_namespace + ) + + log_info("#{project.owner.name} created a new project \"#{project.name_with_namespace}\"") + end end def after_update(project) project.send_move_instructions if project.namespace_id_changed? + project.rename_repo if project.path_changed? + + GitlabShellWorker.perform_async( + :update_repository_head, + project.path_with_namespace, + project.default_branch + ) if project.default_branch_changed? + end + + def before_destroy(project) + project.repository.expire_cache unless project.empty_repo? end def after_destroy(project) @@ -18,14 +37,13 @@ class ProjectObserver < ActiveRecord::Observer project.path_with_namespace ) + GitlabShellWorker.perform_async( + :remove_repository, + project.path_with_namespace + ".wiki" + ) + project.satellite.destroy log_info("Project \"#{project.name}\" was removed") end - - protected - - def log_info message - Gitlab::AppLogger.info message - end end diff --git a/app/observers/system_hook_observer.rb b/app/observers/system_hook_observer.rb index 312cd2b3622..3a649fd590d 100644 --- a/app/observers/system_hook_observer.rb +++ b/app/observers/system_hook_observer.rb @@ -1,67 +1,11 @@ -class SystemHookObserver < ActiveRecord::Observer +class SystemHookObserver < BaseObserver observe :user, :project, :users_project - - def after_create(model) - if model.kind_of? Project - SystemHook.all_hooks_fire({ - event_name: "project_create", - name: model.name, - path: model.path, - project_id: model.id, - owner_name: model.owner.name, - owner_email: model.owner.email, - created_at: model.created_at - }) - elsif model.kind_of? User - SystemHook.all_hooks_fire({ - event_name: "user_create", - name: model.name, - email: model.email, - created_at: model.created_at - }) - - elsif model.kind_of? UsersProject - SystemHook.all_hooks_fire({ - event_name: "user_add_to_team", - project_name: model.project.name, - project_path: model.project.path, - project_id: model.project_id, - user_name: model.user.name, - user_email: model.user.email, - project_access: model.repo_access_human, - created_at: model.created_at - }) - end + def after_create(model) + SystemHooksService.execute_hooks_for(model, :create) end def after_destroy(model) - if model.kind_of? Project - SystemHook.all_hooks_fire({ - event_name: "project_destroy", - name: model.name, - path: model.path, - project_id: model.id, - owner_name: model.owner.name, - owner_email: model.owner.email, - }) - elsif model.kind_of? User - SystemHook.all_hooks_fire({ - event_name: "user_destroy", - name: model.name, - email: model.email - }) - - elsif model.kind_of? UsersProject - SystemHook.all_hooks_fire({ - event_name: "user_remove_from_team", - project_name: model.project.name, - project_path: model.project.path, - project_id: model.project_id, - user_name: model.user.name, - user_email: model.user.email, - project_access: model.repo_access_human - }) - end + SystemHooksService.execute_hooks_for(model, :destroy) end end diff --git a/app/observers/user_observer.rb b/app/observers/user_observer.rb index c1179ed7881..fba0f1006d9 100644 --- a/app/observers/user_observer.rb +++ b/app/observers/user_observer.rb @@ -1,8 +1,8 @@ -class UserObserver < ActiveRecord::Observer +class UserObserver < BaseObserver def after_create(user) log_info("User \"#{user.name}\" (#{user.email}) was created") - Notify.delay.new_user_email(user.id, user.password) + notification.new_user(user) end def after_destroy user @@ -10,18 +10,11 @@ class UserObserver < ActiveRecord::Observer end def after_save user + # Ensure user has namespace + user.create_namespace!(path: user.username, name: user.username) unless user.namespace + if user.username_changed? - if user.namespace - user.namespace.update_attributes(path: user.username) - else - user.create_namespace!(path: user.username, name: user.username) - end + user.namespace.update_attributes(path: user.username, name: user.username) end end - - protected - - def log_info message - Gitlab::AppLogger.info message - end end diff --git a/app/observers/users_group_observer.rb b/app/observers/users_group_observer.rb new file mode 100644 index 00000000000..ecdbede89d9 --- /dev/null +++ b/app/observers/users_group_observer.rb @@ -0,0 +1,9 @@ +class UsersGroupObserver < BaseObserver + def after_create(membership) + notification.new_group_member(membership) + end + + def after_update(membership) + notification.update_group_member(membership) + end +end diff --git a/app/observers/users_project_observer.rb b/app/observers/users_project_observer.rb index 66b421753f0..ca9649c76ab 100644 --- a/app/observers/users_project_observer.rb +++ b/app/observers/users_project_observer.rb @@ -1,7 +1,6 @@ -class UsersProjectObserver < ActiveRecord::Observer +class UsersProjectObserver < BaseObserver def after_commit(users_project) return if users_project.destroyed? - Notify.delay.project_access_granted_email(users_project.id) end def after_create(users_project) @@ -10,6 +9,12 @@ class UsersProjectObserver < ActiveRecord::Observer action: Event::JOINED, author_id: users_project.user.id ) + + notification.new_team_member(users_project) + end + + def after_update(users_project) + notification.update_team_member(users_project) end def after_destroy(users_project) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb new file mode 100644 index 00000000000..f9d43e60de6 --- /dev/null +++ b/app/services/git_push_service.rb @@ -0,0 +1,194 @@ +class GitPushService + attr_accessor :project, :user, :push_data, :push_commits + + # This method will be called after each git update + # and only if the provided user and project is present in GitLab. + # + # All callbacks for post receive action should be placed here. + # + # Next, this method: + # 1. Creates the push event + # 2. Ensures that the project satellite exists + # 3. Updates merge requests + # 4. Recognizes cross-references from commit messages + # 5. Executes the project's web hooks + # 6. Executes the project's services + # + def execute(project, user, oldrev, newrev, ref) + @project, @user = project, user + + # Collect data for this git push + @push_commits = project.repository.commits_between(oldrev, newrev) + @push_data = post_receive_data(oldrev, newrev, ref) + + create_push_event + + project.ensure_satellite_exists + project.discover_default_branch + project.repository.expire_cache + + if push_to_existing_branch?(ref, oldrev) + project.update_merge_requests(oldrev, newrev, ref, @user) + process_commit_messages(ref) + end + + if push_to_branch?(ref) + project.execute_hooks(@push_data.dup) + project.execute_services(@push_data.dup) + end + + if push_to_new_branch?(ref, oldrev) + # Re-find the pushed commits. + if is_default_branch?(ref) + # Initial push to the default branch. Take the full history of that branch as "newly pushed". + @push_commits = project.repository.commits(newrev) + else + # Use the pushed commits that aren't reachable by the default branch + # as a heuristic. This may include more commits than are actually pushed, but + # that shouldn't matter because we check for existing cross-references later. + @push_commits = project.repository.commits_between(project.default_branch, newrev) + end + + process_commit_messages(ref) + end + end + + # This method provide a sample data + # generated with post_receive_data method + # for given project + # + def sample_data(project, user) + @project, @user = project, user + @push_commits = project.repository.commits(project.default_branch, nil, 3) + post_receive_data(@push_commits.last.id, @push_commits.first.id, "refs/heads/#{project.default_branch}") + end + + protected + + def create_push_event + Event.create!( + project: project, + action: Event::PUSHED, + data: push_data, + author_id: push_data[:user_id] + ) + end + + # Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched, + # close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables. + def process_commit_messages ref + is_default_branch = is_default_branch?(ref) + + @push_commits.each do |commit| + # Close issues if these commits were pushed to the project's default branch and the commit message matches the + # 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 + Thread.current[:current_user] = author + Thread.current[:current_commit] = commit + + issues_to_close.each { |i| i.close && i.save } + end + + # Create cross-reference notes for any other references. Omit any issues that were referenced in an + # issue-closing phrase, or have already been mentioned from this commit (probably from this commit + # 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) + end + end + end + + # Produce a hash of post-receive data + # + # data = { + # before: String, + # after: String, + # ref: String, + # user_id: String, + # user_name: String, + # repository: { + # name: String, + # url: String, + # description: String, + # homepage: String, + # }, + # commits: Array, + # total_commits_count: Fixnum + # } + # + def post_receive_data(oldrev, newrev, ref) + # Total commits count + push_commits_count = push_commits.size + + # Get latest 20 commits ASC + push_commits_limited = push_commits.last(20) + + # Hash to be passed as post_receive_data + data = { + before: oldrev, + after: newrev, + ref: ref, + user_id: user.id, + user_name: user.name, + repository: { + name: project.name, + url: project.url_to_repo, + description: project.description, + homepage: project.web_url, + }, + commits: [], + total_commits_count: push_commits_count + } + + # For performance purposes maximum 20 latest commits + # will be passed as post receive hook data. + # + push_commits_limited.each do |commit| + data[:commits] << { + id: commit.id, + message: commit.safe_message, + timestamp: commit.committed_date.xmlschema, + url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/#{commit.id}", + author: { + name: commit.author_name, + email: commit.author_email + } + } + end + + data + end + + def push_to_existing_branch? ref, oldrev + ref_parts = ref.split('/') + + # Return if this is not a push to a branch (e.g. new commits) + ref_parts[1] =~ /heads/ && oldrev != "0000000000000000000000000000000000000000" + end + + def push_to_new_branch? ref, oldrev + ref_parts = ref.split('/') + + ref_parts[1] =~ /heads/ && oldrev == "0000000000000000000000000000000000000000" + end + + def push_to_branch? ref + ref =~ /refs\/heads/ + end + + def is_default_branch? ref + ref == "refs/heads/#{project.default_branch}" + end + + def commit_user commit + User.where(email: commit.author_email).first || + User.where(name: commit.author_name).first || + user + end +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb new file mode 100644 index 00000000000..750a71aea6b --- /dev/null +++ b/app/services/notification_service.rb @@ -0,0 +1,251 @@ +# NotificationService class +# +# Used for notifying users with emails about different events +# +# Ex. +# NotificationService.new.new_issue(issue, current_user) +# +class NotificationService + # Always notify user about ssh key added + # only if ssh key is not deploy key + # + # This is security email so it will be sent + # even if user disabled notifications + def new_key(key) + if key.user + mailer.new_ssh_key_email(key.id) + end + end + + # When create an issue we should send next emails: + # + # * issue assignee if his notification level is not Disabled + # * project team members with notification level higher then Participating + # + def new_issue(issue, current_user) + new_resource_email(issue, issue.project, 'new_issue_email') + end + + # When we close an issue we should send next emails: + # + # * issue author if his notification level is not Disabled + # * issue assignee if his notification level is not Disabled + # * project team members with notification level higher then Participating + # + def close_issue(issue, current_user) + close_resource_email(issue, issue.project, current_user, 'closed_issue_email') + end + + # When we reassign an issue we should send next emails: + # + # * issue old assignee if his notification level is not Disabled + # * issue new assignee if his notification level is not Disabled + # + def reassigned_issue(issue, current_user) + reassign_resource_email(issue, issue.project, current_user, 'reassigned_issue_email') + end + + + # When create a merge request we should send next emails: + # + # * mr assignee if his notification level is not Disabled + # + def new_merge_request(merge_request, current_user) + new_resource_email(merge_request, merge_request.target_project, 'new_merge_request_email') + end + + # When we reassign a merge_request we should send next emails: + # + # * merge_request old assignee if his notification level is not Disabled + # * merge_request assignee if his notification level is not Disabled + # + def reassigned_merge_request(merge_request, current_user) + reassign_resource_email(merge_request, merge_request.target_project, current_user, 'reassigned_merge_request_email') + end + + # When we close a merge request we should send next emails: + # + # * merge_request author if his notification level is not Disabled + # * merge_request assignee if his notification level is not Disabled + # * project team members with notification level higher then Participating + # + def close_mr(merge_request, current_user) + close_resource_email(merge_request, merge_request.target_project, current_user, 'closed_merge_request_email') + end + + # When we merge a merge request we should send next emails: + # + # * merge_request author if his notification level is not Disabled + # * merge_request assignee if his notification level is not Disabled + # * project team members with notification level higher then Participating + # + def merge_mr(merge_request) + recipients = reject_muted_users([merge_request.author, merge_request.assignee], merge_request.target_project) + recipients = recipients.concat(project_watchers(merge_request.target_project)).uniq + + recipients.each do |recipient| + mailer.merged_merge_request_email(recipient.id, merge_request.id) + end + end + + # Notify new user with email after creation + def new_user(user) + # Don't email omniauth created users + mailer.new_user_email(user.id, user.password) unless user.extern_uid? + end + + # Notify users on new note in system + # + # TODO: split on methods and refactor + # + def new_note(note) + # ignore wall messages + return true unless note.noteable_type.present? + + # ignore gitlab service messages + return true if note.note =~ /\A_Status changed to closed_/ + + opts = { noteable_type: note.noteable_type, project_id: note.project_id } + + target = note.noteable + if target.respond_to?(:participants) + recipients = target.participants + else + recipients = note.mentioned_users + end + + if note.commit_id.present? + opts.merge!(commit_id: note.commit_id) + recipients << note.commit_author + else + opts.merge!(noteable_id: note.noteable_id) + end + + # Get users who left comment in thread + recipients = recipients.concat(User.where(id: Note.where(opts).pluck(:author_id))) + + # Merge project watchers + recipients = recipients.concat(project_watchers(note.project)).compact.uniq + + # Reject mutes users + recipients = reject_muted_users(recipients, note.project) + + # Reject author + recipients.delete(note.author) + + # build notify method like 'note_commit_email' + notify_method = "note_#{note.noteable_type.underscore}_email".to_sym + + recipients.each do |recipient| + mailer.send(notify_method, recipient.id, note.id) + end + end + + def new_team_member(users_project) + mailer.project_access_granted_email(users_project.id) + end + + def update_team_member(users_project) + mailer.project_access_granted_email(users_project.id) + end + + def new_group_member(users_group) + mailer.group_access_granted_email(users_group.id) + end + + def update_group_member(users_group) + mailer.group_access_granted_email(users_group.id) + end + + protected + + # Get project users with WATCH notification level + def project_watchers(project) + project_watchers = [] + member_methods = { project => :users_projects } + member_methods.merge!(project.group => :users_groups) if project.group + + member_methods.each do |object, member_method| + # Get project notification settings since it has higher priority + user_ids = object.send(member_method).where(notification_level: Notification::N_WATCH).pluck(:user_id) + project_watchers += User.where(id: user_ids) + + # next collect users who use global settings with watch state + user_ids = object.send(member_method).where(notification_level: Notification::N_GLOBAL).pluck(:user_id) + project_watchers += User.where(id: user_ids, notification_level: Notification::N_WATCH) + end + + project_watchers.uniq + end + + # Remove users with disabled notifications from array + # Also remove duplications and nil recipients + def reject_muted_users(users, project = nil) + users = users.compact.uniq + + users.reject do |user| + next user.notification.disabled? unless project + + tm = project.users_projects.find_by_user_id(user.id) + + if !tm && project.group + tm = project.group.users_groups.find_by_user_id(user.id) + end + + # reject users who globally disabled notification and has no membership + next user.notification.disabled? unless tm + + # reject users who disabled notification in project + next true if tm.notification.disabled? + + # reject users who have N_GLOBAL in project and disabled in global settings + tm.notification.global? && user.notification.disabled? + end + end + + def new_resource_email(target, project, method) + if target.respond_to?(:participants) + recipients = target.participants + else + recipients = [] + end + recipients = reject_muted_users(recipients, project) + recipients = recipients.concat(project_watchers(project)).uniq + recipients.delete(target.author) + + recipients.each do |recipient| + mailer.send(method, recipient.id, target.id) + end + end + + def close_resource_email(target, project, current_user, method) + recipients = reject_muted_users([target.author, target.assignee], project) + recipients = recipients.concat(project_watchers(project)).uniq + recipients.delete(current_user) + + recipients.each do |recipient| + mailer.send(method, recipient.id, target.id, current_user.id) + end + end + + def reassign_resource_email(target, project, current_user, method) + recipients = User.where(id: [target.assignee_id, target.assignee_id_was]) + + # Add watchers to email list + recipients = recipients.concat(project_watchers(project)) + + # reject users with disabled notifications + recipients = reject_muted_users(recipients, project) + + # Reject me from recipients if I reassign an item + recipients.delete(current_user) + + recipients.each do |recipient| + mailer.send(method, recipient.id, target.id, target.assignee_id_was) + end + end + + def mailer + Notify.delay + end +end diff --git a/app/services/project_transfer_service.rb b/app/services/project_transfer_service.rb new file mode 100644 index 00000000000..7150c1c78c0 --- /dev/null +++ b/app/services/project_transfer_service.rb @@ -0,0 +1,42 @@ +# ProjectTransferService class +# +# Used for transfer project to another namespace +# +class ProjectTransferService + include Gitlab::ShellAdapter + + class TransferError < StandardError; end + + attr_accessor :project + + def transfer(project, new_namespace) + Project.transaction do + old_path = project.path_with_namespace + new_path = File.join(new_namespace.try(:path) || '', project.path) + + if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present? + raise TransferError.new("Project with same path in target namespace already exists") + end + + project.namespace = new_namespace + project.save! + + # Move main repository + unless gitlab_shell.mv_repository(old_path, new_path) + raise TransferError.new('Cannot move project') + end + + # Move wiki repo also if present + gitlab_shell.mv_repository("#{old_path}.wiki", "#{new_path}.wiki") + + # create satellite repo + project.ensure_satellite_exists + + # clear project cached events + project.reset_events_cache + + true + end + end +end + diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb new file mode 100644 index 00000000000..39aec943f75 --- /dev/null +++ b/app/services/system_hooks_service.rb @@ -0,0 +1,62 @@ +class SystemHooksService + def self.execute_hooks_for(model, event) + execute_hooks(build_event_data(model, event)) + end + + private + + def self.execute_hooks(data) + SystemHook.all.each do |sh| + async_execute_hook sh, data + end + end + + def self.async_execute_hook(hook, data) + Sidekiq::Client.enqueue(SystemHookWorker, hook.id, data) + end + + def self.build_event_data(model, event) + data = { + event_name: build_event_name(model, event), + created_at: model.created_at + } + + case model + when Project + owner = model.owner + + data.merge!({ + name: model.name, + path: model.path, + path_with_namespace: model.path_with_namespace, + project_id: model.id, + owner_name: owner.name, + owner_email: owner.respond_to?(:email) ? owner.email : nil + }) + when User + data.merge!({ + name: model.name, + email: model.email + }) + when UsersProject + data.merge!({ + project_name: model.project.name, + project_path: model.project.path, + project_id: model.project_id, + user_name: model.user.name, + user_email: model.user.email, + project_access: model.human_access + }) + end + end + + def self.build_event_name(model, event) + case model + when UsersProject + return "user_add_to_team" if event == :create + return "user_remove_from_team" if event == :destroy + else + "#{model.class.name.downcase}_#{event.to_s}" + end + end +end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index 3dbf2860bd4..98794c9470b 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -8,15 +8,23 @@ class AttachmentUploader < CarrierWave::Uploader::Base end def image? - img_ext = %w(png jpg jpeg) + img_ext = %w(png jpg jpeg gif bmp tiff) if file.respond_to?(:extension) - img_ext.include?(file.extension) + img_ext.include?(file.extension.downcase) else # Not all CarrierWave storages respond to :extension - ext = file.path.split('.').last + ext = file.path.split('.').last.downcase img_ext.include?(ext) end rescue false end + + def secure_url + Gitlab.config.gitlab.relative_url_root + "/files/#{model.class.to_s.underscore}/#{model.id}/#{file.filename}" + end + + def file_storage? + self.class.storage == CarrierWave::Storage::File + end end diff --git a/app/views/admin/resque/show.html.haml b/app/views/admin/background_jobs/show.html.haml index 499738f9a06..2d4ffc10d5f 100644 --- a/app/views/admin/resque/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -1,4 +1,4 @@ -%h3.page_title Background Jobs +%h3.page-title Background Jobs %br .ui-box %iframe{src: sidekiq_path, width: '100%', height: 900, style: "border: none"} diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 46a876294ce..9ed788d0d9d 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -1,7 +1,7 @@ .admin_dash.row .span4 .ui-box - %h5.title Projects + .title Projects .data.padded = link_to admin_projects_path do %h1= Project.count @@ -9,20 +9,20 @@ = link_to 'New Project', new_project_path, class: "btn btn-small" .span4 .ui-box - %h5.title Groups + .title Users .data.padded - = link_to admin_groups_path do - %h1= Group.count + = link_to admin_users_path do + %h1= User.count %hr - = link_to 'New Group', new_admin_group_path, class: "btn btn-small" + = link_to 'New User', new_admin_user_path, class: "btn btn-small" .span4 .ui-box - %h5.title Users + .title Groups .data.padded - = link_to admin_users_path do - %h1= User.count + = link_to admin_groups_path do + %h1= Group.count %hr - = link_to 'New User', new_admin_user_path, class: "btn btn-small" + = link_to 'New Group', new_admin_group_path, class: "btn btn-small" .row .span4 @@ -50,6 +50,10 @@ %h4 Stats %hr %p + Forks + %span.light.pull-right + = ForkedProjectLink.count + %p Issues %span.light.pull-right = Issue.count diff --git a/app/views/admin/groups/edit.html.haml b/app/views/admin/groups/edit.html.haml index dce044956c3..8ed463a8191 100644 --- a/app/views/admin/groups/edit.html.haml +++ b/app/views/admin/groups/edit.html.haml @@ -1,22 +1,25 @@ -%h3.page_title Rename Group +%h3.page-title Edit Group %hr = form_for [:admin, @group] do |f| - if @group.errors.any? .alert.alert-error %span= @group.errors.full_messages.first - .clearfix.group_name_holder + .control-group.group_name_holder = f.label :name do Group name is - .input - = f.text_field :name, placeholder: "Example Group", class: "xxlarge" + .controls + = f.text_field :name, placeholder: "Example Group", class: "input-xxlarge" + .control-group.group-description-holder + = f.label :description, "Details" + .controls + = f.text_area :description, maxlength: 250, class: "input-xxlarge js-gfm-input", rows: 4 - - .clearfix.group_name_holder + .control-group.group_name_holder = f.label :path do %span.cred Group path is - .input - = f.text_field :path, placeholder: "example-group", class: "xxlarge danger" + .controls + = f.text_field :path, placeholder: "example-group", class: "input-xxlarge danger" %ul.cred %li Changing group path can have unintended side effects. %li Renaming group path will rename directory for all related projects @@ -24,5 +27,5 @@ %li It will change the git path to repositories under this group. .form-actions - = f.submit 'Rename group', class: "btn btn-remove" + = f.submit 'Save changes', class: "btn btn-primary" = 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 25ce66575bf..c9d7c24f204 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -1,35 +1,45 @@ -%h3.page_title - Groups +%h3.page-title + Groups (#{@groups.total_count}) %small allows you to keep projects organized. Use groups for uniting related projects. - = link_to 'New Group', new_admin_group_path, class: "btn btn-small pull-right" + = link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right" %br = form_tag admin_groups_path, method: :get, class: 'form-inline' do - = text_field_tag :name, params[:name], class: "xlarge" + = text_field_tag :name, params[:name], class: "span6" = submit_tag "Search", class: "btn submit btn-primary" -%table - %thead - %tr - %th - Name - %i.icon-sort-down - %th Path - %th Projects - %th Owner - %th.cred Danger Zone! +%hr +%ul.bordered-list - @groups.each do |group| - %tr - %td - %strong= link_to group.name, [:admin, group] - %td= group.path - %td= group.projects.count - %td - = link_to group.owner_name, admin_user_path(group.owner_id) - %td.bgred - = link_to 'Rename', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn btn-small" - = link_to 'Destroy', [:admin, group], confirm: "REMOVE #{group.name}? Are you sure?", method: :delete, class: "btn btn-small btn-remove" -= paginate @groups, theme: "admin" + %li + .clearfix + .pull-right.prepend-top-10 + = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn btn-small" + = link_to 'Destroy', [:admin, group], confirm: "REMOVE #{group.name}? Are you sure?", method: :delete, class: "btn btn-small btn-remove" + + %h4 + = link_to [:admin, group] do + = group.name + + → + %span.monospace + %i.icon-folder-close + %strong #{group.path}/ + + .clearfix.light.append-bottom-10 + %span + %b Members: + %span.badge= group.members.size + \| + %span + %b Projects: + %span.badge= group.projects.count + + .clearfix + %p + = truncate group.description, length: 150 + += paginate @groups, theme: "gitlab" diff --git a/app/views/admin/groups/new.html.haml b/app/views/admin/groups/new.html.haml index 60c6fa5ad09..0ae35eb6b43 100644 --- a/app/views/admin/groups/new.html.haml +++ b/app/views/admin/groups/new.html.haml @@ -1,21 +1,27 @@ -%h3.page_title New Group +%h3.page-title New Group %hr = form_for [:admin, @group] do |f| - if @group.errors.any? .alert.alert-error %span= @group.errors.full_messages.first - .clearfix + .control-group = f.label :name do Group name is - .input - = f.text_field :name, placeholder: "Ex. OpenSource", class: "xxlarge left" - - = f.submit 'Create group', class: "btn btn-primary" + .controls + = f.text_field :name, placeholder: "Ex. OpenSource", class: "input-xxlarge left" + .control-group.group-description-holder + = f.label :description, "Details" + .controls + = f.text_area :description, maxlength: 250, class: "input-xxlarge js-gfm-input", rows: 4 + + .form-actions + = f.submit 'Create group', class: "btn btn-create" + %hr .padded %ul %li Group is kind of directory for several projects %li All created groups are private %li People within a group see only projects they have access to - %li All projects of group will be stored in group directory + %li All projects of group will be stored in a group directory %li You will be able to move existing projects into group diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 90f8fc0f814..1566c345809 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -1,127 +1,76 @@ -%h3.page_title +%h3.page-title Group: #{@group.name} -%br -%table.zebra-striped - %thead - %tr - %th Group - %th - %tr - %td - %b - Name: - %td - = @group.name - - = link_to edit_admin_group_path(@group), class: "btn btn-small pull-right" do - %i.icon-edit - Rename - %tr - %td - %b - Path: - %td - %span.monospace= File.join(Gitlab.config.gitlab_shell.repos_path, @group.path) - %tr - %td - %b - Owner: - %td - = @group.owner_name - .pull-right - = link_to "#", class: "btn btn-small change-owner-link" do - %i.icon-edit - Change owner + = link_to edit_admin_group_path(@group), class: "btn btn-small pull-right" do + %i.icon-edit + Edit +%hr +.row + .span6 + .ui-box + .title + Group info: + %ul.well-list + %li + %span.light Name: + %strong= @group.name + %li + %span.light Path: + %strong + = @group.path - %tr.change-owner-holder.hide - %td.bgred - %b.cred - New Owner: - %td.bgred - = form_for [:admin, @group] do |f| - = f.select :owner_id, User.all.map { |user| [user.name, user.id] }, {}, {class: 'chosen'} - %div - = f.submit 'Change Owner', class: "btn btn-remove" - = link_to "Cancel", "#", class: "btn change-owner-cancel-link" + %li + %span.light Description: + %strong + = @group.description -- if @group.projects.any? - %fieldset - %legend Projects (#{@group.projects.count}) - %table - %thead - %tr - %th Project name - %th Path - %th Users - %th.cred Danger Zone! - - @group.projects.each do |project| - %tr - %td - = link_to project.name_with_namespace, [:admin, project] - %td - %span.monospace= project.path_with_namespace + ".git" - %td= project.users.count - %td.bgred - = link_to 'Transfer project to global namespace', remove_project_admin_group_path(@group, project_id: project.id), confirm: 'Remove project from group and move to global namespace. Are you sure?', method: :delete, class: "btn btn-remove small" + %li + %span.light Created at: + %strong + = @group.created_at.stamp("March 1, 1999") - = form_tag project_teams_update_admin_group_path(@group), id: "new_team_member", class: "bulk_import", method: :put do - %table.zebra-striped - %thead - %tr - %th Users - %th Project Access: - - - @group.users.each do |user| - - next unless user - %tr{class: "user_#{user.id}"} - %td.name= link_to user.name, admin_user_path(user) - %td.projects_access - - user.authorized_projects.in_namespace(@group).each do |project| - - u_p = user.users_projects.in_project(project).first - - next unless u_p - %span - = project.name_with_namespace - = link_to "(#{ u_p.project_access_human })", edit_admin_project_member_path(project, user) - %tr - %td.input= select_tag :user_ids, options_from_collection_for_select(@users , :id, :name), multiple: true, data: {placeholder: 'Select users'}, class: 'chosen span5' - %td= select_tag :project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3"} + .ui-box + .title + Projects + %small + (#{@group.projects.count}) + %ul.well-list + - @group.projects.sort_by(&:name).each do |project| + %li + %strong + = link_to project.name_with_namespace, [:admin, project] + %span.pull-right.light + %span.monospace= project.path_with_namespace + ".git" - %tr - %td= submit_tag 'Add user to projects in group', class: "btn btn-primary" - %td + .span6 + .ui-box + .title + Add user(s) to the group: + .ui-box-body.form-holder + %p.light Read more about project permissions %strong= link_to "here", help_permissions_path, class: "vlink" -- else - %fieldset - %legend Group is empty - -= form_tag project_update_admin_group_path(@group), class: "bulk_import", method: :put do - %fieldset - %legend Move projects to group - .alert - You can move only projects with existing repos - %br - Group projects will be moved in group directory and will not be accessible by old path - .clearfix - = label_tag :project_ids do - Projects - .input - = select_tag :project_ids, options_from_collection_for_select(@projects , :id, :name_with_namespace), multiple: true, data: {placeholder: 'Select projects'}, class: 'chosen span5' - .form-actions - = submit_tag 'Add', class: "btn btn-primary" - -:javascript - $(function(){ - var modal = $('.change-owner-holder'); - $('.change-owner-link').bind("click", function(){ - $(this).hide(); - modal.show(); - }); - $('.change-owner-cancel-link').bind("click", function(){ - modal.hide(); - $('.change-owner-link').show(); - }) - }) - + = form_tag project_teams_update_admin_group_path(@group), id: "new_team_member", class: "bulk_import", method: :put do + %div + = users_select_tag(:user_ids, multiple: true) + %div.prepend-top-10 + = select_tag :group_access, options_for_select(UsersGroup.group_access_roles), class: "project-access-select chosen" + %hr + = submit_tag 'Add users into group', class: "btn btn-create" + .ui-box + .title + %strong #{@group.name} + Group Members + %small + (#{@group.users_groups.count}) + %ul.well-list.group-users-list + - @group.users_groups.order('group_access DESC').each do |member| + - user = member.user + %li{class: dom_class(user)} + %strong + = link_to user.name, admin_user_path(user) + %span.pull-right.light + = member.human_access + = link_to group_users_group_path(@group, member), 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.icon-minus.icon-white diff --git a/app/views/admin/hooks/_data_ex.html.erb b/app/views/admin/hooks/_data_ex.html.erb index 652ee5aa56f..b69aa92716d 100644 --- a/app/views/admin/hooks/_data_ex.html.erb +++ b/app/views/admin/hooks/_data_ex.html.erb @@ -1,23 +1,26 @@ <% data_ex_str = <<eos 1. Project created: { - "created_at": "2012-07-21T07:30:54Z", - "event_name": "project_create", - "name": "StoreCloud", - "owner_email": "johnsmith@gmail.com", - "owner_name": "John Smith", - "path": "storecloud", - "project_id": 74 + "created_at": "2012-07-21T07:30:54Z", + "event_name": "project_create", + "name": "StoreCloud", + "owner_email": "johnsmith@gmail.com" + "owner_name": "John Smit", + "path": "stormcloud", + "path_with_namespace": "jsmith/stormcloud", + "project_id": 74, } 2. Project destroyed: { - "event_name": "project_destroy", - "name": "Underscore", - "owner_email": "johnsmith@gmail.com", - "owner_name": "John Smith", - "path": "underscore", - "project_id": 73 + "created_at": "2012-07-21T07:30:58Z", + "event_name": "project_destroy", + "name": "Underscore", + "owner_email": "johnsmith@gmail.com" + "owner_name": "John Smith", + "path": "underscore", + "path_with_namespace": "jsmith/underscore", + "project_id": 73, } 3. New Team Member: @@ -28,8 +31,8 @@ "project_id": 74, "project_name": "StoreCloud", "project_path": "storecloud", - "owner_email": "johnsmith@gmail.com", - "owner_name": "John Smith", + "user_email": "johnsmith@gmail.com", + "user_name": "John Smith", } 4. Team Member Removed: @@ -40,8 +43,8 @@ "project_id": 74, "project_name": "StoreCloud", "project_path": "storecloud", - "owner_email": "johnsmith@gmail.com", - "owner_name": "John Smith", + "user_email": "johnsmith@gmail.com", + "user_name": "John Smith", } 5. User created: diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index acbf7a108b8..eb6570af30e 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -1,6 +1,6 @@ .alert.alert-info %span - Post receive hooks for binding events. + Post-receive hooks for binding events. %br Read more about system hooks %strong #{link_to "here", help_system_hooks_path, class: "vlink"} @@ -10,12 +10,12 @@ .alert.alert-error - @hook.errors.full_messages.each do |msg| %p= msg - .clearfix + .control-group = f.label :url, "URL:" - .input - = f.text_field :url, class: "text_field xxlarge" + .controls + = f.text_field :url, class: "text_field input-xxlarge input-xpadding" - = f.submit "Add System Hook", class: "btn btn-primary" + = f.submit "Add System Hook", class: "btn btn-create" %hr -if @hooks.any? diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 9ddd781c6ec..cce8aeb02c7 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -8,60 +8,60 @@ %li = link_to "sidekiq.log", "#sidekiq", 'data-toggle' => 'tab' -%p.light To prevent perfomance issues admin logs output the last 2000 lines +%p.light To prevent performance issues admin logs output the last 2000 lines .tab-content .tab-pane.active#githost - .file_holder#README - .file_title + .file-holder#README + .file-title %i.icon-file githost.log .pull-right = link_to '#', class: 'log-bottom' do %i.icon-arrow-down Scroll down - .file_content.logs + .file-content.logs %ol - Gitlab::GitLogger.read_latest.each do |line| %li %p= line .tab-pane#application - .file_holder#README - .file_title + .file-holder#README + .file-title %i.icon-file application.log .pull-right = link_to '#', class: 'log-bottom' do %i.icon-arrow-down Scroll down - .file_content.logs + .file-content.logs %ol - Gitlab::AppLogger.read_latest.each do |line| %li %p= line .tab-pane#production - .file_holder#README - .file_title + .file-holder#README + .file-title %i.icon-file production.log .pull-right = link_to '#', class: 'log-bottom' do %i.icon-arrow-down Scroll down - .file_content.logs + .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 + .file-holder#README + .file-title %i.icon-file sidekiq.log .pull-right = link_to '#', class: 'log-bottom' do %i.icon-arrow-down Scroll down - .file_content.logs + .file-content.logs %ol - Gitlab::Logger.read_latest_for('sidekiq.log').each do |line| %li diff --git a/app/views/admin/projects/_form.html.haml b/app/views/admin/projects/_form.html.haml deleted file mode 100644 index ebf69924a25..00000000000 --- a/app/views/admin/projects/_form.html.haml +++ /dev/null @@ -1,77 +0,0 @@ -= form_for [:admin, project] do |f| - -if project.errors.any? - .alert.alert-error - %ul - - project.errors.full_messages.each do |msg| - %li= msg - - .clearfix.project_name_holder - = f.label :name do - Project name is - .input - = f.text_field :name, placeholder: "Example Project", class: "xxlarge" - - - if project.repo_exists? - %fieldset.adv_settings - %legend Advanced settings: - .clearfix - = f.label :path do - Path - .input - = text_field_tag :ppath, @project.repository.path_to_repo, class: "xlarge", disabled: true - - .clearfix - = f.label :default_branch, "Default Branch" - .input= f.select(:default_branch, @project.repository.heads.map(&:name), {}, style: "width:210px;") - - %fieldset.adv_settings - %legend Features: - - .clearfix - = f.label :issues_enabled, "Issues" - .input= f.check_box :issues_enabled - - .clearfix - = f.label :merge_requests_enabled, "Merge Requests" - .input= f.check_box :merge_requests_enabled - - .clearfix - = f.label :wall_enabled, "Wall" - .input= f.check_box :wall_enabled - - .clearfix - = f.label :wiki_enabled, "Wiki" - .input= f.check_box :wiki_enabled - - %fieldset.features - %legend Public mode: - .clearfix - = f.label :public do - %span Allow public http clone - .input= f.check_box :public - - %fieldset.features - %legend Transfer: - .control-group - = f.label :namespace_id do - %span Namespace - .controls - = f.select :namespace_id, namespaces_options(@project.namespace_id, :all), {}, {class: 'chosen'} - %br - %ul.prepend-top-10.cred - %li Be careful. Changing project namespace can have unintended side effects - %li You can transfer project only to namespaces you can manage - %li You will need to update your local repositories to point to the new location. - - - .actions - = f.submit 'Save Project', class: "btn btn-save" - = link_to 'Cancel', admin_projects_path, class: "btn btn-cancel" - - - -:javascript - $(function(){ - new Projects(); - }) - diff --git a/app/views/admin/projects/edit.html.haml b/app/views/admin/projects/edit.html.haml deleted file mode 100644 index 7b59a0cc753..00000000000 --- a/app/views/admin/projects/edit.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -%h3.page_title #{@project.name} → Edit project -%hr -= render 'form', project: @project diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 15b2778252a..bc297209ae5 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -1,9 +1,3 @@ -%h3.page_title - Projects - = link_to 'New Project', new_project_path, class: "btn btn-small pull-right" - -%hr - .row .span4 .admin-filter @@ -14,9 +8,9 @@ = text_field_tag :name, params[:name], class: "span2" .control-group - = label_tag :namespace_id, 'Namespace:', class: 'control-label' + = label_tag :owner_id, 'Owner:', class: 'control-label' .controls - = select_tag :namespace_id, namespaces_options(params[:namespace_id], :all), class: "chosen span2", prompt: "Any" + = users_select_tag :owner_id, selected: params[:owner_id], class: 'input-large' .control-group = label_tag :public_only, 'Public Only', class: 'control-label' .controls @@ -41,21 +35,21 @@ = link_to "Reset", admin_projects_path, class: "btn" .span8 .ui-box - %h5.title + .title Projects (#{@projects.total_count}) + .pull-right + = link_to 'New Project', new_project_path, class: "btn btn-new" %ul.well-list - @projects.each do |project| %li - if project.public - %i.icon-share + = public_icon - else - %i.icon-lock.cgreen + = private_icon = link_to project.name_with_namespace, [:admin, project] .pull-right - = link_to 'Edit', edit_admin_project_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small" - = link_to 'Destroy', [:admin, project], confirm: "REMOVE #{project.name}? Are you sure?", method: :delete, class: "btn btn-small btn-remove" + = link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small" + = link_to 'Destroy', [project], confirm: remove_project_message(project), method: :delete, class: "btn btn-small btn-remove" - if @projects.blank? %p.nothing_here_message 0 projects matches - - else - %li.bottom - = paginate @projects, theme: "gitlab" + = paginate @projects, theme: "gitlab" diff --git a/app/views/admin/projects/members/_form.html.haml b/app/views/admin/projects/members/_form.html.haml deleted file mode 100644 index 8041202980d..00000000000 --- a/app/views/admin/projects/members/_form.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -= form_for @team_member_relation, as: :team_member, url: admin_project_member_path(@project, @member) do |f| - -if @team_member_relation.errors.any? - .alert.alert-error - %ul - - @team_member_relation.errors.full_messages.each do |msg| - %li= msg - - .clearfix - %label Project Access: - .input - = f.select :project_access, options_for_select(Project.access_options, @team_member_relation.project_access), {}, class: "project-access-select chosen span3" - - %br - .actions - = f.submit 'Save', class: "btn btn-primary" - = link_to 'Cancel', :back, class: "btn" diff --git a/app/views/admin/projects/members/edit.html.haml b/app/views/admin/projects/members/edit.html.haml deleted file mode 100644 index 2d76deb2aca..00000000000 --- a/app/views/admin/projects/members/edit.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -%p.slead - Edit access for - = link_to @member.name, admin_user_path(@member) - in - = link_to @project.name_with_namespace, admin_project_path(@project) - -%hr -= render 'form' diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 65b921170fd..c8a87328207 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -1,129 +1,111 @@ -%h3.page_title +%h3.page-title Project: #{@project.name_with_namespace} - = link_to edit_admin_project_path(@project), class: "btn pull-right" do + = link_to edit_project_path(@project), class: "btn pull-right" do %i.icon-edit Edit +%hr +.row + .span6 + .ui-box + .title + Project info: + %ul.well-list + %li + %span.light Name: + %strong + = link_to @project.name, project_path(@project) + %li + %span.light Namespace: + %strong + - if @project.namespace + = link_to @project.namespace.human_name, [:admin, @project.group || @project.owner] + - else + Global + %li + %span.light Owned by: + %strong + - if @project.owner + = link_to @project.owner_name, [:admin, @project.owner] + - else + (deleted) + %li + %span.light Created by: + %strong + = @project.creator.try(:name) || '(deleted)' -%br -%table.zebra-striped - %thead - %tr - %th Project - %th - %tr - %td - %b - Name: - %td - = @project.name - %tr - %td - %b - Namespace: - %td - - if @project.namespace - = @project.namespace.human_name - - else - Global - %tr - %td - %b - Owned by: - %td - - if @project.owner - = link_to @project.owner_name, admin_user_path(@project.owner) - - else - (deleted) - %tr - %td - %b - Created by: - %td - = @project.creator.try(:name) || '(deleted)' - %tr - %td - %b - Created at: - %td - = @project.created_at.stamp("March 1, 1999") - %tr - %td - %b - Smart HTTP: - %td - = link_to @project.http_url_to_repo - %tr - %td - %b - SSH: - %td - = link_to @project.ssh_url_to_repo - - if @project.public - %tr.bgred - %td - %b - Public Read-Only Code access: - %td - = check_box_tag 'public', nil, @project.public + %li + %span.light Created at: + %strong + = @project.created_at.stamp("March 1, 1999") -- if @repository - %table.zebra-striped - %thead - %tr - %th Repository - %th - %tr - %td - %b - FS Path: - %td - %code= @repository.path_to_repo - %tr - %td - %b - Last commit at: - %td - = last_commit(@project) + %li + %span.light http: + %strong + = link_to @project.http_url_to_repo + %li + %span.light ssh: + %strong + = link_to @project.ssh_url_to_repo + - if @project.repository.exists? + %li + %span.light fs: + %strong + = @repository.path_to_repo -%br -%h5 - Team - %small - (#{@project.users.count}) -%br -%table.zebra-striped.team_members - %thead - %tr - %th Name - %th Project Access - %th Repository Access - %th + %li + %span.light last commit: + %strong + = last_commit(@project) + - else + %li + %span.light repository: + %strong.cred + does not exist - - @project.users.each do |tm| - %tr - %td - = link_to tm.name, admin_user_path(tm) - %td= @project.project_access_human(tm) - %td= link_to 'Edit Access', edit_admin_project_member_path(@project, tm), class: "btn btn-small" - %td= link_to 'Remove from team', admin_project_member_path(@project, tm), confirm: 'Are you sure?', method: :delete, class: "btn btn-remove small" + %li + %span.light access: + %strong + - if @project.public + %span.cblue + %i.icon-share + Public + - else + %span.cgreen + %i.icon-lock + Private + .span6 + - if @group + .ui-box + .title + %strong #{@group.name} + group members (#{@group.users_groups.count}) + .pull-right + = link_to admin_group_path(@group), class: 'btn btn-small' do + %i.icon-edit + %ul.well-list + - @group.users_groups.order('group_access DESC').each do |member| + = render 'users_groups/users_group', member: member, show_controls: false -%br -%h5 Add new team member -%br -= form_tag team_update_admin_project_path(@project), class: "bulk_import", method: :put do - %table.zebra-striped - %thead - %tr - %th Users - %th Project Access: - - %tr - %td= select_tag :user_ids, options_from_collection_for_select(@users , :id, :name), multiple: true, data: {placeholder: 'Select users'}, class: 'chosen span5' - %td= select_tag :project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3"} - - %tr - %td= submit_tag 'Add', class: "btn btn-primary" - %td - Read more about project permissions - %strong= link_to "here", help_permissions_path, class: "vlink" + .ui-box + .title + Project members + %small + (#{@project.users.count}) + .pull-right + = link_to project_team_index_path(@project), class: "btn btn-tiny" do + %i.icon-edit + Manage Access + %ul.well-list.team_members + - @project.users_projects.each do |users_project| + - user = users_project.user + %li.users_project + %strong + = link_to user.name, admin_user_path(user) + .pull-right + - if users_project.owner? + %span.light Owner + - else + %span.light= users_project.human_access + = link_to project_team_member_path(@project, user), confirm: remove_from_project_team_message(@project, user), method: :delete, remote: true, class: "btn btn-small btn-remove" do + %i.icon-remove diff --git a/app/views/admin/teams/edit.html.haml b/app/views/admin/teams/edit.html.haml deleted file mode 100644 index 9282398ce5b..00000000000 --- a/app/views/admin/teams/edit.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -%h3.page_title Rename Team -%hr -= form_for @team, url: admin_team_path(@team), method: :put do |f| - - if @team.errors.any? - .alert.alert-error - %span= @team.errors.full_messages.first - .clearfix.team_name_holder - = f.label :name do - Team name is - .input - = f.text_field :name, placeholder: "Example Team", class: "xxlarge" - - .clearfix.team_name_holder - = f.label :path do - %span.cred Team path is - .input - = f.text_field :path, placeholder: "example-team", class: "xxlarge danger" - %ul.cred - %li It will change web url for access team and team projects. - - .form-actions - = f.submit 'Rename team', class: "btn btn-remove" - = link_to 'Cancel', admin_teams_path, class: "btn btn-cancel" diff --git a/app/views/admin/teams/index.html.haml b/app/views/admin/teams/index.html.haml deleted file mode 100644 index 1f2f4763f76..00000000000 --- a/app/views/admin/teams/index.html.haml +++ /dev/null @@ -1,38 +0,0 @@ -%h3.page_title - Teams - %small - simple Teams description - - = link_to 'New Team', new_admin_team_path, class: "btn btn-small pull-right" - %br - -= form_tag admin_teams_path, method: :get, class: 'form-inline' do - = text_field_tag :name, params[:name], class: "xlarge" - = submit_tag "Search", class: "btn submit btn-primary" - -%table - %thead - %tr - %th - Name - %i.icon-sort-down - %th Path - %th Projects - %th Members - %th Owner - %th.cred Danger Zone! - - - @teams.each do |team| - %tr - %td - %strong= link_to team.name, admin_team_path(team) - %td= team.path - %td= team.projects.count - %td= team.members.count - %td - = link_to team.owner.name, admin_user_path(team.owner_id) - %td.bgred - = link_to 'Rename', edit_admin_team_path(team), id: "edit_#{dom_id(team)}", class: "btn btn-small" - = link_to 'Destroy', admin_team_path(team), confirm: "REMOVE #{team.name}? Are you sure?", method: :delete, class: "btn btn-small btn-remove" - -= paginate @teams, theme: "admin" diff --git a/app/views/admin/teams/members/_form.html.haml b/app/views/admin/teams/members/_form.html.haml deleted file mode 100644 index f1388aab4bb..00000000000 --- a/app/views/admin/teams/members/_form.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -= form_tag admin_team_member_path(@team, @member), method: :put do - -if @member.errors.any? - .alert.alert-error - %ul - - @member.errors.full_messages.each do |msg| - %li= msg - - .clearfix - %label Default access for Team projects: - .input - = select_tag :default_project_access, options_for_select(UserTeam.access_roles, @team.default_projects_access(@member)), class: "project-access-select chosen span3" - .clearfix - %label Team admin? - .input - = check_box_tag :group_admin, true, @team.admin?(@member) - - %br - .actions - = submit_tag 'Save', class: "btn btn-primary" - = link_to 'Cancel', :back, class: "btn" diff --git a/app/views/admin/teams/members/edit.html.haml b/app/views/admin/teams/members/edit.html.haml deleted file mode 100644 index a82847ee5f8..00000000000 --- a/app/views/admin/teams/members/edit.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -%h3 - Edit access #{@member.name} in #{@team.name} team - -%hr -%table.zebra-striped - %tr - %td User: - %td= @member.name - %tr - %td Team: - %td= @team.name - %tr - %td Since: - %td= member_since(@team, @member).stamp("Nov 11, 2010") - -= render 'form' diff --git a/app/views/admin/teams/members/new.html.haml b/app/views/admin/teams/members/new.html.haml deleted file mode 100644 index a37c941db53..00000000000 --- a/app/views/admin/teams/members/new.html.haml +++ /dev/null @@ -1,29 +0,0 @@ -%h3.page_title - Team: #{@team.name} - -%fieldset - %legend Members (#{@team.members.count}) - = form_tag admin_team_members_path(@team), id: "team_members", class: "bulk_import", method: :post do - %table#members_list - %thead - %tr - %th User name - %th Default project access - %th Team access - %th - - @team.members.each do |member| - %tr.member - %td - = link_to [:admin, member] do - = member.name - %small= "(#{member.email})" - %td= @team.human_default_projects_access(member) - %td= @team.admin?(member) ? "Admin" : "Member" - %td - %tr - %td= select_tag :user_ids, options_from_collection_for_select(@users , :id, :name_with_email), multiple: true, data: {placeholder: 'Select users'}, class: 'chosen span5' - %td= select_tag :default_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" } - %td - %span= check_box_tag :group_admin - %span Admin? - %td= submit_tag 'Add', class: "btn btn-primary", id: :add_members_to_team diff --git a/app/views/admin/teams/new.html.haml b/app/views/admin/teams/new.html.haml deleted file mode 100644 index 5d55a7975ee..00000000000 --- a/app/views/admin/teams/new.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -%h3.page_title New Team -%hr -= form_for @team, url: admin_teams_path do |f| - - if @team.errors.any? - .alert.alert-error - %span= @team.errors.full_messages.first - .clearfix - = f.label :name do - Team name is - .input - = f.text_field :name, placeholder: "Ex. OpenSource", class: "xxlarge left" - - = f.submit 'Create team', class: "btn btn-primary" - %hr - .padded - %ul - %li All created teams are public (users can view who enter into team and which project are assigned for this team) - %li People within a team see only projects they have access to - %li You will be able to assign existing projects for team diff --git a/app/views/admin/teams/projects/_form.html.haml b/app/views/admin/teams/projects/_form.html.haml deleted file mode 100644 index 5b79d518d42..00000000000 --- a/app/views/admin/teams/projects/_form.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -= form_tag admin_team_project_path(@team, @project), method: :put do - -if @project.errors.any? - .alert.alert-error - %ul - - @project.errors.full_messages.each do |msg| - %li= msg - - .clearfix - %label Max access for Team members: - .input - = select_tag :greatest_project_access, options_for_select(UserTeam.access_roles, @team.max_project_access(@project)), class: "project-access-select chosen span3" - - %br - .actions - = submit_tag 'Save', class: "btn btn-primary" - = link_to 'Cancel', :back, class: "btn" diff --git a/app/views/admin/teams/projects/edit.html.haml b/app/views/admin/teams/projects/edit.html.haml deleted file mode 100644 index b91a4982b81..00000000000 --- a/app/views/admin/teams/projects/edit.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -%h3 - Edit max access in #{@project.name} for #{@team.name} team - -%hr -%table.zebra-striped - %tr - %td Project: - %td= @project.name - %tr - %td Team: - %td= @team.name - %tr - %td Since: - %td= assigned_since(@team, @project).stamp("Nov 11, 2010") - -= render 'form' diff --git a/app/views/admin/teams/projects/new.html.haml b/app/views/admin/teams/projects/new.html.haml deleted file mode 100644 index b60dad35214..00000000000 --- a/app/views/admin/teams/projects/new.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -%h3.page_title - Team: #{@team.name} - -%fieldset - %legend Projects (#{@team.projects.count}) - = form_tag admin_team_projects_path(@team), id: "assign_projects", class: "bulk_import", method: :post do - %table#projects_list - %thead - %tr - %th Project name - %th Max access - %th - - @team.projects.each do |project| - %tr.project - %td - = link_to project.name_with_namespace, [:admin, project] - %td - %span= @team.human_max_project_access(project) - %td - %tr - %td= select_tag :project_ids, options_from_collection_for_select(@projects , :id, :name_with_namespace), multiple: true, data: {placeholder: 'Select projects'}, class: 'chosen span5' - %td= select_tag :greatest_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" } - %td= submit_tag 'Add', class: "btn btn-primary", id: :assign_projects_to_team diff --git a/app/views/admin/teams/show.html.haml b/app/views/admin/teams/show.html.haml deleted file mode 100644 index e5d079981c0..00000000000 --- a/app/views/admin/teams/show.html.haml +++ /dev/null @@ -1,101 +0,0 @@ -%h3.page_title - Team: #{@team.name} - -%br -%table.zebra-striped - %thead - %tr - %th Team - %th - %tr - %td - %b - Name: - %td - = @team.name - - = link_to edit_admin_team_path(@team), class: "btn btn-small pull-right" do - %i.icon-edit - Rename - %tr - %td - %b - Owner: - %td - = @team.owner.name - .pull-right - = link_to "#", class: "btn btn-small change-owner-link" do - %i.icon-edit - Change owner - - %tr.change-owner-holder.hide - %td.bgred - %b.cred - New Owner: - %td.bgred - = form_for @team, url: admin_team_path(@team) do |f| - = f.select :owner_id, User.all.map { |user| [user.name, user.id] }, {}, {class: 'chosen'} - %div - = f.submit 'Change Owner', class: "btn btn-remove" - = link_to "Cancel", "#", class: "btn change-owner-cancel-link" - -%fieldset - %legend - Members (#{@team.members.count}) - %span= link_to 'Add members', new_admin_team_member_path(@team), class: "btn btn-primary btn-small pull-right", id: :add_members_to_team - - if @team.members.any? - %table#members_list - %thead - %tr - %th User name - %th Default project access - %th Team access - %th.cred.span3 Danger Zone! - - @team.members.each do |member| - %tr.member{ class: "user_#{member.id}"} - %td - = link_to [:admin, member] do - = member.name - %small= "(#{member.email})" - %td= @team.human_default_projects_access(member) - %td= @team.admin?(member) ? "Admin" : "Member" - %td.bgred - = link_to 'Edit', edit_admin_team_member_path(@team, member), class: "btn btn-small" - - = link_to 'Remove', admin_team_member_path(@team, member), confirm: 'Remove member from team. Are you sure?', method: :delete, class: "btn btn-remove btn-small", id: "remove_member_#{member.id}" - -%fieldset - %legend - Projects (#{@team.projects.count}) - %span= link_to 'Add projects', new_admin_team_project_path(@team), class: "btn btn-primary btn-small pull-right", id: :assign_projects_to_team - - if @team.projects.any? - %table#projects_list - %thead - %tr - %th Project name - %th Max access - %th.cred.span3 Danger Zone! - - @team.projects.each do |project| - %tr.project - %td - = link_to project.name_with_namespace, [:admin, project] - %td - %span= @team.human_max_project_access(project) - %td.bgred - = link_to 'Edit', edit_admin_team_project_path(@team, project), class: "btn btn-small" - - = link_to 'Relegate', admin_team_project_path(@team, project), confirm: 'Remove project from team. Are you sure?', method: :delete, class: "btn btn-remove small", id: "relegate_project_#{project.id}" - -:javascript - $(function(){ - var modal = $('.change-owner-holder'); - $('.change-owner-link').bind("click", function(){ - $(this).hide(); - modal.show(); - }); - $('.change-owner-cancel-link').bind("click", function(){ - modal.hide(); - $('.change-owner-link').show(); - }) - }) - diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index 48876338a23..3f930c45fa6 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -1,87 +1,90 @@ .user_new - = form_for [:admin, @admin_user] do |f| - -if @admin_user.errors.any? + = form_for [:admin, @user] do |f| + -if @user.errors.any? #error_explanation %ul.unstyled.alert.alert-error - - @admin_user.errors.full_messages.each do |msg| + - @user.errors.full_messages.each do |msg| %li= msg %fieldset %legend Account - .clearfix + .control-group = f.label :name - .input + .controls = f.text_field :name, required: true, autocomplete: "off" %span.help-inline * required - .clearfix + .control-group = f.label :username - .input + .controls = f.text_field :username, required: true, autocomplete: "off" %span.help-inline * required - .clearfix + .control-group = f.label :email - .input + .controls = f.text_field :email, required: true, autocomplete: "off" %span.help-inline * required - %fieldset - %legend Password - .clearfix - = f.label :password - .input= f.password_field :password, disabled: f.object.force_random_password - .clearfix - = f.label :password_confirmation - .input= f.password_field :password_confirmation, disabled: f.object.force_random_password - -if f.object.new_record? - .clearfix - = f.label :force_random_password do - %span Generate random password - .input= f.check_box :force_random_password, {}, true, nil + - if @user.new_record? + %fieldset + %legend Password + .control-group + = f.label :password + .controls + %strong + A temporary password will be generated and sent to user. + %br + User will be forced to change it after first sign in + - else + %fieldset + %legend Password + .control-group + = f.label :password + .controls= f.password_field :password, disabled: f.object.force_random_password + .control-group + = f.label :password_confirmation + .controls= f.password_field :password_confirmation, disabled: f.object.force_random_password %fieldset %legend Access .row .span8 - .clearfix + .control-group = f.label :projects_limit - .input= f.number_field :projects_limit + .controls= f.number_field :projects_limit - .clearfix + .control-group = f.label :can_create_group - .input= f.check_box :can_create_group - - .clearfix - = f.label :can_create_team - .input= f.check_box :can_create_team + .controls= f.check_box :can_create_group - .clearfix + .control-group = f.label :admin do %strong.cred Administrator - .input= f.check_box :admin + .controls= f.check_box :admin .span4 - - unless @admin_user.new_record? + - unless @user.new_record? .alert.alert-error - - if @admin_user.blocked + - if @user.blocked? %p This user is blocked and is not able to login to GitLab - = link_to 'Unblock User', unblock_admin_user_path(@admin_user), method: :put, class: "btn btn-small" + = link_to 'Unblock User', unblock_admin_user_path(@user), method: :put, class: "btn btn-small" - else %p Blocked users will be removed from all projects & will not be able to login to GitLab. - = link_to 'Block User', block_admin_user_path(@admin_user), confirm: 'USER WILL BE BLOCKED! Are you sure?', method: :put, class: "btn btn-small btn-remove" + = link_to 'Block User', block_admin_user_path(@user), confirm: 'USER WILL BE BLOCKED! Are you sure?', method: :put, class: "btn btn-small btn-remove" %fieldset %legend Profile - .clearfix + .control-group = f.label :skype - .input= f.text_field :skype - .clearfix + .controls= f.text_field :skype + .control-group = f.label :linkedin - .input= f.text_field :linkedin - .clearfix + .controls= f.text_field :linkedin + .control-group = f.label :twitter - .input= f.text_field :twitter + .controls= f.text_field :twitter - .actions - = f.submit 'Save', class: "btn btn-save" - - if @admin_user.new_record? + .form-actions + - if @user.new_record? + = f.submit 'Create user', class: "btn btn-create" = link_to 'Cancel', admin_users_path, class: "btn btn-cancel" - else - = link_to 'Cancel', admin_user_path(@admin_user), class: "btn btn-cancel" + = f.submit 'Save changes', class: "btn btn-save" + = link_to 'Cancel', admin_user_path(@user), class: "btn btn-cancel" diff --git a/app/views/admin/users/edit.html.haml b/app/views/admin/users/edit.html.haml index f8ff77b8f53..2a4f8c60546 100644 --- a/app/views/admin/users/edit.html.haml +++ b/app/views/admin/users/edit.html.haml @@ -1,5 +1,5 @@ -%h3.page_title - #{@admin_user.name} → +%h3.page-title + #{@user.name} → %i.icon-edit Edit user %hr diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index f5bb8b0681d..b32f0ae87cc 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -1,8 +1,3 @@ -%h3.page_title - Users - = link_to 'New User', new_admin_user_path, class: "btn btn-small pull-right" -%br - .row .span3 .admin-filter @@ -32,10 +27,12 @@ .span9 .ui-box - %h5.title - Users (#{@admin_users.total_count}) + .title + Users (#{@users.total_count}) + .pull-right + = link_to 'New User', new_admin_user_path, class: "btn btn-new" %ul.well-list - - @admin_users.each do |user| + - @users.each do |user| %li - if user.blocked? %i.icon-lock.cred @@ -53,10 +50,9 @@ = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-small" - unless user == current_user - - if user.blocked + - if user.blocked? = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-small success" - else = link_to 'Block', block_admin_user_path(user), confirm: 'USER WILL BE BLOCKED! Are you sure?', method: :put, class: "btn btn-small btn-remove" = link_to 'Destroy', [:admin, user], confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?", method: :delete, class: "btn btn-small btn-remove" - %li.bottom - = paginate @admin_users, theme: "gitlab" + = paginate @users, theme: "gitlab" diff --git a/app/views/admin/users/new.html.haml b/app/views/admin/users/new.html.haml index 1e82b249cf1..a1c90c48946 100644 --- a/app/views/admin/users/new.html.haml +++ b/app/views/admin/users/new.html.haml @@ -1,4 +1,4 @@ -%h3.page_title +%h3.page-title %i.icon-plus New user %hr diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index c5d60194820..3df9903e409 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -1,83 +1,137 @@ +%h3.page-title + User: + = @user.name + - if @user.blocked? + %span.cred (Blocked) + - if @user.admin + %span.cred (Admin) + + .pull-right + = link_to edit_admin_user_path(@user), class: "btn grouped" do + %i.icon-edit + Edit + - if @user.blocked? + = link_to 'Unblock', unblock_admin_user_path(@user), method: :put, class: "btn grouped success" +%hr + .row .span6 - %h3.page_title - = image_tag gravatar_icon(@admin_user.email, 90), class: "avatar s90" - = @admin_user.name - - if @admin_user.blocked - %span.cred (Blocked) - - if @admin_user.admin - %span.cred (Admin) - .pull-right - = link_to edit_admin_user_path(@admin_user), class: "btn pull-right" do - %i.icon-edit - Edit - %br - %small @#{@admin_user.username} - %br - %small member since #{@admin_user.created_at.stamp("Nov 12, 2031")} - .clearfix - %hr - %h5 - Add User to Projects - %small - Read more about project permissions - %strong= link_to "here", help_permissions_path, class: "vlink" - %br - = form_tag team_update_admin_user_path(@admin_user), class: "bulk_import", method: :put do - .control-group - = label_tag :project_ids, "Projects", class: 'control-label' - .controls - = select_tag :project_ids, options_from_collection_for_select(@not_in_projects , :id, :name_with_namespace), multiple: true, data: {placeholder: 'Select projects'}, class: 'chosen span3' - .control-group - = label_tag :project_access, "Project Access", class: 'control-label' - .controls - = select_tag :project_access, options_for_select(Project.access_options), class: "project-access-select chosen span3" - - .form-actions - = submit_tag 'Add', class: "btn btn-create" + .ui-box + .title + Account: .pull-right - %br + = image_tag gravatar_icon(@user.email, 32), class: "avatar s32" + %ul.well-list + %li + %span.light Name: + %strong= @user.name + %li + %span.light Username: + %strong + = @user.username + %li + %span.light Email: + %strong + = mail_to @user.email + %li + %span.light Can create groups: + %strong + = @user.can_create_group ? "Yes" : "No" + %li + %span.light Personal projects limit: + %strong + = @user.projects_limit + %li + %span.light Member since: + %strong + = @user.created_at.stamp("Nov 12, 2031") - - if @admin_user.owned_groups.present? - .ui-box - %h5.title Owned groups: - %ul.well-list - - @admin_user.groups.each do |group| + %li + %span.light Last sign-in at: + %strong + - if @user.last_sign_in_at + = @user.last_sign_in_at.stamp("Nov 12, 2031") + - else + never + + - if @user.ldap_user? + %li + %span.light LDAP uid: + %strong + = @user.extern_uid + + - if @user.created_by + %li + %span.light Created by: + %strong + = link_to @user.created_by.name, [:admin, @user.created_by] + + - unless @user == current_user + .alert + %h4 Block user + %br + %p Blocking user has the following effects: + %ul + %li User will not be able to login + %li User will not be able to access git repositories + %li User will be removed from joined projects and groups + %li Personal projects will be left + %li Owned groups will be left + = link_to 'Block user', block_admin_user_path(@user), confirm: 'USER WILL BE BLOCKED! Are you sure?', method: :put, class: "btn btn-remove" + + .alert.alert-error + %h4 + Remove user + %br + %p Deleting a user has the following effects: + %ul + %li All user content like authored issues, snippets, comments will be removed + - rp = @user.personal_projects.count + - unless rp.zero? + %li #{pluralize rp, 'personal project'} will be removed and cannot be restored + - if @user.solo_owned_groups.present? %li - %strong= link_to group.name, admin_group_path(group) + Next groups with all content will be removed: + %strong #{@user.solo_owned_groups.map(&:name).join(', ')} + = link_to 'Remove user', [:admin, @user], confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?", method: :delete, class: "btn btn-remove" - - if @admin_user.owned_teams.present? + .span6 + - if @user.users_groups.present? .ui-box - %h5.title Owned teams: + .title Groups: %ul.well-list - - @admin_user.owned_teams.each do |team| - %li - %strong= link_to team.name, admin_team_path(team) - + - @user.users_groups.each do |user_group| + - group = user_group.group + %li.users_group + %strong= link_to group.name, admin_group_path(group) + .pull-right + %span.light= user_group.human_access + - unless user_group.owner? + = link_to group_users_group_path(group, user_group), 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.icon-remove.icon-white - .span6 - = render 'users/profile', user: @admin_user .ui-box - %h5.title Projects (#{@projects.count}) + .title Projects (#{@projects.count}) %ul.well-list - - @projects.each do |project| - %li + - @projects.sort_by(&:name_with_namespace).each do |project| + - tm = project.team.find_tm(@user.id) + %li.users_project = link_to admin_project_path(project), class: dom_class(project) do - if project.namespace = project.namespace.human_name \/ %strong.well-title = truncate(project.name, length: 45) - %span.pull-right.light - - if project.owner == @admin_user - %i.icon-wrench - - tm = project.team.get_tm(@admin_user.id) - - if tm - = tm.project_access_human - = link_to edit_admin_project_member_path(project, tm.user), class: "btn btn-small" do - %i.icon-edit - = link_to admin_project_member_path(project, tm.user), confirm: 'Are you sure?', method: :delete, class: "btn btn-small btn-remove" do - %i.icon-remove - %p.light - %i.icon-wrench - – user is a project owner + + - if tm + .pull-right + - if tm.owner? + %span.light Owner + - else + %span.light= tm.human_access + + - if tm.respond_to? :project + = link_to project_team_member_path(project, @user), confirm: remove_from_project_team_message(project, @user), remote: true, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from project' do + %i.icon-remove + + diff --git a/app/views/blame/_head.html.haml b/app/views/blame/_head.html.haml deleted file mode 100644 index ef9e6c9c532..00000000000 --- a/app/views/blame/_head.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -%ul.nav.nav-tabs - %li - = render partial: 'shared/ref_switcher', locals: {destination: 'tree', path: params[:path]} - = nav_link(controller: :refs) do - = link_to 'Source', project_tree_path(@project, @ref) - %li.pull-right - = render "shared/clone_panel" diff --git a/app/views/commit/show.html.haml b/app/views/commit/show.html.haml deleted file mode 100644 index 485f2d1e67c..00000000000 --- a/app/views/commit/show.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -= render "commits/commit_box" - -%p.pull-right.cgray - This commit has - %span.cgreen #{@commit.stats.additions} additions - and - %span.cred #{@commit.stats.deletions} deletions - -= render "commits/diffs", diffs: @commit.diffs -= render "notes/notes_with_form" - -:javascript - $(function(){ - $('.files .file').each(function(){ - new CommitFile(this); - }); - }); diff --git a/app/views/commits/_commit.html.haml b/app/views/commits/_commit.html.haml deleted file mode 100644 index eb0312d01e1..00000000000 --- a/app/views/commits/_commit.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -%li.commit - .browse_code_link_holder - %p - %strong= link_to "Browse Code »", project_tree_path(@project, commit), class: "right" - %p - = link_to commit.short_id(8), project_commit_path(@project, commit), class: "commit_short_id" - = commit.author_link avatar: true, size: 24 - - = link_to_gfm truncate(commit.title, length: 50), project_commit_path(@project, commit.id), class: "row_title" - - %span.committed_ago - = time_ago_in_words(commit.committed_date) - ago - - - %span.notes_count - - notes = @project.notes.for_commit_id(commit.id) - - if notes.any? - %span.btn.disabled.grouped - %i.icon-comment - = notes.count diff --git a/app/views/commits/_commit_box.html.haml b/app/views/commits/_commit_box.html.haml deleted file mode 100644 index 4c80c13ced1..00000000000 --- a/app/views/commits/_commit_box.html.haml +++ /dev/null @@ -1,50 +0,0 @@ -.ui-box.ui-box-show - .ui-box-head - .pull-right - - if @notes_count > 0 - %span.btn.disabled.grouped - %i.icon-comment - = @notes_count - .left.btn-group - %a.btn.grouped.dropdown-toggle{ data: {toggle: :dropdown} } - %i.icon-download-alt - Download as - %span.caret - %ul.dropdown-menu - %li= link_to "Email Patches", project_commit_path(@project, @commit, format: :patch) - %li= link_to "Plain Diff", project_commit_path(@project, @commit, format: :diff) - = link_to project_tree_path(@project, @commit), class: "btn btn-primary grouped" do - %span Browse Code » - %h3.commit-title.page_title - = gfm escape_once(@commit.title) - - if @commit.description.present? - %pre.commit-description - = gfm escape_once(@commit.description) - .ui-box-body - .row - .span5 - .author - = @commit.author_link avatar: true, size: 32 - authored - %time{title: @commit.authored_date.stamp("Aug 21, 2011 9:23pm")} - #{time_ago_in_words(@commit.authored_date)} ago - - if @commit.different_committer? - .committer - → - = @commit.committer_link - committed - %time{title: @commit.committed_date.stamp("Aug 21, 2011 9:23pm")} - #{time_ago_in_words(@commit.committed_date)} ago - .span6.pull-right - .pull-right - .sha-block - %span.cgray commit - %span.label_commit= @commit.id - .clearfix - .pull-right - .sha-block - %span.cgray= pluralize(@commit.parents.count, "parent") - - @commit.parents.each do |parent| - = link_to parent.id[0...10], project_commit_path(@project, parent) - - diff --git a/app/views/commits/_commits.html.haml b/app/views/commits/_commits.html.haml deleted file mode 100644 index 191320933d3..00000000000 --- a/app/views/commits/_commits.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -- @commits.group_by { |c| c.committed_date.to_date }.each do |day, commits| - %div.ui-box - %h5.title - %i.icon-calendar - %span= day.stamp("28 Aug, 2010") - %ul.well-list= render commits diff --git a/app/views/commits/_diffs.html.haml b/app/views/commits/_diffs.html.haml deleted file mode 100644 index 76f9f267b41..00000000000 --- a/app/views/commits/_diffs.html.haml +++ /dev/null @@ -1,49 +0,0 @@ -- if @suppress_diff - .alert.alert-block - %p - %strong Warning! Large commit with more than #{Commit::DIFF_SAFE_SIZE} files changed. - %p To prevent performance issue we rejected diff information. - %p - But if you still want to see diff - = link_to "click this link", project_commit_path(@project, @commit, force_show_diff: true), class: "underlined_link" - -%p.cgray - Showing #{pluralize(diffs.count, "changed file")} -.file-stats - = render "commits/diff_head", diffs: diffs - -.files - - unless @suppress_diff - - diffs.each_with_index do |diff, i| - - next if diff.diff.empty? - - file = (@commit.tree / diff.new_path) - - file = (@commit.prev_commit.tree / diff.old_path) unless file - - next unless file - .file{id: "diff-#{i}"} - .header - - if diff.deleted_file - %span= diff.old_path - - - if @commit.prev_commit - = link_to project_tree_path(@project, tree_join(@commit.prev_commit_id, diff.new_path)), {:class => 'btn pull-right view-file'} do - View file @ - %span.commit-short-id= @commit.short_id(6) - - else - %span= diff.new_path - - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode - %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" - - = link_to project_tree_path(@project, tree_join(@commit.id, diff.new_path)), {:class => 'btn btn-tiny pull-right view-file'} do - View file @ - %span.commit-short-id= @commit.short_id(6) - - .content - -# Skipp all non non-supported blobs - - next unless file.respond_to?('text?') - - if file.text? - = render "commits/text_file", diff: diff, index: i - - elsif file.image? - - old_file = (@commit.prev_commit.tree / diff.old_path) if !@commit.prev_commit.nil? - = render "commits/image", diff: diff, old_file: old_file, file: file, index: i - - else - %p.nothing_here_message No preview for this file type diff --git a/app/views/compare/_form.html.haml b/app/views/compare/_form.html.haml deleted file mode 100644 index 7c0688a2287..00000000000 --- a/app/views/compare/_form.html.haml +++ /dev/null @@ -1,39 +0,0 @@ -%div - - unless params[:to] - %p.slead - Fill input field with commit id like - %code.label_branch 4eedf23 - or branch/tag name like - %code.label_branch master - and press compare button for commits list, code diff. - - %br - - = form_tag project_compare_index_path(@project), method: :post do - .clearfix - .pull-left - - if params[:to] && params[:from] - = link_to 'switch', {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has_tooltip', title: 'Switch base of comparison'} - = text_field_tag :from, params[:from], placeholder: "master", class: "xlarge" - = "..." - = text_field_tag :to, params[:to], placeholder: "aa8b4ef", class: "xlarge" - .pull-left - - = submit_tag "Compare", class: "btn btn-primary wide commits-compare-btn" - - if @refs_are_same - .alert - %span Refs are the same - - - -:javascript - $(function() { - var availableTags = #{@project.repository.ref_names.to_json}; - - $("#from, #to").autocomplete({ - source: availableTags, - minLength: 1 - }); - - disableButtonIfEmptyField('#to', '.commits-compare-btn'); - }); diff --git a/app/views/compare/index.html.haml b/app/views/compare/index.html.haml deleted file mode 100644 index 6c9a5fd8305..00000000000 --- a/app/views/compare/index.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -= render "commits/head" - -%h3.page_title - Compare View -%hr - -= render "form" diff --git a/app/views/compare/show.html.haml b/app/views/compare/show.html.haml deleted file mode 100644 index d8ea3727d57..00000000000 --- a/app/views/compare/show.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -= render "commits/head" - -%h3.page_title - Compare View -%hr - -= render "form" - -- if @commits.present? - %div.ui-box - %h5.title - Commits (#{@commits.count}) - %ul.well-list= render @commits - - - unless @diffs.empty? - %h4 Diff - = render "commits/diffs", diffs: @diffs diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml index 2b7d23c225d..89117726317 100644 --- a/app/views/dashboard/_activities.html.haml +++ b/app/views/dashboard/_activities.html.haml @@ -1,10 +1,5 @@ = render "events/event_last_push", event: @last_push - -.event_filter - = event_filter_link EventFilter.push, 'Push events' - = event_filter_link EventFilter.merged, 'Merge events' - = event_filter_link EventFilter.comments, 'Comments' - = event_filter_link EventFilter.team, 'Team' += render 'shared/event_filter' - if @events.any? .content_list diff --git a/app/views/dashboard/_filter.html.haml b/app/views/dashboard/_filter.html.haml deleted file mode 100644 index 82e679d5927..00000000000 --- a/app/views/dashboard/_filter.html.haml +++ /dev/null @@ -1,33 +0,0 @@ -= form_tag dashboard_filter_path(entity), method: 'get' do - %fieldset.dashboard-search-filter - = search_field_tag "search", params[:search], { placeholder: 'Search', class: 'search-text-input' } - = button_tag type: 'submit', class: 'btn' do - %i.icon-search - - %fieldset - %legend Status: - %ul.nav.nav-pills.nav-stacked - %li{class: ("active" if !params[:status])} - = link_to dashboard_filter_path(entity, status: nil) do - Open - %li{class: ("active" if params[:status] == 'closed')} - = link_to dashboard_filter_path(entity, status: 'closed') do - Closed - %li{class: ("active" if params[:status] == 'all')} - = link_to dashboard_filter_path(entity, status: 'all') do - All - - %fieldset - %legend Projects: - %ul.nav.nav-pills.nav-stacked - - @projects.each do |project| - - unless entities_per_project(project, entity).zero? - %li{class: ("active" if params[:project_id] == project.id.to_s)} - = link_to dashboard_filter_path(entity, project_id: project.id) do - = project.name_with_namespace - %small.pull-right= entities_per_project(project, entity) - - %fieldset - %hr - = link_to "Reset", dashboard_filter_path(entity), class: 'btn pull-right' - diff --git a/app/views/dashboard/_groups.html.haml b/app/views/dashboard/_groups.html.haml index ba8d3029eaf..b4f3866228d 100644 --- a/app/views/dashboard/_groups.html.haml +++ b/app/views/dashboard/_groups.html.haml @@ -1,18 +1,19 @@ .ui-box - %h5.title - Groups - %small - (#{groups.count}) + .title.clearfix + = search_field_tag :filter_group, nil, placeholder: 'Filter by name', class: 'dash-filter' - if current_user.can_create_group? %span.pull-right - = link_to new_group_path, class: "btn btn-tiny info" do + = link_to new_group_path, class: "btn btn-new" do %i.icon-plus - New Group - %ul.well-list + New group + %ul.well-list.dash-list - groups.each do |group| - %li + %li.group-row = link_to group_path(id: group.path), class: dom_class(group) do - %strong.well-title= truncate(group.name, length: 35) - %span.pull-right.light - - if group.owner == current_user - %i.icon-wrench + %span.group-name.filter-title + = truncate(group.name, length: 35) + %span.arrow + %i.icon-angle-right + - if groups.blank? + %li + %h3.nothing_here_message You have no groups yet. diff --git a/app/views/dashboard/_project.html.haml b/app/views/dashboard/_project.html.haml new file mode 100644 index 00000000000..50b833f3d82 --- /dev/null +++ b/app/views/dashboard/_project.html.haml @@ -0,0 +1,12 @@ += link_to project_path(project), class: dom_class(project) do + %span.namespace-name + - if project.namespace + = project.namespace.human_name + \/ + %span.project-name.filter-title + = truncate(project.name, length: 25) + %span.arrow + %i.icon-angle-right + %span.last-activity + %span Last activity: + %span.date= project_last_activity(project) diff --git a/app/views/dashboard/_projects.html.haml b/app/views/dashboard/_projects.html.haml index 30fb7268014..b79b27fc95a 100644 --- a/app/views/dashboard/_projects.html.haml +++ b/app/views/dashboard/_projects.html.haml @@ -1,31 +1,25 @@ -.projects_box - %h5.title - Projects - %small - (#{@projects_count}) +.ui-box + .title.clearfix + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'dash-filter' - if current_user.can_create_project? %span.pull-right - = link_to new_project_path, class: "btn btn-tiny info" do + = link_to new_project_path, class: "btn btn-new" do %i.icon-plus - New Project + New project - %ul.well-list + %ul.well-list.dash-list - projects.each do |project| - %li - = link_to project_path(project), class: dom_class(project) do - - if project.namespace - = project.namespace.human_name - \/ - %strong.well-title - = truncate(project.name, length: 25) - %span.arrow - → - %span.last_activity - %strong Last activity: - %span= project_last_activity(project) + %li.project-row + = render "project", project: project + - if projects.blank? %li %h3.nothing_here_message There are no projects here. - - if @projects_count > 20 + - if @projects_count > @projects_limit %li.bottom - %strong= link_to "show all projects", projects_dashboard_path + %span.light + #{@projects_limit} of #{pluralize(@projects_count, 'project')} displayed. + .pull-right.append-right-10 + = link_to projects_dashboard_path do + Show all + %i.icon-angle-right diff --git a/app/views/dashboard/_sidebar.html.haml b/app/views/dashboard/_sidebar.html.haml index 7c6daf6ec31..fed4b2776ae 100644 --- a/app/views/dashboard/_sidebar.html.haml +++ b/app/views/dashboard/_sidebar.html.haml @@ -1,16 +1,25 @@ -- if @teams.present? - = render "teams", teams: @teams -- if @groups.present? - = render "groups", groups: @groups -= render "projects", projects: @projects -%div +%ul.nav.nav-tabs.dash-sidebar-tabs + %li.active + = link_to '#projects', 'data-toggle' => 'tab', id: 'sidebar-projects-tab' do + Projects + %span.badge= @projects_count + %li + = link_to '#groups', 'data-toggle' => 'tab', id: 'sidebar-groups-tab' do + Groups + %span.badge= @groups.count + +.tab-content + .tab-pane.active#projects + = render "projects", projects: @projects + .tab-pane#groups + = render "groups", groups: @groups + +.prepend-top-20 %span.rss-icon = link_to dashboard_path(:atom, { private_token: current_user.private_token }) do - = image_tag "rss_ui.png", title: "feed" - %strong News Feed + %strong + %i.icon-rss + News Feed %hr -.gitlab-promo - = link_to "Homepage", "http://gitlab.org" - = link_to "Blog", "http://blog.gitlab.org" - = link_to "@gitlabhq", "https://twitter.com/gitlabhq" += render 'shared/promo' diff --git a/app/views/dashboard/_teams.html.haml b/app/views/dashboard/_teams.html.haml deleted file mode 100644 index f56115856a7..00000000000 --- a/app/views/dashboard/_teams.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -.ui-box.teams-box - %h5.title - Teams - %small - (#{@teams.count}) - %span.pull-right - = link_to new_team_path, class: "btn btn-tiny info" do - %i.icon-plus - New Team - %ul.well-list - - @teams.each do |team| - %li - = link_to team_path(id: team.path), class: dom_class(team) do - %strong.well-title= truncate(team.name, length: 35) - %span.pull-right.light - - if team.owner == current_user - %i.icon-wrench - - tm = current_user.user_team_user_relationships.find_by_user_team_id(team.id) - - if tm - = tm.access_human diff --git a/app/views/dashboard/_zero_authorized_projects.html.haml b/app/views/dashboard/_zero_authorized_projects.html.haml index 4b0d0d6873d..79d5dca8845 100644 --- a/app/views/dashboard/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/_zero_authorized_projects.html.haml @@ -1,12 +1,36 @@ -%h3.nothing_here_message - There are no projects you have access to. - %br - - if current_user.can_create_project? - You can create up to - = current_user.projects_limit - projects. Click on button below to add a new one - .link_holder - = link_to new_project_path, class: "btn btn-primary" do - New Project » - - else - If you will be added to project - it will be displayed here +%h3.page-title Welcome to GitLab! +%p.light Self Hosted Git Management application. +%hr +%div + .dashboard-intro-icon + %i.icon-bookmark-empty + %div + %p.slead + You don't have access to any projects right now. + %br + - if current_user.can_create_project? + You can create up to + %strong= pluralize(current_user.projects_limit, "project") + "." + Click on the button below to add a new one + - else + If you are added to a project, it will be displayed here + + - if current_user.can_create_project? + .link_holder + = link_to new_project_path, class: "btn btn-new" do + New project » + +- if current_user.can_create_group? + %hr + %div + .dashboard-intro-icon + %i.icon-group + %div + %p.slead + You can create a group for several dependent projects. + %br + Group is the best way to manage projects and members + .link_holder + = link_to new_group_path, class: "btn btn-new" do + New group » + diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index affe01a7ef9..82880d5ef63 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -1,24 +1,13 @@ -%h3.page_title - Issues - %small (assigned to you) - %small.pull-right #{@issues.total_count} issues +%h3.page-title + Issues assigned to me + %span.pull-right #{@issues.total_count} issues +%p.light + For all issues you should visit the project's issues page, or use the search panel to find a specific issue. %hr .row .span3 - = render 'filter', entity: 'issue' + = render 'shared/filter', entity: 'issue' .span9 - - if @issues.any? - - @issues.group_by(&:project).each do |group| - %div.ui-box - - project = group[0] - %h5.title - = link_to_project project - %ul.well-list.issues_table - - group[1].each do |issue| - = render(partial: 'issues/show', locals: {issue: issue}) - %hr - = paginate @issues, theme: "gitlab" - - else - %p.nothing_here_message Nothing to show here + = render 'shared/issues' diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index a311729dd4d..9c96edeefd5 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -1,11 +1,13 @@ -%h3.page_title +%h3.page-title Merge Requests - %small (authored by or assigned to you) - %small.pull-right #{@merge_requests.total_count} merge requests + %span.pull-right #{@merge_requests.total_count} merge requests + +%p.light + Only merge requests created by you or assigned to you are listed here. %hr .row .span3 - = render 'filter', entity: 'merge_request' + = render 'shared/filter', entity: 'merge_request' .span9 = render 'shared/merge_requests' diff --git a/app/views/dashboard/projects.html.haml b/app/views/dashboard/projects.html.haml index 8e21b0c7e02..0dcb1a87e9a 100644 --- a/app/views/dashboard/projects.html.haml +++ b/app/views/dashboard/projects.html.haml @@ -1,56 +1,89 @@ -%h3.page_title - Projects - %span - (#{@projects.total_count}) - - if current_user.can_create_project? - %span.pull-right - = link_to new_project_path, class: "btn btn-tiny info" do - %i.icon-plus - New Project - - +%h3.page-title My Projects +%p.light + All projects you have access to are listed here. Public projects are not included here unless you are a member %hr .row .span3 %ul.nav.nav-pills.nav-stacked = nav_tab :scope, nil do - = link_to "All", projects_dashboard_path + = link_to projects_dashboard_path do + All + %span.pull-right + = current_user.authorized_projects.count = nav_tab :scope, 'personal' do - = link_to "Personal", projects_dashboard_path(scope: 'personal') + = link_to projects_dashboard_path(scope: 'personal') do + Personal + %span.pull-right + = current_user.personal_projects.count = nav_tab :scope, 'joined' do - = link_to "Joined", projects_dashboard_path(scope: 'joined') + = link_to projects_dashboard_path(scope: 'joined') do + Joined + %span.pull-right + = current_user.authorized_projects.joined(current_user).count + = nav_tab :scope, 'owned' do + = link_to projects_dashboard_path(scope: 'owned') do + Owned + %span.pull-right + = current_user.owned_projects.count + + + - if @groups.present? + %fieldset + %legend Groups + %ul.bordered-list + - @groups.each do |group| + %li{ class: (group.name == params[:group]) ? 'active' : 'light' } + = link_to projects_dashboard_path(group: group.name) do + %i.icon-folder-close-alt + = group.name + %small.pull-right + = group.projects.count + + + + - if @labels.present? + %fieldset + %legend Labels + %ul.bordered-list + - @labels.each do |label| + %li{ class: (label.name == params[:label]) ? 'active' : 'light' } + = link_to projects_dashboard_path(scope: params[:scope], label: label.name) do + %i.icon-tag + = label.name .span9 - = form_tag projects_dashboard_path, method: 'get' do - %fieldset.dashboard-search-filter - = hidden_field_tag "scope", params[:scope] - = search_field_tag "search", params[:search], { placeholder: 'Search', class: 'left input-xxlarge' } - = button_tag type: 'submit', class: 'btn' do - %i.icon-search - - %ul.well-list + %ul.bordered-list.my-projects.top-list - @projects.each do |project| - %li.clearfix - .left - = link_to project_path(project), class: dom_class(project) do - - if project.namespace - = project.namespace.human_name - \/ - %strong.well-title - = truncate(project.name, length: 25) - %br - %small.light - %strong Last activity: - %span= project_last_activity(project) - .pull-right.light - - if project.owner == current_user - %i.icon-wrench - - tm = project.team.get_tm(current_user.id) - - if tm - = tm.project_access_human - - - if @projects.blank? %li - %h3.nothing_here_message There are no projects here. - .bottom= paginate @projects, theme: "gitlab" + %h4.project-title + %span.access-icon + - if project.public + = public_icon + - else + = private_icon + = link_to project_path(project), class: dom_class(project) do + %strong= project.name_with_namespace + + - if project.forked_from_project + %small.pull-right + %i.icon-code-fork + Forked from: + = link_to project.forked_from_project.name_with_namespace, project_path(project.forked_from_project) + .project-info + .pull-right + - project.labels.each do |label| + %span.label.label-info + %i.icon-tag + = label.name + - if project.description.present? + %p= truncate project.description, length: 100 + .last-activity + %span.light Last activity: + %span.date= project_last_activity(project) + + - if @projects.blank? + %li + %h3.nothing_here_message There are no projects here. + .bottom + = paginate @projects, theme: "gitlab" diff --git a/app/views/dashboard/show.atom.builder b/app/views/dashboard/show.atom.builder index 2bb42a65bac..a913df92299 100644 --- a/app/views/dashboard/show.atom.builder +++ b/app/views/dashboard/show.atom.builder @@ -1,18 +1,17 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "Dashboard feed#{" - #{current_user.name}" if current_user.name.present?}" - xml.link :href => projects_url(:atom), :rel => "self", :type => "application/atom+xml" - xml.link :href => projects_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? - event = EventDecorator.decorate(event) xml.entry do - event_link = event.feed_url - event_title = event.feed_title - event_summary = event.feed_summary + 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 diff --git a/app/views/dashboard/show.html.haml b/app/views/dashboard/show.html.haml index 1a66ba4fb37..2305eae1f71 100644 --- a/app/views/dashboard/show.html.haml +++ b/app/views/dashboard/show.html.haml @@ -1,5 +1,5 @@ - if @has_authorized_projects - .projects + .dashboard .activities.span8 = render 'activities' .side.span4 @@ -7,6 +7,3 @@ - else = render "zero_authorized_projects" - -:javascript - dashboardPage(); diff --git a/app/views/deploy_keys/_show.html.haml b/app/views/deploy_keys/_show.html.haml deleted file mode 100644 index 635054350ec..00000000000 --- a/app/views/deploy_keys/_show.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%tr - %td - %a{href: project_deploy_key_path(key.project, key)} - %strong= key.title - %td - %span.update-author - Added - = time_ago_in_words(key.created_at) - ago - %td - = link_to 'Remove', project_deploy_key_path(key.project, key), confirm: 'Are you sure?', method: :delete, class: "btn btn-remove delete-key btn-small pull-right" - diff --git a/app/views/deploy_keys/index.html.haml b/app/views/deploy_keys/index.html.haml deleted file mode 100644 index db167f4e2f2..00000000000 --- a/app/views/deploy_keys/index.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -= render "repositories/head" - -%p.slead - Deploy keys allow read-only access to repository. It matches perfectly for CI, staging or production servers. - - - if can? current_user, :admin_project, @project - = link_to new_project_deploy_key_path(@project), class: "btn btn-small", title: "New Deploy Key" do - Add Deploy Key -- if @keys.any? - %table - %thead - %tr - %th Keys - %th - %th - - @keys.each do |key| - = render(partial: 'show', locals: {key: key}) diff --git a/app/views/deploy_keys/new.html.haml b/app/views/deploy_keys/new.html.haml deleted file mode 100644 index e973cb7d305..00000000000 --- a/app/views/deploy_keys/new.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -= render "repositories/head" - -%h3.page_title New Deploy key -%hr - -= render 'form' diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb index 5399a961570..adc9b672092 100644 --- a/app/views/devise/confirmations/new.html.erb +++ b/app/views/devise/confirmations/new.html.erb @@ -1,6 +1,6 @@ <h2>Resend confirmation instructions</h2> -<%= form_for(resource, :as => resource_name, :url => confirmation_path(resource_name), :html => { :method => :post }) do |f| %> +<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> <%= devise_error_messages! %> <div><%= f.label :email %><br /> @@ -9,4 +9,4 @@ <div><%= f.submit "Resend confirmation instructions" %></div> <% end %> -<%= render :partial => "devise/shared/links" %> +<%= render partial: "devise/shared/links" %> diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb index a6ea8ca17e8..7b4fd526964 100644 --- a/app/views/devise/mailer/confirmation_instructions.html.erb +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -2,4 +2,4 @@ <p>You can confirm your account through the link below:</p> -<p><%= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) %></p> +<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @resource.confirmation_token) %></p> diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb index ae9e888abb9..e1144e943b4 100644 --- a/app/views/devise/mailer/reset_password_instructions.html.erb +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -2,7 +2,7 @@ <p>Someone has requested a link to change your password, and you can do this through the link below.</p> -<p><%= link_to 'Change my password', edit_password_url(@resource, :reset_password_token => @resource.reset_password_token) %></p> +<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @resource.reset_password_token) %></p> <p>If you didn't request this, please ignore this email.</p> <p>Your password won't change until you access the link above and create a new one.</p> diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb index 2263c219522..0429883f05b 100644 --- a/app/views/devise/mailer/unlock_instructions.html.erb +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -4,4 +4,4 @@ <p>Click the link below to unlock your account:</p> -<p><%= link_to 'Unlock my account', unlock_url(@resource, :unlock_token => @resource.unlock_token) %></p> +<p><%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @resource.unlock_token) %></p> diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml index e5800025c6d..d85b4ab08b2 100644 --- a/app/views/devise/passwords/edit.html.haml +++ b/app/views/devise/passwords/edit.html.haml @@ -1,5 +1,4 @@ = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: "login-box" }) do |f| - = image_tag "login-logo.png", width: "304", height: "66", class: "login-logo", alt: "Login Logo" %h3 Change your password = devise_error_messages! = f.hidden_field :reset_password_token diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb deleted file mode 100644 index 0e39f318726..00000000000 --- a/app/views/devise/passwords/new.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { class: "login-box", method: :post }) do |f| %> - <%= image_tag "login-logo.png", width: "304", height: "66", class: "login-logo", alt: "Login Logo" %> - <%= devise_error_messages! %> - <%= f.email_field :email, placeholder: "Email", class: "text" %> - <br/> - <br/> - <%= f.submit "Reset password", class: "btn-primary btn" %> - <div class="pull-right"> <%= link_to "Sign in", new_session_path(resource_name), class: "btn" %><br /></div> -<% end %> diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml new file mode 100644 index 00000000000..3df65d037af --- /dev/null +++ b/app/views/devise/passwords/new.html.haml @@ -0,0 +1,10 @@ += form_for(resource, as: resource_name, url: password_path(resource_name), html: { class: "login-box", method: :post }) do |f| + %h3.page-title Reset password + = devise_error_messages! + = f.email_field :email, placeholder: "Email", class: "text" + %br/ + %br/ + = f.submit "Reset password", class: "btn-primary btn" + .pull-right + = link_to "Sign in", new_session_path(resource_name), class: "btn" + %br/ diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index dd26e8a47b8..139acf28a9f 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -1,6 +1,6 @@ <h2>Edit <%= resource_name.to_s.humanize %></h2> -<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f| %> +<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> <%= devise_error_messages! %> <div><%= f.label :email %><br /> @@ -18,11 +18,11 @@ <div><%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br /> <%= f.password_field :current_password %></div> -<div><%= f.submit "Update", :class => "input_button" %></div> +<div><%= f.submit "Update", class: "input_button" %></div> <% end %> <h3>Cancel my account</h3> -<p>Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), :confirm => "Are you sure?", :method => :delete %>.</p> +<p>Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), confirm: "Are you sure?", method: :delete %>.</p> <%= link_to "Back", :back %> diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml index 12b0438229b..d749d7bac09 100644 --- a/app/views/devise/registrations/new.html.haml +++ b/app/views/devise/registrations/new.html.haml @@ -1,5 +1,6 @@ = form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: "login-box" }) do |f| - = image_tag "login-logo.png", width: "304", height: "66", class: "login-logo", alt: "Login Logo" + %h3.page-title Sign Up + %br = devise_error_messages! %div = f.text_field :name, class: "text top", placeholder: "Name", required: true diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml new file mode 100644 index 00000000000..0c8be9d5c48 --- /dev/null +++ b/app/views/devise/sessions/_new_base.html.haml @@ -0,0 +1,14 @@ += form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| + = f.text_field :login, class: "text top", placeholder: "Username or Email", autofocus: "autofocus" + = f.password_field :password, class: "text bottom", placeholder: "Password" + - if devise_mapping.rememberable? + .clearfix.append-bottom-10 + %label.checkbox.remember_me{for: "user_remember_me"} + = f.check_box :remember_me + %span Remember me + %div + = f.submit "Sign in", class: "btn-create btn" + .pull-right + = link_to "Forgot your password?", new_password_path(resource_name), class: "btn" + + diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index 7968b0e9c9f..575d33949b6 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,29 +1,5 @@ -= form_tag(user_omniauth_callback_path(:ldap), :class => "login-box", :id => 'new_ldap_user' ) do - = image_tag "login-logo.png", :width => "304", :height => "66", :class => "login-logo", :alt => "Login Logo" - = text_field_tag :username, nil, {:class => "text top", :placeholder => "LDAP Login"} - = password_field_tag :password, nil, {:class => "text bottom", :placeholder => "Password"} += form_tag(user_omniauth_callback_path(:ldap), id: 'new_ldap_user' ) do + = text_field_tag :username, nil, {class: "text top", placeholder: "LDAP Login", autofocus: "autofocus"} + = password_field_tag :password, nil, {class: "text bottom", placeholder: "Password"} %br/ - = submit_tag "LDAP Sign in", :class => "btn-primary btn" - - if devise_mapping.omniauthable? - - (resource_class.omniauth_providers - [:ldap]).each do |provider| - %hr/ - = link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider), :class => "btn btn-primary" - %br/ - %hr/ - %a#other_form_toggle{:href => "#", :onclick => "javascript:$('#new_user').toggle();"} Other Sign in - :javascript - $(function() { - $('#new_user').toggle(); - }); -= form_for(resource, :as => resource_name, :url => session_path(resource_name), :html => { :class => "login-box" }) do |f| - = f.text_field :email, :class => "text top", :placeholder => "Email" - = f.password_field :password, :class => "text bottom", :placeholder => "Password" - - if devise_mapping.rememberable? - .clearfix.inputs-list - %label.checkbox.remember_me{:for => "user_remember_me"} - = f.check_box :remember_me - %span Remember me - %br/ - = f.submit "Sign in", :class => "btn-primary btn" - .pull-right - = render :partial => "devise/shared/links" + = submit_tag "LDAP Sign in", class: "btn-create btn" diff --git a/app/views/devise/sessions/_oauth_providers.html.haml b/app/views/devise/sessions/_oauth_providers.html.haml new file mode 100644 index 00000000000..935bc6af505 --- /dev/null +++ b/app/views/devise/sessions/_oauth_providers.html.haml @@ -0,0 +1,11 @@ +- providers = (enabled_oauth_providers - [:ldap]) +- if providers.present? + %hr + %div{:'data-no-turbolink' => 'data-no-turbolink'} + %span Sign in with: + - providers.each do |provider| + %span + - if default_providers.include?(provider) + = link_to authbutton(provider, 32), omniauth_authorize_path(resource_name, provider) + - else + = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), class: "btn" diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index d904e701b8a..c6e1d8db577 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,28 +1,30 @@ -- if ldap_enable? - = render :partial => 'devise/sessions/new_ldap' -- else - = form_for(resource, :as => resource_name, :url => session_path(resource_name), :html => { :class => "login-box" }) do |f| - = image_tag "login-logo.png", :width => "304", :height => "66", :class => "login-logo", :alt => "Login Logo" - = f.email_field :email, :class => "text top", :placeholder => "Email", :autofocus => "autofocus" - = f.password_field :password, :class => "text bottom", :placeholder => "Password" - - if devise_mapping.rememberable? - .clearfix.inputs-list - %label.checkbox.remember_me{:for => "user_remember_me"} - = f.check_box :remember_me - %span Remember me - %br/ - = f.submit "Sign in", :class => "btn-create btn" - .pull-right - = link_to "Forgot your password?", new_password_path(resource_name), :class => "btn" - %br/ - - if Gitlab.config.gitlab.signup_enabled - %hr/ +.login-box + %h3.page-title Sign in + - 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' + .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' + + - else + = render partial: 'devise/sessions/new_base' + + + = render 'devise/sessions/oauth_providers' if devise_mapping.omniauthable? + + - if Gitlab.config.gitlab.signup_enabled + %hr + %div Don't have an account? - = link_to "Sign up", new_registration_path(resource_name) - - if devise_mapping.omniauthable? && resource_class.omniauth_providers.present? - %hr - %div - %span Sign in with: - - resource_class.omniauth_providers.each do |provider| - %span - = link_to authbutton(provider, 32), omniauth_authorize_path(resource_name, provider) + %strong + = link_to "Sign up", new_registration_path(resource_name) + + - if extra_config.has_key?('sign_in_text') + %hr + = markdown(extra_config.sign_in_text) diff --git a/app/views/devise/shared/_links.erb b/app/views/devise/shared/_links.erb index d7499d14ec5..a47b5ff1ec7 100644 --- a/app/views/devise/shared/_links.erb +++ b/app/views/devise/shared/_links.erb @@ -1,5 +1,5 @@ <%- if controller_name != 'sessions' %> - <%= link_to "Sign in", new_session_path(resource_name), :class => "btn" %><br /> + <%= link_to "Sign in", new_session_path(resource_name), class: "btn" %><br /> <% end -%> <%- if devise_mapping.registerable? && controller_name != 'registrations' %> @@ -7,7 +7,7 @@ <% end -%> <%- if devise_mapping.recoverable? && controller_name != 'passwords' %> -<%= link_to "Forgot your password?", new_password_path(resource_name), :class => "btn" %><br /> +<%= link_to "Forgot your password?", new_password_path(resource_name), class: "btn" %><br /> <% end -%> <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb index b787e648ca2..f9277d1673f 100644 --- a/app/views/devise/unlocks/new.html.erb +++ b/app/views/devise/unlocks/new.html.erb @@ -1,6 +1,6 @@ <h2>Resend unlock instructions</h2> -<%= form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => { :method => :post }) do |f| %> +<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> <%= devise_error_messages! %> <div><%= f.label :email %><br /> @@ -9,4 +9,4 @@ <div><%= f.submit "Resend unlock instructions" %></div> <% end %> -<%= render :partial => "devise/shared/links" %> +<%= render partial: "devise/shared/links" %> diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml index f2d082cb77d..6aa78f0c2a8 100644 --- a/app/views/errors/access_denied.html.haml +++ b/app/views/errors/access_denied.html.haml @@ -1,5 +1,5 @@ %h1.http_status_code 403 -%h3.page_title Access Denied +%h3.page-title Access Denied %hr %p You are not allowed to access this page. %p Read more about project permissions #{link_to "here", help_permissions_path, class: "vlink"} diff --git a/app/views/errors/encoding.html.haml b/app/views/errors/encoding.html.haml index a0aa6306489..7021f06dd7f 100644 --- a/app/views/errors/encoding.html.haml +++ b/app/views/errors/encoding.html.haml @@ -1,4 +1,4 @@ %h1.http_status_code 500 -%h3.page_title Encoding Error +%h3.page-title Encoding Error %hr %p Page can't be loaded because of an encoding error. diff --git a/app/views/errors/git_not_found.html.haml b/app/views/errors/git_not_found.html.haml index 5c9c4953284..d8ed7773207 100644 --- a/app/views/errors/git_not_found.html.haml +++ b/app/views/errors/git_not_found.html.haml @@ -1,5 +1,5 @@ %h1.http_status_code 404 -%h3.page_title Git Resource Not found +%h3.page-title Git Resource Not found %hr %p Application can't get access to some branch or commit in your repository. It diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml index ee23d2197b4..4b97ddefc72 100644 --- a/app/views/errors/not_found.html.haml +++ b/app/views/errors/not_found.html.haml @@ -1,4 +1,4 @@ %h1.http_status_code 404 -%h3.page_title The resource you were looking for doesn't exist. +%h3.page-title The resource you were looking for doesn't exist. %hr %p You may have mistyped the address or the page may have moved. diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml index ea417aa9f30..4d4b24009f4 100644 --- a/app/views/events/_commit.html.haml +++ b/app/views/events/_commit.html.haml @@ -1,8 +1,5 @@ -- commit = CommitDecorator.decorate(commit) %li.commit - %p - = link_to commit.short_id(8), project_commit_path(project, commit), class: "commit_short_id" - %span= commit.author_name - – - = image_tag gravatar_icon(commit.author_email), class: "avatar", width: 16 - = gfm escape_once(truncate(commit.title, length: 50)) rescue "--broken encoding" + .commit-row-title + = link_to commit[:id][0..8], project_commit_path(project, commit[:id]), class: "commit_short_id", alt: '' + + = gfm escape_once(truncate(commit[:message], length: 70)) rescue "--broken encoding" diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 719f6c3787f..b3543460d65 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -1,15 +1,15 @@ - if event.proper? - %div.event-item + .event-item{class: "#{event.body? ? "event-block" : "event-inline" }"} %span.cgray.pull-right #{time_ago_in_words(event.created_at)} ago. - = image_tag gravatar_icon(event.author_email), class: "avatar s24" + = cache event do + = image_tag gravatar_icon(event.author_email), class: "avatar s24", alt:'' - - if event.push? - = render "events/event/push", event: event - .clearfix - - elsif event.note? - = render "events/event/note", event: event - - else - = render "events/event/common", event: event + - if event.push? + = render "events/event/push", event: event + - elsif event.note? + = render "events/event/note", event: event + - else + = render "events/event/common", event: event diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml index 2c2f270cf6c..de5634d3c55 100644 --- a/app/views/events/_event_last_push.html.haml +++ b/app/views/events/_event_last_push.html.haml @@ -1,5 +1,5 @@ - if show_last_push_widget?(event) - .event_lp + .event-last-push %span You pushed to = link_to project_commits_path(event.project, event.ref_name) do %strong= truncate(event.ref_name, length: 28) @@ -8,6 +8,7 @@ %span = time_ago_in_words(event.created_at) ago. - - = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-new-mr" do - Create Merge Request + .pull-right + = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-create btn-small" do + Create Merge Request + %hr diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index d09e6e03f01..e44b366040f 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -1,12 +1,12 @@ -%div{:xmlns => "http://www.w3.org/1999/xhtml"} +%div{xmlns: "http://www.w3.org/1999/xhtml"} - event.commits.first(15).each do |commit| %p - %strong= commit.author_name - = link_to "(##{commit.short_id})", project_commit_path(event.project, :id => commit.id) + %strong= commit[:author][:name] + = link_to "(##{commit[:id][0...8]})", project_commit_path(event.project, id: commit[:id]) %i at - = commit.committed_date.strftime("%Y-%m-%d %H:%M:%S") - %blockquote= simple_format(escape_once(commit.safe_message)) + = commit[:timestamp].to_time.to_s(:short) + %blockquote= simple_format(escape_once(commit[:message])) - if event.commits_count > 15 %p %i diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index 53cbe1c94ce..a9d3adf41df 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -2,11 +2,15 @@ %span.author_name= link_to_author event %span.event_label{class: event.action_name}= event_action_name(event) - if event.target - %strong= link_to_gfm truncate(event.target_title), [event.project, event.target] + %strong= link_to "##{event.target_iid}", [event.project, event.target] - else - %strong= gfm truncate(event.target_title) + %strong= gfm event.target_title at - if event.project = link_to_project event.project - else = event.project_name +- if event.target.respond_to?(:title) + .event-body + .event-note + = event.target.title diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index 20c3b927067..db5f3ebb00f 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -1,34 +1,22 @@ .event-title %span.author_name= link_to_author event - %span.event_label commented on - - if event.note_target - - if event.note_commit? - = event.note_target_type - = link_to event.note_short_commit_id, project_commit_path(event.project, event.note_commit_id), class: "commit_short_id" - - else - = link_to [event.project, event.note_target] do - %strong - #{event.note_target_type} ##{truncate event.note_target_id} - - - elsif event.wall_note? - = link_to 'wall', wall_project_path(event.project) - - else - %strong (deleted) - at + %span.event_label commented on #{event_note_title_html(event)} at - if event.project = link_to_project event.project - else = event.project_name .event-body - %i.icon-comment-alt.event-note-icon - %span.event-note - = markdown truncate(event.target.note, length: 70) + .event-note + .md + %i.icon-comment-alt.event-note-icon + = event_note(event.target.note) - note = event.target - if note.attachment.url - = link_to note.attachment.url, target: "_blank", class: 'note-file-attach' do - - if note.attachment.image? + - if note.attachment.image? + = link_to note.attachment.url, target: '_blank' do = image_tag note.attachment.url, class: 'note-image-attach' - - else + - else + = link_to note.attachment.secure_url, target: "_blank", class: 'note-file-attach' do %i.icon-paper-clip = note.attachment_identifier diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 119b8e828d0..adba9a5f619 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -5,7 +5,7 @@ %strong= event.ref_name - else = link_to project_commits_path(event.project, event.ref_name) do - %strong= event.ref_name + %strong= truncate(event.ref_name, length: 30) at %strong= link_to_project event.project @@ -21,5 +21,5 @@ %li.commits-stat - if event.commits_count > 2 %span ... and #{event.commits_count - 2} more commits. - = link_to project_compare_path(event.project, from: event.parent_commit.id, to: event.last_commit.id) do - %strong Compare → #{event.parent_commit.id[0..7]}...#{event.last_commit.id[0..7]} + = 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]} diff --git a/app/views/graph/_head.html.haml b/app/views/graph/_head.html.haml deleted file mode 100644 index fba9a958a19..00000000000 --- a/app/views/graph/_head.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -%h3.page_title Project Network Graph -%hr - -.clearfix - .pull-left - = render partial: 'shared/ref_switcher', locals: {destination: 'graph', path: @path} - - .search.pull-right - = form_tag project_graph_path(@project, params[:id]), method: :get do |f| - .control-group - = label_tag :search , "Looking for commit:", class: 'control-label light' - .controls - = text_field_tag :q, @q, placeholder: "Input SHA", class: "search-input xlarge" - = button_tag type: 'submit', class: 'btn vtop' do - %i.icon-search - diff --git a/app/views/graph/show.html.haml b/app/views/graph/show.html.haml deleted file mode 100644 index e45aca1ddcb..00000000000 --- a/app/views/graph/show.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -= render "head" -.graph_holder - %h4 - %small You can move around the graph by using the arrow keys. - #holder.graph - .loading.loading-gray - -:javascript - var branch_graph; - $(function(){ - branch_graph = new BranchGraph($("#holder"), { - url: '#{project_graph_path(@project, @ref, q: @q, format: :json)}', - commit_url: '#{project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")}', - ref: '#{@ref}', - commit_id: '#{@commit.id}' - }); - }); diff --git a/app/views/groups/_filter.html.haml b/app/views/groups/_filter.html.haml index c14fc8e5e4b..9fbc6c190cc 100644 --- a/app/views/groups/_filter.html.haml +++ b/app/views/groups/_filter.html.haml @@ -1,11 +1,5 @@ = form_tag group_filter_path(entity), method: 'get' do - %fieldset.dashboard-search-filter - = search_field_tag "search", params[:search], { placeholder: 'Search', class: 'search-text-input' } - = button_tag type: 'submit', class: 'btn' do - %i.icon-search - %fieldset - %legend Status: %ul.nav.nav-pills.nav-stacked %li{class: ("active" if !params[:status])} = link_to group_filter_path(entity, status: nil) do @@ -26,6 +20,8 @@ = link_to group_filter_path(entity, project_id: project.id) do = project.name_with_namespace %small.pull-right= entities_per_project(project, entity) + - if @projects.blank? + %p.nothing_here_message This group has no projects yet %fieldset %hr diff --git a/app/views/groups/_new_group_member.html.haml b/app/views/groups/_new_group_member.html.haml index 9cdbea60370..234392c03e1 100644 --- a/app/views/groups/_new_group_member.html.haml +++ b/app/views/groups/_new_group_member.html.haml @@ -1,18 +1,20 @@ -= form_for @team_member, as: :team_member, url: team_members_group_path(@group) do |f| += form_for @users_group, url: group_users_groups_path(@group) do |f| %fieldset - %legend= "New Team member(s) for projects in #{@group.name}" + %legend + New member(s) for + %strong #{@group.name} + group - %h6 1. Choose people you want in the team - .clearfix + %p 1. Choose users you want in the group + .control-group = f.label :user_ids, "People" - .input= select_tag(:user_ids, options_from_collection_for_select(User.active.alphabetically, :id, :name), {data: {placeholder: "Select users"}, class: "chosen xxlarge", multiple: true}) + .controls= users_select_tag(:user_ids, multiple: true, class: 'input-large') - %h6 2. Set access level for them - .clearfix - = f.label :project_access, "Project Access" - .input= select_tag :project_access, options_for_select(Project.access_options, @team_member.project_access), class: "project-access-select chosen" + %p 2. Set access level for them + .control-group + = f.label :group_access, "Group Access" + .controls= select_tag :group_access, options_for_select(UsersGroup.group_access_roles, @users_group.group_access), class: "project-access-select chosen" .form-actions - = hidden_field_tag :redirect_to, people_group_path(@group) - = f.submit 'Add', class: "btn btn-save" + = f.submit 'Add users into group', class: "btn btn-create" diff --git a/app/views/groups/_new_member.html.haml b/app/views/groups/_new_member.html.haml deleted file mode 100644 index b3424b01bcb..00000000000 --- a/app/views/groups/_new_member.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -= form_for @team_member, as: :team_member, url: project_team_members_path(@project, @team_member) do |f| - %fieldset - %legend= "New Team member(s) for #{@project.name}" - - %h6 1. Choose people you want in the team - .clearfix - = f.label :user_ids, "People" - .input= select_tag(:user_ids, options_from_collection_for_select(User.not_in_project(@project).alphabetically, :id, :name), {data: {placeholder: "Select users"}, class: "chosen xxlarge", multiple: true}) - - %h6 2. Set access level for them - .clearfix - = f.label :project_access, "Project Access" - .input= select_tag :project_access, options_for_select(Project.access_options, @team_member.project_access), class: "project-access-select chosen" - - .form-actions - = hidden_field_tag :redirect_to, people_group_path(@group, project_id: @project.id) - = f.submit 'Add', class: "btn btn-save" - diff --git a/app/views/groups/_people_filter.html.haml b/app/views/groups/_people_filter.html.haml deleted file mode 100644 index 901a037adf3..00000000000 --- a/app/views/groups/_people_filter.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -= form_tag people_group_path(@group), method: 'get' do - %fieldset - %legend Projects: - %ul.nav.nav-pills.nav-stacked - - @projects.each do |project| - %li{class: ("active" if params[:project_id] == project.id.to_s)} - = link_to people_group_path(@group, project_id: project.id) do - = project.name_with_namespace - %small.pull-right= project.users.count - - %fieldset - %hr - = link_to "Reset", people_group_path(@group), class: 'btn pull-right' - diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 4fa4a177983..16a3c60f660 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -1,22 +1,21 @@ -.projects_box - %h5.title - Projects - %small - (#{projects.count}) +.ui-box + .title + Projects (#{projects.count}) - if can? current_user, :manage_group, @group %span.pull-right - = link_to new_project_path(namespace_id: @group.id), class: "btn btn-tiny info" do + = link_to new_project_path(namespace_id: @group.id), class: "btn btn-new" do %i.icon-plus - New Project + New project %ul.well-list - if projects.blank? %p.nothing_here_message This groups has no projects yet - projects.each do |project| - %li + %li.project-row = link_to project_path(project), class: dom_class(project) do - %strong.well-title= truncate(project.name, length: 25) + %span.project-name + = truncate(project.name, length: 25) %span.arrow - → - %span.last_activity - %strong Last activity: - %span= project_last_activity(project) + %i.icon-angle-right + %span.last-activity + %span Last activity: + %span.date= project_last_activity(project) diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 7202ef26c70..2682d31ff47 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,50 +1,75 @@ -%h3.page_title Edit Group -%hr -= form_for @group do |f| - - if @group.errors.any? - .alert.alert-error - %span= @group.errors.full_messages.first - .clearfix - = f.label :name do - Group name is - .input - = f.text_field :name, placeholder: "Ex. OpenSource", class: "xxlarge left" - - = f.submit 'Save group', class: "btn btn-save" -%hr +.row + .span2 + %ul.nav.nav-pills.nav-stacked.nav-stacked-menu + %li.active + = link_to '#tab-edit', 'data-toggle' => 'tab' do + %i.icon-edit + Edit Group + %li + = link_to '#tab-projects', 'data-toggle' => 'tab' do + %i.icon-folder-close + Projects + %li + = link_to 'Remove', '#tab-remove', 'data-toggle' => 'tab' + .span10 + .tab-content + .tab-pane.active#tab-edit + .ui-box + .title + %strong= @group.name + group settings: + %div.form-holder + = form_for @group do |f| + - if @group.errors.any? + .alert.alert-error + %span= @group.errors.full_messages.first + .control-group + = f.label :name do + Group name is + .controls + = f.text_field :name, placeholder: "Ex. OpenSource", class: "input-xxlarge left" -.row - .span7 - .ui-box - %h5.title Projects - %ul.well-list - - @group.projects.each do |project| - %li - - if project.public - %i.icon-share - - else - %i.icon-lock.cgreen - = link_to project.name_with_namespace, project - .pull-right - = link_to 'Team', project_team_index_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small" - = link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small" - = link_to 'Remove', project, confirm: "REMOVE #{project.name}? Are you sure?", method: :delete, class: "btn btn-small btn-remove" + .control-group.group-description-holder + = f.label :description, "Details" + .controls + = f.text_area :description, maxlength: 250, class: "input-xxlarge js-gfm-input", rows: 4 + + .form-actions + = f.submit 'Save group', class: "btn btn-save" + + .tab-pane#tab-projects + .ui-box + .title + %strong= @group.name + projects: + - if can? current_user, :manage_group, @group + %span.pull-right + = link_to new_project_path(namespace_id: @group.id), class: "btn btn-tiny" do + %i.icon-plus + New Project + %ul.well-list + - @group.projects.each do |project| + %li + - if project.public + = public_icon + - else + = private_icon + = link_to project.name_with_namespace, project + .pull-right + = link_to 'Members', project_team_index_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small" + = link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small" + = link_to 'Remove', project, confirm: remove_project_message(project), method: :delete, class: "btn btn-small btn-remove" + - if @group.projects.blank? + %p.nothing_here_message This group has no projects yet + + .tab-pane#tab-remove + .ui-box.ui-box-danger + .title Remove group + .ui-box-body + %p + Remove of group will cause removing all child projects and resources. + %p + %strong Removed group can not be restored! - .span5 - .ui-box - %h5.title Transfer group - .padded - %p - Transferring group will cause loss of admin control over group and all child projects - = form_for @group do |f| - = f.select :owner_id, User.all.map { |user| [user.name, user.id] }, {}, {class: 'chosen'} - = f.submit 'Transfer group', class: "btn btn-small" - .ui-box - %h5.title Remove group - .padded.bgred - %p - Remove of group will cause removing all child projects and resources - %br - Removed group can not be restored! - = link_to 'Remove Group', @group, confirm: 'Removed group can not be restored! Are you sure?', method: :delete, class: "btn btn-remove btn-small" + = link_to 'Remove Group', @group, confirm: 'Removed group can not be restored! Are you sure?', method: :delete, class: "btn btn-remove" diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 94682bdd51e..482613f172d 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -1,23 +1,15 @@ -%h3.page_title - Issues - %small (assigned to you) - %small.pull-right #{@issues.total_count} issues +%h3.page-title + Issues assigned to me + %span.pull-right #{@issues.total_count} issues +%p.light + Only issues from + %strong #{@group.name} + group are listed here. To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page. %hr + .row .span3 - = render 'filter', entity: 'issue' + = render 'shared/filter', entity: 'issue' .span9 - - if @issues.any? - - @issues.group_by(&:project).each do |group| - %div.ui-box - - project = group[0] - %h5.title - = link_to_project project - %ul.well-list.issues_table - - group[1].each do |issue| - = render(partial: 'issues/show', locals: {issue: issue}) - %hr - = paginate @issues, theme: "gitlab" - - else - %p.nothing_here_message Nothing to show here + = render 'shared/issues' diff --git a/app/views/groups/members.html.haml b/app/views/groups/members.html.haml new file mode 100644 index 00000000000..124560e4786 --- /dev/null +++ b/app/views/groups/members.html.haml @@ -0,0 +1,20 @@ +%h3.page-title + Group members +%p.light + Members of group have access to all group projects. + Read more about permissions + %strong= link_to "here", help_permissions_path, class: "vlink" + +%hr +- can_manage_group = current_user.can? :manage_group, @group +.ui-box + .title + %strong #{@group.name} + group members + %small + (#{@members.count}) + %ul.well-list + - @members.each do |member| + = render 'users_groups/users_group', member: member, show_controls: can_manage_group +- if can_manage_group + = render "new_group_member" diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index a311729dd4d..8a9b03535bc 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -1,11 +1,14 @@ -%h3.page_title +%h3.page-title Merge Requests - %small (authored by or assigned to you) - %small.pull-right #{@merge_requests.total_count} merge requests + %span.pull-right #{@merge_requests.total_count} merge requests +%p.light + Authored or assigned to you from + %strong #{@group.name} + group. To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. %hr .row .span3 - = render 'filter', entity: 'merge_request' + = render 'shared/filter', entity: 'merge_request' .span9 = render 'shared/merge_requests' diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 73be474e278..02049bb2ee6 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -1,21 +1,28 @@ -%h3.page_title New Group -%hr = form_for @group do |f| - if @group.errors.any? .alert.alert-error %span= @group.errors.full_messages.first - .clearfix + .control-group = f.label :name do Group name is - .input - = f.text_field :name, placeholder: "Ex. OpenSource", class: "xxlarge left" - - = f.submit 'Create group', class: "btn btn-create" - %hr - .padded - %ul - %li Group is kind of directory for several projects - %li All created groups are private - %li People within a group see only projects they have access to - %li All projects of group will be stored in group directory - %li You will be able to move existing projects into group + .controls + = f.text_field :name, placeholder: "Ex. OpenSource", class: "input-xxlarge left" + + .control-group.group-description-holder + = f.label :description, "Details" + .controls + = f.text_area :description, maxlength: 250, class: "input-xxlarge js-gfm-input", rows: 4 + + .control-group + .controls + %ul + %li Group is kind of directory for several projects + %li All created groups are private + %li People within a group see only projects they have access to + %li All projects of group will be stored in a group directory + %li You will be able to move existing projects into group + + .form-actions + = f.submit 'Create group', class: "btn btn-create" + + diff --git a/app/views/groups/people.html.haml b/app/views/groups/people.html.haml deleted file mode 100644 index 3e4eb082f56..00000000000 --- a/app/views/groups/people.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -.row - .span3 - = render 'people_filter' - .span9 - - if can?(current_user, :manage_group, @group) - = render (@project ? "new_member" : "new_group_member") - .ui-box - %h5.title - Team - %small - (#{@users.size}) - %ul.well-list - - @users.each do |user| - %li - = image_tag gravatar_icon(user.email, 16), class: "avatar s16" - %strong= user.name - %span.cgray= user.email - - if @group.owner == user - %span.btn.btn-small.disabled.pull-right Group Owner - diff --git a/app/views/groups/search.html.haml b/app/views/groups/search.html.haml deleted file mode 100644 index f56bbadeac0..00000000000 --- a/app/views/groups/search.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -= form_tag search_group_path(@group), method: :get, class: 'form-inline' do |f| - .padded - = label_tag :search do - %strong Looking for - .input - = search_field_tag :search, params[:search], placeholder: "issue 143", class: "input-xxlarge search-text-input", id: "dashboard_search" - = submit_tag 'Search', class: "btn btn-primary wide" -- if params[:search].present? - = render 'search/result' diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index 9aa52ea5593..edf03642d82 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -1,17 +1,16 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "Dashboard feed#{" - #{current_user.name}" if current_user.name.present?}" - xml.link :href => projects_url(:atom), :rel => "self", :type => "application/atom+xml" - xml.link :href => projects_url, :rel => "alternate", :type => "text/html" + 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.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? - event = EventDecorator.decorate(event) xml.entry do - event_link = event.feed_url - event_title = event.feed_title + 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 diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index a140b401b9d..e613ed3eaa3 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,4 +1,4 @@ -.projects +.dashboard .activities.span8 = render "events/event_last_push", event: @last_push = link_to dashboard_path, class: 'btn btn-tiny' do @@ -6,24 +6,22 @@ %span.cgray You will only see events from projects in this group %hr + = render 'shared/event_filter' - if @events.any? .content_list - else %p.nothing_here_message Project activity will be displayed here .loading.hide .side.span4 + - if @group.description.present? + .description-block + = @group.description = render "projects", projects: @projects - %div - %span.rss-icon - = link_to dashboard_path(:atom, { private_token: current_user.private_token }) do - = image_tag "rss_ui.png", title: "feed" - %strong News Feed + .prepend-top-20 + = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed" do + %strong + %i.icon-rss + News Feed %hr - .gitlab-promo - = link_to "Homepage", "http://gitlabhq.com" - = link_to "Blog", "http://blog.gitlabhq.com" - = link_to "@gitlabhq", "https://twitter.com/gitlabhq" - -:javascript - $(function(){ Pager.init(20, true); }); + = render 'shared/promo' diff --git a/app/views/help/_api_layout.html.haml b/app/views/help/_api_layout.html.haml new file mode 100644 index 00000000000..502cc31a80c --- /dev/null +++ b/app/views/help/_api_layout.html.haml @@ -0,0 +1,13 @@ +.row + .span3 + .append-bottom-20 + = link_to help_path, class: 'btn btn-small' do + %i.icon-angle-left + Back to help + %ul.nav.nav-pills.nav-stacked + - %w(README projects project_snippets repositories deploy_keys users groups session issues milestones merge_requests notes system_hooks).each do |file| + %li{class: file == @category ? 'active' : nil} + = link_to file.titleize, help_api_file_path(file) + + .span9.pull-right + = yield diff --git a/app/views/help/_layout.html.haml b/app/views/help/_layout.html.haml index fa5e3a30b29..ac8660dcbb4 100644 --- a/app/views/help/_layout.html.haml +++ b/app/views/help/_layout.html.haml @@ -2,7 +2,7 @@ .span3{:"data-spy" => 'affix'} .ui-box .title - %h5 Help + Help %ul.well-list %li %strong= link_to "Workflow", help_workflow_path diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml new file mode 100644 index 00000000000..e979e7c0d07 --- /dev/null +++ b/app/views/help/_shortcuts.html.haml @@ -0,0 +1,30 @@ +#modal-shortcuts.modal.hide + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h3 Keyboard Shortcuts + .modal-body + %h5 Global Shortcuts + %p + %span.label.label-inverse s + – + Focus Search + %p + %span.label.label-inverse ? + – + Show this dialog + + %h5 Project Files browsing + %p + %span.label.label-inverse + %i.icon-arrow-up + – + Move selection up + %p + %span.label.label-inverse + %i.icon-arrow-down + – + Move selection down + %p + %span.label.label-inverse Enter + – + Open selection diff --git a/app/views/help/api.html.haml b/app/views/help/api.html.haml index d771f1e9f38..3d2cf50b7f2 100644 --- a/app/views/help/api.html.haml +++ b/app/views/help/api.html.haml @@ -1,105 +1,14 @@ -= render layout: 'help/layout' do - %h3.page_title API - %br - - %ul.nav.nav-tabs.log-tabs.nav-small-tabs - %li.active - = link_to "README", "#README", 'data-toggle' => 'tab' - %li - = link_to "Projects", "#projects", 'data-toggle' => 'tab' - %li - = link_to "Snippets", "#snippets", 'data-toggle' => 'tab' - %li - = link_to "Repositories", "#repositories", 'data-toggle' => 'tab' - %li - = link_to "Users", "#users", 'data-toggle' => 'tab' - %li - = link_to "Session", "#session", 'data-toggle' => 'tab' - %li - = link_to "Issues", "#issues", 'data-toggle' => 'tab' - %li - = link_to "Milestones", "#milestones", 'data-toggle' => 'tab' - %li - = link_to "Notes", "#notes", 'data-toggle' => 'tab' - - .tab-content - .tab-pane.active#README - .file_holder - .file_title - %i.icon-file - README - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "README.md")) - - .tab-pane#projects - .file_holder - .file_title - %i.icon-file - Projects - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "projects.md")) - - .tab-pane#snippets - .file_holder - .file_title - %i.icon-file - Projects Snippets - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "snippets.md")) - - .tab-pane#repositories - .file_holder - .file_title - %i.icon-file - Projects - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "repositories.md")) - - .tab-pane#users - .file_holder - .file_title - %i.icon-file - Users - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "users.md")) - - .tab-pane#session - .file_holder - .file_title - %i.icon-file - Session - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "session.md")) - - .tab-pane#issues - .file_holder - .file_title - %i.icon-file - Issues - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "issues.md")) - - .tab-pane#milestones - .file_holder - .file_title - %i.icon-file - Milestones - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "milestones.md")) - - .tab-pane#notes - .file_holder - .file_title - %i.icon-file - Notes - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "notes.md")) += render layout: 'help/api_layout' do + %h3.page-title + %span.light API + %span + \/ + = @category.titleize + + .file-holder + .file-title + %i.icon-file + = @category + .file-content.wiki + = preserve do + = markdown File.read(Rails.root.join("doc", "api", "#{@category}.md")) diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index 1a4411c8f30..ff01136f5bb 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -1,39 +1,42 @@ -%h3.page_title - GITLAB +%h2.page-title + GitLab .pull-right - %span= Gitlab::Version - %small= Gitlab::Revision -%hr -%p.lead + %span= Gitlab::VERSION + %small= Gitlab::REVISION +%p.slead Self Hosted Git Management %br - Fast, secure and stable solution based on Ruby on Rails & Gitolite. - -%br + Fast, secure and stable solution based on Ruby on Rails. .row .span4 .ui-box .title - %h5 Quick help + Quick help %ul.well-list %li Email your - = mail_to Gitlab.config.gitlab.support_email, "support contact" + = mail_to gitlab_config.support_email, "support contact" %li Use the - = link_to "search bar", '#', onclick: "$("#search").focus();" + = link_to "search bar", '#', onclick: "$('#search').focus();" on the top of this page %li + Use + = link_to "shortcuts", '#', onclick: "new Shortcuts()" + %li Ask in our - = link_to "support forum", "https://groups.google.com/forum/#!forum/gitlabhq" + = link_to "mailing list", "https://groups.google.com/forum/#!forum/gitlabhq" + or on + = link_to "Stack Overflow", "http://stackoverflow.com/questions/tagged/gitlab" %li Browse our = link_to "issue tracker", "https://github.com/gitlabhq/gitlabhq/issues" + .span4 .ui-box .title - %h5 User documentation + User documentation %ul.well-list %li %strong= link_to "Workflow", help_workflow_path @@ -62,7 +65,7 @@ .span4 .ui-box .title - %h5 Admin documentation + Admin documentation %ul.well-list %li diff --git a/app/views/help/markdown.html.haml b/app/views/help/markdown.html.haml index 92c1e49be49..ec9d13f2d6b 100644 --- a/app/views/help/markdown.html.haml +++ b/app/views/help/markdown.html.haml @@ -1,127 +1,6 @@ = render layout: 'help/layout' do - %h3.page_title GitLab Flavored Markdown - %br + %h3.page-title GitLab Flavored Markdown - .row - .span8 - %p - For GitLab we developed something we call "GitLab Flavored Markdown" (GFM). - It extends the standard Markdown in a few significant ways adds some useful functionality. - - %p You can use GFM in: - %ul - %li commit messages - %li comments - %li wall posts - %li issues - %li merge requests - %li milestones - %li wiki pages - - .span4 - .alert.alert-info - %p - If you're not already familiar with Markdown, you should spend 15 minutes and go over the excellent - %strong= link_to "Markdown Syntax Guide", "http://daringfireball.net/projects/markdown/syntax" - at Daring Fireball. - - .row - .span8 - %h3 Differences from traditional Markdown - - %h4 Newlines - - %p - The biggest difference that GFM introduces is in the handling of linebreaks. - With traditional Markdown you can hard wrap paragraphs of text and they will be combined into a single paragraph. We find this to be the cause of a huge number of unintentional formatting errors. - GFM treats newlines in paragraph-like content as real line breaks, which is probably what you intended. - - - %p The next paragraph contains two phrases separated by a single newline character: - %pre= "Roses are red\nViolets are blue" - %p becomes - = markdown "Roses are red\nViolets are blue" - - %h4 Multiple underscores in words - - %p - It is not reasonable to italicize just <em>part</em> of a word, especially when you're dealing with code and names often appear with multiple underscores. - Therefore, GFM ignores multiple underscores in words. - - %pre= "perform_complicated_task\ndo_this_and_do_that_and_another_thing" - %p becomes - = markdown "perform_complicated_task\ndo_this_and_do_that_and_another_thing" - - %h4 URL autolinking - - %p - GFM will autolink standard URLs you copy and paste into your text. - So if you want to link to a URL (instead of a textual link), you can simply put the URL in verbatim and it will be turned into a link to that URL. - - %h4 Fenced code blocks - - %p - Markdown converts text with four spaces at the front of each line to code blocks. - GFM supports that, but we also support fenced blocks. - Just wrap your code blocks in <code>```</code> and you won't need to indent manually to trigger a code block. - - %pre= %Q{```ruby\nrequire 'redcarpet'\nmarkdown = Redcarpet.new("Hello World!")\nputs markdown.to_html\n```} - %p becomes - = markdown %Q{```ruby\nrequire 'redcarpet'\nmarkdown = Redcarpet.new("Hello World!")\nputs markdown.to_html\n```} - - %h4 Emoji - - .row - .span8 - :ruby - puts markdown %Q{Sometimes you want to be :cool: and add some :sparkles: to your :speech_balloon:. Well we have a :gift: for you: - - :exclamation: You can use emoji anywhere GFM is supported. :sunglasses: - - You can use it to point out a :bug: or warn about :monkey:patches. And if someone improves your really :snail: code, send them a :bouquet: or some :candy:. People will :heart: you for that. - - If you are :new: to this, don't be :fearful:. You can easily join the emoji :circus_tent:. All you need to do is to :book: up on the supported codes. - } - - .span4 - .alert.alert-info - %p - Consult the - %strong= link_to "Emoji Cheat Sheet", "http://www.emoji-cheat-sheet.com/" - for a list of all supported emoji codes. - - .row - .span8 - %h4 Special GitLab references - - %p - GFM recognizes special references. - You can easily reference e.g. a team member, an issue or a commit within a project. - GFM will turn that reference into a link so you can navigate between them easily. - - %p GFM will recognize the following references: - %ul - %li - %code @foo - for team members - %li - %code #123 - for issues - %li - %code !123 - for merge request - %li - %code $123 - for snippets - %li - %code 1234567 - for commits - - -# this example will only be shown if the user has a project with at least one issue - - if @project = current_user.authorized_projects.first - - if issue = @project.issues.first - %p For example in your #{link_to @project.name, project_path(@project)} project, writing: - %pre= "This is related to ##{issue.id}. @#{current_user.username} is working on solving it." - %p becomes: - = markdown "This is related to ##{issue.id}. @#{current_user.username} is working on solving it." - - @project = nil # Prevent this from bubbling up to page title + .help_body + = preserve do + = markdown File.read(Rails.root.join("doc", "markdown", "markdown.md")) diff --git a/app/views/help/permissions.html.haml b/app/views/help/permissions.html.haml index 2075753e82a..bab1e7c0a41 100644 --- a/app/views/help/permissions.html.haml +++ b/app/views/help/permissions.html.haml @@ -1,66 +1,210 @@ = render layout: 'help/layout' do - %h3.page_title Permissions - %br + %h3.page-title Permissions + %p.light User has different abilities depends on access level he has in particular group or project + %hr - %fieldset - %legend Guest - %ul - %li Create new issue - %li Leave comments - %li Write on project wall + %h4 Project: + %table.table + %thead + %tr + %th Action + %th Guest + %th Reporter + %th Developer + %th Master + %th Owner + %tbody + %tr + %td Create new issue + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Leave comments + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Write on project wall + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Pull project code + %td + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Download project + %td + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Create code snippets + %td + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Create new merge request + %td + %td + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Create new branches + %td + %td + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Push to non-protected branches + %td + %td + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Remove non-protected branches + %td + %td + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Add tags + %td + %td + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Write a wiki + %td + %td + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Add new team members + %td + %td + %td + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Push to protected branches + %td + %td + %td + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Remove protected branches + %td + %td + %td + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Edit project + %td + %td + %td + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Add Deploy Keys to project + %td + %td + %td + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Configure Project Hooks + %td + %td + %td + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Switch public mode + %td + %td + %td + %td + %td.permission-x ✓ + %tr + %td Transfer project to another namespace + %td + %td + %td + %td + %td.permission-x ✓ + %tr + %td Remove project + %td + %td + %td + %td + %td.permission-x ✓ - %fieldset - %legend Reporter - %ul - %li Create new issue - %li Leave comments - %li Write on project wall - %li Pull project code - %li Download project - %li Create a code snippets - - - %fieldset - %legend Developer - %ul - %li Create new issue - %li Leave comments - %li Write on project wall - %li Pull project code - %li Download project - %li Create new merge request - %li Create a code snippets - %li Create new branches - %li Push to non-protected branches - %li Remove non-protected branches - %li Add tags - %li Write a wiki - - %fieldset - %legend Master - %ul - %li Create new issue - %li Leave comments - %li Write on project wall - %li Pull project code - %li Download project - %li Create new merge request - %li Create a code snippets - %li Create new branches - %li Push to non-protected branches - %li Remove non-protected branches - %li Add tags - %li Write a wiki - %li Add new team members - %li Push to protected branches - %li Remove protected branches - %li Push with force option - %li Edit project - %li Add Deploy Keys to project - %li Configure Project Hooks - - %fieldset - %legend Owner - %ul - %li Transfer project to another namespace - %li Remove project + %h4 Group + %table.table + %thead + %tr + %th Action + %th Guest + %th Reporter + %th Developer + %th Master + %th Owner + %tbody + %tr + %td Browse group + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %td.permission-x ✓ + %tr + %td Edit group + %td + %td + %td + %td + %td.permission-x ✓ + %tr + %td Create project in group + %td + %td + %td + %td + %td.permission-x ✓ + %tr + %td Manage group members + %td + %td + %td + %td + %td.permission-x ✓ + %tr + %td Remove group + %td + %td + %td + %td + %td.permission-x ✓ diff --git a/app/views/help/public_access.html.haml b/app/views/help/public_access.html.haml index 66de17a34ed..c67402ee319 100644 --- a/app/views/help/public_access.html.haml +++ b/app/views/help/public_access.html.haml @@ -1,10 +1,9 @@ = render layout: 'help/layout' do - %h3.page_title Public Access - %br + %h3.page-title Public Access %p GitLab allows you to open selected projects to be accessed publicly. - These projects will be clonable + These projects will be cloneable %em without any authentication. Also they will be listed on the #{link_to "public access directory", public_root_path}. diff --git a/app/views/help/raketasks.html.haml b/app/views/help/raketasks.html.haml index bcc874fc390..17a02e913ee 100644 --- a/app/views/help/raketasks.html.haml +++ b/app/views/help/raketasks.html.haml @@ -1,6 +1,5 @@ = render layout: 'help/layout' do - %h3.page_title GitLab Rake Tasks - %br + %h3.page-title GitLab Rake Tasks %p.slead GitLab provides some specific rake tasks to enable special features or perform maintenance tasks. @@ -19,46 +18,46 @@ .tab-content .tab-pane.active#features - .file_holder - .file_title + .file-holder + .file-title %i.icon-file Features - .file_content.wiki + .file-content.wiki = preserve do = markdown File.read(Rails.root.join("doc", "raketasks", "features.md")) .tab-pane#maintenance - .file_holder - .file_title + .file-holder + .file-title %i.icon-file Maintenance - .file_content.wiki + .file-content.wiki = preserve do = markdown File.read(Rails.root.join("doc", "raketasks", "maintenance.md")) .tab-pane#user_management - .file_holder - .file_title + .file-holder + .file-title %i.icon-file User Management - .file_content.wiki + .file-content.wiki = preserve do = markdown File.read(Rails.root.join("doc", "raketasks", "user_management.md")) .tab-pane#cleanup - .file_holder - .file_title + .file-holder + .file-title %i.icon-file Cleanup - .file_content.wiki + .file-content.wiki = preserve do = markdown File.read(Rails.root.join("doc", "raketasks", "cleanup.md")) .tab-pane#backup_restore - .file_holder - .file_title + .file-holder + .file-title %i.icon-file Backup & Restore - .file_content.wiki + .file-content.wiki = preserve do = markdown File.read(Rails.root.join("doc", "raketasks", "backup_restore.md")) diff --git a/app/views/help/shortcuts.js.haml b/app/views/help/shortcuts.js.haml new file mode 100644 index 00000000000..99ed042ea3b --- /dev/null +++ b/app/views/help/shortcuts.js.haml @@ -0,0 +1,3 @@ +:plain + $("body").append("#{escape_javascript(render('shortcuts'))}"); + $("#modal-shortcuts").modal(); diff --git a/app/views/help/ssh.html.haml b/app/views/help/ssh.html.haml index 114415977b4..72a21b89cab 100644 --- a/app/views/help/ssh.html.haml +++ b/app/views/help/ssh.html.haml @@ -1,6 +1,5 @@ = render layout: 'help/layout' do - %h3.page_title SSH Keys - %br + %h3.page-title SSH Keys %p.slead SSH key allows you to establish a secure connection between your computer and GitLab diff --git a/app/views/help/system_hooks.html.haml b/app/views/help/system_hooks.html.haml index c49011a2269..3e401a6e19f 100644 --- a/app/views/help/system_hooks.html.haml +++ b/app/views/help/system_hooks.html.haml @@ -1,6 +1,5 @@ = render layout: 'help/layout' do - %h3.page_title System hooks - %br + %h3.page-title System hooks %p.slead Your GitLab instance can perform HTTP POST requests on the following events: create_project, delete_project, create_user, delete_user, change_team_member. diff --git a/app/views/help/web_hooks.html.haml b/app/views/help/web_hooks.html.haml index 09745f73614..25865251de3 100644 --- a/app/views/help/web_hooks.html.haml +++ b/app/views/help/web_hooks.html.haml @@ -1,6 +1,5 @@ = render layout: 'help/layout' do - %h3.page_title Web hooks - %br + %h3.page-title Web hooks %p.slead Every GitLab project can trigger a web server whenever the repo is pushed to. @@ -9,5 +8,5 @@ %br GitLab will send POST request with commits information on every push. %h5 Hooks request example: - = render "hooks/data_ex" + = render "projects/hooks/data_ex" diff --git a/app/views/help/workflow.html.haml b/app/views/help/workflow.html.haml index 495b7c6e6fc..2b8950cd5c2 100644 --- a/app/views/help/workflow.html.haml +++ b/app/views/help/workflow.html.haml @@ -1,6 +1,5 @@ = render layout: 'help/layout' do - %h3.page_title Workflow - %br + %h3.page-title Workflow %ol.help %li diff --git a/app/views/issues/_filter.html.haml b/app/views/issues/_filter.html.haml deleted file mode 100644 index 21efaa5357c..00000000000 --- a/app/views/issues/_filter.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -= form_tag project_issues_path(@project), method: 'get' do - %fieldset - %ul.nav.nav-pills.nav-stacked - %li{class: ("active" if !params[:status])} - = link_to project_issues_path(@project, status: nil) do - Open - %li{class: ("active" if params[:status] == 'assigned-to-me')} - = link_to project_issues_path(@project, status: 'assigned-to-me') do - Assigned To Me - %li{class: ("active" if params[:status] == 'closed')} - = link_to project_issues_path(@project, status: 'closed') do - Closed - %li{class: ("active" if params[:status] == 'all')} - = link_to project_issues_path(@project, status: 'all') do - All - - %fieldset - %hr - = link_to "Reset", project_issues_path(@project), class: 'btn pull-right' - diff --git a/app/views/issues/_form.html.haml b/app/views/issues/_form.html.haml deleted file mode 100644 index 6d7613a700d..00000000000 --- a/app/views/issues/_form.html.haml +++ /dev/null @@ -1,85 +0,0 @@ -%div.issue-form-holder - %h3.page_title= @issue.new_record? ? "New Issue" : "Edit Issue ##{@issue.id}" - = form_for [@project, @issue] do |f| - -if @issue.errors.any? - .alert.alert-error - - @issue.errors.full_messages.each do |msg| - %span= msg - %br - .ui-box.ui-box-show - .ui-box-head - .clearfix - = f.label :title do - %strong= "Subject *" - .input - = f.text_field :title, maxlength: 255, class: "xxlarge js-gfm-input", autofocus: true, required: true - .ui-box-body - .clearfix - .issue_assignee.pull-left - = f.label :assignee_id do - %i.icon-user - Assign to - .input= f.select(:assignee_id, @project.users.all.collect {|p| [ p.name, p.id ] }, { include_blank: "Select a user" }, {class: 'chosen'}) - .issue_milestone.pull-left - = f.label :milestone_id do - %i.icon-time - Milestone - .input= f.select(:milestone_id, @project.milestones.active.all.collect {|p| [ p.title, p.id ] }, { include_blank: "Select milestone" }, {class: 'chosen'}) - - .ui-box-bottom - .clearfix - = f.label :label_list do - %i.icon-tag - Labels - .input - = f.text_field :label_list, maxlength: 2000, class: "xxlarge" - %p.hint Separate with comma. - - .clearfix - = f.label :description, "Details" - .input - = f.text_area :description, maxlength: 2000, class: "xxlarge js-gfm-input", rows: 14 - %p.hint Issues are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. - - - .actions - - if @issue.new_record? - = f.submit 'Submit new issue', class: "btn btn-create" - -else - = f.submit 'Save changes', class: "btn-save btn" - - - cancel_path = @issue.new_record? ? project_issues_path(@project) : project_issue_path(@project, @issue) - = link_to "Cancel", cancel_path, class: 'btn btn-cancel' - - - - -:javascript - $(function(){ - $("#issue_label_list") - .bind( "keydown", function( event ) { - if ( event.keyCode === $.ui.keyCode.TAB && - $( this ).data( "autocomplete" ).menu.active ) { - event.preventDefault(); - } - }) - .autocomplete({ - minLength: 0, - source: function( request, response ) { - response( $.ui.autocomplete.filter( - #{raw labels_autocomplete_source}, extractLast( request.term ) ) ); - }, - focus: function() { - return false; - }, - select: function(event, ui) { - var terms = split( this.value ); - terms.pop(); - terms.push( ui.item.value ); - terms.push( "" ); - this.value = terms.join( ", " ); - return false; - } - }); - }); - diff --git a/app/views/issues/_issues.html.haml b/app/views/issues/_issues.html.haml deleted file mode 100644 index 3bbd293dba2..00000000000 --- a/app/views/issues/_issues.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- @issues.each do |issue| - = render(partial: 'issues/show', locals: {issue: issue}) - -- if @issues.present? - %li.bottom - .left= paginate @issues, remote: true, theme: "gitlab" - .pull-right - %span.issue_counter #{@issues.total_count} - issues for this filter -- else - %li - %h4.nothing_here_message Nothing to show here - diff --git a/app/views/issues/index.html.haml b/app/views/issues/index.html.haml deleted file mode 100644 index 875f29e2600..00000000000 --- a/app/views/issues/index.html.haml +++ /dev/null @@ -1,50 +0,0 @@ -= render "issues/head" -.issues_content - %h3.page_title - Issues - %span (<span class=issue_counter>#{@issues.total_count}</span>) - .pull-right - .span5 - - 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-primary pull-right", title: "New Issue", id: "new_issue_link" do - %i.icon-plus - New Issue - = form_tag search_project_issues_path(@project), method: :get, remote: true, id: "issue_search_form", class: 'pull-right' do - = hidden_field_tag :project_id, @project.id, { id: 'project_id' } - = hidden_field_tag :status, params[:status] - = search_field_tag :issue_search, nil, { placeholder: 'Search', class: 'issue_search span3 pull-right neib search-text-input' } - - .clearfix - -.row - .span3 - = render 'filter', entity: 'issue' - .span9 - %div#issues-table-holder.ui-box - .title - = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left" - .clearfix - .issues_bulk_update.hide - = form_tag bulk_update_project_issues_path(@project), method: :post do - %span.update_issues_text Update selected issues with - .left - = select_tag('update[status]', options_for_select(['open', 'closed']), prompt: "Status") - = select_tag('update[assignee_id]', options_from_collection_for_select(@project.users.all, "id", "name", params[:assignee_id]), prompt: "Assignee") - = select_tag('update[milestone_id]', options_from_collection_for_select(issues_active_milestones, "id", "title", params[:milestone_id]), prompt: "Milestone") - = hidden_field_tag 'update[issues_ids]', [] - = hidden_field_tag :status, params[:status] - = button_tag "Save", class: "btn update_selected_issues btn-small btn-save" - .issues_filters - = form_tag project_issues_path(@project), method: :get do - = select_tag(:label_name, options_for_select(issue_tags, params[:label_name]), prompt: "Labels") - = select_tag(:assignee_id, options_from_collection_for_select([unassigned_filter] + @project.users.all, "id", "name", params[:assignee_id]), prompt: "Assignee") - = select_tag(:milestone_id, options_from_collection_for_select([unassigned_filter] + issues_active_milestones, "id", "title", params[:milestone_id]), prompt: "Milestone") - = hidden_field_tag :status, params[:status] - - %ul#issues-table.well-list.issues_table - = render "issues" - -:javascript - $(function(){ - issuesPage(); - }) diff --git a/app/views/issues/index.js.haml b/app/views/issues/index.js.haml deleted file mode 100644 index 48d7f582be2..00000000000 --- a/app/views/issues/index.js.haml +++ /dev/null @@ -1,2 +0,0 @@ -:plain - $('#issues-table').html("#{escape_javascript(render('issues'))}"); diff --git a/app/views/issues/show.html.haml b/app/views/issues/show.html.haml deleted file mode 100644 index 474955cc665..00000000000 --- a/app/views/issues/show.html.haml +++ /dev/null @@ -1,59 +0,0 @@ -%h3.page_title - Issue ##{@issue.id} - - %small - created at - = @issue.created_at.stamp("Aug 21, 2011") - - %span.pull-right - - if can?(current_user, :admin_project, @project) || @issue.author == current_user - - if @issue.closed - = link_to 'Reopen', project_issue_path(@project, @issue, issue: {closed: false }, status_only: true), method: :put, class: "btn grouped reopen_issue" - - else - = link_to 'Close', project_issue_path(@project, @issue, issue: {closed: true }, status_only: true), method: :put, class: "btn grouped close_issue", title: "Close Issue" - - if can?(current_user, :admin_project, @project) || @issue.author == current_user - = link_to edit_project_issue_path(@project, @issue), class: "btn grouped" do - %i.icon-edit - Edit - -.pull-right - .span3#votes= render 'votes/votes_block', votable: @issue - -.back_link - = link_to project_issues_path(@project) do - ← To issues list - - -.ui-box.ui-box-show - .ui-box-head - %h4.box-title - - if @issue.closed - .error.status_info Closed - = gfm escape_once(@issue.title) - - .ui-box-body - %cite.cgray - Created by #{link_to_member(@project, @issue.author)} - - if @issue.assignee - \ and currently assigned to #{link_to_member(@project, @issue.assignee)} - - - if @issue.milestone - - milestone = @issue.milestone - %cite.cgray and attached to milestone - %strong= link_to_gfm truncate(milestone.title, length: 20), project_milestone_path(milestone.project, milestone) - - .pull-right - - @issue.labels.each do |label| - %span.label - %i.icon-tag - = label.name - - - - if @issue.description.present? - .ui-box-bottom - .wiki - = preserve do - = markdown @issue.description - - -.voting_notes#notes= render "notes/notes_with_form" diff --git a/app/views/kaminari/admin/_first_page.html.haml b/app/views/kaminari/admin/_first_page.html.haml deleted file mode 100644 index 41c9c0b3af6..00000000000 --- a/app/views/kaminari/admin/_first_page.html.haml +++ /dev/null @@ -1,9 +0,0 @@ --# Link to the "First" page --# available local variables --# url: url to the first page --# current_page: a page object for the currently displayed page --# num_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%span.first - = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote diff --git a/app/views/kaminari/admin/_gap.html.haml b/app/views/kaminari/admin/_gap.html.haml deleted file mode 100644 index 3ffd12f8587..00000000000 --- a/app/views/kaminari/admin/_gap.html.haml +++ /dev/null @@ -1,9 +0,0 @@ --# Non-link tag that stands for skipped pages... --# available local variables --# current_page: a page object for the currently displayed page --# num_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%li{class: "page"} - %span.page.gap - = raw(t 'views.pagination.truncate') diff --git a/app/views/kaminari/admin/_last_page.html.haml b/app/views/kaminari/admin/_last_page.html.haml deleted file mode 100644 index b03a206224c..00000000000 --- a/app/views/kaminari/admin/_last_page.html.haml +++ /dev/null @@ -1,9 +0,0 @@ --# Link to the "Last" page --# available local variables --# url: url to the last page --# current_page: a page object for the currently displayed page --# num_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%span.last - = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {remote: remote} diff --git a/app/views/kaminari/admin/_next_page.html.haml b/app/views/kaminari/admin/_next_page.html.haml deleted file mode 100644 index 00c5f0b6f4e..00000000000 --- a/app/views/kaminari/admin/_next_page.html.haml +++ /dev/null @@ -1,9 +0,0 @@ --# Link to the "Next" page --# available local variables --# url: url to the next page --# current_page: a page object for the currently displayed page --# num_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%li.next - = link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, rel: 'next', remote: remote diff --git a/app/views/kaminari/admin/_page.html.haml b/app/views/kaminari/admin/_page.html.haml deleted file mode 100644 index a52d883b9a8..00000000000 --- a/app/views/kaminari/admin/_page.html.haml +++ /dev/null @@ -1,10 +0,0 @@ --# Link showing page number --# available local variables --# page: a page object for "this" page --# url: url to this page --# current_page: a page object for the currently displayed page --# num_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%li{class: "page#{' active' if page.current?}"} - = link_to page, url, {remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil} diff --git a/app/views/kaminari/admin/_paginator.html.haml b/app/views/kaminari/admin/_paginator.html.haml deleted file mode 100644 index 6f9fb332261..00000000000 --- a/app/views/kaminari/admin/_paginator.html.haml +++ /dev/null @@ -1,17 +0,0 @@ --# The container tag --# available local variables --# current_page: a page object for the currently displayed page --# num_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote --# paginator: the paginator that renders the pagination tags inside -= paginator.render do - %div.pagination - %ul - = prev_page_tag unless current_page.first? - - each_page do |page| - - if page.left_outer? || page.right_outer? || page.inside_window? - = page_tag page - - elsif !page.was_truncated? - = gap_tag - = next_page_tag unless current_page.last? diff --git a/app/views/kaminari/admin/_prev_page.html.haml b/app/views/kaminari/admin/_prev_page.html.haml deleted file mode 100644 index f673abdb3ae..00000000000 --- a/app/views/kaminari/admin/_prev_page.html.haml +++ /dev/null @@ -1,9 +0,0 @@ --# Link to the "Previous" page --# available local variables --# url: url to the previous page --# current_page: a page object for the currently displayed page --# num_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%li{class: "prev" } - = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote diff --git a/app/views/kaminari/gitlab/_gap.html.haml b/app/views/kaminari/gitlab/_gap.html.haml index f82f185ac35..3ffd12f8587 100644 --- a/app/views/kaminari/gitlab/_gap.html.haml +++ b/app/views/kaminari/gitlab/_gap.html.haml @@ -4,5 +4,6 @@ -# num_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%span.page.gap - = raw(t 'views.pagination.truncate') +%li{class: "page"} + %span.page.gap + = raw(t 'views.pagination.truncate') diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml index 296cceb080b..00c5f0b6f4e 100644 --- a/app/views/kaminari/gitlab/_next_page.html.haml +++ b/app/views/kaminari/gitlab/_next_page.html.haml @@ -5,5 +5,5 @@ -# num_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%span.next +%li.next = link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, rel: 'next', remote: remote diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml index 19456dcc058..a52d883b9a8 100644 --- a/app/views/kaminari/gitlab/_page.html.haml +++ b/app/views/kaminari/gitlab/_page.html.haml @@ -6,5 +6,5 @@ -# num_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%span{class: "page#{' current' if page.current?}"} - = link_to_unless page.current?, page, url, {remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil} +%li{class: "page#{' active' if page.current?}"} + = link_to page, url, {remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil} diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml index 6dd5a5782a2..6f9fb332261 100644 --- a/app/views/kaminari/gitlab/_paginator.html.haml +++ b/app/views/kaminari/gitlab/_paginator.html.haml @@ -6,11 +6,12 @@ -# remote: data-remote -# paginator: the paginator that renders the pagination tags inside = paginator.render do - %nav.gitlab_pagination - = prev_page_tag - - each_page do |page| - - if page.left_outer? || page.right_outer? || page.inside_window? - = page_tag page - - elsif !page.was_truncated? - = gap_tag - = next_page_tag + %div.pagination + %ul + = prev_page_tag unless current_page.first? + - each_page do |page| + - if page.left_outer? || page.right_outer? || page.inside_window? + = page_tag page + - elsif !page.was_truncated? + = gap_tag + = next_page_tag unless current_page.last? diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml index 5c2061690ac..f673abdb3ae 100644 --- a/app/views/kaminari/gitlab/_prev_page.html.haml +++ b/app/views/kaminari/gitlab/_prev_page.html.haml @@ -5,5 +5,5 @@ -# num_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%span.prev +%li{class: "prev" } = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote diff --git a/app/views/keys/_form.html.haml b/app/views/keys/_form.html.haml deleted file mode 100644 index fe26216b1d5..00000000000 --- a/app/views/keys/_form.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -%div - = form_for @key do |f| - -if @key.errors.any? - .alert.alert-error - %ul - - @key.errors.full_messages.each do |msg| - %li= msg - - .clearfix - = f.label :title - .input= f.text_field :title - .clearfix - = f.label :key - .input - = f.text_area :key, class: [:xxlarge, :thin_area] - %p.hint - Paste your public key here. Read more about how generate it - = link_to "here", help_ssh_path - - - .actions - = f.submit 'Save', class: "btn btn-save" - = link_to "Cancel", keys_path, class: "btn btn-cancel" - diff --git a/app/views/keys/_show.html.haml b/app/views/keys/_show.html.haml deleted file mode 100644 index 52bbea6fc7b..00000000000 --- a/app/views/keys/_show.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%tr - %td - = link_to key_path(key) do - %strong= key.title - %td - %span.cgray - Added - = time_ago_in_words(key.created_at) - ago - %td - = link_to 'Remove', key, confirm: 'Are you sure?', method: :delete, class: "btn btn-small btn-remove delete-key pull-right" - diff --git a/app/views/keys/edit.html.haml b/app/views/keys/edit.html.haml deleted file mode 100644 index 60a3afedddc..00000000000 --- a/app/views/keys/edit.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -%h1 Editing key - -= render 'form' - -= link_to 'Show', @key -\| -= link_to 'Back', keys_path diff --git a/app/views/keys/index.html.haml b/app/views/keys/index.html.haml deleted file mode 100644 index 7730b344a7d..00000000000 --- a/app/views/keys/index.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -%h3.page_title - SSH Keys - = link_to "Add new", new_key_path, class: "btn pull-right" - -%hr -%p.slead - SSH key allows you to establish a secure connection between your computer and GitLab - - -%table#keys-table - %thead - %tr - %th Name - %th Added - %th - - @keys.each do |key| - = render(partial: 'show', locals: {key: key}) - - if @keys.blank? - %tr - %td{colspan: 3} - %p.nothing_here_message There are no SSH keys with access to your account. - diff --git a/app/views/keys/show.html.haml b/app/views/keys/show.html.haml deleted file mode 100644 index 059fe5e5806..00000000000 --- a/app/views/keys/show.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -%h3.page_title - Public key: - = @key.title - %small - created at - = @key.created_at.stamp("Aug 21, 2011") -.back_link - = link_to keys_path do - ← To keys list -%hr - -%pre= @key.key -.pull-right - = link_to 'Remove', @key, confirm: 'Are you sure?', method: :delete, class: "btn btn-remove delete-key" diff --git a/app/views/labels/_label.html.haml b/app/views/labels/_label.html.haml deleted file mode 100644 index 027b041d58e..00000000000 --- a/app/views/labels/_label.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -%li - %strong - %i.icon-tag - = label.name - .pull-right - = link_to project_issues_path(label_name: label.name) do - %strong - = pluralize(label.count, 'issue') - = "»" diff --git a/app/views/labels/index.html.haml b/app/views/labels/index.html.haml deleted file mode 100644 index 6eb2c00e56d..00000000000 --- a/app/views/labels/index.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -= render "issues/head" - -%h3.page_title - Labels -%br -%div.ui-box - %ul.well-list.labels-table - - @labels.each do |label| - = render 'label', label: label - - - unless @labels.present? - %li - %h3.nothing_here_message Nothing to show here - diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml index 9961ce8dd34..cc8ea066cb9 100644 --- a/app/views/layouts/_flash.html.haml +++ b/app/views/layouts/_flash.html.haml @@ -1,3 +1,8 @@ -- if text = alert || notice - #flash-container - %h4= text +.flash-container + - if alert + .flash-alert + = alert + + - elsif notice + .flash-notice + = notice diff --git a/app/views/layouts/_google_analytics.html.haml b/app/views/layouts/_google_analytics.html.haml new file mode 100644 index 00000000000..81e03c7eff2 --- /dev/null +++ b/app/views/layouts/_google_analytics.html.haml @@ -0,0 +1,10 @@ +:javascript + var _gaq = _gaq || []; + _gaq.push(['_setAccount', '#{extra_config.google_analytics_id}']); + _gaq.push(['_trackPageview']); + + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 4b4f5da3324..0775abea3dd 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -7,6 +7,9 @@ = stylesheet_link_tag "application" = javascript_include_tag "application" = csrf_meta_tags + = include_gon + + = render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id') -# Atom feed - if current_user diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml index 8f4f3d7815f..6492c122ba0 100644 --- a/app/views/layouts/_head_panel.html.haml +++ b/app/views/layouts/_head_panel.html.haml @@ -8,31 +8,33 @@ %span.separator %h1.project_name= title %ul.nav + %li + %a + %div.hide.turbolink-spinner + %i.icon-refresh.icon-spin + Loading... + %li + = render "layouts/search" + %li + = link_to public_root_path, title: "Public area", class: 'has_bottom_tooltip', 'data-original-title' => 'Public area' do + %i.icon-globe + %li + = link_to user_snippets_path(current_user), title: "My snippets", class: 'has_bottom_tooltip', 'data-original-title' => 'Public area' do + %i.icon-paste - if current_user.is_admin? %li = link_to admin_root_path, title: "Admin area", class: 'has_bottom_tooltip', 'data-original-title' => 'Admin area' do %i.icon-cogs - if current_user.can_create_project? %li - = link_to new_project_path, title: "Create New Project", class: 'has_bottom_tooltip', 'data-original-title' => 'New project' do + = link_to new_project_path, title: "New project", class: 'has_bottom_tooltip', 'data-original-title' => 'New project' do %i.icon-plus %li - = link_to profile_path, title: "My Profile", class: 'has_bottom_tooltip', 'data-original-title' => 'Your profile' do + = link_to profile_path, title: "My profile", class: 'has_bottom_tooltip', 'data-original-title' => 'My profile' do %i.icon-user - %li.separator %li - = render "layouts/search" + = link_to destroy_user_session_path, class: "logout", method: :delete, title: "Logout", class: 'has_bottom_tooltip', 'data-original-title' => 'Logout' do + %i.icon-signout %li - .account-box - = link_to profile_path, class: "pic" do - = image_tag gravatar_icon(current_user.email) - .account-links - = link_to profile_path, class: "username" do - %i.icon-user.icon-white - My profile - = link_to destroy_user_session_path, class: "logout", method: :delete do - %i.icon-signout.icon-white - Logout - - -= render "layouts/init_auto_complete" + = link_to current_user, class: "profile-pic", id: 'profile-pic' do + = image_tag gravatar_icon(current_user.email, 26), alt: '' diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index 8f8c7d8885e..7d16a75af6e 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -1,17 +1,4 @@ :javascript - $(function() { - GitLab.GfmAutoComplete.Members.url = "#{ "/api/v3/projects/#{@project.id}/members" if @project }"; - GitLab.GfmAutoComplete.Members.params.private_token = "#{current_user.private_token}"; - - GitLab.GfmAutoComplete.Emoji.data = #{raw emoji_autocomplete_source}; - // convert the list so that the items have the right format for completion - GitLab.GfmAutoComplete.Emoji.data = $.map(GitLab.GfmAutoComplete.Emoji.data, function(value) { - return { - name: value, - insert: value+':', - image: '#{image_path("emoji")}/'+value+'.png' - } - }); - - GitLab.GfmAutoComplete.setup(); - }); + GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_project_path(@project)}" + GitLab.GfmAutoComplete.Emoji.assetBase = '#{image_path("emoji")}' + GitLab.GfmAutoComplete.setup(); diff --git a/app/views/layouts/_page_title.html.haml b/app/views/layouts/_page_title.html.haml index ea72b25add9..54da5074763 100644 --- a/app/views/layouts/_page_title.html.haml +++ b/app/views/layouts/_page_title.html.haml @@ -1,2 +1,2 @@ -- if content_for?(:page_title) - = yield :page_title +- if content_for?(:page-title) + = yield :page-title diff --git a/app/views/layouts/_public_head_panel.html.haml b/app/views/layouts/_public_head_panel.html.haml new file mode 100644 index 00000000000..3c4bd857c22 --- /dev/null +++ b/app/views/layouts/_public_head_panel.html.haml @@ -0,0 +1,22 @@ +%header.navbar.navbar-static-top.navbar-gitlab + .navbar-inner + .container + %div.app_logo + %span.separator + = link_to public_root_path, class: "home" do + %h1 GITLAB + %span.separator + %h1.project_name + - if @project + = project_title(@project) + - else + Public Projects + + %ul.nav + %li + %a + %div.hide.turbolink-spinner + %i.icon-refresh.icon-spin + Loading... + %li + = link_to "Sign in", new_session_path(:user), class: 'btn btn-sign-in' diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index c484af04704..9a0db99332a 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,13 +1,10 @@ .search = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| - = text_field_tag "search", nil, placeholder: "Search", class: "search-input" + = text_field_tag "search", nil, placeholder: search_placeholder, class: "search-input" = hidden_field_tag :group_id, @group.try(:id) - = hidden_field_tag :project_id, @project.try(:id) - -:javascript - $(function(){ - $("#search").autocomplete({ - source: #{raw search_autocomplete_source}, - select: function(event, ui) { location.href = ui.item.url } - }); - }); + - if @project && @project.persisted? + = hidden_field_tag :project_id, @project.id + = hidden_field_tag :search_code, true + = hidden_field_tag :repository_ref, @ref + = submit_tag 'Go' if ENV['RAILS_ENV'] == 'test' + .search-autocomplete-json.hide{:'data-autocomplete-opts' => search_autocomplete_source } diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index a01886cdabf..3a23cbdb376 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -1,27 +1,11 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "Admin area" - %body{class: "#{app_theme} admin"} - = render "layouts/flash" + %body{class: "#{app_theme} admin", :'data-page' => body_data_page} = render "layouts/head_panel", title: "Admin area" - .container - %ul.main_menu - = nav_link(controller: :dashboard, html_options: {class: 'home'}) do - = link_to admin_root_path, title: "Stats" do - %i.icon-home - = nav_link(controller: :projects) do - = link_to "Projects", admin_projects_path - = nav_link(controller: :teams) do - = link_to "Teams", admin_teams_path - = nav_link(controller: :groups) do - = link_to "Groups", admin_groups_path - = nav_link(controller: :users) do - = link_to "Users", admin_users_path - = nav_link(controller: :logs) do - = link_to "Logs", admin_logs_path - = nav_link(controller: :hooks) do - = link_to "Hooks", admin_hooks_path - = nav_link(controller: :resque) do - = link_to "Background Jobs", admin_resque_path + = render "layouts/flash" + %nav.main-nav + .container= render 'layouts/nav/admin' + .container .content= yield diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 7ee44238d10..792fe5e4a28 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,28 +1,11 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "Dashboard" - %body{class: "#{app_theme} application"} - = render "layouts/flash" + %body{class: "#{app_theme} application", :'data-page' => body_data_page } = render "layouts/head_panel", title: "Dashboard" - .container - %ul.main_menu - = nav_link(path: 'dashboard#show', html_options: {class: 'home'}) do - = link_to root_path, title: "Home" do - %i.icon-home - = nav_link(path: 'dashboard#projects') do - = link_to projects_dashboard_path do - Projects - = nav_link(path: 'dashboard#issues') do - = link_to issues_dashboard_path do - Issues - %span.count= current_user.assigned_issues.opened.count - = nav_link(path: 'dashboard#merge_requests') do - = link_to merge_requests_dashboard_path do - Merge Requests - %span.count= current_user.cared_merge_requests.opened.count - = nav_link(path: 'search#show') do - = link_to "Search", search_path - = nav_link(controller: :help) do - = link_to "Help", help_path + = render "layouts/flash" + %nav.main-nav + .container= render 'layouts/nav/dashboard' + .container .content= yield diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 36c6b4c6c35..c4729836faa 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -3,4 +3,13 @@ = render "layouts/head" %body.ui_basic.login-page = render "layouts/flash" - .container= yield + .container + .content + %center + %h1 GitLab + %p.light + GitLab is open source software to collaborate on code. + %br + #{link_to "Sign in", new_user_session_path} or browse for #{link_to "public projects", public_projects_path}. + %hr + = yield diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 3554d88f10c..df2350b1535 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -2,8 +2,8 @@ %html{ lang: "en"} = render "layouts/head", title: "Error" %body{class: "#{app_theme} application"} + = render "layouts/head_panel", title: "" if current_user = render "layouts/flash" - = render "layouts/head_panel", title: "" .container .content %center.padded.prepend-top-20 diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 9057ad50ce6..0e955d59ff8 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -1,31 +1,11 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "#{@group.name}" - %body{class: "#{app_theme} application"} - = render "layouts/flash" + %body{class: "#{app_theme} application", :'data-page' => body_data_page} = render "layouts/head_panel", title: "group: #{@group.name}" - .container - %ul.main_menu - = nav_link(path: 'groups#show', html_options: {class: 'home'}) do - = link_to group_path(@group), title: "Home" do - %i.icon-home - = nav_link(path: 'groups#issues') do - = link_to issues_group_path(@group) do - Issues - %span.count= current_user.assigned_issues.opened.of_group(@group).count - = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group) do - Merge Requests - %span.count= current_user.cared_merge_requests.opened.of_group(@group).count - = nav_link(path: 'groups#search') do - = link_to "Search", search_group_path(@group) - = nav_link(path: 'groups#people') do - = link_to "People", people_group_path(@group) - - - if can?(current_user, :manage_group, @group) - = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group), class: "tab " do - %i.icon-edit - Edit Group + = render "layouts/flash" + %nav.main-nav + .container= render 'layouts/nav/group' + .container .content= yield diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml new file mode 100644 index 00000000000..73946f99889 --- /dev/null +++ b/app/views/layouts/nav/_admin.html.haml @@ -0,0 +1,17 @@ +%ul + = nav_link(controller: :dashboard, html_options: {class: 'home'}) do + = link_to admin_root_path, title: "Stats" do + %i.icon-home + = nav_link(controller: :projects) do + = link_to "Projects", admin_projects_path + = nav_link(controller: :groups) do + = link_to "Groups", admin_groups_path + = nav_link(controller: :users) do + = link_to "Users", admin_users_path + = nav_link(controller: :logs) do + = link_to "Logs", admin_logs_path + = nav_link(controller: :hooks) do + = link_to "Hooks", admin_hooks_path + = nav_link(controller: :background_jobs) do + = link_to "Background Jobs", admin_background_jobs_path + diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml new file mode 100644 index 00000000000..cae24c3170d --- /dev/null +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -0,0 +1,18 @@ +%ul + = nav_link(path: 'dashboard#show', html_options: {class: 'home'}) do + = link_to root_path, title: "Home" do + %i.icon-home + = nav_link(path: 'dashboard#projects') do + = link_to projects_dashboard_path do + Projects + = nav_link(path: 'dashboard#issues') do + = link_to issues_dashboard_path do + Issues + %span.count= current_user.assigned_issues.opened.count + = nav_link(path: 'dashboard#merge_requests') do + = link_to merge_requests_dashboard_path do + Merge Requests + %span.count= current_user.cared_merge_requests.opened.count + = nav_link(controller: :help) do + = link_to "Help", help_path + diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml new file mode 100644 index 00000000000..a8bb3b91559 --- /dev/null +++ b/app/views/layouts/nav/_group.html.haml @@ -0,0 +1,20 @@ +%ul + = nav_link(path: 'groups#show', html_options: {class: 'home'}) do + = link_to group_path(@group), title: "Home" do + %i.icon-home + = nav_link(path: 'groups#issues') do + = link_to issues_group_path(@group) do + Issues + %span.count= current_user.assigned_issues.opened.of_group(@group).count + = nav_link(path: 'groups#merge_requests') do + = link_to merge_requests_group_path(@group) do + Merge Requests + %span.count= current_user.cared_merge_requests.opened.of_group(@group).count + = nav_link(path: 'groups#members') do + = link_to "Members", members_group_path(@group) + + - if can?(current_user, :manage_group, @group) + = nav_link(path: 'groups#edit') do + = link_to edit_group_path(@group), class: "tab " do + Settings + diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml new file mode 100644 index 00000000000..7c3acfc398a --- /dev/null +++ b/app/views/layouts/nav/_profile.html.haml @@ -0,0 +1,19 @@ +%ul + = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do + = link_to profile_path, title: "Profile" do + %i.icon-home + = nav_link(path: 'profiles#account') do + = link_to "Account", account_profile_path + = nav_link(controller: :notifications) do + = link_to "Notifications", profile_notifications_path + = nav_link(controller: :keys) do + = link_to profile_keys_path do + SSH Keys + %span.count= current_user.keys.count + = nav_link(path: 'profiles#design') do + = link_to "Design", design_profile_path + = nav_link(controller: :groups) do + = link_to "Groups", profile_groups_path + = nav_link(path: 'profiles#history') do + = link_to "History", history_profile_path + diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml new file mode 100644 index 00000000000..e9d535f6972 --- /dev/null +++ b/app/views/layouts/nav/_project.html.haml @@ -0,0 +1,50 @@ +%ul + = nav_link(path: 'projects#show', html_options: {class: "home"}) do + = link_to project_path(@project), title: "Project" do + %i.icon-home + + - if project_nav_tab? :files + = nav_link(controller: %w(tree blob blame edit_tree)) do + = link_to 'Files', project_tree_path(@project, @ref || @repository.root_ref) + + - if project_nav_tab? :commits + = nav_link(controller: %w(commit commits compare repositories protected_branches tags branches)) do + = link_to "Commits", project_commits_path(@project, @ref || @repository.root_ref) + + - if project_nav_tab? :network + = nav_link(controller: %w(network)) do + = link_to "Network", project_network_path(@project, @ref || @repository.root_ref) + + - if project_nav_tab? :graphs + = nav_link(controller: %w(graphs)) do + = link_to "Graphs", project_graph_path(@project, @ref || @repository.root_ref) + + - if project_nav_tab? :issues + = nav_link(controller: %w(issues milestones labels)) do + = link_to url_for_project_issues do + Issues + - if @project.used_default_issues_tracker? + %span.count.issue_counter= @project.issues.opened.count + + - if project_nav_tab? :merge_requests + = nav_link(controller: :merge_requests) do + = link_to project_merge_requests_path(@project) do + Merge Requests + %span.count.merge_counter= @project.merge_requests.opened.count + + - if project_nav_tab? :wiki + = nav_link(controller: :wikis) do + = link_to 'Wiki', project_wiki_path(@project, :home) + + - if project_nav_tab? :wall + = nav_link(controller: :walls) do + = link_to 'Wall', project_wall_path(@project) + + - if project_nav_tab? :snippets + = nav_link(controller: :snippets) do + = link_to 'Snippets', project_snippets_path(@project) + + - if project_nav_tab? :settings + = nav_link(html_options: {class: "#{project_tab_class}"}) do + = link_to edit_project_path(@project), class: "stat-tab tab " do + Settings diff --git a/app/views/layouts/navless.html.haml b/app/views/layouts/navless.html.haml new file mode 100644 index 00000000000..7325664bc19 --- /dev/null +++ b/app/views/layouts/navless.html.haml @@ -0,0 +1,10 @@ +!!! 5 +%html{ lang: "en"} + = render "layouts/head", title: @title + %body{class: "#{app_theme} application", :'data-page' => body_data_page} + = render "layouts/head_panel", title: @title + = render "layouts/flash" + + .container.navless-container + .content + = yield diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 3db1f59b54d..f88abeca887 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -4,29 +4,19 @@ %title GitLab - %body{bgcolor: "#EAEAEA", style: "margin: 0; padding: 0; background: #EAEAEA"} - %table{align: "center", border: "0", cellpadding: "0", cellspacing: "0", style: "padding: 35px 0; background: #EAEAEA;", width: "100%"} + %body + %h1{style: "background: #EEE; border-bottom: 1px solid #DDD; color: #474D57; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} + GitLab + - if @project + \| + = link_to @project.name_with_namespace, project_url(@project), style: 'color: #29B; text-decoration: none' + %table{align: "left", border: "0", cellpadding: "0", cellspacing: "0", style: "padding: 10px 0;", width: "100%"} %tr - %td{align: "center", style: "margin: 0; padding: 0; background: #EAEAEA;"} - %table.header{align: "center", border: "0", cellpadding: "0", cellspacing: "0", style: "font-family: Helvetica, Arial, sans-serif; background:#333", width: "600"} - %tr - %td{style: "font-size: 0px;", width: "20"} - \ - %td{align: "left", style: "padding: 10px 0", width: "580"} - %h1{style: "color: #BBBBBB; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 0; line-height: 32px;"} - GITLAB - - if @project - \/ #{@project.name_with_namespace} - %table{align: "center", bgcolor: "#fff", border: "0", cellpadding: "0", cellspacing: "0", style: "font-family: Helvetica, Arial, sans-serif; background: #fff;", width: "600"} - %tr= yield - %tr - %td{align: "left", colspan: "2", height: "3", style: "padding: font-size: 0; line-height: 0; height: 3px;", width: "600"} - %table.footer{align: "center", border: "0", cellpadding: "0", cellspacing: "0", style: "font-family: Helvetica, Arial, sans-serif; line-height: 10px;", width: "600"} - %tr - %td{align: "center", style: "padding: 5px 0 10px; font-size: 11px; color:#7d7a7a; margin: 0; line-height: 1.2;font-family: Helvetica, Arial, sans-serif;", valign: "top"} - %br - %p{style: "font-size: 11px; color:#7d7a7a; margin: 0; padding: 0; font-family: Helvetica, Arial, sans-serif;"} - You're receiving this notification because you are a member of the - - if @project - #{@project.name_with_namespace} - project team. + %td{align: "left", style: "margin: 0; padding: 10px;"} + = yield + %br + %tr + %td{align: "left", style: "margin: 0; padding: 10px;"} + %p{style: "font-size:small;color:#777"} + - if @project + You're receiving this notification because you are a member of the #{@project.name_with_namespace} project team. diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index 57f250c775b..30a0532bc2b 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,23 +1,11 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "Profile" - %body{class: "#{app_theme} profile"} - = render "layouts/flash" + %body{class: "#{app_theme} profile", :'data-page' => body_data_page} = render "layouts/head_panel", title: "Profile" - .container - %ul.main_menu - = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do - = link_to profile_path, title: "Profile" do - %i.icon-home - = nav_link(path: 'profiles#account') do - = link_to "Account", account_profile_path - = nav_link(controller: :keys) do - = link_to keys_path do - SSH Keys - %span.count= current_user.keys.count - = nav_link(path: 'profiles#design') do - = link_to "Design", design_profile_path - = nav_link(path: 'profiles#history') do - = link_to "History", history_profile_path + = render "layouts/flash" + %nav.main-nav + .container= render 'layouts/nav/profile' + .container .content= yield diff --git a/app/views/layouts/project_resource.html.haml b/app/views/layouts/project_resource.html.haml deleted file mode 100644 index 13fb8637bf6..00000000000 --- a/app/views/layouts/project_resource.html.haml +++ /dev/null @@ -1,45 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: @project.name_with_namespace - %body{class: "#{app_theme} project"} - = render "layouts/flash" - = render "layouts/head_panel", title: project_title(@project) - - if can?(current_user, :download_code, @project) - = render 'shared/no_ssh' - - .container - %ul.main_menu - = nav_link(html_options: {class: "home #{project_tab_class}"}) do - = link_to project_path(@project), title: "Project" do - %i.icon-home - - - if @project.repo_exists? - - if can? current_user, :download_code, @project - = nav_link(controller: %w(tree blob blame)) do - = link_to 'Files', project_tree_path(@project, @ref || @repository.root_ref) - = nav_link(controller: %w(commit commits compare repositories protected_branches)) do - = link_to "Commits", project_commits_path(@project, @ref || @repository.root_ref) - = nav_link(controller: %w(graph)) do - = link_to "Network", project_graph_path(@project, @ref || @repository.root_ref) - - - if @project.issues_enabled - = nav_link(controller: %w(issues milestones labels)) do - = link_to project_issues_filter_path(@project) do - Issues - %span.count.issue_counter= @project.issues.opened.count - - - if @project.repo_exists? && @project.merge_requests_enabled - = nav_link(controller: :merge_requests) do - = link_to project_merge_requests_path(@project) do - Merge Requests - %span.count.merge_counter= @project.merge_requests.opened.count - - - if @project.wall_enabled - = nav_link(path: 'projects#wall') do - = link_to 'Wall', wall_project_path(@project) - - - if @project.wiki_enabled - = nav_link(controller: :wikis) do - = link_to 'Wiki', project_wiki_path(@project, :index) - - .content= yield diff --git a/app/views/layouts/project_settings.html.haml b/app/views/layouts/project_settings.html.haml new file mode 100644 index 00000000000..ea739da73d8 --- /dev/null +++ b/app/views/layouts/project_settings.html.haml @@ -0,0 +1,20 @@ +!!! 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 } + = render "layouts/head_panel", title: project_title(@project) + = render "layouts/init_auto_complete" + = render "layouts/flash" + - if can?(current_user, :download_code, @project) + = render 'shared/no_ssh' + + %nav.main-nav + .container= render 'layouts/nav/project' + + .container + .content + .row + .span2 + = render "projects/settings_nav" + .span10 + = yield diff --git a/app/views/layouts/projects.html.haml b/app/views/layouts/projects.html.haml new file mode 100644 index 00000000000..6d8bf9b710b --- /dev/null +++ b/app/views/layouts/projects.html.haml @@ -0,0 +1,15 @@ +!!! 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 } + = render "layouts/head_panel", title: project_title(@project) + = render "layouts/init_auto_complete" + = render "layouts/flash" + - if can?(current_user, :download_code, @project) + = render 'shared/no_ssh' + + %nav.main-nav + .container= render 'layouts/nav/project' + + .container + .content= yield diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index fa368e9be2e..f922dcc4203 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -1,17 +1,11 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "Public Projects" - %body{class: "#{app_theme} application"} - %header.navbar.navbar-static-top.navbar-gitlab - .navbar-inner - .container - %div.app_logo - %span.separator - = link_to root_path, class: "home" do - %h1 GITLAB - %span.separator - %h1.project_name Public Projects - .container - .content - .prepend-top-20 - = yield + %body{class: "ui_mars application", :'data-page' => body_data_page} + - if current_user + = render "layouts/head_panel", title: "Public Projects" + - else + = render "layouts/public_head_panel" + + .container.navless-container + .content= yield diff --git a/app/views/layouts/public_projects.html.haml b/app/views/layouts/public_projects.html.haml new file mode 100644 index 00000000000..cfe6a63055a --- /dev/null +++ b/app/views/layouts/public_projects.html.haml @@ -0,0 +1,9 @@ +!!! 5 +%html{ lang: "en"} + = render "layouts/head", title: @project.name_with_namespace + %body{class: "ui_mars application", :'data-page' => body_data_page} + = render "layouts/public_head_panel" + %nav.main-nav + .container= render 'layouts/nav/project' + .container + .content= yield diff --git a/app/views/layouts/search.html.haml b/app/views/layouts/search.html.haml new file mode 100644 index 00000000000..177b3a4f8f4 --- /dev/null +++ b/app/views/layouts/search.html.haml @@ -0,0 +1,10 @@ +!!! 5 +%html{ lang: "en"} + = render "layouts/head", title: "Search" + %body{class: "#{app_theme} application", :'data-page' => body_data_page} + = render "layouts/head_panel", title: "Search" + = render "layouts/flash" + + .container.navless-container + .content + = yield diff --git a/app/views/layouts/user_team.html.haml b/app/views/layouts/user_team.html.haml index 19bbc373f46..e64e68d2446 100644 --- a/app/views/layouts/user_team.html.haml +++ b/app/views/layouts/user_team.html.haml @@ -1,39 +1,11 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "#{@team.name}" - %body{class: "#{app_theme} application"} - = render "layouts/flash" + %body{class: "#{app_theme} application", :'data-page' => body_data_page} = render "layouts/head_panel", title: "team: #{@team.name}" - .container - %ul.main_menu - = nav_link(path: 'teams#show', html_options: {class: 'home'}) do - = link_to team_path(@team), title: "Home" do - %i.icon-home - - = nav_link(path: 'teams#issues') do - = link_to issues_team_path(@team) do - Issues - %span.count= Issue.opened.of_user_team(@team).count - - = nav_link(path: 'teams#merge_requests') do - = link_to merge_requests_team_path(@team) do - Merge Requests - %span.count= MergeRequest.opened.of_user_team(@team).count - - = nav_link(controller: [:members]) do - = link_to team_members_path(@team), class: "team-tab tab" do - Members - %span.count= @team.members.count - - - if can? current_user, :admin_user_team, @team - = nav_link(controller: [:projects]) do - = link_to team_projects_path(@team), class: "team-tab tab" do - Projects - %span.count= @team.projects.count - - = nav_link(path: 'teams#edit') do - = link_to edit_team_path(@team), class: "stat-tab tab " do - %i.icon-edit - Edit Team + = render "layouts/flash" + %nav.main-nav + .container= render 'layouts/nav/team' + .container .content= yield diff --git a/app/views/merge_requests/_filter.html.haml b/app/views/merge_requests/_filter.html.haml deleted file mode 100644 index 4b48306ed05..00000000000 --- a/app/views/merge_requests/_filter.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -= form_tag project_issues_path(@project), method: 'get' do - %fieldset - %ul.nav.nav-pills.nav-stacked - %li{class: ("active" if (params[:f] == 'open' || !params[:f]))} - = link_to project_merge_requests_path(@project, f: 'open', milestone_id: params[:milestone_id]) do - Open - %li{class: ("active" if params[:f] == "closed")} - = link_to project_merge_requests_path(@project, f: "closed", milestone_id: params[:milestone_id]) do - Closed - %li{class: ("active" if params[:f] == 'assigned-to-me')} - = link_to project_merge_requests_path(@project, f: 'assigned-to-me', milestone_id: params[:milestone_id]) do - Assigned To Me - %li{class: ("active" if params[:f] == 'all')} - = link_to project_merge_requests_path(@project, f: 'all', milestone_id: params[:milestone_id]) do - All - - %fieldset - %hr - = link_to "Reset", project_merge_requests_path(@project), class: 'btn pull-right' - diff --git a/app/views/merge_requests/_form.html.haml b/app/views/merge_requests/_form.html.haml deleted file mode 100644 index 816c852d24b..00000000000 --- a/app/views/merge_requests/_form.html.haml +++ /dev/null @@ -1,81 +0,0 @@ -= form_for [@project, @merge_request], html: { class: "#{controller.action_name}-merge-request form-horizontal" } do |f| - -if @merge_request.errors.any? - .alert.alert-error - %ul - - @merge_request.errors.full_messages.each do |msg| - %li= msg - - %fieldset - %legend 1. Select Branches - - .row - .span5 - .mr_branch_box - %h5.cgray From (Head Branch) - .body - .padded= f.select(:source_branch, @repository.heads.map(&:name), { include_blank: "Select branch" }, {class: 'chosen span4'}) - .mr_source_commit - - .span2 - %center= image_tag "merge.png", class: 'mr_direction_tip' - .span5 - .mr_branch_box - %h5.cgray To (Base Branch) - .body - .padded= f.select(:target_branch, @repository.heads.map(&:name), { include_blank: "Select branch" }, {class: 'chosen span4'}) - .mr_target_commit - - %fieldset - %legend 2. Fill info - - .ui-box.ui-box-show - .ui-box-head - .clearfix - = f.label :title do - %strong= "Title *" - .input= f.text_field :title, class: "input-xxlarge pad js-gfm-input", maxlength: 255, rows: 5, required: true - .ui-box-body - .clearfix - .left - = f.label :assignee_id do - %i.icon-user - Assign to - .input= f.select(:assignee_id, @project.users.all.collect {|p| [ p.name, p.id ] }, { include_blank: "Select user" }, {class: 'chosen span3'}) - .left - = f.label :milestone_id do - %i.icon-time - Milestone - .input= f.select(:milestone_id, @project.milestones.active.all.collect {|p| [ p.title, p.id ] }, { include_blank: "Select milestone" }, {class: 'chosen'}) - - .control-group - - .form-actions - - if @merge_request.new_record? - = f.submit 'Submit merge request', class: "btn btn-create" - -else - = f.submit 'Save changes', class: "btn btn-save" - - if @merge_request.new_record? - = link_to project_merge_requests_path(@project), class: "btn btn-cancel" do - Cancel - - else - = link_to project_merge_request_path(@project, @merge_request), class: "btn btn-cancel" do - Cancel - -:javascript - $(function(){ - disableButtonIfEmptyField("#merge_request_title", ".btn-save"); - - var source_branch = $("#merge_request_source_branch") - , target_branch = $("#merge_request_target_branch"); - - $.get("#{branch_from_project_merge_requests_path(@project)}", {ref: source_branch.val() }); - $.get("#{branch_to_project_merge_requests_path(@project)}", {ref: target_branch.val() }); - - source_branch.live("change", function() { - $.get("#{branch_from_project_merge_requests_path(@project)}", {ref: $(this).val() }); - }); - - target_branch.live("change", function() { - $.get("#{branch_to_project_merge_requests_path(@project)}", {ref: $(this).val() }); - }); - }); diff --git a/app/views/merge_requests/_merge_request.html.haml b/app/views/merge_requests/_merge_request.html.haml deleted file mode 100644 index 09c55d98465..00000000000 --- a/app/views/merge_requests/_merge_request.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -%li{ class: mr_css_classes(merge_request) } - .pull-right - .left - - if merge_request.merged? - %span.btn.btn-small.disabled.grouped - %strong - %i.icon-ok - = "MERGED" - - if merge_request.notes.any? - %span.btn.btn-small.disabled.grouped - %i.icon-comment - = merge_request.mr_and_commit_notes.count - - if merge_request.milestone_id? - %span.btn.btn-small.disabled.grouped - %i.icon-time - = merge_request.milestone.title - %span.btn.btn-small.disabled.grouped - = merge_request.source_branch - → - = merge_request.target_branch - = image_tag gravatar_icon(merge_request.author_email), class: "avatar" - - %p= link_to_gfm truncate(merge_request.title, length: 80), project_merge_request_path(merge_request.project, merge_request), class: "row_title" - - %span.update-author - %small.cdark= "##{merge_request.id}" - authored by #{merge_request.author_name} - = time_ago_in_words(merge_request.created_at) - ago - - - if merge_request.votes_count > 0 - = render 'votes/votes_inline', votable: merge_request diff --git a/app/views/merge_requests/_show.html.haml b/app/views/merge_requests/_show.html.haml deleted file mode 100644 index cefd33c0cdf..00000000000 --- a/app/views/merge_requests/_show.html.haml +++ /dev/null @@ -1,39 +0,0 @@ -.merge-request - = render "merge_requests/show/mr_title" - = render "merge_requests/show/how_to_merge" - = render "merge_requests/show/mr_box" - = render "merge_requests/show/mr_accept" - - if @project.gitlab_ci? - = render "merge_requests/show/mr_ci" - = render "merge_requests/show/commits" - - - if @commits.present? - %ul.nav.nav-tabs - %li.notes-tab{data: {action: 'notes'}} - = link_to project_merge_request_path(@project, @merge_request) do - %i.icon-comment - Discussion - %li.diffs-tab{data: {action: 'diffs'}} - = link_to diffs_project_merge_request_path(@project, @merge_request) do - %i.icon-list-alt - Diff - - .notes.tab-content.voting_notes#notes{ class: (controller.action_name == 'show') ? "" : "hide" } - = render "notes/notes_with_form" - .diffs.tab-content - = render "merge_requests/show/diffs" if @diffs - .status - -:javascript - var merge_request; - $(function(){ - merge_request = new MergeRequest({ - url_to_automerge_check: "#{automerge_check_project_merge_request_path(@project, @merge_request)}", - check_enable: #{@merge_request.state == MergeRequest::UNCHECKED ? "true" : "false"}, - url_to_ci_check: "#{ci_status_project_merge_request_path(@project, @merge_request)}", - ci_enable: #{@project.gitlab_ci? ? "true" : "false"}, - current_state: "#{@merge_request.human_state}", - action: "#{controller.action_name}" - }); - }); - diff --git a/app/views/merge_requests/branch_from.js.haml b/app/views/merge_requests/branch_from.js.haml deleted file mode 100644 index 0637fdcb72e..00000000000 --- a/app/views/merge_requests/branch_from.js.haml +++ /dev/null @@ -1,2 +0,0 @@ -:plain - $(".mr_source_commit").html("#{commit_to_html(@commit)}"); diff --git a/app/views/merge_requests/branch_to.js.haml b/app/views/merge_requests/branch_to.js.haml deleted file mode 100644 index 974100d1ba7..00000000000 --- a/app/views/merge_requests/branch_to.js.haml +++ /dev/null @@ -1,2 +0,0 @@ -:plain - $(".mr_target_commit").html("#{commit_to_html(@commit)}"); diff --git a/app/views/merge_requests/edit.html.haml b/app/views/merge_requests/edit.html.haml deleted file mode 100644 index eee148994d7..00000000000 --- a/app/views/merge_requests/edit.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%h3.page_title - = "Edit merge request #{@merge_request.id}" -%hr -= render 'form' diff --git a/app/views/merge_requests/index.html.haml b/app/views/merge_requests/index.html.haml deleted file mode 100644 index 3073c8f6bab..00000000000 --- a/app/views/merge_requests/index.html.haml +++ /dev/null @@ -1,35 +0,0 @@ -- if can? current_user, :write_merge_request, @project - = link_to new_project_merge_request_path(@project), class: "pull-right btn btn-primary", title: "New Merge Request" do - %i.icon-plus - New Merge Request -%h3.page_title - Merge Requests - -%br - - -.row - .span3 - = render 'filter' - .span9 - .ui-box - .title - = form_tag project_merge_requests_path(@project), id: "merge_requests_search_form", method: :get, class: :left do - = select_tag(:assignee_id, options_from_collection_for_select([unassigned_filter] + @project.users.all, "id", "name", params[:assignee_id]), prompt: "Assignee") - = select_tag(:milestone_id, options_from_collection_for_select([unassigned_filter] + @project.milestones.order("id desc").all, "id", "title", params[:milestone_id]), prompt: "Milestone") - = hidden_field_tag :f, params[:f] - .clearfix - - %ul.well-list - = render @merge_requests - - if @merge_requests.blank? - %li - %h4.nothing_here_message Nothing to show here - - if @merge_requests.present? - %li.bottom - .left= paginate @merge_requests, theme: "gitlab" - .pull-right - %span.cgray.pull-right #{@merge_requests.total_count} merge requests for this filter - -:javascript - $(merge_requestsPage); diff --git a/app/views/merge_requests/new.html.haml b/app/views/merge_requests/new.html.haml deleted file mode 100644 index 594089995ea..00000000000 --- a/app/views/merge_requests/new.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -%h3.page_title New Merge Request -%hr -= render 'form' diff --git a/app/views/merge_requests/show/_diffs.html.haml b/app/views/merge_requests/show/_diffs.html.haml deleted file mode 100644 index 0807454c4b0..00000000000 --- a/app/views/merge_requests/show/_diffs.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -- if @merge_request.valid_diffs? - = render "commits/diffs", diffs: @diffs -- elsif @merge_request.broken_diffs? - %h4.nothing_here_message - Can't load diff. - You can - = link_to "download it", project_merge_request_path(@project, @merge_request), format: :diff, class: "vlink" - instead. -- else - %h4.nothing_here_message Nothing to merge diff --git a/app/views/merge_requests/show/_how_to_merge.html.haml b/app/views/merge_requests/show/_how_to_merge.html.haml deleted file mode 100644 index 69881d4352f..00000000000 --- a/app/views/merge_requests/show/_how_to_merge.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -%div#modal_merge_info.modal.hide - .modal-header - %a.close{href: "#"} × - %h3 How To Merge - .modal-body - %pre.dark - = preserve do - git checkout #{@merge_request.target_branch} - git fetch origin - git merge origin/#{@merge_request.source_branch} - git push origin #{@merge_request.target_branch} - - -:javascript - $(function(){ - var modal = $('#modal_merge_info').modal({modal: true, show:false}); - $('.how_to_merge_link').bind("click", function(){ - modal.show(); - }); - $('.modal-header .close').bind("click", function(){ - modal.hide(); - }) - }) - diff --git a/app/views/merge_requests/show/_mr_box.html.haml b/app/views/merge_requests/show/_mr_box.html.haml deleted file mode 100644 index 644d7fcc58e..00000000000 --- a/app/views/merge_requests/show/_mr_box.html.haml +++ /dev/null @@ -1,34 +0,0 @@ -.ui-box.ui-box-show - .ui-box-head - %h4.box-title - - if @merge_request.merged - .error.status_info - %i.icon-ok - Merged - - elsif @merge_request.closed - .error.status_info Closed - = gfm escape_once(@merge_request.title) - - .ui-box-body - %div - %cite.cgray - Created at #{@merge_request.created_at.stamp("Aug 21, 2011")} by #{link_to_member(@project, @merge_request.author)} - - if @merge_request.assignee - \, currently assigned to #{link_to_member(@project, @merge_request.assignee)} - - if @merge_request.milestone - - milestone = @merge_request.milestone - %cite.cgray and attached to milestone - %strong= link_to_gfm truncate(milestone.title, length: 20), project_milestone_path(milestone.project, milestone) - - - - if @merge_request.closed - .ui-box-bottom - - if @merge_request.merged? - %span - Merged by #{link_to_member(@project, @merge_request.merge_event.author)} - %small #{time_ago_in_words(@merge_request.merge_event.created_at)} ago. - - elsif @merge_request.closed_event - %span - Closed by #{link_to_member(@project, @merge_request.closed_event.author)} - %small #{time_ago_in_words(@merge_request.closed_event.created_at)} ago. - diff --git a/app/views/milestones/show.html.haml b/app/views/milestones/show.html.haml deleted file mode 100644 index 43d82a54dd6..00000000000 --- a/app/views/milestones/show.html.haml +++ /dev/null @@ -1,94 +0,0 @@ -.row - .span6 - %h3.page_title - Milestone ##{@milestone.id} - %small - = @milestone.expires_at - .back_link - = link_to project_milestones_path(@project) do - ← To milestones list - .span6 - .pull-right - - unless @milestone.closed - = link_to new_project_issue_path(@project, issue: { milestone_id: @milestone.id }), class: "btn btn-small grouped", title: "New Issue" do - %i.icon-plus - New Issue - = link_to 'Browse Issues', project_issues_path(@milestone.project, milestone_id: @milestone.id), class: "btn edit-milestone-link small grouped" - - if can?(current_user, :admin_milestone, @project) - = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-small grouped" do - %i.icon-edit - Edit - - - -- if @milestone.can_be_closed? - %hr - %p - %span All issues for this milestone are closed. You may close milestone now. - = link_to 'Close Milestone', project_milestone_path(@project, @milestone, milestone: {closed: true }), method: :put, class: "btn btn-small btn-remove" - -.ui-box.ui-box-show - .ui-box-head - %h4.box-title - - if @milestone.closed - .error.status_info Closed - - elsif @milestone.expired? - .error.status_info Expired - - = gfm escape_once(@milestone.title) - - .ui-box-body - %p - Progress: - #{@milestone.closed_items_count} closed - – - #{@milestone.open_items_count} open - %span.pull-right= @milestone.expires_at - .progress.progress-info - .bar{style: "width: #{@milestone.percent_complete}%;"} - - - - if @milestone.description.present? - .ui-box-bottom - = preserve do - = markdown @milestone.description - - -.row - .span6 - .ui-box.milestone-issue-filter - .title - %ul.nav.nav-pills - %li.active= link_to('Open Issues', '#') - %li=link_to('All Issues', '#') - %ul.well-list - - @issues.each do |issue| - %li{data: {closed: issue.closed}} - = link_to [@project, issue] do - %span.badge.badge-info ##{issue.id} - – - = link_to_gfm truncate(issue.title, length: 60), [@project, issue] - - .span6 - .ui-box.milestone-merge-requests-filter - .title - %ul.nav.nav-pills - %li.active= link_to('Open Merge Requests', '#') - %li=link_to('All Merge Requests', '#') - %ul.well-list - - @merge_requests.each do |merge_request| - %li{data: {closed: merge_request.closed}} - = link_to [@project, merge_request] do - %span.badge.badge-info ##{merge_request.id} - – - = link_to_gfm truncate(merge_request.title, length: 60), [@project, merge_request] - -%hr -%h6 Participants: -%div - - @users.each do |user| - = link_to tm_path(user.tm_of(@project)), class: 'float-link' do - = user.avatar_image - = user.name - -.clearfix diff --git a/app/views/notes/_diff_notes_with_reply.html.haml b/app/views/notes/_diff_notes_with_reply.html.haml deleted file mode 100644 index 0808f86b090..00000000000 --- a/app/views/notes/_diff_notes_with_reply.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- note = notes.first # example note -%tr.notes_holder - %td.notes_line{ colspan: 2 } - %span.btn.disabled - %i.icon-comment - = notes.count - %td.notes_content - %ul.notes{ rel: note.discussion_id } - = render notes - - = render "notes/discussion_reply_button", note: note diff --git a/app/views/notes/_note.html.haml b/app/views/notes/_note.html.haml deleted file mode 100644 index 4d3007a0ed1..00000000000 --- a/app/views/notes/_note.html.haml +++ /dev/null @@ -1,37 +0,0 @@ -%li{ id: dom_id(note), class: dom_class(note), data: { discussion: note.discussion_id } } - .note-header - .note-actions - = link_to "##{dom_id(note)}", name: dom_id(note) do - %i.icon-link - Link here - - - if(note.author_id == current_user.id) || can?(current_user, :admin_note, @project) - = link_to project_note_path(@project, note), title: "Remove comment", method: :delete, confirm: 'Are you sure you want to remove comment?', remote: true, class: "danger js-note-delete" do - %i.icon-trash.cred - = image_tag gravatar_icon(note.author.email), class: "avatar s32" - = link_to_member(@project, note.author, avatar: false) - %span.note-last-update - = time_ago_in_words(note.updated_at) - ago - - - if note.upvote? - %span.vote.upvote.label.label-success - %i.icon-thumbs-up - \+1 - - if note.downvote? - %span.vote.downvote.label.label-error - %i.icon-thumbs-down - \-1 - - - .note-body - = preserve do - = markdown(note.note) - - if note.attachment.url - - if note.attachment.image? - = image_tag note.attachment.url, class: 'note-image-attach' - .attachment.pull-right - = link_to note.attachment.url, target: "_blank" do - %i.icon-paper-clip - = note.attachment_identifier - .clear diff --git a/app/views/notes/_notes_with_form.html.haml b/app/views/notes/_notes_with_form.html.haml deleted file mode 100644 index 2566edd81ad..00000000000 --- a/app/views/notes/_notes_with_form.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -%ul#notes-list.notes -.js-notes-busy - -.js-main-target-form -- if can? current_user, :write_note, @project - = render "notes/form" - -:javascript - $(function(){ - NoteList.init("#{@target_id}", "#{@target_type}", "#{project_notes_path(@project)}"); - }); diff --git a/app/views/notes/_reversed_notes_with_form.html.haml b/app/views/notes/_reversed_notes_with_form.html.haml deleted file mode 100644 index bb583b8c3b8..00000000000 --- a/app/views/notes/_reversed_notes_with_form.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -.js-main-target-form -- if can? current_user, :write_note, @project - = render "notes/form" - -%ul#new-notes-list.reversed.notes -%ul#notes-list.reversed.notes -.notes-busy.js-notes-busy - -:javascript - $(function(){ - NoteList.init("#{@target_id}", "#{@target_type}", "#{project_notes_path(@project)}"); - }); diff --git a/app/views/notes/index.js.haml b/app/views/notes/index.js.haml deleted file mode 100644 index f0826100fbf..00000000000 --- a/app/views/notes/index.js.haml +++ /dev/null @@ -1,15 +0,0 @@ -- unless @notes.blank? - var notesHtml = "#{escape_javascript(render 'notes/notes')}"; - - new_note_ids = @notes.map(&:id) - - if loading_more_notes? - NoteList.appendMoreNotes(#{new_note_ids}, notesHtml); - - - elsif loading_new_notes? - NoteList.replaceNewNotes(#{new_note_ids}, notesHtml); - - - else - NoteList.setContent(#{new_note_ids}, notesHtml); - -- else - - if loading_more_notes? - NoteList.finishedLoadingMore(); diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml new file mode 100644 index 00000000000..88c4df55b7f --- /dev/null +++ b/app/views/notify/_note_message.html.haml @@ -0,0 +1,6 @@ +%p + %strong #{@note.author_name} + left next message: + +%cite{style: 'color: #666'} + = markdown(@note.note) diff --git a/app/views/notify/closed_issue_email.html.haml b/app/views/notify/closed_issue_email.html.haml new file mode 100644 index 00000000000..325cd44eb4b --- /dev/null +++ b/app/views/notify/closed_issue_email.html.haml @@ -0,0 +1,5 @@ +%p + = "Issue was closed by #{@updated_by.name}" +%p + = "Issue ##{@issue.iid}" + = link_to_gfm truncate(@issue.title, length: 45), project_issue_url(@issue.project, @issue), title: @issue.title diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml new file mode 100644 index 00000000000..49f160a0d5f --- /dev/null +++ b/app/views/notify/closed_issue_email.text.haml @@ -0,0 +1,3 @@ += "Issue was closed by #{@updated_by.name}" + +Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)} diff --git a/app/views/notify/closed_merge_request_email.html.haml b/app/views/notify/closed_merge_request_email.html.haml new file mode 100644 index 00000000000..45770cc85de --- /dev/null +++ b/app/views/notify/closed_merge_request_email.html.haml @@ -0,0 +1,9 @@ +%p + = "Merge Request #{@merge_request.iid} was closed by #{@updated_by.name}" +%p + = link_to_gfm truncate(@merge_request.title, length: 40), project_merge_request_url(@merge_request.target_project, @merge_request) +%p + != merge_path_description(@merge_request, '→') +%p + Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} + diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml new file mode 100644 index 00000000000..ee434ec8cb2 --- /dev/null +++ b/app/views/notify/closed_merge_request_email.text.haml @@ -0,0 +1,8 @@ += "Merge Request #{@merge_request.iid} was closed by #{@updated_by.name}" + +Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} + += merge_path_description(@merge_request, 'to') + +Author: #{@merge_request.author_name} +Assignee: #{@merge_request.assignee_name} diff --git a/app/views/notify/group_access_granted_email.html.haml b/app/views/notify/group_access_granted_email.html.haml new file mode 100644 index 00000000000..5023ec737a5 --- /dev/null +++ b/app/views/notify/group_access_granted_email.html.haml @@ -0,0 +1,5 @@ +%p + = "You have been granted #{@membership.human_access} access to group" +%p + = link_to group_url(@group) do + = @group.name diff --git a/app/views/notify/group_access_granted_email.text.erb b/app/views/notify/group_access_granted_email.text.erb new file mode 100644 index 00000000000..331bb98d5c9 --- /dev/null +++ b/app/views/notify/group_access_granted_email.text.erb @@ -0,0 +1,4 @@ + +You have been granted <%= @membership.human_access %> access to group <%= @group.name %> + +<%= url_for(group_url(@group)) %> diff --git a/app/views/notify/issue_status_changed_email.html.haml b/app/views/notify/issue_status_changed_email.html.haml index 27168eef742..7706b3f7516 100644 --- a/app/views/notify/issue_status_changed_email.html.haml +++ b/app/views/notify/issue_status_changed_email.html.haml @@ -1,16 +1,5 @@ -%td.content{align: "left", style: "font-family: Helvetica, Arial, sans-serif; padding: 20px 0 0;", valign: "top", width: "600"} - %table{border: "0", cellpadding: "0", cellspacing: "0", style: "color: #717171; font: normal 11px Helvetica, Arial, sans-serif; margin: 0; padding: 0;", width: "600"} - %tr - %td{width: "21"} - %td - %h2{style: "color:#646464; font-weight: bold; margin: 0; padding: 0; line-height: 26px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; "} - = "Issue was #{@issue_status} by #{@updated_by.name}" - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %tr - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %td{align: "left", style: "padding: 20px 0 0;"} - %p{style: "color:#646464 !important; line-height: 26px; font-size: 16px; font-family: Helvetica, Arial, sans-serif; "} - = "Issue ##{@issue.id}" - = link_to_gfm truncate(@issue.title, length: 45), project_issue_url(@issue.project, @issue), title: @issue.title - %br - +%p + = "Issue was #{@issue_status} by #{@updated_by.name}" +%p + = "Issue ##{@issue.iid}" + = link_to_gfm truncate(@issue.title, length: 45), project_issue_url(@issue.project, @issue), title: @issue.title diff --git a/app/views/notify/issue_status_changed_email.text.erb b/app/views/notify/issue_status_changed_email.text.erb new file mode 100644 index 00000000000..4200881f7e8 --- /dev/null +++ b/app/views/notify/issue_status_changed_email.text.erb @@ -0,0 +1,4 @@ +Issue was <%= @issue_status %> by <%= @updated_by.name %> + +Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> + diff --git a/app/views/notify/merged_merge_request_email.html.haml b/app/views/notify/merged_merge_request_email.html.haml new file mode 100644 index 00000000000..e2bc9cf5c04 --- /dev/null +++ b/app/views/notify/merged_merge_request_email.html.haml @@ -0,0 +1,9 @@ +%p + = "Merge Request #{@merge_request.iid} was merged" +%p + = link_to_gfm truncate(@merge_request.title, length: 40), project_merge_request_url(@merge_request.target_project, @merge_request) +%p + != merge_path_description(@merge_request, '→') +%p + Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} + diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml new file mode 100644 index 00000000000..550f677fed4 --- /dev/null +++ b/app/views/notify/merged_merge_request_email.text.haml @@ -0,0 +1,8 @@ += "Merge Request #{@merge_request.iid} was merged" + +Merge Request Url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} + += merge_path_description(@merge_request, 'to') + +Author: #{@merge_request.author_name} +Assignee: #{@merge_request.assignee_name} diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index 3cb5351319e..3b3c148517a 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -1,15 +1,9 @@ -%td.content{align: "left", style: "font-family: Helvetica, Arial, sans-serif; padding: 20px 0 0;", valign: "top", width: "600"} - %table{border: "0", cellpadding: "0", cellspacing: "0", style: "color: #717171; font: normal 11px Helvetica, Arial, sans-serif; margin: 0; padding: 0;", width: "600"} - %tr - %td{width: "21"} - %td - %h2{style: "color:#646464; font-weight: bold; margin: 0; padding: 0; line-height: 26px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; "} - New Issue was created and assigned to you. - %td{width: "21"} - %tr - %td{width: "21"} - %td{align: "left", style: "padding: 20px 0 0;"} - %p{style: "color:#646464 !important; line-height: 26px; font-size: 16px; font-family: Helvetica, Arial, sans-serif; "} - = "Issue ##{@issue.id}" - = link_to_gfm truncate(@issue.title, length: 45), project_issue_url(@issue.project, @issue), title: @issue.title - %br +%p + New Issue was created. +%p + = "Issue ##{@issue.iid}" + = link_to_gfm truncate(@issue.title, length: 45), project_issue_url(@issue.project, @issue), title: @issue.title +%p + Author: #{@issue.author_name} +%p + Assignee: #{@issue.assignee_name} diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb new file mode 100644 index 00000000000..d36f54eb1ca --- /dev/null +++ b/app/views/notify/new_issue_email.text.erb @@ -0,0 +1,5 @@ +New Issue was created. + +Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> +Author: <%= @issue.author_name %> +Asignee: <%= @issue.assignee_name %> diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index 990d4d2aa87..6b2782ebb0b 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -1,19 +1,9 @@ -%td.content{align: "left", style: "font-family: Helvetica, Arial, sans-serif; padding: 20px 0 0;", valign: "top", width: "600"} - %table{border: "0", cellpadding: "0", cellspacing: "0", style: "color: #717171; font: normal 11px Helvetica, Arial, sans-serif; margin: 0; padding: 0;", width: "600"} - %tr - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %td{align: "left", style: "padding: 20px 0 0;"} - %h2{style: "color:#646464; font-weight: bold; margin: 0; padding: 0; line-height: 26px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; "} - = "New Merge Request !#{@merge_request.id}" - %p{style: "color:#646464 !important; line-height: 26px; font-size: 16px; font-family: Helvetica, Arial, sans-serif; "} - = link_to_gfm truncate(@merge_request.title, length: 40), project_merge_request_url(@merge_request.project, @merge_request) - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %tr - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %td{style: "padding: 15px 0 15px;", valign: "top"} - %p{style: "color:#767676; font-weight: normal; margin: 0; padding: 0; line-height: 20px; font-size: 12px;font-family: Helvetica, Arial, sans-serif; "} - Branches: #{@merge_request.source_branch} → #{@merge_request.target_branch} - %p{style: "color:#767676; font-weight: normal; margin: 0; padding: 0; line-height: 20px; font-size: 12px;font-family: Helvetica, Arial, sans-serif; "} - Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} - %td +%p + = "New Merge Request !#{@merge_request.iid}" +%p + = link_to_gfm truncate(@merge_request.title, length: 40), project_merge_request_url(@merge_request.target_project, @merge_request) +%p + != merge_path_description(@merge_request, '→') +%p + Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb new file mode 100644 index 00000000000..2d27350486e --- /dev/null +++ b/app/views/notify/new_merge_request_email.text.erb @@ -0,0 +1,8 @@ +New Merge Request <%= @merge_request.iid %> + +<%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %> + +<%= merge_path_description(@merge_request, 'to') %> +Author: <%= @merge_request.author_name %> +Asignee: <%= @merge_request.assignee_name %> + diff --git a/app/views/notify/new_ssh_key_email.html.haml b/app/views/notify/new_ssh_key_email.html.haml new file mode 100644 index 00000000000..deb0822d8f2 --- /dev/null +++ b/app/views/notify/new_ssh_key_email.html.haml @@ -0,0 +1,10 @@ +%p + Hi #{@user.name}! +%p + A new public key was added to your account: +%p + title: + %code= @key.title +%p + If this key was added in error, you can remove it here: + = 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 new file mode 100644 index 00000000000..5f0080c2b76 --- /dev/null +++ b/app/views/notify/new_ssh_key_email.text.erb @@ -0,0 +1,7 @@ +Hi <%= @user.name %>! + +A new public key was added to your account: + +title.................. <%= @key.title %> + +If this key was added in error, you can remove it here: <%= profile_keys_url %> diff --git a/app/views/notify/new_user_email.html.haml b/app/views/notify/new_user_email.html.haml index e8e9735587e..09518cd3c7f 100644 --- a/app/views/notify/new_user_email.html.haml +++ b/app/views/notify/new_user_email.html.haml @@ -1,27 +1,21 @@ -%td.content{align: "left", style: "font-family: Helvetica, Arial, sans-serif; padding: 20px 0 0;", valign: "top", width: "600"} - %table{border: "0", cellpadding: "0", cellspacing: "0", style: "color: #717171; font: normal 11px Helvetica, Arial, sans-serif; margin: 0; padding: 0;", width: "600"} - %tr - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %td{align: "left", style: "padding: 20px 0 0;"} - %h2{style: "color:#646464; font-weight: bold; margin: 0; padding: 0; line-height: 26px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; "} - Hi #{@user['name']}! - %p{style: "color:#767676; font-weight: normal; margin: 0; padding: 0; line-height: 20px; font-size: 12px;font-family: Helvetica, Arial, sans-serif; "} - - if Gitlab.config.gitlab.signup_enabled - Account has been created successfully. - - else - Administrator created account for you. Now you are a member of company GitLab application. - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %tr - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %td{style: "padding: 15px 0 15px;", valign: "top"} - %p{style: "color:#767676; font-weight: normal; margin: 0; padding: 0; line-height: 28px; font-size: 16px;font-family: Helvetica, Arial, sans-serif; "} - login.......................................... - %code= @user['email'] - %p{style: "color:#767676; font-weight: normal; margin: 0; padding: 0; line-height: 28px; font-size: 16px;font-family: Helvetica, Arial, sans-serif; "} - - unless Gitlab.config.gitlab.signup_enabled - password.................................. - %code= @password - %p{style: "color:#767676; font-weight: normal; margin: 0; padding: 0; line-height: 28px; font-size: 12px;font-family: Helvetica, Arial, sans-serif; "} - = link_to "Click here to login", root_url - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} +%p + Hi #{@user['name']}! +%p + - if Gitlab.config.gitlab.signup_enabled + Your account has been created successfully. + - else + The Administrator created an account for you. Now you are a member of the company GitLab application. +%p + login.......................................... + %code= @user['email'] +- if @user.created_by_id + %p + password.................................. + %code= @password + + %p + You will be forced to change this password immediately after login. + +%p + = link_to "Click here to login", root_url diff --git a/app/views/notify/new_user_email.text.erb b/app/views/notify/new_user_email.text.erb new file mode 100644 index 00000000000..c21c95d3047 --- /dev/null +++ b/app/views/notify/new_user_email.text.erb @@ -0,0 +1,13 @@ +Hi <%= @user.name %>! + +The Administrator created an account for you. Now you are a member of the company GitLab application. + +login.................. <%= @user.email %> +<% if @user.created_by_id %> + password............... <%= @password %> + + You will be forced to change this password immediately after login. +<% end %> + + +Click here to login: <%= url_for(root_url) %> diff --git a/app/views/notify/note_commit_email.html.haml b/app/views/notify/note_commit_email.html.haml index e87f9c120e4..620b258fc15 100644 --- a/app/views/notify/note_commit_email.html.haml +++ b/app/views/notify/note_commit_email.html.haml @@ -1,23 +1,5 @@ -%td.content{align: "left", style: "font-family: Helvetica, Arial, sans-serif; padding: 20px 0 0;", valign: "top", width: "600"} - %table{border: "0", cellpadding: "0", cellspacing: "0", style: "color: #717171; font: normal 11px Helvetica, Arial, sans-serif; margin: 0; padding: 0;", width: "600"} - %tr - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %td{align: "left", style: "padding: 20px 0 0;"} - %h2{style: "color:#646464; font-weight: bold; margin: 0; padding: 0; line-height: 26px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; "} - = "New comment for Commit #{@commit.short_id}" - = link_to_gfm truncate(@commit.title, length: 16), project_commit_url(@note.project, id: @commit.id, anchor: "note_#{@note.id}") - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %tr - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %td{style: "padding: 15px 0 15px;", valign: "top"} - %p{style: "color:#767676; font-weight: normal; margin: 0; padding: 0; line-height: 20px; font-size: 12px;font-family: Helvetica, Arial, sans-serif; "} - %a{href: "#", style: "color: #0eb6ce; text-decoration: none;"} #{@note.author_name} - left next message: - %br - %table{border: "0", cellpadding: "0", cellspacing: "0", width: "558"} - %tr - %td{valign: "top"} - %div{ style: "background:#f5f5f5; padding:20px;border:1px solid #ddd" } - = markdown(@note.note) - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} +%p + = "New comment for Commit #{@commit.short_id}" + = link_to_gfm truncate(@commit.title, length: 16), project_commit_url(@note.project, id: @commit.id, anchor: "note_#{@note.id}") += render 'note_message' diff --git a/app/views/notify/note_commit_email.text.erb b/app/views/notify/note_commit_email.text.erb new file mode 100644 index 00000000000..aab8e5cfb6c --- /dev/null +++ b/app/views/notify/note_commit_email.text.erb @@ -0,0 +1,9 @@ +New comment for Commit <%= @commit.short_id %> + +<%= url_for(project_commit_url(@note.project, id: @commit.id, anchor: "note_#{@note.id}")) %> + + +Author: <%= @note.author_name %> + +<%= @note.note %> + diff --git a/app/views/notify/note_issue_email.html.haml b/app/views/notify/note_issue_email.html.haml index 832f5df4463..b3230953e7d 100644 --- a/app/views/notify/note_issue_email.html.haml +++ b/app/views/notify/note_issue_email.html.haml @@ -1,23 +1,4 @@ -%td.content{align: "left", style: "font-family: Helvetica, Arial, sans-serif; padding: 20px 0 0;", valign: "top", width: "600"} - %table{border: "0", cellpadding: "0", cellspacing: "0", style: "color: #717171; font: normal 11px Helvetica, Arial, sans-serif; margin: 0; padding: 0;", width: "600"} - %tr - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %td{align: "left", style: "padding: 20px 0 0;"} - %h2{style: "color:#646464 !important; font-weight: bold; margin: 0; padding: 0; line-height: 26px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; "} - = "New comment for Issue ##{@issue.id}" - = link_to_gfm truncate(@issue.title, length: 35), project_issue_url(@issue.project, @issue, anchor: "note_#{@note.id}") - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %tr - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %td{style: "padding: 15px 0 15px;", valign: "top"} - %p{style: "color:#767676; font-weight: normal; margin: 0; padding: 0; line-height: 20px; font-size: 12px;font-family: Helvetica, Arial, sans-serif; "} - %a{href: "#", style: "color: #0eb6ce; text-decoration: none;"} #{@note.author_name} - left next message: - %br - %table{border: "0", cellpadding: "0", cellspacing: "0", width: "558"} - %tr - %td{valign: "top"} - %div{ style: "background:#f5f5f5; padding:20px;border:1px solid #ddd" } - = markdown(@note.note) - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - +%p + = "New comment for Issue ##{@issue.iid}" + = link_to_gfm truncate(@issue.title, length: 35), project_issue_url(@issue.project, @issue, anchor: "note_#{@note.id}") += render 'note_message' diff --git a/app/views/notify/note_issue_email.text.erb b/app/views/notify/note_issue_email.text.erb new file mode 100644 index 00000000000..8a61f54a337 --- /dev/null +++ b/app/views/notify/note_issue_email.text.erb @@ -0,0 +1,9 @@ +New comment for Issue <%= @issue.iid %> + +<%= url_for(project_issue_url(@issue.project, @issue, anchor: "note_#{@note.id}")) %> + + +Author: <%= @note.author_name %> + +<%= @note.note %> + diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index 3857f2f0318..d587b068486 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,27 +1,8 @@ -%td.content{align: "left", style: "font-family: Helvetica, Arial, sans-serif; padding: 20px 0 0;", valign: "top", width: "600"} - %table{border: "0", cellpadding: "0", cellspacing: "0", style: "color: #717171; font: normal 11px Helvetica, Arial, sans-serif; margin: 0; padding: 0;", width: "600"} - %tr - %td{width: "21"} - %td - %h2{style: "color:#646464; font-weight: normal;"} - - if @note.for_diff_line? - = link_to "New comment on diff", diffs_project_merge_request_url(@merge_request.project, @merge_request, anchor: "note_#{@note.id}") - - else - = link_to "New comment", project_merge_request_url(@merge_request.project, @merge_request, anchor: "note_#{@note.id}") - for Merge Request ##{@merge_request.id} - %cite "#{truncate(@merge_request.title, length: 20)}" - %td{width: "21"} - %tr - %td{width: "21"} - %td - %p - %strong #{@note.author_name} - left next message: - %br - %table{border: "0", cellpadding: "0", cellspacing: "0", width: "558"} - %tr - %td{valign: "top"} - %div{ style: "background:#f5f5f5; padding:10px 20px;border:1px solid #ddd" } - = markdown(@note.note) - %td{width: "21"} - +%p + - if @note.for_diff_line? + = link_to "New comment on diff", diffs_project_merge_request_url(@merge_request.target_project, @merge_request, anchor: "note_#{@note.id}") + - else + = link_to "New comment", project_merge_request_url(@merge_request.target_project, @merge_request, anchor: "note_#{@note.id}") + for Merge Request ##{@merge_request.iid} + %cite "#{truncate(@merge_request.title, length: 20)}" += render 'note_message' diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb new file mode 100644 index 00000000000..79e72ca16c6 --- /dev/null +++ b/app/views/notify/note_merge_request_email.text.erb @@ -0,0 +1,9 @@ +New comment for Merge Request <%= @merge_request.iid %> + +<%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %> + + +<%= @note.author_name %> + +<%= @note.note %> + diff --git a/app/views/notify/note_wall_email.html.haml b/app/views/notify/note_wall_email.html.haml index 0661c5801b8..92200e83efa 100644 --- a/app/views/notify/note_wall_email.html.haml +++ b/app/views/notify/note_wall_email.html.haml @@ -1,22 +1,5 @@ -%td.content{align: "left", style: "font-family: Helvetica, Arial, sans-serif; padding: 20px 0 0;", valign: "top", width: "600"} - %table{border: "0", cellpadding: "0", cellspacing: "0", style: "color: #717171; font: normal 11px Helvetica, Arial, sans-serif; margin: 0; padding: 0;", width: "600"} - %tr - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %td{align: "left", style: "padding: 20px 0 0;"} - %h2{style: "color:#646464; font-weight: bold; margin: 0; padding: 0; line-height: 26px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; "} - New message on - = link_to "Project Wall", wall_project_url(@note.project, anchor: "note_#{@note.id}") - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %tr - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %td{style: "padding: 15px 0 15px;", valign: "top"} - %p{style: "color:#767676; font-weight: normal; margin: 0; padding: 0; line-height: 20px; font-size: 12px;font-family: Helvetica, Arial, sans-serif; "} - %a{href: "#", style: "color: #0eb6ce; text-decoration: none;"} #{@note.author_name} - left next message: - %br - %table{border: "0", cellpadding: "0", cellspacing: "0", width: "558"} - %tr - %td{valign: "top"} - %div{ style: "background:#f5f5f5; padding:20px;border:1px solid #ddd" } - = markdown(@note.note) - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} +%p + New message on + = link_to "Project Wall", project_wall_url(@note.project, anchor: "note_#{@note.id}") + += render 'note_message' diff --git a/app/views/notify/note_wall_email.text.erb b/app/views/notify/note_wall_email.text.erb new file mode 100644 index 00000000000..ee0dc3db1db --- /dev/null +++ b/app/views/notify/note_wall_email.text.erb @@ -0,0 +1,9 @@ +New message on the project wall <%= @note.project %> + +<%= url_for(project_wall_url(@note.project, anchor: "note_#{@note.id}")) %> + + +<%= @note.author_name %> + +<%= @note.note %> + diff --git a/app/views/notify/project_access_granted_email.html.haml b/app/views/notify/project_access_granted_email.html.haml index 11117bf0b33..ce34f825358 100644 --- a/app/views/notify/project_access_granted_email.html.haml +++ b/app/views/notify/project_access_granted_email.html.haml @@ -1,15 +1,5 @@ -%td.content{align: "left", style: "font-family: Helvetica, Arial, sans-serif; padding: 20px 0 0;", valign: "top", width: "600"} - %table{border: "0", cellpadding: "0", cellspacing: "0", style: "color: #717171; font: normal 11px Helvetica, Arial, sans-serif; margin: 0; padding: 0;", width: "600"} - %tr - %td{width: "21"} - %td - %h2{style: "color:#646464;" } - = "You have been granted #{@users_project.project_access_human} access to project" - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %tr - %td{width: "21"} - %td - %h3 - = link_to project_url(@project) do - = @project.name_with_namespace - %br +%p + = "You have been granted #{@users_project.human_access} access to project" +%p + = link_to project_url(@project) do + = @project.name_with_namespace diff --git a/app/views/notify/project_access_granted_email.text.erb b/app/views/notify/project_access_granted_email.text.erb new file mode 100644 index 00000000000..66c57def375 --- /dev/null +++ b/app/views/notify/project_access_granted_email.text.erb @@ -0,0 +1,4 @@ + +You have been granted <%= @users_project.human_access %> access to project <%= @project.name_with_namespace %> + +<%= url_for(project_url(@project)) %> diff --git a/app/views/notify/project_was_moved_email.html.haml b/app/views/notify/project_was_moved_email.html.haml index 222bd0fecea..3e761c43435 100644 --- a/app/views/notify/project_was_moved_email.html.haml +++ b/app/views/notify/project_was_moved_email.html.haml @@ -1,25 +1,11 @@ -%td.content{align: "left", style: "font-family: Helvetica, Arial, sans-serif; padding: 20px 0 0;", valign: "top", width: "600"} - %table{border: "0", cellpadding: "0", cellspacing: "0", style: "color: #555; font: normal 11px Helvetica, Arial, sans-serif; margin: 0; padding: 0;", width: "600"} - %tr - %td{width: "21"} - %td - %h2 - = "Project was moved to another location" - %td{width: "21"} - %tr - %td{width: "21"} - %td - %p - The project is now located under - = link_to project_url(@project) do - = @project.name_with_namespace - %p - To update the remote url in your local repository run: - %br - %table{border: "0", cellpadding: "0", cellspacing: "0", width: "558"} - %tr - %td{valign: "top"} - %p{ style: "background:#f5f5f5; padding:10px; border:1px solid #ddd" } - git remote set-url origin #{@project.ssh_url_to_repo} - %br - %td{ width: "21"} +%p + = "Project was moved to another location" +%p + The project is now located under + = link_to project_url(@project) do + = @project.name_with_namespace +%p + To update the remote url in your local repository run: +%p{ style: "background:#f5f5f5; padding:10px; border:1px solid #ddd" } + git remote set-url origin #{@project.ssh_url_to_repo} +%br diff --git a/app/views/notify/project_was_moved_email.text.erb b/app/views/notify/project_was_moved_email.text.erb new file mode 100644 index 00000000000..7889c7b9cc4 --- /dev/null +++ b/app/views/notify/project_was_moved_email.text.erb @@ -0,0 +1,8 @@ +Project was moved to another location + +The project is now located under +<%= project_url(@project) %> + + +To update the remote url in your local repository run: + git remote set-url origin <%= @project.ssh_url_to_repo %> diff --git a/app/views/notify/reassigned_issue_email.html.haml b/app/views/notify/reassigned_issue_email.html.haml index bc2d6f7078b..b0edff44ce6 100644 --- a/app/views/notify/reassigned_issue_email.html.haml +++ b/app/views/notify/reassigned_issue_email.html.haml @@ -1,16 +1,11 @@ -%td.content{align: "left", style: "font-family: Helvetica, Arial, sans-serif; padding: 20px 0 0;", valign: "top", width: "600"} - %table{border: "0", cellpadding: "0", cellspacing: "0", style: "color: #717171; font: normal 11px Helvetica, Arial, sans-serif; margin: 0; padding: 0;", width: "600"} - %tr - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %td - %h2{style: "color:#646464; font-weight: bold; margin: 0; padding: 0; line-height: 26px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; "} - = "Reassigned Issue ##{@issue.id}" - = link_to_gfm truncate(@issue.title, length: 30), project_issue_url(@issue.project, @issue) - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %tr - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %td{style: "padding: 15px 0 15px;", valign: "top"} - %p{style: "color:#767676; font-weight: normal; margin: 0; padding: 0; line-height: 20px; font-size: 12px;font-family: Helvetica, Arial, sans-serif; "} - Assignee changed from #{@previous_assignee.name} to #{@issue.assignee_name} - %td +%p + = "Reassigned Issue ##{@issue.iid}" + = link_to_gfm truncate(@issue.title, length: 30), project_issue_url(@issue.project, @issue) +%p + Assignee changed + - if @previous_assignee + from + %strong #{@previous_assignee.name} + to + %strong #{@issue.assignee_name} diff --git a/app/views/notify/reassigned_issue_email.text.erb b/app/views/notify/reassigned_issue_email.text.erb new file mode 100644 index 00000000000..bc0d0567922 --- /dev/null +++ b/app/views/notify/reassigned_issue_email.text.erb @@ -0,0 +1,5 @@ +Reassigned Issue <%= @issue.iid %> + +<%= url_for(project_issue_url(@issue.project, @issue)) %> + +Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee %> to <%= @issue.assignee_name %> diff --git a/app/views/notify/reassigned_merge_request_email.html.haml b/app/views/notify/reassigned_merge_request_email.html.haml index 8f7308b3dba..3a615f86e69 100644 --- a/app/views/notify/reassigned_merge_request_email.html.haml +++ b/app/views/notify/reassigned_merge_request_email.html.haml @@ -1,16 +1,10 @@ -%td.content{align: "left", style: "font-family: Helvetica, Arial, sans-serif; padding: 20px 0 0;", valign: "top", width: "600"} - %table{border: "0", cellpadding: "0", cellspacing: "0", style: "color: #717171; font: normal 11px Helvetica, Arial, sans-serif; margin: 0; padding: 0;", width: "600"} - %tr - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %td{align: "left", style: "padding: 20px 0 0;"} - %h2{style: "color:#646464; font-weight: bold; margin: 0; padding: 0; line-height: 26px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; "} - = "Reassigned Merge Request !#{@merge_request.id}" - = link_to_gfm truncate(@merge_request.title, length: 30), project_merge_request_url(@merge_request.project, @merge_request) - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %tr - %td{style: "font-size: 1px; line-height: 1px;", width: "21"} - %td{style: "padding: 15px 0 15px;", valign: "top"} - %p{style: "color:#767676; font-weight: normal; margin: 0; padding: 0; line-height: 20px; font-size: 12px;font-family: Helvetica, Arial, sans-serif; "} - Assignee changed from #{@previous_assignee.name} to #{@merge_request.assignee_name} - %td - +%p + = "Reassigned Merge Request !#{@merge_request.iid}" + = link_to_gfm truncate(@merge_request.title, length: 30), project_merge_request_url(@merge_request.target_project, @merge_request) +%p + Assignee changed + - if @previous_assignee + from + %strong #{@previous_assignee.name} + to + %strong #{@merge_request.assignee_name} diff --git a/app/views/notify/reassigned_merge_request_email.text.erb b/app/views/notify/reassigned_merge_request_email.text.erb new file mode 100644 index 00000000000..eecf055ff6f --- /dev/null +++ b/app/views/notify/reassigned_merge_request_email.text.erb @@ -0,0 +1,7 @@ +Reassigned Merge Request <%= @merge_request.iid %> + +<%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %> + + +Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee %> to <%= @merge_request.assignee_name %> + diff --git a/app/views/profiles/account.html.haml b/app/views/profiles/account.html.haml index 5465d1f96e9..42c7ec051cb 100644 --- a/app/views/profiles/account.html.haml +++ b/app/views/profiles/account.html.haml @@ -1,86 +1,141 @@ -- if Gitlab.config.omniauth.enabled - %fieldset - %legend Social Accounts - .oauth_select_holder - %p.hint Tip: Click on icon to activate sigin with one of the following services - - User.omniauth_providers.each do |provider| - %span{class: oauth_active_class(provider) } - = link_to authbutton(provider, 32), omniauth_authorize_path(User, provider) +%h3.page-title + Account settings +%p.light + You can change your password, username and private token here. + - if current_user.ldap_user? + Some options are unavailable for LDAP accounts +%hr +.row + .span2 + %ul.nav.nav-pills.nav-stacked.nav-stacked-menu + %li.active + = link_to '#tab-token', 'data-toggle' => 'tab' do + Private Token + %li + = link_to '#tab-password', 'data-toggle' => 'tab' do + Password -%fieldset - %legend - Private token - %span.cred.pull-right - keep it secret! - .padded - = form_for @user, url: reset_private_token_profile_path, method: :put do |f| - .data - %p.slead - Private token used to access application resources without authentication. - %br - It can be used for atom feed or API - %p.cgray - - if current_user.private_token - = text_field_tag "token", current_user.private_token, class: "xxlarge large_text" - = f.submit 'Reset', confirm: "Are you sure?", class: "btn btn-primary btn-build-token" - - else - %span You don`t have one yet. Click generate to fix it. - = f.submit 'Generate', class: "btn success btn-build-token" + - if show_profile_social_tab? + %li + = link_to '#tab-social', 'data-toggle' => 'tab' do + Social Accounts -%fieldset - %legend Password - = form_for @user, url: update_password_profile_path, method: :put do |f| - .padded - %p.slead After successful password update you will be redirected to login page where you should login with new password - -if @user.errors.any? - .alert.alert-error - %ul - - @user.errors.full_messages.each do |msg| - %li= msg + - if show_profile_username_tab? + %li + = link_to '#tab-username', 'data-toggle' => 'tab' do + Change Username - .clearfix - = f.label :password - .input= f.password_field :password, required: true - .clearfix - = f.label :password_confirmation - .input - = f.password_field :password_confirmation, required: true - .clearfix - .input - = f.submit 'Save password', class: "btn btn-save" + - if show_profile_remove_tab? + %li + = link_to '#tab-remove', 'data-toggle' => 'tab' do + Remove Account + .span10 + .tab-content + .tab-pane.active#tab-token + %fieldset.update-token + %legend + Private token + %span.cred.pull-right + keep it secret! + %div + = form_for @user, url: reset_private_token_profile_path, method: :put do |f| + .data + %p.slead + Your private token is used to access application resources without authentication. + %br + It can be used for atom feeds or the API. + %p.cgray + - if current_user.private_token + = text_field_tag "token", current_user.private_token, class: "input-xxlarge large_text input-xpadding" + = f.submit 'Reset', confirm: "Are you sure?", class: "btn btn-primary btn-build-token" + - else + %span You don`t have one yet. Click generate to fix it. + = f.submit 'Generate', class: "btn success btn-build-token" + .tab-pane#tab-password + %fieldset.update-password + %legend Password + - if current_user.ldap_user? + %h3.nothing_here_message Not available for LDAP user + - else + = form_for @user, url: update_password_profile_path, method: :put do |f| + %div + %p.slead + You must provide current password in order to change it. + %br + After a successful password update you will be redirected to login page where you should login with your new password + -if @user.errors.any? + .alert.alert-error + %ul + - @user.errors.full_messages.each do |msg| + %li= msg + .control-group + = f.label :current_password, class: 'cgreen' + .controls= f.password_field :current_password, required: true + .control-group + = f.label :password, 'New password' + .controls= f.password_field :password, required: true + .control-group + = f.label :password_confirmation + .controls + = f.password_field :password_confirmation, required: true + .control-group + .controls + = f.submit 'Save password', class: "btn btn-save" + - if show_profile_social_tab? + .tab-pane#tab-social + %fieldset + %legend Social Accounts + .oauth_select_holder + %p.hint Tip: Click on icon to activate signin with one of the following services + - enabled_social_providers.each do |provider| + %span{class: oauth_active_class(provider) } + = link_to authbutton(provider, 32), omniauth_authorize_path(User, provider) -%fieldset.update-username - %legend - Username - %small.cred.pull-right - Changing your username can have unintended side effects! - = form_for @user, url: update_username_profile_path, method: :put, remote: true do |f| - .padded - = f.label :username - .input - = f.text_field :username, required: true - - %span.loading-gif.hide= image_tag "ajax_loader.gif" - %span.update-success.cgreen.hide - %i.icon-ok - Saved - %span.update-failed.cred.hide - %i.icon-remove - Failed - %ul.cred - %li It will change web url for personal projects. - %li It will change the git path to repositories for personal projects. - .input - = f.submit 'Save username', class: "btn btn-save" + - if show_profile_username_tab? + .tab-pane#tab-username + %fieldset.update-username + %legend + Username + %small.cred.pull-right + Changing your username can have unintended side effects! + = form_for @user, url: update_username_profile_path, method: :put, remote: true do |f| + %div + .control-group + = f.label :username + .controls + = f.text_field :username, required: true + + %span.loading-gif.hide= image_tag "ajax_loader.gif" + %span.update-success.cgreen.hide + %i.icon-ok + Saved + %span.update-failed.cred.hide + %i.icon-remove + Failed + %ul.cred + %li This will change the web URL for personal projects. + %li This will change the git path to repositories for personal projects. + .controls + = f.submit 'Save username', class: "btn btn-save" -- if Gitlab.config.gitlab.signup_enabled - %fieldset.remove-account - %legend - Remove account - %small.cred.pull-right - Before removing the account you must remove all projects! - = link_to 'Delete account', user_registration_path, confirm: "REMOVE #{current_user.name}? Are you sure?", method: :delete, class: "btn btn-remove delete-key btn-small pull-right"
\ No newline at end of file + - if show_profile_remove_tab? + .tab-pane#tab-remove + %fieldset.remove-account + %legend + Remove account + %div + %p Deleting an account has the following effects: + %ul + %li All user content like authored issues, snippets, comments will be removed + - rp = current_user.personal_projects.count + - unless rp.zero? + %li #{pluralize rp, 'personal project'} will be removed and cannot be restored + - if current_user.solo_owned_groups.present? + %li + Next groups will be abandoned. You should transfer or remove them: + %strong #{current_user.solo_owned_groups.map(&:name).join(', ')} + = link_to 'Delete account', user_registration_path, confirm: "REMOVE #{current_user.name}? Are you sure?", method: :delete, class: "btn btn-remove" diff --git a/app/views/profiles/design.html.haml b/app/views/profiles/design.html.haml index f4b50677203..0d8075b7d43 100644 --- a/app/views/profiles/design.html.haml +++ b/app/views/profiles/design.html.haml @@ -1,10 +1,13 @@ +%h3.page-title + My appearance settings +%p.light + Appearance settings saved to your profile and available across all devices +%hr + = form_for @user, url: profile_path, remote: true, method: :put do |f| %fieldset.application-theme %legend Application theme - .update-feedback.hide - %i.icon-ok - Saved .themes_opts = label_tag do .prev.default @@ -24,7 +27,7 @@ = label_tag do .prev.gray = f.radio_button :theme_id, 4 - SlateGray + Gray = label_tag do .prev.violet @@ -36,17 +39,10 @@ %fieldset.code-preview-theme %legend Code preview theme - .update-feedback.hide - %i.icon-ok - Saved .code_highlight_opts - = label_tag do - .prev - = image_tag "white.png" - = f.radio_button :dark_scheme, false - White code preview - = label_tag do - .prev - = image_tag "dark.png" - = f.radio_button :dark_scheme, true - Dark code preview + - color_schemes.each do |color_scheme_id, color_scheme| + = label_tag do + .prev + = image_tag "#{color_scheme}-scheme-preview.png" + = f.radio_button :color_scheme_id, color_scheme_id + = color_scheme.gsub(/[-_]+/, ' ').humanize diff --git a/app/views/profiles/groups/index.html.haml b/app/views/profiles/groups/index.html.haml new file mode 100644 index 00000000000..6fc27be5db4 --- /dev/null +++ b/app/views/profiles/groups/index.html.haml @@ -0,0 +1,38 @@ +%h3.page-title + Group membership + - if current_user.can_create_group? + %span.pull-right + = link_to new_group_path, class: "btn btn-new" do + %i.icon-plus + New Group +%p.light + Group members have access to all a group's projects +%hr +.ui-box + .title + %strong Groups + (#{@user_groups.count}) + %ul.well-list + - @user_groups.each do |user_group| + - group = user_group.group + %li + .pull-right + - if can?(current_user, :manage_group, group) + = link_to edit_group_path(group), class: "btn-small btn grouped" do + %i.icon-cogs + Settings + + = link_to leave_profile_group_path(group), confirm: "Are you sure you want to leave #{group.name} group?", method: :delete, class: "btn-small btn grouped", title: 'Remove user from group' do + %i.icon-signout + Leave + + = link_to group, class: 'group-name' do + %strong= group.name + + as + %strong #{user_group.human_access} + + %div.light + #{pluralize(group.projects.count, "project")}, #{pluralize(group.users.count, "user")} + += paginate @user_groups diff --git a/app/views/profiles/history.html.haml b/app/views/profiles/history.html.haml index aa7006c569b..3951c47b5f2 100644 --- a/app/views/profiles/history.html.haml +++ b/app/views/profiles/history.html.haml @@ -1,3 +1,8 @@ +%h3.page-title + Account history +%p.light + All events created by your account are listed here +%hr .profile_history = render @events %hr diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml new file mode 100644 index 00000000000..158979c0ee5 --- /dev/null +++ b/app/views/profiles/keys/_form.html.haml @@ -0,0 +1,23 @@ +%div + = form_for [:profile, @key] do |f| + - if @key.errors.any? + .alert.alert-error + %ul + - @key.errors.full_messages.each do |msg| + %li= msg + + .control-group + = f.label :title + .controls= f.text_field :title, class: "input-xlarge" + .control-group + = f.label :key + .controls + %p.light + Paste your public key here. Read more about how to generate a key on #{link_to "the SSH help page", help_ssh_path}. + = f.text_area :key, class: "input-xxlarge thin_area" + + + .form-actions + = f.submit 'Add key', class: "btn btn-create" + = link_to "Cancel", profile_keys_path, class: "btn btn-cancel" + diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml new file mode 100644 index 00000000000..d0a3fe32c35 --- /dev/null +++ b/app/views/profiles/keys/_key.html.haml @@ -0,0 +1,11 @@ +%li + = link_to profile_key_path(key) do + %strong= key.title + %span + (#{key.fingerprint}) + %span.cgray + added + = time_ago_in_words(key.created_at) + ago + + = link_to 'Remove', profile_key_path(key), confirm: 'Are you sure?', method: :delete, class: "btn btn-small btn-remove delete-key pull-right" diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml new file mode 100644 index 00000000000..79fdb164089 --- /dev/null +++ b/app/views/profiles/keys/index.html.haml @@ -0,0 +1,22 @@ +%h3.page-title + My SSH keys + .pull-right + = link_to "Add SSH Key", new_profile_key_path, class: "btn btn-new" +%p.light + SSH keys allow you to establish a secure connection between your computer and GitLab + %br + Before you can add an SSH key you need to + = link_to "generate it", help_ssh_path +%hr + + +.ui-box + .title + SSH Keys (#{@keys.count}) + %ul.well-list#keys-table + = render @keys + - if @keys.blank? + %li + %h3.nothing_here_message There are no SSH keys with access to your account. + + diff --git a/app/views/keys/new.html.haml b/app/views/profiles/keys/new.html.haml index fff3805890e..f1b8fe08d5c 100644 --- a/app/views/keys/new.html.haml +++ b/app/views/profiles/keys/new.html.haml @@ -1,4 +1,4 @@ -%h3.page_title Add an SSH Key +%h3.page-title Add an SSH Key %hr = render 'form' diff --git a/app/views/profiles/keys/show.html.haml b/app/views/profiles/keys/show.html.haml new file mode 100644 index 00000000000..b736ab17087 --- /dev/null +++ b/app/views/profiles/keys/show.html.haml @@ -0,0 +1,22 @@ +.row + .span4 + .ui-box + .title + SSH Key + %ul.well-list + %li + %span.light Title: + %strong= @key.title + %li + %span.light Created at: + %strong= @key.created_at.stamp("Aug 21, 2011") + + .span8 + %p + %span.light Fingerprint: + %strong= @key.fingerprint + %pre.well-pre + = @key.key + +.pull-right + = link_to 'Remove', profile_key_path(@key), confirm: 'Are you sure?', method: :delete, class: "btn btn-remove delete-key" diff --git a/app/views/profiles/notifications/_settings.html.haml b/app/views/profiles/notifications/_settings.html.haml new file mode 100644 index 00000000000..5f62c8099d0 --- /dev/null +++ b/app/views/profiles/notifications/_settings.html.haml @@ -0,0 +1,31 @@ +%li + .row + .span4 + %span + = notification_icon(notification) + + - if membership.kind_of? UsersGroup + = link_to membership.group.name, membership.group + - else + = link_to_project(membership.project) + .span7 + = form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do + = hidden_field_tag :notification_type, type, id: dom_id(membership, 'notification_type') + = hidden_field_tag :notification_id, membership.id, id: dom_id(membership, 'notification_id') + + = label_tag do + = radio_button_tag :notification_level, Notification::N_GLOBAL, notification.global?, id: dom_id(membership, 'notification_level'), class: 'trigger-submit' + %span Use global setting + + = label_tag do + = radio_button_tag :notification_level, Notification::N_DISABLED, notification.disabled?, id: dom_id(membership, 'notification_level'), class: 'trigger-submit' + %span Disabled + + = label_tag do + = radio_button_tag :notification_level, Notification::N_PARTICIPATING, notification.participating?, id: dom_id(membership, 'notification_level'), class: 'trigger-submit' + %span Participating + + = label_tag do + = radio_button_tag :notification_level, Notification::N_WATCH, notification.watch?, id: dom_id(membership, 'notification_level'), class: 'trigger-submit' + %span Watch + diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml new file mode 100644 index 00000000000..8353b2f5f23 --- /dev/null +++ b/app/views/profiles/notifications/show.html.haml @@ -0,0 +1,58 @@ +%h3.page-title + Notifications settings +%p.light + GitLab uses the email specified in your profile for notifications +%hr +.alert.alert-info + %p + %i.icon-circle.cred + %strong Disabled + – You will not get any notifications via email + %p + %i.icon-circle.cblue + %strong Participating + – You will only receive notifications from related resources (e.g. from your commits or assigned issues) + %p + %i.icon-circle.cgreen + %strong Watch + – You will receive all notifications from projects in which you participate + +.row + .span4 + %h4 + = notification_icon(@notification) + Global setting + .span7 + = form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do + = hidden_field_tag :notification_type, 'global' + + = label_tag do + = radio_button_tag :notification_level, Notification::N_DISABLED, @notification.disabled?, class: 'trigger-submit' + %span Disabled + + = label_tag do + = radio_button_tag :notification_level, Notification::N_PARTICIPATING, @notification.participating?, class: 'trigger-submit' + %span Participating + + = label_tag do + = radio_button_tag :notification_level, Notification::N_WATCH, @notification.watch?, class: 'trigger-submit' + %span Watch + +%br += link_to '#', class: 'js-toggle-visibility-link' do + %span.btn.btn-tiny + %i.icon-chevron-down + %span Advanced notifications settings +.js-toggle-visibility-container.hide + %hr + %h4 Groups: + %ul.bordered-list + - @users_groups.each do |users_group| + - notification = Notification.new(users_group) + = render 'settings', type: 'group', membership: users_group, notification: notification + + %h4 Projects: + %ul.bordered-list + - @users_projects.each do |users_project| + - notification = Notification.new(users_project) + = render 'settings', type: 'project', membership: users_project, notification: notification diff --git a/app/views/profiles/notifications/update.js.haml b/app/views/profiles/notifications/update.js.haml new file mode 100644 index 00000000000..84c6ab25599 --- /dev/null +++ b/app/views/profiles/notifications/update.js.haml @@ -0,0 +1,6 @@ +- if @saved + :plain + new Flash("Notification settings saved", "notice") +- else + :plain + new Flash("Failed to save new settings", "alert") diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml new file mode 100644 index 00000000000..a4e7dadd16a --- /dev/null +++ b/app/views/profiles/passwords/new.html.haml @@ -0,0 +1,22 @@ += form_for @user, url: profile_password_path, method: :post do |f| + .light-well.padded + %p.slead + Please set new password before proceed. + %br + After successful password update you will be redirected to login screen + -if @user.errors.any? + .alert.alert-error + %ul + - @user.errors.full_messages.each do |msg| + %li= msg + + .control-group + = f.label :password + .controls= f.password_field :password, required: true + .control-group + = f.label :password_confirmation + .controls + = f.password_field :password_confirmation, required: true + .control-group + .controls + = f.submit 'Set new password', class: "btn btn-create" diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 3cf6330cc3c..25bf7912f1e 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,6 +1,5 @@ -.profile_avatar_holder - = image_tag gravatar_icon(@user.email, 90) -%h3.page_title += image_tag gravatar_icon(@user.email, 60), alt: '', class: 'avatar s60' +%h3.page-title = @user.name %br %small @@ -51,7 +50,7 @@ %legend Tips: %ul %li - %p You can change your password on Account page + %p You can change your password on the Account page - if Gitlab.config.gravatar.enabled %li %p You can change your avatar at #{link_to "gravatar.com", "http://gravatar.com"} @@ -67,30 +66,25 @@ Need a group for several dependent projects? = link_to new_group_path, class: "btn btn-tiny" do Create a group - - if current_user.can_create_team? - %li - %p - Want to share a team between projects? - = link_to new_team_path, class: "btn btn-tiny" do - Create a team - %fieldset - %legend - Personal projects: - %small.pull-right - %span= current_user.personal_projects.count - of - %span= current_user.projects_limit - .padded - .progress - .bar{style: "width: #{current_user.projects_limit_percent}%;"} + - unless current_user.projects_limit_left > 100 + %fieldset + %legend + Personal projects: + %small.pull-right + %span= current_user.personal_projects.count + of + %span= current_user.projects_limit + .padded + .progress + .bar{style: "width: #{current_user.projects_limit_percent}%;"} %fieldset %legend SSH public keys: %span.pull-right - = link_to pluralize(current_user.keys.count, 'key'), keys_path + = link_to pluralize(current_user.keys.count, 'key'), profile_keys_path .padded - = link_to "Add Public Key", new_key_path, class: "btn btn-small" + = link_to "Add Public Key", new_profile_key_path, class: "btn btn-small" .form-actions - = f.submit 'Save', class: "btn btn-save" + = f.submit 'Save changes', class: "btn btn-save" diff --git a/app/views/projects/_clone_panel.html.haml b/app/views/projects/_clone_panel.html.haml index e52df19be96..c2f85e8ebe8 100644 --- a/app/views/projects/_clone_panel.html.haml +++ b/app/views/projects/_clone_panel.html.haml @@ -1,17 +1,56 @@ .project_clone_panel .row - .span7 + .span8 .form-horizontal= render "shared/clone_panel" - .span4.pull-right + .span3.pull-right .pull-right - unless @project.empty_repo? + - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace + - if current_user.already_forked?(@project) + = link_to project_path(current_user.fork_of(@project)), class: 'btn grouped disabled' do + %i.icon-code-fork + Forked + - else + = link_to fork_project_path(@project), title: "Fork", class: "btn grouped", method: "POST" do + %i.icon-code-fork + Fork - if can? current_user, :download_code, @project - = link_to archive_project_repository_path(@project), class: "btn-small btn grouped" do + = link_to archive_project_repository_path(@project), class: "btn grouped" do %i.icon-download-alt - Download - - if @project.merge_requests_enabled && can?(current_user, :write_merge_request, @project) - = link_to new_project_merge_request_path(@project), title: "New Merge Request", class: "btn-small btn grouped" do - Merge Request - - if @project.issues_enabled && can?(current_user, :write_issue, @project) - = link_to new_project_issue_path(@project), title: "New Issue", class: "btn-small btn grouped" do - Issue + %span.only-wide Download + + - if current_user + .dropdown.pull-right + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.icon-plus-sign-alt + %span.only-wide New + %b.caret + %ul.dropdown-menu + - if @project.issues_enabled && can?(current_user, :write_issue, @project) + %li + = link_to url_for_new_issue, title: "New Issue" do + Issue + - if @project.merge_requests_enabled && can?(current_user, :write_merge_request, @project) + %li + = link_to new_project_merge_request_path(@project), title: "New Merge Request" do + Merge Request + - if @project.snippets_enabled && can?(current_user, :write_snippet, @project) + %li + = link_to new_project_snippet_path(@project), title: "New Snippet" do + Snippet + - if can? current_user, :push_code, @project + %li.divider + %li + = link_to new_project_branch_path(@project) do + %i.icon-code-fork + Git branch + %li + = link_to new_project_tag_path(@project) do + %i.icon-tag + Git tag + + - if can?(current_user, :admin_team_member, @project) + %li.divider + %li + = link_to new_project_team_member_path(@project), title: "New project member" do + Project member diff --git a/app/views/projects/_errors.html.haml b/app/views/projects/_errors.html.haml new file mode 100644 index 00000000000..bb9759353a3 --- /dev/null +++ b/app/views/projects/_errors.html.haml @@ -0,0 +1,4 @@ +- if @project.errors.any? + .alert.alert-error + %button{ type: "button", class: "close", "data-dismiss" => "alert"} × + = @project.errors.full_messages.first diff --git a/app/views/projects/_form.html.haml b/app/views/projects/_form.html.haml deleted file mode 100644 index 0336654dc69..00000000000 --- a/app/views/projects/_form.html.haml +++ /dev/null @@ -1,85 +0,0 @@ -= form_for(@project, remote: true) do |f| - - if @project.errors.any? - .alert.alert-error - %ul - - @project.errors.full_messages.each do |msg| - %li= msg - .clearfix.project_name_holder - = f.label :name do - Project name is - .input - = f.text_field :name, placeholder: "Example Project", class: "xxlarge" - - unless @repository.heads.empty? - .clearfix - = f.label :default_branch, "Default Branch" - .input= f.select(:default_branch, @repository.heads.map(&:name), {}, style: "width:210px;") - - - %fieldset.features - %legend Features: - - .control-group - = f.label :issues_enabled, "Issues", class: 'control-label' - .controls - = f.check_box :issues_enabled - %span.descr Lightweight issue tracking system for this project - - .control-group - = f.label :merge_requests_enabled, "Merge Requests", class: 'control-label' - .controls - = f.check_box :merge_requests_enabled - %span.descr Submit changes to be merged upstream. - - .control-group - = f.label :wall_enabled, "Wall", class: 'control-label' - .controls - = f.check_box :wall_enabled - %span.descr Simple chat system for broadcasting inside project - - .control-group - = f.label :wiki_enabled, "Wiki", class: 'control-label' - .controls - = f.check_box :wiki_enabled - %span.descr Pages for project documentation - - - if can?(current_user, :change_public_mode, @project) - %fieldset.features - %legend - %i.icon-share - Public mode: - .control-group - = f.label :public, class: 'control-label' do - %span Public clone access - .controls - = f.check_box :public - %span.descr - If checked, this project can be cloned - %em without any - authentification. - It will also be listed on the #{link_to "public access directory", public_root_path}. - - - - if can? current_user, :change_namespace, @project - %fieldset.features - %legend Transfer: - .control-group - = f.label :namespace_id do - %span Namespace - .controls - = f.select :namespace_id, namespaces_options(@project.namespace_id || Namespace::global_id), {prompt: 'Choose a project namespace'}, {class: 'chosen'} - %br - %ul.prepend-top-10.cred - %li Be careful. Changing project namespace can have unintended side effects - %li You can transfer project only to namespaces you can manage - %li You will need to update your local repositories to point to the new location. - - - %br - - .actions - = f.submit 'Save', class: "btn btn-save" - = link_to 'Cancel', @project, class: "btn" - - unless @project.new_record? - - if can?(current_user, :remove_project, @project) - .pull-right - = link_to 'Remove Project', @project, confirm: 'Removed project can not be restored! Are you sure?', method: :delete, class: "btn btn-remove" diff --git a/app/views/projects/_new_form.html.haml b/app/views/projects/_new_form.html.haml deleted file mode 100644 index ba3ccc421cd..00000000000 --- a/app/views/projects/_new_form.html.haml +++ /dev/null @@ -1,48 +0,0 @@ -= form_for(@project, remote: true) do |f| - - if @project.errors.any? - .alert.alert-error - %span= @project.errors.full_messages.first - .clearfix.project_name_holder - = f.label :name do - Project name is - .input - = f.text_field :name, placeholder: "Example Project", class: "xxlarge" - = f.submit 'Create project', class: "btn btn-create project-submit" - - - if current_user.can_select_namespace? - .clearfix - = f.label :namespace_id do - %span Namespace - .input - = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'chosen'} - - - .clearfix - .input - = link_to "#", class: 'appear-link' do - %i.icon-upload-alt - %span Import existing repository? - .clearfix.appear-data - = f.label :import_url do - %span Import existing repo - .input - = f.text_field :import_url, class: 'xlarge', placeholder: 'https://github.com/randx/six.git' - .light - URL should be clonable - - %p.padded - New projects are private by default. You choose who can see the project and commit to repository. - %hr - - - if current_user.can_create_group? - .clearfix - .input.light - Need a group for several dependent projects? - = link_to new_group_path, class: "btn btn-tiny" do - Create a group - - if current_user.can_create_team? - .clearfix - .input.light - Want to share a project between team? - = link_to new_team_path, class: "btn btn-tiny" do - Create a team diff --git a/app/views/projects/_project_head.html.haml b/app/views/projects/_project_head.html.haml deleted file mode 100644 index b8c88853a62..00000000000 --- a/app/views/projects/_project_head.html.haml +++ /dev/null @@ -1,31 +0,0 @@ -%ul.nav.nav-tabs - = nav_link(path: 'projects#show') do - = link_to project_path(@project), class: "activities-tab tab" do - %i.icon-home - Show - = nav_link(controller: [:team_members, :teams]) do - = link_to project_team_index_path(@project), class: "team-tab tab" do - %i.icon-user - Team - = nav_link(path: 'projects#files') do - = link_to 'Attachments', files_project_path(@project), class: "files-tab tab" - = nav_link(controller: :snippets) do - = link_to 'Snippets', project_snippets_path(@project), class: "snippets-tab tab" - - - if can? current_user, :admin_project, @project - = nav_link(controller: :deploy_keys, html_options: {class: 'pull-right'}) do - = link_to project_deploy_keys_path(@project) do - %span - Deploy Keys - = nav_link(controller: :hooks, html_options: {class: 'pull-right'}) do - = link_to project_hooks_path(@project) do - %span - Hooks - = nav_link(controller: :services, html_options: {class: 'pull-right'}) do - = link_to project_services_path(@project) do - %span - Services - = nav_link(path: 'projects#edit', html_options: {class: 'pull-right'}) do - = link_to edit_project_path(@project), class: "stat-tab tab " do - %i.icon-edit - Edit diff --git a/app/views/projects/_settings_nav.html.haml b/app/views/projects/_settings_nav.html.haml new file mode 100644 index 00000000000..f59e2871aa3 --- /dev/null +++ b/app/views/projects/_settings_nav.html.haml @@ -0,0 +1,21 @@ +%ul.nav.nav-pills.nav-stacked.nav-stacked-menu + = nav_link(path: 'projects#edit') do + = link_to edit_project_path(@project), class: "stat-tab tab " do + %i.icon-edit + Edit Project + = nav_link(controller: [:team_members, :teams]) do + = link_to project_team_index_path(@project), class: "team-tab tab" do + %i.icon-group + Members + = nav_link(controller: :deploy_keys) do + = link_to project_deploy_keys_path(@project) do + %span + Deploy Keys + = nav_link(controller: :hooks) do + = link_to project_hooks_path(@project) do + %span + Web Hooks + = nav_link(controller: :services) do + = link_to project_services_path(@project) do + %span + Services diff --git a/app/views/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 36d81e6af38..cdca8b2e634 100644 --- a/app/views/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -1,34 +1,24 @@ -= render "head" +%h3.page-title Blame view #tree-holder.tree-holder - %ul.breadcrumb - %li - %span.arrow - = link_to project_tree_path(@project, @ref) do - = @project.name - - @tree.breadcrumbs(6) do |link| - \/ - %li= link - .clear - - .file_holder - .file_title + .file-holder + .file-title %i.icon-file %span.file_name - = @tree.name - %small= number_to_human_size @tree.size - %span.options= render "tree/blob_actions" - .file_content.blame + = @path + %small= number_to_human_size @blob.size + %span.options= render "projects/blob/actions" + .file-content.blame %table - current_line = 1 - @blame.each do |commit, lines| - - commit = CommitDecorator.decorate(Commit.new(commit)) + - commit = Commit.new(commit) %tr %td.blame-commit %span.commit = link_to commit.short_id(8), project_commit_path(@project, commit), class: "commit_short_id" - = commit.author_link avatar: true, size: 16 + = commit_author_link(commit, avatar: true, size: 16) = link_to_gfm truncate(commit.title, length: 20), project_commit_path(@project, commit.id), class: "row_title" %td.lines.blame-numbers @@ -39,8 +29,11 @@ - else - lines.each do |line| = current_line + \ - current_line += 1 %td.lines %pre - - lines.each do |line| - = line + :erb + <% lines.each do |line| %> + <%= line %> + <% end %> diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml new file mode 100644 index 00000000000..5641c528a4f --- /dev/null +++ b/app/views/projects/blob/_actions.html.haml @@ -0,0 +1,12 @@ +.btn-group.tree-btn-group + -# only show edit link for text files + - if @blob.text? + = link_to "edit", project_edit_tree_path(@project, @id), class: "btn btn-small", disabled: !allowed_tree_edit? + = link_to "raw", project_raw_path(@project, @id), class: "btn btn-small", target: "_blank" + -# only show normal/blame view links for text files + - if @blob.text? + - if current_page? project_blame_path(@project, @id) + = link_to "normal view", project_blob_path(@project, @id), class: "btn btn-small" + - else + = link_to "blame", project_blame_path(@project, @id), class: "btn btn-small" unless @blob.empty? + = link_to "history", project_commits_path(@project, @id), class: "btn btn-small" diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml new file mode 100644 index 00000000000..6524aaeef4c --- /dev/null +++ b/app/views/projects/blob/_blob.html.haml @@ -0,0 +1,32 @@ +%ul.breadcrumb + %li + %i.icon-angle-right + = link_to project_tree_path(@project, @ref) do + = @project.path + - tree_breadcrumbs(@tree, 6) do |title, path| + \/ + %li + - if path + - if path.end_with?(@path) + = link_to project_blob_path(@project, path) do + %span.cblue + = truncate(title, length: 40) + - else + = link_to truncate(title, length: 40), project_tree_path(@project, path) + - else + = link_to title, '#' + +%div#tree-content-holder.tree-content-holder + .file-holder + .file-title + %i.icon-file + %span.file_name + = blob.name + %small= number_to_human_size blob.size + %span.options= render "actions" + - if blob.text? + = render "text", blob: blob + - elsif blob.image? + = render "image", blob: blob + - else + = render "download", blob: blob diff --git a/app/views/tree/blob/_download.html.haml b/app/views/projects/blob/_download.html.haml index 864c209db76..f3da1a2a219 100644 --- a/app/views/tree/blob/_download.html.haml +++ b/app/views/projects/blob/_download.html.haml @@ -1,6 +1,6 @@ -.file_content.blob_file +.file-content.blob_file %center - = link_to project_blob_path(@project, @id) do + = link_to project_raw_path(@project, @id) do %div.padded %h4 %i.icon-download-alt diff --git a/app/views/tree/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml index 7b23f0c810c..c090f690d1d 100644 --- a/app/views/tree/blob/_image.html.haml +++ b/app/views/projects/blob/_image.html.haml @@ -1,2 +1,2 @@ -.file_content.image_file +.file-content.image_file %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"} diff --git a/app/views/tree/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml index 122e275219d..bed493d6d8c 100644 --- a/app/views/tree/blob/_text.html.haml +++ b/app/views/projects/blob/_text.html.haml @@ -1,12 +1,12 @@ - if gitlab_markdown?(blob.name) - .file_content.wiki + .file-content.wiki = preserve do = markdown(blob.data) - elsif markup?(blob.name) - .file_content.wiki + .file-content.wiki = raw GitHub::Markup.render(blob.name, blob.data) - else - .file_content.code + .file-content.code - unless blob.empty? %div{class: user_color_scheme_class} = raw blob.colorize(formatter: :gitlab) diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml new file mode 100644 index 00000000000..d96595bc7f0 --- /dev/null +++ b/app/views/projects/blob/show.html.haml @@ -0,0 +1,4 @@ +%div.tree-ref-holder + = render 'shared/ref_switcher', destination: 'blob', path: @path +%div#tree-holder.tree-holder + = render 'blob', blob: @blob diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml new file mode 100644 index 00000000000..b8392525791 --- /dev/null +++ b/app/views/projects/branches/_branch.html.haml @@ -0,0 +1,32 @@ +- commit = Commit.new(Gitlab::Git::Commit.new(branch.commit)) +%li + %h4 + = link_to project_commits_path(@project, branch.name) do + %strong= truncate(branch.name, length: 60) + - if branch.name == @repository.root_ref + %span.label.label-info default + - if @project.protected_branch? branch.name + %span.label.label-success + %i.icon-lock + .pull-right + - if can?(current_user, :download_code, @project) + = link_to archive_project_repository_path(@project, ref: branch.name), class: 'btn grouped btn-small' do + %i.icon-download-alt + Download + = link_to project_compare_index_path(@project, from: branch.name, to: branch.name), class: 'btn grouped btn-small', title: "Compare" do + %i.icon-copy + Compare + + - if can?(current_user, :admin_project, @project) && branch.name != @repository.root_ref + = link_to project_branch_path(@project, branch.name), class: 'btn grouped btn-small remove-row', method: :delete, confirm: 'Removed branch cannot be restored. Are you sure?', remote: true do + %i.icon-trash + + %p + = link_to project_commit_path(@project, commit.id), class: 'commit_short_id' do + = commit.short_id + = image_tag gravatar_icon(commit.author_email), class: "avatar s16", alt: '' + %span.light + = gfm escape_once(truncate(commit.title, length: 40)) + %span + = time_ago_in_words(commit.committed_date) + ago diff --git a/app/views/projects/branches/_filter.html.haml b/app/views/projects/branches/_filter.html.haml new file mode 100644 index 00000000000..7ea11a74a2b --- /dev/null +++ b/app/views/projects/branches/_filter.html.haml @@ -0,0 +1,17 @@ +%ul.nav.nav-pills.nav-stacked + = nav_link(path: 'branches#recent') do + = link_to 'Recent', recent_project_branches_path(@project) + = nav_link(path: 'protected_branches#index') do + = link_to project_protected_branches_path(@project) do + Protected + %i.icon-lock + = nav_link(path: 'branches#index') do + = link_to 'All branches', project_branches_path(@project) + + +%hr +- if can? current_user, :push_code, @project + = link_to new_project_branch_path(@project), class: 'btn btn-create' do + %i.icon-add-sign + New branch + diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml new file mode 100644 index 00000000000..45b9c6c8521 --- /dev/null +++ b/app/views/projects/branches/index.html.haml @@ -0,0 +1,10 @@ += render "projects/commits/head" +.row + .span3 + = render "filter" + .span9 + - unless @branches.empty? + %ul.bordered-list.top-list + - @branches.each do |branch| + = render "projects/branches/branch", branch: branch + = paginate @branches, theme: 'gitlab'
\ No newline at end of file diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml new file mode 100644 index 00000000000..37612d3fd7b --- /dev/null +++ b/app/views/projects/branches/new.html.haml @@ -0,0 +1,24 @@ +%h3.page-title + %i.icon-code-fork + New branch += form_tag project_branches_path, method: :post do + .control-group + = label_tag :branch_name, 'Name for new branch', class: 'control-label' + .controls + = text_field_tag :branch_name, nil, placeholder: 'feature/dashboard', required: true, tabindex: 1 + .control-group + = label_tag :ref, 'Create from', class: 'control-label' + .controls + = text_field_tag :ref, nil, placeholder: 'master', required: true, tabindex: 2 + .light branch name or commit SHA + .form-actions + = submit_tag 'Create branch', class: 'btn btn-create', tabindex: 3 + = link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel' + +:javascript + var availableTags = #{@project.repository.ref_names.to_json}; + + $("#ref").autocomplete({ + source: availableTags, + minLength: 1 + }); diff --git a/app/views/projects/branches/recent.html.haml b/app/views/projects/branches/recent.html.haml new file mode 100644 index 00000000000..25f416c78f2 --- /dev/null +++ b/app/views/projects/branches/recent.html.haml @@ -0,0 +1,8 @@ += render "projects/commits/head" +.row + .span3 + = render "filter" + .span9 + %ul.bordered-list.top-list + - @branches.each do |branch| + = render "projects/branches/branch", branch: branch
\ No newline at end of file diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml new file mode 100644 index 00000000000..1f493452064 --- /dev/null +++ b/app/views/projects/commit/_commit_box.html.haml @@ -0,0 +1,47 @@ +.pull-right + %div + - if @notes_count > 0 + %span.btn.disabled.grouped + %i.icon-comment + = @notes_count + .pull-left.btn-group + %a.btn.grouped.dropdown-toggle{ data: {toggle: :dropdown} } + %i.icon-download-alt + Download as + %span.caret + %ul.dropdown-menu + %li= link_to "Email Patches", project_commit_path(@project, @commit, format: :patch) + %li= link_to "Plain Diff", project_commit_path(@project, @commit, format: :diff) + = link_to project_tree_path(@project, @commit), class: "btn btn-primary grouped" do + %span Browse Code » + %div + +%p + %span.light Commit + = link_to @commit.id, project_commit_path(@project, @commit) +.commit-info-row + %span.light Authored by + %strong + = commit_author_link(@commit, avatar: true, size: 24) + %time{title: @commit.authored_date.stamp("Aug 21, 2011 9:23pm")} + #{time_ago_in_words(@commit.authored_date)} ago + +- if @commit.different_committer? + .commit-info-row + %span.light Committed by + %strong + = commit_committer_link(@commit, avatar: true, size: 24) + %time{title: @commit.committed_date.stamp("Aug 21, 2011 9:23pm")} + #{time_ago_in_words(@commit.committed_date)} ago + +.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) + +.commit-box + %h3.commit-title + = gfm escape_once(@commit.title) + - if @commit.description.present? + %pre.commit-description + = gfm escape_once(@commit.description) diff --git a/app/views/commit/huge_commit.html.haml b/app/views/projects/commit/huge_commit.html.haml index 7f0bcf38037..210d8d9e207 100644 --- a/app/views/commit/huge_commit.html.haml +++ b/app/views/projects/commit/huge_commit.html.haml @@ -1,3 +1,3 @@ -= render "commits/commit_box" += render "projects/commit/commit_box" .alert.alert-error %h4 Commit diffs are too big to be displayed diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml new file mode 100644 index 00000000000..da1b4c10f87 --- /dev/null +++ b/app/views/projects/commit/show.html.haml @@ -0,0 +1,3 @@ += render "commit_box" += render "projects/commits/diffs", diffs: @commit.diffs, project: @project += render "projects/notes/notes_with_form" diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml new file mode 100644 index 00000000000..d70fd96accd --- /dev/null +++ b/app/views/projects/commits/_commit.html.haml @@ -0,0 +1,19 @@ +%li.commit + .commit-row-title + = link_to commit.short_id(8), project_commit_path(project, commit), class: "commit_short_id" + + = link_to_gfm truncate(commit.title, length: 70), project_commit_path(project, commit.id), class: "commit-row-message" + = link_to "Browse Code »", project_tree_path(project, commit), class: "pull-right" + .notes_count + - notes = project.notes.for_commit_id(commit.id) + - if notes.any? + %span.badge.badge-info + %i.icon-comment + = notes.count + + .commit-row-info + = commit_author_link(commit, avatar: true, size: 16) + %time.committed_ago{ datetime: commit.committed_date, title: commit.committed_date.stamp("Aug 21, 2011 9:23pm") } + = time_ago_in_words(commit.committed_date) + ago + diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml new file mode 100644 index 00000000000..b6404778073 --- /dev/null +++ b/app/views/projects/commits/_commits.html.haml @@ -0,0 +1,11 @@ +- @commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits| + .row.commits-row + .span2 + %h4 + %i.icon-calendar + %span= day.stamp("28 Aug, 2010") + %p= pluralize(commits.count, 'commit') + .span10 + %ul.well-list + = render commits, project: @project + %hr.lists-separator diff --git a/app/views/commits/_diff_head.html.haml b/app/views/projects/commits/_diff_head.html.haml index 5aa542287fe..5aa542287fe 100644 --- a/app/views/commits/_diff_head.html.haml +++ b/app/views/projects/commits/_diff_head.html.haml diff --git a/app/views/projects/commits/_diffs.html.haml b/app/views/projects/commits/_diffs.html.haml new file mode 100644 index 00000000000..c51f1b6eff5 --- /dev/null +++ b/app/views/projects/commits/_diffs.html.haml @@ -0,0 +1,70 @@ +- @suppress_diff ||= @suppress_diff || @force_suppress_diff +- if @suppress_diff + .alert.alert-block + %p + %strong Warning! This is a large diff. + %p + To preserve performance the diff is not shown. + - if current_controller?(:commit) or current_controller?(:merge_requests) + Please, download the diff as + - if current_controller?(:commit) + = link_to "plain diff", project_commit_path(@project, @commit, format: :diff), class: "underlined_link" + or + = link_to "email patch", project_commit_path(@project, @commit, format: :patch), class: "underlined_link" + - else + = link_to "plain diff", project_merge_request_path(@project, @merge_request, format: :diff), class: "underlined_link" + or + = link_to "email patch", project_merge_request_path(@project, @merge_request, format: :patch), class: "underlined_link" + instead. + - unless @force_suppress_diff + %p + If you still want to see the diff + = link_to "click this link", url_for(force_show_diff: true), class: "underlined_link" + +%p.commit-stat-summary + Showing + %strong.cdark #{pluralize(diffs.count, "changed file")} + - if current_controller?(:commit) + - unless @commit.has_zero_stats? + with + %strong.cgreen #{@commit.stats.additions} additions + and + %strong.cred #{@commit.stats.deletions} deletions +.file-stats + = render "projects/commits/diff_head", diffs: diffs + +.files + - unless @suppress_diff + - diffs.each_with_index do |diff, i| + - next if diff.diff.empty? + - file = Gitlab::Git::Blob.new(project.repository, @commit.id, @ref, diff.new_path) + - file = Gitlab::Git::Blob.new(project.repository, @commit.parent_id, @ref, diff.old_path) unless file.exists? + - next unless file.exists? + .file{id: "diff-#{i}"} + .header + - if diff.deleted_file + %span= diff.old_path + + - if @commit.parent_ids.present? + = link_to project_blob_path(project, tree_join(@commit.parent_id, diff.new_path)), {:class => 'btn btn-tiny pull-right view-file'} do + View file @ + %span.commit-short-id= @commit.short_id(6) + - else + %span= diff.new_path + - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode + %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" + + = link_to project_blob_path(project, tree_join(@commit.id, diff.new_path)), {:class => 'btn btn-tiny pull-right view-file'} do + View file @ + %span.commit-short-id= @commit.short_id(6) + + .content + -# Skipp all non non-supported blobs + - next unless file.respond_to?('text?') + - if file.text? + = render "projects/commits/text_file", diff: diff, index: i + - elsif file.image? + - old_file = Gitlab::Git::Blob.new(project.repository, @commit.parent_id, @ref, diff.old_path) if @commit.parent_id + = render "projects/commits/image", diff: diff, old_file: old_file, file: file, index: i + - else + %p.nothing_here_message No preview for this file type diff --git a/app/views/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 02debe426fe..c2da9f273b3 100644 --- a/app/views/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -4,15 +4,15 @@ = nav_link(controller: [:commit, :commits]) do = link_to 'Commits', project_commits_path(@project, @repository.root_ref) = nav_link(controller: :compare) do - = link_to 'Compare', project_compare_index_path(@project) + = link_to 'Compare', project_compare_index_path(@project, from: @repository.root_ref, to: @ref || @repository.root_ref) = nav_link(html_options: {class: branches_tab_class}) do - = link_to project_repository_path(@project) do + = link_to recent_project_branches_path(@project) do Branches %span.badge= @repository.branches.length - = nav_link(controller: :repositories, action: :tags) do - = link_to tags_project_repository_path(@project) do + = nav_link(controller: :tags) do + = link_to project_tags_path(@project) do Tags %span.badge= @repository.tags.length @@ -21,8 +21,7 @@ Stats - - if current_controller?(:commits) && current_user.private_token + - if current_user && current_controller?(:commits) && current_user.private_token %li.pull-right - %span.rss-icon - = link_to project_commits_path(@project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Feed" do - = image_tag "rss_ui.png", title: "feed" + = link_to project_commits_path(@project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Feed" do + %i.icon-rss diff --git a/app/views/commits/_image.html.haml b/app/views/projects/commits/_image.html.haml index db02fa333b9..73f87289d3d 100644 --- a/app/views/commits/_image.html.haml +++ b/app/views/projects/commits/_image.html.haml @@ -9,7 +9,7 @@ %div.two-up.view %span.wrap .frame.deleted - %a{href: project_tree_path(@project, tree_join(@commit.id, diff.old_path))} + %a{href: project_blob_path(@project, tree_join(@commit.parent_id, diff.old_path))} %img{src: "data:#{old_file.mime_type};base64,#{Base64.encode64(old_file.data)}"} %p.image-info.hide %span.meta-filesize= "#{number_to_human_size old_file.size}" @@ -21,7 +21,7 @@ %span.meta-height %span.wrap .frame.added - %a{href: project_tree_path(@project, tree_join(@commit.id, diff.new_path))} + %a{href: project_blob_path(@project, tree_join(@commit.id, diff.new_path))} %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} %p.image-info.hide %span.meta-filesize= "#{number_to_human_size file.size}" diff --git a/app/views/projects/commits/_inline_commit.html.haml b/app/views/projects/commits/_inline_commit.html.haml new file mode 100644 index 00000000000..5be8460e061 --- /dev/null +++ b/app/views/projects/commits/_inline_commit.html.haml @@ -0,0 +1,9 @@ +%li.commit.inline-commit + .commit-row-title + = link_to commit.short_id(8), project_commit_path(project, commit), class: "commit_short_id" + + = link_to_gfm truncate(commit.title, length: 40), project_commit_path(project, commit.id), class: "commit-row-message" + %time.committed_ago{ datetime: commit.committed_date, title: commit.committed_date.stamp("Aug 21, 2011 9:23pm") } + = time_ago_in_words(commit.committed_date) + ago + diff --git a/app/views/commits/_text_file.html.haml b/app/views/projects/commits/_text_file.html.haml index 760fd07ed8b..c724213878a 100644 --- a/app/views/commits/_text_file.html.haml +++ b/app/views/projects/commits/_text_file.html.haml @@ -3,8 +3,8 @@ %a.supp_diff_link Diff suppressed. Click to show %table.text-file{class: "#{'hide' if too_big}"} - - each_diff_line(diff, index) do |line, type, line_code, line_new, line_old| - %tr.line_holder{ id: line_code } + - each_diff_line(diff, index) do |line, type, line_code, line_new, line_old, raw_line| + %tr.line_holder{ id: line_code, class: "#{type}" } - if type == "match" %td.old_line= "..." %td.new_line= "..." @@ -13,11 +13,11 @@ %td.old_line = link_to raw(type == "new" ? " " : line_old), "##{line_code}", id: line_code - if @comments_allowed - = render "notes/diff_note_link", line_code: line_code + = render "projects/notes/diff_note_link", line_code: line_code %td.new_line= link_to raw(type == "old" ? " " : line_new) , "##{line_code}", id: line_code %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line) - if @reply_allowed - comments = @line_notes.select { |n| n.line_code == line_code }.sort_by(&:created_at) - unless comments.empty? - = render "notes/diff_notes_with_reply", notes: comments + = render "projects/notes/diff_notes_with_reply", notes: comments, line: line diff --git a/app/views/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index 46f9838e84a..46f9838e84a 100644 --- a/app/views/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder diff --git a/app/views/commits/show.html.haml b/app/views/projects/commits/show.html.haml index d180b8ec426..723c5a1c340 100644 --- a/app/views/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -1,8 +1,10 @@ = render "head" - if @path.present? - %ul.breadcrumb - = breadcrumbs + %ul.breadcrumb.commit-breadcrumb + %li.light + History for + = commits_breadcrumbs %div{id: dom_id(@project)} #commits-list= render "commits" @@ -11,7 +13,5 @@ - if @commits.count == @limit :javascript - $(function(){ - CommitsList.init("#{@ref}", #{@limit}); - }); + CommitsList.init("#{@ref}", #{@limit}); diff --git a/app/views/commits/show.js.haml b/app/views/projects/commits/show.js.haml index 797bc14cc1b..045c9dd83d7 100644 --- a/app/views/commits/show.js.haml +++ b/app/views/projects/commits/show.js.haml @@ -1,3 +1,3 @@ :plain - CommitsList.append(#{@commits.count}, "#{escape_javascript(render(partial: 'commits/commits'))}"); + CommitsList.append(#{@commits.count}, "#{escape_javascript(render('projects/commits/commits'))}"); diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml new file mode 100644 index 00000000000..ca1ea4073e6 --- /dev/null +++ b/app/views/projects/compare/_form.html.haml @@ -0,0 +1,29 @@ += form_tag project_compare_index_path(@project), method: :post do + .clearfix + .pull-left + - if params[:to] && params[:from] + = link_to 'switch', {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has_tooltip', title: 'Switch base of comparison'} + .input-prepend + %span.add-on.input-xpadding from + = text_field_tag :from, params[:from], class: "span3 input-xpadding" + = "..." + .input-prepend + %span.add-on.input-xpadding to + = text_field_tag :to, params[:to], class: "span3 input-xpadding" + .pull-left + + = submit_tag "Compare", class: "btn btn-create commits-compare-btn" + - if compare_to_mr_button? + = link_to compare_mr_path, class: 'prepend-left-10' do + %strong Make a merge request + + +:javascript + var availableTags = #{@project.repository.ref_names.to_json}; + + $("#from, #to").autocomplete({ + source: availableTags, + minLength: 1 + }); + + disableButtonIfEmptyField('#to', '.commits-compare-btn'); diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml new file mode 100644 index 00000000000..4745bfbeaaf --- /dev/null +++ b/app/views/projects/compare/index.html.haml @@ -0,0 +1,16 @@ += render "projects/commits/head" + +%h3.page-title + Compare View +%p.slead + Compare branches, tags or commit ranges. + %br + Fill input field with commit id like + %code.label-branch 4eedf23 + or branch/tag name like + %code.label-branch master + and press compare button for the commits list and a code diff. + %br + Changes are shown <b>from</b> the version in the first field <b>to</b> the version in the second field. + += render "form" diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml new file mode 100644 index 00000000000..4be62d36917 --- /dev/null +++ b/app/views/projects/compare/show.html.haml @@ -0,0 +1,35 @@ += render "projects/commits/head" + +%h3.page-title + Compare View + += render "form" + +- if @commits.size > 100 + .alert.alert-block + %p + %strong Warning! This comparison includes more than 100 commits. + %p To preserve performance the line diff is not shown. + +- if @commits.present? + %div.ui-box + .title + Commits (#{@commits.count}) + %ul.well-list= render Commit.decorate(@commits), project: @project + + - unless @diffs.empty? + %h4 Diff + = render "projects/commits/diffs", diffs: @diffs, project: @project +- else + .light-well + %center + %h4 + There isn't anything to compare. + %p.slead + - if params[:to] == params[:from] + You'll need to use different branch names to get a valid comparison. + - else + %span.label-branch #{params[:from]} + and + %span.label-branch #{params[:to]} + are the same. diff --git a/app/views/projects/create.js.haml b/app/views/projects/create.js.haml index 296c8688f47..a444b8b59a6 100644 --- a/app/views/projects/create.js.haml +++ b/app/views/projects/create.js.haml @@ -3,8 +3,7 @@ location.href = "#{project_path(@project)}"; - else :plain - $('.project_new_holder').show(); - $("#new_project").replaceWith("#{escape_javascript(render('new_form'))}"); + $(".project-edit-errors").html("#{escape_javascript(render('errors'))}"); + $('.project-submit').enable(); $('.save-project-loader').hide(); - new Projects(); - $('select.chosen').chosen() + $('.project-edit-container').show(); diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml new file mode 100644 index 00000000000..45f80ecd556 --- /dev/null +++ b/app/views/projects/deploy_keys/_deploy_key.html.haml @@ -0,0 +1,25 @@ +%li + .pull-right + - if @available_keys.include?(deploy_key) + = link_to enable_project_deploy_key_path(@project, deploy_key), class: 'btn btn-small', method: :put do + %i.icon-plus + Enable + - else + - if deploy_key.projects.count > 1 + = link_to disable_project_deploy_key_path(@project, deploy_key), class: 'btn btn-small', method: :put do + %i.icon-off + Disable + - else + = link_to 'Remove', project_deploy_key_path(@project, deploy_key), confirm: 'You are going to remove deploy key. Are you sure?', method: :delete, class: "btn btn-remove delete-key btn-small pull-right" + + + = link_to project_deploy_key_path(deploy_key.projects.include?(@project) ? @project : deploy_key.projects.first, deploy_key) do + %i.icon-key + %strong= deploy_key.title + + %p.light.prepend-top-10 + - deploy_key.projects.map(&:name_with_namespace).each do |project_name| + %span.label= project_name + %small.pull-right + Created #{time_ago_in_words(deploy_key.created_at)} ago + diff --git a/app/views/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index 5fb83021dc0..d49b2204537 100644 --- a/app/views/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -6,18 +6,18 @@ - @key.errors.full_messages.each do |msg| %li= msg - .clearfix + .control-group = f.label :title - .input= f.text_field :title - .clearfix + .controls= f.text_field :title, class: 'input-xlarge' + .control-group = f.label :key - .input - = f.text_area :key, class: [:xxlarge, :thin_area] - %p.hint - Paste a machine public key here. Read more about how generate it + .controls + %p.light + Paste a machine public key here. Read more about how to generate it = link_to "here", help_ssh_path + = f.text_area :key, class: "input-xxlarge thin_area" - .actions - = f.submit 'Save', class: "btn-save btn" + .form-actions + = f.submit 'Create', class: "btn-create btn" = link_to "Cancel", project_deploy_keys_path(@project), class: "btn btn-cancel" diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml new file mode 100644 index 00000000000..53d6e36c62c --- /dev/null +++ b/app/views/projects/deploy_keys/index.html.haml @@ -0,0 +1,32 @@ +%h3.page-title + Deploy keys allow read-only access to the repository + + = link_to new_project_deploy_key_path(@project), class: "btn btn-new pull-right", title: "New Deploy Key" do + %i.icon-plus + New Deploy Key + +%p.light + Deploy keys can be used for CI, staging or production servers. + You can create a deploy key or add an existing one + +%hr.clearfix + +.row + .span5.enabled-keys + %h5 + %strong.cgreen Enabled deploy keys + for this project + %ul.bordered-list + = render @enabled_keys + - if @enabled_keys.blank? + .light-well + %p.nothing_here_message Create a #{link_to 'new deploy key', new_project_deploy_key_path(@project)} or add an existing one + .span5.available-keys + %h5 + %strong Deploy keys + from projects available to you + %ul.bordered-list + = render @available_keys + - if @available_keys.blank? + .light-well + %p.nothing_here_message Deploy keys from projects you have access to will be displayed here diff --git a/app/views/projects/deploy_keys/new.html.haml b/app/views/projects/deploy_keys/new.html.haml new file mode 100644 index 00000000000..186d6b58972 --- /dev/null +++ b/app/views/projects/deploy_keys/new.html.haml @@ -0,0 +1,4 @@ +%h3.page-title New Deploy key +%hr + += render 'form' diff --git a/app/views/deploy_keys/show.html.haml b/app/views/projects/deploy_keys/show.html.haml index 227afecb061..49566f83d20 100644 --- a/app/views/deploy_keys/show.html.haml +++ b/app/views/projects/deploy_keys/show.html.haml @@ -1,14 +1,13 @@ -= render "repositories/head" -%h3.page_title +%h3.page-title Deploy key: = @key.title %small created at = @key.created_at.stamp("Aug 21, 2011") -.back_link +.back-link = link_to project_deploy_keys_path(@project) do ← To keys list %hr %pre= @key.key .pull-right - = link_to 'Remove', project_deploy_key_path(@key.project, @key), confirm: 'Are you sure?', method: :delete, class: "btn-remove btn delete-key" + = link_to 'Remove', project_deploy_key_path(@project, @key), confirm: 'Are you sure?', method: :delete, class: "btn-remove btn delete-key" diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index fdd537da3aa..117ee13140e 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -1,12 +1,171 @@ -= render "project_head" -.project_edit_holder - %h3.page_title Edit Project - %hr - = render "projects/form" -%div.save-project-loader.hide +.project-edit-container + .project-edit-errors + .project-edit-content + %div + %h3.page-title + Project settings: + %p.light Some settings, such as "Transfer Project", are hidden inside the danger area below + %hr + .form-holder + = form_for(@project, remote: true) do |f| + %fieldset + .control-group.project_name_holder + = f.label :name do + Project name is + .controls + = f.text_field :name, placeholder: "Example Project", class: "span5" + + + .control-group + = f.label :description do + Project description + %span.light (optional) + .controls + = f.text_area :description, placeholder: "awesome project", class: "span5", rows: 3, maxlength: 250 + + - if @project.repository.exists? && @project.repository.branch_names.any? + .control-group + = f.label :default_branch, "Default Branch" + .controls= f.select(:default_branch, @repository.branch_names, {}, {class: 'chosen'}) + + + - if can?(current_user, :change_public_mode, @project) + %fieldset.public-mode + %legend + Public mode: + .control-group + = f.label :public, class: 'control-label' do + %span Public access + .controls + = f.check_box :public + %span.descr + If checked, this project can be cloned + %em without any + authentication. + It will also be listed on the #{link_to "public access directory", public_root_path}. + %em Any + user will have #{link_to "Guest", help_permissions_path} permissions on the repository. + + %fieldset.features + %legend + Labels: + .control-group + = f.label :label_list, "Labels", class: 'control-label' + .controls + = f.text_field :label_list, maxlength: 2000, class: "span5" + %p.hint Separate labels with commas. + + %fieldset.features + %legend + Features: + .control-group + = f.label :issues_enabled, "Issues", class: 'control-label' + .controls + = f.check_box :issues_enabled + %span.descr Lightweight issue tracking system for this project + + - if Project.issues_tracker.values.count > 1 + .control-group + = f.label :issues_tracker, "Issues tracker", class: 'control-label' + .controls= f.select(:issues_tracker, Project.issues_tracker.values, {}, { disabled: !@project.issues_enabled }) + + .control-group + = f.label :issues_tracker_id, "Project name or id in issues tracker", class: 'control-label' + .controls= f.text_field :issues_tracker_id, disabled: !@project.can_have_issues_tracker_id? + + .control-group + = f.label :merge_requests_enabled, "Merge Requests", class: 'control-label' + .controls + = f.check_box :merge_requests_enabled + %span.descr Submit changes to be merged upstream. + + .control-group + = f.label :wiki_enabled, "Wiki", class: 'control-label' + .controls + = f.check_box :wiki_enabled + %span.descr Pages for project documentation + + .control-group + = f.label :wall_enabled, "Wall", class: 'control-label' + .controls + = f.check_box :wall_enabled + %span.descr Simple chat system for broadcasting inside project + + .control-group + = f.label :snippets_enabled, "Snippets", class: 'control-label' + .controls + = f.check_box :snippets_enabled + %span.descr Share code pastes with others out of git repository + + + .form-actions + = f.submit 'Save changes', class: "btn btn-save" + + + + %center.light.prepend-top-20.padded + %h3 + %i.icon-warning-sign + Dangerous settings + %p Project settings below may result in data loss! + = link_to '#', class: 'btn js-toggle-visibility-link' do + Show it to me + %i.icon-chevron-down + + .js-toggle-visibility-container.hide + - if can?(current_user, :change_namespace, @project) + .ui-box.ui-box-danger + .title Transfer project + .errors-holder + .form-holder + = form_for(@project, url: transfer_project_path(@project), remote: true, html: { class: 'transfer-project' }) do |f| + .control-group + = f.label :namespace_id do + %span Namespace + .controls + .control-group + = f.select :namespace_id, namespaces_options(@project.namespace_id), {prompt: 'Choose a project namespace'}, {class: 'chosen'} + %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 + %p.nothing_here_message Only the project owner can transfer a project + + .ui-box.ui-box-danger + .title Rename repository + .errors-holder + .form-holder + = form_for(@project) do |f| + .control-group + = f.label :path do + %span Path + .controls + .control-group + = f.text_field :path + %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-remove" + + - if can?(current_user, :remove_project, @project) + .ui-box.ui-box-danger + .title Remove project + .ui-box-body + %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, confirm: remove_project_message(@project), method: :delete, class: "btn btn-remove" + - else + %p.nothing_here_message Only project owner can remove a project + +.save-project-loader.hide %center = image_tag "ajax_loader.gif" - %h3 Saving project. Please wait a few minutes - -:javascript - $(function(){ new Projects(); }); + %h3 Saving project. + %p Please wait a moment, this page will automatically refresh when ready. diff --git a/app/views/projects/edit_tree/show.html.haml b/app/views/projects/edit_tree/show.html.haml new file mode 100644 index 00000000000..b1fb4f8637d --- /dev/null +++ b/app/views/projects/edit_tree/show.html.haml @@ -0,0 +1,46 @@ +%h3.page-title Edit mode +.file-editor + = form_tag(project_edit_tree_path(@project, @id), method: :put, class: "form-horizontal") do + .file-holder + .file-title + %i.icon-file + %span.file_name + = @path + %small + on + %strong= @ref + %span.options + .btn-group.tree-btn-group + = link_to "Cancel", project_blob_path(@project, @id), class: "btn btn-tiny btn-cancel", confirm: leave_edit_message + .file-content.code + %pre#editor= @blob.data + + .control-group.commit_message-group + = label_tag 'commit_message', class: "control-label" do + Commit message + .controls + = text_area_tag 'commit_message', '', placeholder: "Update #{@blob.name}", required: true, rows: 3 + .form-actions + = hidden_field_tag 'last_commit', @last_commit + = hidden_field_tag 'content', '', id: "file-content" + .commit-button-annotation + = button_tag "Commit changes", class: 'btn commit-btn js-commit-button btn-primary' + .message + to branch + %strong= @ref + = link_to "Cancel", project_blob_path(@project, @id), class: "btn btn-cancel", confirm: leave_edit_message + +:javascript + ace.config.set("modePath", gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}/ace-src-noconflict") + var ace_mode = "#{@blob.language.try(:ace_mode)}"; + var editor = ace.edit("editor"); + if (ace_mode) { + editor.getSession().setMode('ace/mode/' + ace_mode); + } + + disableButtonIfEmptyField("#commit_message", ".js-commit-button"); + + $(".js-commit-button").click(function(){ + $("#file-content").val(editor.getValue()); + $(".file-editor form").submit(); + }); diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 22aaaf0f2b2..9f3502e90de 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,35 +1,47 @@ -= render "project_head" = render 'clone_panel' -%div.git-empty - %fieldset - %legend Git global setup: - %pre.dark - = preserve do - git config --global user.name "#{current_user.name}" - git config --global user.email "#{current_user.email}" +- if @project.import? && !@project.imported + .save-project-loader + %center + = image_tag "ajax_loader.gif" + %h3 Importing repository. + %p.monospace git clone --bare #{@project.import_url} + %p Please wait until we import repository for you. Refresh at will. + :javascript + new ProjectImport(); - %fieldset - %legend Create Repository - %pre.dark - = preserve do - mkdir #{@project.path} - cd #{@project.path} - git init - touch README - git add README - git commit -m 'first commit' - git remote add origin #{@project.url_to_repo} - git push -u origin master +- else + %div.git-empty + %fieldset + %legend Git global setup: + %pre.dark + :preserve + git config --global user.name "#{git_user_name}" + git config --global user.email "#{git_user_email}" - %fieldset - %legend Existing Git Repo? - %pre.dark - = preserve do - cd existing_git_repo - git remote add origin #{@project.url_to_repo} - git push -u origin master + %fieldset + %legend Create Repository + %pre.dark + :preserve + mkdir #{@project.path} + cd #{@project.path} + git init + touch README + git add README + git commit -m 'first commit' + %span.clone= "git remote add origin #{@project.url_to_repo}" + :preserve + git push -u origin master + + %fieldset + %legend Existing Git Repo? + %pre.dark + :preserve + cd existing_git_repo + %span.clone= "git remote add origin #{@project.url_to_repo}" + :preserve + git push -u origin master - if can? current_user, :remove_project, @project .prepend-top-20 - = link_to 'Remove project', @project, confirm: 'Are you sure?', method: :delete, class: "btn btn-remove pull-right" + = link_to 'Remove project', @project, confirm: remove_project_message(@project), method: :delete, class: "btn btn-remove pull-right" diff --git a/app/views/projects/files.html.haml b/app/views/projects/files.html.haml deleted file mode 100644 index d108308318e..00000000000 --- a/app/views/projects/files.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -= render "project_head" -- unless @notes.empty? - %table - %thead - %tr - %th File name - %th - - - @notes.each do |note| - %tr - %td - %a{href: note.attachment.url} - = image_tag gravatar_icon(note.author_email), class: "avatar s24" - = note.attachment_identifier - %td - Added - = time_ago_in_words(note.created_at) - ago -- else - %p.slead All files attached to project wall, issues etc will be displayed here - - diff --git a/app/views/projects/fork.html.haml b/app/views/projects/fork.html.haml new file mode 100644 index 00000000000..a1c109e5d62 --- /dev/null +++ b/app/views/projects/fork.html.haml @@ -0,0 +1,19 @@ +.alert.alert-error.alert-block + %h4 + %i.icon-code-fork + Fork Error! + %p + You are trying to fork + = link_to_project @project + but it fails due to next 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.icon-code-fork + Try to Fork again diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml new file mode 100644 index 00000000000..a21cb9e7861 --- /dev/null +++ b/app/views/projects/graphs/show.html.haml @@ -0,0 +1,34 @@ +.loading-graph + %center + .loading + %h3.page-title Building repository graph. + %p Please wait a moment, this page will automatically refresh when ready. + +.stat-graph + .header.clearfix + .pull-right + %select + %option{:value => "commits"} Commits + %option{:value => "additions"} Additions + %option{:value => "deletions"} Deletions + %h3#date_header.page-title + %p.light + Commits to #{@project.default_branch}, excluding merge commits. Limited by 6,000 commits + %input#brush_change{:type => "hidden"} + .graphs + #contributors-master + #contributors.clearfix + %ol.contributors-list.clearfix + +:javascript + $(".stat-graph").hide(); + + $.ajax({ + type: "GET", + url: location.href, + complete: function() { + $(".stat-graph").fadeIn(); + $(".loading-graph").hide(); + }, + dataType: "script" + }); diff --git a/app/views/projects/graphs/show.js.haml b/app/views/projects/graphs/show.js.haml new file mode 100644 index 00000000000..43e776e5330 --- /dev/null +++ b/app/views/projects/graphs/show.js.haml @@ -0,0 +1,19 @@ +- if @success + :plain + controller = new ContributorsStatGraph + controller.init(#{@log}) + + $("select").change( function () { + var field = $(this).val() + controller.set_current_field(field) + controller.redraw_master() + controller.redraw_authors() + }) + + $("#brush_change").change( function () { + controller.change_date_header() + controller.redraw_authors() + }) +- else + :plain + $('.stat-graph').replaceWith('<div class="alert alert-error">Failed to load graph</div>') diff --git a/app/views/hooks/_data_ex.html.erb b/app/views/projects/hooks/_data_ex.html.erb index b4281fa18c7..b4281fa18c7 100644 --- a/app/views/hooks/_data_ex.html.erb +++ b/app/views/projects/hooks/_data_ex.html.erb diff --git a/app/views/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 88a5a7dcffe..f748eb29294 100644 --- a/app/views/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -1,42 +1,35 @@ -= render "projects/project_head" +%h3.page-title + Post-receive hooks -- if can? current_user, :admin_project, @project - .alert.alert-info - %span - Post receive hooks for binding events when someone push to repository. - %br - Read more about web hooks - %strong #{link_to "here", help_web_hooks_path, class: "vlink"} +%p.light + #{link_to "Post-receive hooks ", help_web_hooks_path, class: "vlink"} can be + used for binding events when someone pushes to the repository. + +%hr.clearfix = form_for [@project, @hook], as: :hook, url: project_hooks_path(@project), html: { class: 'form-inline' } do |f| -if @hook.errors.any? .alert.alert-error - @hook.errors.full_messages.each do |msg| %p= msg - .clearfix + .control-group = f.label :url, "URL:" - .input - = f.text_field :url, class: "text_field xxlarge" + .controls + = f.text_field :url, class: "text_field input-xxlarge input-xpadding", placeholder: 'http://example.com/trigger-ci.json' - = f.submit "Add Web Hook", class: "btn btn-primary" + = f.submit "Add Web Hook", class: "btn btn-create" %hr -if @hooks.any? - %h3.page_title - Hooks (#{@hooks.count}) - %br - %table - %thead - %tr - %th URL - %th - - @hooks.each do |hook| - %tr - %td + .ui-box + .title + Hooks (#{@hooks.count}) + %ul.well-list + - @hooks.each do |hook| + %li %span.badge.badge-info POST → %span.monospace= hook.url - %td .pull-right = link_to 'Test Hook', test_project_hook_path(@project, hook), class: "btn btn-small grouped" = link_to 'Remove', project_hook_path(@project, hook), confirm: 'Are you sure?', method: :delete, class: "btn btn-remove btn-small grouped" diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml new file mode 100644 index 00000000000..6acad9134d1 --- /dev/null +++ b/app/views/projects/issues/_form.html.haml @@ -0,0 +1,95 @@ +%div.issue-form-holder + %h3.page-title= @issue.new_record? ? "New Issue" : "Edit Issue ##{@issue.id}" + = form_for [@project, @issue] do |f| + -if @issue.errors.any? + .alert.alert-error + - @issue.errors.full_messages.each do |msg| + %span= msg + %br + .ui-box.ui-box-show + .ui-box-head + .control-group + = f.label :title do + %strong= "Subject *" + .controls + = f.text_field :title, maxlength: 255, class: "input-xxlarge js-gfm-input", autofocus: true, required: true + .ui-box-body + .control-group + .issue_assignee.pull-left + = f.label :assignee_id do + %i.icon-user + Assign to + .controls + .pull-left + = f.select(:assignee_id, @project.team.members.sort_by(&:name).map {|p| [ p.name, p.id ] }, { include_blank: "Select a user" }, {class: 'chosen'}) + .pull-right + + = link_to 'Assign to me', '#', class: 'btn btn-small assign-to-me-link' + .issue_milestone.pull-left + = f.label :milestone_id do + %i.icon-time + Milestone + .controls= f.select(:milestone_id, @project.milestones.active.all.collect {|p| [ p.title, p.id ] }, { include_blank: "Select milestone" }, {class: 'chosen'}) + + .ui-box-bottom + .control-group + = f.label :label_list do + %i.icon-tag + Labels + .controls + = f.text_field :label_list, maxlength: 2000, class: "input-xxlarge" + %p.hint Separate labels with commas. + + .control-group + = f.label :description, "Details" + .controls + = f.text_area :description, class: "input-xxlarge js-gfm-input", rows: 14 + %p.hint Issues are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + + + .form-actions + - if @issue.new_record? + = f.submit 'Submit new issue', class: "btn btn-create" + -else + = f.submit 'Save changes', class: "btn-save btn" + + - cancel_path = @issue.new_record? ? project_issues_path(@project) : project_issue_path(@project, @issue) + = link_to "Cancel", cancel_path, class: 'btn btn-cancel' + + + + +:javascript + $("#issue_label_list") + .bind( "keydown", function( event ) { + if ( event.keyCode === $.ui.keyCode.TAB && + $( this ).data( "autocomplete" ).menu.active ) { + event.preventDefault(); + } + }) + .bind("click", function(event) { + $(this).autocomplete("search", ""); + }) + .autocomplete({ + minLength: 0, + source: function( request, response ) { + response( $.ui.autocomplete.filter( + #{raw labels_autocomplete_source}, extractLast( request.term ) ) ); + }, + focus: function() { + return false; + }, + select: function(event, ui) { + var terms = split( this.value ); + terms.pop(); + terms.push( ui.item.value ); + terms.push( "" ); + this.value = terms.join( ", " ); + return false; + } + }); + + $('.assign-to-me-link').on('click', function(e){ + $('#issue_assignee_id').val("#{current_user.id}").trigger("chosen:updated"); + e.preventDefault(); + }); diff --git a/app/views/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml index 7e0b2cde074..438cc02b477 100644 --- a/app/views/issues/_head.html.haml +++ b/app/views/projects/issues/_head.html.haml @@ -5,7 +5,7 @@ = link_to 'Milestones', project_milestones_path(@project), class: "tab" = nav_link(controller: :labels) do = link_to 'Labels', project_labels_path(@project), class: "tab" - %li.pull-right - %span.rss-icon + - if current_user + %li.pull-right = link_to project_issues_path(@project, :atom, { private_token: current_user.private_token }) do - = image_tag "rss_ui.png", title: "feed" + %i.icon-rss diff --git a/app/views/issues/_show.html.haml b/app/views/projects/issues/_issue.html.haml index fa888618066..b9a2c18efdc 100644 --- a/app/views/issues/_show.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -1,39 +1,44 @@ %li{ id: dom_id(issue), class: issue_css_classes(issue), url: project_issue_path(issue.project, issue) } - if controller.controller_name == 'issues' - .issue_check + .issue-check = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue", disabled: !can?(current_user, :modify_issue, issue) - .pull-right - - if issue.notes.any? - %span.btn.btn-small.disabled.grouped - %i.icon-comment - = issue.notes.count - - if can? current_user, :modify_issue, issue - - if issue.closed - = link_to 'Reopen', project_issue_path(issue.project, issue, issue: {closed: false }, status_only: true), method: :put, class: "btn btn-small grouped reopen_issue", remote: true - - else - = link_to 'Close', project_issue_path(issue.project, issue, issue: {closed: true }, status_only: true), method: :put, class: "btn btn-small grouped close_issue", remote: true - = link_to edit_project_issue_path(issue.project, issue), class: "btn btn-small edit-issue-link grouped" do - %i.icon-edit - Edit - - - if issue.assignee - = image_tag gravatar_icon(issue.assignee_email), class: "avatar" - - else - = image_tag "no_avatar.png", class: "avatar" - %p= link_to_gfm truncate(issue.title, length: 100), project_issue_path(issue.project, issue), class: "row_title" + .issue-title + %span.light= "##{issue.iid}" + = link_to_gfm truncate(issue.title, length: 100), project_issue_path(issue.project, issue), class: "row_title" - %span.update-author - %span.cdark= "##{issue.id}" + .issue-info - if issue.assignee - assigned to #{issue.assignee_name} + assigned to #{link_to_member(@project, issue.assignee)} - else - - + unassigned - if issue.votes_count > 0 = render 'votes/votes_inline', votable: issue - %span + - if issue.notes.any? + %span + %i.icon-comments + = issue.notes.count + - if issue.milestone + %span + %i.icon-time + = issue.milestone.title + .pull-right + %small updated #{time_ago_in_words(issue.updated_at)} ago + + .issue-labels - issue.labels.each do |label| - %span.label + %span{class: "label #{label_css_class(label.name)}"} %i.icon-tag = label.name + + .issue-actions + - if can? current_user, :modify_issue, issue + - if issue.closed? + = link_to 'Reopen', project_issue_path(issue.project, issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-small grouped reopen_issue", remote: true + - else + = link_to 'Close', project_issue_path(issue.project, issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-small grouped close_issue", remote: true + = link_to edit_project_issue_path(issue.project, issue), class: "btn btn-small edit-issue-link grouped" do + %i.icon-edit + Edit + + diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml new file mode 100644 index 00000000000..539c45edd94 --- /dev/null +++ b/app/views/projects/issues/_issues.html.haml @@ -0,0 +1,93 @@ +.ui-box + .title + = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left" + .clearfix + .issues_bulk_update.hide + = form_tag bulk_update_project_issues_path(@project), method: :post do + %span Update selected issues with + = select_tag('update[status]', options_for_select(['open', 'closed']), prompt: "Status") + = select_tag('update[assignee_id]', options_from_collection_for_select(@project.team.members, "id", "name", params[:assignee_id]), prompt: "Assignee") + = select_tag('update[milestone_id]', options_from_collection_for_select(project_active_milestones, "id", "title", params[:milestone_id]), prompt: "Milestone") + = hidden_field_tag 'update[issues_ids]', [] + = hidden_field_tag :status, params[:status] + = button_tag "Save", class: "btn update_selected_issues btn-small btn-save" + .issues-filters + %span Filter by + .dropdown.inline.prepend-left-10 + %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} + %i.icon-tags + %span.light labels: + - if params[:label_name].present? + %strong= params[:label_name] + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to project_filter_path(label_name: nil) do + Any + - issue_label_names.each do |label_name| + %li + = link_to project_filter_path(label_name: label_name) do + %span{class: "label #{label_css_class(label_name)}"} + %i.icon-tag + = label_name + .dropdown.inline.prepend-left-10 + %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} + %i.icon-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 + - @project.team.members.sort_by(&:name).each do |user| + %li + = link_to project_filter_path(assignee_id: user.id) do + = image_tag gravatar_icon(user.email), class: "avatar s16", alt: '' + = user.name + + .dropdown.inline.prepend-left-10 + %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} + %i.icon-time + %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 + + + %ul.well-list.issues-list + = render @issues + - if @issues.blank? + %li + %h4.nothing_here_message No issues to show + +- if @issues.present? + .pull-right + %span.issue_counter #{@issues.total_count} + issues for this filter + + = paginate @issues, remote: true, theme: "gitlab" diff --git a/app/views/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml index b1bc3ba0eba..b1bc3ba0eba 100644 --- a/app/views/issues/edit.html.haml +++ b/app/views/projects/issues/edit.html.haml diff --git a/app/views/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder index 00ddd4bf702..00ddd4bf702 100644 --- a/app/views/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml new file mode 100644 index 00000000000..81594b4972e --- /dev/null +++ b/app/views/projects/issues/index.html.haml @@ -0,0 +1,23 @@ += render "head" +.issues_content + %h3.page-title + Issues + %span (<span class=issue_counter>#{@issues.total_count}</span>) + .pull-right + .span6 + - 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-right", title: "New Issue", id: "new_issue_link" do + %i.icon-plus + New Issue + = form_tag project_issues_path(@project), method: :get, remote: true, id: "issue_search_form", class: 'pull-right' do + = hidden_field_tag :status, params[:status], id: 'search_status' + = hidden_field_tag :assignee_id, params[:assignee_id], id: 'search_assignee_id' + = hidden_field_tag :milestone_id, params[:milestone_id], id: 'search_milestone_id' + = hidden_field_tag :label_name, params[:label_name], id: 'search_label_name' + = search_field_tag :issue_search, nil, { placeholder: 'Filter by title or description', class: 'input-xpadding issue_search input-xlarge append-right-10 search-text-input' } + +.row + .span3 + = render 'shared/project_filter', project_entities_path: project_issues_path(@project) + .span9.issues-holder + = render "issues" diff --git a/app/views/projects/issues/index.js.haml b/app/views/projects/issues/index.js.haml new file mode 100644 index 00000000000..1be6a64f535 --- /dev/null +++ b/app/views/projects/issues/index.js.haml @@ -0,0 +1,4 @@ +:plain + $('.issues-holder').html("#{escape_javascript(render('issues'))}"); + History.replaceState({path: "#{request.url}"}, document.title, "#{request.url}"); + Issues.reload(); diff --git a/app/views/issues/new.html.haml b/app/views/projects/issues/new.html.haml index b1bc3ba0eba..b1bc3ba0eba 100644 --- a/app/views/issues/new.html.haml +++ b/app/views/projects/issues/new.html.haml diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml new file mode 100644 index 00000000000..6d1a088721c --- /dev/null +++ b/app/views/projects/issues/show.html.haml @@ -0,0 +1,74 @@ +%h3.page-title + Issue ##{@issue.iid} + + %small + created at + = @issue.created_at.stamp("Aug 21, 2011") + + %span.pull-right + - if can?(current_user, :write_issue, @project) + = link_to new_project_issue_path(@project), class: "btn grouped", title: "New Issue", id: "new_issue_link" do + %i.icon-plus + New Issue + - if can?(current_user, :modify_issue, @issue) + - if @issue.closed? + = link_to 'Reopen', project_issue_path(@project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn grouped reopen_issue" + - else + = link_to 'Close', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn grouped close_issue", title: "Close Issue" + + = link_to edit_project_issue_path(@project, @issue), class: "btn grouped" do + %i.icon-edit + Edit + +.pull-right + .span3#votes= render 'votes/votes_block', votable: @issue + +.back-link + = link_to project_issues_path(@project) do + ← To issues list + + +.ui-box.ui-box-show + .ui-box-head + %h4.box-title + - if @issue.closed? + .error.status_info Closed + = gfm escape_once(@issue.title) + + .ui-box-body + %cite.cgray + Created by #{link_to_member(@project, @issue.author)} + - if @issue.assignee + \ and currently assigned to #{link_to_member(@project, @issue.assignee)} + + - if @issue.milestone + - milestone = @issue.milestone + %cite.cgray and attached to milestone + %strong= link_to_gfm truncate(milestone.title, length: 20), project_milestone_path(milestone.project, milestone) + + .pull-right + - @issue.labels.each do |label| + %span{class: "label #{label_css_class(label.name)}"} + %i.icon-tag + = label.name + + + - if @issue.description.present? + .ui-box-bottom + .wiki + = preserve do + = markdown @issue.description + +- content_for :note_actions do + - if can?(current_user, :modify_issue, @issue) + - if @issue.closed? + = link_to 'Reopen Issue', project_issue_path(@project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn grouped reopen_issue" + - else + = link_to 'Close Issue', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn grouped close_issue", title: "Close Issue" + +.participants + %cite.cgray #{@issue.participants.count} participants + - @issue.participants.each do |participant| + = link_to_member(@project, participant, name: false, size: 24) + +.voting_notes#notes= render "projects/notes/notes_with_form" diff --git a/app/views/issues/update.js.haml b/app/views/projects/issues/update.js.haml index 7f66022a2de..7f66022a2de 100644 --- a/app/views/issues/update.js.haml +++ b/app/views/projects/issues/update.js.haml diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml new file mode 100644 index 00000000000..2b1aafc546b --- /dev/null +++ b/app/views/projects/labels/_label.html.haml @@ -0,0 +1,15 @@ +- frequency = @project.issues.tagged_with(label.name).count +%li + %strong + %span{class: "label #{label_css_class(label.name)}"} + %i.icon-tag + - if frequency.zero? + %span.light= label.name + - else + = label.name + .pull-right + - unless frequency.zero? + = link_to project_issues_path(label_name: label.name) do + %strong + = pluralize(frequency, 'issue') + = "»" diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml new file mode 100644 index 00000000000..a058d09a1ce --- /dev/null +++ b/app/views/projects/labels/index.html.haml @@ -0,0 +1,10 @@ += render "projects/issues/head" + +- if @labels.present? + %ul.bordered-list.labels-table + - @labels.each do |label| + = render 'label', label: label + +- else + .light-well + %h3.nothing_here_message Add first label to your issues or #{link_to 'generate', generate_project_labels_path(@project), method: :post} default set of labels diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml new file mode 100644 index 00000000000..ce72756303e --- /dev/null +++ b/app/views/projects/merge_requests/_form.html.haml @@ -0,0 +1,87 @@ += form_for [@project, @merge_request], html: { class: "#{controller.action_name}-merge-request form-horizontal" } do |f| + -if @merge_request.errors.any? + .alert.alert-error + %ul + - @merge_request.errors.full_messages.each do |msg| + %li= msg + + .merge-request-branches + .row + .span5 + .clearfix + .pull-left + = f.select(:source_project_id,[[@merge_request.source_project.path_with_namespace,@merge_request.source_project.id]] , {}, {class: 'source_project chosen span3'}) + .pull-left + + %i.icon-code-fork + = f.select(:source_branch, @merge_request.source_project.repository.branch_names, { include_blank: "Select branch" }, {class: 'source_branch chosen span2'}) + .mr_source_commit.prepend-top-10 + .span2 + %h2.merge-request-angle.light + %i.icon-long-arrow-right + .span5 + .clearfix + .pull-left + - projects = @project.forked_from_project.nil? ? [@project] : [ @project,@project.forked_from_project] + = f.select(:target_project_id, projects.map { |proj| [proj.path_with_namespace,proj.id] }, {include_blank: "Select Target Project" }, {class: 'target_project chosen span3'}) + .pull-left + + %i.icon-code-fork + = f.select(:target_branch, @target_branches, { include_blank: "Select branch" }, {class: 'target_branch chosen span2'}) + .mr_target_commit.prepend-top-10 + + %hr + .merge-request-form-info + .control-group + = f.label :title do + %strong= "Title *" + .controls= f.text_field :title, class: "input-xxlarge pad js-gfm-input", maxlength: 255, rows: 5, required: true + .control-group + .left + = f.label :assignee_id do + %i.icon-user + Assign to + .controls= f.select(:assignee_id, @project.team.members.sort_by(&:name).map {|p| [ p.name, p.id ] }, { include_blank: "Select user" }, {class: 'chosen span3'}) + .left + = f.label :milestone_id do + %i.icon-time + Milestone + .controls= f.select(:milestone_id, @project.milestones.active.all.map {|p| [ p.title, p.id ] }, { include_blank: "Select milestone" }, {class: 'chosen'}) + .control-group + = f.label :description, "Description" + .controls + = f.text_area :description, class: "input-xxlarge js-gfm-input", rows: 14 + %p.hint Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + + + .form-actions + - if @merge_request.new_record? + = f.submit 'Submit merge request', class: "btn btn-create" + -else + = f.submit 'Save changes', class: "btn btn-save" + - if @merge_request.new_record? + = link_to project_merge_requests_path(@source_project), class: "btn btn-cancel" do + Cancel + - else + = link_to project_merge_request_path(@target_project, @merge_request), class: "btn btn-cancel" do + Cancel + +:javascript + disableButtonIfEmptyField("#merge_request_title", ".btn-save"); + + var source_branch = $("#merge_request_source_branch") + , target_branch = $("#merge_request_target_branch") + , target_project = $("#merge_request_target_project_id"); + + $.get("#{branch_from_project_merge_requests_path(@source_project)}", {ref: source_branch.val() }); + $.get("#{branch_to_project_merge_requests_path(@source_project)}", {target_project_id: target_project.val(),ref: target_branch.val() }); + + target_project.on("change", function() { + $.get("#{update_branches_project_merge_requests_path(@source_project)}", {target_project_id: $(this).val() }); + }); + source_branch.on("change", function() { + $.get("#{branch_from_project_merge_requests_path(@source_project)}", {ref: $(this).val() }); + }); + target_branch.on("change", function() { + $.get("#{branch_to_project_merge_requests_path(@source_project)}", {target_project_id: target_project.val(),ref: $(this).val() }); + }); diff --git a/app/views/merge_requests/_head.html.haml b/app/views/projects/merge_requests/_head.html.haml index 35a86e6511c..35a86e6511c 100644 --- a/app/views/merge_requests/_head.html.haml +++ b/app/views/projects/merge_requests/_head.html.haml diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml new file mode 100644 index 00000000000..933d78bcbfb --- /dev/null +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -0,0 +1,37 @@ +%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.icon-ok + = "MERGED" + - else + %span.pull-right + - if merge_request.for_fork? + %span.light + = "#{merge_request.source_project.path_with_namespace}" + = "#{merge_request.source_branch}" + %i.icon-angle-right.light + = "#{merge_request.target_branch}" + - else + = "#{merge_request.source_branch}" + %i.icon-angle-right.light + = "#{merge_request.target_branch}" + .merge-request-info + - if merge_request.author + authored by #{link_to_member(merge_request.source_project, merge_request.author)} + - if merge_request.votes_count > 0 + = render 'votes/votes_inline', votable: merge_request + - if merge_request.notes.any? + %span + %i.icon-comments + = merge_request.mr_and_commit_notes.count + - if merge_request.milestone_id? + %span + %i.icon-time + = merge_request.milestone.title + + + .pull-right + %small updated #{time_ago_in_words(merge_request.updated_at)} ago diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml new file mode 100644 index 00000000000..47c14abdedd --- /dev/null +++ b/app/views/projects/merge_requests/_show.html.haml @@ -0,0 +1,38 @@ +.merge-request + = render "projects/merge_requests/show/mr_title" + = render "projects/merge_requests/show/how_to_merge" + = render "projects/merge_requests/show/mr_box" + = render "projects/merge_requests/show/mr_accept" + - if @merge_request.source_project.gitlab_ci? + = render "projects/merge_requests/show/mr_ci" + = render "projects/merge_requests/show/commits" + + - if @commits.present? + %ul.nav.nav-tabs + %li.notes-tab{data: {action: 'notes'}} + = link_to project_merge_request_path(@project, @merge_request) do + %i.icon-comment + Discussion + %li.diffs-tab{data: {action: 'diffs'}} + = link_to diffs_project_merge_request_path(@project, @merge_request) do + %i.icon-list-alt + Diff + + .notes.tab-content.voting_notes#notes{ class: (controller.action_name == 'show') ? "" : "hide" } + = render "projects/notes/notes_with_form" + .diffs.tab-content + - if current_page?(action: 'diffs') + = render "projects/merge_requests/show/diffs" + .status + +:javascript + var merge_request; + + merge_request = new MergeRequest({ + url_to_automerge_check: "#{automerge_check_project_merge_request_path(@project, @merge_request)}", + check_enable: #{@merge_request.unchecked? ? "true" : "false"}, + url_to_ci_check: "#{ci_status_project_merge_request_path(@project, @merge_request)}", + ci_enable: #{@project.gitlab_ci? ? "true" : "false"}, + current_status: "#{@merge_request.merge_status_name}", + action: "#{controller.action_name}" + }); diff --git a/app/views/merge_requests/automerge.js.haml b/app/views/projects/merge_requests/automerge.js.haml index e01ff662e7d..e01ff662e7d 100644 --- a/app/views/merge_requests/automerge.js.haml +++ b/app/views/projects/merge_requests/automerge.js.haml diff --git a/app/views/projects/merge_requests/branch_from.js.haml b/app/views/projects/merge_requests/branch_from.js.haml new file mode 100644 index 00000000000..ec4d7f2121b --- /dev/null +++ b/app/views/projects/merge_requests/branch_from.js.haml @@ -0,0 +1,7 @@ +:plain + $(".mr_source_commit").html("#{commit_to_html(@commit, @source_project)}"); + var mrTitle = $('#merge_request_title'); + + if(mrTitle.val().length == 0) { + mrTitle.val("#{params[:ref].titleize}"); + } diff --git a/app/views/projects/merge_requests/branch_to.js.haml b/app/views/projects/merge_requests/branch_to.js.haml new file mode 100644 index 00000000000..f4e2886ee44 --- /dev/null +++ b/app/views/projects/merge_requests/branch_to.js.haml @@ -0,0 +1,2 @@ +:plain + $(".mr_target_commit").html("#{commit_to_html(@commit, @target_project)}"); diff --git a/app/views/merge_requests/commits.js.haml b/app/views/projects/merge_requests/commits.js.haml index 923b1ea032f..923b1ea032f 100644 --- a/app/views/merge_requests/commits.js.haml +++ b/app/views/projects/merge_requests/commits.js.haml diff --git a/app/views/merge_requests/diffs.html.haml b/app/views/projects/merge_requests/diffs.html.haml index 2a5b8b1441e..2a5b8b1441e 100644 --- a/app/views/merge_requests/diffs.html.haml +++ b/app/views/projects/merge_requests/diffs.html.haml diff --git a/app/views/merge_requests/diffs.js.haml b/app/views/projects/merge_requests/diffs.js.haml index 266892c01ef..2964f0ec462 100644 --- a/app/views/merge_requests/diffs.js.haml +++ b/app/views/projects/merge_requests/diffs.js.haml @@ -1,2 +1,2 @@ :plain - merge_request.$(".diffs").html("#{escape_javascript(render(partial: "merge_requests/show/diffs"))}"); + merge_request.$(".diffs").html("#{escape_javascript(render(partial: "projects/merge_requests/show/diffs"))}"); diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml new file mode 100644 index 00000000000..67a1541d9bf --- /dev/null +++ b/app/views/projects/merge_requests/edit.html.haml @@ -0,0 +1,4 @@ +%h3.page-title + = "Edit merge request ##{@merge_request.id}" +%hr += render 'form' diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml new file mode 100644 index 00000000000..2bd5a027a02 --- /dev/null +++ b/app/views/projects/merge_requests/index.html.haml @@ -0,0 +1,76 @@ +- 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.icon-plus + New Merge Request +%h3.page-title + Merge Requests + %span (#{@merge_requests.total_count}) + + +.row + .span3 + = render 'shared/project_filter', project_entities_path: project_merge_requests_path(@project) + .span9 + .ui-box + .title + .mr-filters + %span Filter by + .dropdown.inline.prepend-left-10 + %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} + %i.icon-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 + - @project.team.members.sort_by(&:name).each do |user| + %li + = link_to project_filter_path(assignee_id: user.id) do + = image_tag gravatar_icon(user.email), class: "avatar s16", alt: '' + = user.name + + .dropdown.inline.prepend-left-10 + %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} + %i.icon-time + %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 + + %ul.well-list.mr-list + = render @merge_requests + - if @merge_requests.blank? + %li + %h4.nothing_here_message No merge requests to show + - if @merge_requests.present? + .pull-right + %span.cgray.pull-right #{@merge_requests.total_count} merge requests for this filter + + = paginate @merge_requests, theme: "gitlab" + +:javascript + $(merge_requestsPage); diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml new file mode 100644 index 00000000000..c962811a3e4 --- /dev/null +++ b/app/views/projects/merge_requests/invalid.html.haml @@ -0,0 +1,17 @@ +.merge-request + = render "projects/merge_requests/show/mr_title" + = render "projects/merge_requests/show/mr_box" + + .alert.alert-error + %h5 + %i.icon-exclamation-sign + We cannot find + %span.label-branch= @merge_request.source_branch + or + %span.label-branch= @merge_request.target_branch + branches in the repository. + %p + Maybe it was removed or never pushed. + %p + Please close Merge Request or change branches with existing one + diff --git a/app/views/projects/merge_requests/new.html.haml b/app/views/projects/merge_requests/new.html.haml new file mode 100644 index 00000000000..8ee0e1a8d46 --- /dev/null +++ b/app/views/projects/merge_requests/new.html.haml @@ -0,0 +1,3 @@ +%h3.page-title New Merge Request +%hr += render 'form' diff --git a/app/views/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 2a5b8b1441e..2a5b8b1441e 100644 --- a/app/views/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml diff --git a/app/views/merge_requests/show.js.haml b/app/views/projects/merge_requests/show.js.haml index 2ce6eb63290..2ce6eb63290 100644 --- a/app/views/merge_requests/show.js.haml +++ b/app/views/projects/merge_requests/show.js.haml diff --git a/app/views/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index 5e27b6dc25a..7b0e67053a5 100644 --- a/app/views/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -1,30 +1,30 @@ - if @commits.present? .ui-box - %h5.title + .title %i.icon-list Commits (#{@commits.count}) .commits - if @commits.count > 8 %ul.first-commits.well-list - @commits.first(8).each do |commit| - = render "commits/commit", commit: commit + = render "projects/commits/commit", commit: commit, project: @merge_request.source_project %li.bottom 8 of #{@commits.count} commits displayed. %strong %a.show-all-commits Click here to show all %ul.all-commits.hide.well-list - @commits.each do |commit| - = render "commits/commit", commit: commit + = render "projects/commits/commit", commit: commit, project: @merge_request.source_project - else %ul.well-list - @commits.each do |commit| - = render "commits/commit", commit: commit + = render "projects/commits/commit", commit: commit, project: @merge_request.source_project - else - %h5 + %h4.nothing_here_message Nothing to merge from - %span.label #{@merge_request.source_branch} + %span.label-branch #{@merge_request.source_branch} to - %span.label #{@merge_request.target_branch} + %span.label-branch #{@merge_request.target_branch} %br diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml new file mode 100644 index 00000000000..25f63804858 --- /dev/null +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -0,0 +1,10 @@ +- if @merge_request.valid_diffs? + = render "projects/commits/diffs", diffs: @merge_request.diffs, project: @merge_request.source_project +- elsif @merge_request.broken_diffs? + %h4.nothing_here_message + Can't load diff. + You can + = link_to "download it", project_merge_request_path(@merge_request.source_project, @merge_request), format: :diff, class: "vlink" + instead. +- else + %h4.nothing_here_message Nothing to merge 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 new file mode 100644 index 00000000000..030ac285f2a --- /dev/null +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -0,0 +1,51 @@ +%div#modal_merge_info.modal.hide + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h3 How to merge + .modal-body + - if @merge_request.for_fork? + - source_remote = @merge_request.source_project.namespace.nil? ? "source" :@merge_request.source_project.namespace.path + - target_remote = @merge_request.target_project.namespace.nil? ? "target" :@merge_request.target_project.namespace.path + %p + %strong Step 1. + Checkout target branch and get recent objects from GitLab + Assuming remote for #{@merge_request.target_project.path_with_namespace} is called #{target_remote} + remote for #{@merge_request.source_project.path_with_namespace} is called #{source_remote} + %pre.dark + :preserve + git checkout #{target_remote} #{@merge_request.target_branch} + git fetch #{source_remote} + %p + %strong Step 2. + Merge source branch into target branch and push changes to GitLab + %pre.dark + :preserve + git merge #{source_remote}/#{@merge_request.source_branch} + git push #{target_remote} #{@merge_request.target_branch} + - else + %p + %strong Step 1. + Checkout target branch and get recent objects from GitLab + %pre.dark + :preserve + git checkout #{@merge_request.target_branch} + git fetch origin + %p + %strong Step 2. + Merge source branch into target branch and push changes to GitLab + %pre.dark + :preserve + git merge origin/#{@merge_request.source_branch} + git push origin #{@merge_request.target_branch} + + +:javascript + $(function(){ + var modal = $('#modal_merge_info').modal({modal: true, show:false}); + $('.how_to_merge_link').bind("click", function(){ + modal.show(); + }); + $('.modal-header .close').bind("click", function(){ + modal.hide(); + }) + }) diff --git a/app/views/merge_requests/show/_mr_accept.html.haml b/app/views/projects/merge_requests/show/_mr_accept.html.haml index c2c04b863e7..299e1bc1e08 100644 --- a/app/views/merge_requests/show/_mr_accept.html.haml +++ b/app/views/projects/merge_requests/show/_mr_accept.html.haml @@ -1,9 +1,9 @@ -- unless can?(current_user, :accept_mr, @project) +- unless @allowed_to_merge .alert - %strong Only masters can accept MR + %strong You don't have permission to merge this MR -- if @merge_request.open? && @commits.any? && can?(current_user, :accept_mr, @project) +- if @show_merge_controls .automerge_widget.can_be_merged{style: "display:none"} .alert.alert-success %span @@ -11,11 +11,12 @@ %p You can accept this request automatically. If you still want to do it manually - - %strong= link_to "click here", "#", class: "how_to_merge_link vlink", title: "How To Merge" + %strong + = link_to "click here", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" for instructions .accept_group - = f.submit "Accept Merge Request", class: "btn success accept_merge_request" - - unless @project.root_ref? @merge_request.source_branch + = f.submit "Accept Merge Request", class: "btn btn-create accept_merge_request" + - unless @merge_request.disallow_source_branch_removal? .remove_branch_holder = label_tag :should_remove_source_branch, class: "checkbox" do = check_box_tag :should_remove_source_branch @@ -26,12 +27,12 @@ .automerge_widget.no_satellite{style: "display:none"} .alert.alert-error %span - %strong This repository does not have satellite. Ask administrator to fix this issue + %strong This repository does not have satellite. Ask an administrator to fix this issue .automerge_widget.cannot_be_merged{style: "display:none"} .alert.alert-disabled %span - = link_to "Show how to merge", "#", class: "how_to_merge_link btn btn-small padded", title: "How To Merge" + = link_to "Show how to merge", "#modal_merge_info", class: "how_to_merge_link btn padded", title: "How To Merge", "data-toggle" => "modal" %strong This request can't be merged with GitLab. You should do it manually @@ -46,5 +47,7 @@ %strong This merge request already can not be merged. Try to reload page. .merge-in-progress.hide - %span.cgray Merge is in progress. Please wait. Page will be automatically reloaded. - = image_tag "ajax_loader.gif" + %span.cgray + %i.icon-refresh.icon-spin + + Merge is in progress. Please wait. Page will be automatically reloaded. diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml new file mode 100644 index 00000000000..1f750e22c65 --- /dev/null +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -0,0 +1,48 @@ +.ui-box.ui-box-show + .ui-box-head + %h4.box-title + = gfm escape_once(@merge_request.title) + - if @merge_request.merged? + .success.status_info + %i.icon-ok + Merged + - elsif @merge_request.closed? + .error.status_info Closed + + .ui-box-body + %div + %cite.cgray + Created at #{@merge_request.created_at.stamp("Aug 21, 2011")} by #{link_to_member(@project, @merge_request.author)} + - if @merge_request.assignee + \, currently assigned to #{link_to_member(@project, @merge_request.assignee)} + - if @merge_request.milestone + - milestone = @merge_request.milestone + %cite.cgray and attached to milestone + %strong= link_to_gfm truncate(milestone.title, length: 20), project_milestone_path(milestone.project, milestone) + + + - if @merge_request.description.present? + .ui-box-bottom + .wiki + = preserve do + = markdown @merge_request.description + + - if @merge_request.closed? + .ui-box-bottom.alert-error + %span + %i.icon-remove + Closed by #{link_to_member(@project, @merge_request.closed_event.author)} + %small #{time_ago_in_words(@merge_request.closed_event.created_at)} ago. + - if @merge_request.merged? + .ui-box-bottom.alert-success + %span + %i.icon-ok + Merged by #{link_to_member(@project, @merge_request.merge_event.author)} + #{time_ago_in_words(@merge_request.merge_event.created_at)} ago. + - if !@closes_issues.empty? && @merge_request.opened? + .ui-box-bottom.alert-info + %span + %i.icon-ok + Accepting this merge request will close #{@closes_issues.size == 1 ? 'issue' : 'issues'} + = succeed '.' do + != gfm(@closes_issues.map { |i| "##{i.iid}" }.to_sentence) diff --git a/app/views/merge_requests/show/_mr_ci.html.haml b/app/views/projects/merge_requests/show/_mr_ci.html.haml index dd1e78a0205..9b15c4526e9 100644 --- a/app/views/merge_requests/show/_mr_ci.html.haml +++ b/app/views/projects/merge_requests/show/_mr_ci.html.haml @@ -1,4 +1,4 @@ -- if @merge_request.open? && @commits.any? +- if @commits.any? .ci_widget.ci-success{style: "display:none"} .alert.alert-success %i.icon-ok diff --git a/app/views/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 8119728dcb9..096a9167645 100644 --- a/app/views/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,13 +1,20 @@ -%h3.page_title - = "Merge Request ##{@merge_request.id}:" +%h3.page-title + = "Merge Request ##{@merge_request.iid}:" - %span.label_branch= @merge_request.source_branch - → - %span.label_branch= @merge_request.target_branch + -if @merge_request.for_fork? + %span.label-branch + %span.label-project= truncate(@merge_request.source_project.path_with_namespace, length: 25) + #{@merge_request.source_branch} + → + %span.label-branch= @merge_request.target_branch + - else + %span.label-branch= @merge_request.source_branch + → + %span.label-branch= @merge_request.target_branch %span.pull-right - if can?(current_user, :modify_merge_request, @merge_request) - - if @merge_request.open? + - if @merge_request.opened? .left.btn-group %a.btn.grouped.dropdown-toggle{ data: {toggle: :dropdown} } %i.icon-download-alt @@ -17,15 +24,15 @@ %li= link_to "Email Patches", project_merge_request_path(@project, @merge_request, format: :patch) %li= link_to "Plain Diff", project_merge_request_path(@project, @merge_request, format: :diff) - = link_to 'Close', project_merge_request_path(@project, @merge_request, merge_request: {closed: true }, status_only: true), method: :put, class: "btn grouped btn-close", title: "Close merge request" + = link_to 'Close', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :close }), method: :put, class: "btn grouped btn-close", title: "Close merge request" - = link_to edit_project_merge_request_path(@project, @merge_request), class: "btn grouped" do + = link_to edit_project_merge_request_path(@project, @merge_request), class: "btn grouped", id:"edit_merge_request" do %i.icon-edit Edit .pull-right .span3#votes= render 'votes/votes_block', votable: @merge_request -.back_link +.back-link = link_to project_merge_requests_path(@project) do ← To merge requests diff --git a/app/views/projects/merge_requests/update_branches.js.haml b/app/views/projects/merge_requests/update_branches.js.haml new file mode 100644 index 00000000000..dfccb586ec7 --- /dev/null +++ b/app/views/projects/merge_requests/update_branches.js.haml @@ -0,0 +1,5 @@ +:plain + $(".target_branch").html("#{escape_javascript(options_for_select(@target_branches))}"); + $(".target_branch").trigger("chosen:updated"); + $(".mr_target_commit").html(""); + $(".target_branch").trigger("change"); diff --git a/app/views/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index fbaf64a305c..78e4cd2243e 100644 --- a/app/views/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -1,5 +1,5 @@ -%h3.page_title= @milestone.new_record? ? "New Milestone" : "Edit Milestone ##{@milestone.id}" -.back_link +%h3.page-title= @milestone.new_record? ? "New Milestone" : "Edit Milestone ##{@milestone.id}" +.back-link = link_to project_milestones_path(@project) do ← To milestones @@ -26,13 +26,13 @@ .span6 .control-group = f.label :due_date, "Due Date", class: "control-label" - .input= f.hidden_field :due_date + .controls= f.hidden_field :due_date .controls .datepicker .form-actions - if @milestone.new_record? - = f.submit 'Create milestone', class: "btn-save btn" + = f.submit 'Create milestone', class: "btn-create btn" = link_to "Cancel", project_milestones_path(@project), class: "btn btn-cancel" -else = f.submit 'Save changes', class: "btn-save btn" @@ -40,10 +40,8 @@ :javascript - $(function() { - disableButtonIfEmptyField("#milestone_title", ".btn-save"); - $( ".datepicker" ).datepicker({ - dateFormat: "yy-mm-dd", - onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } - }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); - }); + disableButtonIfEmptyField("#milestone_title", ".btn-save"); + $( ".datepicker" ).datepicker({ + dateFormat: "yy-mm-dd", + onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } + }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); diff --git a/app/views/projects/milestones/_issues.html.haml b/app/views/projects/milestones/_issues.html.haml new file mode 100644 index 00000000000..bf81cfda45f --- /dev/null +++ b/app/views/projects/milestones/_issues.html.haml @@ -0,0 +1,11 @@ +.ui-box + .title= title + %ul.well-list + - issues.each do |issue| + %li + = link_to [@project, issue] do + %span.badge{class: issue.closed? ? 'badge-important' : 'badge-info'} ##{issue.iid} + = link_to_gfm truncate(issue.title, length: 40), [@project, issue] + - if issue.assignee + .pull-right + = image_tag gravatar_icon(issue.assignee.email, 16), class: "avatar s16" diff --git a/app/views/projects/milestones/_merge_request.html.haml b/app/views/projects/milestones/_merge_request.html.haml new file mode 100644 index 00000000000..7f815894069 --- /dev/null +++ b/app/views/projects/milestones/_merge_request.html.haml @@ -0,0 +1,5 @@ +%li + = link_to [@project, merge_request] do + %span.badge.badge-info ##{merge_request.id} + – + = link_to_gfm truncate(merge_request.title, length: 60), [@project, merge_request] diff --git a/app/views/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index 00e20117f36..bc3368b765c 100644 --- a/app/views/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -1,23 +1,21 @@ -%li{class: "milestone milestone-#{milestone.closed ? 'closed' : 'open'}", id: dom_id(milestone) } +%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone) } .pull-right - - if can?(current_user, :admin_milestone, milestone.project) and milestone.open? + - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? = link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-small edit-milestone-link grouped" do %i.icon-edit Edit + = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-small btn-remove" %h4 = link_to_gfm truncate(milestone.title, length: 100), project_milestone_path(milestone.project, milestone) - - if milestone.expired? and not milestone.closed + - if milestone.expired? and not milestone.closed? %span.cred (Expired) %small = milestone.expires_at - if milestone.is_empty? %span.muted Empty - else - .row - .span4 - .progress.progress-info - .bar{style: "width: #{milestone.percent_complete}%;"} - .span6 + %div + %div = link_to project_issues_path(milestone.project, milestone_id: milestone.id) do = pluralize milestone.issues.count, 'Issue' @@ -25,3 +23,5 @@ = pluralize milestone.merge_requests.count, 'Merge Request' %span.light #{milestone.percent_complete}% complete + .progress.progress-info + .bar{style: "width: #{milestone.percent_complete}%;"} diff --git a/app/views/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index b1bc3ba0eba..b1bc3ba0eba 100644 --- a/app/views/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml diff --git a/app/views/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index b78f17053fd..ddb46bcb5cb 100644 --- a/app/views/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,13 +1,15 @@ -= render "issues/head" += render "projects/issues/head" .milestones_content - %h3.page_title + %h3.page-title Milestones - if can? current_user, :admin_milestone, @project - = link_to "New Milestone", new_project_milestone_path(@project), class: "pull-right btn btn-small", title: "New Milestone" - %br - %div.ui-box - .title - %ul.nav.nav-pills + = link_to new_project_milestone_path(@project), class: "pull-right btn btn-new", title: "New Milestone" do + %i.icon-plus + New Milestone + + .row + .span3 + %ul.nav.nav-pills.nav-stacked %li{class: ("active" if (params[:f] == "active" || !params[:f]))} = link_to project_milestones_path(@project, f: "active") do Active @@ -17,12 +19,13 @@ %li{class: ("active" if params[:f] == "all")} = link_to project_milestones_path(@project, f: "all") do All + .span9 + .ui-box + %ul.well-list + = render @milestones - %ul.well-list - = render @milestones + - if @milestones.blank? + %li + %h3.nothing_here_message No milestones to show - - if @milestones.present? - %li.bottom= paginate @milestones, theme: "gitlab" - - else - %li - %h3.nothing_here_message Nothing to show here + = paginate @milestones, theme: "gitlab" diff --git a/app/views/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml index b1bc3ba0eba..b1bc3ba0eba 100644 --- a/app/views/milestones/new.html.haml +++ b/app/views/projects/milestones/new.html.haml diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml new file mode 100644 index 00000000000..b755a813419 --- /dev/null +++ b/app/views/projects/milestones/show.html.haml @@ -0,0 +1,105 @@ += render "projects/issues/head" +%h3.page-title + Milestone ##{@milestone.id} + %small + = @milestone.expires_at + .pull-right + - if can?(current_user, :admin_milestone, @project) + = link_to edit_project_milestone_path(@project, @milestone), class: "btn grouped" do + %i.icon-edit + Edit + - if @milestone.active? + = link_to 'Close Milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-remove" + - else + = link_to 'Reopen Milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn" + +- if @milestone.issues.any? && @milestone.can_be_closed? + .alert.alert-success + %span All issues for this milestone are closed. You may close milestone now. + +.back-link + = link_to project_milestones_path(@project) do + ← To milestones list + + +.ui-box.ui-box-show + .ui-box-head + %h4.box-title + - if @milestone.closed? + .error.status_info Closed + - elsif @milestone.expired? + .error.status_info Expired + + = gfm escape_once(@milestone.title) + + .ui-box-body + %p + Progress: + #{@milestone.closed_items_count} closed + – + #{@milestone.open_items_count} open + %span.pull-right= @milestone.expires_at + .progress.progress-info + .bar{style: "width: #{@milestone.percent_complete}%;"} + + + - if @milestone.description.present? + .ui-box-bottom + = preserve do + = markdown @milestone.description + + +%ul.nav.nav-tabs + %li.active + = link_to '#tab-issues', 'data-toggle' => 'tab' do + Issues + %span.badge= @issues.count + %li + = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do + Merge Requests + %span.badge= @merge_requests.count + %li + = link_to '#tab-participants', 'data-toggle' => 'tab' do + Participants + %span.badge= @users.count + + .pull-right + = link_to new_project_issue_path(@project, issue: { milestone_id: @milestone.id }), class: "btn btn-small grouped", title: "New Issue" do + %i.icon-plus + New Issue + = link_to 'Browse Issues', project_issues_path(@milestone.project, milestone_id: @milestone.id), class: "btn btn-small edit-milestone-link grouped" + +.tab-content + .tab-pane.active#tab-issues + .row + .span4 + = render('issues', title: 'Unstarted Issues (open and unassigned)', issues: @issues.opened.unassigned) + .span4 + = render('issues', title: 'Ongoing Issues (open and assigned)', issues: @issues.opened.assigned) + .span4 + = render('issues', title: 'Completed Issues (closed)', issues: @issues.closed) + + .tab-pane#tab-merge-requests + .row + .span6 + .ui-box + .title Open + %ul.well-list + - @merge_requests.opened.each do |merge_request| + = render 'merge_request', merge_request: merge_request + .span6 + .ui-box + .title Closed + %ul.well-list + - @merge_requests.closed.each do |merge_request| + = render 'merge_request', merge_request: merge_request + + .tab-pane#tab-participants + %ul.bordered-list + - @users.each do |user| + %li + = link_to user, title: user.name, class: "dark" do + = image_tag gravatar_icon(user.email, 32), class: "avatar s32" + %strong= truncate(user.name, lenght: 40) + %br + %small.cgray= user.username diff --git a/app/views/projects/milestones/update.js.haml b/app/views/projects/milestones/update.js.haml new file mode 100644 index 00000000000..3ff84915e97 --- /dev/null +++ b/app/views/projects/milestones/update.js.haml @@ -0,0 +1,2 @@ +:plain + $('##{dom_id(@milestone)}').fadeOut(); diff --git a/app/views/projects/network/_head.html.haml b/app/views/projects/network/_head.html.haml new file mode 100644 index 00000000000..2790ed6f594 --- /dev/null +++ b/app/views/projects/network/_head.html.haml @@ -0,0 +1,23 @@ +.clearfix + .pull-left + = render partial: 'shared/ref_switcher', locals: {destination: 'graph'} + .pull-left + = form_tag project_network_path(@project, @id), method: :get do |f| + .control-group + = label_tag :filter_ref, "Begin with the selected commit", class: 'control-label light' + .controls + = check_box_tag :filter_ref, 1, @options[:filter_ref] + - @options.each do |key, value| + = hidden_field_tag(key, value, id: nil) unless key == "filter_ref" + + .search.pull-right + = form_tag project_network_path(@project, @id), method: :get do |f| + .control-group + = label_tag :search , "Looking for commit:", class: 'control-label light' + .controls + = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Input an extended SHA1 syntax", class: "search-input input-xlarge" + = button_tag type: 'submit', class: 'btn vtop' do + %i.icon-search + - @options.each do |key, value| + = hidden_field_tag(key, value, id: nil) unless key == "extended_sha1" + diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml new file mode 100644 index 00000000000..492f77341f7 --- /dev/null +++ b/app/views/projects/network/show.html.haml @@ -0,0 +1,14 @@ += render "head" +.project-network + .tip + You can move around the graph by using the arrow keys. + .network-graph + .loading.loading-gray + +:javascript + new Network({ + url: '#{project_network_path(@project, @ref, @options.merge(format: :json))}', + commit_url: '#{project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")}', + ref: '#{@ref}', + commit_id: '#{@commit.id}' + }) diff --git a/app/views/projects/network/show.json.erb b/app/views/projects/network/show.json.erb new file mode 100644 index 00000000000..f0bedcf2d35 --- /dev/null +++ b/app/views/projects/network/show.json.erb @@ -0,0 +1,23 @@ +<% self.formats = ["html"] %> + +<%= raw( + { + days: @graph.days.compact.map { |d| [d.day, d.strftime("%b")] }, + commits: @graph.commits.map do |c| + { + parents: parents_zip_spaces(c.parents(@graph.map), c.parent_spaces), + author: { + name: c.author_name, + email: c.author_email, + icon: gravatar_icon(c.author_email, 20) + }, + time: c.time, + space: c.spaces.first, + refs: get_refs(@graph.repo, c), + id: c.sha, + date: c.date, + message: c.message, + } + end + }.to_json +) %> diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 933cb671142..0213576927b 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -1,12 +1,71 @@ -.project_new_holder - %h3.page_title - New Project - %hr - = render 'new_form' -%div.save-project-loader.hide +.project-edit-container + .project-edit-errors + = render 'projects/errors' + .project-edit-content + + = form_for @project, remote: true do |f| + .control-group.project-name-holder + = f.label :name do + %strong Project name is + .controls + = f.text_field :name, placeholder: "Example Project", class: "input-xlarge", tabindex: 1, autofocus: true + %span.help-inline + = link_to "#", class: 'js-toggle-visibility-link' do + %span Customize repository name? + + .control-group.js-toggle-visibility-container.hide + = f.label :path do + %span Repository name + .controls + .input-append + = f.text_field :path + %span.add-on .git + + + - if current_user.can_select_namespace? + .control-group + = f.label :namespace_id do + %span Namespace + .controls + = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'chosen', tabindex: 2} + + .control-group + .controls + = link_to "#", class: 'appear-link' do + %i.icon-upload-alt + %span Import existing repository? + .control-group.appear-data.import-url-data + = f.label :import_url do + %span Import existing repo + .controls + = f.text_field :import_url, class: 'input-xlarge', placeholder: 'https://github.com/randx/six.git' + .light + URL must be cloneable + .control-group + = f.label :description do + Description + %span.light (optional) + .controls + = f.text_area :description, placeholder: "awesome project", class: "input-xlarge", rows: 3, maxlength: 250, tabindex: 3 + .control-group.project-public-holder + = f.label :public do + %span Public project + .controls + = f.check_box :public, { checked: Gitlab.config.gitlab.default_projects_features.public }, true, false + %span.help-inline Make project visible to everyone + + .form-actions + = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 + + - if current_user.can_create_group? + .pull-right + .controls.light + Need a group for several dependent projects? + = link_to new_group_path, class: "btn btn-tiny" do + Create a group + +.save-project-loader.hide %center = image_tag "ajax_loader.gif" - %h3 Creating project & repository. Please wait a few minutes - -:javascript - $(function(){ new Projects(); }); + %h3 Creating project & repository. + %p Please wait a moment, this page will automatically refresh when ready. diff --git a/app/views/notes/_diff_note_link.html.haml b/app/views/projects/notes/_diff_note_link.html.haml index 377c926a204..377c926a204 100644 --- a/app/views/notes/_diff_note_link.html.haml +++ b/app/views/projects/notes/_diff_note_link.html.haml diff --git a/app/views/projects/notes/_diff_notes_with_reply.html.haml b/app/views/projects/notes/_diff_notes_with_reply.html.haml new file mode 100644 index 00000000000..9537ab18caa --- /dev/null +++ b/app/views/projects/notes/_diff_notes_with_reply.html.haml @@ -0,0 +1,13 @@ +- note = notes.first # example note +-# Check if line want not changed since comment was left +- if !defined?(line) || line == note.diff_line + %tr.notes_holder + %td.notes_line{ colspan: 2 } + %span.btn.disabled + %i.icon-comment + = notes.count + %td.notes_content + %ul.notes{ rel: note.discussion_id } + = render notes + + = render "projects/notes/discussion_reply_button", note: note diff --git a/app/views/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml index 24cb4228174..a964d86a8dc 100644 --- a/app/views/notes/_discussion.html.haml +++ b/app/views/projects/notes/_discussion.html.haml @@ -8,7 +8,7 @@ = link_to "javascript:;", class: "js-details-target turn-off js-toggler-target" do %i.icon-eye-open Show discussion - = image_tag gravatar_icon(note.author.email), class: "avatar s32" + = image_tag gravatar_icon(note.author_email), class: "avatar s32" %div = link_to_member(@project, note.author, avatar: false) - if note.for_merge_request? @@ -23,7 +23,7 @@ discussion on this merge request diff - elsif note.for_commit? started a discussion on commit - #{link_to note.noteable.short_id, project_commit_path(@project, note.noteable)} + #{link_to note.noteable.short_id, project_commit_path(note.project, note.noteable)} = link_to_commit_diff_line_note(note) if note.for_diff_line? - else %cite.cgray started a discussion @@ -36,9 +36,9 @@ ago .discussion-body - if note.for_diff_line? - - if note.diff + - if note.active? .content - .file= render "notes/discussion_diff", discussion_notes: discussion_notes, note: note + .file= render "projects/notes/discussion_diff", discussion_notes: discussion_notes, note: note - else = link_to 'show outdated discussion', '#', class: 'js-show-outdated-discussion' %div.hide.outdated-discussion @@ -51,7 +51,7 @@ .content .notes{ rel: discussion_notes.first.discussion_id } = render discussion_notes - = render "notes/discussion_reply_button", note: discussion_notes.first + = render "projects/notes/discussion_reply_button", note: discussion_notes.first -# will be shown when the other one is hidden .discussion-hidden.content.hide diff --git a/app/views/notes/_discussion_diff.html.haml b/app/views/projects/notes/_discussion_diff.html.haml index 790b77333d5..c3f41a1b6b5 100644 --- a/app/views/notes/_discussion_diff.html.haml +++ b/app/views/projects/notes/_discussion_diff.html.haml @@ -9,7 +9,7 @@ %br/ .content %table - - each_diff_line(diff, note.diff_file_index) do |line, type, line_code, line_new, line_old| + - each_diff_line_near(diff, note.diff_file_index, note.line_code) do |line, type, line_code, line_new, line_old| %tr.line_holder{ id: line_code } - if type == "match" %td.old_line= "..." @@ -21,5 +21,4 @@ %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw "#{line} " - if line_code == note.line_code - = render "notes/diff_notes_with_reply", notes: discussion_notes - - break # cut off diff after notes + = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/notes/_discussion_reply_button.html.haml b/app/views/projects/notes/_discussion_reply_button.html.haml index d1c5ccc29db..d1c5ccc29db 100644 --- a/app/views/notes/_discussion_reply_button.html.haml +++ b/app/views/projects/notes/_discussion_reply_button.html.haml diff --git a/app/views/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index a154c31e5ab..7add2921830 100644 --- a/app/views/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project, @note], remote: true, html: { multipart: true, id: nil, class: "new_note js-new-note-form" } do |f| += form_for [@project, @note], remote: true, html: { multipart: true, id: nil, class: "new_note js-new-note-form common-note-form" } do |f| = note_target_fields = f.hidden_field :commit_id @@ -22,18 +22,11 @@ .note-form-actions .buttons = f.submit 'Add Comment', class: "btn comment-btn grouped js-comment-button" + = yield(:note_actions) + %a.btn.grouped.js-close-discussion-note-form Cancel .note-form-option - = label_tag :notify do - = check_box_tag :notify, 1, !@note.for_commit? - %span.light Notify team via email - - .js-notify-commit-author - = label_tag :notify_author do - = check_box_tag :notify_author, 1 , @note.for_commit? - %span.light Notify commit author - .note-form-option %a.choose-btn.btn.btn-small.js-choose-note-attachment-button %i.icon-paper-clip %span Choose File ... diff --git a/app/views/notes/_form_errors.html.haml b/app/views/projects/notes/_form_errors.html.haml index 0851536f0da..0b68bf243f0 100644 --- a/app/views/notes/_form_errors.html.haml +++ b/app/views/projects/notes/_form_errors.html.haml @@ -1,3 +1,3 @@ -.error_message.js-errors +.error-message.js-errors - note.errors.full_messages.each do |msg| %div= msg diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml new file mode 100644 index 00000000000..324b698f3b5 --- /dev/null +++ b/app/views/projects/notes/_note.html.haml @@ -0,0 +1,66 @@ +%li{ id: dom_id(note), class: dom_class(note), data: { discussion: note.discussion_id } } + .note-header + .note-actions + = link_to "##{dom_id(note)}", name: dom_id(note) do + %i.icon-link + Link here + + - if(note.author_id == current_user.try(:id)) || can?(current_user, :admin_note, @project) + = link_to "#", title: "Edit comment", class: "js-note-edit" do + %i.icon-edit + Edit + + = link_to project_note_path(@project, note), title: "Remove comment", method: :delete, confirm: 'Are you sure you want to remove this comment?', remote: true, class: "danger js-note-delete" do + %i.icon-trash.cred + Remove + = image_tag gravatar_icon(note.author_email), class: "avatar s32" + = link_to_member(@project, note.author, avatar: false) + %span.note-last-update + = note_timestamp(note) + + - if note.upvote? + %span.vote.upvote.label.label-success + %i.icon-thumbs-up + \+1 + - if note.downvote? + %span.vote.downvote.label.label-error + %i.icon-thumbs-down + \-1 + + + .note-body + .note-text + = preserve do + = markdown(note.note) + + .note-edit-form + = form_for note, url: project_note_path(@project, note), method: :put, remote: true do |f| + = f.text_area :note, class: 'note_text js-note-text js-gfm-input turn-on' + + .form-actions + = f.submit 'Save changes', class: "btn btn-primary btn-save" + + .note-form-option + %a.choose-btn.btn.btn-small.js-choose-note-attachment-button + %i.icon-paper-clip + %span Choose File ... + + %span.file_name.js-attachment-filename File name... + = f.file_field :attachment, class: "js-note-attachment-input hide" + + = link_to 'Cancel', "#", class: "btn btn-cancel note-edit-cancel" + + + - if note.attachment.url + .note-attachment + - if note.attachment.image? + = link_to note.attachment.url, target: '_blank' do + = image_tag note.attachment.url, class: 'note-image-attach' + .attachment.pull-right + = link_to note.attachment.secure_url, target: "_blank" do + %i.icon-paper-clip + = note.attachment_identifier + = link_to delete_attachment_project_note_path(@project, note), + title: "Delete this attachment", method: :delete, remote: true, confirm: 'Are you sure you want to remove the attachment?', class: "danger js-note-attachment-delete" do + %i.icon-trash.cred + .clear diff --git a/app/views/notes/_notes.html.haml b/app/views/projects/notes/_notes.html.haml index 4904249aeff..ac8901fe704 100644 --- a/app/views/notes/_notes.html.haml +++ b/app/views/projects/notes/_notes.html.haml @@ -8,4 +8,4 @@ - else - @notes.each do |note| - next unless note.author - = render 'note', note: note + = render note diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml new file mode 100644 index 00000000000..ac28c7432ef --- /dev/null +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -0,0 +1,9 @@ +%ul#notes-list.notes +.js-notes-busy + +.js-main-target-form +- if can? current_user, :write_note, @project + = render "projects/notes/form" + +:javascript + NoteList.init("#{@target_id}", "#{@target_type}", "#{project_notes_path(@project)}"); diff --git a/app/views/notes/create.js.haml b/app/views/projects/notes/create.js.haml index c573d406b73..c113b3482ec 100644 --- a/app/views/notes/create.js.haml +++ b/app/views/projects/notes/create.js.haml @@ -1,21 +1,18 @@ - if @note.valid? - var noteHtml = "#{escape_javascript(render "notes/note", note: @note)}"; + var noteHtml = "#{escape_javascript(render @note)}"; - if note_for_main_target?(@note) - - if @note.for_wall? - NoteList.appendNewWallNote(#{@note.id}, noteHtml); - - else - NoteList.appendNewNote(#{@note.id}, noteHtml); + NoteList.appendNewNote(#{@note.id}, noteHtml); - else :plain - var firstDiscussionNoteHtml = "#{escape_javascript(render "notes/diff_notes_with_reply", notes: [@note])}"; + var firstDiscussionNoteHtml = "#{escape_javascript(render "projects/notes/diff_notes_with_reply", notes: [@note])}"; NoteList.appendNewDiscussionNote("#{@note.discussion_id}", firstDiscussionNoteHtml, noteHtml); - else - var errorsHtml = "#{escape_javascript(render 'notes/form_errors', note: @note)}"; + var errorsHtml = "#{escape_javascript(render 'projects/notes/form_errors', note: @note)}"; - if note_for_main_target?(@note) NoteList.errorsOnForm(errorsHtml); - else - NoteList.errorsOnForm(errorsHtml, "#{@note.discussion_id}");
\ No newline at end of file + NoteList.errorsOnForm(errorsHtml, "#{@note.discussion_id}"); diff --git a/app/views/projects/notes/index.js.haml b/app/views/projects/notes/index.js.haml new file mode 100644 index 00000000000..6c4ed203497 --- /dev/null +++ b/app/views/projects/notes/index.js.haml @@ -0,0 +1,4 @@ +- unless @notes.blank? + var notesHtml = "#{escape_javascript(render 'projects/notes/notes')}"; + - new_note_ids = @notes.map(&:id) + NoteList.setContent(#{new_note_ids}, notesHtml); diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml new file mode 100644 index 00000000000..8930ec4b30a --- /dev/null +++ b/app/views/projects/protected_branches/index.html.haml @@ -0,0 +1,53 @@ += render "projects/commits/head" +.row + .span3 + = render "projects/branches/filter" + .span9 + .alert.alert-info + %p Protected branches designed to prevent push for all except #{link_to "masters", help_permissions_path, class: "vlink"}. + %p This ability allows: + %ul + %li keep stable branches secured + %li forced code review before merge to protected branches + %p Read more about project permissions #{link_to "here", help_permissions_path, class: "underlined_link"} + + - if can? current_user, :admin_project, @project + = form_for [@project, @protected_branch] do |f| + -if @protected_branch.errors.any? + .alert.alert-error + %ul + - @protected_branch.errors.full_messages.each do |msg| + %li= msg + + .entry.clearfix + = f.label :name, "Branch" + .span3 + = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: "Select branch"}, {class: "chosen span3"}) + + = f.submit 'Protect', class: "btn-create btn" + - unless @branches.empty? + %h5 Already Protected: + %ul.bordered-list.protected-branches-list + - @branches.each do |branch| + %li + %h4 + = link_to project_commits_path(@project, branch.name) do + %strong= branch.name + - if @project.root_ref?(branch.name) + %span.label.label-info default + %span.label.label-success + %i.icon-lock + .pull-right + - if can? current_user, :admin_project, @project + = link_to 'Unprotect', [@project, branch], confirm: 'Branch will be writable for developers. Are you sure?', method: :delete, class: "btn btn-remove btn-small" + + - if commit = branch.commit + = link_to project_commit_path(@project, commit.id), class: 'commit_short_id' do + = commit.short_id + %span.light + = gfm escape_once(truncate(commit.title, length: 40)) + %span + = time_ago_in_words(commit.committed_date) + ago + - else + (branch was removed from repository) diff --git a/app/views/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml index 23a6dae7810..213c54f5f75 100644 --- a/app/views/refs/logs_tree.js.haml +++ b/app/views/projects/refs/logs_tree.js.haml @@ -1,8 +1,9 @@ -- @logs.each do |content_data| +- @logs.each do |content_data| - file_name = content_data[:file_name] - commit = content_data[:commit] + - next unless commit :plain var row = $("table.table_#{@hex_path} tr.file_#{hexdigest(file_name)}"); row.find("td.tree_time_ago").html('#{escape_javascript time_ago_in_words(commit.committed_date)} ago'); - row.find("td.tree_commit").html('#{escape_javascript render("tree/tree_commit_column", commit: commit)}'); + row.find("td.tree_commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}'); diff --git a/app/views/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml index eaf15ca77d6..faa3ed1746c 100644 --- a/app/views/repositories/_feed.html.haml +++ b/app/views/projects/repositories/_feed.html.haml @@ -1,5 +1,4 @@ - commit = update -- commit = CommitDecorator.new(commit) %tr %td = link_to project_commits_path(@project, commit.head.name) do @@ -12,7 +11,7 @@ %div = link_to project_commits_path(@project, commit.id) do %code= commit.short_id - = image_tag gravatar_icon(commit.author_email), class: "", width: 16 + = image_tag gravatar_icon(commit.author_email), class: "", width: 16, alt: '' = gfm escape_once(truncate(commit.title, length: 40)) %td %span.pull-right.cgray diff --git a/app/views/repositories/stats.html.haml b/app/views/projects/repositories/stats.html.haml index dde35ea38aa..454296e82fd 100644 --- a/app/views/repositories/stats.html.haml +++ b/app/views/projects/repositories/stats.html.haml @@ -1,8 +1,8 @@ -= render "commits/head" += render "projects/commits/head" .row - .span5 - %h4 - Stats: + .span6 + %div#activity-chart.chart + %hr %p %b Total commits: %span= @stats.commits_count @@ -13,14 +13,13 @@ %b Authors: %span= @stats.authors_count - %br - %div#activity-chart - .span7 + + .span6 %h4 Top 50 Committers: %ol.styled - @stats.authors[0...50].each do |author| %li - = image_tag gravatar_icon(author.email, 16), class: 'avatar s16' + = image_tag gravatar_icon(author.email, 16), class: 'avatar s16', alt: '' = author.name %small.light= author.email .pull-right @@ -28,14 +27,7 @@ :javascript - $(function(){ - var labels = [#{@graph.labels.to_json}]; - var commits = [#{@graph.commits.join(', ')}]; - var r = Raphael('activity-chart'); - r.text(160, 10, "Commit activity for last #{@graph.weeks} weeks").attr({ font: "13px sans-serif" }); - r.barchart( - 10, 10, 400, 160, - [commits], - {colors:["#456"]} - ).label(labels, true); - }) + var labels = [#{@graph.labels.to_json}]; + var commits = [#{@graph.commits.join(', ')}]; + var title = "Commit activity for last #{@graph.weeks} weeks"; + Chart.init(labels, commits, title); diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml new file mode 100644 index 00000000000..a099193cb6e --- /dev/null +++ b/app/views/projects/services/_form.html.haml @@ -0,0 +1,48 @@ +%h3.page-title + - if @service.activated? + %span.cgreen + %i.icon-circle + - else + %span.cgray + %i.icon-circle-blank + = @service.title + +%p= @service.description + +.back-link + = link_to project_services_path(@project) do + ← to services + +%hr + += form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put) do |f| + - if @service.errors.any? + .alert.alert-error + %ul + - @service.errors.full_messages.each do |msg| + %li= msg + + + .control-group + = f.label :active, "Active", class: "control-label" + .controls + = f.check_box :active + + - @service.fields.each do |field| + - name = field[:name] + - type = field[:type] + - placeholder = field[:placeholder] + + .control-group + = f.label name, class: "control-label" + .controls + - if type == 'text' + = f.text_field name, class: "input-xlarge", placeholder: placeholder + - elsif type == 'checkbox' + = f.check_box name + + .form-actions + = f.submit 'Save', class: 'btn btn-save' + + - if @service.valid? && @service.activated? && @service.can_test? + = link_to 'Test settings', test_project_service_path(@project, @service.to_param), class: 'btn btn-small' diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml new file mode 100644 index 00000000000..bcc5832792f --- /dev/null +++ b/app/views/projects/services/edit.html.haml @@ -0,0 +1 @@ += render 'form' diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml new file mode 100644 index 00000000000..82b85a18acd --- /dev/null +++ b/app/views/projects/services/index.html.haml @@ -0,0 +1,17 @@ +%h3.page-title Services +%p.light Services allow you to integrate GitLab with other applications +%hr + +%ul.bordered-list + - @services.each do |service| + %li + %h4 + - if service.activated? + %span.cgreen + %i.icon-circle + - else + %span.cgray + %i.icon-circle-blank + = link_to edit_project_service_path(@project, service.to_param) do + = service.title + %p= service.description diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 2c4f55eb646..06ca5169dff 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,8 +1,47 @@ -= render "project_head" = render 'clone_panel' -= render "events/event_last_push", event: @last_push -.content_list= render @events -.loading.hide -:javascript - $(function(){ Pager.init(20); }); +.row + .span9 + = render "events/event_last_push", event: @last_push + = render 'shared/event_filter' + .content_list + .loading.hide + .span3 + .light-well + %h3.page-title + = @project.name + - if @project.description.present? + %p.light= @project.description + + %hr + %p + %p + %span.light Repo size is + #{@project.repository.size} MB + %p + %span.light Created at + #{@project.created_at.stamp('Aug 22, 2013')} + %p + %span.light Owned by + - if @project.group + #{link_to @project.group.name, @project.group} Group + - else + #{link_to @project.owner_name, @project.owner} + - if @project.forked_from_project + %p + %i.icon-code-fork + Forked from: + = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) + + %hr + %p + = link_to pluralize(@repository.round_commit_count, 'commit'), project_commits_path(@project, @ref || @repository.root_ref) + %p + = link_to pluralize(@repository.branch_names.count, 'branch'), project_branches_path(@project) + %p + = link_to pluralize(@repository.tag_names.count, 'tag'), project_tags_path(@project) + + - 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" diff --git a/app/views/projects/snippets/_blob.html.haml b/app/views/projects/snippets/_blob.html.haml new file mode 100644 index 00000000000..f14a2bd4ec0 --- /dev/null +++ b/app/views/projects/snippets/_blob.html.haml @@ -0,0 +1,15 @@ +.file-holder + .file-title + %i.icon-file + %strong= @snippet.file_name + %span.options + .btn-group.tree-btn-group.pull-right + - if can?(current_user, :admin_project_snippet, @project) || @snippet.author == current_user + = link_to "Edit", edit_project_snippet_path(@project, @snippet), class: "btn btn-tiny", title: 'Edit Snippet' + = link_to "raw", raw_project_snippet_path(@project, @snippet), class: "btn btn-tiny", target: "_blank" + .file-content.code + - unless @snippet.content.empty? + %div{class: user_color_scheme_class} + = raw @snippet.colorize(formatter: :gitlab) + - else + %p.nothing_here_message Empty file diff --git a/app/views/projects/snippets/_form.html.haml b/app/views/projects/snippets/_form.html.haml new file mode 100644 index 00000000000..d414ee2d1ec --- /dev/null +++ b/app/views/projects/snippets/_form.html.haml @@ -0,0 +1,44 @@ +%h3.page-title + = @snippet.new_record? ? "New Snippet" : "Edit Snippet ##{@snippet.id}" +%hr +.snippet-form-holder + = form_for [@project, @snippet], as: :project_snippet, url: url do |f| + -if @snippet.errors.any? + .alert.alert-error + %ul + - @snippet.errors.full_messages.each do |msg| + %li= msg + + .control-group + = f.label :title + .controls= f.text_field :title, placeholder: "Example Snippet", class: 'input-xlarge', required: true + .control-group + = f.label "Lifetime" + .controls= f.select :expires_at, lifetime_select_options, {}, {class: 'chosen span2'} + .control-group + .file-editor + = f.label :file_name, "File" + .controls + .file-holder.snippet + .file-title + = f.text_field :file_name, placeholder: "example.rb", class: 'snippet-file-name', required: true + .file-content.code + %pre#editor= @snippet.content + = f.hidden_field :content, class: 'snippet-file-content' + + .form-actions + - if @snippet.new_record? + = f.submit 'Create snippet', class: "btn-create btn" + - else + = f.submit 'Save', class: "btn-save btn" + = link_to "Cancel", project_snippets_path(@project), class: "btn btn-cancel" + + - unless @snippet.new_record? + = link_to 'Remove snippet', project_snippet_path(@project, @snippet), confirm: 'Are you sure?', method: :delete, class: "btn pull-right btn-remove delete-snippet prepend-left-10", id: "destroy_snippet_#{@snippet.id}" + +:javascript + var editor = ace.edit("editor"); + $(".snippet-form-holder form").submit(function(){ + $(".snippet-file-content").val(editor.getValue()); + }); + diff --git a/app/views/projects/snippets/_snippet.html.haml b/app/views/projects/snippets/_snippet.html.haml new file mode 100644 index 00000000000..fc1c0893b08 --- /dev/null +++ b/app/views/projects/snippets/_snippet.html.haml @@ -0,0 +1,21 @@ +%li + %h4.snippet-title + = link_to reliable_snippet_path(snippet) do + = truncate(snippet.title, length: 60) + %span.cgray.monospace.tiny.pull-right + = snippet.file_name + + %small.pull-right.cgray + Expires: + - if snippet.expires_at + = snippet.expires_at.to_date.to_s(:short) + - else + Never + + .snippet-info + = "##{snippet.id}" + %span + by + = image_tag gravatar_icon(snippet.author_email), class: "avatar avatar-inline s16" + = snippet.author_name + %span.light #{time_ago_in_words(snippet.created_at)} ago diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml new file mode 100644 index 00000000000..e28b7d4937e --- /dev/null +++ b/app/views/projects/snippets/edit.html.haml @@ -0,0 +1 @@ += render "projects/snippets/form", url: project_snippet_path(@project, @snippet) diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml new file mode 100644 index 00000000000..c40f63d05b3 --- /dev/null +++ b/app/views/projects/snippets/index.html.haml @@ -0,0 +1,15 @@ +%h3.page-title + Snippets + - if can? current_user, :write_project_snippet, @project + = link_to new_project_snippet_path(@project), class: "btn btn-new pull-right", title: "New Snippet" do + Add new snippet + +%p.light + Share code pastes with others out of git repository + +%hr +%ul.bordered-list + = render partial: "projects/snippets/snippet", collection: @snippets + - if @snippets.empty? + %li + %h3.nothing_here_message Nothing here. diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml new file mode 100644 index 00000000000..460af34f676 --- /dev/null +++ b/app/views/projects/snippets/new.html.haml @@ -0,0 +1 @@ += render "projects/snippets/form", url: project_snippets_path(@project, @snippet) diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml new file mode 100644 index 00000000000..4a07ebf7fd9 --- /dev/null +++ b/app/views/projects/snippets/show.html.haml @@ -0,0 +1,11 @@ +%h3.page-title + = @snippet.title + + %small.pull-right + = "##{@snippet.id}" + %span.light + by + = image_tag gravatar_icon(@snippet.author_email), class: "avatar avatar-inline s16" + = @snippet.author_name +%div= render 'projects/snippets/blob' +%div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml new file mode 100644 index 00000000000..5361517b2fc --- /dev/null +++ b/app/views/projects/tags/index.html.haml @@ -0,0 +1,52 @@ += render "projects/commits/head" + +- if can? current_user, :push_code, @project + .pull-right + = link_to new_project_tag_path(@project), class: 'btn btn-create new-tag-btn' do + %i.icon-add-sign + New tag + +%p + Tags give ability to mark specific points in history as being important +%hr + +- unless @tags.empty? + %ul.bordered-list + - @tags.each do |tag| + - commit = Commit.new(Gitlab::Git::Commit.new(tag.commit)) + %li + %h4 + = link_to project_commits_path(@project, tag.name), class: "" do + %i.icon-tag + = tag.name + %small + = truncate(tag.message || '', length: 70) + .pull-right + %small.cdark + %i.icon-calendar + = time_ago_in_words(commit.committed_date) + ago + %p.prepend-left-20 + = link_to commit.short_id(8), project_commit_path(@project, commit), class: "monospace" + – + = link_to_gfm truncate(commit.title, length: 70), project_commit_path(@project, commit.id), class: "cdark" + + %span.pull-right + - if can? current_user, :download_code, @project + = link_to archive_project_repository_path(@project, ref: tag.name), class: 'btn grouped btn-small' do + %i.icon-download-alt + Download + - if can?(current_user, :admin_project, @project) + = link_to project_tag_path(@project, tag.name), class: 'btn btn-small remove-row', method: :delete, confirm: 'Removed tag cannot be restored. Are you sure?', remote: true do + %i.icon-trash + + = paginate @tags, theme: 'gitlab' + +- else + %h3.nothing_here_message + Repository has no tags yet. + %br + %small + Use git tag command to add a new one: + %br + %span.monospace git tag -a v1.4 -m 'version 1.4' diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml new file mode 100644 index 00000000000..67030785e06 --- /dev/null +++ b/app/views/projects/tags/new.html.haml @@ -0,0 +1,24 @@ +%h3.page-title + %i.icon-code-fork + New tag += form_tag project_tags_path, method: :post do + .control-group + = label_tag :tag_name, 'Name for new tag', class: 'control-label' + .controls + = text_field_tag :tag_name, nil, placeholder: 'v3.0.1', required: true, tabindex: 1 + .control-group + = label_tag :ref, 'Create from', class: 'control-label' + .controls + = text_field_tag :ref, nil, placeholder: 'master', required: true, tabindex: 2 + .light Branch name or commit SHA + .form-actions + = submit_tag 'Create tag', class: 'btn btn-create', tabindex: 3 + = link_to 'Cancel', project_tags_path(@project), class: 'btn btn-cancel' + +:javascript + var availableTags = #{@project.repository.ref_names.to_json}; + + $("#ref").autocomplete({ + source: availableTags, + minLength: 1 + }); diff --git a/app/views/projects/team_members/_form.html.haml b/app/views/projects/team_members/_form.html.haml new file mode 100644 index 00000000000..5214a54e909 --- /dev/null +++ b/app/views/projects/team_members/_form.html.haml @@ -0,0 +1,24 @@ +%h3.page-title + = "New project member(s)" + += form_for @user_project_relation, as: :team_member, url: project_team_members_path(@project) do |f| + -if @user_project_relation.errors.any? + .alert.alert-error + %ul + - @user_project_relation.errors.full_messages.each do |msg| + %li= msg + + %p 1. Choose people you want in the project + .control-group + = f.label :user_ids, "People" + .controls + = users_select_tag(:user_ids, multiple: true) + + %p 2. Set access level for them + .control-group + = f.label :project_access, "Project Access" + .controls= select_tag :project_access, options_for_select(Gitlab::Access.options, @user_project_relation.project_access), class: "project-access-select chosen" + + .form-actions + = f.submit 'Add users', class: "btn btn-create" + = link_to "Cancel", project_team_index_path(@project), class: "btn btn-cancel" diff --git a/app/views/projects/team_members/_group_members.html.haml b/app/views/projects/team_members/_group_members.html.haml new file mode 100644 index 00000000000..68f08006854 --- /dev/null +++ b/app/views/projects/team_members/_group_members.html.haml @@ -0,0 +1,10 @@ +.ui-box + .title + %strong #{@group.name} + group members (#{@group.users_groups.count}) + .pull-right + = link_to members_group_path(@group), class: 'btn btn-small' do + %i.icon-edit + %ul.well-list + - @group.users_groups.order('group_access DESC').each do |member| + = render 'users_groups/users_group', member: member, show_controls: false diff --git a/app/views/projects/team_members/_team.html.haml b/app/views/projects/team_members/_team.html.haml new file mode 100644 index 00000000000..2daf6847665 --- /dev/null +++ b/app/views/projects/team_members/_team.html.haml @@ -0,0 +1,9 @@ +.team-table + - can_admin_project = (can? current_user, :admin_project, @project) + .ui-box + .title + %strong #{@project.name} + project members (#{members.count}) + %ul.well-list + - members.each do |team_member| + = render 'team_member', member: team_member, current_user_can_admin_project: can_admin_project diff --git a/app/views/projects/team_members/_team_member.html.haml b/app/views/projects/team_members/_team_member.html.haml new file mode 100644 index 00000000000..916cf2e7a87 --- /dev/null +++ b/app/views/projects/team_members/_team_member.html.haml @@ -0,0 +1,17 @@ +- user = member.user +%li{id: dom_id(user), class: "team_member_row access-#{member.human_access.downcase}"} + .pull-right + - if current_user_can_admin_project + - unless @project.personal? && user == current_user + .pull-left + = form_for(member, as: :team_member, url: project_team_member_path(@project, member.user)) do |f| + = f.select :project_access, options_for_select(UsersProject.access_roles, member.project_access), {}, class: "medium project-access-select span2 trigger-submit" + + = link_to project_team_member_path(@project, user), confirm: remove_from_project_team_message(@project, user), method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from team' do + %i.icon-minus.icon-white + = image_tag gravatar_icon(user.email, 32), class: "avatar s32" + %p + %strong= user.name + %span.cgray= user.username + + diff --git a/app/views/projects/team_members/import.html.haml b/app/views/projects/team_members/import.html.haml new file mode 100644 index 00000000000..1d98b986210 --- /dev/null +++ b/app/views/projects/team_members/import.html.haml @@ -0,0 +1,14 @@ +%h3.page-title + = "Import members from another project" +%p.light + Only project members will be imported. Group members will be skipped. +%hr += form_tag apply_import_project_team_members_path(@project), method: 'post' do + .padded + = label_tag :source_project_id, "Project" + .controls= select_tag(:source_project_id, options_from_collection_for_select(current_user.authorized_projects, :id, :name_with_namespace), prompt: "Select project", class: "chosen xxlarge", required: true) + + .form-actions + = submit_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/team_members/index.html.haml b/app/views/projects/team_members/index.html.haml new file mode 100644 index 00000000000..acbe82919f1 --- /dev/null +++ b/app/views/projects/team_members/index.html.haml @@ -0,0 +1,16 @@ +%h3.page-title + Users with access to this project + + - if can? current_user, :admin_team_member, @project + %span.pull-right + = link_to new_project_team_member_path(@project), class: "btn btn-new grouped", title: "New project member" do + New project member + = link_to import_project_team_members_path(@project), class: "btn grouped", title: "Import members from another project" do + Import members + +%p.light + Read more about project permissions + %strong= link_to "here", help_permissions_path, class: "vlink" += render "team", members: @users_projects +- if @group + = render "group_members" diff --git a/app/views/projects/team_members/new.html.haml b/app/views/projects/team_members/new.html.haml new file mode 100644 index 00000000000..b1bc3ba0eba --- /dev/null +++ b/app/views/projects/team_members/new.html.haml @@ -0,0 +1 @@ += render "form" diff --git a/app/views/team_members/update.js.haml b/app/views/projects/team_members/update.js.haml index c68fe9574a2..c68fe9574a2 100644 --- a/app/views/team_members/update.js.haml +++ b/app/views/projects/team_members/update.js.haml diff --git a/app/views/projects/teams/available.html.haml b/app/views/projects/teams/available.html.haml deleted file mode 100644 index da7823638b9..00000000000 --- a/app/views/projects/teams/available.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -= render "projects/project_head" - -%h3.page_title - = "Assign project to team of users" -%hr -%p.slead - Read more about assign to team of users #{link_to "here", '#', class: 'vlink'}. -= form_tag assign_project_teams_path(@project), method: 'post' do - %p.slead Choose Team of users you want to assign: - .padded - = label_tag :team_id, "Team" - .input= select_tag(:team_id, options_from_collection_for_select(@teams, :id, :name), prompt: "Select team", class: "chosen xxlarge", required: true) - %p.slead Choose greatest user acces in team you want to assign: - .padded - = label_tag :team_ids, "Permission" - .input= select_tag :greatest_project_access, options_for_select(UserTeam.access_roles), {class: "project-access-select chosen span3" } - - - .actions - = submit_tag 'Assign', 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 new file mode 100644 index 00000000000..10b0de98c04 --- /dev/null +++ b/app/views/projects/transfer.js.haml @@ -0,0 +1,7 @@ +- 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 + location.href = "#{edit_project_path(@project)}"; diff --git a/app/views/projects/tree.js.haml b/app/views/projects/tree.js.haml deleted file mode 100644 index ba5d53c16a2..00000000000 --- a/app/views/projects/tree.js.haml +++ /dev/null @@ -1,5 +0,0 @@ -:plain - $("#tree-holder table").hide("slide", { direction: "left" }, 150, function(){ - $("#tree-holder").html("#{escape_javascript(render(partial: "tree", locals: {repo: @repo, commit: @commit, tree: @tree}))}"); - $("#tree-holder table").show("slide", { direction: "right" }, 150); - }); diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml new file mode 100644 index 00000000000..b179ad7d245 --- /dev/null +++ b/app/views/projects/tree/_blob_item.html.haml @@ -0,0 +1,9 @@ +%tr{ class: "tree-item #{tree_hex_class(blob_item)}" } + %td.tree-item-file-name + = tree_icon(type) + %span= link_to truncate(blob_item.name, length: 40), project_blob_path(@project, tree_join(@id || @commit.id, blob_item.name)) + %td.tree_time_ago.cgray + %span.log_loading.hide + Loading commit data... + = image_tag "ajax_loader_tree.gif", width: 14 + %td.tree_commit{ colspan: 2 } diff --git a/app/views/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml index e9bb112745b..98bacb49562 100644 --- a/app/views/tree/_readme.html.haml +++ b/app/views/projects/tree/_readme.html.haml @@ -1,8 +1,8 @@ -.file_holder#README - .file_title +.file-holder#README + .file-title %i.icon-file = readme.name - .file_content.wiki + .file-content.wiki - if gitlab_markdown?(readme.name) = preserve do = markdown(readme.data) diff --git a/app/views/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml index 092a024afbc..26aad16e2c0 100644 --- a/app/views/tree/_submodule_item.html.haml +++ b/app/views/projects/tree/_submodule_item.html.haml @@ -4,7 +4,7 @@ %tr{ class: "tree-item", url: url } %td.tree-item-file-name = image_tag "submodule.png" - %strong= truncate(name, length: 40) + %span= truncate(name, length: 40) %td %code= submodule_item.id[0..10] %td{ colspan: 2 } diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml new file mode 100644 index 00000000000..ae5f30c0004 --- /dev/null +++ b/app/views/projects/tree/_tree.html.haml @@ -0,0 +1,51 @@ +%ul.breadcrumb + %li + %i.icon-angle-right + = link_to project_tree_path(@project, @ref) do + = @project.path + - tree_breadcrumbs(tree, 6) do |title, path| + \/ + %li + - if path + = link_to truncate(title, length: 40), project_tree_path(@project, path) + - else + = link_to title, '#' + +%div#tree-content-holder.tree-content-holder + %table#tree-slider{class: "table_#{@hex_path} tree-table" } + %thead + %tr + %th Name + %th Last Update + %th + Last Commit + + %i.icon-angle-right + + %small.light + = link_to @commit.short_id, project_commit_path(@project, @commit) + – + = truncate(@commit.title, length: 50) + %th= link_to "history", project_commits_path(@project, @id), class: "pull-right" + + - if tree.up_dir? + %tr.tree-item + %td.tree-item-file-name + = image_tag "file_empty.png", size: '16x16' + = link_to "..", project_tree_path(@project, up_dir_path(tree)) + %td + %td + %td + + = render_tree(tree) + + - if tree.readme + = render "projects/tree/readme", readme: tree.readme + +%div.tree_progress + +:javascript + // Load last commit log for each file in tree + $('#tree-slider').waitForImages(function() { + ajaxGet('#{@logs_path}'); + }); diff --git a/app/views/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml index 9d02132b0f4..7ae2582c130 100644 --- a/app/views/tree/_tree_commit_column.html.haml +++ b/app/views/projects/tree/_tree_commit_column.html.haml @@ -1,2 +1,2 @@ -%span.tree_author= commit.author_link avatar: true +%span.tree_author= commit_author_link(commit, avatar: true) = link_to_gfm truncate(commit.title, length: 80), project_commit_path(@project, commit.id), class: "tree-commit-link" diff --git a/app/views/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml index 0a76d5c21b6..f8856afc866 100644 --- a/app/views/tree/_tree_item.html.haml +++ b/app/views/projects/tree/_tree_item.html.haml @@ -1,7 +1,7 @@ %tr{ class: "tree-item #{tree_hex_class(tree_item)}" } %td.tree-item-file-name = tree_icon(type) - %strong= link_to truncate(tree_item.name, length: 40), project_tree_path(@project, tree_join(@id || @commit.id, tree_item.name)) + %span= link_to truncate(tree_item.name, length: 40), project_tree_path(@project, tree_join(@id || @commit.id, tree_item.name)) %td.tree_time_ago.cgray %span.log_loading.hide Loading commit data... diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml new file mode 100644 index 00000000000..0f7692aba7f --- /dev/null +++ b/app/views/projects/tree/show.html.haml @@ -0,0 +1,4 @@ +%div.tree-ref-holder + = render 'shared/ref_switcher', destination: 'tree', path: @path +%div#tree-holder.tree-holder + = render "tree", tree: @tree diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml index f44ed529182..cbb21f2b9fb 100644 --- a/app/views/projects/update.js.haml +++ b/app/views/projects/update.js.haml @@ -3,6 +3,7 @@ location.href = "#{edit_project_path(@project)}"; - else :plain - $('.project_edit_holder').show(); - $(".edit_project").replaceWith("#{escape_javascript(render('form'))}"); + $(".project-edit-errors").html("#{escape_javascript(render('errors'))}"); $('.save-project-loader').hide(); + $('.project-edit-container').show(); + $('.project-edit-content .btn-save').enableButton(); diff --git a/app/views/projects/update_failed.js.haml b/app/views/projects/update_failed.js.haml deleted file mode 100644 index a3ac5f4088f..00000000000 --- a/app/views/projects/update_failed.js.haml +++ /dev/null @@ -1,2 +0,0 @@ -:plain - $(".save-project-loader").replaceWith(errorMessage('#{escape_javascript(@error.message)}')); diff --git a/app/views/projects/wall.html.haml b/app/views/projects/wall.html.haml deleted file mode 100644 index 82b565def43..00000000000 --- a/app/views/projects/wall.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -%div.wall_page - = render "notes/reversed_notes_with_form" diff --git a/app/views/projects/walls/show.html.haml b/app/views/projects/walls/show.html.haml new file mode 100644 index 00000000000..88aecee0815 --- /dev/null +++ b/app/views/projects/walls/show.html.haml @@ -0,0 +1,23 @@ +%div.wall-page + %ul.notes + + - if can? current_user, :write_note, @project + .note-form-holder + = form_for [@project, @note], remote: true, html: { multipart: true, id: nil, class: "new_note wall-note-form" } do |f| + = note_target_fields + .note_text_and_preview + = f.text_area :note, size: 255, class: 'note_text js-note-text js-gfm-input turn-on' + .note-form-actions + .buttons + = f.submit 'Add Comment', class: "btn comment-btn grouped js-comment-button" + + .note-form-option + %a.choose-btn.btn.btn-small.js-choose-note-attachment-button + %i.icon-paper-clip + %span Choose File ... + + %span.file_name.js-attachment-filename File name... + = f.file_field :attachment, class: "js-note-attachment-input hide" + + .hint.pull-right CTRL + Enter to send message + .clearfix diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml new file mode 100644 index 00000000000..16061c9dcbb --- /dev/null +++ b/app/views/projects/wikis/_form.html.haml @@ -0,0 +1,39 @@ += form_for [@project, @wiki] do |f| + -if @wiki.errors.any? + #error_explanation + %h2= "#{pluralize(@wiki.errors.count, "error")} prohibited this wiki from being saved:" + %ul + - @wiki.errors.full_messages.each do |msg| + %li= msg + + .ui-box.ui-box-show + .ui-box-head + %h3.page-title + .edit-wiki-header + = @wiki.title.titleize + = f.hidden_field :title, value: @wiki.title + = f.select :format, options_for_select(GollumWiki::MARKUPS, {selected: @wiki.format}), {}, class: "pull-right input-medium" + = f.label :format, class: "pull-right", style: "padding-right: 20px;" + .ui-box-body + .controls + %span.cgray + Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + To link to a (new) page you can just type + %code [Link Title](page-slug) + \. + + .ui-box-bottom + .control-group + = f.label :content + .controls= f.text_area :content, class: 'span8 js-gfm-input' + .ui-box-bottom + .control-group + = f.label :commit_message + .controls= f.text_field :message, class: 'span8' + .form-actions + - if @wiki && @wiki.persisted? + = f.submit 'Save changes', class: "btn-save btn" + = link_to "Cancel", project_wiki_path(@project, @wiki), class: "btn btn-cancel" + - else + = f.submit 'Create page', class: "btn-create btn" + = link_to "Cancel", project_wiki_path(@project, :home), class: "btn btn-cancel" diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml new file mode 100644 index 00000000000..1001bb10c4f --- /dev/null +++ b/app/views/projects/wikis/_main_links.html.haml @@ -0,0 +1,8 @@ +%span.pull-right + - if (@wiki && @wiki.persisted?) + = link_to history_project_wiki_path(@project, @wiki), class: "btn grouped" do + Page History + - if can?(current_user, :write_wiki, @project) + = link_to edit_project_wiki_path(@project, @wiki), class: "btn grouped" do + %i.icon-edit + Edit diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml new file mode 100644 index 00000000000..0a7e51e974c --- /dev/null +++ b/app/views/projects/wikis/_nav.html.haml @@ -0,0 +1,19 @@ +%ul.nav.nav-tabs + = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do + = link_to 'Home', project_wiki_path(@project, :home) + + = nav_link(path: 'wikis#pages') do + = link_to 'Pages', pages_project_wikis_path(@project) + + = nav_link(path: 'wikis#git_access') do + = link_to git_access_project_wikis_path(@project) do + %i.icon-download-alt + Git Access + + - if can?(current_user, :write_wiki, @project) + .pull-right + = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do + %i.icon-plus + New Page + += render 'projects/wikis/new' diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml new file mode 100644 index 00000000000..f64772b2001 --- /dev/null +++ b/app/views/projects/wikis/_new.html.haml @@ -0,0 +1,12 @@ +%div#modal-new-wiki.modal.hide + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h3.page-title New Wiki Page + .modal-body + = label_tag :new_wiki_path do + %span Page slug + = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'input-xlarge', required: true, :'data-wikis-path' => project_wikis_path(@project) + %p.hint + Please don't use spaces and slashes + .modal-footer + = link_to 'Build', '#', class: 'build-new-wiki btn btn-create' diff --git a/app/views/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 9e221aba47d..66be82777c9 100644 --- a/app/views/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,8 +1,10 @@ -%h3.page_title Editing page -%hr += render 'nav' +%h3.page-title + Editing page + = render 'main_links' = render 'form' .pull-right - - if can? current_user, :admin_wiki, @project + - if @wiki.persisted? && can?(current_user, :admin_wiki, @project) = link_to project_wiki_path(@project, @wiki), confirm: "Are you sure you want to delete this page?", method: :delete, class: "btn btn-small btn-remove" do - Delete this page
\ No newline at end of file + Delete this page diff --git a/app/views/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml index 08b59f0328b..48058124f97 100644 --- a/app/views/wikis/empty.html.haml +++ b/app/views/projects/wikis/empty.html.haml @@ -1,4 +1,4 @@ -%h3.page_title Empty page +%h3.page-title Empty page %hr .error_message You are not allowed to create wiki pages diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml new file mode 100644 index 00000000000..dd01bb99041 --- /dev/null +++ b/app/views/projects/wikis/git_access.html.haml @@ -0,0 +1,36 @@ += render 'nav' +%h3.page-title + Git access for + %strong= @gollum_wiki.path_with_namespace + = render 'main_links' + +.content + .project_clone_panel + .row + .span7 + .form-horizontal + .input-prepend.project_clone_holder + %button{class: "btn active", :"data-clone" => @gollum_wiki.ssh_url_to_repo} SSH + %button{class: "btn", :"data-clone" => @gollum_wiki.http_url_to_repo}= Gitlab.config.gitlab.protocol.upcase + = text_field_tag :project_clone, @gollum_wiki.url_to_repo, class: "one_click_select input-xxlarge", readonly: true + .git-empty + %fieldset + %legend Install Gollum: + %pre.dark + :preserve + gem install gollum + + %legend Clone Your Wiki: + %pre.dark + :preserve + git clone #{@gollum_wiki.ssh_url_to_repo} + cd #{@gollum_wiki.path} + + %legend Start Gollum And Edit Locally: + %pre.dark + :preserve + gollum + == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin + >> Thin web server (v1.5.0 codename Knife) + >> Maximum connections set to 1024 + >> Listening on 0.0.0.0:4567, CTRL+C to stop diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml new file mode 100644 index 00000000000..344a5e88e6e --- /dev/null +++ b/app/views/projects/wikis/history.html.haml @@ -0,0 +1,30 @@ += render 'nav' +%h3.page-title + %span.light History for + = link_to @wiki.title.titleize, project_wiki_path(@project, @wiki) + +%table + %thead + %tr + %th Page version + %th Author + %th Commit Message + %th Last updated + %th Format + %tbody + - @wiki.versions.each do |version| + - commit = version + %tr + %td + = link_to project_wiki_path(@project, @wiki, version_id: commit.id) do + = commit.short_id + %td + = commit_author_link(commit, avatar: true, size: 24) + %td + = commit.title + %td + = time_ago_in_words(version.date) + ago + %td + %strong + = @wiki.page.wiki.page(@wiki.page.name, commit.id).try(:format) diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml new file mode 100644 index 00000000000..59d104a985b --- /dev/null +++ b/app/views/projects/wikis/pages.html.haml @@ -0,0 +1,11 @@ += render 'nav' +%h3.page-title + All Pages +%ul.bordered-list + - @wiki_pages.each do |wiki_page| + %li + %h4 + = link_to wiki_page.title.titleize, project_wiki_path(@project, wiki_page) + %small (#{wiki_page.format}) + .pull-right + %small Last edited #{time_ago_in_words(wiki_page.commit.created_at)} ago diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml new file mode 100644 index 00000000000..63e7f12b7fe --- /dev/null +++ b/app/views/projects/wikis/show.html.haml @@ -0,0 +1,15 @@ += render 'nav' +%h3.page-title + = @wiki.title.titleize + = render 'main_links' +- if @wiki.historical? + .warning_message + This is an old version of this page. + You can view the #{link_to "most recent version", project_wiki_path(@project, @wiki)} or browse the #{link_to "history", history_project_wiki_path(@project, @wiki)}. + +.file-holder + .file-content.wiki + = preserve do + = render_wiki_content(@wiki) + +%p.time Last edited by #{commit_author_link(@wiki.commit, avatar: true, size: 16)} #{time_ago_in_words @wiki.commit.created_at} ago diff --git a/app/views/protected_branches/index.html.haml b/app/views/protected_branches/index.html.haml deleted file mode 100644 index 15644de552f..00000000000 --- a/app/views/protected_branches/index.html.haml +++ /dev/null @@ -1,54 +0,0 @@ -= render "commits/head" -.row - .span3 - = render "repositories/filter" - .span9 - .alert - %p Protected branches designed to prevent push for all except #{link_to "masters", help_permissions_path, class: "vlink"}. - %p This ability allows: - %ul - %li keep stable branches secured - %li forced code review before merge to protected branches - %p Read more about project permissions #{link_to "here", help_permissions_path, class: "vlink"} - - - if can? current_user, :admin_project, @project - = form_for [@project, @protected_branch] do |f| - -if @protected_branch.errors.any? - .alert.alert-error - %ul - - @protected_branch.errors.full_messages.each do |msg| - %li= msg - - .entry.clearfix - = f.label :name, "Branch" - .span3 - = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: "Select branch"}, {class: "chosen span3"}) - - = f.submit 'Protect', class: "btn-primary btn" - - - unless @branches.empty? - %table - %thead - %tr - %th Name - %th Last commit - %th - %tbody - - @branches.each do |branch| - %tr - %td - = link_to project_commits_path(@project, branch.name) do - %strong= branch.name - - if @project.root_ref?(branch.name) - %span.label default - %td - - if branch.commit - = link_to project_commit_path(@project, branch.commit.id) do - = truncate branch.commit.id.to_s, length: 10 - = time_ago_in_words(branch.commit.committed_date) - ago - - else - (branch was removed from repository) - %td - - if can? current_user, :admin_project, @project - = link_to 'Unprotect', [@project, branch], confirm: 'Are you sure?', method: :delete, class: "btn btn-remove btn-small" diff --git a/app/views/public/projects/index.html.haml b/app/views/public/projects/index.html.haml index 21e9d2e6029..21aee644579 100644 --- a/app/views/public/projects/index.html.haml +++ b/app/views/public/projects/index.html.haml @@ -1,16 +1,42 @@ -%h3.page_title - Projects - %small with read-only access +.row + .span6 + %h3.page-title + Projects (#{@projects.total_count}) + .light + You can browse public projects in read-only mode until signed in. + + .span6 + .pull-right + = form_tag public_projects_path, method: :get, class: 'form-inline' do |f| + .search-holder + = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "span3 search-text-input", id: "projects_search" + = submit_tag 'Search', class: "btn btn-primary wide" %hr +.public-projects + %ul.bordered-list.top-list + - @projects.each do |project| + %li + %h4 + = link_to project_path(project) do + = project.name_with_namespace + .pull-right + %pre.public-clone git clone #{project.http_url_to_repo} -%ul.unstyled - - @projects.each do |project| - %li.clearfix - %h5 - %i.icon-share - = project.name_with_namespace - .pull-right - %pre.dark.tiny git clone #{project.http_url_to_repo} + - if project.description.present? + %p + = project.description + .repo-info + - unless project.empty_repo? + = link_to pluralize(project.repository.round_commit_count, 'commit'), project_commits_path(project, project.default_branch) + · + = link_to pluralize(project.repository.branch_names.count, 'branch'), project_branches_path(project) + · + = link_to pluralize(project.repository.tag_names.count, 'tag'), project_tags_path(project) + - else + %i.icon-warning-sign + Empty repository + - unless @projects.present? + %h3.nothing_here_message No public projects -= paginate @projects, theme: "admin" + = paginate @projects, theme: "gitlab" diff --git a/app/views/repositories/_branch.html.haml b/app/views/repositories/_branch.html.haml deleted file mode 100644 index a6faa5fd633..00000000000 --- a/app/views/repositories/_branch.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -- commit = Commit.new(branch.commit) -- commit = CommitDecorator.decorate(commit) -%tr - %td - = link_to project_commits_path(@project, branch.name) do - - if @project.protected_branch? branch.name - %i.icon-lock - - else - %i.icon-unlock - %strong= truncate(branch.name, length: 60) - - if branch.name == @repository.root_ref - %span.label default - %td - = link_to project_commit_path(@project, commit.id), class: 'commit_short_id' do - = commit.short_id - = image_tag gravatar_icon(commit.author_email), class: "avatar s16" - %span.light - = gfm escape_once(truncate(commit.title, length: 40)) - %span - = time_ago_in_words(commit.committed_date) - ago - %td - - if can? current_user, :download_code, @project - = link_to archive_project_repository_path(@project, ref: branch.name) do - %i.icon-download-alt - Download - diff --git a/app/views/repositories/_filter.html.haml b/app/views/repositories/_filter.html.haml deleted file mode 100644 index e718d48190a..00000000000 --- a/app/views/repositories/_filter.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -%ul.nav.nav-pills.nav-stacked - = nav_link(path: 'repositories#show') do - = link_to 'Recent', project_repository_path(@project) - = nav_link(path: 'protected_branches#index') do - = link_to project_protected_branches_path(@project) do - Protected - %i.icon-lock - = nav_link(path: 'repositories#branches') do - = link_to 'All branches', branches_project_repository_path(@project) diff --git a/app/views/repositories/_head.html.haml b/app/views/repositories/_head.html.haml deleted file mode 100644 index bc96f306eee..00000000000 --- a/app/views/repositories/_head.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render "projects/project_head" diff --git a/app/views/repositories/branches.html.haml b/app/views/repositories/branches.html.haml deleted file mode 100644 index 14b5082e44e..00000000000 --- a/app/views/repositories/branches.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -= render "commits/head" -.row - .span3 - = render "filter" - .span9 - - unless @branches.empty? - %table - %thead - %tr - %th Name - %th Last commit - %th - %tbody - - @branches.each do |branch| - = render "repositories/branch", branch: branch diff --git a/app/views/repositories/show.html.haml b/app/views/repositories/show.html.haml deleted file mode 100644 index e58e16f8bf1..00000000000 --- a/app/views/repositories/show.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -= render "commits/head" -.row - .span3 - = render "filter" - .span9 - %table - %thead - %tr - %th Name - %th Last commit - %th - - @activities.each do |update| - = render "repositories/branch", branch: update.head - diff --git a/app/views/repositories/tags.html.haml b/app/views/repositories/tags.html.haml deleted file mode 100644 index d4b8bbe10d4..00000000000 --- a/app/views/repositories/tags.html.haml +++ /dev/null @@ -1,39 +0,0 @@ -= render "commits/head" -- unless @tags.empty? - %table - %thead - %tr - %th Name - %th Last commit - %th - - @tags.each do |tag| - - commit = Commit.new(tag.commit) - - commit = CommitDecorator.decorate(commit) - %tr - %td - %strong - = link_to project_commits_path(@project, tag.name), class: "" do - %i.icon-tag - = tag.name - %small.light= truncate(tag.message || '', length: 70) - %td - = image_tag gravatar_icon(commit.author_email), class: "avatar s16" - = link_to project_commit_path(@project, commit.id), class: 'commit_short_id' do - = commit.short_id - %span.light - = time_ago_in_words(commit.committed_date) - ago - %td - - if can? current_user, :download_code, @project - = link_to archive_project_repository_path(@project, ref: tag.name) do - %i.icon-download-alt - Download - -- else - %h3.nothing_here_message - Repository has no tags yet. - %br - %small - Use git tag command to add a new one: - %br - %span.monospace git tag -a v1.4 -m 'version 1.4' diff --git a/app/views/search/_blob.html.haml b/app/views/search/_blob.html.haml new file mode 100644 index 00000000000..559fdd794fc --- /dev/null +++ b/app/views/search/_blob.html.haml @@ -0,0 +1,10 @@ +%li + .file-holder + .file-title + = link_to project_blob_path(@project, tree_join(blob.ref, blob.filename), :anchor => "L" + blob.startline.to_s) do + %i.icon-file + %strong + = blob.filename + .file-content.code.term + %div{class: user_color_scheme_class} + = raw blob.colorize( formatter: :gitlab, options: { first_line_number: blob.startline } ) diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index 3fe17dce857..f7a00b23480 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -1,24 +1,35 @@ -%fieldset - %legend Groups: - %ul.nav.nav-pills.nav-stacked - %li{class: ("active" if params[:group_id].blank?)} - = link_to search_path(group_id: nil, search: params[:search]) do +.dropdown.inline + %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} + %i.icon-tags + %span.light Group: + - if @group.present? + %strong= @group.name + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to search_path(group_id: nil) do Any - - current_user.authorized_groups.each do |group| - %li{class: ("active" if params[:group_id] == group.id.to_s)} + - current_user.authorized_groups.sort_by(&:name).each do |group| + %li = link_to search_path(group_id: group.id, search: params[:search]) do = group.name -%fieldset - %legend Projects: - %ul.nav.nav-pills.nav-stacked - %li{class: ("active" if params[:project_id].blank?)} - = link_to search_path(project_id: nil, search: params[:search]) do +.dropdown.inline.prepend-left-10 + %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} + %i.icon-tags + %span.light Project: + - if @project.present? + %strong= @project.name_with_namespace + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to search_path(project_id: nil) do Any - - current_user.authorized_projects.each do |project| - %li{class: ("active" if params[:project_id] == project.id.to_s)} + - current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project| + %li = link_to search_path(project_id: project.id, search: params[:search]) do = project.name_with_namespace - -= hidden_field_tag :group_id, params[:group_id] -= hidden_field_tag :project_id, params[:project_id] diff --git a/app/views/search/_result.html.haml b/app/views/search/_result.html.haml index bfa46075baa..5f7540d1b16 100644 --- a/app/views/search/_result.html.haml +++ b/app/views/search/_result.html.haml @@ -1,9 +1,19 @@ %fieldset %legend Search results - %span.cgray (#{@projects.count + @merge_requests.count + @issues.count + @wiki_pages.count}) + %span.cgray (#{@total_results}) + +- if @project + %ul.nav.nav-pills + %li{class: ("active" if params[:search_code].present?)} + = link_to search_path(params.merge(search_code: true)) do + Repository Code + %li{class: ("active" if params[:search_code].blank?)} + = link_to search_path(params.merge(search_code: nil)) do + Everything else + .search_results - %ul.well-list + %ul.bordered-list - @projects.each do |project| %li project: @@ -12,19 +22,28 @@ - @merge_requests.each do |merge_request| %li merge request: - = link_to [merge_request.project, merge_request] do - %span ##{merge_request.id} + = link_to [merge_request.target_project, merge_request] do + %span ##{merge_request.iid} %strong.term = truncate merge_request.title, length: 50 - %span.light (#{merge_request.project.name_with_namespace}) + - if merge_request.for_fork? + %span.light (#{merge_request.source_project.name_with_namespace}:#{merge_request.source_branch} → #{merge_request.target_project.name_with_namespace}:#{merge_request.target_branch}) + - else + %span.light (#{merge_request.source_branch} → #{merge_request.target_branch}) + - if merge_request.closed? + %span.label Closed + - @issues.each do |issue| %li issue: = link_to [issue.project, issue] do - %span ##{issue.id} + %span ##{issue.iid} %strong.term = truncate issue.title, length: 50 %span.light (#{issue.project.name_with_namespace}) + - if issue.closed? + %span.label Closed + - @wiki_pages.each do |wiki_page| %li wiki: @@ -33,8 +52,11 @@ = truncate wiki_page.title, length: 50 %span.light (#{wiki_page.project.name_with_namespace}) + - @blobs.each do |blob| + = render 'blob', blob: blob + + = paginate @blobs, theme: 'gitlab' + :javascript - $(function() { - $(".search_results .term").highlight("#{escape_javascript(params[:search])}"); - }) + $(".search_results .term").highlight("#{escape_javascript(params[:search])}"); diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index 5914c22df6e..f1f65981b37 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -2,14 +2,15 @@ .search-holder = label_tag :search do %span Looking for - .input + .controls = search_field_tag :search, params[:search], placeholder: "issue 143", class: "input-xxlarge search-text-input", id: "dashboard_search" - = submit_tag 'Search', class: "btn btn-primary wide" - .clearfix - .row - .span3 - = render 'filter', f: f - .span9 - .results - - if params[:search].present? - = render 'search/result' + = hidden_field_tag :project_id, params[:project_id] + = hidden_field_tag :group_id, params[:group_id] + = hidden_field_tag :search_code, params[:search_code] + = submit_tag 'Search', class: "btn btn-create" + .prepend-top-10 + = render 'filter', f: f + + .results.prepend-top-10 + - if params[:search].present? + = render 'search/result' diff --git a/app/views/services/_gitlab_ci.html.haml b/app/views/services/_gitlab_ci.html.haml deleted file mode 100644 index dfde643849e..00000000000 --- a/app/views/services/_gitlab_ci.html.haml +++ /dev/null @@ -1,46 +0,0 @@ -%h3.page_title - GitLab CI - %small Continuous integration server from GitLab - .pull-right - - if @service.active - %small.cgreen Enabled - - else - %small.cgray Disabled - - - -.back_link - = link_to project_services_path(@project) do - ← to services - -%hr -= form_for(@service, :as => :service, :url => project_service_path(@project, :gitlab_ci), :method => :put) do |f| - - if @service.errors.any? - .alert.alert-error - %ul - - @service.errors.full_messages.each do |msg| - %li= msg - - - .control-group - = f.label :active, "Active", class: "control-label" - .controls - = f.check_box :active - - .control-group - = f.label :project_url, "Project URL", class: "control-label" - .controls - = f.text_field :project_url, class: "input-xlarge", placeholder: "http://ci.gitlabhq.com/projects/3" - - .control-group - = f.label :token, class: "control-label" do - CI Project token - .controls - = f.text_field :token, class: "input-xlarge", placeholder: "GitLab CI project specific token" - - - .form-actions - = f.submit 'Save', class: 'btn btn-save' - - - if @service.valid? && @service.active - = link_to 'Test settings', test_project_service_path(@project), class: 'btn btn-small' diff --git a/app/views/services/edit.html.haml b/app/views/services/edit.html.haml deleted file mode 100644 index d893847f1ae..00000000000 --- a/app/views/services/edit.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -= render "projects/project_head" -= render 'gitlab_ci' diff --git a/app/views/services/index.html.haml b/app/views/services/index.html.haml deleted file mode 100644 index 27dbf502569..00000000000 --- a/app/views/services/index.html.haml +++ /dev/null @@ -1,31 +0,0 @@ -= render "projects/project_head" -%h3.page_title Services -%br - -%ul.ui-box.well-list - %li - %h4.cgreen - = link_to edit_project_service_path(@project, :gitlab_ci) do - GitLab CI - %small Continuous integration server from GitLab - .pull-right - - if @gitlab_ci_service.try(:active) - %small.cgreen - %i.icon-ok - Enabled - - else - %small.cgray - %i.icon-off - Disabled - %li.disabled - %h4 - Jenkins CI - %small An extendable open source continuous integration server - .pull-right - %small Not implemented yet - %li.disabled - %h4 - Campfire - %small Web-based group chat tool - .pull-right - %small Not implemented yet diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 7b5de4a6274..5120902fa0e 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -1,5 +1,11 @@ -.input-prepend.project_clone_holder +.input-prepend.input-append.project_clone_holder %button{class: "btn active", :"data-clone" => @project.ssh_url_to_repo} SSH - %button{class: "btn", :"data-clone" => @project.http_url_to_repo}= Gitlab.config.gitlab.protocol.upcase - - = text_field_tag :project_clone, @project.url_to_repo, class: "one_click_select input-xxlarge" + %button{class: "btn", :"data-clone" => @project.http_url_to_repo}= gitlab_config.protocol.upcase + = text_field_tag :project_clone, @project.url_to_repo, class: "one_click_select span7", readonly: true + %span.add-on + - if @project.public + = public_icon + %span.cblue public + - else + = private_icon + %span.cgreen private diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml new file mode 100644 index 00000000000..ee0b57fbe5a --- /dev/null +++ b/app/views/shared/_event_filter.html.haml @@ -0,0 +1,5 @@ +.event_filter + = event_filter_link EventFilter.push, 'Push events' + = event_filter_link EventFilter.merged, 'Merge events' + = event_filter_link EventFilter.comments, 'Comments' + = event_filter_link EventFilter.team, 'Team' diff --git a/app/views/shared/_filter.html.haml b/app/views/shared/_filter.html.haml new file mode 100644 index 00000000000..2f051cea48b --- /dev/null +++ b/app/views/shared/_filter.html.haml @@ -0,0 +1,29 @@ += form_tag filter_path(entity), method: 'get' do + %fieldset + %ul.nav.nav-pills.nav-stacked + %li{class: ("active" if params[:status].blank?)} + = link_to filter_path(entity, status: nil) do + Open + %li{class: ("active" if params[:status] == 'closed')} + = link_to filter_path(entity, status: 'closed') do + Closed + %li{class: ("active" if params[:status] == 'all')} + = link_to filter_path(entity, status: 'all') do + All + + %fieldset + %legend Projects + %ul.nav.nav-pills.nav-pills-small.nav-stacked + - @projects.each do |project| + - unless entities_per_project(project, entity).zero? + %li{class: ("active" if params[:project_id] == project.id.to_s)} + = link_to filter_path(entity, project_id: project.id) do + = project.name_with_namespace + %small.pull-right= entities_per_project(project, entity) + + %fieldset + - if params[:status].present? || params[:project_id].present? + = link_to filter_path(entity, status: nil, project_id: nil), class: 'pull-right cgray' do + %i.icon-remove + %strong Clear filter + diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml new file mode 100644 index 00000000000..3b3888a50e9 --- /dev/null +++ b/app/views/shared/_issues.html.haml @@ -0,0 +1,15 @@ +- if @issues.any? + - @issues.group_by(&:project).each do |group| + .ui-box.small-box + - project = group[0] + .title + = link_to_project project + = link_to 'show all', project_issues_path(project), class: 'pull-right' + + %ul.well-list.issues-list + - group[1].each do |issue| + = render 'projects/issues/issue', issue: issue + = paginate @issues, theme: "gitlab" +- else + %p.nothing_here_message No issues to show + diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml index 85391a34316..b7a7ca8fcc8 100644 --- a/app/views/shared/_merge_requests.html.haml +++ b/app/views/shared/_merge_requests.html.haml @@ -1,14 +1,13 @@ - if @merge_requests.any? - - @merge_requests.group_by(&:project).each do |group| - .ui-box + - @merge_requests.group_by(&:target_project).each do |group| + .ui-box.small-box - project = group[0] - %h5.title + .title = link_to_project project - %ul.well-list + %ul.well-list.mr-list - group[1].each do |merge_request| - = render(partial: 'merge_requests/merge_request', locals: {merge_request: merge_request}) - %hr + = render 'projects/merge_requests/merge_request', merge_request: merge_request = paginate @merge_requests, theme: "gitlab" - else - %h3.nothing_here_message Nothing to show here + %h3.nothing_here_message No merge requests to show diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml index 5fdcea850b2..6d363807d62 100644 --- a/app/views/shared/_no_ssh.html.haml +++ b/app/views/shared/_no_ssh.html.haml @@ -1,3 +1,3 @@ -- if current_user.require_ssh_key? - %p.error_message.centered - You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', new_key_path} to your profile +- if current_user.require_ssh_key? && alert.blank? && notice.blank? + %p.error-message.centered + You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', new_profile_key_path} to your profile diff --git a/app/views/shared/_project_filter.html.haml b/app/views/shared/_project_filter.html.haml new file mode 100644 index 00000000000..f3d032ef986 --- /dev/null +++ b/app/views/shared/_project_filter.html.haml @@ -0,0 +1,32 @@ += form_tag project_entities_path, method: 'get' do + %fieldset + - if current_user + %ul.nav.nav-pills.nav-stacked + %li{class: ("active" if params[:scope].blank?)} + = link_to project_filter_path(scope: nil) do + Everyone's + %li{class: ("active" if params[:scope] == 'assigned-to-me')} + = link_to project_filter_path(scope: 'assigned-to-me') do + Assigned to me + %li{class: ("active" if params[:scope] == 'created-by-me')} + = link_to project_filter_path(scope: 'created-by-me') do + Created by me + + %ul.nav.nav-pills.nav-stacked + %li{class: ("active" if params[:state].blank?)} + = link_to project_filter_path(state: nil) do + Open + %li{class: ("active" if params[:state] == 'closed')} + = link_to project_filter_path(state: 'closed') do + Closed + %li{class: ("active" if params[:state] == 'all')} + = link_to project_filter_path(state: 'all') do + All + + %fieldset + - if %w(state scope milestone_id assignee_id label_name).select { |k| params[k].present? }.any? + = link_to project_entities_path, class: 'cgray pull-right' do + %i.icon-remove + %strong Clear filter + + diff --git a/app/views/shared/_promo.html.haml b/app/views/shared/_promo.html.haml new file mode 100644 index 00000000000..c97f8ba0f0e --- /dev/null +++ b/app/views/shared/_promo.html.haml @@ -0,0 +1,4 @@ +.gitlab-promo + = link_to "Homepage", "http://gitlab.org" + = link_to "Blog", "http://blog.gitlab.org" + = link_to "@gitlabhq", "https://twitter.com/gitlabhq" diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 8b44cf1944e..dc8c656e12e 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -3,3 +3,5 @@ = hidden_field_tag :destination, destination - if defined?(path) = hidden_field_tag :path, path + - @options && @options.each do |key, value| + = hidden_field_tag key, value, id: nil diff --git a/app/views/snippets/_blob.html.haml b/app/views/snippets/_blob.html.haml index 017a33b34f3..c2e0d97a117 100644 --- a/app/views/snippets/_blob.html.haml +++ b/app/views/snippets/_blob.html.haml @@ -1,10 +1,14 @@ -.file_holder - .file_title +.file-holder + .file-title %i.icon-file %strong= @snippet.file_name %span.options - = link_to "raw", raw_project_snippet_path(@project, @snippet), class: "btn btn-tiny", target: "_blank" - .file_content.code + .btn-group.tree-btn-group.pull-right + - if @snippet.author == current_user + = link_to "Edit", edit_snippet_path(@snippet), class: "btn btn-tiny", title: 'Edit Snippet' + = link_to "Delete", snippet_path(@snippet), method: :delete, confirm: "Are you sure?", class: "btn btn-tiny", title: 'Delete Snippet' + = link_to "Raw", raw_snippet_path(@snippet), class: "btn btn-tiny", target: "_blank" + .file-content.code - unless @snippet.content.empty? %div{class: user_color_scheme_class} = raw @snippet.colorize(formatter: :gitlab) diff --git a/app/views/snippets/_form.html.haml b/app/views/snippets/_form.html.haml index 77162cdcde3..e77550e7be3 100644 --- a/app/views/snippets/_form.html.haml +++ b/app/views/snippets/_form.html.haml @@ -1,36 +1,41 @@ -%h3.page_title +%h3.page-title = @snippet.new_record? ? "New Snippet" : "Edit Snippet ##{@snippet.id}" %hr .snippet-form-holder - = form_for [@project, @snippet] do |f| + = form_for @snippet, as: :personal_snippet, url: url do |f| -if @snippet.errors.any? .alert.alert-error %ul - @snippet.errors.full_messages.each do |msg| %li= msg - .clearfix + .control-group = f.label :title - .input= f.text_field :title, placeholder: "Example Snippet", class: 'input-xlarge', required: true - .clearfix - = f.label "Lifetime" - .input= f.select :expires_at, lifetime_select_options, {}, {class: 'chosen span2'} - .clearfix + .controls= f.text_field :title, placeholder: "Example Snippet", class: 'input-xlarge', required: true + .control-group + = f.label "Private?" + .controls + = f.check_box :private, {class: ''} + .control-group .file-editor = f.label :file_name, "File" - .input - .file_holder.snippet - .file_title + .controls + .file-holder.snippet + .file-title = f.text_field :file_name, placeholder: "example.rb", class: 'snippet-file-name', required: true - .file_content.code + .file-content.code %pre#editor= @snippet.content = f.hidden_field :content, class: 'snippet-file-content' .form-actions - = f.submit 'Save', class: "btn-save btn" - = link_to "Cancel", project_snippets_path(@project), class: " btn" + - if @snippet.new_record? + = f.submit 'Create snippet', class: "btn-create btn" + - else + = f.submit 'Save', class: "btn-save btn" + + = link_to "Cancel", snippets_path(@project), class: "btn btn-cancel" - unless @snippet.new_record? - .pull-right= link_to 'Destroy', [@project, @snippet], confirm: 'Are you sure?', method: :delete, class: "btn pull-right danger delete-snippet", id: "destroy_snippet_#{@snippet.id}" + .pull-right= link_to 'Destroy', snippet_path(@snippet), confirm: 'Removed snippet cannot be restored! Are you sure?', method: :delete, class: "btn pull-right danger delete-snippet", id: "destroy_snippet_#{@snippet.id}" :javascript diff --git a/app/views/snippets/_snippet.html.haml b/app/views/snippets/_snippet.html.haml index a576500c15d..9689c9c4d38 100644 --- a/app/views/snippets/_snippet.html.haml +++ b/app/views/snippets/_snippet.html.haml @@ -1,13 +1,23 @@ -%tr - %td - = image_tag gravatar_icon(snippet.author_email), class: "avatar s24" - %a{href: project_snippet_path(snippet.project, snippet)} - %strong= truncate(snippet.title, length: 60) - %td - = snippet.file_name - %td - %span.cgray - - if snippet.expires_at - = snippet.expires_at.to_date.to_s(:short) - - else - Never +%li + %h4.snippet-title + = link_to reliable_snippet_path(snippet) do + = truncate(snippet.title, length: 60) + - if snippet.private? + %span.label.label-success + %i.icon-lock + private + %span.cgray.monospace.tiny.pull-right + = snippet.file_name + + %small.pull-right.cgray + - if snippet.project_id? + = link_to snippet.project.name_with_namespace, project_path(snippet.project) + + .snippet-info + = "##{snippet.id}" + %span + by + = link_to user_snippets_path(snippet.author) do + = image_tag gravatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: '' + = snippet.author_name + %span.light #{time_ago_in_words(snippet.created_at)} ago diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml new file mode 100644 index 00000000000..05365dd8b88 --- /dev/null +++ b/app/views/snippets/_snippets.html.haml @@ -0,0 +1,7 @@ +%ul.bordered-list + = render partial: 'snippet', collection: @snippets + - if @snippets.empty? + %li + %h3.nothing_here_message Nothing here. + += paginate @snippets, theme: 'gitlab' diff --git a/app/views/snippets/current_user_index.html.haml b/app/views/snippets/current_user_index.html.haml new file mode 100644 index 00000000000..51030f965a1 --- /dev/null +++ b/app/views/snippets/current_user_index.html.haml @@ -0,0 +1,34 @@ +%h3.page-title + My Snippets + .pull-right + = link_to new_snippet_path, class: "btn btn-new grouped", title: "New Snippet" do + Add new snippet + = link_to snippets_path, class: "btn grouped" do + Discover snippets + +%p.light + Share code pastes with others out of git repository +%hr + +.row + .span3 + %ul.nav.nav-pills.nav-stacked + = nav_tab :scope, nil do + = link_to user_snippets_path(@user) do + All + %span.pull-right + = @user.snippets.count + = nav_tab :scope, 'private' do + = link_to user_snippets_path(@user, scope: 'private') do + Private + %span.pull-right + = @user.snippets.private.count + = nav_tab :scope, 'public' do + = link_to user_snippets_path(@user, scope: 'public') do + Public + %span.pull-right + = @user.snippets.public.count + + .span9.my-snippets + = render 'snippets' + diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml index 8afaf46e95d..1b88a85faf1 100644 --- a/app/views/snippets/edit.html.haml +++ b/app/views/snippets/edit.html.haml @@ -1,2 +1 @@ -= render "projects/project_head" -= render "snippets/form" += render "snippets/form", url: snippet_path(@snippet) diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml index 28a533d238f..2f6c914a159 100644 --- a/app/views/snippets/index.html.haml +++ b/app/views/snippets/index.html.haml @@ -1,21 +1,15 @@ -= render "projects/project_head" +%h3.page-title + Public snippets -%h3.page_title - Snippets - %small share code pastes with others out of git repository - - - if can? current_user, :write_snippet, @project - = link_to new_project_snippet_path(@project), class: "btn btn-small add_new pull-right", title: "New Snippet" do + .pull-right + = link_to new_snippet_path, class: "btn btn-new grouped", title: "New Snippet" do Add new snippet -%br -%table - %thead - %tr - %th Title - %th File Name - %th Expires At - = render @snippets - - if @snippets.empty? - %tr - %td{colspan: 3} - %h3.nothing_here_message Nothing here. + = link_to user_snippets_path(current_user), class: "btn grouped" do + My snippets + +%p.light + Public snippets created by you and other users are listed here + +%hr += render 'snippets' + diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml index 8afaf46e95d..90e0a1f79da 100644 --- a/app/views/snippets/new.html.haml +++ b/app/views/snippets/new.html.haml @@ -1,2 +1 @@ -= render "projects/project_head" -= render "snippets/form" += render "snippets/form", url: snippets_path(@snippet) diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index e6bcd88f830..37f9e7576f5 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -1,11 +1,31 @@ -= render "projects/project_head" - -%h3.page_title +%h3.page-title = @snippet.title - %small= @snippet.file_name - - if can?(current_user, :admin_snippet, @project) || @snippet.author == current_user - = link_to "Edit", edit_project_snippet_path(@project, @snippet), class: "btn btn-small pull-right" -%br + - if @snippet.private? + %span.label.label-success + %i.icon-lock + private + + .pull-right + = link_to new_snippet_path, class: "btn btn-new btn-small", title: "New Snippet" do + Add new snippet + + +.append-bottom-20 + .pull-right + = "##{@snippet.id}" + %span.light + by + = link_to user_snippets_path(@snippet.author) do + = image_tag gravatar_icon(@snippet.author_email), class: "avatar avatar-inline s16" + = @snippet.author_name + + .back-link + - if @snippet.author == current_user + = link_to user_snippets_path(current_user) do + ← my snippets + - else + = link_to snippets_path do + ← discover snippets + %div= render 'blob' -%div#notes= render "notes/notes_with_form" diff --git a/app/views/snippets/user_index.html.haml b/app/views/snippets/user_index.html.haml new file mode 100644 index 00000000000..49636c3f5f0 --- /dev/null +++ b/app/views/snippets/user_index.html.haml @@ -0,0 +1,12 @@ +%h3.page-title + = image_tag gravatar_icon(@user.email), class: "avatar s24" + = @user.name + %span + \/ + Snippets + = link_to new_snippet_path, class: "btn btn-small add_new pull-right", title: "New Snippet" do + Add new snippet + +%hr + += render 'snippets' diff --git a/app/views/team_members/_form.html.haml b/app/views/team_members/_form.html.haml deleted file mode 100644 index 05bea2db87e..00000000000 --- a/app/views/team_members/_form.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -%h3.page_title - = "New Team member(s)" -%hr -= form_for @user_project_relation, as: :team_member, url: project_team_members_path(@project) do |f| - -if @user_project_relation.errors.any? - .alert.alert-error - %ul - - @user_project_relation.errors.full_messages.each do |msg| - %li= msg - - %h6 1. Choose people you want in the team - .clearfix - = f.label :user_ids, "People" - .input= select_tag(:user_ids, options_from_collection_for_select(User.active.not_in_project(@project).alphabetically, :id, :name), {data: {placeholder: "Select users"}, class: "chosen xxlarge", multiple: true}) - - %h6 2. Set access level for them - .clearfix - = f.label :project_access, "Project Access" - .input= select_tag :project_access, options_for_select(Project.access_options, @user_project_relation.project_access), class: "project-access-select chosen" - - .actions - = f.submit 'Add users', class: "btn btn-create" - = link_to "Cancel", project_team_index_path(@project), class: "btn btn-cancel" diff --git a/app/views/team_members/_show.html.haml b/app/views/team_members/_show.html.haml deleted file mode 100644 index 3df2caed64a..00000000000 --- a/app/views/team_members/_show.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -- user = member.user -- allow_admin = can? current_user, :admin_project, @project -%li{id: dom_id(user), class: "team_member_row user_#{user.id}"} - .row - .span6 - = link_to project_team_member_path(@project, user), title: user.name, class: "dark" do - = image_tag gravatar_icon(user.email, 40), class: "avatar s32" - = link_to project_team_member_path(@project, user), title: user.name, class: "dark" do - %strong= truncate(user.name, lenght: 40) - %br - %small.cgray= user.email - - .span5.pull-right - - if allow_admin - .left - = form_for(member, as: :team_member, url: project_team_member_path(@project, member.user)) do |f| - = f.select :project_access, options_for_select(UsersProject.access_roles, member.project_access), {}, class: "medium project-access-select span2" - .pull-right - - if current_user == user - %span.btn.disabled This is you! - - if @project.namespace_owner == user - %span.btn.disabled Owner - - elsif user.blocked - %span.btn.disabled.blocked Blocked - - elsif allow_admin - = link_to project_team_member_path(@project, user), confirm: remove_from_project_team_message(@project, user), method: :delete, class: "btn-tiny btn btn-remove" do - %i.icon-minus.icon-white - diff --git a/app/views/team_members/_show_team.html.haml b/app/views/team_members/_show_team.html.haml deleted file mode 100644 index f1555f0b87b..00000000000 --- a/app/views/team_members/_show_team.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -- team = team_rel.user_team -- allow_admin = can? current_user, :admin_team_member, @project -%li{id: dom_id(team), class: "user_team_row team_#{team.id}"} - .row - .span6 - %strong= link_to team.name, team_path(team), title: team.name, class: "dark" - %br - %small.cgray Members: #{team.members.count} - - .span5.pull-right - .pull-right - - if allow_admin - .left - = link_to resign_project_team_path(@project, team), method: :delete, confirm: "Are you shure?", class: "btn btn-remove small" do - %i.icon-minus.icon-white diff --git a/app/views/team_members/_team.html.haml b/app/views/team_members/_team.html.haml deleted file mode 100644 index 365d9b65942..00000000000 --- a/app/views/team_members/_team.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- grouper_project_members(@project).each do |access, members| - .ui-box - %h5.title - = Project.access_options.key(access).pluralize - %small= members.size - %ul.well-list - - members.sort_by(&:user_name).each do |up| - = render(partial: 'team_members/show', locals: {member: up}) - - -:javascript - $(function(){ - $('.repo-access-select, .project-access-select').live("change", function() { - $(this.form).submit(); - }); - }) diff --git a/app/views/team_members/_teams.html.haml b/app/views/team_members/_teams.html.haml deleted file mode 100644 index 156fdd1befa..00000000000 --- a/app/views/team_members/_teams.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- grouper_project_teams(@project).each do |access, teams| - .ui-box - %h5.title - = UserTeam.access_roles.key(access).pluralize - %small= teams.size - %ul.well-list - - teams.sort_by(&:team_name).each do |tofr| - = render(partial: 'team_members/show_team', locals: {team_rel: tofr}) - - -:javascript - $(function(){ - $('.repo-access-select, .project-access-select').live("change", function() { - $(this.form).submit(); - }); - }) diff --git a/app/views/team_members/create.js.haml b/app/views/team_members/create.js.haml deleted file mode 100644 index b7dff35a269..00000000000 --- a/app/views/team_members/create.js.haml +++ /dev/null @@ -1,13 +0,0 @@ -- if @user_project_relation.valid? - :plain - $("#new_team_member").hide("slide", { direction: "right" }, 150, function(){ - $("#team-table").show("slide", { direction: "left" }, 150, function() { - $("#new_team_member").remove(); - $("#team-table").replaceWith("#{escape_javascript(render('projects/team'))}"); - $(".add_new").show(); - }); - }); -- else - :plain - $("#new_team_member").replaceWith("#{escape_javascript(render('form'))}"); - $('select#team_member_user_id').chosen(); diff --git a/app/views/team_members/import.html.haml b/app/views/team_members/import.html.haml deleted file mode 100644 index d6c81befd08..00000000000 --- a/app/views/team_members/import.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -= render "projects/project_head" - -%h3.page_title - = "Import team from another project" -%hr -%p.slead - Read more about project team import #{link_to "here", '#', class: 'vlink'}. -= form_tag apply_import_project_team_members_path(@project), method: 'post' do - %p.slead Choose project you want to use as team source: - .padded - = label_tag :source_project_id, "Project" - .input= select_tag(:source_project_id, options_from_collection_for_select(current_user.authorized_projects, :id, :name_with_namespace), prompt: "Select project", class: "chosen xxlarge", required: true) - - .actions - = submit_tag 'Import', class: "btn btn-save" - = link_to "Cancel", project_team_index_path(@project), class: "btn btn-cancel" - diff --git a/app/views/team_members/index.html.haml b/app/views/team_members/index.html.haml deleted file mode 100644 index 3264f58cb32..00000000000 --- a/app/views/team_members/index.html.haml +++ /dev/null @@ -1,33 +0,0 @@ -= render "projects/project_head" -%h3.page_title - Team Members - (#{@project.users.count}) - %small - Read more about project permissions - %strong= link_to "here", help_permissions_path, class: "vlink" - - - if can? current_user, :admin_team_member, @project - %span.pull-right - = link_to import_project_team_members_path(@project), class: "btn btn-small grouped", title: "Import team from another project" do - Import team from another project - = link_to available_project_teams_path(@project), class: "btn btn-small grouped", title: "Assign project to team of users" do - Assign project to Team of users - = link_to new_project_team_member_path(@project), class: "btn btn-primary small grouped", title: "New Team Member" do - New Team Member - -%hr - -.clearfix -%div.team-table - = render partial: "team_members/team", locals: {project: @project} - - -%h3.page_title - Assigned teams - (#{@project.user_teams.count}) - -%hr - -.clearfix -%div.team-table - = render partial: "team_members/teams", locals: {project: @project} diff --git a/app/views/team_members/new.html.haml b/app/views/team_members/new.html.haml deleted file mode 100644 index 40eb4cebf08..00000000000 --- a/app/views/team_members/new.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -= render "projects/project_head" -= render "team_members/form" diff --git a/app/views/team_members/show.html.haml b/app/views/team_members/show.html.haml deleted file mode 100644 index 192948eff7d..00000000000 --- a/app/views/team_members/show.html.haml +++ /dev/null @@ -1,59 +0,0 @@ -- allow_admin = can? current_user, :admin_project, @project - -.team_member_show - - if can? current_user, :admin_project, @project - = link_to 'Remove from team', project_team_member_path(@project, @member), confirm: 'Are you sure?', method: :delete, class: "btn btn-remove pull-right" - .profile_avatar_holder - = image_tag gravatar_icon(@member.email, 60), class: "borders" - %h3.page_title - = @member.name - %small (@#{@member.username}) - - %hr - .back_link - %br - = link_to project_team_index_path(@project), class: "" do - ← To team list - %br - .row - .span6 - %table.lite - %tr - %td Email - %td= mail_to @member.email - %tr - %td Skype - %td= @member.skype - - unless @member.linkedin.blank? - %tr - %td LinkedIn - %td= @member.linkedin - - unless @member.twitter.blank? - %tr - %td Twitter - %td= @member.twitter - - unless @member.bio.blank? - %tr - %td Bio - %td= @member.bio - .span6 - %table.lite - %tr - %td Member since - %td= @user_project_relation.created_at.stamp("Aug 21, 2011") - %tr - %td - Project Access: - %small (#{link_to "read more", help_permissions_path, class: "vlink"}) - %td - = form_for(@user_project_relation, as: :team_member, url: project_team_member_path(@project, @member)) do |f| - = f.select :project_access, options_for_select(Project.access_options, @user_project_relation.project_access), {}, class: "project-access-select", disabled: !allow_admin - %hr - = render @events -:javascript - $(function(){ - $('.repo-access-select, .project-access-select').live("change", function() { - $(this.form).submit(); - }); - }) - diff --git a/app/views/teams/_filter.html.haml b/app/views/teams/_filter.html.haml deleted file mode 100644 index f461fcad42e..00000000000 --- a/app/views/teams/_filter.html.haml +++ /dev/null @@ -1,33 +0,0 @@ -= form_tag team_filter_path(entity), method: 'get' do - %fieldset.dashboard-search-filter - = search_field_tag "search", params[:search], { placeholder: 'Search', class: 'search-text-input' } - = button_tag type: 'submit', class: 'btn' do - %i.icon-search - - %fieldset - %legend Status: - %ul.nav.nav-pills.nav-stacked - %li{class: ("active" if !params[:status])} - = link_to team_filter_path(entity, status: nil) do - Open - %li{class: ("active" if params[:status] == 'closed')} - = link_to team_filter_path(entity, status: 'closed') do - Closed - %li{class: ("active" if params[:status] == 'all')} - = link_to team_filter_path(entity, status: 'all') do - All - - %fieldset - %legend Projects: - %ul.nav.nav-pills.nav-stacked - - @projects.each do |project| - - unless entities_per_project(project, entity).zero? - %li{class: ("active" if params[:project_id] == project.id.to_s)} - = link_to team_filter_path(entity, project_id: project.id) do - = project.name_with_namespace - %small.pull-right= entities_per_project(project, entity) - - %fieldset - %hr - = link_to "Reset", team_filter_path(entity), class: 'btn pull-right' - diff --git a/app/views/teams/_projects.html.haml b/app/views/teams/_projects.html.haml deleted file mode 100644 index 5677255b15b..00000000000 --- a/app/views/teams/_projects.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -.projects_box - %h5.title - Projects - %small - (#{projects.count}) - - if can? current_user, :manage_user_team, @team - %span.pull-right - = link_to new_team_project_path(@team), class: "btn btn-tiny info" do - %i.icon-plus - Assign Project - %ul.well-list - - if projects.blank? - %p.nothing_here_message This team has no projects yet - - projects.each do |project| - %li - = link_to project_path(project), class: dom_class(project) do - %strong.well-title= truncate(project.name, length: 25) - %span.arrow - → - %span.last_activity - %strong Last activity: - %span= project_last_activity(project) diff --git a/app/views/teams/edit.html.haml b/app/views/teams/edit.html.haml deleted file mode 100644 index 3435583600d..00000000000 --- a/app/views/teams/edit.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -%h3.page_title= "Edit Team #{@team.name}" -%hr -= form_for @team, url: team_path(@team) do |f| - - if @team.errors.any? - .alert.alert-error - %span= @team.errors.full_messages.first - .clearfix - = f.label :name do - Team name is - .input - = f.text_field :name, placeholder: "Ex. OpenSource", class: "xxlarge left" - - .clearfix - = f.label :path do - Team path is - .input - = f.text_field :path, placeholder: "opensource", class: "xxlarge left" - .form-actions - = f.submit 'Save team changes', class: "btn btn-primary" - = link_to 'Delete team', team_path(@team), method: :delete, confirm: "You are shure?", class: "btn btn-remove pull-right" diff --git a/app/views/teams/issues.html.haml b/app/views/teams/issues.html.haml deleted file mode 100644 index c6a68c37b9c..00000000000 --- a/app/views/teams/issues.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -%h3.page_title - Issues - %small (in Team projects assigned to Team members) - %small.pull-right #{@issues.total_count} issues - -%hr -.row - .span3 - = render 'filter', entity: 'issue' - .span9 - - if @issues.any? - - @issues.group_by(&:project).each do |group| - %div.ui-box - - @project = group[0] - %h5.title - = link_to_project @project - %ul.well-list.issues_table - - group[1].each do |issue| - = render(partial: 'issues/show', locals: {issue: issue}) - %hr - = paginate @issues, theme: "gitlab" - - else - %p.nothing_here_message Nothing to show here diff --git a/app/views/teams/members/_form.html.haml b/app/views/teams/members/_form.html.haml deleted file mode 100644 index c22ee78305f..00000000000 --- a/app/views/teams/members/_form.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -= form_tag admin_team_member_path(@team, @member), method: :put do - -if @member.errors.any? - .alert.alert-error - %ul - - @member.errors.full_messages.each do |msg| - %li= msg - - .clearfix - %label Default access for Team projects: - .input - = select_tag :default_project_access, options_for_select(UserTeam.access_roles, @team.default_projects_access(@member)), class: "project-access-select chosen span3" - .clearfix - %label Team admin? - .input - = check_box_tag :group_admin, true, @team.admin?(@member) - - %br - .actions - = submit_tag 'Save', class: "btn btn-save" - = link_to 'Cancel', :back, class: "btn" diff --git a/app/views/teams/members/_show.html.haml b/app/views/teams/members/_show.html.haml deleted file mode 100644 index 6cddb8e4826..00000000000 --- a/app/views/teams/members/_show.html.haml +++ /dev/null @@ -1,31 +0,0 @@ -- user = member.user -- allow_admin = can? current_user, :manage_user_team, @team -%li{id: dom_id(member), class: "team_member_row user_#{user.id}"} - .row - .span5 - = link_to user_path(user.username), title: user.name, class: "dark" do - = image_tag gravatar_icon(user.email, 40), class: "avatar s32" - = link_to user_path(user.username), title: user.name, class: "dark" do - %strong= truncate(user.name, lenght: 40) - %br - %small.cgray= user.email - - .span6.pull-right - - if allow_admin - .left.span2 - = form_for(member, as: :team_member, url: team_member_path(@team, user)) do |f| - = f.select :permission, options_for_select(UsersProject.access_roles, @team.default_projects_access(user)), {}, class: "medium project-access-select span2" - .left.span2 - %span - = check_box_tag :group_admin, true, @team.admin?(user) - Admin access - .pull-right - - if current_user == user - %span.btn.disabled This is you! - - if @team.owner == user - %span.btn.disabled.btn-success Owner - - elsif user.blocked - %span.btn.disabled.blocked Blocked - - elsif allow_admin - = link_to team_member_path(@team, user), confirm: remove_from_user_team_message(@team, user), method: :delete, class: "btn-tiny btn btn-remove" do - %i.icon-minus.icon-white diff --git a/app/views/teams/members/_team.html.haml b/app/views/teams/members/_team.html.haml deleted file mode 100644 index d8afc1fa371..00000000000 --- a/app/views/teams/members/_team.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- grouped_user_team_members(@team).each do |access, members| - .ui-box - %h5.title - = Project.access_options.key(access).pluralize - %small= members.size - %ul.well-list - - members.sort_by(&:user_name).each do |up| - = render(partial: 'teams/members/show', locals: {member: up}) - - -:javascript - $(function(){ - $('.repo-access-select, .project-access-select').live("change", function() { - $(this.form).submit(); - }); - }) diff --git a/app/views/teams/members/edit.html.haml b/app/views/teams/members/edit.html.haml deleted file mode 100644 index 375880496ab..00000000000 --- a/app/views/teams/members/edit.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -%h3.page_title - Edit access #{@member.name} in #{@team.name} team - -%hr -%table.zebra-striped - %tr - %td User: - %td= @member.name - %tr - %td Team: - %td= @team.name - %tr - %td Since: - %td= member_since(@team, @member).stamp("Nov 11, 2010") - -= render 'form' diff --git a/app/views/teams/members/index.html.haml b/app/views/teams/members/index.html.haml deleted file mode 100644 index 87438266cfb..00000000000 --- a/app/views/teams/members/index.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -%h3.page_title - Team Members - (#{@members.count}) - %small - Read more about project permissions - %strong= link_to "here", help_permissions_path, class: "vlink" - - - if can? current_user, :manage_user_team, @team - %span.pull-right - = link_to new_team_member_path(@team), class: "btn btn-primary small grouped", title: "New Team Member" do - New Team Member -%hr - - -.clearfix -%div.team-table - = render partial: "teams/members/team", locals: {project: @team} diff --git a/app/views/teams/members/new.html.haml b/app/views/teams/members/new.html.haml deleted file mode 100644 index 083e137e0ae..00000000000 --- a/app/views/teams/members/new.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -%h3.page_title - Team: #{@team.name} - -%fieldset - %legend Members (#{@team.members.count}) - = form_tag team_members_path(@team), id: "team_members", class: "bulk_import", method: :post do - %table#members_list - %thead - %tr - %th User name - %th Default project access - %th Team access - %th - - @team.members.each do |member| - %tr.member - %td - = member.name - %small= "(#{member.email})" - %td= @team.human_default_projects_access(member) - %td= @team.admin?(member) ? "Admin" : "Member" - %td - %tr - %td= select_tag :user_ids, options_from_collection_for_select(@users , :id, :name_with_email), multiple: true, data: {placeholder: 'Select users'}, class: 'chosen span5' - %td= select_tag :default_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" } - %td - %span= check_box_tag :group_admin - %span Admin? - %td= submit_tag 'Add User', class: "btn btn-create", id: :add_members_to_team diff --git a/app/views/teams/members/show.html.haml b/app/views/teams/members/show.html.haml deleted file mode 100644 index f760c2dae3a..00000000000 --- a/app/views/teams/members/show.html.haml +++ /dev/null @@ -1,60 +0,0 @@ -- allow_admin = can? current_user, :admin_project, @project -- user = @team_member.user - -.team_member_show - - if can? current_user, :admin_project, @project - = link_to 'Remove from team', project_team_member_path(project_id: @project, id: @team_member.id), confirm: 'Are you sure?', method: :delete, class: "pull-right btn btn-remove" - .profile_avatar_holder - = image_tag gravatar_icon(user.email, 60), class: "borders" - %h3.page_title - = user.name - %small (@#{user.username}) - - %hr - .back_link - %br - = link_to project_team_index_path(@project), class: "" do - ← To team list - %br - .row - .span6 - %table.lite - %tr - %td Email - %td= mail_to user.email - %tr - %td Skype - %td= user.skype - - unless user.linkedin.blank? - %tr - %td LinkedIn - %td= user.linkedin - - unless user.twitter.blank? - %tr - %td Twitter - %td= user.twitter - - unless user.bio.blank? - %tr - %td Bio - %td= user.bio - .span6 - %table.lite - %tr - %td Member since - %td= @team_member.created_at.stamp("Aug 21, 2011") - %tr - %td - Project Access: - %small (#{link_to "read more", help_permissions_path, class: "vlink"}) - %td - = form_for(@team_member, as: :team_member, url: project_team_member_path(@project, @team_member)) do |f| - = f.select :project_access, options_for_select(Project.access_options, @team_member.project_access), {}, class: "project-access-select", disabled: !allow_admin - %hr - = render @events -:javascript - $(function(){ - $('.repo-access-select, .project-access-select').live("change", function() { - $(this.form).submit(); - }); - }) - diff --git a/app/views/teams/merge_requests.html.haml b/app/views/teams/merge_requests.html.haml deleted file mode 100644 index 417d1aa6040..00000000000 --- a/app/views/teams/merge_requests.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -%h3.page_title - Merge Requests - %small (authored by or assigned to Team members) - %small.pull-right #{@merge_requests.total_count} merge requests - -%hr -.row - .span3 - = render 'filter', entity: 'merge_request' - .span9 - - if @merge_requests.any? - - @merge_requests.group_by(&:project).each do |group| - .ui-box - - @project = group[0] - %h5.title - = link_to_project @project - %ul.well-list - - group[1].each do |merge_request| - = render(partial: 'merge_requests/merge_request', locals: {merge_request: merge_request}) - %hr - = paginate @merge_requests, theme: "gitlab" - - - else - %h3.nothing_here_message Nothing to show here diff --git a/app/views/teams/new.html.haml b/app/views/teams/new.html.haml deleted file mode 100644 index 38f61c11c0c..00000000000 --- a/app/views/teams/new.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -%h3.page_title New Team -%hr -= form_for @team, url: teams_path do |f| - - if @team.errors.any? - .alert.alert-error - %span= @team.errors.full_messages.first - .clearfix - = f.label :name do - Team name is - .input - = f.text_field :name, placeholder: "Ex. Ruby Developers", class: "xxlarge left" - - = f.submit 'Create team', class: "btn btn-create" - %hr - .padded - %ul - %li All created teams are public (users can view who enter into team and which project are assigned for this team) - %li People within a team see only projects they have access to - %li You will be able to assign existing projects for team diff --git a/app/views/teams/projects/_form.html.haml b/app/views/teams/projects/_form.html.haml deleted file mode 100644 index d2c89b0c36b..00000000000 --- a/app/views/teams/projects/_form.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -= form_tag team_project_path(@team, @project), method: :put do - -if @project.errors.any? - .alert.alert-error - %ul - - @project.errors.full_messages.each do |msg| - %li= msg - - .clearfix - %label Max access for Team members: - .input - = select_tag :greatest_project_access, options_for_select(UserTeam.access_roles, @team.max_project_access(@project)), class: "project-access-select chosen span3" - - %br - .actions - = submit_tag 'Save', class: "btn btn-save" - = link_to 'Cancel', :back, class: "btn btn-cancel" diff --git a/app/views/teams/projects/edit.html.haml b/app/views/teams/projects/edit.html.haml deleted file mode 100644 index 82c7d734815..00000000000 --- a/app/views/teams/projects/edit.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -%h3.page_title - Edit max access in #{link_to @project.name_with_namespace, @project} for #{link_to(@team.name, team_path(@team))} team - -%hr - -= render 'form' diff --git a/app/views/teams/projects/index.html.haml b/app/views/teams/projects/index.html.haml deleted file mode 100644 index 696ee29c778..00000000000 --- a/app/views/teams/projects/index.html.haml +++ /dev/null @@ -1,36 +0,0 @@ -%h3.page_title - Assigned projects (#{@team.projects.count}) - %small - Read more about project permissions - %strong= link_to "here", help_permissions_path, class: "vlink" - - - if current_user.can?(:manage_user_team, @team) && @avaliable_projects.any? - %span.pull-right - = link_to new_team_project_path(@team), class: "btn btn-primary small grouped", title: "New Team Member" do - Assign project to Team - -%hr - -- if @team.projects.present? - %table.projects-table - %thead - %tr - %th Project name - %th Max access - - if current_user.can?(:admin_user_team, @team) - %th.span3 - - - @team.projects.each do |project| - %tr.project - %td - = link_to project.name_with_namespace, project_path(project) - %td - %span= @team.human_max_project_access(project) - - - if current_user.can?(:admin_user_team, @team) - %td.bgred - = link_to 'Edit max access', edit_team_project_path(@team, project), class: "btn btn-small" - = link_to 'Relegate', team_project_path(@team, project), confirm: 'Remove project from team and move to global namespace. Are you sure?', method: :delete, class: "btn btn-remove small" - -- else - %p.nothing_here_message This team has no projects yet diff --git a/app/views/teams/projects/new.html.haml b/app/views/teams/projects/new.html.haml deleted file mode 100644 index 3f3671aa0a4..00000000000 --- a/app/views/teams/projects/new.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -%h3.page_title - Team: #{@team.name} - -%fieldset - %legend Projects (#{@team.projects.count}) - = form_tag team_projects_path(@team), id: "assign_projects", class: "bulk_import", method: :post do - %table#projects_list - %thead - %tr - %th Project name - %th Max access - %th - - @team.projects.each do |project| - %tr.project - %td - = link_to project.name_with_namespace, team_project_path(@team, project) - %td - %span= @team.human_max_project_access(project) - %td - %tr - %td= select_tag :project_ids, options_from_collection_for_select(@avaliable_projects , :id, :name_with_namespace), multiple: true, data: {placeholder: 'Select projects'}, class: 'chosen span5' - %td= select_tag :greatest_project_access, options_for_select(UserTeam.access_roles), {class: "project-access-select chosen span3" } - %td= submit_tag 'Add Project', class: "btn btn-create", id: :assign_projects_to_team diff --git a/app/views/teams/show.html.haml b/app/views/teams/show.html.haml deleted file mode 100644 index d6e80e2a51e..00000000000 --- a/app/views/teams/show.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -.projects - .activities.span8 - = link_to dashboard_path, class: 'btn btn-tiny' do - ← To dashboard - - %span.cgray Events and projects are filtered in scope of team - %hr - - if @events.any? - .content_list - - else - %p.nothing_here_message Projects activity will be displayed here - .loading.hide - .side.span4 - = render "projects", projects: @projects - %div - %span.rss-icon - = link_to dashboard_path(:atom, { private_token: current_user.private_token }) do - = image_tag "rss_ui.png", title: "feed" - %strong News Feed - - %hr - .gitlab-promo - = link_to "Homepage", "http://gitlabhq.com" - = link_to "Blog", "http://blog.gitlabhq.com" - = link_to "@gitlabhq", "https://twitter.com/gitlabhq" - -:javascript - $(function(){ Pager.init(20, true); }); diff --git a/app/views/teams/show.js.haml b/app/views/teams/show.js.haml deleted file mode 100644 index 7e5a148e5ef..00000000000 --- a/app/views/teams/show.js.haml +++ /dev/null @@ -1,2 +0,0 @@ -:plain - Pager.append(#{@events.count}, "#{escape_javascript(render(@events))}"); diff --git a/app/views/tree/_blob.html.haml b/app/views/tree/_blob.html.haml deleted file mode 100644 index ebf1ee2c678..00000000000 --- a/app/views/tree/_blob.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -.file_holder - .file_title - %i.icon-file - %span.file_name - = blob.name - %small= number_to_human_size blob.size - %span.options= render "tree/blob_actions" - - if blob.text? - = render "tree/blob/text", blob: blob - - elsif blob.image? - = render "tree/blob/image", blob: blob - - else - = render "tree/blob/download", blob: blob diff --git a/app/views/tree/_blob_actions.html.haml b/app/views/tree/_blob_actions.html.haml deleted file mode 100644 index 0bde968d0e6..00000000000 --- a/app/views/tree/_blob_actions.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -.btn-group.tree-btn-group - -# only show edit link for text files - - if @tree.text? - = link_to "edit", edit_project_tree_path(@project, @id), class: "btn btn-tiny", disabled: !allowed_tree_edit? - = link_to "raw", project_blob_path(@project, @id), class: "btn btn-tiny", target: "_blank" - -# only show normal/blame view links for text files - - if @tree.text? - - if current_page? project_blame_path(@project, @id) - = link_to "normal view", project_tree_path(@project, @id), class: "btn btn-tiny" - - else - = link_to "blame", project_blame_path(@project, @id), class: "btn btn-tiny" - = link_to "history", project_commits_path(@project, @id), class: "btn btn-tiny" diff --git a/app/views/tree/_head.html.haml b/app/views/tree/_head.html.haml deleted file mode 100644 index 32c3882400e..00000000000 --- a/app/views/tree/_head.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -%ul.nav.nav-tabs - %li - = render partial: 'shared/ref_switcher', locals: {destination: 'tree', path: @path} - = nav_link(controller: :tree) do - = link_to 'Source', project_tree_path(@project, @ref) - %li.pull-right - = render "shared/clone_panel" diff --git a/app/views/tree/_tree.html.haml b/app/views/tree/_tree.html.haml deleted file mode 100644 index 29a2ed02d31..00000000000 --- a/app/views/tree/_tree.html.haml +++ /dev/null @@ -1,48 +0,0 @@ -%ul.breadcrumb - %li - %span.arrow - = link_to project_tree_path(@project, @ref) do - = @project.name - - tree.breadcrumbs(6) do |title, path| - \/ - %li - - if path - = link_to truncate(title, length: 40), project_tree_path(@project, path) - - else - = link_to title, '#' - -.clear -%div.tree_progress - -%div#tree-content-holder.tree-content-holder - - if tree.is_blob? - = render "tree/blob", blob: tree - - else - %table#tree-slider{class: "table_#{@hex_path} tree-table" } - %thead - %tr - %th Name - %th Last Update - %th Last Commit - %th= link_to "history", project_commits_path(@project, @id), class: "btn btn-tiny pull-right" - - - if tree.up_dir? - %tr.tree-item - %td.tree-item-file-name - = image_tag "file_empty.png", size: '16x16' - = link_to "..", project_tree_path(@project, tree.up_dir_path) - %td - %td - %td - - = render_tree(tree.contents) - - - if tree.readme - = render "tree/readme", readme: tree.readme - -- unless tree.is_blob? - :javascript - // Load last commit log for each file in tree - $(window).load(function(){ - ajaxGet('#{@logs_path}'); - }); diff --git a/app/views/tree/edit.html.haml b/app/views/tree/edit.html.haml deleted file mode 100644 index 81918e509b8..00000000000 --- a/app/views/tree/edit.html.haml +++ /dev/null @@ -1,44 +0,0 @@ -.file-editor - = form_tag(project_tree_path(@project, @id), method: :put, class: "form-horizontal") do - .file_holder - .file_title - %i.icon-file - %span.file_name - = @tree.path - %small - on - %strong= @ref - %span.options - .btn-group.tree-btn-group - = link_to "Cancel", project_tree_path(@project, @id), class: "btn btn-tiny btn-cancel", confirm: "Are you sure?" - .file_content.code - %pre#editor= @tree.data - - .control-group.commit_message-group - = label_tag 'commit_message', class: "control-label" do - Commit message - .controls - = text_area_tag 'commit_message', '', placeholder: "Update #{@tree.name}", required: true, rows: 3 - .form-actions - = hidden_field_tag 'last_commit', @last_commit - = hidden_field_tag 'content', '', id: :file_content - .commit-button-annotation - = button_tag "Commit", class: 'btn commit-btn js-commit-button' - .message - to branch - %strong= @ref - = link_to "Cancel", project_tree_path(@project, @id), class: "btn btn-cancel", confirm: "Are you sure?" - -:javascript - var ace_mode = "#{@tree.language.try(:ace_mode)}"; - var editor = ace.edit("editor"); - if (ace_mode) { - editor.getSession().setMode('ace/mode/' + ace_mode); - } - - disableButtonIfEmptyField("#commit_message", ".js-commit-button"); - - $(".js-commit-button").click(function(){ - $("#file_content").val(editor.getValue()); - $(".file-editor form").submit(); - }); diff --git a/app/views/tree/show.html.haml b/app/views/tree/show.html.haml deleted file mode 100644 index a4034f22fac..00000000000 --- a/app/views/tree/show.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -= render "head" -%div#tree-holder.tree-holder - = render "tree", tree: @tree diff --git a/app/views/tree/show.js.haml b/app/views/tree/show.js.haml deleted file mode 100644 index fadd5e2251f..00000000000 --- a/app/views/tree/show.js.haml +++ /dev/null @@ -1,10 +0,0 @@ -:plain - // Load Files list - $("#tree-holder").html("#{escape_javascript(render(partial: "tree", locals: {tree: @tree}))}"); - $("#tree-content-holder").show("slide", { direction: "right" }, 150); - $('.project-refs-form #path').val("#{@path}"); - - // Load last commit log for each file in tree - $('#tree-slider').waitForImages(function() { - ajaxGet('#{@logs_path}'); - }); diff --git a/app/views/users/_profile.html.haml b/app/views/users/_profile.html.haml index de08bc46e70..4cd1eebdf91 100644 --- a/app/views/users/_profile.html.haml +++ b/app/views/users/_profile.html.haml @@ -1,23 +1,23 @@ .ui-box - %h5.title + .title Profile %ul.well-list %li - %strong Email - %span.pull-right= mail_to user.email + %span.light Member since + %strong= user.created_at.stamp("Aug 21, 2011") - unless user.skype.blank? %li - %strong Skype - %span.pull-right= user.skype + %span.light Skype: + %strong= user.skype - unless user.linkedin.blank? %li - %strong LinkedIn - %span.pull-right= user.linkedin + %span.light LinkedIn: + %strong= user.linkedin - unless user.twitter.blank? %li - %strong Twitter - %span.pull-right= user.twitter + %span.light Twitter: + %strong= user.twitter - unless user.bio.blank? %li - %strong Bio - %span.pull-right= user.bio + %span.light Bio: + %span= user.bio diff --git a/app/views/users/_projects.html.haml b/app/views/users/_projects.html.haml index 4bee2f0c8ff..f1b2c8dd7f7 100644 --- a/app/views/users/_projects.html.haml +++ b/app/views/users/_projects.html.haml @@ -1,5 +1,5 @@ .ui-box - %h5.title Projects + .title Projects %ul.well-list - @projects.each do |project| %li @@ -9,12 +9,3 @@ \/ %strong.well-title = truncate(project.name, length: 45) - %span.pull-right.light - - if project.owner == user - %i.icon-wrench - - tm = project.team.get_tm(user.id) - - if tm - = tm.project_access_human -%p.light - %i.icon-wrench - – user is a project owner diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 9341737a5ee..743ab0949a1 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,20 +1,20 @@ .row .span8 - %h3.page_title - = image_tag gravatar_icon(@user.email, 90), class: "avatar s90" + %h3.page-title + = image_tag gravatar_icon(@user.email, 90), class: "avatar s90", alt: '' = @user.name - if @user == current_user .pull-right - = link_to profile_path, class: 'btn btn-small' do + = link_to profile_path, class: 'btn' do %i.icon-edit Edit Profile %br - %small @#{@user.username} + %small #{@user.username} %br %small member since #{@user.created_at.stamp("Nov 12, 2031")} .clearfix %hr - %h5 Recent events + %h4 User Activity: = render @events .span4 = render 'profile', user: @user diff --git a/app/views/users_groups/_users_group.html.haml b/app/views/users_groups/_users_group.html.haml new file mode 100644 index 00000000000..5cdb5bb8c40 --- /dev/null +++ b/app/views/users_groups/_users_group.html.haml @@ -0,0 +1,23 @@ +- user = member.user +- return unless user +%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)} + = image_tag gravatar_icon(user.email, 16), class: "avatar s16" + %strong= user.name + %span.cgray= user.username + - if user == current_user + %span.label.label-success It's you + + %span.pull-right + %strong= member.human_access + + - if show_controls && can?(current_user, :manage_group, @group) && current_user != user + = link_to '#', class: "btn-tiny btn js-toggle-button", title: 'Edit access level' do + %i.icon-edit + = link_to group_users_group_path(@group, member), 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.icon-minus.icon-white + + .edit-member.hide.js-toggle-content + = form_for [@group, member], remote: true do |f| + .alert.prepend-top-20 + = f.select :group_access, options_for_select(UsersGroup.group_access_roles, member.group_access) + = f.submit 'Save', class: 'btn btn-save btn-small' diff --git a/app/views/users_groups/update.js.haml b/app/views/users_groups/update.js.haml new file mode 100644 index 00000000000..5bad48abafd --- /dev/null +++ b/app/views/users_groups/update.js.haml @@ -0,0 +1,2 @@ +:plain + $("##{dom_id(@member)}").replaceWith('#{escape_javascript(render(@member, member: @member, show_controls: true))}'); diff --git a/app/views/votes/_votes_inline.html.haml b/app/views/votes/_votes_inline.html.haml index 91bd200df44..ee805474830 100644 --- a/app/views/votes/_votes_inline.html.haml +++ b/app/views/votes/_votes_inline.html.haml @@ -1,6 +1,9 @@ .votes.votes-inline - .upvotes= votable.upvotes - .progress - .bar.bar-success{style: "width: #{votable.upvotes_in_percent}%;"} - .bar.bar-danger{style: "width: #{votable.downvotes_in_percent}%;"} - .downvotes= votable.downvotes + - unless votable.upvotes.zero? + .upvotes + + #{votable.upvotes} + - unless votable.downvotes.zero? + \/ + - unless votable.downvotes.zero? + .downvotes + \- #{votable.downvotes} diff --git a/app/views/wikis/_form.html.haml b/app/views/wikis/_form.html.haml deleted file mode 100644 index 7758b129b07..00000000000 --- a/app/views/wikis/_form.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -= form_for [@project, @wiki] do |f| - -if @wiki.errors.any? - #error_explanation - %h2= "#{pluralize(@wiki.errors.count, "error")} prohibited this wiki from being saved:" - %ul - - @wiki.errors.full_messages.each do |msg| - %li= msg - - .ui-box.ui-box-show - .ui-box-head - = f.label :title - .input= f.text_field :title, class: 'span8' - = f.hidden_field :slug - .ui-box-body - .input - %span.cgray - Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. - To link to a (new) page you can just type - %code [Link Title](page-slug) - \. - - .ui-box-bottom - = f.label :content - .input= f.text_area :content, class: 'span8 js-gfm-input' - .actions - = f.submit 'Save', class: "btn-save btn" - = link_to "Cancel", project_wiki_path(@project, :index), class: "btn btn-cancel" diff --git a/app/views/wikis/history.html.haml b/app/views/wikis/history.html.haml deleted file mode 100644 index 18df8e1d71b..00000000000 --- a/app/views/wikis/history.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -%h3.page_title - %span.cgray History for - = @wiki_pages.first.title -%br -%table - %thead - %tr - %th Page version - %th Last updated - %th Updated by - %tbody - - @wiki_pages.each_with_index do |wiki_page, i| - %tr - %td - %strong - = link_to project_wiki_path(@project, wiki_page, version_id: wiki_page.id) do - Version - = @wiki_pages.count - i - %td - = wiki_page.created_at.to_s(:short) - (#{time_ago_in_words(wiki_page.created_at)} - ago) - %td= link_to_member(@project, wiki_page.user) diff --git a/app/views/wikis/pages.html.haml b/app/views/wikis/pages.html.haml deleted file mode 100644 index 2e0f091ce72..00000000000 --- a/app/views/wikis/pages.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -%h3.page_title All Pages -%br -%table - %thead - %tr - %th Title - %th Slug - %th Last updated - %th Updated by - %tbody - - @wiki_pages.each do |wiki_page| - %tr - %td - %strong= link_to wiki_page.title, project_wiki_path(@project, wiki_page) - %td= wiki_page.slug - %td - = wiki_page.created_at.to_s(:short) do - (#{time_ago_in_words(wiki_page.created_at)} - ago) - %td= link_to_member(@project, wiki_page.user) diff --git a/app/views/wikis/show.html.haml b/app/views/wikis/show.html.haml deleted file mode 100644 index 7ff8b5cc01e..00000000000 --- a/app/views/wikis/show.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -%h3.page_title - = @wiki.title - %span.pull-right - = link_to pages_project_wikis_path(@project), class: "btn btn-small grouped" do - Pages - - if can? current_user, :write_wiki, @project - = link_to history_project_wiki_path(@project, @wiki), class: "btn btn-small grouped" do - History - = link_to edit_project_wiki_path(@project, @wiki), class: "btn btn-small grouped" do - %i.icon-edit - Edit -%br -- if @wiki != @most_recent_wiki - .warning_message - This is an old version of this page. - You can view the #{link_to "most recent version", project_wiki_path(@project, @wiki)} or browse the #{link_to "history", history_project_wiki_path(@project, @wiki)}. - -.file_holder - .file_content.wiki - = preserve do - = markdown @wiki.content - -%p.time Last edited by #{link_to_member @project, @wiki.user}, #{time_ago_in_words @wiki.created_at} ago diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb index 0a921b1bd44..cfeda88bbc5 100644 --- a/app/workers/gitlab_shell_worker.rb +++ b/app/workers/gitlab_shell_worker.rb @@ -1,6 +1,6 @@ class GitlabShellWorker include Sidekiq::Worker - include Gitolited + include Gitlab::ShellAdapter sidekiq_options queue: :gitlab_shell diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 3ef6d5977cf..6416aa608ec 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -1,5 +1,6 @@ class PostReceive include Sidekiq::Worker + include Gitlab::Identifier sidekiq_options queue: :post_receive @@ -8,7 +9,7 @@ class PostReceive if repo_path.start_with?(Gitlab.config.gitlab_shell.repos_path.to_s) repo_path.gsub!(Gitlab.config.gitlab_shell.repos_path.to_s, "") else - Gitlab::GitLogger.error("POST-RECEIVE: Check gitlab.yml config for correct gitlab_shell.repos_path variable. \"#{Gitlab.config.gitlab_shell.repos_path}\" does not match \"#{repo_path}\"") + log("Check gitlab.yml config for correct gitlab_shell.repos_path variable. \"#{Gitlab.config.gitlab_shell.repos_path}\" does not match \"#{repo_path}\"") end repo_path.gsub!(/.git$/, "") @@ -17,31 +18,21 @@ class PostReceive project = Project.find_with_namespace(repo_path) if project.nil? - Gitlab::GitLogger.error("POST-RECEIVE: Triggered hook for non-existing project with full path \"#{repo_path} \"") + log("Triggered hook for non-existing project with full path \"#{repo_path} \"") return false end - user = if identifier.blank? - # Local push from gitlab - email = project.repository.commit(newrev).author.email rescue nil - User.find_by_email(email) if email - - elsif identifier =~ /\Auser-\d+\Z/ - # git push over http - user_id = identifier.gsub("user-", "") - User.find_by_id(user_id) - - elsif identifier =~ /\Akey-\d+\Z/ - # git push over ssh - key_id = identifier.gsub("key-", "") - Key.find_by_id(key_id).try(:user) - end + user = identify(identifier, project, newrev) unless user - Gitlab::GitLogger.error("POST-RECEIVE: Triggered hook for non-existing user \"#{identifier} \"") + log("Triggered hook for non-existing user \"#{identifier} \"") return false end - project.trigger_post_receive(oldrev, newrev, ref, user) + GitPushService.new.execute(project, user, oldrev, newrev, ref) + end + + def log(message) + Gitlab::GitLogger.error("POST-RECEIVE: #{message}") end end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb new file mode 100644 index 00000000000..a3b4bd0c9b5 --- /dev/null +++ b/app/workers/repository_import_worker.rb @@ -0,0 +1,22 @@ +class RepositoryImportWorker + include Sidekiq::Worker + include Gitlab::ShellAdapter + + sidekiq_options queue: :gitlab_shell + + def perform(project_id) + project = Project.find(project_id) + result = gitlab_shell.send(:import_repository, + project.path_with_namespace, + project.import_url) + + if result + project.imported = true + project.save + project.satellite.create unless project.satellite.exists? + project.discover_default_branch + else + project.imported = false + end + end +end diff --git a/config/application.rb b/config/application.rb index d71de88ebe3..8ac07ef337a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -4,7 +4,7 @@ require 'rails/all' if defined?(Bundler) # If you precompile assets before deploying to production, use this line - # Bundler.require(*Rails.groups(:assets => %w(development test))) + # Bundler.require(*Rails.groups(assets: %w(development test))) # If you want your assets lazily compiled in production, use this line Bundler.require(:default, :assets, Rails.env) end @@ -24,6 +24,7 @@ module Gitlab # Activate observers that should always be running. config.active_record.observers = :activity_observer, + :project_activity_cache_observer, :issue_observer, :key_observer, :merge_request_observer, @@ -31,6 +32,7 @@ module Gitlab :project_observer, :system_hook_observer, :user_observer, + :users_group_observer, :users_project_observer # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. @@ -66,5 +68,14 @@ module Gitlab # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' + + # Uncomment and customize the last line to run in a non-root path + # WARNING: This feature is no longer supported + # Note that three settings need to be changed for this to work. + # 1) In your application.rb file: config.relative_url_root = "/gitlab" + # 2) In your gitlab.yml file: relative_url_root: /gitlab + # 3) In your unicorn.rb: ENV['RAILS_RELATIVE_URL_ROOT'] + # + # config.relative_url_root = "/gitlab" end end diff --git a/config/aws.yml.example b/config/aws.yml.example new file mode 100644 index 00000000000..29d029b078d --- /dev/null +++ b/config/aws.yml.example @@ -0,0 +1,19 @@ +# See https://github.com/jnicklas/carrierwave#using-amazon-s3 +# for more options +production: + access_key_id: AKIA1111111111111UA + secret_access_key: secret + bucket: mygitlab.production.us + region: us-east-1 + +development: + access_key_id: AKIA1111111111111UA + secret_access_key: secret + bucket: mygitlab.development.us + region: us-east-1 + +test: + access_key_id: AKIA1111111111111UA + secret_access_key: secret + bucket: mygitlab.test.us + region: us-east-1 diff --git a/config/database.yml.mysql b/config/database.yml.mysql index 436bea77f0f..a3eff1a74f8 100644 --- a/config/database.yml.mysql +++ b/config/database.yml.mysql @@ -6,7 +6,7 @@ production: encoding: utf8 reconnect: false database: gitlabhq_production - pool: 5 + pool: 10 username: root password: "secure password" # host: localhost diff --git a/config/database.yml.postgresql b/config/database.yml.postgresql index 2bc0884f099..4b74f3348f8 100644 --- a/config/database.yml.postgresql +++ b/config/database.yml.postgresql @@ -5,8 +5,8 @@ production: adapter: postgresql encoding: unicode database: gitlabhq_production - pool: 5 - username: gitlab + pool: 10 + username: git password: # host: localhost # port: 5432 diff --git a/config/environments/production.rb b/config/environments/production.rb index 52fb8877cf3..e3476be8fba 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -40,7 +40,14 @@ Gitlab::Application.configure do # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) # Use a different cache store in production - config.cache_store = :memory_store + config_file = Rails.root.join('config', 'resque.yml') + + resque_url = if File.exists?(config_file) + YAML.load_file(config_file)[Rails.env] + else + "redis://localhost:6379" + end + config.cache_store = :redis_store, resque_url # Enable serving of images, stylesheets, and JavaScripts from an asset server # config.action_controller.asset_host = "http://assets.example.com" @@ -52,7 +59,7 @@ Gitlab::Application.configure do # config.action_mailer.raise_delivery_errors = false # Enable threaded mode - # config.threadsafe! + # config.threadsafe! unless $rails_rake_task # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation can not be found) @@ -68,8 +75,8 @@ Gitlab::Application.configure do config.action_mailer.delivery_method = :sendmail # Defaults to: # # config.action_mailer.sendmail_settings = { - # # :location => '/usr/sbin/sendmail', - # # :arguments => '-i -t' + # # location: '/usr/sbin/sendmail', + # # arguments: '-i -t' # # } config.action_mailer.perform_deliveries = true config.action_mailer.raise_delivery_errors = true diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 44154456430..c1cc9f872f3 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -1,5 +1,5 @@ -# # # # # # # # # # # # # # # # # # -# Gitlab application config file # +# # # # # # # # # # # # # # # # # # +# GitLab application config file # # # # # # # # # # # # # # # # # # # # # How to use: @@ -18,8 +18,14 @@ production: &base host: localhost port: 80 https: false - # Uncomment and customize to run in non-root path - # Note that ENV['RAILS_RELATIVE_URL_ROOT'] in config/unicorn.rb may need to be changed + + # Uncomment and customize the last line to run in a non-root path + # WARNING: This feature is no longer supported + # Note that three settings need to be changed for this to work. + # 1) In your application.rb file: config.relative_url_root = "/gitlab" + # 2) In your gitlab.yml file: relative_url_root: /gitlab + # 3) In your unicorn.rb: ENV['RAILS_RELATIVE_URL_ROOT'] + # # relative_url_root: /gitlab # Uncomment and customize if you can't use the default user to run GitLab (default: 'git') @@ -32,18 +38,69 @@ production: &base # Email address of your support contact (default: same as email_from) support_email: support@localhost - ## Project settings + ## User settings default_projects_limit: 10 + # default_can_create_group: false # default: true + # username_changing_enabled: false # default: true - User can change her username/namespace + ## Default theme + ## BASIC = 1 + ## MARS = 2 + ## MODERN = 3 + ## GRAY = 4 + ## COLOR = 5 + # default_theme: 2 # default: 2 + + + ## Users management # signup_enabled: true # default: false - Account passwords are not sent via the email if signup is enabled. + ## Automatic issue closing + # If a commit message matches this regular express, all issues referenced from the matched text will be closed + # if it's pushed to a project's default branch. + # issue_closing_pattern: ^([Cc]loses|[Ff]ixes) +#\d+ + + ## Default project features settings + default_projects_features: + issues: true + merge_requests: true + wiki: true + wall: false + snippets: false + public: false + + ## External issues trackers + issues_tracker: + # redmine: + # ## If not nil, link 'Issues' on project page will be replaced with this + # ## Use placeholders: + # ## :project_id - GitLab project identifier + # ## :issues_tracker_id - Project Name or Id in external issue tracker + # project_url: "http://redmine.sample/projects/:issues_tracker_id" + # + # ## If not nil, links from /#\d/ entities from commit messages will replaced with this + # ## Use placeholders: + # ## :project_id - GitLab project identifier + # ## :issues_tracker_id - Project Name or Id in external issue tracker + # ## :id - Issue id (from commit messages) + # issues_url: "http://redmine.sample/issues/:id" + # + # ## If not nil, linkis to creating new issues will be replaced with this + # ## Use placeholders: + # ## :project_id - GitLab project identifier + # ## :issues_tracker_id - Project Name or Id in external issue tracker + # new_issue_url: "http://redmine.sample/projects/:issues_tracker_id/issues/new" + # + # jira: + # project_url: "http://jira.sample/issues/?jql=project=:issues_tracker_id" + # issues_url: "http://jira.sample/browse/:id" + # new_issue_url: "http://jira.sample/secure/CreateIssue.jspa" + ## Gravatar gravatar: - enabled: true # Use user avatar images from Gravatar.com (default: true) + enabled: true # Use user avatar image from Gravatar.com (default: true) # plain_url: "http://..." # default: http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=mm # ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=mm - - # # 2. Auth settings # ========================== @@ -58,23 +115,23 @@ production: &base method: 'ssl' # "ssl" or "plain" bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' password: '_the_password_of_the_bind_user' + allow_username_or_email_login: true - ## Omniauth settings + ## OmniAuth settings omniauth: - # Enable ability for users - # Allow logging in via Twitter, Google, etc. using Omniauth providers + # Allow login via Twitter, Google, etc. using OmniAuth providers enabled: false # CAUTION! - # This allows users to login without having a user account first (default: false) + # This allows users to login without having a user account first (default: false). # User accounts will be created automatically when authentication was successful. allow_single_sign_on: false - # Locks down those users until they have been cleared by the admin (default: true) + # Locks down those users until they have been cleared by the admin (default: true). block_auto_created_users: true ## Auth providers - # Uncomment the lines and fill in the data of the auth provider you want to use - # If your favorite auth provider is not listed you can user others: + # Uncomment the following lines and fill in the data of the auth provider you want to use + # If your favorite auth provider is not listed you can use others: # see https://github.com/gitlabhq/gitlabhq/wiki/Using-Custom-Omniauth-Providers # The 'app_id' and 'app_secret' parameters are always passed as the first two # arguments, followed by optional 'args' which can be either a hash or an array. @@ -113,7 +170,7 @@ production: &base upload_pack: true receive_pack: true - # If you use non-standart ssh port you need to specify it + # If you use non-standard ssh port you need to specify it # ssh_port: 22 ## Git settings @@ -121,17 +178,35 @@ production: &base # Use the default values unless you really know what you are doing git: bin_path: /usr/bin/git - # Max size of git object like commit, in bytes - # This value can be increased if you have a very large commits + # Max size of a git object (e.g. a commit), in bytes + # This value can be increased if you have very large commits max_size: 5242880 # 5.megabytes - # Git timeout to read commit, in seconds + # Git timeout to read a commit, in seconds timeout: 10 + # + # 4. Extra customization + # ========================== + + extra: + ## Google analytics. Uncomment if you want it + # google_analytics_id: '_your_tracking_id' + + ## Text under sign-in page (Markdown enabled) + # sign_in_text: | + #  + # [Learn more about CompanyName](http://www.companydomain.com/) + development: <<: *base test: <<: *base + issues_tracker: + redmine: + 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" staging: <<: *base diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index b3fba99ebf3..1c8758d9420 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -37,26 +37,46 @@ 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['omniauth'] ||= Settingslogic.new({}) Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? Settings.omniauth['providers'] ||= [] +Settings['issues_tracker'] ||= {} + # # GitLab # Settings['gitlab'] ||= Settingslogic.new({}) -Settings.gitlab['default_projects_limit'] ||= 10 +Settings.gitlab['default_projects_limit'] ||= 10 +Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? +Settings.gitlab['default_theme'] = Gitlab::Theme::MARS if Settings.gitlab['default_theme'].nil? Settings.gitlab['host'] ||= 'localhost' Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? Settings.gitlab['port'] ||= Settings.gitlab.https ? 443 : 80 -Settings.gitlab['relative_url_root'] ||= '' +Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" Settings.gitlab['email_from'] ||= "gitlab@#{Settings.gitlab.host}" Settings.gitlab['support_email'] ||= Settings.gitlab.email_from Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url) Settings.gitlab['user'] ||= 'git' +Settings.gitlab['user_home'] ||= begin + Etc.getpwnam(Settings.gitlab['user']).dir +rescue ArgumentError # no user configured + '/home/' + Settings.gitlab['user'] +end Settings.gitlab['signup_enabled'] ||= false +Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? +Settings.gitlab['issue_closing_pattern'] = '^([Cc]loses|[Ff]ixes) #(\d+)' if Settings.gitlab['issue_closing_pattern'].nil? +Settings.gitlab['default_projects_features'] ||= {} +Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? +Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? +Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? +Settings.gitlab.default_projects_features['wall'] = false if Settings.gitlab.default_projects_features['wall'].nil? +Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? +Settings.gitlab.default_projects_features['public'] = false if Settings.gitlab.default_projects_features['public'].nil? # # Gravatar @@ -70,10 +90,10 @@ Settings.gravatar['ssl_url'] ||= 'https://secure.gravatar.com/avatar/%{hash}? # GitLab Shell # Settings['gitlab_shell'] ||= Settingslogic.new({}) -Settings.gitlab_shell['hooks_path'] ||= '/home/git/gitlab-shell/hooks/' +Settings.gitlab_shell['hooks_path'] ||= Settings.gitlab['user_home'] + '/gitlab-shell/hooks/' Settings.gitlab_shell['receive_pack'] = true if Settings.gitlab_shell['receive_pack'].nil? Settings.gitlab_shell['upload_pack'] = true if Settings.gitlab_shell['upload_pack'].nil? -Settings.gitlab_shell['repos_path'] ||= '/home/git/repositories/' +Settings.gitlab_shell['repos_path'] ||= Settings.gitlab['user_home'] + '/repositories/' Settings.gitlab_shell['ssh_host'] ||= (Settings.gitlab.host || 'localhost') Settings.gitlab_shell['ssh_port'] ||= 22 Settings.gitlab_shell['ssh_user'] ||= Settings.gitlab.user @@ -97,3 +117,17 @@ Settings.git['timeout'] ||= 10 Settings['satellites'] ||= Settingslogic.new({}) Settings.satellites['path'] = File.expand_path(Settings.satellites['path'] || "tmp/repo_satellites/", Rails.root) + +# +# Extra customization +# +Settings['extra'] ||= Settingslogic.new({}) + +# +# Testing settings +# +if Rails.env.test? + Settings.gitlab['default_projects_limit'] = 42 + Settings.gitlab['default_can_create_group'] = false + Settings.gitlab['default_can_create_team'] = false +end diff --git a/config/initializers/2_app.rb b/config/initializers/2_app.rb index 748f15a11d9..e2f98002347 100644 --- a/config/initializers/2_app.rb +++ b/config/initializers/2_app.rb @@ -1,8 +1,13 @@ module Gitlab - Version = File.read(Rails.root.join("VERSION")) - Revision = `git log --pretty=format:'%h' -n 1` + VERSION = File.read(Rails.root.join("VERSION")).strip + REVISION = `git log --pretty=format:'%h' -n 1` def self.config Settings end end + +# +# Load all libs for threadsafety +# +Dir["#{Rails.root}/lib/**/*.rb"].each { |file| require file } diff --git a/config/initializers/3_grit_ext.rb b/config/initializers/3_grit_ext.rb index 097c301a06a..8b298e821e7 100644 --- a/config/initializers/3_grit_ext.rb +++ b/config/initializers/3_grit_ext.rb @@ -1,9 +1,6 @@ require 'grit' require 'pygments' +Grit::Git.git_binary = Gitlab.config.git.bin_path Grit::Git.git_timeout = Gitlab.config.git.timeout Grit::Git.git_max_size = Gitlab.config.git.max_size - -Grit::Blob.class_eval do - include Linguist::BlobHelper -end diff --git a/config/initializers/4_sidekiq.rb b/config/initializers/4_sidekiq.rb index 6abe6e74fa0..c90d376273d 100644 --- a/config/initializers/4_sidekiq.rb +++ b/config/initializers/4_sidekiq.rb @@ -4,19 +4,19 @@ config_file = Rails.root.join('config', 'resque.yml') resque_url = if File.exists?(config_file) YAML.load_file(config_file)[Rails.env] else - "localhost:6379" + "redis://localhost:6379" end Sidekiq.configure_server do |config| config.redis = { - url: "redis://#{resque_url}", + url: resque_url, namespace: 'resque:gitlab' } end Sidekiq.configure_client do |config| config.redis = { - url: "redis://#{resque_url}", + url: resque_url, namespace: 'resque:gitlab' } end diff --git a/config/initializers/5_backend.rb b/config/initializers/5_backend.rb index 73436608c93..e60d9559c94 100644 --- a/config/initializers/5_backend.rb +++ b/config/initializers/5_backend.rb @@ -3,3 +3,9 @@ require Rails.root.join("lib", "gitlab", "backend", "grack_auth") # GIT over SSH require Rails.root.join("lib", "gitlab", "backend", "shell") + +# GitLab shell adapter +require Rails.root.join("lib", "gitlab", "backend", "shell_adapter") + +# Gitlab Git repos path +Gitlab::Git::Repository.repos_path = Gitlab.config.gitlab_shell.repos_path diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index 9bb62f6d55a..45bc68f3220 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -1 +1,19 @@ CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/ + +aws_file = Rails.root.join('config', 'aws.yml') + +if File.exists?(aws_file) + AWS_CONFIG = YAML.load(File.read(aws_file))[Rails.env] + + CarrierWave.configure do |config| + config.fog_credentials = { + provider: 'AWS', # required + aws_access_key_id: AWS_CONFIG['access_key_id'], # required + aws_secret_access_key: AWS_CONFIG['secret_access_key'], # required + region: AWS_CONFIG['region'], # optional, defaults to 'us-east-1' + } + config.fog_directory = AWS_CONFIG['bucket'] # required + config.fog_public = false # optional, defaults to true + config.fog_attributes = {'Cache-Control'=>'max-age=315576000'} # optional, defaults to {} + end +end diff --git a/config/initializers/connection_fix.rb b/config/initializers/connection_fix.rb deleted file mode 100644 index 16cb69ca68b..00000000000 --- a/config/initializers/connection_fix.rb +++ /dev/null @@ -1,36 +0,0 @@ -# from http://gist.github.com/238999 -# -# If your workers are inactive for a long period of time, they'll lose -# their MySQL connection. -# -# This hack ensures we re-connect whenever a connection is -# lost. Because, really. why not? -# -# Stick this in RAILS_ROOT/config/initializers/connection_fix.rb (or somewhere similar) -# -# From: -# http://coderrr.wordpress.com/2009/01/08/activerecord-threading-issues-and-resolutions/ - -if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) - - module ActiveRecord::ConnectionAdapters - - class Mysql2Adapter - alias_method :execute_without_retry, :execute - - def execute(*args) - execute_without_retry(*args) - rescue ActiveRecord::StatementInvalid => e - if e.message =~ /server has gone away/i - warn "Server timed out, retrying" - reconnect! - retry - else - raise e - end - end - end - - end - -end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 97946c54b40..39c1b7c235b 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -23,7 +23,7 @@ Devise.setup do |config| # session. If you need permissions, you should implement that in a before filter. # You can also supply a hash where the value is a boolean determining whether # or not authentication should be aborted when the value is not present. - # config.authentication_keys = [ :email ] + config.authentication_keys = [ :login ] # Configure parameters from the request object used for authentication. Each entry # given should be a request method and it will automatically be passed to the @@ -94,12 +94,12 @@ Devise.setup do |config| # config.extend_remember_period = false # Options to be passed to the created cookie. For instance, you can set - # :secure => true in order to force SSL only cookies. + # secure: true in order to force SSL only cookies. # config.cookie_options = {} # ==> Configuration for :validatable # Range for password length. Default is 6..128. - # config.password_length = 6..128 + config.password_length = 6..128 # Email regex used to validate email formats. It simply asserts that # an one (and only one) @ exists in the given string. This is mainly @@ -202,18 +202,25 @@ Devise.setup do |config| # config.warden do |manager| # manager.failure_app = AnotherApp # manager.intercept_401 = false - # manager.default_strategies(:scope => :user).unshift :some_external_strategy + # 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} + 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'] + 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/haml.rb b/config/initializers/haml.rb new file mode 100644 index 00000000000..7e8ddb3716b --- /dev/null +++ b/config/initializers/haml.rb @@ -0,0 +1 @@ +Haml::Template.options[:ugly] = true diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 6d3a9f07787..16d1d4a9fdd 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -1,7 +1,23 @@ # Be sure to restart your server when you modify this file. +require 'securerandom' + # Your secret key for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -Gitlab::Application.config.secret_token = '0a38e9a40ca5d66d7002a6ade0ed0f8b71058c820163f66cf65d91521ab55255ff708b9909b138008a7f13d68fec575def1dc3ff7200cd72b065896315e0bed2' + +def find_secure_token + token_file = Rails.root.join('.secret') + if File.exist? token_file + # Use the existing token. + File.read(token_file).chomp + else + # Generate a new token of 64 random hexadecimal characters and store it in token_file. + token = SecureRandom.hex(64) + File.write(token_file, token) + token + end +end + +Gitlab::Application.config.secret_token = find_secure_token diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index e777ae2b78d..52a099c3e16 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -2,7 +2,8 @@ Gitlab::Application.config.session_store :cookie_store, key: '_gitlab_session', secure: Gitlab::Application.config.force_ssl, - httponly: true + httponly: true, + path: (Rails.application.config.relative_url_root.nil?) ? '/' : Rails.application.config.relative_url_root # Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information diff --git a/config/initializers/smtp_settings.rb.sample b/config/initializers/smtp_settings.rb.sample new file mode 100644 index 00000000000..e62ad0f4b71 --- /dev/null +++ b/config/initializers/smtp_settings.rb.sample @@ -0,0 +1,18 @@ +# To enable smtp email delivery for your GitLab instance do next: +# 1. Change config/environments/production.rb to use smtp +# config.action_mailer.delivery_method = :smtp +# 2. Rename this file to smtp_settings.rb +# 3. Edit settings inside this file +# 4. Restart GitLab instance +# +if Gitlab::Application.config.action_mailer.delivery_method == :smtp + ActionMailer::Base.smtp_settings = { + address: "email.server.com", + port: 456, + user_name: "smtp", + password: "123456", + domain: "gitlab.company.com", + authentication: :login, + enable_starttls_auto: true + } +end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 3b763cf410d..275273a0b12 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -17,6 +17,7 @@ en: unauthenticated: 'You need to sign in before continuing.' unconfirmed: 'You have to confirm your account before continuing.' locked: 'Your account is locked.' + not_found_in_database: 'Invalid email or password.' invalid: 'Invalid email or password.' invalid_token: 'Invalid authentication token.' timeout: 'Your session expired, please sign in again to continue.' diff --git a/config/resque.yml.example b/config/resque.yml.example index cd3d487408a..3c7ad0e5778 100644 --- a/config/resque.yml.example +++ b/config/resque.yml.example @@ -1,3 +1,3 @@ -development: localhost:6379 -test: localhost:6379 -production: redis.example.com:6379 +development: redis://localhost:6379 +test: redis://localhost:6379 +production: redis://redis.example.com:6379 diff --git a/config/routes.rb b/config/routes.rb index 88667db130e..9d47faa19d5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ require 'sidekiq/web' +require 'api/api' Gitlab::Application.routes.draw do # @@ -7,8 +8,8 @@ Gitlab::Application.routes.draw do get 'search' => "search#show" # API - require 'api' - mount Gitlab::API => '/api' + API::API.logger Rails.logger + mount API::API => '/api' constraint = lambda { |request| request.env["warden"].authenticate? and request.env['warden'].user.admin? } constraints constraint do @@ -28,6 +29,7 @@ Gitlab::Application.routes.draw do # get 'help' => 'help#index' get 'help/api' => 'help#api' + get 'help/api/:category' => 'help#api', as: 'help_api_file' get 'help/markdown' => 'help#markdown' get 'help/permissions' => 'help#permissions' get 'help/public_access' => 'help#public_access' @@ -36,6 +38,17 @@ Gitlab::Application.routes.draw do get 'help/system_hooks' => 'help#system_hooks' get 'help/web_hooks' => 'help#web_hooks' get 'help/workflow' => 'help#workflow' + get 'help/shortcuts' + + # + # Global snippets + # + resources :snippets do + member do + get "raw" + end + end + get "/s/:username" => "snippets#user_index", as: :user_snippets, constraints: { username: /.*/ } # # Public namespace @@ -46,6 +59,11 @@ Gitlab::Application.routes.draw do end # + # Attachments serving + # + get 'files/:type/:id/:filename' => 'files#download', constraints: { id: /\d+/, type: /[a-z]+/, filename: /.+/ } + + # # Admin Area # namespace :admin do @@ -59,16 +77,7 @@ Gitlab::Application.routes.draw do resources :groups, constraints: { id: /[^\/]+/ } do member do - put :project_update put :project_teams_update - delete :remove_project - end - end - - resources :teams, constraints: { id: /[^\/]+/ } do - scope module: :teams do - resources :members, only: [:edit, :update, :destroy, :new, :create] - resources :projects, only: [:edit, :update, :destroy, :new, :create], constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } end end @@ -77,18 +86,8 @@ Gitlab::Application.routes.draw do end resource :logs, only: [:show] - resource :resque, controller: 'resque', only: [:show] - - resources :projects, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ }, except: [:new, :create] do - member do - get :team - put :team_update - end - scope module: :projects, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do - resources :members, only: [:edit, :update, :destroy] - end - end - + resource :background_jobs, controller: 'background_jobs', only: [:show] + resources :projects, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ }, only: [:index, :show] root to: "dashboard#index" end @@ -108,9 +107,19 @@ Gitlab::Application.routes.draw do put :reset_private_token put :update_username end + + scope module: :profiles do + resource :notifications, only: [:show, :update] + resource :password, only: [:new, :create] + resources :keys + resources :groups, only: [:index] do + member do + delete :leave + end + end + end end - resources :keys match "/u/:username" => "users#show", as: :user, constraints: { username: /.*/ } @@ -118,7 +127,7 @@ Gitlab::Application.routes.draw do # # Dashboard Area # - resource :dashboard, controller: "dashboard" do + resource :dashboard, controller: "dashboard", only: [:show] do member do get :projects get :issues @@ -129,28 +138,14 @@ Gitlab::Application.routes.draw do # # Groups Area # - resources :groups, constraints: { id: /[^\/]+/ } do + resources :groups, constraints: {id: /(?:[^.]|\.(?!atom$))+/, format: /atom/} do member do get :issues get :merge_requests - get :search - get :people - post :team_members + get :members end - end - # - # Teams Area - # - resources :teams, constraints: { id: /[^\/]+/ } do - member do - get :issues - get :merge_requests - end - scope module: :teams do - resources :members, only: [:index, :new, :create, :edit, :update, :destroy] - resources :projects, only: [:index, :new, :create, :edit, :update, :destroy], constraints: { id: /[a-zA-Z.0-9_\-\/]+/ } - end + resources :users_groups, only: [:create, :update, :destroy] end resources :projects, constraints: { id: /[^\/]+/ }, only: [:new, :create] @@ -160,130 +155,150 @@ Gitlab::Application.routes.draw do # # Project Area # - resources :projects, constraints: { id: /[a-zA-Z.0-9_\-\/]+/ }, except: [:new, :create, :index], path: "/" do + resources :projects, constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/ }, except: [:new, :create, :index], path: "/" do member do - get "wall" - get "files" + put :transfer + post :fork + get :autocomplete_sources end - resources :blob, only: [:show], constraints: {id: /.+/} - resources :tree, only: [:show, :edit, :update], constraints: {id: /.+/} - resources :commit, only: [:show], constraints: {id: /[[:alnum:]]{6,40}/} - resources :commits, only: [:show], constraints: {id: /.+/} - resources :compare, only: [:index, :create] - resources :blame, only: [:show], constraints: {id: /.+/} - resources :graph, only: [:show], constraints: {id: /.+/} - match "/compare/:from...:to" => "compare#show", as: "compare", - :via => [:get, :post], constraints: {from: /.+/, to: /.+/} - - resources :wikis, only: [:show, :edit, :destroy, :create] do - collection do - get :pages - end + scope module: :projects do + resources :blob, only: [:show], constraints: {id: /.+/} + resources :raw, only: [:show], constraints: {id: /.+/} + resources :tree, only: [:show], constraints: {id: /.+/, format: /(html|js)/ } + resources :edit_tree, only: [:show, :update], constraints: {id: /.+/}, path: 'edit' + resources :commit, only: [:show], constraints: {id: /[[:alnum:]]{6,40}/} + resources :commits, only: [:show], constraints: {id: /(?:[^.]|\.(?!atom$))+/, format: /atom/} + resources :compare, only: [:index, :create] + resources :blame, only: [:show], constraints: {id: /.+/} + resources :network, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/} + resources :graphs, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/} + 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 + end - member do - get "history" - end - end + resources :wikis, only: [:show, :edit, :destroy, :create], constraints: {id: /[a-zA-Z.0-9_\-]+/} do + collection do + get :pages + put ':id' => 'wikis#update' + get :git_access + end - resource :repository do - member do - get "branches" - get "tags" - get "stats" - get "archive" + member do + get "history" + end end - end - resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do - member do - get :test + resource :wall, only: [:show], constraints: {id: /\d+/} do + member do + get 'notes' + end end - end - resources :deploy_keys - resources :protected_branches, only: [:index, :create, :destroy] + resource :repository, only: [:show] do + member do + get "stats" + get "archive" + end + end - resources :refs, only: [] do - collection do - get "switch" + resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do + member do + get :test + end end - member do - # tree viewer logs - get "logs_tree", constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } - get "logs_tree/:path" => "refs#logs_tree", - as: :logs_file, - constraints: { - id: /[a-zA-Z.0-9\/_\-]+/, - path: /.*/ - } + resources :deploy_keys, constraints: {id: /\d+/} do + member do + put :enable + put :disable + end end - end - resources :merge_requests, constraints: {id: /\d+/}, except: [:destroy] do - member do - get :diffs - get :automerge - get :automerge_check - get :ci_status + resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } do + collection do + get :recent + end end - collection do - get :branch_from - get :branch_to + resources :tags, only: [:index, :new, :create, :destroy], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } + resources :protected_branches, only: [:index, :create, :destroy], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } + + resources :refs, only: [] do + collection do + get "switch" + end + + member do + # tree viewer logs + get "logs_tree", constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } + get "logs_tree/:path" => "refs#logs_tree", + as: :logs_file, + constraints: { + id: /[a-zA-Z.0-9\/_\-#%+]+/, + path: /.*/ + } + end end - end - resources :snippets do - member do - get "raw" + resources :merge_requests, constraints: {id: /\d+/}, except: [:destroy] do + member do + get :diffs + get :automerge + get :automerge_check + get :ci_status + end + + collection do + get :branch_from + get :branch_to + get :update_branches + end end - end - resources :hooks, only: [:index, :create, :destroy] do - member do - get :test + resources :hooks, only: [:index, :create, :destroy], constraints: {id: /\d+/} do + member do + get :test + end end - end + resources :team, controller: 'team_members', only: [:index] + resources :milestones, except: [:destroy], constraints: {id: /\d+/} - resources :team, controller: 'team_members', only: [:index] - resources :milestones, except: [:destroy] - resources :labels, only: [:index] - resources :issues, except: [:destroy] do - collection do - post :sort - post :bulk_update - get :search + resources :labels, only: [:index] do + collection do + post :generate + end end - end - - resources :team_members do - collection do - # Used for import team - # from another project - get :import - post :apply_import + resources :issues, constraints: {id: /\d+/}, except: [:destroy] do + collection do + post :bulk_update + end end - end - scope module: :projects do - resources :teams, only: [] do + resources :team_members, except: [:index, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } do collection do - get :available - post :assign + + # Used for import team + # from another project + get :import + post :apply_import end + end + + resources :notes, only: [:index, :create, :destroy, :update], constraints: {id: /\d+/} do member do - delete :resign + delete :delete_attachment end - end - end - resources :notes, only: [:index, :create, :destroy] do - collection do - post :preview + collection do + post :preview + end end end end diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example index 123033486f4..5c933df073a 100644 --- a/config/unicorn.rb.example +++ b/config/unicorn.rb.example @@ -1,68 +1,102 @@ -# uncomment and customize to run in non-root path -# note that config/gitlab.yml web path should also be changed -# ENV['RAILS_RELATIVE_URL_ROOT'] = "/gitlab" +# Sample verbose configuration file for Unicorn (not Rack) +# +# This configuration file documents many features of Unicorn +# that may not be needed for some applications. See +# http://unicorn.bogomips.org/examples/unicorn.conf.minimal.rb +# for a much simpler configuration file. +# +# See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete +# documentation. -app_dir = File.expand_path '../../', __FILE__ +# Use at least one worker per core if you're on a dedicated server, +# more will usually help for _short_ waits on databases/caches. worker_processes 2 -working_directory app_dir -# Load app into the master before forking workers for super-fast -# worker spawn times -preload_app true +# Since Unicorn is never exposed to outside clients, it does not need to +# run on the standard HTTP port (80), there is no reason to start Unicorn +# as root unless it's from system init scripts. +# If running the master process as root and the workers as an unprivileged +# user, do this to switch euid/egid in the workers (also chowns logs): +# user "unprivileged_user", "unprivileged_group" -# nuke workers after 30 seconds (60 is the default) -timeout 30 +# Help ensure your application will always spawn in the symlinked +# "current" directory that Capistrano sets up. +working_directory "/home/git/gitlab" # available in 0.94.0+ -# listen on a Unix domain socket and/or a TCP port, +# listen on both a Unix domain socket and a TCP port, +# we use a shorter backlog for quicker failover when busy +listen "/home/git/gitlab/tmp/sockets/gitlab.socket", :backlog => 64 +listen "127.0.0.1:8080", :tcp_nopush => true -#listen 8080 # listen to port 8080 on all TCP interfaces -#listen "127.0.0.1:8080" # listen to port 8080 on the loopback interface -listen "#{app_dir}/tmp/sockets/gitlab.socket" +# nuke workers after 30 seconds instead of 60 seconds (the default) +timeout 30 -pid "#{app_dir}/tmp/pids/unicorn.pid" -stderr_path "#{app_dir}/log/unicorn.stderr.log" -stdout_path "#{app_dir}/log/unicorn.stdout.log" +# feel free to point this anywhere accessible on the filesystem +pid "/home/git/gitlab/tmp/pids/unicorn.pid" -# http://www.rubyenterpriseedition.com/faq.html#adapt_apps_for_cow -if GC.respond_to?(:copy_on_write_friendly=) +# By default, the Unicorn logger will write to stderr. +# Additionally, some applications/frameworks log to stderr or stdout, +# so prevent them from going to /dev/null when daemonized here: +stderr_path "/home/git/gitlab/log/unicorn.stderr.log" +stdout_path "/home/git/gitlab/log/unicorn.stdout.log" + +# combine Ruby 2.0.0dev or REE with "preload_app true" for memory savings +# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow +preload_app true +GC.respond_to?(:copy_on_write_friendly=) and GC.copy_on_write_friendly = true -end +# Enable this flag to have unicorn test client connections by writing the +# beginning of the HTTP headers before calling the application. This +# prevents calling the application for connections that have disconnected +# while queued. This is only guaranteed to detect clients on the same +# host unicorn runs on, and unlikely to detect disconnects even on a +# fast LAN. +check_client_connection false before_fork do |server, worker| # the following is highly recomended for Rails + "preload_app true" # as there's no need for the master process to hold a connection - defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect! + defined?(ActiveRecord::Base) and + ActiveRecord::Base.connection.disconnect! - ## - # When sent a USR2, Unicorn will suffix its pidfile with .oldbin and - # immediately start loading up a new version of itself (loaded with a new - # version of our app). When this new Unicorn is completely loaded - # it will begin spawning workers. The first worker spawned will check to - # see if an .oldbin pidfile exists. If so, this means we've just booted up - # a new Unicorn and need to tell the old one that it can now die. To do so - # we send it a QUIT. + # The following is only recommended for memory/DB-constrained + # installations. It is not needed if your system can house + # twice as many worker_processes as you have configured. # - # Using this method we get 0 downtime deploys. - + # This allows a new master process to incrementally + # phase out the old master process with SIGTTOU to avoid a + # thundering herd (especially in the "preload_app false" case) + # when doing a transparent upgrade. The last worker spawned + # will then kill off the old master process with a SIGQUIT. old_pid = "#{server.config[:pid]}.oldbin" - - if File.exists?(old_pid) && server.pid != old_pid + if old_pid != server.pid begin sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU Process.kill(sig, File.read(old_pid).to_i) rescue Errno::ENOENT, Errno::ESRCH - # someone else did our job for us end end + # + # Throttle the master from forking too quickly by sleeping. Due + # to the implementation of standard Unix signal handlers, this + # helps (but does not completely) prevent identical, repeated signals + # from being lost when the receiving process is busy. + # sleep 1 end after_fork do |server, worker| - # Unicorn master loads the app then forks off workers - because of the way - # Unix forking works, we need to make sure we aren't using any of the parent's - # sockets, e.g. db connection + # per-process listener ports for debugging/admin/migrations + # addr = "127.0.0.1:#{9293 + worker.nr}" + # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true) + + # the following is *required* for Rails + "preload_app true", + defined?(ActiveRecord::Base) and + ActiveRecord::Base.establish_connection - defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection - # Redis and Memcached would go here but their connections are established - # on demand, so the master never opens a socket + # if preload_app is true, then you may also want to check and + # restart any other shared sockets/descriptors such as Memcached, + # and Redis. TokyoCabinet file handles are safe to reuse + # between any number of forked children (assuming your kernel + # correctly implements pread()/pwrite() system calls) end diff --git a/db/fixtures/development/01_admin.rb b/db/fixtures/development/01_admin.rb index fbe41e4d22d..ecea8211393 100644 --- a/db/fixtures/development/01_admin.rb +++ b/db/fixtures/development/01_admin.rb @@ -7,5 +7,7 @@ User.seed(:id, [ password: "5iveL!fe", password_confirmation: "5iveL!fe", admin: true, + projects_limit: 100, + theme_id: Gitlab::Theme::MARS } ]) diff --git a/db/fixtures/development/02_source_code.rb b/db/fixtures/development/02_source_code.rb deleted file mode 100644 index 4a9e5d0c258..00000000000 --- a/db/fixtures/development/02_source_code.rb +++ /dev/null @@ -1,29 +0,0 @@ -root = Gitlab.config.gitolite.repos_path - -projects = [ - { path: 'underscore.git', git: 'https://github.com/documentcloud/underscore.git' }, - { path: 'diaspora.git', git: 'https://github.com/diaspora/diaspora.git' }, - { path: 'brightbox/brightbox-cli.git', git: 'https://github.com/brightbox/brightbox-cli.git' }, - { path: 'brightbox/puppet.git', git: 'https://github.com/brightbox/puppet.git' }, - { path: 'gitlab/gitlabhq.git', git: 'https://github.com/gitlabhq/gitlabhq.git' }, - { path: 'gitlab/gitlab-ci.git', git: 'https://github.com/gitlabhq/gitlab-ci.git' }, - { path: 'gitlab/gitlab-recipes.git', git: 'https://github.com/gitlabhq/gitlab-recipes.git' }, -] - -projects.each do |project| - project_path = File.join(root, project[:path]) - - if File.exists?(project_path) - print '-' - next - end - - if system("/home/git/gitlab-shell/bin/gitlab-projects import-project #{project[:path]} #{project[:git]}") - print '.' - else - print 'F' - end -end - -puts "OK".green - diff --git a/db/fixtures/development/03_group.rb b/db/fixtures/development/03_group.rb deleted file mode 100644 index 01174a4b72a..00000000000 --- a/db/fixtures/development/03_group.rb +++ /dev/null @@ -1,5 +0,0 @@ -Group.seed(:id, [ - { id: 99, name: "GitLab", path: 'gitlab', owner_id: 1 }, - { id: 100, name: "Brightbox", path: 'brightbox', owner_id: 1 }, - { id: 101, name: "KDE", path: 'kde', owner_id: 1 }, -]) diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index 9904c48e518..43178dee25d 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -1,20 +1,49 @@ -Project.seed(:id, [ +project_urls = [ + 'https://github.com/documentcloud/underscore.git', + 'https://github.com/diaspora/diaspora.git', + 'https://github.com/diaspora/diaspora-project-site.git', + 'https://github.com/diaspora/diaspora-client.git', + 'https://github.com/brightbox/brightbox-cli.git', + 'https://github.com/brightbox/puppet.git', + 'https://github.com/gitlabhq/gitlabhq.git', + 'https://github.com/gitlabhq/gitlab-ci.git', + 'https://github.com/gitlabhq/gitlab-recipes.git', + 'https://github.com/gitlabhq/gitlab-shell.git', + 'https://github.com/gitlabhq/grack.git', + 'https://github.com/twitter/flight.git', + 'https://github.com/twitter/typeahead.js.git', + 'https://github.com/h5bp/html5-boilerplate.git', + 'https://github.com/h5bp/mobile-boilerplate.git', +] - # Global - { id: 1, name: "Underscore.js", path: "underscore", creator_id: 1 }, - { id: 2, name: "Diaspora", path: "diaspora", creator_id: 1 }, +project_urls.each_with_index do |url, i| + group_path, project_path = url.split('/')[-2..-1] - # Brightbox - { id: 3, namespace_id: 100, name: "Brightbox CLI", path: "brightbox-cli", creator_id: 1 }, - { id: 4, namespace_id: 100, name: "Puppet", path: "puppet", creator_id: 1 }, + group = Group.find_by_path(group_path) - # KDE - { id: 5, namespace_id: 101, name: "kdebase", path: "kdebase", creator_id: 1}, - { id: 6, namespace_id: 101, name: "kdelibs", path: "kdelibs", creator_id: 1}, - { id: 7, namespace_id: 101, name: "amarok", path: "amarok", creator_id: 1}, + unless group + group = Group.new( + name: group_path.titleize, + path: group_path + ) + group.owner = User.first + group.save + end - # GitLab - { id: 8, namespace_id: 99, name: "gitlabhq", path: "gitlabhq", creator_id: 1}, - { id: 9, namespace_id: 99, name: "gitlab-ci", path: "gitlab-ci", creator_id: 1}, - { id: 10, namespace_id: 99, name: "gitlab-recipes", path: "gitlab-recipes", creator_id: 1}, -]) + project_path.gsub!(".git", "") + + params = { + import_url: url, + namespace_id: group.id, + name: project_path.titleize + } + + project = Projects::CreateContext.new(User.first, params).execute + + if project.valid? + print '.' + else + puts project.errors.full_messages + print 'F' + end +end diff --git a/db/fixtures/development/05_users.rb b/db/fixtures/development/05_users.rb index abcb0259618..cbb3e636acc 100644 --- a/db/fixtures/development/05_users.rb +++ b/db/fixtures/development/05_users.rb @@ -1,5 +1,5 @@ Gitlab::Seeder.quiet do - (2..300).each do |i| + (2..50).each do |i| begin User.seed(:id, [{ id: i, diff --git a/db/fixtures/development/06_teams.rb b/db/fixtures/development/06_teams.rb index 9fbf21a02d7..a1e01879db5 100644 --- a/db/fixtures/development/06_teams.rb +++ b/db/fixtures/development/06_teams.rb @@ -1,22 +1,23 @@ -Gitlab::Seeder.quiet do - - (1..300).each do |i| - # Random Project - project = Project.scoped.sample - - # Random user - user = User.not_in_project(project).sample +ActiveRecord::Base.observers.disable :all - next unless user - - UsersProject.seed(:id, [{ - id: i, - project_id: project.id, - user_id: user.id, - project_access: UsersProject.access_roles.values.sample - }]) +Gitlab::Seeder.quiet do + Group.all.each do |group| + User.all.sample(4).each do |user| + if group.add_users([user.id], UsersGroup.group_access_roles.values.sample) + print '.' + else + print 'F' + end + end + end - print('.') + Project.all.each do |project| + User.all.sample(4).each do |user| + if project.team << [user, UsersProject.access_roles.values.sample] + print '.' + else + print 'F' + end + end end end -puts "OK".green diff --git a/db/fixtures/development/07_milestones.rb b/db/fixtures/development/07_milestones.rb index a77f619f995..6fe7e246770 100644 --- a/db/fixtures/development/07_milestones.rb +++ b/db/fixtures/development/07_milestones.rb @@ -1,13 +1,18 @@ Milestone.seed(:id, [ - { :id => 1, :project_id => 1, :title => 'v' + Faker::Address.zip_code }, - { :id => 2, :project_id => 1, :title => 'v' + Faker::Address.zip_code }, - { :id => 3, :project_id => 1, :title => 'v' + Faker::Address.zip_code }, - { :id => 4, :project_id => 2, :title => 'v' + Faker::Address.zip_code }, - { :id => 5, :project_id => 2, :title => 'v' + Faker::Address.zip_code }, + { id: 1, project_id: 1, title: 'v' + Faker::Address.zip_code }, + { id: 2, project_id: 1, title: 'v' + Faker::Address.zip_code }, + { id: 3, project_id: 1, title: 'v' + Faker::Address.zip_code }, + { id: 4, project_id: 2, title: 'v' + Faker::Address.zip_code }, + { id: 5, project_id: 2, title: 'v' + Faker::Address.zip_code }, - { :id => 6, :project_id => 2, :title => 'v' + Faker::Address.zip_code }, - { :id => 7, :project_id => 2, :title => 'v' + Faker::Address.zip_code }, - { :id => 8, :project_id => 3, :title => 'v' + Faker::Address.zip_code }, - { :id => 9, :project_id => 3, :title => 'v' + Faker::Address.zip_code }, - { :id => 11, :project_id => 3, :title => 'v' + Faker::Address.zip_code }, + { id: 6, project_id: 2, title: 'v' + Faker::Address.zip_code }, + { id: 7, project_id: 2, title: 'v' + Faker::Address.zip_code }, + { id: 8, project_id: 3, title: 'v' + Faker::Address.zip_code }, + { id: 9, project_id: 3, title: 'v' + Faker::Address.zip_code }, + { id: 11, project_id: 3, title: 'v' + Faker::Address.zip_code }, ]) + +Milestone.all.map do |ml| + ml.set_iid + ml.save +end diff --git a/db/fixtures/development/09_issues.rb b/db/fixtures/development/09_issues.rb index 8978db4742b..31ba77254a3 100644 --- a/db/fixtures/development/09_issues.rb +++ b/db/fixtures/development/09_issues.rb @@ -1,3 +1,5 @@ +ActiveRecord::Base.observers.disable :all + Gitlab::Seeder.quiet do (1..300).each do |i| # Random Project @@ -9,17 +11,22 @@ Gitlab::Seeder.quiet do next unless user user_id = user.id - IssueObserver.current_user = user + Thread.current[:current_user] = user Issue.seed(:id, [{ id: i, project_id: project.id, author_id: user_id, assignee_id: user_id, - closed: [true, false].sample, + state: ['opened', 'closed'].sample, milestone: project.milestones.sample, title: Faker::Lorem.sentence(6) }]) print('.') end + + Issue.all.map do |issue| + issue.set_iid + issue.save + end end diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index 9904b4a1505..1e61ea28636 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -1,5 +1,7 @@ +ActiveRecord::Base.observers.disable :all + Gitlab::Seeder.quiet do - (1..300).each do |i| + (1..100).each do |i| # Random Project project = Project.all.sample @@ -8,19 +10,37 @@ Gitlab::Seeder.quiet do next unless user + next if project.empty_repo? + + branches = project.repository.branch_names.sample(2) + + next if branches.uniq.size < 2 + user_id = user.id - MergeRequestObserver.current_user = user + Thread.current[:current_user] = user + MergeRequest.seed(:id, [{ id: i, - source_branch: 'master', - target_branch: 'feature', - project_id: project.id, + source_branch: branches.first, + target_branch: branches.last, + source_project_id: project.id, + target_project_id: project.id, author_id: user_id, assignee_id: user_id, - closed: [true, false].sample, milestone: project.milestones.sample, title: Faker::Lorem.sentence(6) }]) print('.') end end + +MergeRequest.all.map do |mr| + mr.set_iid + mr.save +end + +puts 'Load diffs for Merge Requests (it will take some time)...' +MergeRequest.all.each do |mr| + mr.reload_code + print '.' +end diff --git a/db/fixtures/development/11_keys.rb b/db/fixtures/development/11_keys.rb index 8e4724c277c..4b53ff411f2 100644 --- a/db/fixtures/development/11_keys.rb +++ b/db/fixtures/development/11_keys.rb @@ -1,3 +1,4 @@ +ActiveRecord::Base.observers.enable :all Gitlab::Seeder.quiet do User.first(30).each_with_index do |user, i| diff --git a/db/fixtures/development/12_snippets.rb b/db/fixtures/development/12_snippets.rb new file mode 100644 index 00000000000..4ca8afe294e --- /dev/null +++ b/db/fixtures/development/12_snippets.rb @@ -0,0 +1,24 @@ +ActiveRecord::Base.observers.disable :all + +Gitlab::Seeder.quiet do + contents = [ + `curl https://gist.github.com/randx/4275756/raw/da2f262920c96d1a970d48bf2e99147954b1f4bd/glus1204.sh`, + `curl https://gist.github.com/randx/3754594/raw/11026a295e6ef3a151c635707a3e1e8e15fc4725/gitlab_setup.sh `, + `curl https://gist.github.com/randx/3065552/raw/29fbd09f4605a5ea22a5a9095e35fd1938dea4d6/gistfile1.sh`, + ] + + (1..50).each do |i| + user = User.all.sample + + PersonalSnippet.seed(:id, [{ + 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, + }]) + print('.') + end +end + diff --git a/db/fixtures/production/001_admin.rb b/db/fixtures/production/001_admin.rb index f119694d11d..1b77d94905d 100644 --- a/db/fixtures/production/001_admin.rb +++ b/db/fixtures/production/001_admin.rb @@ -3,7 +3,10 @@ admin = User.create( name: "Administrator", username: 'root', password: "5iveL!fe", - password_confirmation: "5iveL!fe" + password_confirmation: "5iveL!fe", + password_expires_at: Time.now, + theme_id: Gitlab::Theme::MARS + ) admin.projects_limit = 10000 diff --git a/db/fixtures/test/001_repo.rb b/db/fixtures/test/001_repo.rb index 18fc37cde0c..281e3476df1 100644 --- a/db/fixtures/test/001_repo.rb +++ b/db/fixtures/test/001_repo.rb @@ -19,5 +19,18 @@ FileUtils.cd(REPO_PATH) do # Remove the copy FileUtils.rm(SEED_REPO) end +puts ' done.' +print "Creating seed satellite..." + +SATELLITE_PATH = Rails.root.join('tmp', 'satellite') +# Make directory +FileUtils.mkdir_p(SATELLITE_PATH) +# Clear any potential directory +FileUtils.rm_rf("#{SATELLITE_PATH}/gitlabhq") +# Chdir, clone from the seed +FileUtils.cd(SATELLITE_PATH) do + # Clone the satellite + `git clone --quiet #{REPO_PATH}/gitlabhq #{SATELLITE_PATH}/gitlabhq` +end puts ' done.' diff --git a/db/migrate/20130123114545_add_issues_tracker_to_project.rb b/db/migrate/20130123114545_add_issues_tracker_to_project.rb new file mode 100644 index 00000000000..288d0f07c9a --- /dev/null +++ b/db/migrate/20130123114545_add_issues_tracker_to_project.rb @@ -0,0 +1,5 @@ +class AddIssuesTrackerToProject < ActiveRecord::Migration + def change + add_column :projects, :issues_tracker, :string, default: :gitlab, null: false + end +end diff --git a/db/migrate/20130206084024_add_description_to_namsespace.rb b/db/migrate/20130206084024_add_description_to_namsespace.rb new file mode 100644 index 00000000000..ef02e489d03 --- /dev/null +++ b/db/migrate/20130206084024_add_description_to_namsespace.rb @@ -0,0 +1,5 @@ +class AddDescriptionToNamsespace < ActiveRecord::Migration + def change + add_column :namespaces, :description, :string, default: '', null: false + end +end diff --git a/db/migrate/20130207104426_add_description_to_teams.rb b/db/migrate/20130207104426_add_description_to_teams.rb new file mode 100644 index 00000000000..6d03777901c --- /dev/null +++ b/db/migrate/20130207104426_add_description_to_teams.rb @@ -0,0 +1,5 @@ +class AddDescriptionToTeams < ActiveRecord::Migration + def change + add_column :user_teams, :description, :string, default: '', null: false + end +end diff --git a/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb b/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb new file mode 100644 index 00000000000..71763d18aee --- /dev/null +++ b/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb @@ -0,0 +1,5 @@ +class AddIssuesTrackerIdToProject < ActiveRecord::Migration + def change + add_column :projects, :issues_tracker_id, :string + end +end diff --git a/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb b/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb new file mode 100644 index 00000000000..23797fe1894 --- /dev/null +++ b/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb @@ -0,0 +1,5 @@ +class RenameStateToMergeStatusInMilestone < ActiveRecord::Migration + def change + rename_column :merge_requests, :state, :merge_status + end +end diff --git a/db/migrate/20130218140952_add_state_to_issue.rb b/db/migrate/20130218140952_add_state_to_issue.rb new file mode 100644 index 00000000000..062103d0e33 --- /dev/null +++ b/db/migrate/20130218140952_add_state_to_issue.rb @@ -0,0 +1,5 @@ +class AddStateToIssue < ActiveRecord::Migration + def change + add_column :issues, :state, :string + end +end diff --git a/db/migrate/20130218141038_add_state_to_merge_request.rb b/db/migrate/20130218141038_add_state_to_merge_request.rb new file mode 100644 index 00000000000..ac4108ee311 --- /dev/null +++ b/db/migrate/20130218141038_add_state_to_merge_request.rb @@ -0,0 +1,5 @@ +class AddStateToMergeRequest < ActiveRecord::Migration + def change + add_column :merge_requests, :state, :string + end +end diff --git a/db/migrate/20130218141117_add_state_to_milestone.rb b/db/migrate/20130218141117_add_state_to_milestone.rb new file mode 100644 index 00000000000..c84039106bd --- /dev/null +++ b/db/migrate/20130218141117_add_state_to_milestone.rb @@ -0,0 +1,5 @@ +class AddStateToMilestone < ActiveRecord::Migration + def change + add_column :milestones, :state, :string + end +end diff --git a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb new file mode 100644 index 00000000000..9fa96203ffd --- /dev/null +++ b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb @@ -0,0 +1,14 @@ +class ConvertClosedToStateInIssue < ActiveRecord::Migration + def up + Issue.transaction do + Issue.where(closed: true).update_all(state: :closed) + Issue.where(closed: false).update_all(state: :opened) + end + end + + def down + Issue.transaction do + Issue.where(state: :closed).update_all(closed: true) + end + end +end diff --git a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb new file mode 100644 index 00000000000..ebb7ae585e6 --- /dev/null +++ b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb @@ -0,0 +1,16 @@ +class ConvertClosedToStateInMergeRequest < ActiveRecord::Migration + def up + MergeRequest.transaction do + MergeRequest.where(closed: true, merged: true).update_all(state: :merged) + MergeRequest.where(closed: true, merged: false).update_all(state: :closed) + MergeRequest.where(closed: false).update_all(state: :opened) + end + end + + def down + MergeRequest.transaction do + MergeRequest.where(state: :closed).update_all(closed: true) + MergeRequest.where(state: :merged).update_all(closed: true, merged: true) + end + end +end diff --git a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb new file mode 100644 index 00000000000..1978ea89153 --- /dev/null +++ b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb @@ -0,0 +1,14 @@ +class ConvertClosedToStateInMilestone < ActiveRecord::Migration + def up + Milestone.transaction do + Milestone.where(closed: true).update_all(state: :closed) + Milestone.where(closed: false).update_all(state: :active) + end + end + + def down + Milestone.transaction do + Milestone.where(state: :closed).update_all(closed: true) + end + end +end diff --git a/db/migrate/20130218141444_remove_merged_from_merge_request.rb b/db/migrate/20130218141444_remove_merged_from_merge_request.rb new file mode 100644 index 00000000000..a7bd82f5000 --- /dev/null +++ b/db/migrate/20130218141444_remove_merged_from_merge_request.rb @@ -0,0 +1,9 @@ +class RemoveMergedFromMergeRequest < ActiveRecord::Migration + def up + remove_column :merge_requests, :merged + end + + def down + add_column :merge_requests, :merged, :boolean, default: true, null: false + end +end diff --git a/db/migrate/20130218141507_remove_closed_from_issue.rb b/db/migrate/20130218141507_remove_closed_from_issue.rb new file mode 100644 index 00000000000..95cc064252b --- /dev/null +++ b/db/migrate/20130218141507_remove_closed_from_issue.rb @@ -0,0 +1,9 @@ +class RemoveClosedFromIssue < ActiveRecord::Migration + def up + remove_column :issues, :closed + end + + def down + add_column :issues, :closed, :boolean + end +end diff --git a/db/migrate/20130218141536_remove_closed_from_merge_request.rb b/db/migrate/20130218141536_remove_closed_from_merge_request.rb new file mode 100644 index 00000000000..371835938b2 --- /dev/null +++ b/db/migrate/20130218141536_remove_closed_from_merge_request.rb @@ -0,0 +1,9 @@ +class RemoveClosedFromMergeRequest < ActiveRecord::Migration + def up + remove_column :merge_requests, :closed + end + + def down + add_column :merge_requests, :closed, :boolean + end +end diff --git a/db/migrate/20130218141554_remove_closed_from_milestone.rb b/db/migrate/20130218141554_remove_closed_from_milestone.rb new file mode 100644 index 00000000000..e8dae4a19b1 --- /dev/null +++ b/db/migrate/20130218141554_remove_closed_from_milestone.rb @@ -0,0 +1,9 @@ +class RemoveClosedFromMilestone < ActiveRecord::Migration + def up + remove_column :milestones, :closed + end + + def down + add_column :milestones, :closed, :boolean + end +end diff --git a/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb b/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb new file mode 100644 index 00000000000..d78bd0ae923 --- /dev/null +++ b/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb @@ -0,0 +1,5 @@ +class AddNewMergeStatusToMergeRequest < ActiveRecord::Migration + def change + add_column :merge_requests, :new_merge_status, :string + end +end diff --git a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb new file mode 100644 index 00000000000..b310b35e373 --- /dev/null +++ b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb @@ -0,0 +1,17 @@ +class ConvertMergeStatusInMergeRequest < ActiveRecord::Migration + def up + MergeRequest.transaction do + MergeRequest.where(merge_status: 1).update_all("new_merge_status = 'unchecked'") + MergeRequest.where(merge_status: 2).update_all("new_merge_status = 'can_be_merged'") + MergeRequest.where(merge_status: 3).update_all("new_merge_status = 'cannot_be_merged'") + end + end + + def down + MergeRequest.transaction do + MergeRequest.where(new_merge_status: :unchecked).update_all("merge_status = 1") + MergeRequest.where(new_merge_status: :can_be_merged).update_all("merge_status = 2") + MergeRequest.where(new_merge_status: :cannot_be_merged).update_all("merge_status = 3") + end + end +end diff --git a/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb b/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb new file mode 100644 index 00000000000..9083183beb0 --- /dev/null +++ b/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb @@ -0,0 +1,9 @@ +class RemoveMergeStatusFromMergeRequest < ActiveRecord::Migration + def up + remove_column :merge_requests, :merge_status + end + + def down + add_column :merge_requests, :merge_status, :integer + end +end diff --git a/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb b/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb new file mode 100644 index 00000000000..3f8f38dc979 --- /dev/null +++ b/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb @@ -0,0 +1,5 @@ +class RenameNewMergeStatusToMergeStatusInMilestone < ActiveRecord::Migration + def change + rename_column :merge_requests, :new_merge_status, :merge_status + end +end diff --git a/db/migrate/20130304104623_add_state_to_user.rb b/db/migrate/20130304104623_add_state_to_user.rb new file mode 100644 index 00000000000..8154c21065f --- /dev/null +++ b/db/migrate/20130304104623_add_state_to_user.rb @@ -0,0 +1,5 @@ +class AddStateToUser < ActiveRecord::Migration + def change + add_column :users, :state, :string + end +end diff --git a/db/migrate/20130304104740_convert_blocked_to_state.rb b/db/migrate/20130304104740_convert_blocked_to_state.rb new file mode 100644 index 00000000000..e8d5257ac96 --- /dev/null +++ b/db/migrate/20130304104740_convert_blocked_to_state.rb @@ -0,0 +1,14 @@ +class ConvertBlockedToState < ActiveRecord::Migration + def up + User.transaction do + User.where(blocked: true).update_all(state: :blocked) + User.where(blocked: false).update_all(state: :active) + end + end + + def down + User.transaction do + User.where(state: :blocked).update_all(blocked: :true) + end + end +end diff --git a/db/migrate/20130304105317_remove_blocked_from_user.rb b/db/migrate/20130304105317_remove_blocked_from_user.rb new file mode 100644 index 00000000000..e010474538c --- /dev/null +++ b/db/migrate/20130304105317_remove_blocked_from_user.rb @@ -0,0 +1,9 @@ +class RemoveBlockedFromUser < ActiveRecord::Migration + def up + remove_column :users, :blocked + end + + def down + add_column :users, :blocked, :boolean + end +end diff --git a/db/migrate/20130315124931_user_color_scheme.rb b/db/migrate/20130315124931_user_color_scheme.rb new file mode 100644 index 00000000000..fe139e32ea7 --- /dev/null +++ b/db/migrate/20130315124931_user_color_scheme.rb @@ -0,0 +1,12 @@ +class UserColorScheme < ActiveRecord::Migration + def up + add_column :users, :color_scheme_id, :integer, null: false, default: 1 + User.where(dark_scheme: true).update_all(color_scheme_id: 2) + remove_column :users, :dark_scheme + end + + def down + add_column :users, :dark_scheme, :boolean, null: false, default: false + remove_column :users, :color_scheme_id + end +end diff --git a/db/migrate/20130318212250_add_snippets_to_features.rb b/db/migrate/20130318212250_add_snippets_to_features.rb new file mode 100644 index 00000000000..ad0b4434c43 --- /dev/null +++ b/db/migrate/20130318212250_add_snippets_to_features.rb @@ -0,0 +1,5 @@ +class AddSnippetsToFeatures < ActiveRecord::Migration + def change + add_column :projects, :snippets_enabled, :boolean, null: false, default: true + end +end diff --git a/db/migrate/20130319214458_create_forked_project_links.rb b/db/migrate/20130319214458_create_forked_project_links.rb new file mode 100644 index 00000000000..f91afc26e77 --- /dev/null +++ b/db/migrate/20130319214458_create_forked_project_links.rb @@ -0,0 +1,11 @@ +class CreateForkedProjectLinks < ActiveRecord::Migration + def change + create_table :forked_project_links do |t| + t.integer :forked_to_project_id, null: false + t.integer :forked_from_project_id, null: false + + t.timestamps + end + add_index :forked_project_links, :forked_to_project_id, unique: true + end +end diff --git a/db/migrate/20130323174317_add_private_to_snippets.rb b/db/migrate/20130323174317_add_private_to_snippets.rb new file mode 100644 index 00000000000..92f3a5c7011 --- /dev/null +++ b/db/migrate/20130323174317_add_private_to_snippets.rb @@ -0,0 +1,5 @@ +class AddPrivateToSnippets < ActiveRecord::Migration + def change + add_column :snippets, :private, :boolean, null: false, default: true + end +end diff --git a/db/migrate/20130324151736_add_type_to_snippets.rb b/db/migrate/20130324151736_add_type_to_snippets.rb new file mode 100644 index 00000000000..276aab2ca15 --- /dev/null +++ b/db/migrate/20130324151736_add_type_to_snippets.rb @@ -0,0 +1,5 @@ +class AddTypeToSnippets < ActiveRecord::Migration + def change + add_column :snippets, :type, :string + end +end diff --git a/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb b/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb new file mode 100644 index 00000000000..4c992bac4d1 --- /dev/null +++ b/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb @@ -0,0 +1,9 @@ +class ChangeProjectIdToNullInSnipepts < ActiveRecord::Migration + def up + change_column :snippets, :project_id, :integer, :null => true + end + + def down + change_column :snippets, :project_id, :integer, :null => false + end +end diff --git a/db/migrate/20130324203535_add_type_value_for_snippets.rb b/db/migrate/20130324203535_add_type_value_for_snippets.rb new file mode 100644 index 00000000000..8c05dd2cc71 --- /dev/null +++ b/db/migrate/20130324203535_add_type_value_for_snippets.rb @@ -0,0 +1,8 @@ +class AddTypeValueForSnippets < ActiveRecord::Migration + def up + Snippet.where("project_id IS NOT NULL").update_all(type: 'ProjectSnippet') + end + + def down + end +end diff --git a/db/migrate/20130325173941_add_notification_level_to_user.rb b/db/migrate/20130325173941_add_notification_level_to_user.rb new file mode 100644 index 00000000000..9f466e38c13 --- /dev/null +++ b/db/migrate/20130325173941_add_notification_level_to_user.rb @@ -0,0 +1,5 @@ +class AddNotificationLevelToUser < ActiveRecord::Migration + def change + add_column :users, :notification_level, :integer, null: false, default: 1 + end +end diff --git a/db/migrate/20130326142630_add_index_to_users_authentication_token.rb b/db/migrate/20130326142630_add_index_to_users_authentication_token.rb new file mode 100644 index 00000000000..d42ef113738 --- /dev/null +++ b/db/migrate/20130326142630_add_index_to_users_authentication_token.rb @@ -0,0 +1,5 @@ +class AddIndexToUsersAuthenticationToken < ActiveRecord::Migration + def change + add_index :users, :authentication_token, unique: true + end +end diff --git a/db/migrate/20130403003950_add_last_activity_column_into_project.rb b/db/migrate/20130403003950_add_last_activity_column_into_project.rb new file mode 100644 index 00000000000..2a036bd9993 --- /dev/null +++ b/db/migrate/20130403003950_add_last_activity_column_into_project.rb @@ -0,0 +1,21 @@ +class AddLastActivityColumnIntoProject < ActiveRecord::Migration + def up + add_column :projects, :last_activity_at, :datetime + add_index :projects, :last_activity_at + + Project.find_each do |project| + last_activity_date = if project.last_activity + project.last_activity.created_at + else + project.updated_at + end + + project.update_attribute(:last_activity_at, last_activity_date) + end + end + + def down + remove_index :projects, :last_activity_at + remove_column :projects, :last_activity_at + end +end diff --git a/db/migrate/20130404164628_add_notification_level_to_user_project.rb b/db/migrate/20130404164628_add_notification_level_to_user_project.rb new file mode 100644 index 00000000000..27de5d6bf55 --- /dev/null +++ b/db/migrate/20130404164628_add_notification_level_to_user_project.rb @@ -0,0 +1,5 @@ +class AddNotificationLevelToUserProject < ActiveRecord::Migration + def change + add_column :users_projects, :notification_level, :integer, null: false, default: 3 + end +end diff --git a/db/migrate/20130410175022_remove_wiki_table.rb b/db/migrate/20130410175022_remove_wiki_table.rb new file mode 100644 index 00000000000..9077aa2473c --- /dev/null +++ b/db/migrate/20130410175022_remove_wiki_table.rb @@ -0,0 +1,9 @@ +class RemoveWikiTable < ActiveRecord::Migration + def up + drop_table :wikis + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/migrate/20130419190306_allow_merges_for_forks.rb b/db/migrate/20130419190306_allow_merges_for_forks.rb new file mode 100644 index 00000000000..56ce58a846d --- /dev/null +++ b/db/migrate/20130419190306_allow_merges_for_forks.rb @@ -0,0 +1,13 @@ +class AllowMergesForForks < ActiveRecord::Migration + def self.up + add_column :merge_requests, :target_project_id, :integer, :null => true + MergeRequest.update_all("target_project_id = project_id") + change_column :merge_requests, :target_project_id, :integer, :null => false + rename_column :merge_requests, :project_id, :source_project_id + end + + def self.down + remove_column :merge_requests, :target_project_id + rename_column :merge_requests, :source_project_id,:project_id + end +end diff --git a/db/migrate/20130506085413_add_type_to_key.rb b/db/migrate/20130506085413_add_type_to_key.rb new file mode 100644 index 00000000000..315e7ca77b3 --- /dev/null +++ b/db/migrate/20130506085413_add_type_to_key.rb @@ -0,0 +1,5 @@ +class AddTypeToKey < ActiveRecord::Migration + def change + add_column :keys, :type, :string + end +end diff --git a/db/migrate/20130506090604_create_deploy_keys_projects.rb b/db/migrate/20130506090604_create_deploy_keys_projects.rb new file mode 100644 index 00000000000..0dc8cdeb07d --- /dev/null +++ b/db/migrate/20130506090604_create_deploy_keys_projects.rb @@ -0,0 +1,10 @@ +class CreateDeployKeysProjects < ActiveRecord::Migration + def change + create_table :deploy_keys_projects do |t| + t.integer :deploy_key_id, null: false + t.integer :project_id, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20130506095501_remove_project_id_from_key.rb b/db/migrate/20130506095501_remove_project_id_from_key.rb new file mode 100644 index 00000000000..4214fd45d14 --- /dev/null +++ b/db/migrate/20130506095501_remove_project_id_from_key.rb @@ -0,0 +1,22 @@ +class RemoveProjectIdFromKey < ActiveRecord::Migration + def up + puts 'Migrate deploy keys: ' + Key.where('project_id IS NOT NULL').update_all(type: 'DeployKey') + + DeployKey.all.each do |key| + project = Project.find_by_id(key.project_id) + if project + project.deploy_keys << key + print '.' + end + end + + puts 'Done' + + remove_column :keys, :project_id + end + + def down + add_column :keys, :project_id, :integer + end +end diff --git a/db/migrate/20130522141856_add_more_fields_to_service.rb b/db/migrate/20130522141856_add_more_fields_to_service.rb new file mode 100644 index 00000000000..298e902df2f --- /dev/null +++ b/db/migrate/20130522141856_add_more_fields_to_service.rb @@ -0,0 +1,6 @@ +class AddMoreFieldsToService < ActiveRecord::Migration + def change + add_column :services, :subdomain, :string + add_column :services, :room, :string + end +end diff --git a/db/migrate/20130528184641_add_system_to_notes.rb b/db/migrate/20130528184641_add_system_to_notes.rb new file mode 100644 index 00000000000..1b22a4934f9 --- /dev/null +++ b/db/migrate/20130528184641_add_system_to_notes.rb @@ -0,0 +1,16 @@ +class AddSystemToNotes < ActiveRecord::Migration + class Note < ActiveRecord::Base + end + + def up + add_column :notes, :system, :boolean, default: false, null: false + + Note.reset_column_information + Note.update_all(system: false) + Note.where("note like '_status changed to%'").update_all(system: true) + end + + def down + remove_column :notes, :system + end +end diff --git a/db/migrate/20130611210815_increase_snippet_text_column_size.rb b/db/migrate/20130611210815_increase_snippet_text_column_size.rb new file mode 100644 index 00000000000..f7b4447e43e --- /dev/null +++ b/db/migrate/20130611210815_increase_snippet_text_column_size.rb @@ -0,0 +1,9 @@ +class IncreaseSnippetTextColumnSize < ActiveRecord::Migration + def up + # MYSQL LARGETEXT for snippet + change_column :snippets, :content, :text, :limit => 4294967295 + end + + def down + end +end diff --git a/db/migrate/20130613165816_add_password_expires_at_to_users.rb b/db/migrate/20130613165816_add_password_expires_at_to_users.rb new file mode 100644 index 00000000000..3479c8e64d0 --- /dev/null +++ b/db/migrate/20130613165816_add_password_expires_at_to_users.rb @@ -0,0 +1,5 @@ +class AddPasswordExpiresAtToUsers < ActiveRecord::Migration + def change + add_column :users, :password_expires_at, :datetime + end +end diff --git a/db/migrate/20130613173246_add_created_by_id_to_user.rb b/db/migrate/20130613173246_add_created_by_id_to_user.rb new file mode 100644 index 00000000000..615e96eb156 --- /dev/null +++ b/db/migrate/20130613173246_add_created_by_id_to_user.rb @@ -0,0 +1,5 @@ +class AddCreatedByIdToUser < ActiveRecord::Migration + def change + add_column :users, :created_by_id, :integer + end +end diff --git a/db/migrate/20130614132337_add_improted_to_project.rb b/db/migrate/20130614132337_add_improted_to_project.rb new file mode 100644 index 00000000000..cc882c3f10a --- /dev/null +++ b/db/migrate/20130614132337_add_improted_to_project.rb @@ -0,0 +1,5 @@ +class AddImprotedToProject < ActiveRecord::Migration + def change + add_column :projects, :imported, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20130617095603_create_users_groups.rb b/db/migrate/20130617095603_create_users_groups.rb new file mode 100644 index 00000000000..2efc04f1151 --- /dev/null +++ b/db/migrate/20130617095603_create_users_groups.rb @@ -0,0 +1,11 @@ +class CreateUsersGroups < ActiveRecord::Migration + def change + create_table :users_groups do |t| + t.integer :group_access, null: false + t.integer :group_id, null: false + t.integer :user_id, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20130621195223_add_notification_level_to_user_group.rb b/db/migrate/20130621195223_add_notification_level_to_user_group.rb new file mode 100644 index 00000000000..8c2e3dfcaca --- /dev/null +++ b/db/migrate/20130621195223_add_notification_level_to_user_group.rb @@ -0,0 +1,5 @@ +class AddNotificationLevelToUserGroup < ActiveRecord::Migration + def change + add_column :users_groups, :notification_level, :integer, null: false, default: 3 + end +end diff --git a/db/migrate/20130622115340_add_more_db_index.rb b/db/migrate/20130622115340_add_more_db_index.rb new file mode 100644 index 00000000000..9570a7a3f1e --- /dev/null +++ b/db/migrate/20130622115340_add_more_db_index.rb @@ -0,0 +1,12 @@ +class AddMoreDbIndex < ActiveRecord::Migration + def change + add_index :deploy_keys_projects, :project_id + add_index :web_hooks, :project_id + add_index :protected_branches, :project_id + + add_index :users_groups, :user_id + add_index :snippets, :author_id + add_index :notes, :author_id + add_index :notes, [:noteable_id, :noteable_type] + end +end diff --git a/db/migrate/20130624162710_add_fingerprint_to_key.rb b/db/migrate/20130624162710_add_fingerprint_to_key.rb new file mode 100644 index 00000000000..544a8366727 --- /dev/null +++ b/db/migrate/20130624162710_add_fingerprint_to_key.rb @@ -0,0 +1,6 @@ +class AddFingerprintToKey < ActiveRecord::Migration + def change + add_column :keys, :fingerprint, :string + remove_column :keys, :identifier + end +end diff --git a/db/migrate/20130804151314_add_st_diff_to_note.rb b/db/migrate/20130804151314_add_st_diff_to_note.rb new file mode 100644 index 00000000000..3f9abb975c3 --- /dev/null +++ b/db/migrate/20130804151314_add_st_diff_to_note.rb @@ -0,0 +1,5 @@ +class AddStDiffToNote < ActiveRecord::Migration + def change + add_column :notes, :st_diff, :text, :null => true + end +end diff --git a/db/migrate/20130812143708_add_import_url_to_project.rb b/db/migrate/20130812143708_add_import_url_to_project.rb new file mode 100644 index 00000000000..023a48741b2 --- /dev/null +++ b/db/migrate/20130812143708_add_import_url_to_project.rb @@ -0,0 +1,5 @@ +class AddImportUrlToProject < ActiveRecord::Migration + def change + add_column :projects, :import_url, :string + end +end diff --git a/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb b/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb new file mode 100644 index 00000000000..e55ae38f144 --- /dev/null +++ b/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb @@ -0,0 +1,6 @@ +class AddInternalIdsToIssuesAndMr < ActiveRecord::Migration + def change + add_column :issues, :iid, :integer + add_column :merge_requests, :iid, :integer + end +end diff --git a/db/migrate/20130821090530_remove_deprecated_tables.rb b/db/migrate/20130821090530_remove_deprecated_tables.rb new file mode 100644 index 00000000000..539c0617eeb --- /dev/null +++ b/db/migrate/20130821090530_remove_deprecated_tables.rb @@ -0,0 +1,11 @@ +class RemoveDeprecatedTables < ActiveRecord::Migration + def up + drop_table :user_teams + drop_table :user_team_project_relationships + drop_table :user_team_user_relationships + end + + def down + raise 'No rollback for this migration' + end +end diff --git a/db/migrate/20130821090531_add_internal_ids_to_milestones.rb b/db/migrate/20130821090531_add_internal_ids_to_milestones.rb new file mode 100644 index 00000000000..33e5bae5805 --- /dev/null +++ b/db/migrate/20130821090531_add_internal_ids_to_milestones.rb @@ -0,0 +1,5 @@ +class AddInternalIdsToMilestones < ActiveRecord::Migration + def change + add_column :milestones, :iid, :integer + end +end diff --git a/db/migrate/20130909132950_add_description_to_merge_request.rb b/db/migrate/20130909132950_add_description_to_merge_request.rb new file mode 100644 index 00000000000..9bcd0c7ee06 --- /dev/null +++ b/db/migrate/20130909132950_add_description_to_merge_request.rb @@ -0,0 +1,5 @@ +class AddDescriptionToMergeRequest < ActiveRecord::Migration + def change + add_column :merge_requests, :description, :text, null: true + end +end diff --git a/db/migrate/20130926081215_change_owner_id_for_group.rb b/db/migrate/20130926081215_change_owner_id_for_group.rb new file mode 100644 index 00000000000..8f1992c37ab --- /dev/null +++ b/db/migrate/20130926081215_change_owner_id_for_group.rb @@ -0,0 +1,9 @@ +class ChangeOwnerIdForGroup < ActiveRecord::Migration + def up + change_column :namespaces, :owner_id, :integer, null: true + end + + def down + change_column :namespaces, :owner_id, :integer, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 0f07d2bc8c5..713d9f733d6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,16 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20130131070232) do +ActiveRecord::Schema.define(:version => 20130926081215) do + + create_table "deploy_keys_projects", :force => true do |t| + t.integer "deploy_key_id", :null => false + t.integer "project_id", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "deploy_keys_projects", ["project_id"], :name => "index_deploy_keys_projects_on_project_id" create_table "events", :force => true do |t| t.string "target_type" @@ -32,23 +41,32 @@ ActiveRecord::Schema.define(:version => 20130131070232) do add_index "events", ["target_id"], :name => "index_events_on_target_id" add_index "events", ["target_type"], :name => "index_events_on_target_type" + create_table "forked_project_links", :force => true do |t| + t.integer "forked_to_project_id", :null => false + t.integer "forked_from_project_id", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "forked_project_links", ["forked_to_project_id"], :name => "index_forked_project_links_on_forked_to_project_id", :unique => true + create_table "issues", :force => true do |t| t.string "title" t.integer "assignee_id" t.integer "author_id" t.integer "project_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false - t.boolean "closed", :default => false, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.integer "position", :default => 0 t.string "branch_name" t.text "description" t.integer "milestone_id" + t.string "state" + t.integer "iid" end add_index "issues", ["assignee_id"], :name => "index_issues_on_assignee_id" add_index "issues", ["author_id"], :name => "index_issues_on_author_id" - add_index "issues", ["closed"], :name => "index_issues_on_closed" add_index "issues", ["created_at"], :name => "index_issues_on_created_at" add_index "issues", ["milestone_id"], :name => "index_issues_on_milestone_id" add_index "issues", ["project_id"], :name => "index_issues_on_project_id" @@ -56,65 +74,66 @@ ActiveRecord::Schema.define(:version => 20130131070232) do create_table "keys", :force => true do |t| t.integer "user_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.text "key" t.string "title" - t.string "identifier" - t.integer "project_id" + t.string "type" + t.string "fingerprint" end - add_index "keys", ["identifier"], :name => "index_keys_on_identifier" - add_index "keys", ["project_id"], :name => "index_keys_on_project_id" add_index "keys", ["user_id"], :name => "index_keys_on_user_id" create_table "merge_requests", :force => true do |t| - t.string "target_branch", :null => false - t.string "source_branch", :null => false - t.integer "project_id", :null => false + t.string "target_branch", :null => false + t.string "source_branch", :null => false + t.integer "source_project_id", :null => false t.integer "author_id" t.integer "assignee_id" t.string "title" - t.boolean "closed", :default => false, :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false - t.text "st_commits", :limit => 2147483647 - t.text "st_diffs", :limit => 2147483647 - t.boolean "merged", :default => false, :null => false - t.integer "state", :default => 1, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.text "st_commits", :limit => 2147483647 + t.text "st_diffs", :limit => 2147483647 t.integer "milestone_id" + t.string "state" + t.string "merge_status" + t.integer "target_project_id", :null => false + t.integer "iid" + t.text "description" end add_index "merge_requests", ["assignee_id"], :name => "index_merge_requests_on_assignee_id" add_index "merge_requests", ["author_id"], :name => "index_merge_requests_on_author_id" - add_index "merge_requests", ["closed"], :name => "index_merge_requests_on_closed" add_index "merge_requests", ["created_at"], :name => "index_merge_requests_on_created_at" add_index "merge_requests", ["milestone_id"], :name => "index_merge_requests_on_milestone_id" - add_index "merge_requests", ["project_id"], :name => "index_merge_requests_on_project_id" add_index "merge_requests", ["source_branch"], :name => "index_merge_requests_on_source_branch" + add_index "merge_requests", ["source_project_id"], :name => "index_merge_requests_on_project_id" add_index "merge_requests", ["target_branch"], :name => "index_merge_requests_on_target_branch" add_index "merge_requests", ["title"], :name => "index_merge_requests_on_title" create_table "milestones", :force => true do |t| - t.string "title", :null => false - t.integer "project_id", :null => false + t.string "title", :null => false + t.integer "project_id", :null => false t.text "description" t.date "due_date" - t.boolean "closed", :default => false, :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "state" + t.integer "iid" end add_index "milestones", ["due_date"], :name => "index_milestones_on_due_date" add_index "milestones", ["project_id"], :name => "index_milestones_on_project_id" create_table "namespaces", :force => true do |t| - t.string "name", :null => false - t.string "path", :null => false - t.integer "owner_id", :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.string "name", :null => false + t.string "path", :null => false + t.integer "owner_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.string "type" + t.string "description", :default => "", :null => false end add_index "namespaces", ["name"], :name => "index_namespaces_on_name" @@ -126,17 +145,21 @@ ActiveRecord::Schema.define(:version => 20130131070232) do t.text "note" t.string "noteable_type" t.integer "author_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.integer "project_id" t.string "attachment" t.string "line_code" t.string "commit_id" t.integer "noteable_id" + t.text "st_diff" + t.boolean "system", :default => false, :null => false end + add_index "notes", ["author_id"], :name => "index_notes_on_author_id" add_index "notes", ["commit_id"], :name => "index_notes_on_commit_id" add_index "notes", ["created_at"], :name => "index_notes_on_created_at" + add_index "notes", ["noteable_id", "noteable_type"], :name => "index_notes_on_noteable_id_and_noteable_type" add_index "notes", ["noteable_type"], :name => "index_notes_on_noteable_type" add_index "notes", ["project_id", "noteable_type"], :name => "index_notes_on_project_id_and_noteable_type" add_index "notes", ["project_id"], :name => "index_notes_on_project_id" @@ -145,19 +168,26 @@ ActiveRecord::Schema.define(:version => 20130131070232) do t.string "name" t.string "path" t.text "description" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.integer "creator_id" t.string "default_branch" - t.boolean "issues_enabled", :default => true, :null => false - t.boolean "wall_enabled", :default => true, :null => false - t.boolean "merge_requests_enabled", :default => true, :null => false - t.boolean "wiki_enabled", :default => true, :null => false + t.boolean "issues_enabled", :default => true, :null => false + t.boolean "wall_enabled", :default => true, :null => false + t.boolean "merge_requests_enabled", :default => true, :null => false + t.boolean "wiki_enabled", :default => true, :null => false t.integer "namespace_id" - t.boolean "public", :default => false, :null => false + t.boolean "public", :default => false, :null => false + t.string "issues_tracker", :default => "gitlab", :null => false + t.string "issues_tracker_id" + t.boolean "snippets_enabled", :default => true, :null => false + t.datetime "last_activity_at" + t.boolean "imported", :default => false, :null => false + t.string "import_url" end add_index "projects", ["creator_id"], :name => "index_projects_on_owner_id" + add_index "projects", ["last_activity_at"], :name => "index_projects_on_last_activity_at" add_index "projects", ["namespace_id"], :name => "index_projects_on_namespace_id" create_table "protected_branches", :force => true do |t| @@ -167,6 +197,8 @@ ActiveRecord::Schema.define(:version => 20130131070232) do t.datetime "updated_at", :null => false end + add_index "protected_branches", ["project_id"], :name => "index_protected_branches_on_project_id" + create_table "services", :force => true do |t| t.string "type" t.string "title" @@ -176,21 +208,26 @@ ActiveRecord::Schema.define(:version => 20130131070232) do t.datetime "updated_at", :null => false t.boolean "active", :default => false, :null => false t.string "project_url" + t.string "subdomain" + t.string "room" end add_index "services", ["project_id"], :name => "index_services_on_project_id" create_table "snippets", :force => true do |t| t.string "title" - t.text "content" - t.integer "author_id", :null => false - t.integer "project_id", :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.text "content", :limit => 2147483647 + t.integer "author_id", :null => false + t.integer "project_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.string "file_name" t.datetime "expires_at" + t.boolean "private", :default => true, :null => false + t.string "type" end + add_index "snippets", ["author_id"], :name => "index_snippets_on_author_id" add_index "snippets", ["created_at"], :name => "index_snippets_on_created_at" add_index "snippets", ["expires_at"], :name => "index_snippets_on_expires_at" add_index "snippets", ["project_id"], :name => "index_snippets_on_project_id" @@ -212,31 +249,6 @@ ActiveRecord::Schema.define(:version => 20130131070232) do t.string "name" end - create_table "user_team_project_relationships", :force => true do |t| - t.integer "project_id" - t.integer "user_team_id" - t.integer "greatest_access" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false - end - - create_table "user_team_user_relationships", :force => true do |t| - t.integer "user_id" - t.integer "user_team_id" - t.boolean "group_admin" - t.integer "permission" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false - end - - create_table "user_teams", :force => true do |t| - t.string "name" - t.string "path" - t.integer "owner_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false - end - create_table "users", :force => true do |t| t.string "email", :default => "", :null => false t.string "encrypted_password", :default => "", :null => false @@ -257,10 +269,8 @@ ActiveRecord::Schema.define(:version => 20130131070232) do t.string "linkedin", :default => "", :null => false t.string "twitter", :default => "", :null => false t.string "authentication_token" - t.boolean "dark_scheme", :default => false, :null => false t.integer "theme_id", :default => 1, :null => false t.string "bio" - t.boolean "blocked", :default => false, :null => false t.integer "failed_attempts", :default => 0 t.datetime "locked_at" t.string "extern_uid" @@ -268,22 +278,39 @@ ActiveRecord::Schema.define(:version => 20130131070232) do t.string "username" t.boolean "can_create_group", :default => true, :null => false t.boolean "can_create_team", :default => true, :null => false + t.string "state" + t.integer "color_scheme_id", :default => 1, :null => false + t.integer "notification_level", :default => 1, :null => false + t.datetime "password_expires_at" + t.integer "created_by_id" end add_index "users", ["admin"], :name => "index_users_on_admin" - add_index "users", ["blocked"], :name => "index_users_on_blocked" + add_index "users", ["authentication_token"], :name => "index_users_on_authentication_token", :unique => true add_index "users", ["email"], :name => "index_users_on_email", :unique => true add_index "users", ["extern_uid", "provider"], :name => "index_users_on_extern_uid_and_provider", :unique => true add_index "users", ["name"], :name => "index_users_on_name" add_index "users", ["reset_password_token"], :name => "index_users_on_reset_password_token", :unique => true add_index "users", ["username"], :name => "index_users_on_username" + create_table "users_groups", :force => true do |t| + t.integer "group_access", :null => false + t.integer "group_id", :null => false + t.integer "user_id", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.integer "notification_level", :default => 3, :null => false + end + + add_index "users_groups", ["user_id"], :name => "index_users_groups_on_user_id" + create_table "users_projects", :force => true do |t| - t.integer "user_id", :null => false - t.integer "project_id", :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false - t.integer "project_access", :default => 0, :null => false + t.integer "user_id", :null => false + t.integer "project_id", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.integer "project_access", :default => 0, :null => false + t.integer "notification_level", :default => 3, :null => false end add_index "users_projects", ["project_access"], :name => "index_users_projects_on_project_access" @@ -299,17 +326,6 @@ ActiveRecord::Schema.define(:version => 20130131070232) do t.integer "service_id" end - create_table "wikis", :force => true do |t| - t.string "title" - t.text "content" - t.integer "project_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false - t.string "slug" - t.integer "user_id" - end - - add_index "wikis", ["project_id"], :name => "index_wikis_on_project_id" - add_index "wikis", ["slug"], :name => "index_wikis_on_slug" + add_index "web_hooks", ["project_id"], :name => "index_web_hooks_on_project_id" end diff --git a/doc/api/README.md b/doc/api/README.md index 0618db7e369..6971e08f010 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -1,6 +1,6 @@ # GitLab API -All API requests require authentication. You need to pass a `private_token` parameter by url or header. You can find or reset your private token in your profile. +All API requests require authentication. You need to pass a `private_token` parameter by url or header. If passed as header, the header name must be "PRIVATE-TOKEN" (capital and with dash instead of underscore). You can find or reset your private token in your profile. If no, or an invalid, `private_token` is provided then an error message will be returned with status code 401: @@ -18,8 +18,84 @@ Example of a valid API request: GET http://example.com/api/v3/projects?private_token=QVy1PB7sTxfy4pqfZM1U ``` +Example for a valid API request using curl and authentication via header: + +``` +curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://example.com/api/v3/projects" +``` + + The API uses JSON to serialize data. You don't need to specify `.json` at the end of API URL. + + +## Status codes + +The API is designed to return different status codes according to context and action. In this way +if a request results in an error the caller is able to get insight into what went wrong, e.g. +status code `400 Bad Request` is returned if a required attribute is missing from the request. +The following list gives an overview of how the API functions generally behave. + +API request types: + +* `GET` requests access one or more resources and return the result as JSON +* `POST` requests return `201 Created` if the resource is successfully created and return the newly created resource as JSON +* `GET`, `PUT` and `DELETE` return `200 Ok` if the resource is accessed, modified or deleted successfully, the (modified) result is returned as JSON +* `DELETE` requests are designed to be idempotent, meaning a request a resource still returns `200 Ok` even it was deleted before or is not available. The reasoning behind it is the user is not really interested if the resource existed before or not. + + +The following list shows the possible return codes for API requests. + +Return values: + +* `200 Ok` - The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON +* `201 Created` - The `POST` request was successful and the resource is returned as JSON +* `400 Bad Request` - A required attribute of the API request is missing, e.g. the title of an issue is not given +* `401 Unauthorized` - The user is not authenticated, a valid user token is necessary, see above +* `403 Forbidden` - The request is not allowed, e.g. the user is not allowed to delete a project +* `404 Not Found` - A resource could not be accessed, e.g. an ID for a resource could not be found +* `405 Method Not Allowed` - The request is not supported +* `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists +* `500 Server Error` - While handling the request something went wrong on the server side + +## Sudo +All API requests support performing an api call as if you were another user, if your private token is for an administration account. You need to pass `sudo` parameter by url or header with an id or username of the user you want to perform the operation as. If passed as header, the header name must be "SUDO" (capitals). + +If a non administrative `private_token` is provided then an error message will be returned with status code 403: + +```json +{ + "message": "403 Forbidden: Must be admin to use sudo" +} +``` + +If the sudo user id or username cannot be found then an error message will be returned with status code 404: + +```json +{ + "message": "404 Not Found: No user id or username for: <id/username>" +} +``` + +Example of a valid API with sudo request: + +``` +GET http://example.com/api/v3/projects?private_token=QVy1PB7sTxfy4pqfZM1U&sudo=username +``` +``` +GET http://example.com/api/v3/projects?private_token=QVy1PB7sTxfy4pqfZM1U&sudo=23 +``` + + +Example for a valid API request with sudo using curl and authentication via header: + +``` +curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" --header "SUDO: username" "http://example.com/api/v3/projects" +``` +``` +curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" --header "SUDO: 23" "http://example.com/api/v3/projects" +``` + #### Pagination When listing resources you can pass the following parameters: @@ -29,12 +105,23 @@ When listing resources you can pass the following parameters: ## Contents -+ [Users](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/users.md) -+ [Session](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/session.md) -+ [Projects](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/projects.md) -+ [Groups](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/groups.md) -+ [Snippets](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/snippets.md) -+ [Repositories](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/repositories.md) -+ [Issues](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/issues.md) -+ [Milestones](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/milestones.md) -+ [Notes](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/notes.md) ++ [Users](users.md) ++ [Session](session.md) ++ [Projects](projects.md) ++ [Project Snippets](project_snippets.md) ++ [Repositories](repositories.md) ++ [Merge Requests](merge_requests.md) ++ [Issues](issues.md) ++ [Milestones](milestones.md) ++ [Notes](notes.md) ++ [Deploy Keys](deploy_keys.md) ++ [System Hooks](system_hooks.md) ++ [Groups](groups.md) ++ [User Teams](user_teams.md) + +## Clients + ++ [php-gitlab-api](https://github.com/m4tthumphrey/php-gitlab-api) - PHP ++ [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 diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md new file mode 100644 index 00000000000..fbb1e45bccd --- /dev/null +++ b/doc/api/deploy_keys.md @@ -0,0 +1,87 @@ +## Deploy Keys + +### List deploy keys + +Get a list of a project's deploy keys. + +``` +GET /projects/:id/keys +``` + +Parameters: + ++ `id` (required) - The ID of the project + +```json +[ + { + "id": 1, + "title" : "Public key", + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4 + 596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4 + soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", + }, + { + "id": 3, + "title" : "Another Public key", + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4 + 596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4 + soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" + } +] +``` + + +### Single deploy key + +Get a single key. + +``` +GET /projects/:id/keys/:key_id +``` + +Parameters: + ++ `id` (required) - The ID of the project ++ `key_id` (required) - The ID of the deploy key + +```json +{ + "id": 1, + "title" : "Public key", + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4 + 596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4 + soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" +} +``` + + +### Add deploy key + +Creates a new deploy key for a project. +If deploy key already exists in another project - it will be joined to project but only if original one was is accessible by same user + +``` +POST /projects/:id/keys +``` + +Parameters: + ++ `id` (required) - The ID of the project ++ `title` (required) - New deploy key's title ++ `key` (required) - New deploy key + + +### Delete deploy key + +Delete a deploy key from a project + +``` +DELETE /projects/:id/keys/:key_id +``` + +Parameters: + ++ `id` (required) - The ID of the project ++ `key_id` (required) - The ID of the deploy key + diff --git a/doc/api/groups.md b/doc/api/groups.md index 00a7387c76f..9c551fff83a 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -17,7 +17,8 @@ GET /groups ] ``` -## Details of group + +## Details of a group Get all details of a group. @@ -29,17 +30,90 @@ Parameters: + `id` (required) - The ID of a group + ## New group -Create a new project group. Available only for admin +Creates a new project group. Available only for admin. ``` POST /groups ``` Parameters: -+ `name` (required) - Email -+ `path` - Password -Will return created group with status `201 Created` on success, or `404 Not found` on fail. ++ `name` (required) - The name of the group ++ `path` (required) - The path of the group + +## Transfer project to group + +Transfer a project to the Group namespace. Available only for admin + +``` +POST /groups/:id/projects/:project_id +``` + +Parameters: ++ `id` (required) - The ID of a group ++ `project_id (required) - The ID of a project + + +## Group members + +### List group members + +Get a list of group members viewable by the authenticated user. + +``` +GET /groups/:id/members +``` + +```json +[ + { + id: 1, + username: "raymond_smith", + email: "ray@smith.org", + name: "Raymond Smith", + state: "active", + created_at: "2012-10-22T14:13:35Z", + access_level: 30 + }, + { + id: 2, + username: "john_doe", + email: "joh@doe.org", + name: "John Doe", + state: "active", + created_at: "2012-10-22T14:13:35Z", + access_level: 30 + } +] +``` + +### Add group member + +Adds a user to the list of group members. + +``` +POST /groups/:id/members +``` + +Parameters: + ++ `id` (required) - The ID of a group ++ `user_id` (required) - The ID of a user to add ++ `access_level` (required) - Project access level + + +### Remove user team member + +Removes user from user team. + +``` +DELETE /groups/:id/members/:user_id +``` + +Parameters: ++ `id` (required) - The ID of a user group ++ `user_id` (required) - The ID of a group member diff --git a/doc/api/issues.md b/doc/api/issues.md index 0383b676073..723c8acf381 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -1,6 +1,7 @@ ## List issues -Get all issues created by authenticed user. +Get all issues created by authenticated user. This function takes pagination parameters +`page` and `per_page` to restrict the list of issues. ``` GET /issues @@ -24,7 +25,7 @@ GET /issues "blocked": false, "created_at": "2012-05-23T08:00:58Z" }, - "closed": true, + "state": 'closed', "updated_at": "2012-07-02T17:53:12Z", "created_at": "2012-07-02T17:53:12Z" }, @@ -41,7 +42,7 @@ GET /issues "title": "v1.0", "description": "", "due_date": "2012-07-20", - "closed": false, + "state": 'reopenend', "updated_at": "2012-07-04T13:42:48Z", "created_at": "2012-07-04T13:42:48Z" }, @@ -61,16 +62,18 @@ GET /issues "blocked": false, "created_at": "2012-05-23T08:00:58Z" }, - "closed": false, + "state": 'opened', "updated_at": "2012-07-12T13:43:19Z", "created_at": "2012-06-28T12:58:06Z" } ] ``` + ## List project issues -Get a list of project issues. +Get a list of project issues. This function accepts pagination parameters `page` and `per_page` +to return the list of project issues. ``` GET /projects/:id/issues @@ -80,9 +83,10 @@ Parameters: + `id` (required) - The ID of a project + ## Single issue -Get a project issue. +Gets a single project issue. ``` GET /projects/:id/issues/:issue_id @@ -107,7 +111,7 @@ Parameters: "title": "v1.0", "description": "", "due_date": "2012-07-20", - "closed": false, + "state": 'closed', "updated_at": "2012-07-04T13:42:48Z", "created_at": "2012-07-04T13:42:48Z" }, @@ -127,15 +131,16 @@ Parameters: "blocked": false, "created_at": "2012-05-23T08:00:58Z" }, - "closed": false, + "state": 'opened', "updated_at": "2012-07-12T13:43:19Z", "created_at": "2012-06-28T12:58:06Z" } ``` + ## New issue -Create a new project issue. +Creates a new project issue. ``` POST /projects/:id/issues @@ -150,11 +155,10 @@ Parameters: + `milestone_id` (optional) - The ID of a milestone to assign issue + `labels` (optional) - Comma-separated label names for an issue -Will return created issue with status `201 Created` on success, or `404 Not found` on fail. ## Edit issue -Update an existing project issue. +Updates an existing project issue. This function is also used to mark an issue as closed. ``` PUT /projects/:id/issues/:issue_id @@ -169,7 +173,21 @@ Parameters: + `assignee_id` (optional) - The ID of a user to assign issue + `milestone_id` (optional) - The ID of a milestone to assign issue + `labels` (optional) - Comma-separated label names for an issue -+ `closed` (optional) - The state of an issue (0 = false, 1 = true) ++ `state_event` (optional) - The state event of an issue ('close' to close issue and 'reopen' to reopen it) + + +## Delete existing issue (**Deprecated**) + +The function is deprecated and returns a `405 Method Not Allowed` +error if called. An issue gets now closed and is done by calling `PUT /projects/:id/issues/:issue_id` with +parameter `closed` set to 1. + +``` +DELETE /projects/:id/issues/:issue_id +``` + +Parameters: -Will return updated issue with status `200 OK` on success, or `404 Not found` on fail. ++ `id` (required) - The project ID ++ `issue_id` (required) - The ID of the issue diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 525c55d12c2..111c52112eb 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -1,6 +1,7 @@ ## List merge requests -Get all MR for this project. +Get all merge requests for this project. This function takes pagination parameters +`page` and `per_page` to restrict the list of merge requests. ``` GET /projects/:id/merge_requests @@ -40,9 +41,10 @@ Parameters: ] ``` -## Show MR -Show information about MR. +## Get single MR + +Shows information about a single merge request. ``` GET /projects/:id/merge_request/:merge_request_id @@ -84,7 +86,7 @@ Parameters: ## Create MR -Create MR. +Creates a new merge request. ``` POST /projects/:id/merge_requests @@ -126,9 +128,10 @@ Parameters: } ``` + ## Update MR -Update MR. You can change branches, title, or even close the MR. +Updates an existing merge request. You can change branches, title, or even close the MR. ``` PUT /projects/:id/merge_request/:merge_request_id @@ -172,9 +175,11 @@ Parameters: } } ``` + + ## Post comment to MR -Post comment to MR +Adds a comment to a merge request. ``` POST /projects/:id/merge_request/:merge_request_id/comments @@ -183,10 +188,9 @@ POST /projects/:id/merge_request/:merge_request_id/comments Parameters: + `id` (required) - The ID of a project -+ `merge_request_id` (required) - ID of MR ++ `merge_request_id` (required) - ID of merge request + `note` (required) - Text of comment -Will return created note with status `201 Created` on success, or `404 Not found` on fail. ```json { diff --git a/doc/api/milestones.md b/doc/api/milestones.md index b997e83901b..aa8f1bf5e02 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -1,6 +1,6 @@ ## List project milestones -Get a list of project milestones. +Returns a list of project milestones. ``` GET /projects/:id/milestones @@ -10,9 +10,10 @@ Parameters: + `id` (required) - The ID of a project -## Single milestone -Get a single project milestone. +## Get single milestone + +Gets a single project milestone. ``` GET /projects/:id/milestones/:milestone_id @@ -23,9 +24,10 @@ Parameters: + `id` (required) - The ID of a project + `milestone_id` (required) - The ID of a project milestone -## New milestone -Create a new project milestone. +## Create new milestone + +Creates a new project milestone. ``` POST /projects/:id/milestones @@ -34,14 +36,14 @@ POST /projects/:id/milestones Parameters: + `id` (required) - The ID of a project -+ `milestone_id` (required) - The ID of a project milestone + `title` (required) - The title of an milestone + `description` (optional) - The description of the milestone + `due_date` (optional) - The due date of the milestone + ## Edit milestone -Update an existing project milestone. +Updates an existing project milestone. ``` PUT /projects/:id/milestones/:milestone_id @@ -54,4 +56,5 @@ Parameters: + `title` (optional) - The title of a milestone + `description` (optional) - The description of a milestone + `due_date` (optional) - The due date of the milestone -+ `closed` (optional) - The status of the milestone ++ `state_event` (optional) - The state event of the milestone (close|activate) + diff --git a/doc/api/notes.md b/doc/api/notes.md index a4ba2826076..4b57f636a01 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -1,4 +1,4 @@ -## List notes +## Wall ### List project wall notes @@ -30,22 +30,40 @@ Parameters: + `id` (required) - The ID of a project -### List merge request notes -Get a list of merge request notes. +### Get single wall note + +Returns a single wall note. ``` -GET /projects/:id/merge_requests/:merge_request_id/notes +GET /projects/:id/notes/:note_id ``` Parameters: + `id` (required) - The ID of a project -+ `merge_request_id` (required) - The ID of an merge request ++ `note_id` (required) - The ID of a wall note -### List issue notes -Get a list of issue notes. +### Create new wall note + +Creates a new wall note. + +``` +POST /projects/:id/notes +``` + +Parameters: + ++ `id` (required) - The ID of a project ++ `body` (required) - The content of a note + + +## Issues + +### List project issue notes + +Gets a list of all notes for a single issue. ``` GET /projects/:id/issues/:issue_id/notes @@ -56,54 +74,59 @@ Parameters: + `id` (required) - The ID of a project + `issue_id` (required) - The ID of an issue -### List snippet notes -Get a list of snippet notes. +### Get single issue note + +Returns a single note for a specific project issue ``` -GET /projects/:id/snippets/:snippet_id/notes +GET /projects/:id/issues/:issue_id/notes/:note_id ``` Parameters: + `id` (required) - The ID of a project -+ `snippet_id` (required) - The ID of a snippet ++ `issue_id` (required) - The ID of a project issue ++ `note_id` (required) - The ID of an issue note -## Single note -### Single wall note +### Create new issue note -Get a wall note. +Creates a new note to a single project issue. ``` -GET /projects/:id/notes/:note_id +POST /projects/:id/issues/:issue_id/notes ``` Parameters: + `id` (required) - The ID of a project -+ `note_id` (required) - The ID of a wall note ++ `issue_id` (required) - The ID of an issue ++ `body` (required) - The content of a note -### Single issue note -Get an issue note. +## Snippets + +### List all snippet notes + +Gets a list of all notes for a single snippet. Snippet notes are comments users can post to a snippet. ``` -GET /projects/:id/issues/:issue_id/:notes/:note_id +GET /projects/:id/snippets/:snippet_id/notes ``` Parameters: + `id` (required) - The ID of a project -+ `issue_id` (required) - The ID of a project issue -+ `note_id` (required) - The ID of an issue note ++ `snippet_id` (required) - The ID of a project snippet + -### Single snippet note +### Get single snippet note -Get a snippet note. +Returns a single note for a given snippet. ``` -GET /projects/:id/issues/:snippet_id/:notes/:note_id +GET /projects/:id/snippets/:snippet_id/notes/:note_id ``` Parameters: @@ -112,52 +135,64 @@ Parameters: + `snippet_id` (required) - The ID of a project snippet + `note_id` (required) - The ID of an snippet note -## New note -### New wall note +### Create new snippet note -Create a new wall note. +Creates a new note for a single snippet. Snippet notes are comments users can post to a snippet. ``` -POST /projects/:id/notes +POST /projects/:id/snippets/:snippet_id/notes ``` Parameters: + `id` (required) - The ID of a project ++ `snippet_id` (required) - The ID of an snippet + `body` (required) - The content of a note -Will return created note with status `201 Created` on success, or `404 Not found` on fail. +## Merge Requests -### New issue note +### List all merge request notes -Create a new issue note. +Gets a list of all notes for a single merge request. ``` -POST /projects/:id/issues/:issue_id/notes +GET /projects/:id/merge_requests/:merge_request_id/notes ``` Parameters: + `id` (required) - The ID of a project -+ `issue_id` (required) - The ID of an issue -+ `body` (required) - The content of a note ++ `merge_request_id` (required) - The ID of a project merge request -Will return created note with status `201 Created` on success, or `404 Not found` on fail. -### New snippet note +### Get single merge request note -Create a new snippet note. +Returns a single note for a given merge request. ``` -POST /projects/:id/snippets/:snippet_id/notes +GET /projects/:id/merge_requests/:merge_request_id/notes/:note_id ``` Parameters: + `id` (required) - The ID of a project -+ `snippet_id` (required) - The ID of an snippet ++ `merge_request_id` (required) - The ID of a project merge request ++ `note_id` (required) - The ID of a merge request note + + +### Create new merge request note + +Creates a new note for a single merge request. + +``` +POST /projects/:id/merge_requests/:merge_request_id/notes +``` + +Parameters: + ++ `id` (required) - The ID of a project ++ `merge_request_id` (required) - The ID of a merge request + `body` (required) - The content of a note -Will return created note with status `201 Created` on success, or `404 Not found` on fail. diff --git a/doc/api/snippets.md b/doc/api/project_snippets.md index ceb8a63d06f..04ea367d518 100644 --- a/doc/api/snippets.md +++ b/doc/api/project_snippets.md @@ -10,9 +10,10 @@ Parameters: + `id` (required) - The ID of a project + ## Single snippet -Get a project snippet. +Get a single project snippet. ``` GET /projects/:id/snippets/:snippet_id @@ -42,22 +43,10 @@ Parameters: } ``` -## Snippet content - -Get a raw project snippet. - -``` -GET /projects/:id/snippets/:snippet_id/raw -``` - -Parameters: - -+ `id` (required) - The ID of a project -+ `snippet_id` (required) - The ID of a project's snippet -## New snippet +## Create new snippet -Create a new project snippet. +Creates a new project snippet. The user must have permission to create new snippets. ``` POST /projects/:id/snippets @@ -71,11 +60,10 @@ Parameters: + `lifetime` (optional) - The expiration date of a snippet + `code` (required) - The content of a snippet -Will return created snippet with status `201 Created` on success, or `404 Not found` on fail. -## Edit snippet +## Update snippet -Update an existing project snippet. +Updates an existing project snippet. The user must have permission to change an existing snippet. ``` PUT /projects/:id/snippets/:snippet_id @@ -90,11 +78,11 @@ Parameters: + `lifetime` (optional) - The expiration date of a snippet + `code` (optional) - The content of a snippet -Will return updated snippet with status `200 OK` on success, or `404 Not found` on fail. ## Delete snippet -Delete existing project snippet. +Deletes an existing project snippet. This is an idempotent function and deleting a non-existent +snippet still returns a `200 Ok` status code. ``` DELETE /projects/:id/snippets/:snippet_id @@ -105,5 +93,16 @@ Parameters: + `id` (required) - The ID of a project + `snippet_id` (required) - The ID of a project's snippet -Status code `200` will be returned on success. +## Snippet content + +Returns the raw project snippet as plain text. + +``` +GET /projects/:id/snippets/:snippet_id/raw +``` + +Parameters: + ++ `id` (required) - The ID of a project ++ `snippet_id` (required) - The ID of a project's snippet diff --git a/doc/api/projects.md b/doc/api/projects.md index 82bb0c0d561..5150331e7d7 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1,4 +1,6 @@ -## List projects +## Projects + +### List projects Get a list of projects owned by the authenticated user. @@ -21,14 +23,15 @@ GET /projects "blocked": false, "created_at": "2012-05-23T08:00:58Z" }, - "private": true, + "public": true, "path": "rails", "path_with_namespace": "rails/rails", "issues_enabled": false, "merge_requests_enabled": false, "wall_enabled": true, "wiki_enabled": true, - "created_at": "2012-05-23T08:05:02Z" + "created_at": "2012-05-23T08:05:02Z", + "last_activity_at": "2012-05-23T08:05:02Z" }, { "id": 5, @@ -43,21 +46,25 @@ GET /projects "blocked": false, "created_at": "2012-05-23T08:00:58Z" }, - "private": true, + "public": true, "path": "gitlab", "path_with_namespace": "randx/gitlab", "issues_enabled": true, "merge_requests_enabled": true, "wall_enabled": true, "wiki_enabled": true, - "created_at": "2012-05-30T12:49:20Z" + "snippets_enabled": true, + "created_at": "2012-05-30T12:49:20Z", + "last_activity_at": "2012-05-23T08:05:02Z" } ] ``` -## Single project -Get a specific project, identified by project ID, which is owned by the authentication user. +### Get single project + +Get a specific project, identified by project ID or NAME, which is owned by the authentication user. +Currently namespaced projects cannot retrieved by name. ``` GET /projects/:id @@ -65,12 +72,13 @@ GET /projects/:id Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project ```json { "id": 5, "name": "gitlab", + "name_with_namespace": "GitLab / gitlabhq", "description": null, "default_branch": "api", "owner": { @@ -81,20 +89,91 @@ Parameters: "blocked": false, "created_at": "2012-05-23T08:00:58Z" }, - "private": true, + "public": true, "path": "gitlab", "path_with_namespace": "randx/gitlab", "issues_enabled": true, "merge_requests_enabled": true, "wall_enabled": true, "wiki_enabled": true, - "created_at": "2012-05-30T12:49:20Z" + "snippets_enabled": true, + "created_at": "2012-05-30T12:49:20Z", + "last_activity_at": "2012-05-23T08:05:02Z" } ``` -## Create project +### Get project events + +Get a project events for specific project. +Sorted from newest to latest + +``` +GET /projects/:id/events +``` + +Parameters: + ++ `id` (required) - The ID or NAME of a project + +```json + +[{ + "title": null, + "project_id": 15, + "action_name": "closed", + "target_id": 830, + "target_type": "Issue", + "author_id": 1, + "data": null, + "target_title": "Public project search field" +}, { + "title": null, + "project_id": 15, + "action_name": "opened", + "target_id": null, + "target_type": null, + "author_id": 1, + "data": { + "before": "50d4420237a9de7be1304607147aec22e4a14af7", + "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", + "ref": "refs/heads/master", + "user_id": 1, + "user_name": "Dmitriy Zaporozhets", + "repository": { + "name": "gitlabhq", + "url": "git@dev.gitlab.org:gitlab/gitlabhq.git", + "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.", + "homepage": "https://dev.gitlab.org/gitlab/gitlabhq" + }, + "commits": [{ + "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", + "message": "Add simple search to projects in public area", + "timestamp": "2013-05-13T18:18:08+00:00", + "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428", + "author": { + "name": "Dmitriy Zaporozhets", + "email": "dmitriy.zaporozhets@gmail.com" + } + }], + "total_commits_count": 1 + }, + "target_title": null +}, { + "title": null, + "project_id": 15, + "action_name": "closed", + "target_id": 840, + "target_type": "Issue", + "author_id": 1, + "data": null, + "target_title": "Finish & merge Code search PR" +}] +``` + -Create new project owned by user +### Create project + +Creates new project owned by user. ``` POST /projects @@ -105,15 +184,51 @@ Parameters: + `name` (required) - new project name + `description` (optional) - short project description + `default_branch` (optional) - 'master' by default -+ `issues_enabled` (optional) - enabled by default -+ `wall_enabled` (optional) - enabled by default -+ `merge_requests_enabled` (optional) - enabled by default -+ `wiki_enabled` (optional) - enabled by default ++ `issues_enabled` (optional) ++ `wall_enabled` (optional) ++ `merge_requests_enabled` (optional) ++ `wiki_enabled` (optional) ++ `snippets_enabled` (optional) ++ `public` (optional) + +**Project access levels** + +The project access levels are defined in the `user_project.rb` class. Currently, these levels are recognized: + +``` + GUEST = 10 + REPORTER = 20 + DEVELOPER = 30 + MASTER = 40 +``` + -Will return created project with status `201 Created` on success, or `404 Not -found` on fail. +### Create project for user -## List project team members +Creates a new project owned by user. Available only for admins. + +``` +POST /projects/user/:user_id +``` + +Parameters: + ++ `user_id` (required) - user_id of owner ++ `name` (required) - new project name ++ `description` (optional) - short project description ++ `default_branch` (optional) - 'master' by default ++ `issues_enabled` (optional) ++ `wall_enabled` (optional) ++ `merge_requests_enabled` (optional) ++ `wiki_enabled` (optional) ++ `snippets_enabled` (optional) ++ `public` (optional) + + + +## Team members + +### List project team members Get a list of project team members. @@ -123,12 +238,13 @@ GET /projects/:id/members Parameters: -+ `id` (required) - The ID of a project -+ `query` - Query string ++ `id` (required) - The ID or NAME of a project ++ `query` (optional) - Query string to search for members + -## Get project team member +### Get project team member -Get a project team member. +Gets a project team member. ``` GET /projects/:id/members/:user_id @@ -136,12 +252,11 @@ GET /projects/:id/members/:user_id Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `user_id` (required) - The ID of a user ```json { - "id": 1, "username": "john_smith", "email": "john@example.com", @@ -152,9 +267,12 @@ Parameters: } ``` -## Add project team member -Add a user to a project team. +### Add project team member + +Adds a user to a project team. This is an idempotent method and can be called multiple times +with the same parameters. Adding team membership to a user that is already a member does not +affect the existing membership. ``` POST /projects/:id/members @@ -162,15 +280,14 @@ POST /projects/:id/members Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `user_id` (required) - The ID of a user to add + `access_level` (required) - Project access level -Will return status `201 Created` on success, or `404 Not found` on fail. -## Edit project team member +### Edit project team member -Update project team member to specified access level. +Updates project team member to a specified access level. ``` PUT /projects/:id/members/:user_id @@ -178,13 +295,12 @@ PUT /projects/:id/members/:user_id Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `user_id` (required) - The ID of a team member + `access_level` (required) - Project access level -Will return status `200 OK` on success, or `404 Not found` on fail. -## Remove project team member +### Remove project team member Removes user from project team. @@ -194,14 +310,20 @@ DELETE /projects/:id/members/:user_id Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `user_id` (required) - The ID of a team member -Status code `200` will be returned on success. +This method is idempotent and can be called multiple times with the same parameters. +Revoking team membership for a user who is not currently a team member is considered success. +Please note that the returned JSON currently differs slightly. Thus you should not +rely on the returned JSON structure. -## List project hooks -Get list for project hooks +## Hooks + +### List project hooks + +Get list of project hooks. ``` GET /projects/:id/hooks @@ -209,13 +331,12 @@ GET /projects/:id/hooks Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project -Will return hooks with status `200 OK` on success, or `404 Not found` on fail. -## Get project hook +### Get project hook -Get hook for project +Get a specific hook for project. ``` GET /projects/:id/hooks/:hook_id @@ -223,14 +344,21 @@ GET /projects/:id/hooks/:hook_id Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `hook_id` (required) - The ID of a project hook -Will return hook with status `200 OK` on success, or `404 Not found` on fail. +```json +{ + "id": 1, + "url": "http://example.com/hook", + "created_at": "2012-10-12T17:04:47Z" +} +``` -## Add project hook -Add hook to project +### Add project hook + +Adds a hook to project. ``` POST /projects/:id/hooks @@ -238,14 +366,13 @@ POST /projects/:id/hooks Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `url` (required) - The hook URL -Will return status `201 Created` on success, or `404 Not found` on fail. -## Edit project hook +### Edit project hook -Edit hook for project +Edits a hook for project. ``` PUT /projects/:id/hooks/:hook_id @@ -253,24 +380,122 @@ PUT /projects/:id/hooks/:hook_id Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `hook_id` (required) - The ID of a project hook + `url` (required) - The hook URL -Will return status `201 Created` on success, or `404 Not found` on fail. - -## Delete project hook +### Delete project hook -Delete hook from project +Removes a hook from project. This is an idempotent method and can be called multiple times. +Either the hook is available or not. ``` -DELETE /projects/:id/hooks +DELETE /projects/:id/hooks/:hook_id ``` Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `hook_id` (required) - The ID of hook to delete -Will return status `200 OK` on success, or `404 Not found` on fail. +Note the JSON response differs if the hook is available or not. If the project hook +is available before it is returned in the JSON response or an empty response is returned. + + +## Branches + +### List branches + +Lists all branches of a project. + +``` +GET /projects/:id/repository/branches +``` + +Parameters: + ++ `id` (required) - The ID of the project + + +### List single branch + +Lists a specific branch of a project. + +``` +GET /projects/:id/repository/branches/:branch +``` + +Parameters: + ++ `id` (required) - The ID of the project. ++ `branch` (required) - The name of the branch. + + +### Protect single branch + +Protects a single branch of a project. + +``` +PUT /projects/:id/repository/branches/:branch/protect +``` + +Parameters: + ++ `id` (required) - The ID of the project. ++ `branch` (required) - The name of the branch. + + +### Unprotect single branch + +Unprotects a single branch of a project. + +``` +PUT /projects/:id/repository/branches/:branch/unprotect +``` + +Parameters: + ++ `id` (required) - The ID of the project. ++ `branch` (required) - The name of the branch. + + +## Admin fork relation + +Allows modification of the forked relationship between existing projects. . Available only for admins. + +### Create a forked from/to relation between existing projects. + +``` +POST /projects/:id/fork/:forked_from_id +``` + +Parameters: + ++ `id` (required) - The ID of the project ++ `forked_from_id:` (required) - The ID of the project that was forked from + +### Delete an existing forked from relationship + +``` +DELETE /projects/:id/fork +``` + +Parameter: + ++ `id` (required) - The ID of the project + + +## Search for projects by name + +Search for projects by name which are public or the calling user has access to + +``` +GET /projects/search/:query +``` + +Parameters: + ++ query (required) - A string contained in the project name ++ per_page (optional) - number of projects to return per page ++ page (optional) - the page to retrieve diff --git a/doc/api/repositories.md b/doc/api/repositories.md index fd0ef1f53eb..cb0626972e5 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -1,4 +1,4 @@ -## Project repository branches +## List repository branches Get a list of repository branches from a project, sorted by name alphabetically. @@ -39,7 +39,8 @@ Parameters: ] ``` -## Project repository branch + +## Get single repository branch Get a single project repository branch. @@ -79,12 +80,11 @@ Parameters: } ``` -Will return status code `200` on success or `404 Not found` if the branch is not available. - -## Protect a project repository branch +## Protect repository branch -Protect a single project repository branch. +Protects a single project repository branch. This is an idempotent function, protecting an already +protected repository branch still returns a `200 Ok` status code. ``` PUT /projects/:id/repository/branches/:branch/protect @@ -122,9 +122,11 @@ Parameters: } ``` -## Unprotect a project repository branch -Unprotect a single project repository branch. +## Unprotect repository branch + +Unprotects a single project repository branch. This is an idempotent function, unprotecting an already +unprotected repository branch still returns a `200 Ok` status code. ``` PUT /projects/:id/repository/branches/:branch/unprotect @@ -162,7 +164,8 @@ Parameters: } ``` -## Project repository tags + +## List project repository tags Get a list of repository tags from a project, sorted by name in reverse alphabetical order. @@ -201,7 +204,8 @@ Parameters: ] ``` -## Project repository commits + +## List repository commits Get a list of repository commits in a project. @@ -212,7 +216,7 @@ GET /projects/:id/repository/commits Parameters: + `id` (required) - The ID of a project -+ `ref_name` (optional) - The name of a repository branch or tag ++ `ref_name` (optional) - The name of a repository branch or tag or if not given the default branch ```json [ @@ -235,12 +239,116 @@ Parameters: ] ``` +## Get a single commit + +Get a specific commit identified by the commit hash or name of a branch or tag. + +``` +GET /projects/:id/repository/commits/:sha +``` + +Parameters: + ++ `id` (required) - The ID of a project ++ `sha` (required) - The commit hash or name of a repository branch or tag + +```json +{ + "id": "6104942438c14ec7bd21c6cd5bd995272b3faff6", + "short_id": "6104942438c", + "title": "Sanitize for network graph", + "author_name": "randx", + "author_email": "dmitriy.zaporozhets@gmail.com", + "created_at": "2012-09-20T09:06:12+03:00" +} +``` + + +## Get the diff of a commit + +Get the diff of a commit in a project. + +``` +GET /projects/:id/repository/commits/:sha/diff +``` + +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 +[ + { + "diff": "--- a/doc/update/5.4-to-6.0.md\n+++ b/doc/update/5.4-to-6.0.md\n@@ -71,6 +71,8 @@\n sudo -u git -H bundle exec rake migrate_keys RAILS_ENV=production\n sudo -u git -H bundle exec rake migrate_inline_notes RAILS_ENV=production\n \n+sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production\n+\n ```\n \n ### 6. Update config files", + "new_path": "doc/update/5.4-to-6.0.md", + "old_path": "doc/update/5.4-to-6.0.md", + "a_mode": null, + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false + } +] +``` + +## List repository tree + +Get a list of repository files and directories in a project. + +``` +GET /projects/:id/repository/tree +``` + +Parameters: + ++ `id` (required) - The ID of a project ++ `path` (optional) - The path inside repository. Used to get contend of subdirectories ++ `ref_name` (optional) - The name of a repository branch or tag or if not given the default branch + +```json + +[{ + "name": "assets", + "type": "tree", + "mode": "040000", + "id": "6229c43a7e16fcc7e95f923f8ddadb8281d9c6c6" +}, { + "name": "contexts", + "type": "tree", + "mode": "040000", + "id": "faf1cdf33feadc7973118ca42d35f1e62977e91f" +}, { + "name": "controllers", + "type": "tree", + "mode": "040000", + "id": "95633e8d258bf3dfba3a5268fb8440d263218d74" +}, { + "name": "Rakefile", + "type": "blob", + "mode": "100644", + "id": "35b2f05cbb4566b71b34554cf184a9d0bd9d46d6" +}, { + "name": "VERSION", + "type": "blob", + "mode": "100644", + "id": "803e4a4f3727286c3093c63870c2b6524d30ec4f" +}, { + "name": "config.ru", + "type": "blob", + "mode": "100644", + "id": "dfd2d862237323aa599be31b473d70a8a817943b" +}] + +``` + + ## Raw blob content Get the raw file contents for a file. ``` -GET /projects/:id/repository/commits/:sha/blob +GET /projects/:id/repository/blobs/:sha ``` Parameters: @@ -248,5 +356,3 @@ Parameters: + `id` (required) - The ID of a project + `sha` (required) - The commit or branch name + `filepath` (required) - The path the file - -Will return the raw file contents. diff --git a/doc/api/session.md b/doc/api/session.md index c7e57aaca7a..162d4c8bf78 100644 --- a/doc/api/session.md +++ b/doc/api/session.md @@ -6,10 +6,13 @@ POST /session Parameters: -+ `email` (required) - The email of user ++ `login` (required) - The login of user ++ `email` (required if login missing) - The email of user + `password` (required) - Valid password +__You can login with both GitLab and LDAP credentials now__ + ```json { "id": 1, @@ -17,7 +20,17 @@ Parameters: "email": "john@example.com", "name": "John Smith", "private_token": "dd34asd13as", + "blocked": false, "created_at": "2012-05-23T08:00:58Z", - "blocked": true + "bio": null, + "skype": "", + "linkedin": "", + "twitter": "", + "dark_scheme": false, + "theme_id": 1, + "is_admin": false, + "can_create_group" : true, + "can_create_team" : true, + "can_create_project" : true } ``` diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md new file mode 100644 index 00000000000..dca22c43f83 --- /dev/null +++ b/doc/api/system_hooks.md @@ -0,0 +1,49 @@ +All methods require admin authorization.
+
+## List system hooks
+
+Get list of system hooks
+
+```
+GET /hooks
+```
+
+Parameters:
+
++ **none**
+
+
+## Add new system hook hook
+
+```
+POST /hooks
+```
+
+Parameters:
+
++ `url` (required) - The hook URL
+
+
+## Test system hook
+
+```
+GET /hooks/:id
+```
+
+Parameters:
+
++ `id` (required) - The ID of hook
+
+
+## Delete system hook
+
+Deletes a system hook. This is an idempotent API function and returns `200 Ok` even if the hook
+is not available. If the hook is deleted it is also returned as JSON.
+
+```
+DELETE /hooks/:id
+```
+
+Parameters:
+
++ `id` (required) - The ID of hook
diff --git a/doc/api/user_teams.md b/doc/api/user_teams.md new file mode 100644 index 00000000000..cf467a54667 --- /dev/null +++ b/doc/api/user_teams.md @@ -0,0 +1,209 @@ +## User teams + +### List user teams + +Get a list of user teams viewable by the authenticated user. + +``` +GET /user_teams +``` + +```json +[ + { + id: 1, + name: "User team 1", + path: "user_team1", + owner_id: 1 + }, + { + id: 2, + name: "User team 2", + path: "user_team2", + owner_id: 1 + } +] +``` + + +### Get single user team + +Get a specific user team, identified by user team ID, which is viewable by the authenticated user. + +``` +GET /user_teams/:id +``` + +Parameters: + ++ `id` (required) - The ID of a user_team + +```json +{ + id: 1, + name: "User team 1", + path: "user_team1", + owner_id: 1 +} +``` + + +### Create user team + +Creates new user team owned by user. Available only for admins. + +``` +POST /user_teams +``` + +Parameters: + ++ `name` (required) - new user team name ++ `path` (required) - new user team internal name + + + +## User team members + +### List user team members + +Get a list of project team members. + +``` +GET /user_teams/:id/members +``` + +Parameters: + ++ `id` (required) - The ID of a user_team + + +### Get user team member + +Gets a user team member. + +``` +GET /user_teams/:id/members/:user_id +``` + +Parameters: + ++ `id` (required) - The ID of a user_team ++ `user_id` (required) - The ID of a user + +```json +{ + id: 2, + username: "john_doe", + email: "joh@doe.org", + name: "John Doe", + state: "active", + created_at: "2012-10-22T14:13:35Z", + access_level: 30 +} +``` + + +### Add user team member + +Adds a user to a user team. + +``` +POST /user_teams/:id/members +``` + +Parameters: + ++ `id` (required) - The ID of a user team ++ `user_id` (required) - The ID of a user to add ++ `access_level` (required) - Project access level + + +### Remove user team member + +Removes user from user team. + +``` +DELETE /user_teams/:id/members/:user_id +``` + +Parameters: + ++ `id` (required) - The ID of a user team ++ `user_id` (required) - The ID of a team member + +## User team projects + +### List user team projects + +Get a list of project team projects. + +``` +GET /user_teams/:id/projects +``` + +Parameters: + ++ `id` (required) - The ID of a user_team + + +### Get user team project + +Gets a user team project. + +``` +GET /user_teams/:id/projects/:project_id +``` + +Parameters: + ++ `id` (required) - The ID of a user_team ++ `project_id` (required) - The ID of a user + +```json +{ + id: 12, + name: "project1", + description: null, + default_branch: "develop", + public: false, + path: "project1", + path_with_namespace: "group1/project1", + issues_enabled: false, + merge_requests_enabled: true, + wall_enabled: true, + wiki_enabled: false, + created_at: "2013-03-11T12:59:08Z", + greatest_access_level: 30 +} +``` + + +### Add user team project + +Adds a project to a user team. + +``` +POST /user_teams/:id/projects +``` + +Parameters: + ++ `id` (required) - The ID of a user team ++ `project_id` (required) - The ID of a project to add ++ `greatest_access_level` (required) - Maximum project access level + + +### Remove user team project + +Removes project from user team. + +``` +DELETE /user_teams/:id/projects/:project_id +``` + +Parameters: + ++ `id` (required) - The ID of a user team ++ `project_id` (required) - The ID of a team project + diff --git a/doc/api/users.md b/doc/api/users.md index b94d7c0f789..49afbab8c6a 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -1,6 +1,7 @@ ## List users Get a list of users. +This function takes pagination parameters `page` and `per_page` to restrict the list of users. ``` GET /users @@ -13,36 +14,37 @@ GET /users "username": "john_smith", "email": "john@example.com", "name": "John Smith", - "blocked": false, + "state": "active", "created_at": "2012-05-23T08:00:58Z", "bio": null, "skype": "", "linkedin": "", "twitter": "", - "dark_scheme": false, "extern_uid": "john.smith", "provider": "provider_name", - "theme_id": 1 + "theme_id": 1, + "color_scheme_id": 2 }, { "id": 2, "username": "jack_smith", "email": "jack@example.com", "name": "Jack Smith", - "blocked": false, + "state": "blocked", "created_at": "2012-05-23T08:01:01Z", "bio": null, "skype": "", "linkedin": "", "twitter": "", - "dark_scheme": true, "extern_uid": "jack.smith", "provider": "provider_name", - "theme_id": 1 + "theme_id": 1, + "color_scheme_id": 3 } ] ``` + ## Single user Get a single user. @@ -61,50 +63,53 @@ Parameters: "username": "john_smith", "email": "john@example.com", "name": "John Smith", - "blocked": false, + "state": "active", "created_at": "2012-05-23T08:00:58Z", "bio": null, "skype": "", "linkedin": "", "twitter": "", - "dark_scheme": false, "extern_uid": "john.smith", "provider": "provider_name", - "theme_id": 1 + "theme_id": 1, + "color_scheme_id": 2 } ``` + ## User creation -Create user. Available only for admin + +Creates a new user. Note only administrators can create new users. ``` POST /users ``` Parameters: -+ `email` (required) - Email -+ `password` (required) - Password -+ `username` (required) - Username -+ `name` (required) - Name -+ `skype` - Skype ID -+ `linkedin` - Linkedin -+ `twitter` - Twitter account -+ `projects_limit` - Number of projects user can create -+ `extern_uid` - External UID -+ `provider` - External provider name -+ `bio` - User's bio -Will return created user with status `201 Created` on success, or `404 Not -found` on fail. ++ `email` (required) - Email ++ `password` (required) - Password ++ `username` (required) - Username ++ `name` (required) - Name ++ `skype` (optional) - Skype ID ++ `linkedin` (optional) - Linkedin ++ `twitter` (optional) - Twitter account ++ `projects_limit` (optional) - Number of projects user can create ++ `extern_uid` (optional) - External UID ++ `provider` (optional) - External provider name ++ `bio` (optional) - User's bio + ## User modification -Modify user. Available only for admin + +Modifies an existing user. Only administrators can change attributes of a user. ``` PUT /users/:id ``` Parameters: + + `email` - Email + `username` - Username + `name` - Name @@ -112,28 +117,33 @@ Parameters: + `skype` - Skype ID + `linkedin` - Linkedin + `twitter` - Twitter account -+ `projects_limit` - Limit projects wich user can create ++ `projects_limit` - Limit projects each user can create + `extern_uid` - External UID + `provider` - External provider name + `bio` - User's bio +Note, at the moment this method does only return a 404 error, even in cases where a 409 (Conflict) would +be more appropriate, e.g. when renaming the email address to some existing one. -Will return created user with status `200 OK` on success, or `404 Not -found` on fail. ## User deletion -Delete user. Available only for admin + +Deletes a user. Available only for administrators. This is an idempotent function, calling this function +for a non-existent user id still returns a status code `200 Ok`. The JSON response differs if the user +was actually deleted or not. In the former the user is returned and in the latter not. ``` DELETE /users/:id ``` -Will return deleted user with status `200 OK` on success, or `404 Not -found` on fail. +Parameters: + ++ `id` (required) - The ID of the user + ## Current user -Get currently authenticated user. +Gets currently authenticated user. ``` GET /user @@ -145,17 +155,23 @@ GET /user "username": "john_smith", "email": "john@example.com", "name": "John Smith", - "blocked": false, + "private_token": "dd34asd13as", + "state": "active", "created_at": "2012-05-23T08:00:58Z", "bio": null, "skype": "", "linkedin": "", "twitter": "", - "dark_scheme": false, - "theme_id": 1 + "theme_id": 1, + "color_scheme_id": 2, + "is_admin": false, + "can_create_group" : true, + "can_create_team" : true, + "can_create_project" : true } ``` + ## List SSH keys Get a list of currently authenticated user's SSH keys. @@ -168,14 +184,14 @@ GET /user/keys [ { "id": 1, - "title" : "Public key" + "title" : "Public key", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4 596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4 soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", }, { "id": 3, - "title" : "Another Public key" + "title" : "Another Public key", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4 596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4 soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" @@ -183,6 +199,11 @@ GET /user/keys ] ``` +Parameters: + ++ **none** + + ## Single SSH key Get a single key. @@ -198,15 +219,17 @@ Parameters: ```json { "id": 1, - "title" : "Public key" + "title" : "Public key", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4 596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4 soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" } ``` + + ## Add SSH key -Create new key owned by currently authenticated user +Creates a new key owned by the currently authenticated user. ``` POST /user/keys @@ -217,12 +240,28 @@ Parameters: + `title` (required) - new SSH Key's title + `key` (required) - new SSH key + +## Add SSH key for user + +Create new key owned by specified user. Available only for admin + +``` +POST /users/:id/keys +``` + +Parameters: + ++ `id` (required) - id of specified user ++ `title` (required) - new SSH Key's title ++ `key` (required) - new SSH key + Will return created key with status `201 Created` on success, or `404 Not found` on fail. ## Delete SSH key -Delete key owned by currently authenticated user +Deletes key owned by currently authenticated user. This is an idempotent function and calling it on a key that is already +deleted or not available results in `200 Ok`. ``` DELETE /user/keys/:id @@ -232,4 +271,3 @@ Parameters: + `id` (required) - SSH key ID -Will return `200 OK` on success, or `404 Not Found` on fail. diff --git a/doc/install/databases.md b/doc/install/databases.md index 4c6c084d0b9..6477e1c967c 100644 --- a/doc/install/databases.md +++ b/doc/install/databases.md @@ -11,23 +11,43 @@ GitLab supports the following databases: # Install the database packages sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev + # Pick a database root password (can be anything), type it and press enter + # Retype the database root password and press enter + + # Secure your installation. + sudo mysql_secure_installation + # Login to MySQL - $ mysql -u root -p + mysql -u root -p - # Create a user for GitLab. (change $password to a real password) + # Type the database root password + + # Create a user for GitLab + # do not type the 'mysql>', this is part of the prompt + # change $password in the command below to a real password you pick mysql> CREATE USER 'gitlab'@'localhost' IDENTIFIED BY '$password'; # Create the GitLab production database mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`; - # Grant the GitLab user necessary permissopns on the table. - mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER ON `gitlabhq_production`.* TO 'gitlab'@'localhost'; + # Grant the GitLab user necessary permissions on the table. + mysql> GRANT SELECT, LOCK TABLES, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER ON `gitlabhq_production`.* TO 'gitlab'@'localhost'; # Quit the database session mysql> \q # Try connecting to the new database with the new user - sudo -u gitlab -H mysql -u gitlab -p -D gitlabhq_production + sudo -u git -H mysql -u gitlab -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 + + # You are done installing the database and can go back to the rest of the installation. + ## PostgreSQL @@ -38,14 +58,14 @@ GitLab supports the following databases: sudo -u postgres psql -d template1 # Create a user for GitLab. (change $password to a real password) - template1=# CREATE USER gitlab WITH PASSWORD '$password'; + template1=# CREATE USER git WITH PASSWORD '$password'; # Create the GitLab production database & grant all privileges on database - template1=# CREATE DATABASE gitlabhq_production OWNER gitlab; + template1=# CREATE DATABASE gitlabhq_production OWNER git; # Quit the database session template1=# \q # Try connecting to the new database with the new user - sudo -u gitlab -H psql -d gitlabhq_production + sudo -u git -H psql -d gitlabhq_production diff --git a/doc/install/installation.md b/doc/install/installation.md index 501ae6db87a..71a587d2ee3 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -1,19 +1,20 @@ -This installation guide was created for Debian/Ubuntu and tested on it. +# Select Version to Install +Make sure you view this installation guide 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). -Please read `doc/install/requirements.md` for hardware and platform requirements. + +If this is unclear check the [GitLab Blog](http://blog.gitlab.org/) for installation guide links by version. -**Important Note:** -The following steps have been known to work. -If you deviate from this guide, do it with caution and make sure you don't -violate any assumptions GitLab makes about its environment. -For things like AWS installation scripts, init scripts or config files for -alternative web server have a look at the "Advanced Setup Tips" section. +# Important notes +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. -**Important Note:** -If you find a bug/error in this guide please submit an issue or pull request -following the contribution guide (see `CONTRIBUTING.md`). +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 consult [the installation section in the readme](https://github.com/gitlabhq/gitlabhq#installation). + +The following steps have been known to work. Please **use caution when you deviate** from this guide. Make sure you don't violate any assumptions GitLab makes about its environment. For example many people run into permission problems because they changed the location of directories or run services as the wrong user. + +If you find a bug/error in this guide please **submit a pull request** following the [contributing guide](../../CONTRIBUTING.md). - - - @@ -24,7 +25,7 @@ The GitLab installation consists of setting up the following components: 1. Packages / Dependencies 2. Ruby 3. System Users -4. Gitolite +4. GitLab shell 5. Database 6. GitLab 7. Nginx @@ -32,32 +33,31 @@ The GitLab installation consists of setting up the following components: # 1. Packages / Dependencies -`sudo` is not installed on Debian by default. If you don't have it you'll need -to install it first. +`sudo` is not installed on Debian by default. Make sure your system is +up-to-date and install it. - # run as root - apt-get update && apt-get upgrade && apt-get install sudo - -Make sure your system is up-to-date: - - sudo apt-get update - sudo apt-get upgrade + # run as root! + apt-get update -y + apt-get upgrade -y + apt-get install sudo -y **Note:** -Vim is an editor that is used here whenever there are files that need to be -edited by hand. But, you can use any editor you like instead. +During this installation some files will need to be edited manually. +If you are familiar with vim set it as default editor with the commands below. +If you are not familiar with vim please skip this and keep using the default editor. - # Install vim + # Install vim and set as default editor sudo apt-get install -y vim + sudo update-alternatives --set editor /usr/bin/vim.basic Install the required packages: - sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl git-core openssh-server redis-server postfix checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev + sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl git-core openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev Make sure you have the right version of Python installed. # Install Python - sudo apt-get install python + sudo apt-get install -y python # Make sure that Python is 2.5+ (3.x is not supported at the moment) python --version @@ -71,21 +71,35 @@ Make sure you have the right version of Python installed. # If you get a "command not found" error create a link to the python binary sudo ln -s /usr/bin/python /usr/bin/python2 + # For reStructuredText markup language support install required package: + sudo apt-get install python-docutils + +**Note:** In order to receive mail notifications, make sure to install a +mail server. By default, Debian is shipped with exim4 whereas Ubuntu +does not ship with one. The recommended mail server is postfix and you can install it with: + + sudo apt-get install -y postfix + +Then select 'Internet Site' and press enter to confirm the hostname. # 2. Ruby -Download and compile it: +Remove the old Ruby 1.8 if present + + sudo apt-get remove ruby1.8 + +Download Ruby and compile it: mkdir /tmp/ruby && cd /tmp/ruby - curl --progress http://ftp.ruby-lang.org/pub/ruby/1.9/ruby-1.9.3-p327.tar.gz | tar xz - cd ruby-1.9.3-p327 + curl --progress ftp://ftp.ruby-lang.org/pub/ruby/2.0/ruby-2.0.0-p247.tar.gz | tar xz + cd ruby-2.0.0-p247 ./configure make sudo make install Install the Bundler Gem: - sudo gem install bundler + sudo gem install bundler --no-ri --no-rdoc # 3. System Users @@ -94,27 +108,35 @@ Create a `git` user for Gitlab: sudo adduser --disabled-login --gecos 'GitLab' git + # 4. GitLab shell - # login as git - sudo su git +GitLab Shell is an ssh access and repository management software developed specially for GitLab. - # go to home directory + # Go to home directory cd /home/git - # clone gitlab shell - git clone https://github.com/gitlabhq/gitlab-shell.git + # Clone gitlab shell + sudo -u git -H git clone https://github.com/gitlabhq/gitlab-shell.git - # setup cd gitlab-shell - cp config.yml.example config.yml - ./bin/install + # switch to right version + sudo -u git -H git checkout v1.7.1 + + sudo -u git -H cp config.yml.example config.yml + + # Edit config and replace gitlab_url + # with something like 'http://domain.com/' + sudo -u git -H editor config.yml + + # Do setup + sudo -u git -H ./bin/install # 5. Database -To setup the MySQL/PostgreSQL database and dependencies please see [`doc/install/databases.md`](./databases.md) . +To setup the MySQL/PostgreSQL database and dependencies please see [`doc/install/databases.md`](./databases.md). # 6. GitLab @@ -127,15 +149,14 @@ To setup the MySQL/PostgreSQL database and dependencies please see [`doc/install # Clone GitLab repository sudo -u git -H git clone https://github.com/gitlabhq/gitlabhq.git gitlab - # Go to gitlab dir + # Go to gitlab dir cd /home/git/gitlab - + # Checkout to stable release - sudo -u git -H git checkout 5-0-stable + sudo -u git -H git checkout 6-1-stable **Note:** -You can change `5-0-stable` to `master` if you want the *bleeding edge* version, but -do so with caution! +You can change `6-1-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ## Configure it @@ -146,7 +167,7 @@ do so with caution! # Make sure to change "localhost" to the fully-qualified domain name of your # host serving GitLab where necessary - sudo -u git -H vim config/gitlab.yml + sudo -u git -H editor config/gitlab.yml # Make sure GitLab can write to the log/ and tmp/ directories sudo chown -R git log/ @@ -154,48 +175,83 @@ do so with caution! sudo chmod -R u+rwX log/ sudo chmod -R u+rwX tmp/ - # Make directory for satellites + # Create directory for satellites sudo -u git -H mkdir /home/git/gitlab-satellites + # Create directories for sockets/pids and make sure GitLab can write to them + sudo -u git -H mkdir tmp/pids/ + sudo -u git -H mkdir tmp/sockets/ + sudo chmod -R u+rwX tmp/pids/ + sudo chmod -R u+rwX tmp/sockets/ + + # Create public/uploads directory otherwise backup will fail + sudo -u git -H mkdir public/uploads + sudo chmod -R u+rwX public/uploads + # Copy the example Unicorn config sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb + # Enable cluster mode if you expect to have a high load instance + # Ex. change amount of workers to 3 for 2GB RAM server + sudo -u git -H editor config/unicorn.rb + + # Configure Git global settings for git user, useful when editing via web + # Edit user.email according to what is set in gitlab.yml + sudo -u git -H git config --global user.name "GitLab" + sudo -u git -H git config --global user.email "gitlab@localhost" + sudo -u git -H git config --global core.autocrlf input + **Important Note:** -Make sure to edit both files to match your setup. +Make sure to edit both `gitlab.yml` and `unicorn.rb` to match your setup. ## Configure GitLab DB settings # Mysql sudo -u git cp config/database.yml.mysql config/database.yml + or + # PostgreSQL sudo -u git cp config/database.yml.postgresql config/database.yml -Make sure to update username/password in config/database.yml. + # Make sure to update username/password in config/database.yml. + # You only need to adapt the production settings (first part). + # If you followed the database guide then please do as follows: + # Change 'root' to 'gitlab' + # Change 'secure password' with the value you have given to $password + # You can keep the double quotes around the password + sudo -u git -H editor config/database.yml + + # Make config/database.yml readable to git only + sudo -u git -H chmod o-rwx config/database.yml ## Install Gems cd /home/git/gitlab - sudo gem install charlock_holmes --version '0.6.9' + sudo gem install charlock_holmes --version '0.6.9.4' - # For MySQL (note, the option says "without") - sudo -u git -H bundle install --deployment --without development test postgres + # For MySQL (note, the option says "without ... postgres") + sudo -u git -H bundle install --deployment --without development test postgres aws - # Or for PostgreSQL - sudo -u git -H bundle install --deployment --without development test mysql + # Or for PostgreSQL (note, the option says "without ... mysql") + sudo -u git -H bundle install --deployment --without development test mysql aws -## Initialise Database and Activate Advanced Features +## Initialize Database and Activate Advanced Features sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production + # Type 'yes' to create the database. + + # When done you see 'Administrator account created:' + ## Install Init Script Download the init script (will be /etc/init.d/gitlab): - sudo curl --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/master/init.d/gitlab + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab sudo chmod +x /etc/init.d/gitlab Make GitLab start on boot: @@ -205,10 +261,18 @@ Make GitLab start on boot: ## Check Application Status -Check if GitLab and its environment is configured correctly: +Check if GitLab and its environment are configured correctly: sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +## Start Your GitLab Instance + + sudo service gitlab start + # or + sudo /etc/init.d/gitlab restart + +## Double-check Application Status + 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 @@ -216,39 +280,32 @@ To make sure you didn't miss anything run a more thorough check with: If all items are green, then congratulations on successfully installing GitLab! However there are still a few steps left. -## Start Your GitLab Instance - - sudo service gitlab start - # or - sudo /etc/init.d/gitlab restart - # 7. Nginx **Note:** -If you can't or don't want to use Nginx as your web server, have a look at the -"Advanced Setup Tips" section. +Nginx is the officially supported web server for GitLab. If you cannot or do not want to use Nginx as your web server, have a look at the +[GitLab recipes](https://github.com/gitlabhq/gitlab-recipes). ## Installation - sudo apt-get install nginx + sudo apt-get install -y nginx ## Site Configuration Download an example site config: - sudo curl --output /etc/nginx/sites-available/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/master/nginx/gitlab + sudo cp lib/support/nginx/gitlab /etc/nginx/sites-available/gitlab sudo ln -s /etc/nginx/sites-available/gitlab /etc/nginx/sites-enabled/gitlab Make sure to edit the config file to match your setup: - # Change **YOUR_SERVER_IP** and **YOUR_SERVER_FQDN** - # to the IP address and fully-qualified domain name - # of your host serving GitLab - sudo vim /etc/nginx/sites-enabled/gitlab + # Change YOUR_SERVER_FQDN to the fully-qualified + # domain name of your host serving GitLab. + sudo editor /etc/nginx/sites-available/gitlab ## Restart - sudo /etc/init.d/nginx restart + sudo service nginx restart # Done! @@ -260,7 +317,7 @@ The setup has created an admin account for you. You can use it to log in: 5iveL!fe **Important Note:** -Please go over to your profile page and immediately chage the password, so +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. **Enjoy!** @@ -278,12 +335,12 @@ a different host, you can configure its connection string via the `config/resque.yml` file. # example - production: redis.example.tld:6379 + production: redis://redis.example.tld:6379 ## Custom SSH Connection -If you are running SSH on a non-standard port, you must change the gitlab user'S SSH config. - +If you are running SSH on a non-standard port, you must change the gitlab user's SSH config. + # Add to /home/git/.ssh/config host localhost # Give your setup a name (here: override localhost) user git # Your remote git user @@ -292,7 +349,39 @@ If you are running SSH on a non-standard port, you must change the gitlab user'S You also need to change the corresponding options (e.g. ssh_user, ssh_host, admin_uri) in the `config\gitlab.yml` file. -## User-contributed Configurations +## LDAP authentication + +You can configure LDAP authentication in config/gitlab.yml. Please restart GitLab after editing this file. + +## Using Custom Omniauth Providers + +GitLab uses [Omniauth](http://www.omniauth.org/) for authentication and already ships with a few providers preinstalled (e.g. LDAP, GitHub, Twitter). But sometimes that is not enough and you need to integrate with other authentication solutions. For these cases you can use the Omniauth provider. + +### Steps + +These steps are fairly general and you will need to figure out the exact details from the Omniauth provider's documentation. + +* Stop GitLab + `sudo service gitlab stop` + +* Add provider specific configuration options to your `config/gitlab.yml` (you can use the [auth providers section of the example config](https://github.com/gitlabhq/gitlabhq/blob/master/config/gitlab.yml.example) as a reference) + +* Add the gem to your [Gemfile](https://github.com/gitlabhq/gitlabhq/blob/master/Gemfile) + `gem "omniauth-your-auth-provider"` +* If you're using MySQL, install the new Omniauth provider gem by running the following command: + `sudo -u git -H bundle install --without development test postgres --path vendor/bundle --no-deployment` + +* If you're using PostgreSQL, install the new Omniauth provider gem by running the following command: + `sudo -u git -H bundle install --without development test mysql --path vendor/bundle --no-deployment` + +> These are the same commands you used in the [Install Gems section](#install-gems) with `--path vendor/bundle --no-deployment` instead of `--deployment`. + +* Start GitLab + `sudo service gitlab start` + + +### Examples -You can find things like AWS installation scripts, init scripts or config files -for alternative web server in our [recipes collection](https://github.com/gitlabhq/gitlab-recipes/). +If you have successfully set up a provider that is not shipped with GitLab itself, please let us know. +You can help others by reporting successful configurations and probably share a few insights or provide warnings for common errors or pitfalls by sharing your experience [in the public Wiki](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Working-Custom-Omniauth-Provider-Configurations). +While we can't officially support every possible auth mechanism out there, we'd like to at least help those with special needs. diff --git a/doc/install/requirements.md b/doc/install/requirements.md index ec5b013c5d8..1dba04f4237 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -1,13 +1,3 @@ -# Hardware - -We recommend you to run GitLab on a server with at least 1GB RAM. - -The necessary hard disk space largely depends on the size of the repos you want -to use GitLab with. But as a *rule of thumb* you should have at least as much -free space as your all repos combined take up. - - - # Operating Systems ## Linux @@ -36,8 +26,7 @@ systems. This means you may get it to work on systems running FreeBSD or OS X. ## Windows GitLab does **not** run on Windows and we have no plans of supporting it in the -near future. - +near future. Please consider using a virtual machine to run GitLab. # Rubies @@ -48,9 +37,30 @@ While it is generally possible to use other Rubies (like some work on your part. +# Hardware requirements + +## CPU + +We recommend a processor with **4 cores**. At a minimum you need a processor with 2 cores to responsively run an unmodified installation. + +## Memory + +- 512MB is too little memory, GitLab will be very slow and you will need 250MB of swap +- 768MB is the minimal memory size and supports up to 100 users +- **1GB** is the **recommended** memory size and supports up to 1,000 users +- 1.5GB supports up to 10,000 users + +## 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. 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. + # Installation troubles and reporting success or failure -If you have troubles installing GitLab following the official installation guide +If you have troubles installing GitLab following the [official installation guide](installation.md) or want to share your experience installing GitLab on a not officially supported -platform, please follow the the contribution guide (see CONTRIBUTING.md). +platform, please follow the the [contribution guide](/CONTRIBUTING.md). diff --git a/doc/make_release.md b/doc/make_release.md new file mode 100644 index 00000000000..5be0d5980a1 --- /dev/null +++ b/doc/make_release.md @@ -0,0 +1,56 @@ +# Things to do when creating new release +NOTE: This is a developer guide. If you are trying to install GitLab see the latest stable [installation guide](install/installation.md) and if you are trying to upgrade, see the [upgrade guides](update). +## Install guide up to date? + +* References correct GitLab branch `x-x-stable` and correct GitLab shell tag? + +## Make upgrade guide + +### From x.x to x.x + +#### 0. Any major changes? Database updates? Web server change? File structure changes? + +#### 1. Make backup + +#### 2. Stop server + +#### 3. Do users need to update dependencies like `git`? + +#### 4. Get latest code + +#### 5. Does GitLab shell need to be updated? + +#### 6. Install libs, migrations, etc. + +#### 7. Any config files updated since last release? + +Check if any of these changed since last release (~22nd of last month depending on when last release branch was created): + +* https://github.com/gitlabhq/gitlabhq/commits/master/lib/support/nginx/gitlab +* https://github.com/gitlabhq/gitlab-shell/commits/master/config.yml.example +* https://github.com/gitlabhq/gitlabhq/commits/master/config/gitlab.yml.example +* https://github.com/gitlabhq/gitlabhq/commits/master/config/unicorn.rb.example +* https://github.com/gitlabhq/gitlabhq/commits/master/config/database.yml.mysql +* https://github.com/gitlabhq/gitlabhq/commits/master/config/database.yml.postgresql + +#### 8. Need to update init script? + +Check if changed since last release (~22nd of last month depending on when last release branch was created): https://github.com/gitlabhq/gitlabhq/commits/master/lib/support/init.d/gitlab + +#### 9. Start application + +#### 10. Check application status + +## Make sure code status is good + +* [](http://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch) + +* [](https://travis-ci.org/gitlabhq/gitlabhq) on travis-ci.org (master branch) + +* [](https://codeclimate.com/github/gitlabhq/gitlabhq) + +* [](https://gemnasium.com/gitlabhq/gitlabhq) this button can be yellow (small updates are available) but must not be red (a security fix or an important update is available) + +* [](https://coveralls.io/r/gitlabhq/gitlabhq) + +## Make release branch diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md new file mode 100644 index 00000000000..a84222f9fc2 --- /dev/null +++ b/doc/markdown/markdown.md @@ -0,0 +1,472 @@ +---------------------------------------------- + +Table of Contents +================= + +---------------------------------------------- + +[GitLab Flavored Markdown](#toc_3) +------------------------------- +[Newlines](#toc_4) +[Multiple underscores in words](#toc_5) +[URL autolinking](#toc_6) +[Code and Syntax Highlighting](#toc_7) +[Emoji](#toc_8) +[Special GitLab references](#toc_9) + + + +[Standard Markdown](#toc_10) +------------------------------ +[Headers](#toc_11) +[Emphasis](#toc_20) +[Lists](#toc_21) +[Links](#toc_22) +[Images](#toc_23) +[Blockquotes](#toc_24) +[Inline HTML](#toc_25) +[Horizontal Rule](#toc_26) +[Line Breaks](#toc_27) +[Tables](#toc_28) + +[References](#toc_29) +--------------------- + +---------------------------------------------- + +<a name="gfm" /> +GitLab Flavored Markdown (GFM) +============================== +For GitLab we developed something we call "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. + +You can use GFM in + +* commit messages +* comments +* wall posts +* issues +* merge requests +* milestones +* wiki pages + +<a name="newlines" /> +Newlines +-------- +The biggest difference that GFM introduces is in the handling of linebreaks. With traditional Markdown you can hard wrap paragraphs of text and they will be combined into a single paragraph. We find this to be the cause of a huge number of unintentional formatting errors. GFM treats newlines in paragraph-like content as real line breaks, which is probably what you intended. + +The next paragraph contains two phrases separated by a single newline character: + + Roses are red + Violets are blue + +Roses are red +Violets are blue + +<a name="underscores" /> +Multiple underscores in words +----------------------------- +It is not reasonable to italicize just _part_ of a word, especially when you're dealing with code and names that often appear with multiple underscores. Therefore, GFM ignores multiple underscores in words. + + perform_complicated_task + do_this_and_do_that_and_another_thing + +perform_complicated_task +do_this_and_do_that_and_another_thing + +<a name="autolink" /> +URL autolinking +--------------- +GFM will autolink standard URLs you copy and paste into your text. +So if you want to link to a URL (instead of a textural link), you can simply put the URL in verbatim and it will be turned into a link to that URL. + + http://www.google.com + +http://www.google.com + +<a name="code"/> +## Code and Syntax Highlighting + +Blocks of code are either fenced by lines with three back-ticks <code>```</code>, or are indented with four spaces. Only the fenced code blocks support syntax highlighting. + + +```no-highlight +Inline `code` has `back-ticks around` it. +``` + +Inline `code` has `back-ticks around` it. + +Example: + + ```javascript + var s = "JavaScript syntax highlighting"; + alert(s); + ``` + + ```python + def function(): + #indenting works just fine in the fenced code block + s = "Python syntax highlighting" + print s + ``` + + ```ruby + require 'redcarpet' + markdown = Redcarpet.new("Hello World!") + puts markdown.to_html + ``` + + ``` + No language indicated, so no syntax highlighting. + s = "There is no highlighting for this." + But let's throw in a <b>tag</b>. + ``` + +becomes: + +```javascript +var s = "JavaScript syntax highlighting"; +alert(s); +``` + +```python +def function(): + #indenting works just fine in the fenced code block + s = "Python syntax highlighting" + print s +``` + +```ruby +require 'redcarpet' +markdown = Redcarpet.new("Hello World!") +puts markdown.to_html +``` + +``` +No language indicated, so no syntax highlighting. +s = "There is no highlighting for this." +But let's throw in a <b>tag</b>. +``` + +<a name="emoji"/> +Emoji +----- + + Sometimes you want to be :cool: and add some :sparkles: to your :speech_balloon:. Well we have a :gift: for you: + + :exclamation: You can use emoji anywhere GFM is supported. :sunglasses: + + You can use it to point out a :bug: or warn about :monkey:patches. And if someone improves your really :snail: code, send them a :bouquet: or some :candy:. People will :heart: you for that. + + If you are :new: to this, don't be :fearful:. You can easily join the emoji :circus_tent:. All you need to do is to :book: up on the supported codes. + + Consult the [Emoji Cheat Sheet](http://www.emoji-cheat-sheet.com/) for a list of all supported emoji codes. :thumbsup: + +Sometimes you want to be :cool: and add some :sparkles: to your :speech_balloon:. Well we have a :gift: for you: + +:exclamation: You can use emoji anywhere GFM is supported. :sunglasses: + +You can use it to point out a :bug: or warn about :monkey:patches. And if someone improves your really :snail: code, send them a :bouquet: or some :candy:. People will :heart: you for that. + +If you are :new: to this, don't be :fearful:. You can easily join the emoji :circus_tent:. All you need to do is to :book: up on the supported codes. + +Consult the [Emoji Cheat Sheet](http://www.emoji-cheat-sheet.com/) for a list of all supported emoji codes. :thumbsup: + +<a name="special"/> +Special GitLab References +----- + +GFM recognized special references. +You can easily reference e.g. a team member, an issue, or a commit within a project. +GFM will turn that reference into a link so you can navigate between them easily. + + +GFM will recognize the following: + +* @foo : for team members +* #123 : for issues +* !123 : for merge requests +* $123 : for snippets +* 1234567 : for commits +* [file](path/to/file) : for file references + +<a name="standard"/> + +---------------------------------- +# Standard Markdown + +---------------------------------- +<a name="headers"/> +## Headers + +```no-highlight +# H1 +## H2 +### H3 +#### H4 +##### H5 +###### H6 + +Alternatively, for H1 and H2, an underline-ish style: + +Alt-H1 +====== + +Alt-H2 +------ +``` + +# H1 +## H2 +### H3 +#### H4 +##### H5 +###### H6 + +Alternatively, for H1 and H2, an underline-ish style: + +Alt-H1 +====== + +Alt-H2 +------ + +<a name="emphasis"/> +## Emphasis + +```no-highlight +Emphasis, aka italics, with *asterisks* or _underscores_. + +Strong emphasis, aka bold, with **asterisks** or __underscores__. + +Combined emphasis with **asterisks and _underscores_**. + +Strikethrough uses two tildes. ~~Scratch this.~~ +``` + +Emphasis, aka italics, with *asterisks* or _underscores_. + +Strong emphasis, aka bold, with **asterisks** or __underscores__. + +Combined emphasis with **asterisks and _underscores_**. + +Strikethrough uses two tildes. ~~Scratch this.~~ + + +<a name="lists"/> +## Lists + +```no-highlight +1. First ordered list item +2. Another item + * Unordered sub-list. +1. Actual numbers don't matter, just that it's a number + 1. Ordered sub-list +4. And another item. + + Some text that should be aligned with the above item. + +* Unordered list can use asterisks +- Or minuses ++ Or pluses +``` + +1. First ordered list item +2. Another item + * Unordered sub-list. +1. Actual numbers don't matter, just that it's a number + 1. Ordered sub-list +4. And another item. + + Some text that should be aligned with the above item. + +* Unordered list can use asterisks +- Or minuses ++ Or pluses + +<a name="links"/> +## Links + +There are two ways to create links. + + [I'm an inline-style link](https://www.google.com) + + [I'm a reference-style link][Arbitrary case-insensitive reference text] + + [I'm a relative reference to a repository file](../blob/master/LICENSE) + + [You can use numbers for reference-style link definitions][1] + + Or leave it empty and use the [link text itself][] + + Some text to show that the reference links can follow later. + + [arbitrary case-insensitive reference text]: https://www.mozilla.org + [1]: http://slashdot.org + [link text itself]: http://www.reddit.com + +[I'm an inline-style link](https://www.google.com) + +[I'm a reference-style link][Arbitrary case-insensitive reference text] + +[I'm a relative reference to a repository file](../blob/master/LICENSE) + +[You can use numbers for reference-style link definitions][1] + +Or leave it empty and use the [link text itself][] + +Some text to show that the reference links can follow later. + +[arbitrary case-insensitive reference text]: https://www.mozilla.org +[1]: http://slashdot.org +[link text itself]: http://www.reddit.com + +<a name="images"/> +## Images + + Here's our logo (hover to see the title text): + + Inline-style: +  + + Reference-style: + ![alt text][logo] + + [logo]: /assets/logo-white.png "Logo Title Text 2" + +Here's our logo (hover to see the title text): + +Inline-style: + + +Reference-style: +![alt text][logo] + +[logo]: /assets/logo-white.png "Logo Title Text 2" + +<a name="blockquotes"/> +## Blockquotes + +```no-highlight +> Blockquotes are very handy in email to emulate reply text. +> This line is part of the same quote. + +Quote break. + +> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote. +``` + +> Blockquotes are very handy in email to emulate reply text. +> This line is part of the same quote. + +Quote break. + +> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote. + +<a name="html"/> +## Inline HTML + +You can also use raw HTML in your Markdown, and it'll mostly work pretty well. + +```no-highlight +<dl> + <dt>Definition list</dt> + <dd>Is something people use sometimes.</dd> + + <dt>Markdown in HTML</dt> + <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd> +</dl> +``` + +<dl> + <dt>Definition list</dt> + <dd>Is something people use sometimes.</dd> + + <dt>Markdown in HTML</dt> + <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd> +</dl> + +<a name="hr"/> +## Horizontal Rule + +``` +Three or more... + +--- + +Hyphens + +*** + +Asterisks + +___ + +Underscores +``` + +Three or more... + +--- + +Hyphens + +*** + +Asterisks + +___ + +Underscores + +<a name="lines"/> +## Line Breaks + +My basic recommendation for learning how line breaks work is to experiment and discover -- hit <Enter> once (i.e., insert one newline), then hit it twice (i.e., insert two newlines), see what happens. You'll soon learn to get what you want. "Markdown Toggle" is your friend. + +Here are some things to try out: + +``` +Here's a line for us to start with. + +This line is separated from the one above by two newlines, so it will be a *separate paragraph*. + +This line is also a separate paragraph, but... +This line is only separated by a single newline, so it's a separate line in the *same paragraph*. +``` + +Here's a line for us to start with. + +This line is separated from the one above by two newlines, so it will be a *separate paragraph*. + +This line is also begins a separate paragraph, but... +This line is only separated by a single newline, so it's a separate line in the *same paragraph*. + + +<a name="tables"/> +## Tables + +Tables aren't part of the core Markdown spec, but they are part of GFM and Markdown Here supports them. + +``` +| header 1 | header 2 | +| -------- | -------- | +| cell 1 | cell 2 | +| cell 3 | cell 4 | +``` + +Code above produces next output: + +| header 1 | header 2 | +| -------- | -------- | +| cell 1 | cell 2 | +| cell 3 | cell 4 | + + +------------ + +<a name="references"/> +## References + +* This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). +* The [Markdown Syntax Guide](http://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown. +* [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown. diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 9b42afa7ca0..d2da64f3d3c 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -31,7 +31,6 @@ Dumping database tables: - Dumping table wikis... [DONE] Dumping repositories: - Dumping repository abcd... [DONE] -- Dumping repository gitolite-admin.git... [DONE] Creating backup archive: $TIMESTAMP_gitlab_backup.tar [DONE] Deleting tmp directories...[DONE] Deleting old backups... [SKIPPING] @@ -77,6 +76,5 @@ Restoring database tables: - Loading fixture wikis...[SKIPPING] Restoring repositories: - Restoring repository abcd... [DONE] -- Restoring repository gitolite-admin.git... [DONE] Deleting tmp directories...[DONE] ``` diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md index ad9e5a613b0..99809ef434d 100644 --- a/doc/raketasks/cleanup.md +++ b/doc/raketasks/cleanup.md @@ -1,18 +1,12 @@ -### Remove grabage from gitolite config and filesystem. Important! Data loss! +### Remove garbage from filesystem. Important! Data loss! -Remove projects from gitolite config if they dont exist in GitLab database - -``` -bundle exec rake gitlab:cleanup:config RAILS_ENV=production -``` - -Remove namespaces(dirs) from /home/git/repositories if they dont exist in GitLab database +Remove namespaces(dirs) from `/home/git/repositories` if they don't exist in GitLab database. ``` bundle exec rake gitlab:cleanup:dirs RAILS_ENV=production ``` -Remove repositories (global only for now) from /home/git/repositories if they dont exist in GitLab database +Remove repositories (global only for now) from `/home/git/repositories` if they don't exist in GitLab database. ``` bundle exec rake gitlab:cleanup:repos RAILS_ENV=production diff --git a/doc/raketasks/features.md b/doc/raketasks/features.md index 7f7daaf046c..018817d219b 100644 --- a/doc/raketasks/features.md +++ b/doc/raketasks/features.md @@ -17,12 +17,12 @@ bundle exec rake gitlab:enable_namespaces RAILS_ENV=production ``` -### Enable auto merge +### Rebuild project satellites -This command will enable the auto merge feature. After this you will be able to **merge a merge request** via GitLab and use the **online editor**. +This command will build missing satellites for projects. After this you will be able to **merge a merge request** via GitLab and use the **online editor**. ``` -bundle exec rake gitlab:enable_automerge RAILS_ENV=production +bundle exec rake gitlab:satellites:create RAILS_ENV=production ``` Example output: diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md index 110dbd161f7..3033d8c46b4 100644 --- a/doc/raketasks/maintenance.md +++ b/doc/raketasks/maintenance.md @@ -11,33 +11,30 @@ Example output: ``` System information -System: Debian 6.0.6 -Current User: gitlab -Using RVM: yes -RVM Version: 1.17.2 -Ruby Version: ruby-1.9.3-p327 -Gem Version: 1.8.24 -Bundler Version:1.2.3 -Rake Version: 10.0.1 +System: Debian 6.0.7 +Current User: git +Using RVM: no +Ruby Version: 1.9.3p392 +Gem Version: 1.8.23 +Bundler Version:1.3.5 +Rake Version: 10.0.4 GitLab information -Version: 3.1.0 -Resivion: fd5141d -Directory: /home/gitlab/gitlab -DB Adapter: mysql2 -URL: http://localhost:3000 -HTTP Clone URL: http://localhost:3000/some-project.git -SSH Clone URL: git@localhost:some-project.git -Using LDAP: no -Using Omniauth: no - -Gitolite information -Version: v3.04-4-g4524f01 -Admin URI: git@localhost:gitolite-admin -Admin Key: gitlab -Repositories: /home/git/repositories/ -Hooks: /home/git/.gitolite/hooks/ -Git: /usr/bin/git +Version: 5.1.0.beta2 +Revision: 4da8b37 +Directory: /home/git/gitlab +DB Adapter: mysql2 +URL: http://localhost +HTTP Clone URL: http://localhost/some-project.git +SSH Clone URL: git@localhost:some-project.git +Using LDAP: no +Using Omniauth: no + +GitLab Shell +Version: 1.2.0 +Repositories: /home/git/repositories/ +Hooks: /home/git/gitlab-shell/hooks/ +Git: /usr/bin/git ``` @@ -46,8 +43,8 @@ Git: /usr/bin/git Runs the following rake tasks: * gitlab:env:check -* gitlab:gitolite:check -* gitlab:resque:check +* gitlab:gitlab_shell:check +* gitlab:sidekiq:check * gitlab:app:check It will check that each component was setup according to the installation guide and suggest fixes for issues found. @@ -63,64 +60,43 @@ Example output: ``` Checking Environment ... -gitlab user is in git group? ... yes -Has no "-e" in ~git/.profile ... yes -Git configured for gitlab user? ... yes +Git configured for git user? ... yes Has python2? ... yes python2 is supported version? ... yes Checking Environment ... Finished -Checking Gitolite ... +Checking GitLab Shell ... -Using recommended version ... yes -Repo umask is 0007 in .gitolite.rc? ... yes -Allow all Git config keys in .gitolite.rc ... yes -Config directory exists? ... yes -Config directory owned by git:git? ... yes -Config directory access is drwxr-x---? ... yes +GitLab Shell version? ... OK (1.2.0) Repo base directory exists? ... yes +Repo base directory is a symlink? ... no Repo base owned by git:git? ... yes Repo base access is drwxrws---? ... yes -Can clone gitolite-admin? ... yes -Can commit to gitolite-admin? ... yes -post-receive hook exists? ... yes post-receive hook up-to-date? ... yes -post-receive hooks in repos are links: ... -GitLab ... ok -Non-Ascii Files Test ... ok -Touch Commit Test ... ok -Without Master Test ... ok -Git config in repos: ... -GitLab ... ok -Non-Ascii Files Test ... ok -Touch Commit Test ... ok -Without Master Test ... ok +post-receive hooks in repos are links: ... yes -Checking Gitolite ... Finished +Checking GitLab Shell ... Finished -Checking Resque ... +Checking Sidekiq ... Running? ... yes -Checking Resque ... Finished +Checking Sidekiq ... Finished Checking GitLab ... Database config exists? ... yes -Database is not SQLite ... yes +Database is SQLite ... no All migrations up? ... yes GitLab config exists? ... yes -GitLab config not outdated? ... yes +GitLab config outdated? ... no Log directory writable? ... yes Tmp directory writable? ... yes Init script exists? ... yes Init script up-to-date? ... yes -Projects have satellites? ... -GitLab ... yes -Non-Ascii Files Test ... yes -Touch Commit Test ... yes -Without Master Test ... yes +Projects have satellites? ... yes +Redis version >= 2.0.0? ... yes Checking GitLab ... Finished ``` @@ -135,34 +111,18 @@ If necessary, remove the `tmp/repo_satellites` directory and rerun the command b bundle exec rake gitlab:satellites:create RAILS_ENV=production ``` - -### Rebuild each key at gitolite config - -This will send all users ssh public keys to gitolite and grant them access (based on their permission) to their projects. - -``` -bundle exec rake gitlab:gitolite:update_keys RAILS_ENV=production -``` - - -### Rebuild each project at gitolite config - -This makes sure that all projects are present in gitolite and can be accessed. - -``` -bundle exec rake gitlab:gitolite:update_repos RAILS_ENV=production -``` - ### Import bare repositories into GitLab project instance Notes: * 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: -1. copy your bare repos under git base_path (see `config/gitlab.yml` git_host -> base_path) +1. copy your bare repos under git repos_path (see `config/gitlab.yml` gitlab_shell -> repos_path) 2. run the command below ``` @@ -174,5 +134,8 @@ Example output: ``` Processing abcd.git * Created abcd (abcd.git) +Processing group/xyz.git + * Created Group group (2) + * Created xyz (group/xyz.git) [...] ``` diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md index 021ce35931f..8fa2ed1311c 100644 --- a/doc/raketasks/user_management.md +++ b/doc/raketasks/user_management.md @@ -1,4 +1,4 @@ -### Add user to as a developer to all projects +### Add user as a developer to all projects ``` bundle exec rake gitlab:import:user_to_projects[username@domain.tld] diff --git a/doc/update/2.6-to-3.0.md b/doc/update/2.6-to-3.0.md new file mode 100644 index 00000000000..d7047d8eb19 --- /dev/null +++ b/doc/update/2.6-to-3.0.md @@ -0,0 +1,63 @@ +# From 2.6 to 3.0 + +### 1. Stop server & resque + + sudo service gitlab stop + +### 2. Update code & db + + +```bash +# Get latest code +git fetch origin +git checkout v3.0.3 + + +# Install libs +sudo -u gitlab bundle install --without development test postgres + +# update db +sudo -u gitlab bundle exec rake db:migrate RAILS_ENV=production + +# !!! Config should be replaced with a new one. Check it after replace +cp config/gitlab.yml.example config/gitlab.yml + +# update gitolite hooks + +# GITOLITE v2: +sudo cp ./lib/hooks/post-receive /home/git/share/gitolite/hooks/common/post-receive +sudo chown git:git /home/git/share/gitolite/hooks/common/post-receive + +# GITOLITE v3: +sudo cp ./lib/hooks/post-receive /home/git/.gitolite/hooks/common/post-receive +sudo chown git:git /home/git/.gitolite/hooks/common/post-receive + +# set valid path to hooks in gitlab.yml in git_host section +# like this +git_host: + # gitolite 2 + hooks_path: /home/git/share/gitolite/hooks + # gitolite 3 + hooks_path: /home/git/.gitolite/hooks/ + + +# Make some changes to gitolite config +# For more information visit https://github.com/gitlabhq/gitlabhq/pull/1719 + +# gitolite v2 +sudo -u git -H sed -i 's/\(GL_GITCONFIG_KEYS\s*=>*\s*\).\{2\}/\\1"\.\*"/g' /home/git/.gitolite.rc + +# gitlite v3 +sudo -u git -H sed -i "s/\(GIT_CONFIG_KEYS\s*=>*\s*\).\{2\}/\\1'\.\*'/g" /home/git/.gitolite.rc + + +# Check app status +sudo -u gitlab bundle exec rake gitlab:app:status RAILS_ENV=production + + +``` + + +### 3. Start all + + sudo service gitlab start diff --git a/doc/update/2.9-to-3.0.md b/doc/update/2.9-to-3.0.md new file mode 100644 index 00000000000..af929e027a4 --- /dev/null +++ b/doc/update/2.9-to-3.0.md @@ -0,0 +1,37 @@ +# From 2.9 to 3.0 + +### 1. Stop server & resque + + sudo service gitlab stop + +### 2. Follow instructions + +```bash + +# Get latest code +sudo -u gitlab -H git fetch origin +sudo -u gitlab -H git checkout v3.0.3 + +# Install gems +sudo -u gitlab -H bundle install --without development test postgres + +# Migrate db +sudo -u gitlab -H bundle exec rake db:migrate RAILS_ENV=production + +# Make some changes to gitolite v3 config +# For more information visit https://github.com/gitlabhq/gitlabhq/pull/1719 + +# Gitolite version 3 +sudo -u git -H sed -i "s/\(GIT_CONFIG_KEYS\s*=>*\s*\).\{2\}/\\1'\.\*'/g" /home/git/.gitolite.rc + +# If you still use gitolite v2 +sudo -u git -H sed -i 's/\(GL_GITCONFIG_KEYS\s*=>*\s*\).\{2\}/\\1"\.\*"/g' /home/git/.gitolite.rc + +# Check APP Status +sudo -u gitlab -H bundle exec rake gitlab:app:status RAILS_ENV=production +``` + + +### 3. Start all + + sudo service gitlab start diff --git a/doc/update/3.0-to-3.1.md b/doc/update/3.0-to-3.1.md new file mode 100644 index 00000000000..5f06f818d10 --- /dev/null +++ b/doc/update/3.0-to-3.1.md @@ -0,0 +1,108 @@ +# From 3.0 to 3.1 + +__IMPORTANT!__ + +In this release __we moved Resque jobs under own gitlab namespace__. + +Despite a lot of advantages it requires from our users to __replace gitolite post-receive hook with new one__. + +Most of projects has post-receive file as symlink to gitolite `/home/git/.gitolite/hooks/post-receive`. +But some of them may have a real file. In this case you should rewrite it with symlink to gitolite hook. + +I wrote a bash script which will do it automatically for you. Just make sure all path inside is valid for you + +- - - + +### 1. Stop server & resque + + sudo service gitlab stop + +### 2. Update GitLab + +```bash + +# Get latest code +sudo -u gitlab -H git fetch +sudo -u gitlab -H git checkout v3.1.0 + +# Install new charlock_holmes +sudo gem install charlock_holmes --version '0.6.9' + +# Install gems for MySQL +sudo -u gitlab -H bundle install --without development test postgres sqlite + + +# Migrate db +sudo -u gitlab -H bundle exec rake db:migrate RAILS_ENV=production + + +``` + +### 3. Update post-receive hooks + +#### Gitolite 3 + +Step 1: Rewrite post-receive hook + +```bash +# Rewrite hook for gitolite 3 +sudo cp ./lib/hooks/post-receive /home/git/.gitolite/hooks/common/post-receive +sudo chown git:git /home/git/.gitolite/hooks/common/post-receive +``` + +Step 2: Rewrite hooks in all projects to symlink gitolite hook + +```bash +# 1. Check for valid path +sudo -u gitlab -H vim lib/support/rewrite-hooks.sh + +# 2. Run script +sudo -u git -H lib/support/rewrite-hooks.sh +``` + +#### Gitolite v2 + +Step 1: rewrite post-receive hook for gitolite 2 + +``` +sudo cp ./lib/hooks/post-receive /home/git/share/gitolite/hooks/common/post-receive +sudo chown git:git /home/git/share/gitolite/hooks/common/post-receive +``` + +Step 2: Replace symlinks in project to valid place + + + #!/bin/bash + src="/home/git/repositories" + for dir in `ls "$src/"` + do + if [ -d "$src/$dir" ]; then + + if [ "$dir" = "gitolite-admin.git" ] + then + continue + fi + + project_hook="$src/$dir/hooks/post-receive" + gitolite_hook="/home/git/share/gitolite/hooks/common/post-receive" + + ln -s -f $gitolite_hook $project_hook + fi + done + + +### 4. Check app status + +```bash + +# Check APP Status +sudo -u gitlab -H bundle exec rake gitlab:app:status RAILS_ENV=production + + + +``` + + +### 5. Start all + + sudo service gitlab start diff --git a/doc/update/3.1-to-4.0.md b/doc/update/3.1-to-4.0.md new file mode 100644 index 00000000000..c5ae3a40a76 --- /dev/null +++ b/doc/update/3.1-to-4.0.md @@ -0,0 +1,99 @@ +# From 3.1 to 4.0 + +## Important changes + +* Support for SQLite was dropped +* Support for gitolite 2 was dropped +* Projects are organized in namespaces +* The GitLab post-receive hook needs to be updated +* The configuration file needs to be updated +* Availability of `python2` executable + +Most of projects has post-receive file as symlink to gitolite `/home/git/.gitolite/hooks/post-receive`. +But some of them may have a real file. In this case you should rewrite it with symlink to gitolite hook. + +I wrote a bash script which will do it automatically for you. Just make sure all path inside is valid for you + +- - - + +### 1. Stop GitLab & Resque + + sudo service gitlab stop + +### 2. Update GitLab + +```bash + +# Get latest code +sudo -u gitlab -H git fetch +sudo -u gitlab -H git checkout 4-0-stable + +# Install gems for MySQL +sudo -u gitlab -H bundle install --without development test postgres + +# Update repos permissions +sudo chmod -R ug+rwXs /home/git/repositories/ +sudo chown -R git:git /home/git/repositories/ + +# Migrate db +sudo -u gitlab -H bundle exec rake db:migrate RAILS_ENV=production + +# Enable namespaces (**Warning!** All projects in groups will be moved to subdirectories) +sudo -u gitlab -H bundle exec rake gitlab:enable_namespaces RAILS_ENV=production + +``` + +### 3. Update post-receive hooks (Requires gitolite v3 ) + + +Step 1: Rewrite post-receive hook + +```bash +sudo cp ./lib/hooks/post-receive /home/git/.gitolite/hooks/common/post-receive +sudo chown git:git /home/git/.gitolite/hooks/common/post-receive +``` + +Step 2: Update project hooks to be symlinks to the Gitolite hook + +```bash +# 1. Check paths in script +sudo -u gitlab -H vim lib/support/rewrite-hooks.sh + +# 2. Run script +sudo -u git -H lib/support/rewrite-hooks.sh +``` + + +### 4. Replace config with new one + + + # backup old one + sudo -u gitlab -H cp config/gitlab.yml config/gitlab.yml.old + + # copy new one + sudo -u gitlab -H cp config/gitlab.yml.example config/gitlab.yml + + # edit it + sudo -u gitlab -H vim config/gitlab.yml + + +### 5. Disable ssh known_host check for own domain + + + echo "Host localhost + StrictHostKeyChecking no + UserKnownHostsFile=/dev/null" | sudo tee -a /etc/ssh/ssh_config + + echo "Host YOUR_DOMAIN_NAME + StrictHostKeyChecking no + UserKnownHostsFile=/dev/null" | sudo tee -a /etc/ssh/ssh_config + + +### 6. Check GitLab's status + + sudo -u gitlab -H bundle exec rake gitlab:check RAILS_ENV=production + + +### 7. Start GitLab & Resque + + sudo service gitlab start diff --git a/doc/update/4.0-to-4.1.md b/doc/update/4.0-to-4.1.md new file mode 100644 index 00000000000..324af7597b2 --- /dev/null +++ b/doc/update/4.0-to-4.1.md @@ -0,0 +1,57 @@ +# From 4.0 to 4.1 + +## Important changes + +* Resque replaced with Sidekiq +* New options for configuration file added +* Init.d script should be updated +* __requires ruby1.9.3-p327__ + +- - - + +### 1. Stop GitLab & Resque + + sudo service gitlab stop + +### 2. Update GitLab + +```bash +# Set the working directory +cd /home/gitlab/gitlab/ + +# Get latest code +sudo -u gitlab -H git fetch +sudo -u gitlab -H git checkout 4-1-stable + +# Install gems for MySQL +sudo -u gitlab -H bundle install --without development test postgres + +# Migrate db +sudo -u gitlab -H bundle exec rake db:migrate RAILS_ENV=production + +``` + +### 3. Replace init.d script with a new one + +``` +# backup old one +sudo mv /etc/init.d/gitlab /etc/init.d/gitlab.old + +# get new one using sidekiq +sudo curl --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/4-1-stable/init.d/gitlab +sudo chmod +x /etc/init.d/gitlab + +``` + +### 4. Check GitLab's status + + sudo -u gitlab -H bundle exec rake gitlab:check RAILS_ENV=production + + +### 5. Start GitLab & Sidekiq + + sudo service gitlab start + +### 6. Remove old init.d script + + sudo rm /etc/init.d/gitlab.old diff --git a/doc/update/4.1-to-4.2.md b/doc/update/4.1-to-4.2.md new file mode 100644 index 00000000000..536f22415e2 --- /dev/null +++ b/doc/update/4.1-to-4.2.md @@ -0,0 +1,38 @@ +# From 4.1 to 4.2 + +### 1. Stop server & resque + + sudo service gitlab stop + +### 2. Update code & db + +```bash + +#Set the working directory +cd /home/gitlab/gitlab/ + +# Get latest code +sudo -u gitlab git fetch + +sudo -u gitlab git checkout 4-2-stable + +# Install libs +sudo -u gitlab bundle install --without development test postgres --deployment + +# update db +sudo -u gitlab bundle exec rake db:migrate RAILS_ENV=production + +``` + + +### 3. Check GitLab's status + +```bash +sudo -u gitlab -H bundle exec rake gitlab:check RAILS_ENV=production +``` + + + +### 4. Start all + + sudo service gitlab start diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md new file mode 100644 index 00000000000..053a50ebc88 --- /dev/null +++ b/doc/update/4.2-to-5.0.md @@ -0,0 +1,164 @@ +# From 4.2 to 5.0 + +## Important changes + +* We don't use `gitlab` user any more. Everything will be moved to `git` user +* __requires ruby1.9.3__ + + +__0. Stop gitlab__ + + sudo service gitlab stop + +__1. add bash to git user__ + +``` +sudo chsh -s /bin/bash git +``` + +__2. git clone gitlab-shell__ + +``` +cd /home/git/ +sudo -u git -H git clone https://github.com/gitlabhq/gitlab-shell.git /home/git/gitlab-shell +``` + +__3. setup gitlab-shell__ + +```bash +# chmod all repos and files under git +sudo chown git:git -R /home/git/repositories/ + +# login as git +sudo su git +cd /home/git/gitlab-shell + +# copy config +cp config.yml.example config.yml + +# change url to gitlab instance +# ! make sure url end with '/' like 'https://gitlab.example/' +vim config.yml + +# rewrite hooks +./support/rewrite-hooks.sh + +# check ruby version for git user ( 1.9 required!! ) +# gitlab shell requires system ruby 1.9 +ruby -v + +# exit from git user +exit +``` + +__4. Copy gitlab instance to git user__ + +```bash +sudo cp -R /home/gitlab/gitlab /home/git/gitlab +sudo chown git:git -R /home/git/gitlab +sudo rm -rf /home/gitlab/gitlab-satellites + +# if exists +sudo rm /tmp/gitlab.socket +``` + +__5. Update gitlab to recent version__ + +```bash +cd /home/git/gitlab + +# backup current config +sudo -u git -H cp config/gitlab.yml config/gitlab.yml.old + +sudo -u git -H git fetch +sudo -u git -H git checkout 5-0-stable + +# replace config with recent one +sudo -u git -H cp config/gitlab.yml.example config/gitlab.yml + +# edit it +sudo -u git -H vim config/gitlab.yml + + +sudo -u git -H bundle install --without development test postgres --deployment +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production +sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production +sudo -u git -H bundle exec rake gitlab:shell:build_missing_projects RAILS_ENV=production + +sudo -u git -H mkdir /home/git/gitlab-satellites +sudo -u git -H bundle exec rake gitlab:satellites:create RAILS_ENV=production + +# migrate wiki to git +sudo -u git -H bundle exec rake gitlab:wiki:migrate RAILS_ENV=production + + +# check permissions for /home/git/.ssh/ +sudo -u git -H chmod 700 /home/git/.ssh +sudo -u git -H chmod 600 /home/git/.ssh/authorized_keys + +# check permissions for /home/git/gitlab/ +sudo chown -R git /home/git/gitlab/log/ +sudo chown -R git /home/git/gitlab/tmp/ +sudo chmod -R u+rwX /home/git/gitlab/log/ +sudo chmod -R u+rwX /home/git/gitlab/tmp/ +sudo -u git -H mkdir /home/git/gitlab/tmp/pids/ +sudo chmod -R u+rwX /home/git/gitlab/tmp/pids + +``` + +__6. Update init.d script and nginx config__ + +```bash +# init.d +sudo rm /etc/init.d/gitlab +sudo curl --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-0-stable/init.d/gitlab +sudo chmod +x /etc/init.d/gitlab + +# unicorn +sudo -u git -H cp /home/git/gitlab/config/unicorn.rb /home/git/gitlab/config/unicorn.rb.old +sudo -u git -H cp /home/git/gitlab/config/unicorn.rb.example /home/git/gitlab/config/unicorn.rb + +#nginx +# Replace path from '/home/gitlab/' to '/home/git/' +sudo vim /etc/nginx/sites-enabled/gitlab +sudo service nginx restart + + +``` + +__7. Start gitlab instance__ + +``` + + +sudo service gitlab start + +# check if unicorn and sidekiq started +# If not try to logout, also check replaced path from '/home/gitlab/' to '/home/git/' +# in nginx, unicorn, init.d etc +ps aux | grep unicorn +ps aux | grep sidekiq + +``` + +__8. Check installation__ + + +```bash +# In 5-10 seconds lets check gitlab-shell +sudo -u git -H /home/git/gitlab-shell/bin/check + +# Example of success output +# Check GitLab API access: OK +# Check directories and files: +# /home/git/repositories: OK +# /home/git/.ssh/authorized_keys: OK + + +# Now check gitlab instance +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +``` + + +__P.S. If everything works as expected you can remove gitlab user from system__ diff --git a/doc/update/5.0-to-5.1.md b/doc/update/5.0-to-5.1.md new file mode 100644 index 00000000000..45fc3436ebe --- /dev/null +++ b/doc/update/5.0-to-5.1.md @@ -0,0 +1,68 @@ +# From 5.0 to 5.1 + +## Release notes: + +* `unicorn` replaced with `puma` +* merge request cached diff will be truncated + +### 1. Stop server + + sudo service gitlab stop + +### 2. Get latest code + +```bash +cd /home/git/gitlab +sudo -u git -H git fetch +sudo -u git -H git checkout 5-1-stable +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v1.3.0 +# replace your old config with the new one +sudo -u git -H mv config.yml config.yml.old +sudo -u git -H cp config.yml.example config.yml +# edit options to match old config +sudo -u git -H vi config.yml +``` + + +### 4. Install libs, migrations etc + +```bash +cd /home/git/gitlab +sudo rm tmp/sockets/gitlab.socket +sudo -u git -H cp config/puma.rb.example config/puma.rb + +sudo -u git -H bundle install --without development test postgres --deployment +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production +sudo -u git -H bundle exec rake migrate_merge_requests RAILS_ENV=production +sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production +``` + +### 5. Update init.d script with a new one + +```bash +# init.d +sudo rm /etc/init.d/gitlab +sudo curl --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-1-stable/init.d/gitlab +sudo chmod +x /etc/init.d/gitlab +``` + +### 6. Mysql grant privileges + +Only if you are using mysql: + +```bash +mysql -u root -p +mysql> GRANT LOCK TABLES ON `gitlabhq_production`.* TO 'gitlab'@'localhost'; +mysql> \q +``` + +### 7. Start application + + sudo service gitlab start diff --git a/doc/update/5.1-to-5.2.md b/doc/update/5.1-to-5.2.md new file mode 100644 index 00000000000..8599c4323ea --- /dev/null +++ b/doc/update/5.1-to-5.2.md @@ -0,0 +1,99 @@ +# From 5.1 to 5.2 + +### 0. 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) + +```bash +cd /home/git/gitlab +sudo -u git -H RAILS_ENV=production bundle exec rake gitlab:backup:create +``` + +### 1. Stop server + + sudo service gitlab stop + +### 2. Get latest code + +```bash +cd /home/git/gitlab +sudo -u git -H git fetch +sudo -u git -H git checkout 5-2-stable +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v1.4.0 +``` + +### 4. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL +sudo -u git -H bundle install --without development test postgres --deployment + +#PostgreSQL +sudo -u git -H bundle install --without development test mysql --deployment + +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production +``` + +### 5. Update config files + +* Make `/home/git/gitlab/config/gitlab.yml` same as https://github.com/gitlabhq/gitlabhq/blob/5-2-stable/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/puma.rb` same as https://github.com/gitlabhq/gitlabhq/blob/5-2-stable/config/puma.rb.example but with your settings. + +### 6. Update Init script + +```bash +cd /home/git/gitlab +sudo rm /etc/init.d/gitlab +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +sudo chmod +x /etc/init.d/gitlab +``` + +### 7. Create uploads directory + +```bash +cd /home/git/gitlab +sudo -u git -H mkdir public/uploads +sudo chmod -R u+rwX public/uploads +``` + +### 8. Start application + + sudo service gitlab start + sudo service nginx restart + +### 9. 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 complete! + +## Things went south? Revert to previous version (5.1) + +### 1. Revert the code to the previous version +Follow the [`upgrade guide from 5.0 to 5.1`](5.0-to-5.1.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 RAILS_ENV=production bundle exec rake gitlab:backup:restore +``` diff --git a/doc/update/5.2-to-5.3.md b/doc/update/5.2-to-5.3.md new file mode 100644 index 00000000000..e00dfa3951a --- /dev/null +++ b/doc/update/5.2-to-5.3.md @@ -0,0 +1,82 @@ +# From 5.2 to 5.3 + +### 0. 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) + +```bash +cd /home/git/gitlab +sudo -u git -H RAILS_ENV=production bundle exec rake gitlab:backup:create +``` + +### 1. Stop server + + sudo service gitlab stop + +### 2. Get latest code + +```bash +cd /home/git/gitlab +sudo -u git -H git fetch +sudo -u git -H git checkout 5-3-stable +``` + +### 3. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL +sudo -u git -H bundle install --without development test postgres --deployment + +#PostgreSQL +sudo -u git -H bundle install --without development test mysql --deployment + +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production +``` + +### 4. Update config files + +* Make `/home/git/gitlab/config/gitlab.yml` same as https://github.com/gitlabhq/gitlabhq/blob/5-3-stable/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/puma.rb` same as https://github.com/gitlabhq/gitlabhq/blob/5-3-stable/config/puma.rb.example but with your settings. + +### 5. Update Init script + +```bash +sudo rm /etc/init.d/gitlab +sudo curl --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-3-stable/lib/support/init.d/gitlab +sudo chmod +x /etc/init.d/gitlab +``` + +### 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 complete! + +## Things went south? Revert to previous version (5.2) + +### 1. Revert the code to the previous version +Follow the [`upgrade guide from 5.1 to 5.2`](5.1-to-5.2.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 RAILS_ENV=production bundle exec rake gitlab:backup:restore +``` diff --git a/doc/update/5.3-to-5.4.md b/doc/update/5.3-to-5.4.md new file mode 100644 index 00000000000..5fba0e26afa --- /dev/null +++ b/doc/update/5.3-to-5.4.md @@ -0,0 +1,90 @@ +# From 5.3 to 5.4 + +### 0. 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) + +```bash +cd /home/git/gitlab +sudo -u git -H RAILS_ENV=production bundle exec rake gitlab:backup:create +``` + +### 1. Stop server + + sudo service gitlab stop + +### 2. Get latest code + +```bash +cd /home/git/gitlab +sudo -u git -H git fetch +sudo -u git -H git checkout 5-4-stable +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v1.5.0 +``` + +### 4. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL +sudo -u git -H bundle install --without development test postgres --deployment + +#PostgreSQL +sudo -u git -H bundle install --without development test mysql --deployment + +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production +``` + +### 5. Update config files + +* Make `/home/git/gitlab/config/gitlab.yml` same as https://github.com/gitlabhq/gitlabhq/blob/5-4-stable/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/puma.rb` same as https://github.com/gitlabhq/gitlabhq/blob/5-4-stable/config/puma.rb.example but with your settings. + +### 6. Update Init script + +```bash +sudo rm /etc/init.d/gitlab +sudo curl --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-4-stable/lib/support/init.d/gitlab +sudo chmod +x /etc/init.d/gitlab +``` + +### 7. Start application + + sudo service gitlab start + sudo service nginx restart + +### 8. 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 complete! + +## Things went south? Revert to previous version (5.3) + +### 1. Revert the code to the previous version +Follow the [`upgrade guide from 5.2 to 5.3`](5.2-to-5.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 RAILS_ENV=production bundle exec rake gitlab:backup:restore +``` diff --git a/doc/update/5.4-to-6.0.md b/doc/update/5.4-to-6.0.md new file mode 100644 index 00000000000..3b1d9878204 --- /dev/null +++ b/doc/update/5.4-to-6.0.md @@ -0,0 +1,113 @@ +# From 5.4 to 6.0 + +### Deprecations + +#### Global projects + +The root (global) namespace for projects is deprecated. +So you need to move all your global projects under groups or users manually before update or they will be automatically moved to the project owner namespace during the update. When a project is moved all its members will receive an email with instructions how to update their git remote url. Please make sure you disable ssending email when you do a test of the upgrade. + +#### Teams + +We introduce group membership in 6.0 as a replacement for teams. +The old combination of groups and teams was confusing for a lot of people. +And when the members of a team where changed this wasn't reflected in the project permissions. +In GitLab 6.0 you will be able to add members to a group with a permission level for each member. +These group members will have access to the projects in that group. +Any changes to group members will immediately be reflected in the project permissions. +You can even have multiple owners for a group, greatly simplifying administration. + +### 0. 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) + +```bash +cd /home/git/gitlab +sudo -u git -H RAILS_ENV=production bundle exec rake gitlab:backup:create +``` + +### 1. Stop server + + sudo service gitlab stop + +### 2. Get latest code + +```bash +cd /home/git/gitlab +sudo -u git -H git fetch +sudo -u git -H git checkout 6-0-stable +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v1.7.0 +``` + +### 4. Install additional packages + +```bash +# For reStructuredText markup language support install required package: +sudo apt-get install python-docutils +``` + +### 5. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL +sudo -u git -H bundle install --without development test mysql --deployment + +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production +sudo -u git -H bundle exec rake migrate_groups RAILS_ENV=production +sudo -u git -H bundle exec rake migrate_global_projects RAILS_ENV=production +sudo -u git -H bundle exec rake migrate_keys RAILS_ENV=production +sudo -u git -H bundle exec rake migrate_inline_notes RAILS_ENV=production +sudo -u git -H bundle exec rake gitlab:satellites:create RAILS_ENV=production + +# Clear redis cache +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production + +# Clear and precompile assets +sudo -u git -H bundle exec rake assets:clean RAILS_ENV=production +sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production +``` + +### 6. Update config files + +Note: We switched from Puma in GitLab 5.4 to unicorn in GitLab 6.0. + +* Make `/home/git/gitlab/config/gitlab.yml` the same as https://github.com/gitlabhq/gitlabhq/blob/master/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/unicorn.rb` the same as https://github.com/gitlabhq/gitlabhq/blob/master/config/unicorn.rb.example but with your settings. + +### 7. Update Init script + +```bash +sudo rm /etc/init.d/gitlab +sudo curl --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/master/lib/support/init.d/gitlab +sudo chmod +x /etc/init.d/gitlab +``` + +### 8. Start application + + sudo service gitlab start + sudo service nginx restart + +### 9. 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 complete! diff --git a/doc/update/6.0-to-6.1.md b/doc/update/6.0-to-6.1.md new file mode 100644 index 00000000000..2fb762e467a --- /dev/null +++ b/doc/update/6.0-to-6.1.md @@ -0,0 +1,102 @@ +# From 6.0 to 6.1 + +# In 6.1 we remove a lot of deprecated code. +# You should update to 6.0 before installing 6.1 so all the necessary conversions are run. + +### Deprecations + +#### Global issue numbers + +In 6.1 issue numbers are project specific. This means all issues are renumbered and get a new number in their url. If you use an old issue number url and the issue number does not exist yet you are redirected to the new one. This conversion does not trigger if the old number already exists for this project, this is unlikely but will happen with old issues and large projects. + +### 0. 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) + +```bash +cd /home/git/gitlab +sudo -u git -H RAILS_ENV=production bundle exec rake gitlab:backup:create +``` + +### 1. Stop server + + sudo service gitlab stop + +### 2. Get latest code + +```bash +cd /home/git/gitlab +sudo -u git -H git fetch +sudo -u git -H git checkout 6-1-stable +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v1.7.1 +``` + +### 4. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL +sudo -u git -H bundle install --without development test postgres --deployment + +#PostgreSQL +sudo -u git -H bundle install --without development test mysql --deployment + + +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production +sudo -u git -H bundle exec rake migrate_iids RAILS_ENV=production +sudo -u git -H bundle exec rake assets:clean RAILS_ENV=production +sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +### 5. Update config files + +* Make `/home/git/gitlab/config/gitlab.yml` same as https://github.com/gitlabhq/gitlabhq/blob/6-1-stable/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/unicorn.rb` same as https://github.com/gitlabhq/gitlabhq/blob/6-1-stable/config/unicorn.rb.example but with your settings. + +### 6. Update Init script + +```bash +sudo rm /etc/init.d/gitlab +sudo curl --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/6-1-stable/lib/support/init.d/gitlab +sudo chmod +x /etc/init.d/gitlab +``` + +### 7. Start application + + sudo service gitlab start + sudo service nginx restart + +### 8. 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 complete! + +## Things went south? Revert to previous version (6.0) + +### 1. Revert the code to the previous version +Follow the [`upgrade guide from 5.4 to 6.0`](5.4-to-6.0.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 RAILS_ENV=production bundle exec rake gitlab:backup:restore +``` diff --git a/features/admin/groups.feature b/features/admin/groups.feature index 28f35e3a831..6fed9a34869 100644 --- a/features/admin/groups.feature +++ b/features/admin/groups.feature @@ -5,12 +5,16 @@ Feature: Admin Groups And Create gitlab user "John" And I visit admin groups page + Scenario: See group list + Then I should be all groups + Scenario: Create a group When I click new group link And submit form with new group info Then I should be redirected to group page And I should see newly created group + @javascript Scenario: Add user into projects in group When I visit admin group page When I select user "John" from user list as "Reporter" diff --git a/features/admin/teams.feature b/features/admin/teams.feature deleted file mode 100644 index 6a15fddcdcc..00000000000 --- a/features/admin/teams.feature +++ /dev/null @@ -1,70 +0,0 @@ -Feature: Admin Teams - Background: - Given I sign in as an admin - And Create gitlab user "John" - - Scenario: Create a team - When I visit admin teams page - And I click new team link - And submit form with new team info - Then I should be redirected to team page - And I should see newly created team - - Scenario: Add user to team - When I visit admin teams page - When I have clean "HardCoders" team - And I visit "HardCoders" team page - When I click to "Add members" link - When I select user "John" from user list as "Developer" - And submit form with new team member info - Then I should see "John" in teams members list as "Developer" - - Scenario: Assign team to existing project - When I visit admin teams page - When I have "HardCoders" team with "John" member with "Developer" role - When I have "Shop" project - And I visit "HardCoders" team page - Then I should see empty projects table - When I click to "Add projects" link - When I select project "Shop" with max access "Reporter" - And submit form with new team project info - Then I should see "Shop" project in projects list - When I visit "Shop" project admin page - Then I should see "John" user with role "Reporter" in team table - - Scenario: Add user to team with ptojects - When I visit admin teams page - When I have "HardCoders" team with "John" member with "Developer" role - And "HardCoders" team assigned to "Shop" project with "Developer" max role access - When I have gitlab user "Jimm" - And I visit "HardCoders" team page - Then I should see members table without "Jimm" member - When I click to "Add members" link - When I select user "Jimm" ub team members list as "Master" - And submit form with new team member info - Then I should see "Jimm" in teams members list as "Master" - - Scenario: Remove member from team - Given I have users team "HardCoders" - And gitlab user "John" is a member "HardCoders" team - And gitlab user "Jimm" is a member "HardCoders" team - And "HardCoders" team is assigned to "Shop" project - When I visit admin teams page - When I visit "HardCoders" team admin page - Then I shoould see "John" in members list - And I should see "Jimm" in members list - And I should see "Shop" in projects list - When I click on remove "Jimm" user link - Then I should be redirected to "HardCoders" team admin page - And I should not to see "Jimm" user in members list - - Scenario: Remove project from team - Given I have users team "HardCoders" - And gitlab user "John" is a member "HardCoders" team - And gitlab user "Jimm" is a member "HardCoders" team - And "HardCoders" team is assigned to "Shop" project - When I visit admin teams page - When I visit "HardCoders" team admin page - Then I should see "Shop" project in projects list - When I click on "Relegate" link on "Shop" project - Then I should see projects liston team page without "Shop" project diff --git a/features/admin/users.feature b/features/admin/users.feature index 03ac86a367b..7f503cf9235 100644 --- a/features/admin/users.feature +++ b/features/admin/users.feature @@ -6,3 +6,11 @@ Feature: Admin Users Scenario: On Admin Users Given I visit admin users page Then I should see all users + + Scenario: Edit user and change username to non ascii char + When I visit admin users page + And Click edit + And Input non ascii char in username + And Click save + Then See username error message + And Not changed form action url diff --git a/features/dashboard/active_tab.feature b/features/dashboard/active_tab.feature index 6715ea269c7..3e1cf5aa0ca 100644 --- a/features/dashboard/active_tab.feature +++ b/features/dashboard/active_tab.feature @@ -17,11 +17,6 @@ Feature: Dashboard active tab Then the active main tab should be Merge Requests And no other main tabs should be active - Scenario: On Dashboard Search - Given I visit dashboard search page - Then the active main tab should be Search - And no other main tabs should be active - Scenario: On Dashboard Help Given I visit dashboard help page Then the active main tab should be Help diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index 695148b5cdf..e249f392de7 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -16,6 +16,7 @@ Feature: Dashboard And I visit dashboard page Then I should see groups list + @javascript Scenario: I should see last push widget Then I should see last push widget And I click "Create Merge Request" link diff --git a/features/dashboard/projects.feature b/features/dashboard/projects.feature index 17022dab54f..7d8c129face 100644 --- a/features/dashboard/projects.feature +++ b/features/dashboard/projects.feature @@ -1,8 +1,8 @@ -Feature: Dashboard +Feature: Dashboard projects Background: Given I sign in as a user And I own project "Shop" And I visit dashboard projects page - Scenario: I should see issues list + Scenario: I should see projects list Then I should see projects list diff --git a/features/dashboard/search.feature b/features/dashboard/search.feature index 9813d9d1e7c..91d870f46f3 100644 --- a/features/dashboard/search.feature +++ b/features/dashboard/search.feature @@ -2,13 +2,8 @@ Feature: Dashboard Search Background: Given I sign in as a user And I own project "Shop" - And Project "Shop" has wiki page "Contibuting guide" And I visit dashboard search page Scenario: I should see project I am looking for Given I search for "Sho" Then I should see "Shop" project link - - Scenario: I should see wiki page I am looking for - Given I search for "Contibuting" - Then I should see "Contibuting guide" wiki link
\ No newline at end of file diff --git a/features/group/group.feature b/features/group/group.feature index a48affe8e02..9fec19a4dc1 100644 --- a/features/group/group.feature +++ b/features/group/group.feature @@ -19,9 +19,10 @@ Feature: Groups When I visit group merge requests page Then I should see merge requests from this group assigned to me + @javascript Scenario: I should add user to projects in Group Given I have new user "John" - When I visit group people page + When I visit group members page And I select user "John" from list with role "Reporter" Then I should see user "John" in team list diff --git a/features/profile/notifications.feature b/features/profile/notifications.feature new file mode 100644 index 00000000000..e7937953c1b --- /dev/null +++ b/features/profile/notifications.feature @@ -0,0 +1,8 @@ +Feature: Profile Notifications + Background: + Given I sign in as a user + And I own project "Shop" + + Scenario: I visit notifications tab + When I visit profile notifications page + Then I should see global notifications settings diff --git a/features/profile/profile.feature b/features/profile/profile.feature index 95b85a9f911..c74b0993fb3 100644 --- a/features/profile/profile.feature +++ b/features/profile/profile.feature @@ -11,11 +11,25 @@ Feature: Profile Then I change my contact info And I should see new contact info + Scenario: I change my password without old one + Given I visit profile account page + When I try change my password w/o old one + Then I should see a missing password error message + And I should be redirected to account page + Scenario: I change my password Given I visit profile account page Then I change my password And I should be redirected to sign in page + Scenario: My password is expired + Given my password is expired + And I am not an ldap user + And I visit profile account page + Then I redirected to expired password page + And I submit new password + And I redirected to sign in page + Scenario: I unsuccessfully change my password Given I visit profile account page When I unsuccessfully change my password @@ -31,6 +45,11 @@ Feature: Profile When I visit profile history page Then I should see my activity + Scenario: I visit my user page + When I visit profile page + And I click on my profile picture + Then I should see my user page + @javascript Scenario: I change my application theme Given I visit profile design page diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index 2d3e41d3d33..48c217fbea7 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -49,51 +49,38 @@ Feature: Project active tab Scenario: On Project Home/Show Given I visit my project's home page - Then the active sub tab should be Show - And no other sub tabs should be active - And the active main tab should be Home - - Scenario: On Project Home/Team - Given I visit my project's home page - And I click the "Team" tab - Then the active sub tab should be Team - And no other sub tabs should be active - And the active main tab should be Home + Then the active main tab should be Home + And no other main tabs should be active - Scenario: On Project Home/Attachments - Given I visit my project's home page - And I click the "Attachments" tab - Then the active sub tab should be Attachments - And no other sub tabs should be active - And the active main tab should be Home + # Sub Tabs: Settings - Scenario: On Project Home/Snippets - Given I visit my project's home page - And I click the "Snippets" tab - Then the active sub tab should be Snippets - And no other sub tabs should be active - And the active main tab should be Home + Scenario: On Project Settings/Team + Given I visit my project's settings page + And I click the "Team" tab + Then the active sub nav should be Team + And no other sub navs should be active + And the active main tab should be Settings - Scenario: On Project Home/Edit - Given I visit my project's home page + Scenario: On Project Settings/Edit + Given I visit my project's settings page And I click the "Edit" tab - Then the active sub tab should be Edit - And no other sub tabs should be active - And the active main tab should be Home + Then the active sub nav should be Edit + And no other sub navs should be active + And the active main tab should be Settings - Scenario: On Project Home/Hooks - Given I visit my project's home page + Scenario: On Project Settings/Hooks + Given I visit my project's settings page And I click the "Hooks" tab - Then the active sub tab should be Hooks - And no other sub tabs should be active - And the active main tab should be Home + Then the active sub nav should be Hooks + And no other sub navs should be active + And the active main tab should be Settings - Scenario: On Project Home/Deploy Keys - Given I visit my project's home page + Scenario: On Project Settings/Deploy Keys + Given I visit my project's settings page And I click the "Deploy Keys" tab - Then the active sub tab should be Deploy Keys - And no other sub tabs should be active - And the active main tab should be Home + Then the active sub nav should be Deploy Keys + And no other sub navs should be active + And the active main tab should be Settings # Sub Tabs: Commits diff --git a/features/project/commits/commit_diff_comments.feature b/features/project/commits/commit_diff_comments.feature index 884fab527f5..b26019f832f 100644 --- a/features/project/commits/commit_diff_comments.feature +++ b/features/project/commits/commit_diff_comments.feature @@ -83,10 +83,3 @@ Feature: Comments on commit diffs And I submit the diff comment Then I should not see the diff comment form And I should see a discussion reply button - - - #@wip @javascript - #Scenario: I can delete a discussion comment - # Given I leave a diff comment like "Typo, please fix" - # And I delete a diff comment - # Then I should not see a diff comment saying "Typo, please fix" diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index 56069cdc977..fe470f5ac99 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -27,3 +27,15 @@ Feature: Project Browse commits Scenario: I browse commits stats Given I visit my project's commits stats page Then I see commits stats + + Scenario: I browse big commit + Given I visit big commit page + Then I see big commit warning + + Scenario: I browse huge commit + Given I visit huge commit page + Then I see huge commit message + + Scenario: I browse a commit with an image + Given I visit a commit with an image that changed + Then The diff links to both the previous and current image diff --git a/features/project/create_project.feature b/features/project/create_project.feature index b7cdfdb818e..395a3218b2b 100644 --- a/features/project/create_project.feature +++ b/features/project/create_project.feature @@ -9,3 +9,14 @@ Feature: Create Project And fill project form with valid data Then I should see project page And I should see empty project instuctions + + @javascript + Scenario: Empty project instructions + Given I sign in as a user + When I visit new project page + And fill project form with valid data + Then I see empty project instuctions + And I click on HTTP + Then Remote url should update to http link + And If I click on SSH + Then Remote url should update to ssh link
\ No newline at end of file diff --git a/features/project/deploy_keys.feature b/features/project/deploy_keys.feature new file mode 100644 index 00000000000..13e3b9bbd2e --- /dev/null +++ b/features/project/deploy_keys.feature @@ -0,0 +1,23 @@ +Feature: Project Deploy Keys + Background: + Given I sign in as a user + And I own project "Shop" + + Scenario: I should see deploy keys list + Given project has deploy key + When I visit project deploy keys page + Then I should see project deploy keys + + Scenario: I add new deploy key + Given I visit project deploy keys page + When I click 'New Deploy Key' + And I submit new deploy key + Then I should be on deploy keys page + And I should see newly created deploy key + + Scenario: I attach deploy key to project + Given other project has deploy key + And I visit project deploy keys page + When I click attach deploy key + Then I should be on deploy keys page + And I should see newly created deploy key diff --git a/features/project/fork_project.feature b/features/project/fork_project.feature new file mode 100644 index 00000000000..dc477ca3bf3 --- /dev/null +++ b/features/project/fork_project.feature @@ -0,0 +1,14 @@ +Feature: Fork Project + Background: + Given I sign in as a user + And I am a member of project "Shop" + When I visit project "Shop" page + + Scenario: User fork a project + Given I click link "Fork" + 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" + Then I should see a "Name has already been taken" warning diff --git a/features/project/forked_merge_requests.feature b/features/project/forked_merge_requests.feature new file mode 100644 index 00000000000..966905645a2 --- /dev/null +++ b/features/project/forked_merge_requests.feature @@ -0,0 +1,34 @@ +Feature: Project Forked Merge Requests + Background: + Given I sign in as a user + And I am a member of project "Shop" + And I have a project forked off of "Shop" called "Forked Shop" + + @javascript + Scenario: I submit new unassigned merge request to a forked project + Given I visit project "Forked Shop" merge requests page + And I click link "New Merge Request" + And I fill out a "Merge Request On Forked Project" merge request + And I submit the merge request + Then I should see merge request "Merge Request On Forked Project" + + @javascript + Scenario: I can edit a forked merge request + Given I visit project "Forked Shop" merge requests page + And I click link "New Merge Request" + And I fill out a "Merge Request On Forked Project" merge request + And I submit the merge request + And I should see merge request "Merge Request On Forked Project" + And I click link edit "Merge Request On Forked Project" + Then I see the edit page prefilled for "Merge Request On Forked Project" + And I update the merge request title + And I save the merge request + Then I should see the edited merge request + + @javascript + Scenario: I cannot submit an invalid merge request + Given I visit project "Forked Shop" merge requests page + And I click link "New Merge Request" + And I fill out an invalid "Merge Request On Forked Project" merge request + And I submit the merge request + Then I should see validation errors diff --git a/features/project/graph.feature b/features/project/graph.feature new file mode 100644 index 00000000000..cda95f5dda6 --- /dev/null +++ b/features/project/graph.feature @@ -0,0 +1,9 @@ +Feature: Project Graph + Background: + Given I sign in as a user + And I own project "Shop" + And I visit project "Shop" graph page + + @javascript + Scenario: I should see project graphs + Then page should have graphs diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index d6ef384c9a6..67986784bc7 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -3,6 +3,7 @@ Feature: Project Issues Given I sign in as a user And I own project "Shop" And project "Shop" have "Release 0.4" open issue + And project "Shop" have "Tweet control" open issue And project "Shop" have "Release 0.3" closed issue And I visit project "Shop" issues page @@ -37,37 +38,20 @@ Feature: Project Issues @javascript Scenario: I search issue - Given I fill in issue search with "Release" + Given I fill in issue search with "Re" Then I should see "Release 0.4" in issues And I should not see "Release 0.3" in issues + And I should not see "Tweet control" in issues @javascript Scenario: I search issue that not exist - Given I fill in issue search with "Bug" + Given I fill in issue search with "Bu" Then I should not see "Release 0.4" in issues And I should not see "Release 0.3" in issues - @javascript Scenario: I search all issues Given I click link "All" - And I fill in issue search with "0.3" + And I fill in issue search with ".3" Then I should see "Release 0.3" in issues And I should not see "Release 0.4" in issues - - # Disable this two cause of random failing - # TODO: fix after v4.0 released - #@javascript - #Scenario: I create Issue with pre-selected milestone - #Given project "Shop" has milestone "v2.2" - #And project "Shop" has milestone "v3.0" - #And I visit project "Shop" issues page - #When I select milestone "v3.0" - #And I click link "New Issue" - #Then I should see selected milestone with title "v3.0" - - #@javascript - #Scenario: I create Issue with pre-selected assignee - #When I select first assignee from "Shop" project - #And I click link "New Issue" - #Then I should see first assignee from "Shop" as selected assignee diff --git a/features/project/issues/milestones.feature b/features/project/issues/milestones.feature index 50c090cc6a0..2f38acf14d0 100644 --- a/features/project/issues/milestones.feature +++ b/features/project/issues/milestones.feature @@ -22,5 +22,3 @@ Feature: Project Milestones Given the milestone has open and closed issues And I click link "v2.2" Then I should see 3 issues - When I click link "All Issues" - Then I should see 4 issues diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 5b8becbb6c9..63f27c3acc3 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -29,6 +29,7 @@ Feature: Project Merge Requests And I click link "Close" Then I should see closed merge request "Bug NS-04" + @javascript Scenario: I submit new unassigned merge request Given I click link "New Merge Request" And I submit new merge request "Wiki Feature" diff --git a/features/project/network.feature b/features/project/network.feature index 31ce5ad3279..ceae08c1074 100644 --- a/features/project/network.feature +++ b/features/project/network.feature @@ -7,3 +7,34 @@ Feature: Project Network Graph @javascript Scenario: I should see project network Then page should have network graph + And page should select "master" in select box + And page should have "master" on graph + + @javascript + Scenario: I should switch "branch" and "tag" + When I switch ref to "stable" + Then page should select "stable" in select box + And page should have "stable" on graph + When I switch ref to "v2.1.0" + Then page should select "v2.1.0" in select box + And page should have "v2.1.0" on graph + + @javascript + Scenario: I should looking for a commit by SHA + When I looking for a commit by SHA of "v2.1.0" + Then page should have network graph + And page should select "master" in select box + And page should have "v2.1.0" on graph + + @javascript + Scenario: I should filter selected tag + When I switch ref to "v2.1.0" + Then page should have content not cotaining "v2.1.0" + When click "Show only selected branch" checkbox + Then page should not have content not cotaining "v2.1.0" + When click "Show only selected branch" checkbox + Then page should have content not cotaining "v2.1.0" + + Scenario: I should fail to look for a commit + When I look for a commit by ";" + Then page status code should be 404 diff --git a/features/project/project.feature b/features/project/project.feature index 23fef69ee48..59eda4a781d 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -5,6 +5,7 @@ Feature: Project Feature And project "Shop" has push event And I visit project "Shop" page + @javascript Scenario: I should see project activity When I visit project "Shop" page Then I should see project "Shop" activity feed @@ -18,6 +19,3 @@ Feature: Project Feature And change project settings And I save project Then I should see project with new settings - - # @wip - # Scenario: I visit attachments diff --git a/features/project/public_projects.feature b/features/project/public_projects.feature new file mode 100644 index 00000000000..c5a9da14c54 --- /dev/null +++ b/features/project/public_projects.feature @@ -0,0 +1,8 @@ +Feature: Public Projects + Background: + Given I sign in as a user + + Scenario: I should see the list of public projects + When I visit the public projects area + Then I should see the list of public projects + diff --git a/features/project/service.feature b/features/project/service.feature index ca8a4756056..e685c385d1d 100644 --- a/features/project/service.feature +++ b/features/project/service.feature @@ -12,3 +12,15 @@ Feature: Project Services And I click gitlab-ci service link And I fill gitlab-ci settings Then I should see service settings saved + + Scenario: Activate hipchat service + When I visit project "Shop" services page + And I click hipchat service link + And I fill hipchat settings + Then I should see hipchat service settings saved + + Scenario: Activate pivotaltracker service + When I visit project "Shop" services page + And I click pivotaltracker service link + And I fill pivotaltracker settings + Then I should see pivotaltracker service settings saved diff --git a/features/project/snippets.feature b/features/project/snippets.feature new file mode 100644 index 00000000000..dfaa02663a0 --- /dev/null +++ b/features/project/snippets.feature @@ -0,0 +1,35 @@ +Feature: Project Snippets + Background: + Given I sign in as a user + And I own project "Shop" + And project "Shop" have "Snippet one" snippet + And project "Shop" have no "Snippet two" snippet + And I visit project "Shop" snippets page + + Scenario: I should see snippets + Given I visit project "Shop" snippets page + Then I should see "Snippet one" in snippets + And I should not see "Snippet two" in snippets + + Scenario: I create new project snippet + Given I click link "New Snippet" + And I submit new snippet "Snippet three" + Then I should see snippet "Snippet three" + + @javascript + Scenario: I comment on a snippet "Snippet one" + Given I visit snippet page "Snippet one" + And I leave a comment like "Good snippet!" + Then I should see comment "Good snippet!" + + Scenario: I update "Snippet one" + Given I visit snippet page "Snippet one" + And I click link "Edit" + And I submit new title "Snippet new title" + Then I should see "Snippet new title" + + Scenario: I destroy "Snippet one" + Given I visit snippet page "Snippet one" + And I click link "Edit" + And I click link "Remove Snippet" + Then I should not see "Snippet one" in snippets diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 0b8495ffc58..ee26f5371a9 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -12,7 +12,7 @@ Feature: Project Browse files Then I should see files from repository for "8470d70" Scenario: I browse file content - Given I click on "Gemfile" file in repo + Given I click on "Gemfile.lock" file in repo Then I should see it content Scenario: I browse raw file @@ -22,6 +22,6 @@ Feature: Project Browse files @javascript Scenario: I can edit file - Given I click on "Gemfile" file in repo + Given I click on "Gemfile.lock" file in repo And I click button "edit" Then I can edit code diff --git a/features/project/source/git_blame.feature b/features/project/source/git_blame.feature index 93ed20a8c42..3b20437a875 100644 --- a/features/project/source/git_blame.feature +++ b/features/project/source/git_blame.feature @@ -5,6 +5,6 @@ Feature: Project Browse git repo Given I visit project source page Scenario: I blame file - Given I click on "Gemfile" file in repo + Given I click on "Gemfile.lock" file in repo And I click blame button Then I should see git file blame diff --git a/features/project/source/search_code.feature b/features/project/source/search_code.feature new file mode 100644 index 00000000000..13f15cc922f --- /dev/null +++ b/features/project/source/search_code.feature @@ -0,0 +1,9 @@ +Feature: Project Search code + Background: + Given I sign in as a user + And I own project "Shop" + Given I visit project source page + + Scenario: Search for term "Welcome to GitLab" + When I search for term "Welcome to GitLab" + Then I should see files from repository containing "Welcome to GitLab" diff --git a/features/project/team_management.feature b/features/project/team_management.feature index 0ac37620b4e..e153978e043 100644 --- a/features/project/team_management.feature +++ b/features/project/team_management.feature @@ -11,6 +11,7 @@ Feature: Project Team management Then I should be able to see myself in team And I should see "Sam" in team list + @javascript Scenario: Add user to project Given I click link "New Team Member" And I select "Mike" as "Reporter" @@ -20,22 +21,17 @@ Feature: Project Team management Scenario: Update user access Given I should see "Sam" in team list as "Developer" And I change "Sam" role to "Reporter" - Then I visit project "Shop" team page And I should see "Sam" in team list as "Reporter" - Scenario: View team member profile - Given I click link "Sam" - Then I should see "Sam" team profile - Scenario: Cancel team member - Given I click link "Sam" - And I click link "Remove from team" + Given I click cancel link for "Sam" Then I visit project "Shop" team page And I should not see "Sam" in team list Scenario: Import team from another project Given I own project "Website" And "Mike" is "Website" reporter + When I visit project "Shop" team page And I click link "Import team from another project" - When I submit "Website" project for import team + And I submit "Website" project for import team Then I should see "Mike" in team list as "Reporter" diff --git a/features/project/wiki.feature b/features/project/wiki.feature index f052e2f244c..90eb2b79c66 100644 --- a/features/project/wiki.feature +++ b/features/project/wiki.feature @@ -5,5 +5,43 @@ Feature: Project Wiki Given I visit project wiki page Scenario: Add new page - Given I create Wiki page - Then I should see newly created wiki page + Given I create the Wiki Home page + Then I should see the newly created wiki page + + Scenario: Pressing Cancel while editing a brand new Wiki + Given I click on the Cancel button + Then I should be redirected back to the Edit Home Wiki page + + Scenario: Edit existing page + Given I have an existing Wiki page + And I browse to that Wiki page + And I click on the Edit button + And I change the content + Then I should see the updated content + + Scenario: Pressing Cancel while editing an existing Wiki page + Given I have an existing Wiki page + And I browse to that Wiki page + And I click on the Edit button + And I click on the Cancel button + Then I should be redirected back to that Wiki page + + Scenario: View page history + Given I have an existing wiki page + And That page has two revisions + And I browse to that Wiki page + And I click the History button + Then I should see both revisions + + Scenario: Destroy Wiki page + Given I have an existing wiki page + And I browse to that Wiki page + And I click on the Edit button + And I click on the "Delete this page" button + Then The page should be deleted + + Scenario: View all pages + Given I have an existing wiki page + And I browse to that Wiki page + And I click on the "Pages" button + Then I should see the existing page in the pages list diff --git a/features/public/public_projects.feature b/features/public/public_projects.feature new file mode 100644 index 00000000000..178a769194c --- /dev/null +++ b/features/public/public_projects.feature @@ -0,0 +1,18 @@ +Feature: Public Projects Feature + Background: + Given public project "Community" + And private project "Enterprise" + + Scenario: I visit public area + When I visit the public projects area + Then I should see project "Community" + And I should not see project "Enterprise" + + Scenario: I visit public project page + When I visit project "Community" page + Then I should see project "Community" home page + + Scenario: I visit an empty public project page + Given public empty project "Empty Public Project" + When I visit empty project page + Then I should see empty public project details diff --git a/features/snippets/discover_snippets.feature b/features/snippets/discover_snippets.feature new file mode 100644 index 00000000000..d6fd2cd7808 --- /dev/null +++ b/features/snippets/discover_snippets.feature @@ -0,0 +1,10 @@ +Feature: Discover Snippets + Background: + Given I sign in as a user + And I have public "Personal snippet one" snippet + And I have private "Personal snippet private" snippet + + Scenario: I should see snippets + Given I visit snippets page + Then I should see "Personal snippet one" in snippets + And I should not see "Personal snippet private" in snippets diff --git a/features/snippets/snippets.feature b/features/snippets/snippets.feature new file mode 100644 index 00000000000..1119defa17d --- /dev/null +++ b/features/snippets/snippets.feature @@ -0,0 +1,28 @@ +Feature: Snippets Feature + Background: + Given I sign in as a user + And I have public "Personal snippet one" snippet + And I have private "Personal snippet private" snippet + + Scenario: I create new snippet + Given I visit new snippet page + And I submit new snippet "Personal snippet three" + Then I should see snippet "Personal snippet three" + + Scenario: I update "Personal snippet one" + Given I visit snippet page "Personal snippet one" + And I click link "Edit" + And I submit new title "Personal snippet new title" + Then I should see "Personal snippet new title" + + Scenario: Set "Personal snippet one" public + Given I visit snippet page "Personal snippet one" + And I click link "Edit" + And I uncheck "Private" checkbox + Then I should see "Personal snippet one" public + + Scenario: I destroy "Personal snippet one" + Given I visit snippet page "Personal snippet one" + And I click link "Edit" + And I click link "Destroy" + Then I should not see "Personal snippet one" in snippets diff --git a/features/snippets/user_snippets.feature b/features/snippets/user_snippets.feature new file mode 100644 index 00000000000..4c8a91501c4 --- /dev/null +++ b/features/snippets/user_snippets.feature @@ -0,0 +1,22 @@ +Feature: User Snippets + Background: + Given I sign in as a user + And I have public "Personal snippet one" snippet + And I have private "Personal snippet private" 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 + + 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 see "Personal snippet private" in snippets + + Scenario: I can see only my public snippets + Given I visit my snippets page + And I click "Public" filter + Then I should see "Personal snippet one" in snippets + And I should not see "Personal snippet private" in snippets diff --git a/features/steps/admin/admin_groups.rb b/features/steps/admin/admin_groups.rb index cbca2daa701..b4591f227e3 100644 --- a/features/steps/admin/admin_groups.rb +++ b/features/steps/admin/admin_groups.rb @@ -2,6 +2,7 @@ class AdminGroups < Spinach::FeatureSteps include SharedAuthentication include SharedPaths include SharedActiveTab + include Select2Helper When 'I visit admin group page' do visit admin_group_path(current_group) @@ -20,16 +21,18 @@ class AdminGroups < Spinach::FeatureSteps end And 'Create gitlab user "John"' do - create(:user, :name => "John") + create(:user, name: "John") end And 'submit form with new group info' do - fill_in 'group_name', :with => 'gitlab' + fill_in 'group_name', with: 'gitlab' + fill_in 'group_description', with: 'Group description' click_button "Create group" end Then 'I should see newly created group' do page.should have_content "Group: gitlab" + page.should have_content "Group description" end Then 'I should be redirected to group page' do @@ -38,17 +41,24 @@ class AdminGroups < Spinach::FeatureSteps When 'I select user "John" from user list as "Reporter"' do user = User.find_by_name("John") + select2(user.id, from: "#user_ids", multiple: true) within "#new_team_member" do - select user.name, :from => "user_ids" - select "Reporter", :from => "project_access" + select "Reporter", from: "group_access" end - click_button "Add user to projects in group" + click_button "Add users into group" end Then 'I should see "John" in team list in every project as "Reporter"' do - user = User.find_by_name("John") - projects_with_access = find(".user_#{user.id} .projects_access") - projects_with_access.should have_link("Reporter") + within ".group-users-list" do + page.should have_content "John" + page.should have_content "Reporter" + end + end + + step 'I should be all groups' do + Group.all.each do |group| + page.should have_content group.name + end end protected diff --git a/features/steps/admin/admin_projects.rb b/features/steps/admin/admin_projects.rb index dd6b4e9810b..b410b23851b 100644 --- a/features/steps/admin/admin_projects.rb +++ b/features/steps/admin/admin_projects.rb @@ -16,9 +16,7 @@ class AdminProjects < Spinach::FeatureSteps Then 'I should see project details' do project = Project.first current_path.should == admin_project_path(project) - page.should have_content(project.name_with_namespace) page.should have_content(project.creator.name) - page.should have_content('Add new team member') end end diff --git a/features/steps/admin/admin_teams.rb b/features/steps/admin/admin_teams.rb deleted file mode 100644 index 5c66b24bccf..00000000000 --- a/features/steps/admin/admin_teams.rb +++ /dev/null @@ -1,234 +0,0 @@ -class AdminTeams < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedActiveTab - include SharedAdmin - - And 'I have own project' do - create :project - end - - And 'Create gitlab user "John"' do - @user = create(:user, :name => "John") - end - - And 'I click new team link' do - click_link "New Team" - end - - And 'submit form with new team info' do - fill_in 'user_team_name', with: 'gitlab' - click_button 'Create team' - end - - Then 'I should be redirected to team page' do - current_path.should == admin_team_path(UserTeam.last) - end - - And 'I should see newly created team' do - page.should have_content "Team: gitlab" - end - - When 'I visit admin teams page' do - visit admin_teams_path - end - - When 'I have clean "HardCoders" team' do - @team = create :user_team, name: "HardCoders", owner: current_user - end - - And 'I visit "HardCoders" team page' do - visit admin_team_path(UserTeam.find_by_name("HardCoders")) - end - - Then 'I should see only me in members table' do - members_list = find("#members_list .member") - members_list.should have_content(current_user.name) - members_list.should have_content(current_user.email) - end - - When 'I select user "John" from user list as "Developer"' do - @user ||= User.find_by_name("John") - within "#team_members" do - select @user.name, :from => "user_ids" - select "Developer", :from => "default_project_access" - end - end - - And 'submit form with new team member info' do - click_button 'add_members_to_team' - end - - Then 'I should see "John" in teams members list as "Developer"' do - @user ||= User.find_by_name("John") - find_in_list("#members_list .member", @user).must_equal true - end - - When 'I visit "John" user admin page' do - pending 'step not implemented' - end - - Then 'I should see "HardCoders" team in teams table' do - pending 'step not implemented' - end - - When 'I have "HardCoders" team with "John" member with "Developer" role' do - @team = create :user_team, name: "HardCoders", owner: current_user - @user ||= User.find_by_name("John") - @team.add_member(@user, UserTeam.access_roles["Developer"], group_admin: false) - end - - When 'I have "Shop" project' do - @project = create :project, name: "Shop" - end - - Then 'I should see empty projects table' do - page.has_no_css?("#projects_list").must_equal true - end - - When 'I select project "Shop" with max access "Reporter"' do - @project ||= Project.find_by_name("Shop") - within "#assign_projects" do - select @project.name, :from => "project_ids" - select "Reporter", :from => "greatest_project_access" - end - - end - - And 'submit form with new team project info' do - click_button 'assign_projects_to_team' - end - - Then 'I should see "Shop" project in projects list' do - project = Project.find_by_name("Shop") - find_in_list("#projects_list .project", project).must_equal true - end - - When 'I visit "Shop" project admin page' do - project = Project.find_by_name("Shop") - visit admin_project_path(project) - end - - And '"HardCoders" team assigned to "Shop" project with "Developer" max role access' do - @team = UserTeam.find_by_name("HardCoders") - @project = create :project, name: "Shop" - @team.assign_to_project(@project, UserTeam.access_roles["Developer"]) - end - - When 'I have gitlab user "Jimm"' do - create :user, name: "Jimm" - end - - Then 'I should see members table without "Jimm" member' do - user = User.find_by_name("Jimm") - find_in_list("#members_list .member", user).must_equal false - end - - When 'I select user "Jimm" ub team members list as "Master"' do - user = User.find_by_name("Jimm") - within "#team_members" do - select user.name, :from => "user_ids" - select "Developer", :from => "default_project_access" - end - end - - Then 'I should see "Jimm" in teams members list as "Master"' do - user = User.find_by_name("Jimm") - find_in_list("#members_list .member", user).must_equal true - end - - Given 'I have users team "HardCoders"' do - @team = create :user_team, name: "HardCoders" - end - - And 'gitlab user "John" is a member "HardCoders" team' do - @team = UserTeam.find_by_name("HardCoders") - @user = User.find_by_name("John") - @user = create :user, name: "John" unless @user - @team.add_member(@user, UserTeam.access_roles["Master"], group_admin: false) - end - - And 'gitlab user "Jimm" is a member "HardCoders" team' do - @team = UserTeam.find_by_name("HardCoders") - @user = User.find_by_name("Jimm") - @user = create :user, name: "Jimm" unless @user - @team.add_member(@user, UserTeam.access_roles["Master"], group_admin: false) - end - - And '"HardCoders" team is assigned to "Shop" project' do - @team = UserTeam.find_by_name("HardCoders") - @project = create :project, name: "Shop" - @team.assign_to_project(@project, UserTeam.access_roles["Developer"]) - end - - When 'I visit "HardCoders" team admin page' do - visit admin_team_path(UserTeam.find_by_name("HardCoders")) - end - - Then 'I shoould see "John" in members list' do - user = User.find_by_name("John") - find_in_list("#members_list .member", user).must_equal true - end - - And 'I should see "Jimm" in members list' do - user = User.find_by_name("Jimm") - find_in_list("#members_list .member", user).must_equal true - end - - And 'I should see "Shop" in projects list' do - project = Project.find_by_name("Shop") - find_in_list("#projects_list .project", project).must_equal true - end - - When 'I click on remove "Jimm" user link' do - user = User.find_by_name("Jimm") - click_link "remove_member_#{user.id}" - end - - Then 'I should be redirected to "HardCoders" team admin page' do - current_path.should == admin_team_path(UserTeam.find_by_name("HardCoders")) - end - - And 'I should not to see "Jimm" user in members list' do - user = User.find_by_name("Jimm") - find_in_list("#members_list .member", user).must_equal false - end - - When 'I click on "Relegate" link on "Shop" project' do - project = Project.find_by_name("Shop") - click_link "relegate_project_#{project.id}" - end - - Then 'I should see projects liston team page without "Shop" project' do - project = Project.find_by_name("Shop") - find_in_list("#projects_list .project", project).must_equal false - end - - Then 'I should see "John" user with role "Reporter" in team table' do - user = User.find_by_name("John") - find_in_list(".team_members", user).must_equal true - end - - When 'I click to "Add members" link' do - click_link "Add members" - end - - When 'I click to "Add projects" link' do - click_link "Add projects" - end - - protected - - def current_team - @team ||= Team.first - end - - def find_in_list(selector, item) - members_list = all(selector) - entered = false - members_list.each do |member_item| - entered = true if member_item.has_content?(item.name) - end - entered - end -end diff --git a/features/steps/admin/admin_users.rb b/features/steps/admin/admin_users.rb index 1828ae705ce..33c1344eaeb 100644 --- a/features/steps/admin/admin_users.rb +++ b/features/steps/admin/admin_users.rb @@ -8,4 +8,27 @@ class AdminUsers < Spinach::FeatureSteps page.should have_content user.name end end + + And 'Click edit' do + @user = User.first + find("#edit_user_#{@user.id}").click + end + + And 'Input non ascii char in username' do + fill_in 'user_username', with: "\u3042\u3044" + end + + And 'Click save' do + click_button("Save") + end + + Then 'See username error message' do + within "#error_explanation" do + page.should have_content "Username" + end + end + + And 'Not changed form action url' do + page.should have_selector %(form[action="/admin/users/#{@user.username}"]) + end end diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index c6832056435..bde32128b92 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -4,7 +4,7 @@ class Dashboard < Spinach::FeatureSteps include SharedProject Then 'I should see "New Project" link' do - page.should have_link "New Project" + page.should have_link "New project" end Then 'I should see "Shop" project link' do @@ -22,6 +22,7 @@ class Dashboard < Spinach::FeatureSteps Then 'I see prefilled new Merge Request page' do current_path.should == new_project_merge_request_path(@project) + find("#merge_request_target_project_id").value.should == @project.id.to_s find("#merge_request_source_branch").value.should == "new_design" find("#merge_request_target_branch").value.should == "master" find("#merge_request_title").value.should == "New Design" @@ -29,7 +30,7 @@ class Dashboard < Spinach::FeatureSteps Given 'user with name "John Doe" joined project "Shop"' do user = create(:user, {name: "John Doe"}) - project = Project.find_by_name "Shop" + project.team << [user, :master] Event.create( project: project, author_id: user.id, @@ -38,12 +39,11 @@ class Dashboard < Spinach::FeatureSteps end Then 'I should see "John Doe joined project at Shop" event' do - page.should have_content "John Doe joined project at Shop" + page.should have_content "John Doe joined project at #{project.name_with_namespace}" end And 'user with name "John Doe" left project "Shop"' do user = User.find_by_name "John Doe" - project = Project.find_by_name "Shop" Event.create( project: project, author_id: user.id, @@ -52,12 +52,12 @@ class Dashboard < Spinach::FeatureSteps end Then 'I should see "John Doe left project at Shop" event' do - page.should have_content "John Doe left project at Shop" + page.should have_content "John Doe left project at #{project.name_with_namespace}" end And 'I have group with projects' do @group = create(:group) - @project = create(:project, group: @group) + @project = create(:project, namespace: @group) @event = create(:closed_issue_event, project: @project) @project.team << [current_user, :master] @@ -83,4 +83,8 @@ class Dashboard < Spinach::FeatureSteps Then 'I should see 1 project at group list' do page.find('span.last_activity/span').should have_content('1') end + + def project + @project ||= Project.find_by_name "Shop" + end end diff --git a/features/steps/dashboard/dashboard_active_tab.rb b/features/steps/dashboard/dashboard_active_tab.rb index 41ecc48c0d3..8f5f0eed816 100644 --- a/features/steps/dashboard/dashboard_active_tab.rb +++ b/features/steps/dashboard/dashboard_active_tab.rb @@ -15,10 +15,6 @@ class DashboardActiveTab < Spinach::FeatureSteps ensure_active_main_tab('Merge Requests') end - Then 'the active main tab should be Search' do - ensure_active_main_tab('Search') - end - Then 'the active main tab should be Help' do ensure_active_main_tab('Help') end diff --git a/features/steps/dashboard/dashboard_event_filters.rb b/features/steps/dashboard/dashboard_event_filters.rb index afa15c31332..d0fe5c9b64b 100644 --- a/features/steps/dashboard/dashboard_event_filters.rb +++ b/features/steps/dashboard/dashboard_event_filters.rb @@ -1,12 +1,12 @@ class EventFilters < Spinach::FeatureSteps include SharedAuthentication - include SharedPaths + include SharedPaths include SharedProject Then 'I should see push event' do page.should have_selector('span.pushed') end - + Then 'I should not see push event' do page.should_not have_selector('span.pushed') end @@ -20,11 +20,11 @@ class EventFilters < Spinach::FeatureSteps end Then 'I should see merge request event' do - page.should have_selector('span.merged') + page.should have_selector('span.accepted') end And 'I should not see merge request event' do - page.should_not have_selector('span.merged') + page.should_not have_selector('span.accepted') end And 'this project has push event' do @@ -61,14 +61,14 @@ class EventFilters < Spinach::FeatureSteps end And 'this project has merge request event' do - merge_request = create :merge_request, author: @user, project: @project + merge_request = create :merge_request, author: @user, source_project: @project, target_project: @project Event.create( project: @project, action: Event::MERGED, target_id: merge_request.id, target_type: "MergeRequest", author_id: @user.id - ) + ) end When 'I click "push" event filter' do diff --git a/features/steps/dashboard/dashboard_merge_requests.rb b/features/steps/dashboard/dashboard_merge_requests.rb index 7cfa8a13ff8..6c1fa39f081 100644 --- a/features/steps/dashboard/dashboard_merge_requests.rb +++ b/features/steps/dashboard/dashboard_merge_requests.rb @@ -6,18 +6,24 @@ class DashboardMergeRequests < Spinach::FeatureSteps merge_requests = @user.merge_requests merge_requests.each do |mr| page.should have_content(mr.title[0..10]) - page.should have_content(mr.project.name) + page.should have_content(mr.target_project.name) + page.should have_content(mr.source_project.name) end end And 'I have authored merge requests' do - project1 = create :project - project2 = create :project + project1_source = create :project + project1_target= create :project + project2_source = create :project + project2_target = create :project - project1.team << [@user, :master] - project2.team << [@user, :master] - merge_request1 = create :merge_request, author: @user, project: project1 - merge_request2 = create :merge_request, author: @user, project: project2 + project1_source.team << [@user, :master] + project1_target.team << [@user, :master] + project2_source.team << [@user, :master] + project2_target.team << [@user, :master] + + merge_request1 = create :merge_request, author: @user, source_project: project1_source, target_project: project1_target + merge_request2 = create :merge_request, author: @user, source_project: project2_source, target_project: project2_target end end diff --git a/features/steps/dashboard/dashboard_projects.rb b/features/steps/dashboard/dashboard_projects.rb new file mode 100644 index 00000000000..85251565446 --- /dev/null +++ b/features/steps/dashboard/dashboard_projects.rb @@ -0,0 +1,11 @@ +class DashboardProjects < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedProject + + Then 'I should see projects list' do + @user.authorized_projects.all.each do |project| + page.should have_link project.name_with_namespace + end + end +end diff --git a/features/steps/dashboard/dashboard_search.rb b/features/steps/dashboard/dashboard_search.rb index 9c8c879479d..32966a8617a 100644 --- a/features/steps/dashboard/dashboard_search.rb +++ b/features/steps/dashboard/dashboard_search.rb @@ -16,15 +16,4 @@ class DashboardSearch < Spinach::FeatureSteps fill_in "dashboard_search", with: "Contibuting" click_button "Search" end - - And 'Project "Shop" has wiki page "Contibuting guide"' do - @wiki_page = create :wiki, - project: @project, - title: "Contibuting guide", - slug: "contributing" - end - - Then 'I should see "Contibuting guide" wiki link' do - page.should have_link "Contibuting guide" - end end diff --git a/features/steps/group/group.rb b/features/steps/group/group.rb index 5cfa4756ac3..99ec77a7613 100644 --- a/features/steps/group/group.rb +++ b/features/steps/group/group.rb @@ -1,6 +1,7 @@ class Groups < Spinach::FeatureSteps include SharedAuthentication include SharedPaths + include Select2Helper Then 'I should see projects list' do current_user.authorized_projects.each do |project| @@ -9,8 +10,9 @@ class Groups < Spinach::FeatureSteps end And 'I have group with projects' do - @group = create(:group, owner: current_user) - @project = create(:project, group: @group) + @group = create(:group) + @group.add_owner(current_user) + @project = create(:project, namespace: @group) @event = create(:closed_issue_event, project: @project) @project.team << [current_user, :master] @@ -28,7 +30,7 @@ class Groups < Spinach::FeatureSteps Then 'I should see merge requests from this group assigned to me' do assigned_to_me(:merge_requests).each do |issue| - page.should have_content issue.title + page.should have_content issue.title[0..80] end end @@ -38,11 +40,11 @@ class Groups < Spinach::FeatureSteps And 'I select user "John" from list with role "Reporter"' do user = User.find_by_name("John") - within "#new_team_member" do - select user.name, :from => "user_ids" - select "Reporter", :from => "project_access" + within ".new_users_group" do + select2(user.id, from: "#user_ids", multiple: true) + select "Reporter", from: "group_access" end - click_button "Add" + click_button "Add users into group" end Then 'I should see user "John" in team list' do @@ -59,22 +61,25 @@ class Groups < Spinach::FeatureSteps Given 'project from group has merge requests assigned to me' do create :merge_request, - project: project, + source_project: project, + target_project: project, assignee: current_user, author: current_user end When 'I click new group link' do - click_link "New Group" + click_link "New group" end And 'submit form with new group info' do - fill_in 'group_name', :with => 'Samurai' + fill_in 'group_name', with: 'Samurai' + fill_in 'group_description', with: 'Tokugawa Shogunate' click_button "Create group" end Then 'I should see newly created group' do page.should have_content "Samurai" + page.should have_content "Tokugawa Shogunate" page.should have_content "You will only see events from projects in this group" end @@ -83,7 +88,7 @@ class Groups < Spinach::FeatureSteps end And 'I change group name' do - fill_in 'group_name', :with => 'new-name' + fill_in 'group_name', with: 'new-name' click_button "Save group" end diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index b6833f2bde2..5b2a6321265 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -2,78 +2,141 @@ class Profile < Spinach::FeatureSteps include SharedAuthentication include SharedPaths - Then 'I should see my profile info' do + step 'I should see my profile info' do page.should have_content "Profile" page.should have_content @user.name page.should have_content @user.email end - Then 'I change my contact info' do - fill_in "user_skype", :with => "testskype" - fill_in "user_linkedin", :with => "testlinkedin" - fill_in "user_twitter", :with => "testtwitter" - click_button "Save" + step 'I change my contact info' do + fill_in "user_skype", with: "testskype" + fill_in "user_linkedin", with: "testlinkedin" + fill_in "user_twitter", with: "testtwitter" + click_button "Save changes" @user.reload end - And 'I should see new contact info' do + step 'I should see new contact info' do @user.skype.should == 'testskype' @user.linkedin.should == 'testlinkedin' @user.twitter.should == 'testtwitter' end - Then 'I change my password' do - fill_in "user_password", :with => "222333" - fill_in "user_password_confirmation", :with => "222333" - click_button "Save" + step 'I try change my password w/o old one' do + within '.update-password' do + fill_in "user_password", with: "222333" + fill_in "user_password_confirmation", with: "222333" + click_button "Save" + end end - When 'I unsuccessfully change my password' do - fill_in "user_password", with: "password" - fill_in "user_password_confirmation", with: "confirmation" - click_button "Save" + step 'I change my password' do + within '.update-password' do + fill_in "user_current_password", with: "123456" + fill_in "user_password", with: "222333" + fill_in "user_password_confirmation", with: "222333" + click_button "Save" + end end - Then "I should see a password error message" do + step 'I unsuccessfully change my password' do + within '.update-password' do + fill_in "user_current_password", with: "123456" + fill_in "user_password", with: "password" + fill_in "user_password_confirmation", with: "confirmation" + click_button "Save" + end + end + + step "I should see a missing password error message" do + page.should have_content "You must provide a valid current password" + end + + step "I should see a password error message" do page.should have_content "Password doesn't match confirmation" end - And 'I should be redirected to sign in page' do + step 'I should be redirected to sign in page' do current_path.should == new_user_session_path end - Then 'I reset my token' do - @old_token = @user.private_token - click_button "Reset" + step 'I reset my token' do + within '.update-token' do + @old_token = @user.private_token + click_button "Reset" + end end - And 'I should see new token' do + step 'I should see new token' do find("#token").value.should_not == @old_token find("#token").value.should == @user.reload.private_token end - Given 'I have activity' do + step 'I have activity' do create(:closed_issue_event, author: current_user) end - Then 'I should see my activity' do + step 'I should see my activity' do page.should have_content "#{current_user.name} closed issue" end - When "I change my application theme" do - choose "Violet" + step "I change my application theme" do + within '.application-theme' do + choose "Violet" + end end - When "I change my code preview theme" do - choose "Dark code preview" + step "I change my code preview theme" do + within '.code-preview-theme' do + choose "Solarized dark" + end end - Then "I should see the theme change immediately" do + step "I should see the theme change immediately" do page.should have_selector('body.ui_color') page.should_not have_selector('body.ui_basic') end - Then "I should receive feedback that the changes were saved" do - page.should have_content("Saved") + step "I should receive feedback that the changes were saved" do + page.should have_content("saved") + end + + step 'my password is expired' do + current_user.update_attributes(password_expires_at: Time.now - 1.hour) + end + + step "I am not an ldap user" do + current_user.update_attributes(extern_uid: nil, provider: '') + current_user.ldap_user?.should be_false + end + + step 'I redirected to expired password page' do + current_path.should == new_profile_password_path + end + + step 'I submit new password' do + fill_in :user_password, with: '12345678' + fill_in :user_password_confirmation, with: '12345678' + click_button "Set new password" + end + + step 'I redirected to sign in page' do + current_path.should == new_user_session_path + end + + step 'I should be redirected to account page' do + current_path.should == account_profile_path + end + + step 'I click on my profile picture' do + click_link 'profile-pic' + end + + step 'I should see my user page' do + page.should have_content "User Activity" + + within '.navbar-gitlab' do + page.should have_content current_user.name + end end end diff --git a/features/steps/profile/profile_notifications.rb b/features/steps/profile/profile_notifications.rb new file mode 100644 index 00000000000..7a41687dfde --- /dev/null +++ b/features/steps/profile/profile_notifications.rb @@ -0,0 +1,13 @@ +class ProfileNotifications < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + + step 'I visit profile notifications page' do + visit profile_notifications_path + end + + step 'I should see global notifications settings' do + page.should have_content "Notifications settings" + page.should have_content "Global setting" + end +end diff --git a/features/steps/profile/profile_ssh_keys.rb b/features/steps/profile/profile_ssh_keys.rb index 8ae1fa91025..65bfc505d85 100644 --- a/features/steps/profile/profile_ssh_keys.rb +++ b/features/steps/profile/profile_ssh_keys.rb @@ -8,20 +8,20 @@ class ProfileSshKeys < Spinach::FeatureSteps end Given 'I click link "Add new"' do - click_link "Add new" + click_link "Add SSH Key" end And 'I submit new ssh key "Laptop"' do - fill_in "key_title", :with => "Laptop" - fill_in "key_key", :with => "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop" - click_button "Save" + fill_in "key_title", with: "Laptop" + fill_in "key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop" + click_button "Add key" end Then 'I should see new ssh key "Laptop"' do key = Key.find_by_title("Laptop") page.should have_content(key.title) page.should have_content(key.key) - current_path.should == key_path(key) + current_path.should == profile_key_path(key) end Given 'I click link "Work"' do @@ -33,7 +33,7 @@ class ProfileSshKeys < Spinach::FeatureSteps end Then 'I visit profile keys page' do - visit keys_path + visit profile_keys_path end And 'I should not see "Work" ssh key' do @@ -43,6 +43,6 @@ class ProfileSshKeys < Spinach::FeatureSteps end And 'I have ssh key "ssh-rsa Work"' do - create(:key, :user => @user, :title => "ssh-rsa Work", :key => "jfKLJDFKSFJSHFJssh-rsa Work") + create(:key, user: @user, title: "ssh-rsa Work", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+L3TbFegm3k8QjejSwemk4HhlRh+DuN679Pc5ckqE/MPhVtE/+kZQDYCTB284GiT2aIoGzmZ8ee9TkaoejAsBwlA+Wz2Q3vhz65X6sMgalRwpdJx8kSEUYV8ZPV3MZvPo8KdNg993o4jL6G36GDW4BPIyO6FPZhfsawdf6liVD0Xo5kibIK7B9VoE178cdLQtLpS2YolRwf5yy6XR6hbbBGQR+6xrGOdP16eGZDb1CE2bMvvJijjloFqPscGktWOqW+nfh5txwFfBzlfARDTBsS8WZtg3Yoj1kn33kPsWRlgHfNutFRAIynDuDdQzQq8tTtVwm+Yi75RfcPHW8y3P Work") end end diff --git a/features/steps/project/create_project.rb b/features/steps/project/create_project.rb index 0d9727732c7..b59345e7078 100644 --- a/features/steps/project/create_project.rb +++ b/features/steps/project/create_project.rb @@ -17,4 +17,26 @@ class CreateProject < Spinach::FeatureSteps page.should have_content "git remote" page.should have_content Project.last.url_to_repo end + + Then 'I see empty project instuctions' do + page.should have_content "git init" + page.should have_content "git remote" + page.should have_content Project.last.url_to_repo + end + + And 'I click on HTTP' do + click_button 'HTTP' + end + + Then 'Remote url should update to http link' do + page.should have_content "git remote add origin #{Project.last.http_url_to_repo}" + end + + And 'If I click on SSH' do + click_button 'SSH' + end + + Then 'Remote url should update to ssh link' do + page.should have_content "git remote add origin #{Project.last.url_to_repo}" + end end diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb new file mode 100644 index 00000000000..7f7492bfd6d --- /dev/null +++ b/features/steps/project/deploy_keys.rb @@ -0,0 +1,53 @@ +class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + + step 'project has deploy key' do + create(:deploy_keys_project, project: @project) + end + + step 'I should see project deploy keys' do + within '.enabled-keys' do + page.should have_content deploy_key.title + end + end + + step 'I click \'New Deploy Key\'' do + click_link 'New Deploy Key' + end + + step 'I submit new deploy key' do + fill_in "deploy_key_title", with: "laptop" + fill_in "deploy_key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop" + click_button "Create" + end + + step 'I should be on deploy keys page' do + current_path.should == project_deploy_keys_path(@project) + end + + step 'I should see newly created deploy key' do + within '.enabled-keys' do + page.should have_content(deploy_key.title) + end + end + + step 'other project has deploy key' do + @second_project = create :project, namespace: current_user.namespace + @second_project.team << [current_user, :master] + create(:deploy_keys_project, project: @second_project) + end + + step 'I click attach deploy key' do + within '.available-keys' do + click_link 'Enable' + end + end + + protected + + def deploy_key + @project.deploy_keys.last + end +end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index e9ef1495dd1..a96b086fae5 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -9,7 +9,7 @@ class ProjectFeature < Spinach::FeatureSteps end And 'I save project' do - click_button 'Save' + click_button 'Save changes' end Then 'I should see project with new settings' do diff --git a/features/steps/project/project_active_tab.rb b/features/steps/project/project_active_tab.rb index bce67c82962..e04a17168be 100644 --- a/features/steps/project/project_active_tab.rb +++ b/features/steps/project/project_active_tab.rb @@ -10,6 +10,10 @@ class ProjectActiveTab < Spinach::FeatureSteps ensure_active_main_tab('Home') end + Then 'the active main tab should be Settings' do + ensure_active_main_tab('Settings') + end + Then 'the active main tab should be Files' do ensure_active_main_tab('Files') end @@ -41,7 +45,7 @@ class ProjectActiveTab < Spinach::FeatureSteps # Sub Tabs: Home Given 'I click the "Team" tab' do - click_link('Team') + click_link('Members') end Given 'I click the "Attachments" tab' do @@ -57,39 +61,27 @@ class ProjectActiveTab < Spinach::FeatureSteps end Given 'I click the "Hooks" tab' do - click_link('Hooks') + click_link('Web Hooks') end Given 'I click the "Deploy Keys" tab' do click_link('Deploy Keys') end - Then 'the active sub tab should be Show' do - ensure_active_sub_tab('Show') - end - - Then 'the active sub tab should be Team' do - ensure_active_sub_tab('Team') - end - - Then 'the active sub tab should be Attachments' do - ensure_active_sub_tab('Attachments') - end - - Then 'the active sub tab should be Snippets' do - ensure_active_sub_tab('Snippets') + Then 'the active sub nav should be Team' do + ensure_active_sub_nav('Members') end - Then 'the active sub tab should be Edit' do - ensure_active_sub_tab('Edit') + Then 'the active sub nav should be Edit' do + ensure_active_sub_nav('Edit Project') end - Then 'the active sub tab should be Hooks' do - ensure_active_sub_tab('Hooks') + Then 'the active sub nav should be Hooks' do + ensure_active_sub_nav('Web Hooks') end - Then 'the active sub tab should be Deploy Keys' do - ensure_active_sub_tab('Deploy Keys') + Then 'the active sub nav should be Deploy Keys' do + ensure_active_sub_nav('Deploy Keys') end # Sub Tabs: Commits diff --git a/features/steps/project/project_browse_branches.rb b/features/steps/project/project_browse_branches.rb index 2f6e185deea..e77825411f3 100644 --- a/features/steps/project/project_browse_branches.rb +++ b/features/steps/project/project_browse_branches.rb @@ -22,7 +22,7 @@ class ProjectBrowseBranches < Spinach::FeatureSteps end Then 'I should see "Shop" protected branches list' do - within "table" do + within ".protected-branches-list" do page.should have_content "stable" page.should_not have_content "master" end @@ -30,6 +30,6 @@ class ProjectBrowseBranches < Spinach::FeatureSteps And 'project "Shop" has protected branches' do project = Project.find_by_name("Shop") - project.protected_branches.create(:name => "stable") + project.protected_branches.create(name: "stable") end end diff --git a/features/steps/project/project_browse_commits.rb b/features/steps/project/project_browse_commits.rb index 3433c2ba5f6..650bc3a16f7 100644 --- a/features/steps/project/project_browse_commits.rb +++ b/features/steps/project/project_browse_commits.rb @@ -15,11 +15,11 @@ class ProjectBrowseCommits < Spinach::FeatureSteps end Then 'I see commits atom feed' do - commit = CommitDecorator.decorate(@project.repository.commit) + commit = @project.repository.commit page.response_headers['Content-Type'].should have_content("application/atom+xml") - page.body.should have_selector("title", :text => "Recent commits to #{@project.name}") - page.body.should have_selector("author email", :text => commit.author_email) - page.body.should have_selector("entry summary", :text => commit.description) + page.body.should have_selector("title", text: "Recent commits to #{@project.name}") + page.body.should have_selector("author email", text: commit.author_email) + page.body.should have_selector("entry summary", text: commit.description) end Given 'I click on commit link' do @@ -48,14 +48,44 @@ class ProjectBrowseCommits < Spinach::FeatureSteps page.should have_selector('ul.breadcrumb span.divider', count: 3) page.should have_selector('ul.breadcrumb a', count: 4) - find('ul.breadcrumb li:first a')['href'].should match(/#{@project.path_with_namespace}\/commits\/master\z/) + find('ul.breadcrumb li:nth-child(2) a')['href'].should match(/#{@project.path_with_namespace}\/commits\/master\z/) find('ul.breadcrumb li:last a')['href'].should match(%r{master/app/models/project\.rb\z}) end Then 'I see commits stats' do - page.should have_content 'Stats' + page.should have_content 'Top 50 Committers' page.should have_content 'Committers' page.should have_content 'Total commits' page.should have_content 'Authors' end + + Given 'I visit big commit page' do + visit project_commit_path(@project, BigCommits::BIG_COMMIT_ID) + end + + Then 'I see big commit warning' do + page.should have_content BigCommits::BIG_COMMIT_MESSAGE + page.should have_content "Warning! This is a large diff" + page.should have_content "If you still want to see the diff" + end + + Given 'I visit huge commit page' do + visit project_commit_path(@project, BigCommits::HUGE_COMMIT_ID) + end + + Then 'I see huge commit message' do + page.should have_content BigCommits::HUGE_COMMIT_MESSAGE + page.should have_content "Warning! This is a large diff" + page.should_not have_content "If you still want to see the diff" + end + + Given 'I visit a commit with an image that changed' do + visit project_commit_path(@project, 'cc1ba255d6c5ffdce87a357ba7ccc397a4f4026b') + end + + Then 'The diff links to both the previous and current image' do + links = page.all('.two-up span div a') + links[0]['href'].should =~ %r{blob/bc3735004cb45cec5e0e4fa92710897a910a5957} + links[1]['href'].should =~ %r{blob/cc1ba255d6c5ffdce87a357ba7ccc397a4f4026b} + end end diff --git a/features/steps/project/project_browse_files.rb b/features/steps/project/project_browse_files.rb index 4efce0dcffc..71360fb6bd5 100644 --- a/features/steps/project/project_browse_files.rb +++ b/features/steps/project/project_browse_files.rb @@ -16,12 +16,12 @@ class ProjectBrowseFiles < Spinach::FeatureSteps page.should have_content "Gemfile" end - Given 'I click on "Gemfile" file in repo' do - click_link "Gemfile" + Given 'I click on "Gemfile.lock" file in repo' do + click_link "Gemfile.lock" end Then 'I should see it content' do - page.should have_content "rubygems.org" + page.should have_content "DEPENDENCIES" end And 'I click link "raw"' do diff --git a/features/steps/project/project_browse_git_repo.rb b/features/steps/project/project_browse_git_repo.rb index 19edfb076eb..cd9a60f49cb 100644 --- a/features/steps/project/project_browse_git_repo.rb +++ b/features/steps/project/project_browse_git_repo.rb @@ -3,8 +3,8 @@ class ProjectBrowseGitRepo < Spinach::FeatureSteps include SharedProject include SharedPaths - Given 'I click on "Gemfile" file in repo' do - click_link "Gemfile" + Given 'I click on "Gemfile.lock" file in repo' do + click_link "Gemfile.lock" end And 'I click blame button' do @@ -12,7 +12,7 @@ class ProjectBrowseGitRepo < Spinach::FeatureSteps end Then 'I should see git file blame' do - page.should have_content "rubygems.org" + page.should have_content "DEPENDENCIES" page.should have_content "Dmitriy Zaporozhets" page.should have_content "Moving to rails 3.2" end diff --git a/features/steps/project/project_fork.rb b/features/steps/project/project_fork.rb new file mode 100644 index 00000000000..858c7d11b32 --- /dev/null +++ b/features/steps/project/project_fork.rb @@ -0,0 +1,36 @@ +class ForkProject < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedProject + + step 'I click link "Fork"' do + page.should have_content "Shop" + page.should have_content "Fork" + Gitlab::Shell.any_instance.stub(:fork_repository).and_return(true) + click_link "Fork" + end + + step 'I am a member of project "Shop"' do + @project = Project.find_by_name "Shop" + @project ||= create(:project_with_code, name: "Shop", group: create(:group)) + @project.team << [@user, :reporter] + end + + step 'I should see the forked project page' do + page.should have_content "Project was successfully forked." + current_path.should include current_user.namespace.path + @forked_project = Project.find_by_namespace_id(current_user.namespace.path) + end + + step 'I already have a project named "Shop" in my namespace' do + current_user.namespace ||= create(:namespace) + current_user.namespace.should_not be_nil + current_user.namespace.path.should_not be_nil + @my_project = create(:project_with_code, name: "Shop", namespace: current_user.namespace) + end + + step 'I should see a "Name has already been taken" warning' do + page.should have_content "Name has already been taken" + end + +end diff --git a/features/steps/project/project_forked_merge_requests.rb b/features/steps/project/project_forked_merge_requests.rb new file mode 100644 index 00000000000..f7bf085a423 --- /dev/null +++ b/features/steps/project/project_forked_merge_requests.rb @@ -0,0 +1,183 @@ +class ProjectForkedMergeRequests < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedNote + include SharedPaths + include ChosenHelper + + step 'I am a member of project "Shop"' do + @project = Project.find_by_name "Shop" + @project ||= create(:project_with_code, name: "Shop") + @project.team << [@user, :reporter] + end + + step 'I have a project forked off of "Shop" called "Forked Shop"' do + @forking_user = @user + forked_project_link = build(:forked_project_link) + @forked_project = Project.find_by_name "Forked Shop" + @forked_project ||= create(:source_project_with_code, name: "Forked Shop", forked_project_link: forked_project_link, creator_id: @forking_user.id , namespace: @forking_user.namespace) + + forked_project_link.forked_from_project = @project + forked_project_link.forked_to_project = @forked_project + @forked_project.team << [@forking_user , :master] + forked_project_link.save! + end + + step 'I click link "New Merge Request"' do + click_link "New Merge Request" + end + + step 'I should see merge request "Merge Request On Forked Project"' do + @project.merge_requests.size.should >= 1 + @merge_request = @project.merge_requests.last + current_path.should == project_merge_request_path(@project, @merge_request) + @merge_request.title.should == "Merge Request On Forked Project" + @merge_request.source_project.should == @forked_project + @merge_request.source_branch.should == "master" + @merge_request.target_branch.should == "stable" + page.should have_content @forked_project.path_with_namespace + page.should have_content @project.path_with_namespace + page.should have_content @merge_request.source_branch + page.should have_content @merge_request.target_branch + end + + step 'I fill out a "Merge Request On Forked Project" merge request' do + chosen @forked_project.id, from: "#merge_request_source_project_id" + chosen @project.id, from: "#merge_request_target_project_id" + + find(:select, "merge_request_source_project_id", {}).value.should == @forked_project.id.to_s + find(:select, "merge_request_target_project_id", {}).value.should == @project.id.to_s + + chosen "master", from: "#merge_request_source_branch" + chosen "stable", from: "#merge_request_target_branch" + + find(:select, "merge_request_source_branch", {}).value.should == 'master' + find(:select, "merge_request_target_branch", {}).value.should == 'stable' + + fill_in "merge_request_title", with: "Merge Request On Forked Project" + end + + step 'I submit the merge request' do + click_button "Submit merge request" + end + + step 'I follow the target commit link' do + commit = @project.repository.commit + click_link commit.short_id(8) + end + + step 'I should see the commit under the forked from project' do + commit = @project.repository.commit + page.should have_content(commit.message) + end + + step 'I click "Create Merge Request on fork" link' do + click_link "Create Merge Request on fork" + end + + step 'I see prefilled new Merge Request page for the forked project' do + current_path.should == new_project_merge_request_path(@forked_project) + find("#merge_request_source_project_id").value.should == @forked_project.id.to_s + find("#merge_request_target_project_id").value.should == @project.id.to_s + find("#merge_request_source_branch").value.should have_content "new_design" + find("#merge_request_target_branch").value.should have_content "master" + find("#merge_request_title").value.should == "New Design" + verify_commit_link(".mr_target_commit",@project) + verify_commit_link(".mr_source_commit",@forked_project) + end + + step 'I update the merge request title' do + fill_in "merge_request_title", with: "An Edited Forked Merge Request" + end + + step 'I save the merge request' do + click_button "Save changes" + end + + step 'I should see the edited merge request' do + page.should have_content "An Edited Forked Merge Request" + @project.merge_requests.size.should >= 1 + @merge_request = @project.merge_requests.last + current_path.should == project_merge_request_path(@project, @merge_request) + @merge_request.source_project.should == @forked_project + @merge_request.source_branch.should == "master" + @merge_request.target_branch.should == "stable" + page.should have_content @forked_project.path_with_namespace + page.should have_content @project.path_with_namespace + page.should have_content @merge_request.source_branch + page.should have_content @merge_request.target_branch + end + + step 'I should see last push widget' do + page.should have_content "You pushed to new_design" + page.should have_link "Create Merge Request" + end + + step 'project "Forked Shop" has push event' do + @forked_project = Project.find_by_name("Forked Shop") + + data = { + before: "0000000000000000000000000000000000000000", + after: "0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e", + ref: "refs/heads/new_design", + user_id: @user.id, + user_name: @user.name, + repository: { + name: @forked_project.name, + url: "localhost/rubinius", + description: "", + homepage: "localhost/rubinius", + private: true + } + } + + @event = Event.create( + project: @forked_project, + action: Event::PUSHED, + data: data, + author_id: @user.id + ) + end + + + step 'I click link edit "Merge Request On Forked Project"' do + find("#edit_merge_request").click + end + + step 'I see the edit page prefilled for "Merge Request On Forked Project"' do + current_path.should == edit_project_merge_request_path(@project, @merge_request) + page.should have_content "Edit merge request ##{@merge_request.id}" + find("#merge_request_title").value.should == "Merge Request On Forked Project" + find("#merge_request_source_project_id").value.should == @forked_project.id.to_s + find("#merge_request_target_project_id").value.should == @project.id.to_s + find("#merge_request_source_branch").value.should have_content "master" + verify_commit_link(".mr_source_commit",@forked_project) + find("#merge_request_target_branch").value.should have_content "stable" + verify_commit_link(".mr_target_commit",@project) + end + + step 'I fill out an invalid "Merge Request On Forked Project" merge request' do + #If this isn't filled in the rest of the validations won't be triggered + fill_in "merge_request_title", with: "Merge Request On Forked Project" + find(:select, "merge_request_source_project_id", {}).value.should == @forked_project.id.to_s + find(:select, "merge_request_target_project_id", {}).value.should == @forked_project.id.to_s + find(:select, "merge_request_source_branch", {}).value.should == "" + find(:select, "merge_request_target_branch", {}).value.should == "" + end + + step 'I should see validation errors' do + page.should have_content "Source branch can't be blank" + page.should have_content "Target branch can't be blank" + page.should have_content "Branch conflict You can not use same project/branch for source and target" + end + + def project + @project ||= Project.find_by_name!("Shop") + end + + # Verify a link is generated against the correct project + def verify_commit_link(container_div, container_project) + # This should force a wait for the javascript to execute + find(:div,container_div).find(".commit_short_id")['href'].should have_content "#{container_project.path_with_namespace}/commit" + end +end diff --git a/features/steps/project/project_graph.rb b/features/steps/project/project_graph.rb new file mode 100644 index 00000000000..50942b3cbb3 --- /dev/null +++ b/features/steps/project/project_graph.rb @@ -0,0 +1,13 @@ +class ProjectGraph < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + + Then 'page should have graphs' do + page.should have_selector ".stat-graph" + end + + When 'I visit project "Shop" graph page' do + project = Project.find_by_name("Shop") + visit project_graph_path(project, "master") + end +end diff --git a/features/steps/project/project_issues.rb b/features/steps/project/project_issues.rb index 2103aeb1715..801fff78a52 100644 --- a/features/steps/project/project_issues.rb +++ b/features/steps/project/project_issues.rb @@ -12,6 +12,10 @@ class ProjectIssues < Spinach::FeatureSteps page.should_not have_content "Release 0.3" end + And 'I should not see "Tweet control" in issues' do + page.should_not have_content "Tweet control" + end + Given 'I click link "Closed"' do click_link "Closed" end @@ -41,7 +45,7 @@ class ProjectIssues < Spinach::FeatureSteps end And 'I submit new issue "500 error on profile"' do - fill_in "issue_title", :with => "500 error on profile" + fill_in "issue_title", with: "500 error on profile" click_button "Submit new issue" end @@ -56,16 +60,16 @@ class ProjectIssues < Spinach::FeatureSteps page.should have_content issue.project.name end - Given 'I fill in issue search with "Release"' do - fill_in 'issue_search', with: "Release" + Given 'I fill in issue search with "Re"' do + fill_in 'issue_search', with: "Re" end - Given 'I fill in issue search with "Bug"' do - fill_in 'issue_search', with: "Bug" + Given 'I fill in issue search with "Bu"' do + fill_in 'issue_search', with: "Bu" end - And 'I fill in issue search with "0.3"' do - fill_in 'issue_search', with: "0.3" + And 'I fill in issue search with ".3"' do + fill_in 'issue_search', with: ".3" end And 'I fill in issue search with "Something"' do @@ -78,16 +82,16 @@ class ProjectIssues < Spinach::FeatureSteps Given 'project "Shop" has milestone "v2.2"' do project = Project.find_by_name("Shop") - milestone = create(:milestone, :title => "v2.2", :project => project) + milestone = create(:milestone, title: "v2.2", project: project) - 3.times { create(:issue, :project => project, :milestone => milestone) } + 3.times { create(:issue, project: project, milestone: milestone) } end And 'project "Shop" has milestone "v3.0"' do project = Project.find_by_name("Shop") - milestone = create(:milestone, :title => "v3.0", :project => project) + milestone = create(:milestone, title: "v3.0", project: project) - 3.times { create(:issue, :project => project, :milestone => milestone) } + 3.times { create(:issue, project: project, milestone: milestone) } end When 'I select milestone "v3.0"' do @@ -115,17 +119,24 @@ class ProjectIssues < Spinach::FeatureSteps And 'project "Shop" have "Release 0.4" open issue' do project = Project.find_by_name("Shop") create(:issue, - :title => "Release 0.4", - :project => project, - :author => project.users.first) + title: "Release 0.4", + project: project, + author: project.users.first) end - And 'project "Shop" have "Release 0.3" closed issue' do + And 'project "Shop" have "Tweet control" open issue' do project = Project.find_by_name("Shop") create(:issue, - :title => "Release 0.3", - :project => project, - :author => project.users.first, - :closed => true) + title: "Tweet control", + project: project, + author: project.users.first) + end + + And 'project "Shop" have "Release 0.3" closed issue' do + project = Project.find_by_name("Shop") + create(:closed_issue, + title: "Release 0.3", + project: project, + author: project.users.first) end end diff --git a/features/steps/project/project_merge_requests.rb b/features/steps/project/project_merge_requests.rb index 329261add2a..7c70482deb5 100644 --- a/features/steps/project/project_merge_requests.rb +++ b/features/steps/project/project_merge_requests.rb @@ -25,8 +25,8 @@ class ProjectMergeRequests < Spinach::FeatureSteps end Then 'I should see closed merge request "Bug NS-04"' do - mr = MergeRequest.find_by_title("Bug NS-04") - mr.closed.should be_true + merge_request = MergeRequest.find_by_title!("Bug NS-04") + merge_request.closed?.should be_true page.should have_content "Closed by" end @@ -56,54 +56,59 @@ class ProjectMergeRequests < Spinach::FeatureSteps end And 'I submit new merge request "Wiki Feature"' do + #this must come first, so that the target branch is set by the time the "select" for "notes_refactoring" is executed + select project.path_with_namespace, :from => "merge_request_target_project_id" fill_in "merge_request_title", :with => "Wiki Feature" select "master", :from => "merge_request_source_branch" - select "stable", :from => "merge_request_target_branch" + find(:select, "merge_request_target_project_id", {}).value.should == project.id.to_s + find(:select, "merge_request_source_project_id", {}).value.should == project.id.to_s + + #using "notes_refactoring" because "Bug NS-04" uses master/stable, this will fail merge_request validation if the branches are the same + find(:select, "merge_request_target_branch", {}).find(:option, "notes_refactoring", {}).value.should == "notes_refactoring" + select "notes_refactoring", :from => "merge_request_target_branch" + click_button "Submit merge request" end And 'project "Shop" have "Bug NS-04" open merge request' do - project = Project.find_by_name("Shop") create(:merge_request, title: "Bug NS-04", - project: project, + source_project: project, + target_project: project, author: project.users.first) end And 'project "Shop" have "Bug NS-05" open merge request with diffs inside' do - project = Project.find_by_name("Shop") create(:merge_request_with_diffs, title: "Bug NS-05", - project: project, + source_project: project, + target_project: project, author: project.users.first) end And 'project "Shop" have "Feature NS-03" closed merge request' do - project = Project.find_by_name("Shop") - create(:merge_request, + create(:closed_merge_request, title: "Feature NS-03", - project: project, - author: project.users.first, - closed: true) + source_project: project, + target_project: project, + author: project.users.first) end And 'I switch to the diff tab' do - mr = MergeRequest.find_by_title("Bug NS-05") - visit diffs_project_merge_request_path(mr.project, mr) + visit diffs_project_merge_request_path(project, merge_request) end And 'I switch to the merge request\'s comments tab' do - mr = MergeRequest.find_by_title("Bug NS-05") - visit project_merge_request_path(mr.project, mr) + visit project_merge_request_path(project, merge_request) end And 'I click on the first commit in the merge request' do - mr = MergeRequest.find_by_title("Bug NS-05") - click_link mr.commits.first.short_id(8) + + click_link merge_request.commits.first.short_id(8) end And 'I leave a comment on the diff page' do - find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185 .add-diff-note").click + init_diff_note within('.js-temp-notes-holder') do fill_in "note_note", with: "One comment to rule them all" @@ -112,7 +117,7 @@ class ProjectMergeRequests < Spinach::FeatureSteps end And 'I leave a comment like "Line is wrong" on line 185 of the first file' do - find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185 .add-diff-note").click + init_diff_note within(".js-temp-notes-holder") do fill_in "note_note", with: "Line is wrong" @@ -122,31 +127,32 @@ class ProjectMergeRequests < Spinach::FeatureSteps end Then 'I should see a discussion has started on line 185' do - mr = MergeRequest.find_by_title("Bug NS-05") - first_commit = mr.commits.first - first_diff = first_commit.diffs.first page.should have_content "#{current_user.name} started a discussion on this merge request diff" - page.should have_content "#{first_diff.b_path}:L185" + page.should have_content "app/assets/stylesheets/tree.scss:L185" page.should have_content "Line is wrong" end Then 'I should see a discussion has started on commit bcf03b5de6c:L185' do - mr = MergeRequest.find_by_title("Bug NS-05") - first_commit = mr.commits.first - first_diff = first_commit.diffs.first page.should have_content "#{current_user.name} started a discussion on commit" - page.should have_content first_commit.short_id(8) - page.should have_content "#{first_diff.b_path}:L185" + page.should have_content "app/assets/stylesheets/tree.scss:L185" page.should have_content "Line is wrong" end Then 'I should see a discussion has started on commit bcf03b5de6c' do - mr = MergeRequest.find_by_title("Bug NS-05") - first_commit = mr.st_commits.first - first_diff = first_commit.diffs.first page.should have_content "#{current_user.name} started a discussion on commit bcf03b5de6c" - page.should have_content first_commit.short_id(8) page.should have_content "One comment to rule them all" - page.should have_content "#{first_diff.b_path}:L185" + page.should have_content "app/assets/stylesheets/tree.scss:L185" + end + + def project + @project ||= Project.find_by_name!("Shop") + end + + def merge_request + @merge_request ||= MergeRequest.find_by_title!("Bug NS-05") + end + + def init_diff_note + find('a[data-line-code="4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185"]').click end end diff --git a/features/steps/project/project_milestones.rb b/features/steps/project/project_milestones.rb index 1350938ee9a..c4d0d176f3a 100644 --- a/features/steps/project/project_milestones.rb +++ b/features/steps/project/project_milestones.rb @@ -19,7 +19,7 @@ class ProjectMilestones < Spinach::FeatureSteps end And 'I submit new milestone "v2.3"' do - fill_in "milestone_title", :with => "v2.3" + fill_in "milestone_title", with: "v2.3" click_button "Create milestone" end @@ -32,9 +32,9 @@ class ProjectMilestones < Spinach::FeatureSteps And 'project "Shop" has milestone "v2.2"' do project = Project.find_by_name("Shop") - milestone = create(:milestone, :title => "v2.2", :project => project) + milestone = create(:milestone, title: "v2.2", project: project) - 3.times { create(:issue, :project => project, :milestone => milestone) } + 3.times { create(:issue, project: project, milestone: milestone) } end Given 'the milestone has open and closed issues' do @@ -50,12 +50,6 @@ class ProjectMilestones < Spinach::FeatureSteps end Then "I should see 3 issues" do - page.should have_selector('.milestone-issue-filter .well-list li', count: 4) - page.should have_selector('.milestone-issue-filter .well-list li.hide', count: 1) - end - - Then "I should see 4 issues" do - page.should have_selector('.milestone-issue-filter .well-list li', count: 4) - page.should_not have_selector('.milestone-issue-filter .well-list li.hide') + page.should have_selector('#tab-issues li', count: 4) end end diff --git a/features/steps/project/project_network_graph.rb b/features/steps/project/project_network_graph.rb index f26deff9367..127adecf7ed 100644 --- a/features/steps/project/project_network_graph.rb +++ b/features/steps/project/project_network_graph.rb @@ -3,17 +3,90 @@ class ProjectNetworkGraph < Spinach::FeatureSteps include SharedProject Then 'page should have network graph' do - page.should have_content "Project Network Graph" - within ".graph" do - page.should have_content "master" - end + page.should have_selector ".network-graph" end - And 'I visit project "Shop" network page' do - # Stub Graph::JsonBuilder max_size to speed up test (10 commits vs. 650) - Gitlab::Graph::JsonBuilder.stub(max_count: 10) + When 'I visit project "Shop" network page' do + # Stub Graph max_size to speed up test (10 commits vs. 650) + Network::Graph.stub(max_count: 10) project = Project.find_by_name("Shop") - visit project_graph_path(project, "master") + visit project_network_path(project, "master") + end + + And 'page should select "master" in select box' do + page.should have_selector '.chosen-single span', text: "master" + end + + And 'page should select "v2.1.0" in select box' do + page.should have_selector '.chosen-single span', text: "v2.1.0" + end + + And 'page should have "master" on graph' do + within '.network-graph' do + page.should have_content 'master' + end + end + + When 'I switch ref to "stable"' do + page.select 'stable', from: 'ref' + sleep 2 + end + + When 'I switch ref to "v2.1.0"' do + page.select 'v2.1.0', from: 'ref' + sleep 2 + end + + When 'click "Show only selected branch" checkbox' do + find('#filter_ref').click + sleep 2 + end + + Then 'page should have content not cotaining "v2.1.0"' do + within '.network-graph' do + page.should have_content 'cleaning' + end + end + + Then 'page should not have content not cotaining "v2.1.0"' do + within '.network-graph' do + page.should_not have_content 'cleaning' + end + end + + And 'page should select "stable" in select box' do + page.should have_selector '.chosen-single span', text: "stable" + end + + And 'page should select "v2.1.0" in select box' do + page.should have_selector '.chosen-single span', text: "v2.1.0" + end + + And 'page should have "stable" on graph' do + within '.network-graph' do + page.should have_content 'stable' + end + end + + When 'I looking for a commit by SHA of "v2.1.0"' do + within ".content .search" do + fill_in 'extended_sha1', with: '98d6492' + find('button').click + end + sleep 2 + end + + And 'page should have "v2.1.0" on graph' do + within '.network-graph' do + page.should have_content 'v2.1.0' + end + end + + When 'I look for a commit by ";"' do + within ".content .search" do + fill_in 'extended_sha1', with: ';' + find('button').click + end end end diff --git a/features/steps/project/project_search_code.rb b/features/steps/project/project_search_code.rb new file mode 100644 index 00000000000..d117b019a15 --- /dev/null +++ b/features/steps/project/project_search_code.rb @@ -0,0 +1,17 @@ +class ProjectSearchCode < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + + When 'I search for term "Welcome to GitLab"' do + fill_in "search", with: "Welcome to GitLab" + click_button "Go" + click_link 'Repository Code' + end + + Then 'I should see files from repository containing "Welcome to GitLab"' do + page.should have_content "Welcome to GitLab" + page.should have_content "GitLab is a free project and repository management application" + end + +end diff --git a/features/steps/project/project_services.rb b/features/steps/project/project_services.rb index b1668ff7207..a24100ff8c0 100644 --- a/features/steps/project/project_services.rb +++ b/features/steps/project/project_services.rb @@ -9,7 +9,8 @@ class ProjectServices < Spinach::FeatureSteps Then 'I should see list of available services' do page.should have_content 'Services' - page.should have_content 'Jenkins' + page.should have_content 'Campfire' + page.should have_content 'Hipchat' page.should have_content 'GitLab CI' end @@ -19,12 +20,42 @@ class ProjectServices < Spinach::FeatureSteps And 'I fill gitlab-ci settings' do check 'Active' - fill_in 'Project URL', with: 'http://ci.gitlab.org/projects/3' - fill_in 'CI Project token', with: 'verySecret' + fill_in 'Project url', with: 'http://ci.gitlab.org/projects/3' + fill_in 'Token', with: 'verySecret' click_button 'Save' end Then 'I should see service settings saved' do - find_field('Project URL').value.should == 'http://ci.gitlab.org/projects/3' + find_field('Project url').value.should == 'http://ci.gitlab.org/projects/3' + end + + And 'I click hipchat service link' do + click_link 'Hipchat' + end + + And 'I fill hipchat settings' do + check 'Active' + fill_in 'Room', with: 'gitlab' + fill_in 'Token', with: 'verySecret' + click_button 'Save' + end + + Then 'I should see hipchat service settings saved' do + find_field('Room').value.should == 'gitlab' + end + + + And 'I click pivotaltracker service link' do + click_link 'PivotalTracker' + end + + And 'I fill pivotaltracker settings' do + check 'Active' + fill_in 'Token', with: 'verySecret' + click_button 'Save' + end + + Then 'I should see pivotaltracker service settings saved' do + find_field('Token').value.should == 'verySecret' end end diff --git a/features/steps/project/project_snippets.rb b/features/steps/project/project_snippets.rb new file mode 100644 index 00000000000..5082b31198a --- /dev/null +++ b/features/steps/project/project_snippets.rb @@ -0,0 +1,100 @@ +class ProjectSnippets < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedNote + include SharedPaths + + And 'project "Shop" have "Snippet one" snippet' do + create(:project_snippet, + title: "Snippet one", + content: "Test content", + file_name: "snippet.rb", + project: project, + author: project.users.first) + end + + And 'project "Shop" have no "Snippet two" snippet' do + create(:snippet, + title: "Snippet two", + content: "Test content", + file_name: "snippet.rb", + author: project.users.first) + end + + Given 'I click link "New Snippet"' do + click_link "Add new snippet" + end + + Given 'I click link "Snippet one"' do + click_link "Snippet one" + end + + Then 'I should see "Snippet one" in snippets' do + page.should have_content "Snippet one" + end + + And 'I should not see "Snippet two" in snippets' do + page.should_not have_content "Snippet two" + end + + And 'I should not see "Snippet one" in snippets' do + page.should_not have_content "Snippet one" + end + + And 'I click link "Edit"' do + within ".file-title" do + click_link "Edit" + end + end + + And 'I click link "Remove Snippet"' do + click_link "Remove snippet" + end + + And 'I submit new snippet "Snippet three"' do + fill_in "project_snippet_title", :with => "Snippet three" + select "forever", :from => "project_snippet_expires_at" + fill_in "project_snippet_file_name", :with => "my_snippet.rb" + within('.file-editor') do + find(:xpath, "//input[@id='project_snippet_content']").set 'Content of snippet three' + end + click_button "Create snippet" + end + + Then 'I should see snippet "Snippet three"' do + page.should have_content "Snippet three" + page.should have_content "Content of snippet three" + end + + And 'I submit new title "Snippet new title"' do + fill_in "project_snippet_title", :with => "Snippet new title" + click_button "Save" + end + + Then 'I should see "Snippet new title"' do + page.should have_content "Snippet new title" + end + + And 'I leave a comment like "Good snippet!"' do + within('.js-main-target-form') do + fill_in "note_note", with: "Good snippet!" + click_button "Add Comment" + end + end + + Then 'I should see comment "Good snippet!"' do + page.should have_content "Good snippet!" + end + + And 'I visit snippet page "Snippet one"' do + visit project_snippet_path(project, project_snippet) + end + + def project + @project ||= Project.find_by_name!("Shop") + end + + def project_snippet + @project_snippet ||= ProjectSnippet.find_by_title!("Snippet One") + end +end diff --git a/features/steps/project/project_team_management.rb b/features/steps/project/project_team_management.rb index 19352fe0ab8..efebba1be24 100644 --- a/features/steps/project/project_team_management.rb +++ b/features/steps/project/project_team_management.rb @@ -2,65 +2,56 @@ class ProjectTeamManagement < Spinach::FeatureSteps include SharedAuthentication include SharedProject include SharedPaths + include Select2Helper Then 'I should be able to see myself in team' do page.should have_content(@user.name) - page.should have_content(@user.email) + page.should have_content(@user.username) end And 'I should see "Sam" in team list' do user = User.find_by_name("Sam") page.should have_content(user.name) - page.should have_content(user.email) + page.should have_content(user.username) end Given 'I click link "New Team Member"' do - click_link "New Team Member" + click_link "New project member" end And 'I select "Mike" as "Reporter"' do user = User.find_by_name("Mike") + + select2(user.id, from: "#user_ids", multiple: true) within "#new_team_member" do - select user.name, :from => "user_ids" - select "Reporter", :from => "project_access" + select "Reporter", from: "project_access" end click_button "Add users" end Then 'I should see "Mike" in team list as "Reporter"' do - user = User.find_by_name("Mike") - role_id = find(".user_#{user.id} #team_member_project_access").value - role_id.should == UsersProject.access_roles["Reporter"].to_s + within ".access-reporter" do + page.should have_content('Mike') + end end Given 'I should see "Sam" in team list as "Developer"' do - user = User.find_by_name("Sam") - role_id = find(".user_#{user.id} #team_member_project_access").value - role_id.should == UsersProject.access_roles["Developer"].to_s + within ".access-developer" do + page.should have_content('Sam') + end end And 'I change "Sam" role to "Reporter"' do user = User.find_by_name("Sam") - within ".user_#{user.id}" do - select "Reporter", :from => "team_member_project_access" + within "#user_#{user.id}" do + select "Reporter", from: "team_member_project_access" end end And 'I should see "Sam" in team list as "Reporter"' do - user = User.find_by_name("Sam") - role_id = find(".user_#{user.id} #team_member_project_access").value - role_id.should == UsersProject.access_roles["Reporter"].to_s - end - - Given 'I click link "Sam"' do - click_link "Sam" - end - - Then 'I should see "Sam" team profile' do - user = User.find_by_name("Sam") - page.should have_content(user.name) - page.should have_content(user.email) - page.should have_content("To team list") + within ".access-reporter" do + page.should have_content('Sam') + end end And 'I click link "Remove from team"' do @@ -70,15 +61,15 @@ class ProjectTeamManagement < Spinach::FeatureSteps And 'I should not see "Sam" in team list' do user = User.find_by_name("Sam") page.should_not have_content(user.name) - page.should_not have_content(user.email) + page.should_not have_content(user.username) end And 'gitlab user "Mike"' do - create(:user, :name => "Mike") + create(:user, name: "Mike") end And 'gitlab user "Sam"' do - create(:user, :name => "Sam") + create(:user, name: "Sam") end And '"Sam" is "Shop" developer' do @@ -88,7 +79,7 @@ class ProjectTeamManagement < Spinach::FeatureSteps end Given 'I own project "Website"' do - @project = create(:project, :name => "Website") + @project = create(:project, name: "Website", namespace: @user.namespace) @project.team << [@user, :master] end @@ -99,11 +90,18 @@ class ProjectTeamManagement < Spinach::FeatureSteps end And 'I click link "Import team from another project"' do - click_link "Import team from another project" + click_link "Import members from another project" end When 'I submit "Website" project for import team' do - select 'Website', from: 'source_project_id' + project = Project.find_by_name("Website") + select project.name_with_namespace, from: 'source_project_id' click_button 'Import' end + + step 'I click cancel link for "Sam"' do + within "#user_#{User.find_by_name('Sam').id}" do + click_link('Remove user from team') + end + end end diff --git a/features/steps/project/project_wall.rb b/features/steps/project/project_wall.rb index ba9d3533b2c..7c61580eb2c 100644 --- a/features/steps/project/project_wall.rb +++ b/features/steps/project/project_wall.rb @@ -3,4 +3,16 @@ class ProjectWall < Spinach::FeatureSteps include SharedProject include SharedNote include SharedPaths + + + Given 'I write new comment "my special test message"' do + within(".wall-note-form") do + fill_in "note[note]", with: "my special test message" + click_button "Add Comment" + end + end + + Then 'I should see project wall note "my special test message"' do + page.should have_content "my special test message" + end end diff --git a/features/steps/project/project_wiki.rb b/features/steps/project/project_wiki.rb index 902e9ce158c..7aba412d751 100644 --- a/features/steps/project/project_wiki.rb +++ b/features/steps/project/project_wiki.rb @@ -4,17 +4,89 @@ class ProjectWiki < Spinach::FeatureSteps include SharedNote include SharedPaths - Given 'I create Wiki page' do - fill_in "Title", :with => 'Test title' - fill_in "Content", :with => '[link test](test)' - click_on "Save" + Given 'I click on the Cancel button' do + within(:css, ".form-actions") do + click_on "Cancel" + end end - Then 'I should see newly created wiki page' do - page.should have_content "Test title" + Then 'I should be redirected back to the Edit Home Wiki page' do + url = URI.parse(current_url) + url.path.should == project_wiki_path(project, :home) + end + + Given 'I create the Wiki Home page' do + fill_in "Content", with: '[link test](test)' + click_on "Create page" + end + + Then 'I should see the newly created wiki page' do + page.should have_content "Home" page.should have_content "link test" click_link "link test" page.should have_content "Editing page" end + + Given 'I have an existing Wiki page' do + wiki.create_page("existing", "content", :markdown, "first commit") + @page = wiki.find_page("existing") + end + + And 'I browse to that Wiki page' do + visit project_wiki_path(project, @page) + end + + And 'I click on the Edit button' do + click_on "Edit" + end + + And 'I change the content' do + fill_in "Content", with: 'Updated Wiki Content' + click_on "Save changes" + end + + Then 'I should see the updated content' do + page.should have_content "Updated Wiki Content" + end + + Then 'I should be redirected back to that Wiki page' do + url = URI.parse(current_url) + url.path.should == project_wiki_path(project, @page) + end + + And 'That page has two revisions' do + @page.update("new content", :markdown, "second commit") + end + + And 'I click the History button' do + click_on "History" + end + + Then 'I should see both revisions' do + page.should have_content current_user.name + page.should have_content "first commit" + page.should have_content "second commit" + end + + And 'I click on the "Delete this page" button' do + click_on "Delete this page" + end + + Then 'The page should be deleted' do + page.should have_content "Page was successfully deleted" + end + + And 'I click on the "Pages" button' do + click_on "Pages" + end + + Then 'I should see the existing page in the pages list' do + page.should have_content current_user.name + page.should have_content @page.title.titleize + end + + def wiki + @gollum_wiki = GollumWiki.new(project, current_user) + end end diff --git a/features/steps/project/public_projects.rb b/features/steps/project/public_projects.rb new file mode 100644 index 00000000000..7063e7d56ae --- /dev/null +++ b/features/steps/project/public_projects.rb @@ -0,0 +1,9 @@ +class PublicProjects < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + + Then 'I should see the list of public projects' do + page.should have_content "Public Projects" + end +end diff --git a/features/steps/public/projects_feature.rb b/features/steps/public/projects_feature.rb new file mode 100644 index 00000000000..e9a4d56e36b --- /dev/null +++ b/features/steps/public/projects_feature.rb @@ -0,0 +1,61 @@ +class Spinach::Features::PublicProjectsFeature < Spinach::FeatureSteps + include SharedPaths + + step 'I should see project "Community"' do + page.should have_content "Community" + end + + step 'I should not see project "Enterprise"' do + page.should_not have_content "Enterprise" + end + + step 'I should see project "Empty Public Project"' do + page.should have_content "Empty Public Project" + end + + step 'I should see public project details' do + page.should have_content '32 branches' + page.should have_content '16 tags' + end + + step 'I should see project readme' do + page.should have_content 'README.md' + end + + step 'public project "Community"' do + create :project_with_code, name: 'Community', public: true, default_branch: 'master' + end + + step 'public empty project "Empty Public Project"' do + create :project, name: 'Empty Public Project', public: true + end + + step 'I visit empty project page' do + project = Project.find_by_name('Empty Public Project') + visit project_path(project) + end + + step 'I visit project "Community" page' do + project = Project.find_by_name('Community') + visit project_path(project) + end + + step 'I should see empty public project details' do + page.should have_content 'Git global setup' + end + + step 'private project "Enterprise"' do + create :project, name: 'Enterprise' + end + + step 'I should see project "Community" home page' do + page.should have_content 'Repo size is' + end + + private + + def project + @project ||= Project.find_by_name("Community") + end +end + diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb index 446e3b9a8b3..d504fda3327 100644 --- a/features/steps/shared/active_tab.rb +++ b/features/steps/shared/active_tab.rb @@ -3,9 +3,9 @@ module SharedActiveTab def ensure_active_main_tab(content) if content == "Home" - page.find('ul.main_menu li.active').should have_css('i.icon-home') + page.find('.main-nav li.active').should have_css('i.icon-home') else - page.find('ul.main_menu li.active').should have_content(content) + page.find('.main-nav li.active').should have_content(content) end end @@ -13,11 +13,19 @@ module SharedActiveTab page.find('div.content ul.nav-tabs li.active').should have_content(content) end + def ensure_active_sub_nav(content) + page.find('div.content ul.nav-stacked-menu li.active').should have_content(content) + end + And 'no other main tabs should be active' do - page.should have_selector('ul.main_menu li.active', count: 1) + page.should have_selector('.main-nav li.active', count: 1) end And 'no other sub tabs should be active' do page.should have_selector('div.content ul.nav-tabs li.active', count: 1) end + + And 'no other sub navs should be active' do + page.should have_selector('div.content ul.nav-stacked-menu li.active', count: 1) + end end diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 04862338026..9c39a226e1b 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -3,15 +3,13 @@ module SharedDiffNote Given 'I cancel the diff comment' do within(".file") do - find(".js-close-discussion-note-form").trigger("click") + find(".js-close-discussion-note-form").click end end Given 'I delete a diff comment' do - sleep 1 - within(".file") do - first(".js-note-delete").trigger("click") - end + find('.note').hover + find(".js-note-delete").click end Given 'I haven\'t written any diff comment text' do @@ -21,37 +19,37 @@ module SharedDiffNote end Given 'I leave a diff comment like "Typo, please fix"' do - find("#586fb7c4e1add2d4d24e27566ed7064680098646_29_14.line_holder .js-add-diff-note-button").trigger("click") - within(".file") do + find('a[data-line-code="586fb7c4e1add2d4d24e27566ed7064680098646_29_14"]').click + within(".file form[rel$='586fb7c4e1add2d4d24e27566ed7064680098646_29_14']") do fill_in "note[note]", with: "Typo, please fix" - #click_button("Add Comment") find(".js-comment-button").trigger("click") sleep 0.05 end end Given 'I preview a diff comment text like "Should fix it :smile:"' do - find("#586fb7c4e1add2d4d24e27566ed7064680098646_29_14.line_holder .js-add-diff-note-button").trigger("click") - within(".file") do + find('a[data-line-code="586fb7c4e1add2d4d24e27566ed7064680098646_29_14"]').click + within(".file form[rel$='586fb7c4e1add2d4d24e27566ed7064680098646_29_14']") do fill_in "note[note]", with: "Should fix it :smile:" find(".js-note-preview-button").trigger("click") end end Given 'I preview another diff comment text like "DRY this up"' do - find("#586fb7c4e1add2d4d24e27566ed7064680098646_57_41.line_holder .js-add-diff-note-button").trigger("click") - within(".file") do + find('a[data-line-code="586fb7c4e1add2d4d24e27566ed7064680098646_57_41"]').click + + within(".file form[rel$='586fb7c4e1add2d4d24e27566ed7064680098646_57_41']") do fill_in "note[note]", with: "DRY this up" find(".js-note-preview-button").trigger("click") end end Given 'I open a diff comment form' do - find("#586fb7c4e1add2d4d24e27566ed7064680098646_29_14.line_holder .js-add-diff-note-button").trigger("click") + find('a[data-line-code="586fb7c4e1add2d4d24e27566ed7064680098646_29_14"]').click end Given 'I open another diff comment form' do - find("#586fb7c4e1add2d4d24e27566ed7064680098646_57_41.line_holder .js-add-diff-note-button").trigger("click") + find('a[data-line-code="586fb7c4e1add2d4d24e27566ed7064680098646_57_41"]').click end Given 'I write a diff comment like ":-1: I don\'t like this"' do diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 5dcc75f9165..da08da9420d 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -2,8 +2,8 @@ module SharedNote include Spinach::DSL Given 'I delete a comment' do - sleep 1 - first(".js-note-delete").trigger("click") + find('.note').hover + find(".js-note-delete").click end Given 'I haven\'t written any comment text' do @@ -97,21 +97,6 @@ module SharedNote end end - - - # Wall - - Given 'I write new comment "my special test message"' do - within(".js-main-target-form") do - fill_in "note[note]", with: "my special test message" - click_button "Add Comment" - end - end - - Then 'I should see project wall note "my special test message"' do - page.should have_content "my special test message" - end - Then 'I should see comment "XML attached"' do within(".note") do page.should have_content("XML attached") diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index a8e68012d05..c30eccce1c5 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -1,7 +1,7 @@ module SharedPaths include Spinach::DSL - When 'I visit new project page' do + step 'I visit new project page' do visit new_project_path end @@ -9,23 +9,23 @@ module SharedPaths # Group # ---------------------------------------- - When 'I visit group page' do + step 'I visit group page' do visit group_path(current_group) end - When 'I visit group issues page' do + step 'I visit group issues page' do visit issues_group_path(current_group) end - When 'I visit group merge requests page' do + step 'I visit group merge requests page' do visit merge_requests_group_path(current_group) end - When 'I visit group people page' do - visit people_group_path(current_group) + step 'I visit group members page' do + visit members_group_path(current_group) end - When 'I visit group settings page' do + step 'I visit group settings page' do visit edit_group_path(current_group) end @@ -33,27 +33,27 @@ module SharedPaths # Dashboard # ---------------------------------------- - Given 'I visit dashboard page' do + step 'I visit dashboard page' do visit dashboard_path end - Given 'I visit dashboard projects page' do + step 'I visit dashboard projects page' do visit projects_dashboard_path end - Given 'I visit dashboard issues page' do + step 'I visit dashboard issues page' do visit issues_dashboard_path end - Given 'I visit dashboard merge requests page' do + step 'I visit dashboard merge requests page' do visit merge_requests_dashboard_path end - Given 'I visit dashboard search page' do + step 'I visit dashboard search page' do visit search_path end - Given 'I visit dashboard help page' do + step 'I visit dashboard help page' do visit help_path end @@ -61,23 +61,23 @@ module SharedPaths # Profile # ---------------------------------------- - Given 'I visit profile page' do + step 'I visit profile page' do visit profile_path end - Given 'I visit profile account page' do + step 'I visit profile account page' do visit account_profile_path end - Given 'I visit profile SSH keys page' do - visit keys_path + step 'I visit profile SSH keys page' do + visit profile_keys_path end - Given 'I visit profile design page' do + step 'I visit profile design page' do visit design_profile_path end - Given 'I visit profile history page' do + step 'I visit profile history page' do visit history_profile_path end @@ -85,35 +85,35 @@ module SharedPaths # Admin # ---------------------------------------- - Given 'I visit admin page' do + step 'I visit admin page' do visit admin_root_path end - Given 'I visit admin projects page' do + step 'I visit admin projects page' do visit admin_projects_path end - Given 'I visit admin users page' do + step 'I visit admin users page' do visit admin_users_path end - Given 'I visit admin logs page' do + step 'I visit admin logs page' do visit admin_logs_path end - Given 'I visit admin hooks page' do + step 'I visit admin hooks page' do visit admin_hooks_path end - Given 'I visit admin Resque page' do - visit admin_resque_path + step 'I visit admin Resque page' do + visit admin_background_jobs_path end - And 'I visit admin groups page' do + step 'I visit admin groups page' do visit admin_groups_path end - When 'I visit admin teams page' do + step 'I visit admin teams page' do visit admin_teams_path end @@ -121,149 +121,193 @@ module SharedPaths # Generic Project # ---------------------------------------- - Given "I visit my project's home page" do + step "I visit my project's home page" do visit project_path(@project) end - Given "I visit my project's files page" do + step "I visit my project's settings page" do + visit edit_project_path(@project) + end + + step "I visit my project's files page" do visit project_tree_path(@project, root_ref) end - Given "I visit my project's commits page" do + step "I visit my project's commits page" do visit project_commits_path(@project, root_ref, {limit: 5}) end - Given "I visit my project's commits page for a specific path" do + step "I visit my project's commits page for a specific path" do visit project_commits_path(@project, root_ref + "/app/models/project.rb", {limit: 5}) end - Given 'I visit my project\'s commits stats page' do + step 'I visit my project\'s commits stats page' do visit stats_project_repository_path(@project) end - Given "I visit my project's network page" do - # Stub Graph::JsonBuilder max_size to speed up test (10 commits vs. 650) - Gitlab::Graph::JsonBuilder.stub(max_count: 10) + step "I visit my project's network page" do + # Stub Graph max_size to speed up test (10 commits vs. 650) + Network::Graph.stub(max_count: 10) - visit project_graph_path(@project, root_ref) + visit project_network_path(@project, root_ref) end - Given "I visit my project's issues page" do + step "I visit my project's issues page" do visit project_issues_path(@project) end - Given "I visit my project's merge requests page" do + step "I visit my project's merge requests page" do visit project_merge_requests_path(@project) end - Given "I visit my project's wall page" do - visit wall_project_path(@project) + step "I visit my project's wall page" do + visit project_wall_path(@project) end - Given "I visit my project's wiki page" do - visit project_wiki_path(@project, :index) + step "I visit my project's wiki page" do + visit project_wiki_path(@project, :home) end - When 'I visit project hooks page' do + step 'I visit project hooks page' do visit project_hooks_path(@project) end + step 'I visit project deploy keys page' do + visit project_deploy_keys_path(@project) + end + # ---------------------------------------- # "Shop" Project # ---------------------------------------- - And 'I visit project "Shop" page' do - project = Project.find_by_name("Shop") + step 'I visit project "Shop" page' do visit project_path(project) end - When 'I visit edit project "Shop" page' do - project = Project.find_by_name("Shop") + step 'I visit project "Forked Shop" merge requests page' do + visit project_merge_requests_path(@forked_project) + end + + step 'I visit edit project "Shop" page' do visit edit_project_path(project) end - Given 'I visit project branches page' do - visit branches_project_repository_path(@project) + step 'I visit project branches page' do + visit project_branches_path(@project) end - Given 'I visit compare refs page' do + step 'I visit compare refs page' do visit project_compare_index_path(@project) end - Given 'I visit project commits page' do + step 'I visit project commits page' do visit project_commits_path(@project, root_ref, {limit: 5}) end - Given 'I visit project commits page for stable branch' do + step 'I visit project commits page for stable branch' do visit project_commits_path(@project, 'stable', {limit: 5}) end - Given 'I visit project source page' do + step 'I visit project source page' do visit project_tree_path(@project, root_ref) end - Given 'I visit blob file from repo' do - visit project_tree_path(@project, File.join(ValidCommit::ID, ValidCommit::BLOB_FILE_PATH)) + step 'I visit blob file from repo' do + visit project_blob_path(@project, File.join(ValidCommit::ID, ValidCommit::BLOB_FILE_PATH)) end - Given 'I visit project source page for "8470d70"' do + step 'I visit project source page for "8470d70"' do visit project_tree_path(@project, "8470d70") end - Given 'I visit project tags page' do - visit tags_project_repository_path(@project) + step 'I visit project tags page' do + visit project_tags_path(@project) end - Given 'I visit project commit page' do + step 'I visit project commit page' do visit project_commit_path(@project, ValidCommit::ID) end - And 'I visit project "Shop" issues page' do - visit project_issues_path(Project.find_by_name("Shop")) + step 'I visit project "Shop" issues page' do + visit project_issues_path(project) end - Given 'I visit issue page "Release 0.4"' do + step 'I visit issue page "Release 0.4"' do issue = Issue.find_by_title("Release 0.4") visit project_issue_path(issue.project, issue) end - Given 'I visit project "Shop" labels page' do - visit project_labels_path(Project.find_by_name("Shop")) + step 'I visit project "Shop" labels page' do + visit project_labels_path(project) end - Given 'I visit merge request page "Bug NS-04"' do + step 'I visit merge request page "Bug NS-04"' do mr = MergeRequest.find_by_title("Bug NS-04") - visit project_merge_request_path(mr.project, mr) + visit project_merge_request_path(mr.target_project, mr) end - Given 'I visit merge request page "Bug NS-05"' do + step 'I visit merge request page "Bug NS-05"' do mr = MergeRequest.find_by_title("Bug NS-05") - visit project_merge_request_path(mr.project, mr) + visit project_merge_request_path(mr.target_project, mr) + end + + step 'I visit project "Shop" merge requests page' do + visit project_merge_requests_path(project) + end + + step 'I visit forked project "Shop" merge requests page' do + visit project_merge_requests_path(project) + end + + step 'I visit project "Shop" milestones page' do + visit project_milestones_path(project) + end + + step 'I visit project "Shop" team page' do + visit project_team_index_path(project) end - And 'I visit project "Shop" merge requests page' do - visit project_merge_requests_path(Project.find_by_name("Shop")) + step 'I visit project "Shop" wall page' do + visit project_wall_path(project) end - Given 'I visit project "Shop" milestones page' do - @project = Project.find_by_name("Shop") - visit project_milestones_path(@project) + step 'I visit project wiki page' do + visit project_wiki_path(@project, :home) end - Then 'I visit project "Shop" team page' do - visit project_team_index_path(Project.find_by_name("Shop")) + # ---------------------------------------- + # Public Projects + # ---------------------------------------- + + step 'I visit the public projects area' do + visit public_root_path end - Then 'I visit project "Shop" wall page' do - project = Project.find_by_name("Shop") - visit wall_project_path(project) + step 'I visit public page for "Community" project' do + visit public_project_path(Project.find_by_name("Community")) end - Given 'I visit project wiki page' do - visit project_wiki_path(@project, :index) + # ---------------------------------------- + # Snippets + # ---------------------------------------- + + Given 'I visit project "Shop" snippets page' do + visit project_snippets_path(project) + end + + Given 'I visit snippets page' do + visit snippets_path + end + + Given 'I visit new snippet page' do + visit new_snippet_path end def root_ref @project.repository.root_ref end + + def project + project = Project.find_by_name!("Shop") + end end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 5e61a4132c0..c5d8b62bfe7 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -3,13 +3,14 @@ module SharedProject # Create a project without caring about what it's called And "I own a project" do - @project = create(:project) + @project = create(:project_with_code, namespace: @user.namespace) @project.team << [@user, :master] end # Create a specific project called "Shop" And 'I own project "Shop"' do - @project = create(:project, name: "Shop") + @project = Project.find_by_name "Shop" + @project ||= create(:project_with_code, name: "Shop", namespace: @user.namespace) @project.team << [@user, :master] end @@ -41,7 +42,7 @@ module SharedProject Then 'I should see project "Shop" activity feed' do project = Project.find_by_name("Shop") - page.should have_content "#{@user.name} pushed new branch new_design at #{project.name}" + page.should have_content "#{@user.name} pushed new branch new_design at #{project.name_with_namespace}" end Then 'I should see project settings' do @@ -50,6 +51,10 @@ module SharedProject page.should have_content("Features:") end + Then 'page status code should be 404' do + page.status_code.should == 404 + end + def current_project @project ||= Project.first end diff --git a/features/steps/shared/snippet.rb b/features/steps/shared/snippet.rb new file mode 100644 index 00000000000..543e43196a5 --- /dev/null +++ b/features/steps/shared/snippet.rb @@ -0,0 +1,21 @@ +module SharedSnippet + include Spinach::DSL + + And 'I have public "Personal snippet one" snippet' do + create(:personal_snippet, + title: "Personal snippet one", + content: "Test content", + file_name: "snippet.rb", + private: false, + author: current_user) + end + + And 'I have private "Personal snippet private" snippet' do + create(:personal_snippet, + title: "Personal snippet private", + content: "Provate content", + file_name: "private_snippet.rb", + private: true, + author: current_user) + end +end diff --git a/features/steps/snippets/discover_snippets.rb b/features/steps/snippets/discover_snippets.rb new file mode 100644 index 00000000000..3afe019adf6 --- /dev/null +++ b/features/steps/snippets/discover_snippets.rb @@ -0,0 +1,17 @@ +class DiscoverSnippets < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedSnippet + + Then 'I should see "Personal snippet one" in snippets' do + page.should have_content "Personal snippet one" + end + + And 'I should not see "Personal snippet private" in snippets' do + page.should_not have_content "Personal snippet private" + 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 new file mode 100644 index 00000000000..bbdf5b97c84 --- /dev/null +++ b/features/steps/snippets/snippets.rb @@ -0,0 +1,64 @@ +class SnippetsFeature < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedProject + include SharedSnippet + + Given 'I click link "Personal snippet one"' do + click_link "Personal snippet one" + end + + And 'I should not see "Personal snippet one" in snippets' do + page.should_not have_content "Personal snippet one" + end + + And 'I click link "Edit"' do + within ".file-title" do + click_link "Edit" + end + end + + And 'I click link "Destroy"' do + click_link "Destroy" + end + + And 'I submit new snippet "Personal snippet three"' do + fill_in "personal_snippet_title", :with => "Personal snippet three" + fill_in "personal_snippet_file_name", :with => "my_snippet.rb" + within('.file-editor') do + find(:xpath, "//input[@id='personal_snippet_content']").set 'Content of snippet three' + end + click_button "Create snippet" + end + + Then 'I should see snippet "Personal snippet three"' do + page.should have_content "Personal snippet three" + page.should have_content "Content of snippet three" + end + + And 'I submit new title "Personal snippet new title"' do + fill_in "personal_snippet_title", :with => "Personal snippet new title" + click_button "Save" + end + + Then 'I should see "Personal snippet new title"' do + page.should have_content "Personal snippet new title" + end + + And 'I uncheck "Private" checkbox' do + find(:xpath, "//input[@id='personal_snippet_private']").set true + click_button "Save" + end + + Then 'I should see "Personal snippet one" public' do + page.should have_no_xpath("//i[@class='public-snippet']") + end + + And 'I visit snippet page "Personal snippet one"' do + visit snippet_path(snippet) + end + + def snippet + @snippet ||= PersonalSnippet.find_by_title!("Personal snippet one") + end +end diff --git a/features/steps/snippets/user_snippets.rb b/features/steps/snippets/user_snippets.rb new file mode 100644 index 00000000000..15d6da6db3d --- /dev/null +++ b/features/steps/snippets/user_snippets.rb @@ -0,0 +1,41 @@ +class UserSnippets < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedSnippet + + Given 'I visit my snippets page' do + visit user_snippets_path(current_user) + end + + Then 'I should see "Personal snippet one" in snippets' do + page.should have_content "Personal snippet one" + end + + And 'I should see "Personal snippet private" in snippets' do + page.should have_content "Personal snippet private" + end + + Then 'I should not see "Personal snippet one" in snippets' do + page.should_not have_content "Personal snippet one" + end + + And 'I should not see "Personal snippet private" in snippets' do + page.should_not have_content "Personal snippet private" + end + + Given 'I click "Public" filter' do + within('.nav-stacked') do + click_link "Public" + end + end + + Given 'I click "Private" filter' do + within('.nav-stacked') do + click_link "Private" + end + end + + def snippet + @snippet ||= PersonalSnippet.find_by_title!("Personal snippet one") + end +end diff --git a/features/steps/userteams/userteams.rb b/features/steps/userteams/userteams.rb deleted file mode 100644 index be83b4bac05..00000000000 --- a/features/steps/userteams/userteams.rb +++ /dev/null @@ -1,254 +0,0 @@ -class Userteams < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - - When 'I do not have teams with me' do - UserTeam.with_member(current_user).destroy_all - end - - Then 'I should see dashboard page without teams info block' do - page.has_no_css?(".teams-box").must_equal true - end - - When 'I have teams with my membership' do - team = create :user_team, owner: current_user - team.add_member(current_user, UserTeam.access_roles["Master"], true) - end - - Then 'I should see dashboard page with teams information block' do - page.should have_css(".teams-box") - end - - When 'exist user teams' do - team = create :user_team - team.add_member(current_user, UserTeam.access_roles["Master"], true) - end - - And 'I click on "All teams" link' do - click_link("All Teams") - end - - Then 'I should see "All teams" page' do - current_path.should == teams_path - end - - And 'I should see exist teams in teams list' do - team = UserTeam.last - find_in_list(".teams_list tr", team).must_equal true - end - - When 'I click to "New team" link' do - click_link("New Team") - end - - And 'I submit form with new team info' do - fill_in 'name', with: 'gitlab' - click_button 'Create team' - end - - Then 'I should be redirected to new team page' do - team = UserTeam.last - current_path.should == team_path(team) - end - - When 'I have teams with projects and members' do - team = create :user_team, owner: current_user - @project = create :project - team.add_member(current_user, UserTeam.access_roles["Master"], true) - team.assign_to_project(@project, UserTeam.access_roles["Master"]) - @event = create(:closed_issue_event, project: @project) - end - - When 'I visit team page' do - visit team_path(UserTeam.last) - end - - Then 'I should see projects list' do - page.should have_css(".projects_box") - projects_box = find(".projects_box") - projects_box.should have_content(@project.name) - end - - And 'project from team has issues assigned to me' do - team = UserTeam.last - team.projects.each do |project| - project.issues << create(:issue, assignee: current_user) - end - end - - When 'I visit team issues page' do - team = UserTeam.last - visit issues_team_path(team) - end - - Then 'I should see issues from this team assigned to me' do - team = UserTeam.last - team.projects.each do |project| - project.issues.assigned(current_user).each do |issue| - page.should have_content issue.title - end - end - end - - Given 'I have team with projects and members' do - team = create :user_team, owner: current_user - project = create :project - user = create :user - team.add_member(current_user, UserTeam.access_roles["Master"], true) - team.add_member(user, UserTeam.access_roles["Developer"], false) - team.assign_to_project(project, UserTeam.access_roles["Master"]) - end - - Given 'project from team has issues assigned to teams members' do - team = UserTeam.last - team.projects.each do |project| - team.members.each do |member| - project.issues << create(:issue, assignee: member) - end - end - end - - Then 'I should see issues from this team assigned to teams members' do - team = UserTeam.last - team.projects.each do |project| - team.members.each do |member| - project.issues.assigned(member).each do |issue| - page.should have_content issue.title - end - end - end - end - - Given 'project from team has merge requests assigned to me' do - team = UserTeam.last - team.projects.each do |project| - team.members.each do |member| - 3.times { project.merge_requests << create(:merge_request, assignee: member) } - end - end - end - - When 'I visit team merge requests page' do - team = UserTeam.last - visit merge_requests_team_path(team) - end - - Then 'I should see merge requests from this team assigned to me' do - team = UserTeam.last - team.projects.each do |project| - team.members.each do |member| - project.issues.assigned(member).each do |merge_request| - page.should have_content merge_request.title - end - end - end - end - - Given 'project from team has merge requests assigned to team members' do - team = UserTeam.last - team.projects.each do |project| - team.members.each do |member| - 3.times { project.merge_requests << create(:merge_request, assignee: member) } - end - end - end - - Then 'I should see merge requests from this team assigned to me' do - team = UserTeam.last - team.projects.each do |project| - team.members.each do |member| - project.issues.assigned(member).each do |merge_request| - page.should have_content merge_request.title - end - end - end - end - - Given 'I have new user "John"' do - create :user, name: "John" - end - - When 'I visit team people page' do - team = UserTeam.last - visit team_members_path(team) - end - - And 'I select user "John" from list with role "Reporter"' do - user = User.find_by_name("John") - within "#team_members" do - select user.name, :from => "user_ids" - select "Reporter", :from => "default_project_access" - end - click_button "Add" - end - - Then 'I should see user "John" in team list' do - user = User.find_by_name("John") - team_members_list = find(".team-table") - team_members_list.should have_content user.name - end - - And 'I have my own project without teams' do - @project = create :project, namespace: current_user.namespace - end - - And 'I visit my team page' do - team = UserTeam.where(owner_id: current_user.id).last - visit team_path(team) - end - - When 'I click on link "Projects"' do - click_link "Projects" - end - - And 'I click link "Assign project to Team"' do - click_link "Assign project to Team" - end - - Then 'I should see form with my own project in avaliable projects list' do - projects_select = find("#project_ids") - projects_select.should have_content(@project.name) - end - - When 'I submit form with selected project and max access' do - within "#assign_projects" do - select @project.name_with_namespace, :from => "project_ids" - select "Reporter", :from => "greatest_project_access" - end - click_button "Add" - end - - Then 'I should see my own project in team projects list' do - projects = find(".projects-table") - projects.should have_content(@project.name) - end - - When 'I click link "New Team Member"' do - click_link "New Team Member" - end - - protected - - def current_team - @user_team ||= UserTeam.first - end - - def project - current_team.projects.first - end - - def assigned_to_user key, user - project.send(key).where(assignee_id: user) - end - - def find_in_list(selector, item) - members_list = all(selector) - entered = false - members_list.each do |member_item| - entered = true if member_item.has_content?(item.name) - end - entered - end - -end diff --git a/features/support/env.rb b/features/support/env.rb index da40b38b79c..8798e62ea72 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -1,5 +1,10 @@ require 'simplecov' unless ENV['CI'] +if ENV['TRAVIS'] + require 'coveralls' + Coveralls.wear! +end + ENV['RAILS_ENV'] = 'test' require './config/environment' @@ -9,7 +14,7 @@ require 'spinach/capybara' require 'sidekiq/testing/inline' -%w(stubbed_repository valid_commit).each do |f| +%w(valid_commit big_commits select2_helper chosen_helper test_env).each do |f| require Rails.root.join('spec', 'support', f) end @@ -21,19 +26,20 @@ WebMock.allow_net_connect! # require 'capybara/poltergeist' Capybara.javascript_driver = :poltergeist +Capybara.register_driver :poltergeist do |app| + Capybara::Poltergeist::Driver.new(app, :js_errors => false, :timeout => 60) +end Spinach.hooks.on_tag("javascript") do ::Capybara.current_driver = ::Capybara.javascript_driver - ::Capybara.default_wait_time = 5 end - +Capybara.default_wait_time = 60 +Capybara.ignore_hidden_elements = false DatabaseCleaner.strategy = :truncation Spinach.hooks.before_scenario do - # Use tmp dir for FS manipulations - Gitlab.config.gitlab_shell.stub(repos_path: Rails.root.join('tmp', 'test-git-base-path')) - FileUtils.rm_rf Gitlab.config.gitlab_shell.repos_path - FileUtils.mkdir_p Gitlab.config.gitlab_shell.repos_path + TestEnv.setup_stubs + DatabaseCleaner.start end Spinach.hooks.after_scenario do @@ -41,6 +47,7 @@ Spinach.hooks.after_scenario do end Spinach.hooks.before_run do + TestEnv.init(mailer: false, init_repos: true, repos: false) RSpec::Mocks::setup self include FactoryGirl::Syntax::Methods diff --git a/features/teams/team.feature b/features/teams/team.feature deleted file mode 100644 index 9255e0daadb..00000000000 --- a/features/teams/team.feature +++ /dev/null @@ -1,69 +0,0 @@ -Feature: UserTeams - Background: - Given I sign in as a user - And I own project "Shop" - And project "Shop" has push event - - Scenario: No teams, no dashboard info block - When I do not have teams with me - And I visit dashboard page - Then I should see dashboard page without teams info block - - Scenario: I should see teams info block - When I have teams with my membership - And I visit dashboard page - Then I should see dashboard page with teams information block - - Scenario: I should can create new team - When I have teams with my membership - And I visit dashboard page - When I click to "New team" link - And I submit form with new team info - Then I should be redirected to new team page - - Scenario: I should see team dashboard list - When I have teams with projects and members - When I visit team page - Then I should see projects list - - Scenario: I should see team issues list - Given I have team with projects and members - And project from team has issues assigned to me - When I visit team issues page - Then I should see issues from this team assigned to me - - Scenario: I should see teams members issues list - Given I have team with projects and members - Given project from team has issues assigned to teams members - When I visit team issues page - Then I should see issues from this team assigned to teams members - - Scenario: I should see team merge requests list - Given I have team with projects and members - Given project from team has merge requests assigned to me - When I visit team merge requests page - Then I should see merge requests from this team assigned to me - - Scenario: I should see teams members merge requests list - Given I have team with projects and members - Given project from team has merge requests assigned to team members - When I visit team merge requests page - Then I should see merge requests from this team assigned to me - - Scenario: I should add user to projects in Team - Given I have team with projects and members - Given I have new user "John" - When I visit team people page - When I click link "New Team Member" - And I select user "John" from list with role "Reporter" - Then I should see user "John" in team list - - Scenario: I should assign my team to my own project - Given I have team with projects and members - And I have my own project without teams - And I visit my team page - When I click on link "Projects" - And I click link "Assign project to Team" - Then I should see form with my own project in avaliable projects list - When I submit form with selected project and max access - Then I should see my own project in team projects list diff --git a/lib/api.rb b/lib/api.rb deleted file mode 100644 index d9dce7c70cc..00000000000 --- a/lib/api.rb +++ /dev/null @@ -1,25 +0,0 @@ -Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file} - -module Gitlab - class API < Grape::API - version 'v3', using: :path - - rescue_from ActiveRecord::RecordNotFound do - rack_response({'message' => '404 Not found'}.to_json, 404) - end - - format :json - error_format :json - helpers APIHelpers - - mount Groups - mount Users - mount Projects - mount Issues - mount Milestones - mount Session - mount MergeRequests - mount Notes - mount Internal - end -end diff --git a/lib/api/api.rb b/lib/api/api.rb new file mode 100644 index 00000000000..c4c9f166db1 --- /dev/null +++ b/lib/api/api.rb @@ -0,0 +1,42 @@ +Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file} + +module API + class API < Grape::API + version 'v3', using: :path + + rescue_from ActiveRecord::RecordNotFound do + rack_response({'message' => '404 Not found'}.to_json, 404) + end + + rescue_from :all do |exception| + # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60 + # why is this not wrapped in something reusable? + trace = exception.backtrace + + message = "\n#{exception.class} (#{exception.message}):\n" + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) + message << " " << trace.join("\n ") + + API.logger.add Logger::FATAL, message + rack_response({'message' => '500 Internal Server Error'}, 500) + end + + format :json + helpers APIHelpers + + mount Groups + mount Users + mount Projects + mount Repositories + mount Issues + mount Milestones + mount Session + mount MergeRequests + mount Notes + mount Internal + mount SystemHooks + mount ProjectSnippets + mount DeployKeys + mount ProjectHooks + end +end diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb new file mode 100644 index 00000000000..55c947eb176 --- /dev/null +++ b/lib/api/deploy_keys.rb @@ -0,0 +1,84 @@ +module API + # Projects API + class DeployKeys < Grape::API + before { authenticate! } + + resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + + + # Get a specific project's keys + # + # Example Request: + # GET /projects/:id/keys + get ":id/keys" do + present user_project.deploy_keys, with: Entities::SSHKey + end + + # Get single key owned by currently authenticated user + # + # Example Request: + # GET /projects/:id/keys/:id + get ":id/keys/:key_id" do + key = user_project.deploy_keys.find params[:key_id] + present key, with: Entities::SSHKey + end + + # Add new ssh key to currently authenticated user + # If deploy key already exists - it will be joined to project + # but only if original one was is accessible by same user + # + # Parameters: + # key (required) - New SSH Key + # title (required) - New SSH Key's title + # Example Request: + # POST /projects/:id/keys + post ":id/keys" do + attrs = attributes_for_keys [:title, :key] + + if attrs[:key].present? + attrs[:key].strip! + + # check if key already exist in project + key = user_project.deploy_keys.find_by_key(attrs[:key]) + if key + present key, with: Entities::SSHKey + return + end + + # Check for available deploy keys in other projects + key = current_user.accessible_deploy_keys.find_by_key(attrs[:key]) + if key + user_project.deploy_keys << key + present key, with: Entities::SSHKey + return + end + end + + key = DeployKey.new attrs + + if key.valid? && user_project.deploy_keys << key + present key, with: Entities::SSHKey + else + not_found! + end + end + + # Delete existed ssh key of currently authenticated user + # + # Example Request: + # DELETE /projects/:id/keys/:id + delete ":id/keys/:key_id" do + key = user_project.deploy_keys.find params[:key_id] + key.destroy + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index c1873d87b55..1f35e9ec5fc 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1,46 +1,78 @@ -module Gitlab +module API module Entities class User < Grape::Entity expose :id, :username, :email, :name, :bio, :skype, :linkedin, :twitter, - :dark_scheme, :theme_id, :blocked, :created_at, :extern_uid, :provider + :theme_id, :color_scheme_id, :state, :created_at, :extern_uid, :provider + end + + class UserSafe < Grape::Entity + expose :name end class UserBasic < Grape::Entity - expose :id, :username, :email, :name, :blocked, :created_at + expose :id, :username, :email, :name, :state, :created_at end - class UserLogin < UserBasic + class UserLogin < User expose :private_token + expose :is_admin?, as: :is_admin + expose :can_create_group?, as: :can_create_group + expose :can_create_project?, as: :can_create_project + expose :can_create_team?, as: :can_create_team end class Hook < Grape::Entity expose :id, :url, :created_at end + class ForkedFromProject < Grape::Entity + expose :id + expose :name, :name_with_namespace + expose :path, :path_with_namespace + end + class Project < Grape::Entity - expose :id, :name, :description, :default_branch + expose :id, :description, :default_branch, :public, :ssh_url_to_repo, :http_url_to_repo, :web_url expose :owner, using: Entities::UserBasic - expose :private_flag, as: :private + expose :name, :name_with_namespace expose :path, :path_with_namespace - expose :issues_enabled, :merge_requests_enabled, :wall_enabled, :wiki_enabled, :created_at + expose :issues_enabled, :merge_requests_enabled, :wall_enabled, :wiki_enabled, :snippets_enabled, :created_at, :last_activity_at, :public expose :namespace + expose :forked_from_project, using: Entities::ForkedFromProject, :if => lambda{ | project, options | project.forked? } end class ProjectMember < UserBasic - expose :project_access, :as => :access_level do |user, options| + expose :project_access, as: :access_level do |user, options| options[:project].users_projects.find_by_user_id(user.id).project_access end end + class TeamMember < UserBasic + expose :permission, as: :access_level do |user, options| + options[:user_team].user_team_user_relationships.find_by_user_id(user.id).permission + end + end + + class TeamProject < Project + expose :greatest_access, as: :greatest_access_level do |project, options| + options[:user_team].user_team_project_relationships.find_by_project_id(project.id).greatest_access + end + end + class Group < Grape::Entity expose :id, :name, :path, :owner_id end - + class GroupDetail < Group expose :projects, using: Entities::Project end - + class GroupMember < UserBasic + expose :group_access, as: :access_level do |user, options| + options[:group].users_groups.find_by_user_id(user.id).group_access + end + end + class RepoObject < Grape::Entity expose :name, :commit expose :protected do |repo, options| @@ -63,7 +95,7 @@ module Gitlab class Milestone < Grape::Entity expose :id expose (:project_id) {|milestone| milestone.project.id} - expose :title, :description, :due_date, :closed, :updated_at, :created_at + expose :title, :description, :due_date, :state, :updated_at, :created_at end class Issue < Grape::Entity @@ -73,7 +105,7 @@ module Gitlab expose :label_list, as: :labels expose :milestone, using: Entities::Milestone expose :assignee, :author, using: Entities::UserBasic - expose :closed, :updated_at, :created_at + expose :state, :updated_at, :created_at end class SSHKey < Grape::Entity @@ -81,13 +113,15 @@ module Gitlab end class MergeRequest < Grape::Entity - expose :id, :target_branch, :source_branch, :project_id, :title, :closed, :merged + expose :id, :target_branch, :source_branch, :title, :state + expose :target_project_id, as: :project_id expose :author, :assignee, using: Entities::UserBasic end class Note < Grape::Entity expose :id expose :note, as: :body + expose :attachment_identifier, as: :attachment expose :author, using: Entities::UserBasic expose :created_at end @@ -96,5 +130,11 @@ module Gitlab expose :note 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 + end end end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index a67caef0bc5..396554404af 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -1,9 +1,23 @@ -module Gitlab +module API # groups API class Groups < Grape::API before { authenticate! } resource :groups do + helpers do + def find_group(id) + group = Group.find(id) + if current_user.admin or current_user.groups.include? 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 groups list # # Example Request: @@ -20,12 +34,14 @@ module Gitlab # Create group. Available only for admin # # Parameters: - # name (required) - Name - # path (required) - Path + # name (required) - The name of the group + # path (required) - The path of the group # Example Request: # POST /groups post do authenticated_as_admin! + required_attributes! [:name, :path] + attrs = attributes_for_keys [:name, :path] @group = Group.new(attrs) @group.owner = current_user @@ -44,13 +60,79 @@ module Gitlab # Example Request: # GET /groups/:id get ":id" do + group = find_group(params[:id]) + present group, with: Entities::GroupDetail + end + + # Transfer a project to the Group namespace + # + # Parameters: + # id - group id + # project_id - project id + # Example Request: + # POST /groups/:id/projects/:project_id + post ":id/projects/:project_id" do + authenticated_as_admin! @group = Group.find(params[:id]) - if current_user.admin or current_user.groups.include? @group - present @group, with: Entities::GroupDetail + project = Project.find(params[:project_id]) + if project.transfer(@group) + present @group else 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.users_groups + 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.users_groups.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.users_groups.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.users_groups.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 6bd8111c2b2..4f189f35196 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -1,16 +1,43 @@ -module Gitlab +module API module APIHelpers + PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" + PRIVATE_TOKEN_PARAM = :private_token + SUDO_HEADER ="HTTP_SUDO" + SUDO_PARAM = :sudo + def current_user - @current_user ||= User.find_by_authentication_token(params[:private_token] || env["HTTP_PRIVATE_TOKEN"]) + @current_user ||= User.find_by_authentication_token(params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]) + identifier = sudo_identifier() + # If the sudo is the current user do nothing + if (identifier && !(@current_user.id == identifier || @current_user.username == identifier)) + render_api_error!('403 Forbidden: Must be admin to use sudo', 403) unless @current_user.is_admin? + begin + @current_user = User.by_username_or_id(identifier) + rescue => ex + not_found!("No user id or username for: #{identifier}") + end + not_found!("No user id or username for: #{identifier}") if current_user.nil? + end + @current_user + end + + def sudo_identifier() + identifier ||= params[SUDO_PARAM] ||= env[SUDO_HEADER] + # Regex for integers + if (!!(identifier =~ /^[0-9]+$/)) + identifier.to_i + else + identifier + end end def user_project - @project ||= find_project + @project ||= find_project(params[:id]) @project || not_found! end - def find_project - project = Project.find_by_id(params[:id]) || Project.find_with_namespace(params[:id]) + def find_project(id) + project = Project.find_by_id(id) || Project.find_with_namespace(id) if project && can?(current_user, :read_project, project) project @@ -41,6 +68,17 @@ module Gitlab abilities.allowed?(object, action, subject) end + # Checks the occurrences of required attributes, each attribute must be present in the params hash + # or a Bad Request error is invoked. + # + # Parameters: + # keys (required) - A hash consisting of keys that must be present + def required_attributes!(keys) + keys.each do |key| + bad_request!(key) unless params[key].present? + end + end + def attributes_for_keys(keys) attrs = {} keys.each do |key| @@ -55,6 +93,12 @@ module Gitlab render_api_error!('403 Forbidden', 403) end + def bad_request!(attribute) + message = ["400 (Bad request)"] + message << "\"" + attribute.to_s + "\" not given" + render_api_error!(message.join(' '), 400) + end + def not_found!(resource = nil) message = ["404"] message << resource if resource diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 3e5e3a478ba..79f8eb3a543 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -1,23 +1,45 @@ -module Gitlab +module API # Internal access API class Internal < Grape::API + + DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive } + PUSH_COMMANDS = %w{ git-receive-pack } + namespace 'internal' do # # Check if ssh key has access to project code # + # Params: + # key_id - SSH Key id + # project - project path with namespace + # action - git action (git-upload-pack or git-receive-pack) + # ref - branch name + # get "/allowed" do + # 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/ + key = Key.find(params[:key_id]) - project = Project.find_with_namespace(params[:project]) + project = Project.find_with_namespace(project_path) git_cmd = params[:action] + return false unless project - if key.is_deploy_key - project == key.project && git_cmd == 'git-upload-pack' + + if key.is_a? DeployKey + key.projects.include?(project) && DOWNLOAD_COMMANDS.include?(git_cmd) else user = key.user + + return false if user.blocked? + action = case git_cmd - when 'git-upload-pack' + when *DOWNLOAD_COMMANDS then :download_code - when 'git-receive-pack' + when *PUSH_COMMANDS then if project.protected_branch?(params[:ref]) :push_code_to_protected_branches @@ -35,12 +57,14 @@ module Gitlab # get "/discover" do key = Key.find(params[:key_id]) - present key.user, with: Entities::User + present key.user, with: Entities::UserSafe end get "/check" do { - api_version: '3' + api_version: API.version, + gitlab_version: Gitlab::VERSION, + gitlab_rev: Gitlab::REVISION, } end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 4d832fbe593..a15203d1563 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -1,7 +1,8 @@ -module Gitlab +module API # Issues API class Issues < Grape::API before { authenticate! } + before { Thread.current[:current_user] = current_user } resource :issues do # Get currently authenticated user's issues @@ -48,6 +49,7 @@ module Gitlab # Example Request: # POST /projects/:id/issues post ":id/issues" do + required_attributes! [:title] attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id] attrs[:label_list] = params[:labels] if params[:labels].present? @issue = user_project.issues.new attrs @@ -69,16 +71,16 @@ module Gitlab # assignee_id (optional) - The ID of a user to assign issue # milestone_id (optional) - The ID of a milestone to assign issue # labels (optional) - The labels of an issue - # closed (optional) - The state of an issue (0 = false, 1 = true) + # state_event (optional) - The state event of an issue (close|reopen) # Example Request: # PUT /projects/:id/issues/:issue_id put ":id/issues/:issue_id" do @issue = user_project.issues.find(params[:issue_id]) authorize! :modify_issue, @issue - attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id, :closed] + attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id, :state_event] attrs[:label_list] = params[:labels] if params[:labels].present? - IssueObserver.current_user = current_user + if @issue.update_attributes attrs present @issue, with: Entities::Issue else diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 470cd1e1c2d..d690f1d07e7 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -1,9 +1,28 @@ -module Gitlab +module API # MergeRequest API class MergeRequests < Grape::API before { authenticate! } + before { Thread.current[:current_user] = current_user } resource :projects do + helpers do + def handle_merge_request_errors!(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + elsif errors[:branch_conflict].any? + error!(errors[:branch_conflict], 422) + end + not_found! + end + + def not_fork?(target_project_id, user_project) + target_project_id.nil? || target_project_id == user_project.id.to_s + end + + def target_matches_fork(target_project_id,user_project) + user_project.forked? && user_project.forked_from_project.id.to_s == target_project_id + end + end # List merge requests # @@ -40,9 +59,10 @@ module Gitlab # # Parameters: # - # id (required) - The ID of a project + # id (required) - The ID of a project - this will be the source of the merge request # source_branch (required) - The source branch # target_branch (required) - The target branch + # target_project - The target project of the merge request defaults to the :id of the project # assignee_id - Assignee user ID # title (required) - Title of MR # @@ -51,16 +71,27 @@ module Gitlab # post ":id/merge_requests" do authorize! :write_merge_request, user_project - - attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title] + required_attributes! [:source_branch, :target_branch, :title] + attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id] merge_request = user_project.merge_requests.new(attrs) merge_request.author = current_user + merge_request.source_project = user_project + target_project_id = attrs[:target_project_id] + if not_fork?(target_project_id, user_project) + merge_request.target_project = user_project + else + if target_matches_fork(target_project_id,user_project) + merge_request.target_project = Project.find_by_id(attrs[:target_project_id]) + else + render_api_error!('(Bad Request) Specified target project that is not the source project, or the source fork of the project.', 400) + end + end if merge_request.save merge_request.reload_code present merge_request, with: Entities::MergeRequest else - not_found! + handle_merge_request_errors! merge_request.errors end end @@ -73,12 +104,12 @@ module Gitlab # target_branch - The target branch # assignee_id - Assignee user ID # title - Title of MR - # closed - Status of MR. true - closed + # state_event - Status of MR. (close|reopen|merge) # Example: # PUT /projects/:id/merge_request/:merge_request_id # put ":id/merge_request/:merge_request_id" do - attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :closed] + attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :state_event] merge_request = user_project.merge_requests.find(params[:merge_request_id]) authorize! :modify_merge_request, merge_request @@ -88,7 +119,7 @@ module Gitlab merge_request.mark_as_unchecked present merge_request, with: Entities::MergeRequest else - not_found! + handle_merge_request_errors! merge_request.errors end end @@ -102,6 +133,8 @@ module Gitlab # POST /projects/:id/merge_request/:merge_request_id/comments # post ":id/merge_request/:merge_request_id/comments" do + required_attributes! [:note] + merge_request = user_project.merge_requests.find(params[:merge_request_id]) note = merge_request.notes.new(note: params[:note], project_id: user_project.id) note.author = current_user diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index 6aca9d01b09..aee12e7dc40 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -1,4 +1,4 @@ -module Gitlab +module API # Milestones API class Milestones < Grape::API before { authenticate! } @@ -41,6 +41,7 @@ module Gitlab # POST /projects/:id/milestones post ":id/milestones" do authorize! :admin_milestone, user_project + required_attributes! [:title] attrs = attributes_for_keys [:title, :description, :due_date] @milestone = user_project.milestones.new attrs @@ -59,14 +60,14 @@ module Gitlab # title (optional) - The title of a milestone # description (optional) - The description of a milestone # due_date (optional) - The due date of a milestone - # closed (optional) - The status of the milestone + # state_event (optional) - The state event of the milestone (close|activate) # Example Request: # PUT /projects/:id/milestones/:milestone_id put ":id/milestones/:milestone_id" do authorize! :admin_milestone, user_project @milestone = user_project.milestones.find(params[:milestone_id]) - attrs = attributes_for_keys [:title, :description, :due_date, :closed] + attrs = attributes_for_keys [:title, :description, :due_date, :state_event] if @milestone.update_attributes attrs present @milestone, with: Entities::Milestone else diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 70344d6e381..cb2bc764476 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -1,4 +1,4 @@ -module Gitlab +module API # Notes API class Notes < Grape::API before { authenticate! } @@ -14,6 +14,10 @@ module Gitlab # GET /projects/:id/notes get ":id/notes" do @notes = user_project.notes.common + + # Get recent notes if recent = true + @notes = @notes.order('id DESC') if params[:recent] + present paginate(@notes), with: Entities::Note end @@ -37,12 +41,16 @@ module Gitlab # Example Request: # POST /projects/:id/notes post ":id/notes" do + required_attributes! [:body] + @note = user_project.notes.new(note: params[:body]) @note.author = current_user if @note.save present @note, with: Entities::Note else + # :note is exposed as :body, but :note is set on error + bad_request!(:note) if @note.errors[:note].any? not_found! end end @@ -89,6 +97,8 @@ module Gitlab # POST /projects/:id/issues/:noteable_id/notes # POST /projects/:id/snippets/:noteable_id/notes post ":id/#{noteables_str}/:#{noteable_id_str}/notes" do + required_attributes! [:body] + @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) @note = @noteable.notes.new(note: params[:body]) @note.author = current_user diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb new file mode 100644 index 00000000000..28501256795 --- /dev/null +++ b/lib/api/project_hooks.rb @@ -0,0 +1,108 @@ +module API + # Projects API + class ProjectHooks < Grape::API + before { authenticate! } + + resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + + # Get project hooks + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id/hooks + get ":id/hooks" do + authorize! :admin_project, user_project + @hooks = paginate user_project.hooks + present @hooks, with: Entities::Hook + end + + # Get a project hook + # + # Parameters: + # id (required) - The ID of a project + # hook_id (required) - The ID of a project hook + # Example Request: + # GET /projects/:id/hooks/:hook_id + get ":id/hooks/:hook_id" do + authorize! :admin_project, user_project + @hook = user_project.hooks.find(params[:hook_id]) + present @hook, with: Entities::Hook + end + + + # Add hook to project + # + # Parameters: + # id (required) - The ID of a project + # url (required) - The hook URL + # Example Request: + # POST /projects/:id/hooks + post ":id/hooks" do + authorize! :admin_project, user_project + required_attributes! [:url] + + @hook = user_project.hooks.new({"url" => params[:url]}) + if @hook.save + present @hook, with: Entities::Hook + else + if @hook.errors[:url].present? + error!("Invalid url given", 422) + end + not_found! + end + end + + # Update an existing project hook + # + # Parameters: + # id (required) - The ID of a project + # hook_id (required) - The ID of a project hook + # url (required) - The hook URL + # Example Request: + # PUT /projects/:id/hooks/:hook_id + put ":id/hooks/:hook_id" do + @hook = user_project.hooks.find(params[:hook_id]) + authorize! :admin_project, user_project + required_attributes! [:url] + + attrs = attributes_for_keys [:url] + if @hook.update_attributes attrs + present @hook, with: Entities::Hook + else + if @hook.errors[:url].present? + error!("Invalid url given", 422) + end + not_found! + end + end + + # Deletes project hook. This is an idempotent function. + # + # Parameters: + # id (required) - The ID of a project + # hook_id (required) - The ID of hook to delete + # Example Request: + # DELETE /projects/:id/hooks/:hook_id + delete ":id/hooks/:hook_id" do + authorize! :admin_project, user_project + required_attributes! [:hook_id] + + begin + @hook = ProjectHook.find(params[:hook_id]) + @hook.destroy + rescue + # ProjectHook can raise Error if hook_id not found + end + end + end + end +end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb new file mode 100644 index 00000000000..bee6544ea3d --- /dev/null +++ b/lib/api/project_snippets.rb @@ -0,0 +1,123 @@ +module API + # Projects API + class ProjectSnippets < Grape::API + before { authenticate! } + + resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + + # Get a project snippets + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id/snippets + get ":id/snippets" do + present paginate(user_project.snippets), with: Entities::ProjectSnippet + end + + # Get a project snippet + # + # Parameters: + # id (required) - The ID of a project + # snippet_id (required) - The ID of a project snippet + # Example Request: + # GET /projects/:id/snippets/:snippet_id + get ":id/snippets/:snippet_id" do + @snippet = user_project.snippets.find(params[:snippet_id]) + present @snippet, with: Entities::ProjectSnippet + end + + # Create a new project snippet + # + # Parameters: + # id (required) - The ID of a project + # title (required) - The title of a snippet + # file_name (required) - The name of a snippet file + # lifetime (optional) - The expiration date of a snippet + # code (required) - The content of a snippet + # Example Request: + # POST /projects/:id/snippets + post ":id/snippets" do + authorize! :write_project_snippet, user_project + required_attributes! [:title, :file_name, :code] + + attrs = attributes_for_keys [:title, :file_name] + attrs[:expires_at] = params[:lifetime] if params[:lifetime].present? + attrs[:content] = params[:code] if params[:code].present? + @snippet = user_project.snippets.new attrs + @snippet.author = current_user + + if @snippet.save + present @snippet, with: Entities::ProjectSnippet + else + not_found! + end + end + + # Update an existing project snippet + # + # Parameters: + # id (required) - The ID of a project + # snippet_id (required) - The ID of a project snippet + # title (optional) - The title of a snippet + # file_name (optional) - The name of a snippet file + # lifetime (optional) - The expiration date of a snippet + # code (optional) - The content of a snippet + # Example Request: + # PUT /projects/:id/snippets/:snippet_id + put ":id/snippets/:snippet_id" do + @snippet = user_project.snippets.find(params[:snippet_id]) + authorize! :modify_project_snippet, @snippet + + attrs = attributes_for_keys [:title, :file_name] + attrs[:expires_at] = params[:lifetime] if params[:lifetime].present? + attrs[:content] = params[:code] if params[:code].present? + + if @snippet.update_attributes attrs + present @snippet, with: Entities::ProjectSnippet + else + not_found! + end + end + + # Delete a project snippet + # + # Parameters: + # id (required) - The ID of a project + # snippet_id (required) - The ID of a project snippet + # Example Request: + # DELETE /projects/:id/snippets/:snippet_id + delete ":id/snippets/:snippet_id" do + begin + @snippet = user_project.snippets.find(params[:snippet_id]) + authorize! :modify_project_snippet, @snippet + @snippet.destroy + rescue + end + end + + # Get a raw project snippet + # + # Parameters: + # id (required) - The ID of a project + # snippet_id (required) - The ID of a project snippet + # Example Request: + # GET /projects/:id/snippets/:snippet_id/raw + get ":id/snippets/:snippet_id/raw" do + @snippet = user_project.snippets.find(params[:snippet_id]) + + env['api.format'] = :txt + content_type 'text/plain' + present @snippet.content + end + end + end +end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index d416121a78a..cf357b23c40 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -1,9 +1,18 @@ -module Gitlab +module API # Projects API class Projects < Grape::API before { authenticate! } resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + # Get a projects list for authenticated user # # Example Request: @@ -13,6 +22,15 @@ module Gitlab present @projects, with: Entities::Project end + # Get an owned projects list for authenticated user + # + # Example Request: + # GET /projects/owned + get '/owned' do + @projects = paginate current_user.owned_projects + present @projects, with: Entities::Project + end + # Get a single project # # Parameters: @@ -23,32 +41,128 @@ module Gitlab present user_project, with: Entities::Project end + # Get a single project events + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id + get ":id/events" do + limit = (params[:per_page] || 20).to_i + offset = (params[:page] || 0).to_i * limit + events = user_project.events.recent.limit(limit).offset(offset) + + present events, with: Entities::Event + end + # Create new project # # Parameters: # name (required) - name for new project # description (optional) - short project description # default_branch (optional) - 'master' by default - # issues_enabled (optional) - enabled by default - # wall_enabled (optional) - enabled by default - # merge_requests_enabled (optional) - enabled by default - # wiki_enabled (optional) - enabled by default + # issues_enabled (optional) + # wall_enabled (optional) + # merge_requests_enabled (optional) + # wiki_enabled (optional) + # snippets_enabled (optional) + # namespace_id (optional) - defaults to user namespace + # public (optional) - false by default # Example Request # POST /projects post do + required_attributes! [:name] attrs = attributes_for_keys [:name, - :description, - :default_branch, - :issues_enabled, - :wall_enabled, - :merge_requests_enabled, - :wiki_enabled] + :path, + :description, + :default_branch, + :issues_enabled, + :wall_enabled, + :merge_requests_enabled, + :wiki_enabled, + :snippets_enabled, + :namespace_id, + :public] @project = ::Projects::CreateContext.new(current_user, attrs).execute if @project.saved? present @project, with: Entities::Project else + if @project.errors[:limit_reached].present? + error!(@project.errors[:limit_reached], 403) + end + not_found! + end + end + + # Create new project for a specified user. Only available to admin users. + # + # Parameters: + # user_id (required) - The ID of a user + # name (required) - name for new project + # description (optional) - short project description + # default_branch (optional) - 'master' by default + # issues_enabled (optional) + # wall_enabled (optional) + # merge_requests_enabled (optional) + # wiki_enabled (optional) + # snippets_enabled (optional) + # public (optional) + # Example Request + # POST /projects/user/:user_id + post "user/:user_id" do + authenticated_as_admin! + user = User.find(params[:user_id]) + attrs = attributes_for_keys [:name, + :description, + :default_branch, + :issues_enabled, + :wall_enabled, + :merge_requests_enabled, + :wiki_enabled, + :snippets_enabled, + :public] + @project = ::Projects::CreateContext.new(user, attrs).execute + if @project.saved? + present @project, with: Entities::Project + else + not_found! + end + end + + + # Mark this project as forked from another + # + # Parameters: + # id: (required) - The ID of the project being marked as a fork + # forked_from_id: (required) - The ID of the project it was forked from + # Example Request: + # POST /projects/:id/fork/:forked_from_id + post ":id/fork/:forked_from_id" do + authenticated_as_admin! + forked_from_project = find_project(params[:forked_from_id]) + unless forked_from_project.nil? + if user_project.forked_from_project.nil? + user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id) + else + render_api_error!("Project already forked", 409) + end + else not_found! end + + end + + # Remove a forked_from relationship + # + # Parameters: + # id: (required) - The ID of the project being marked as a fork + # Example Request: + # DELETE /projects/:id/fork + delete ":id/fork" do + authenticated_as_admin! + unless user_project.forked_project_link.nil? + user_project.forked_project_link.destroy + end end # Get a project team members @@ -89,16 +203,22 @@ module Gitlab # POST /projects/:id/members post ":id/members" do authorize! :admin_project, user_project - users_project = user_project.users_projects.new( - user_id: params[:user_id], - project_access: params[:access_level] - ) + required_attributes! [:user_id, :access_level] + + # either the user is already a team member or a new one + team_member = user_project.team_member_by_id(params[:user_id]) + if team_member.nil? + team_member = user_project.users_projects.new( + user_id: params[:user_id], + project_access: params[:access_level] + ) + end - if users_project.save - @member = users_project.user + if team_member.save + @member = team_member.user present @member, with: Entities::ProjectMember, project: user_project else - not_found! + handle_project_member_errors team_member.errors end end @@ -112,13 +232,16 @@ module Gitlab # PUT /projects/:id/members/:user_id put ":id/members/:user_id" do authorize! :admin_project, user_project - users_project = user_project.users_projects.find_by_user_id params[:user_id] + required_attributes! [:access_level] + + team_member = user_project.users_projects.find_by_user_id(params[:user_id]) + not_found!("User can not be found") if team_member.nil? - if users_project.update_attributes(project_access: params[:access_level]) - @member = users_project.user + if team_member.update_attributes(project_access: params[:access_level]) + @member = team_member.user present @member, with: Entities::ProjectMember, project: user_project else - not_found! + handle_project_member_errors team_member.errors end end @@ -131,297 +254,27 @@ module Gitlab # DELETE /projects/:id/members/:user_id delete ":id/members/:user_id" do authorize! :admin_project, user_project - users_project = user_project.users_projects.find_by_user_id params[:user_id] - users_project.destroy - end - - # Get project hooks - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/hooks - get ":id/hooks" do - authorize! :admin_project, user_project - @hooks = paginate user_project.hooks - present @hooks, with: Entities::Hook - end - - # Get a project hook - # - # Parameters: - # id (required) - The ID of a project - # hook_id (required) - The ID of a project hook - # Example Request: - # GET /projects/:id/hooks/:hook_id - get ":id/hooks/:hook_id" do - @hook = user_project.hooks.find(params[:hook_id]) - present @hook, with: Entities::Hook - end - - - # Add hook to project - # - # Parameters: - # id (required) - The ID of a project - # url (required) - The hook URL - # Example Request: - # POST /projects/:id/hooks - post ":id/hooks" do - authorize! :admin_project, user_project - @hook = user_project.hooks.new({"url" => params[:url]}) - if @hook.save - present @hook, with: Entities::Hook + team_member = user_project.users_projects.find_by_user_id(params[:user_id]) + unless team_member.nil? + team_member.destroy else - error!({'message' => '404 Not found'}, 404) + {message: "Access revoked", id: params[:user_id].to_i} end end - # Update an existing project hook + # search for projects current_user has access to # # Parameters: - # id (required) - The ID of a project - # hook_id (required) - The ID of a project hook - # url (required) - The hook URL + # query (required) - A string contained in the project name + # per_page (optional) - number of projects to return per page + # page (optional) - the page to retrieve # Example Request: - # PUT /projects/:id/hooks/:hook_id - put ":id/hooks/:hook_id" do - @hook = user_project.hooks.find(params[:hook_id]) - authorize! :admin_project, user_project - - attrs = attributes_for_keys [:url] - - if @hook.update_attributes attrs - present @hook, with: Entities::Hook - else - not_found! - end + # GET /projects/search/:query + get "/search/:query" do + ids = current_user.authorized_projects.map(&:id) + projects = Project.where("(id in (?) OR public = true) AND (name LIKE (?))", ids, "%#{params[:query]}%") + present paginate(projects), with: Entities::Project end - - # Delete project hook - # - # Parameters: - # id (required) - The ID of a project - # hook_id (required) - The ID of hook to delete - # Example Request: - # DELETE /projects/:id/hooks - delete ":id/hooks" do - authorize! :admin_project, user_project - @hook = user_project.hooks.find(params[:hook_id]) - @hook.destroy - end - - # Get a project repository branches - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/repository/branches - get ":id/repository/branches" do - present user_project.repo.heads.sort_by(&:name), with: Entities::RepoObject, project: user_project - end - - # Get a single branch - # - # Parameters: - # id (required) - The ID of a project - # branch (required) - The name of the branch - # Example Request: - # GET /projects/:id/repository/branches/:branch - get ":id/repository/branches/:branch" do - @branch = user_project.repo.heads.find { |item| item.name == params[:branch] } - not_found!("Branch does not exist") if @branch.nil? - present @branch, with: Entities::RepoObject, project: user_project - end - - # Protect a single branch - # - # Parameters: - # id (required) - The ID of a project - # branch (required) - The name of the branch - # Example Request: - # PUT /projects/:id/repository/branches/:branch/protect - put ":id/repository/branches/:branch/protect" do - @branch = user_project.repo.heads.find { |item| item.name == params[:branch] } - protected = user_project.protected_branches.find_by_name(@branch.name) - - unless protected - user_project.protected_branches.create(:name => @branch.name) - end - - present @branch, with: Entities::RepoObject, project: user_project - end - - # Unprotect a single branch - # - # Parameters: - # id (required) - The ID of a project - # branch (required) - The name of the branch - # Example Request: - # PUT /projects/:id/repository/branches/:branch/unprotect - put ":id/repository/branches/:branch/unprotect" do - @branch = user_project.repo.heads.find { |item| item.name == params[:branch] } - protected = user_project.protected_branches.find_by_name(@branch.name) - - if protected - protected.destroy - end - - present @branch, with: Entities::RepoObject, project: user_project - end - - # Get a project repository tags - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/repository/tags - get ":id/repository/tags" do - present user_project.repo.tags.sort_by(&:name).reverse, with: Entities::RepoObject - end - - # Get a project repository commits - # - # Parameters: - # id (required) - The ID of a project - # ref_name (optional) - The name of a repository branch or tag - # Example Request: - # GET /projects/:id/repository/commits - get ":id/repository/commits" do - authorize! :download_code, user_project - - page = params[:page] || 0 - per_page = params[:per_page] || 20 - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' - - commits = user_project.repository.commits(ref, nil, per_page, page * per_page) - present CommitDecorator.decorate(commits), with: Entities::RepoCommit - end - - # Get a project snippets - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/snippets - get ":id/snippets" do - present paginate(user_project.snippets), with: Entities::ProjectSnippet - end - - # Get a project snippet - # - # Parameters: - # id (required) - The ID of a project - # snippet_id (required) - The ID of a project snippet - # Example Request: - # GET /projects/:id/snippets/:snippet_id - get ":id/snippets/:snippet_id" do - @snippet = user_project.snippets.find(params[:snippet_id]) - present @snippet, with: Entities::ProjectSnippet - end - - # Create a new project snippet - # - # Parameters: - # id (required) - The ID of a project - # title (required) - The title of a snippet - # file_name (required) - The name of a snippet file - # lifetime (optional) - The expiration date of a snippet - # code (required) - The content of a snippet - # Example Request: - # POST /projects/:id/snippets - post ":id/snippets" do - authorize! :write_snippet, user_project - - attrs = attributes_for_keys [:title, :file_name] - attrs[:expires_at] = params[:lifetime] if params[:lifetime].present? - attrs[:content] = params[:code] if params[:code].present? - @snippet = user_project.snippets.new attrs - @snippet.author = current_user - - if @snippet.save - present @snippet, with: Entities::ProjectSnippet - else - not_found! - end - end - - # Update an existing project snippet - # - # Parameters: - # id (required) - The ID of a project - # snippet_id (required) - The ID of a project snippet - # title (optional) - The title of a snippet - # file_name (optional) - The name of a snippet file - # lifetime (optional) - The expiration date of a snippet - # code (optional) - The content of a snippet - # Example Request: - # PUT /projects/:id/snippets/:snippet_id - put ":id/snippets/:snippet_id" do - @snippet = user_project.snippets.find(params[:snippet_id]) - authorize! :modify_snippet, @snippet - - attrs = attributes_for_keys [:title, :file_name] - attrs[:expires_at] = params[:lifetime] if params[:lifetime].present? - attrs[:content] = params[:code] if params[:code].present? - - if @snippet.update_attributes attrs - present @snippet, with: Entities::ProjectSnippet - else - not_found! - end - end - - # Delete a project snippet - # - # Parameters: - # id (required) - The ID of a project - # snippet_id (required) - The ID of a project snippet - # Example Request: - # DELETE /projects/:id/snippets/:snippet_id - delete ":id/snippets/:snippet_id" do - @snippet = user_project.snippets.find(params[:snippet_id]) - authorize! :modify_snippet, @snippet - - @snippet.destroy - end - - # Get a raw project snippet - # - # Parameters: - # id (required) - The ID of a project - # snippet_id (required) - The ID of a project snippet - # Example Request: - # GET /projects/:id/snippets/:snippet_id/raw - get ":id/snippets/:snippet_id/raw" do - @snippet = user_project.snippets.find(params[:snippet_id]) - content_type 'text/plain' - present @snippet.content - end - - # Get a raw file contents - # - # Parameters: - # id (required) - The ID of a project - # sha (required) - The commit or branch name - # filepath (required) - The path to the file to display - # Example Request: - # GET /projects/:id/repository/commits/:sha/blob - get ":id/repository/commits/:sha/blob" do - authorize! :download_code, user_project - - ref = params[:sha] - - commit = user_project.repository.commit ref - not_found! "Commit" unless commit - - tree = Tree.new commit.tree, ref, params[:filepath] - not_found! "File" unless tree.try(:tree) - - content_type tree.mime_type - present tree.data - end - end end end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb new file mode 100644 index 00000000000..fef32d3a2fe --- /dev/null +++ b/lib/api/repositories.rb @@ -0,0 +1,190 @@ +module API + # Projects API + class Repositories < Grape::API + before { authenticate! } + + resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + + # Get a project repository branches + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id/repository/branches + get ":id/repository/branches" do + present user_project.repo.heads.sort_by(&:name), with: Entities::RepoObject, project: user_project + end + + # Get a single branch + # + # Parameters: + # id (required) - The ID of a project + # branch (required) - The name of the branch + # Example Request: + # GET /projects/:id/repository/branches/:branch + get ":id/repository/branches/:branch" do + @branch = user_project.repo.heads.find { |item| item.name == params[:branch] } + not_found!("Branch does not exist") if @branch.nil? + present @branch, with: Entities::RepoObject, project: user_project + end + + # Protect a single branch + # + # Parameters: + # id (required) - The ID of a project + # branch (required) - The name of the branch + # Example Request: + # PUT /projects/:id/repository/branches/:branch/protect + put ":id/repository/branches/:branch/protect" do + @branch = user_project.repo.heads.find { |item| item.name == params[:branch] } + not_found! unless @branch + protected = user_project.protected_branches.find_by_name(@branch.name) + + unless protected + user_project.protected_branches.create(name: @branch.name) + end + + present @branch, with: Entities::RepoObject, project: user_project + end + + # Unprotect a single branch + # + # Parameters: + # id (required) - The ID of a project + # branch (required) - The name of the branch + # Example Request: + # PUT /projects/:id/repository/branches/:branch/unprotect + put ":id/repository/branches/:branch/unprotect" do + @branch = user_project.repo.heads.find { |item| item.name == params[:branch] } + not_found! unless @branch + protected = user_project.protected_branches.find_by_name(@branch.name) + + if protected + protected.destroy + end + + present @branch, with: Entities::RepoObject, project: user_project + end + + # Get a project repository tags + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id/repository/tags + get ":id/repository/tags" do + present user_project.repo.tags.sort_by(&:name).reverse, with: Entities::RepoObject + end + + # Get a project repository commits + # + # Parameters: + # id (required) - The ID of a project + # ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used + # Example Request: + # GET /projects/:id/repository/commits + get ":id/repository/commits" do + authorize! :download_code, user_project + + page = (params[:page] || 0).to_i + per_page = (params[:per_page] || 20).to_i + ref = params[:ref_name] || user_project.try(:default_branch) || 'master' + + commits = user_project.repository.commits(ref, nil, per_page, page * per_page) + present commits, with: Entities::RepoCommit + end + + # Get a specific commit of a project + # + # Parameters: + # id (required) - The ID of a project + # sha (required) - The commit hash or name of a repository branch or tag + # Example Request: + # GET /projects/:id/repository/commits/:sha + get ":id/repository/commits/:sha" do + authorize! :download_code, user_project + sha = params[:sha] + commit = user_project.repository.commit(sha) + not_found! "Commit" unless commit + present commit, with: Entities::RepoCommit + end + + # Get the diff for a specific commit of a project + # + # Parameters: + # id (required) - The ID of a project + # sha (required) - The commit or branch name + # Example Request: + # GET /projects/:id/repository/commits/:sha/diff + get ":id/repository/commits/:sha/diff" do + authorize! :download_code, user_project + sha = params[:sha] + result = CommitLoadContext.new(user_project, current_user, {id: sha}).execute + not_found! "Commit" unless result[:commit] + result[:commit].diffs + end + + # Get a project repository tree + # + # Parameters: + # id (required) - The ID of a project + # ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used + # Example Request: + # GET /projects/:id/repository/tree + get ":id/repository/tree" do + authorize! :download_code, user_project + + ref = params[:ref_name] || user_project.try(:default_branch) || 'master' + path = params[:path] || nil + + commit = user_project.repository.commit(ref) + tree = Tree.new(user_project.repository, commit.id, ref, path) + + trees = [] + + %w(trees blobs submodules).each do |type| + trees += tree.send(type).map { |t| { name: t.name, type: type.singularize, mode: t.mode, id: t.id } } + end + + trees + end + + # Get a raw file contents + # + # Parameters: + # id (required) - The ID of a project + # sha (required) - The commit or branch name + # filepath (required) - The path to the file to display + # Example Request: + # GET /projects/:id/repository/blobs/:sha + get [ ":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob" ] do + authorize! :download_code, user_project + required_attributes! [:filepath] + + ref = params[:sha] + + repo = user_project.repository + + commit = repo.commit(ref) + not_found! "Commit" unless commit + + blob = Gitlab::Git::Blob.new(repo, commit.id, ref, params[:filepath]) + not_found! "File" unless blob.exists? + + env['api.format'] = :txt + + content_type blob.mime_type + present blob.data + end + end + end +end + diff --git a/lib/api/session.rb b/lib/api/session.rb index b4050160ae4..cc646895914 100644 --- a/lib/api/session.rb +++ b/lib/api/session.rb @@ -1,20 +1,21 @@ -module Gitlab +module API # Users API class Session < Grape::API # Login to get token # + # Parameters: + # login (*required) - user login + # email (*required) - user email + # password (required) - user password + # # Example Request: # POST /session post "/session" do - resource = User.find_for_database_authentication(email: params[:email]) - - return unauthorized! unless resource + auth = Gitlab::Auth.new + user = auth.find(params[:email] || params[:login], params[:password]) - if resource.valid_password?(params[:password]) - present resource, with: Entities::UserLogin - else - unauthorized! - end + return unauthorized! unless user + present user, with: Entities::UserLogin end end end diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb new file mode 100644 index 00000000000..3e239c5afe7 --- /dev/null +++ b/lib/api/system_hooks.rb @@ -0,0 +1,70 @@ +module API + # Hooks API + class SystemHooks < Grape::API + before { + authenticate! + authenticated_as_admin! + } + + resource :hooks do + # Get the list of system hooks + # + # Example Request: + # GET /hooks + get do + @hooks = SystemHook.all + present @hooks, with: Entities::Hook + end + + # Create new system hook + # + # Parameters: + # url (required) - url for system hook + # Example Request + # POST /hooks + post do + attrs = attributes_for_keys [:url] + required_attributes! [:url] + @hook = SystemHook.new attrs + if @hook.save + present @hook, with: Entities::Hook + else + not_found! + end + end + + # Test a hook + # + # Example Request + # GET /hooks/:id + get ":id" do + @hook = SystemHook.find(params[:id]) + data = { + event_name: "project_create", + name: "Ruby", + path: "ruby", + project_id: 1, + owner_name: "Someone", + owner_email: "example@gitlabhq.com" + } + @hook.execute(data) + data + end + + # Delete a hook. This is an idempotent function. + # + # Parameters: + # id (required) - ID of the hook + # Example Request: + # DELETE /hooks/:id + delete ":id" do + begin + @hook = SystemHook.find(params[:id]) + @hook.destroy + rescue + # SystemHook raises an Error if no hook with id found + end + end + end + end +end diff --git a/lib/api/users.rb b/lib/api/users.rb index 7ea90c75e9e..00dc2311ffd 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -1,4 +1,4 @@ -module Gitlab +module API # Users API class Users < Grape::API before { authenticate! } @@ -9,7 +9,10 @@ module Gitlab # Example Request: # GET /users get do - @users = paginate User + @users = User.scoped + @users = @users.active if params[:active].present? + @users = @users.search(params[:search]) if params[:search].present? + @users = paginate @users present @users, with: Entities::User end @@ -41,8 +44,9 @@ module Gitlab # POST /users 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] - user = User.new attrs, as: :admin + user = User.build_user(attrs, as: :admin) if user.save present user, with: Entities::User else @@ -59,7 +63,7 @@ module Gitlab # skype - Skype ID # linkedin - Linkedin # twitter - Twitter account - # projects_limit - Limit projects wich user can create + # projects_limit - Limit projects each user can create # extern_uid - External authentication provider UID # provider - External provider # bio - Bio @@ -67,16 +71,38 @@ module Gitlab # PUT /users/:id put ":id" do authenticated_as_admin! + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :extern_uid, :provider, :bio] - user = User.find_by_id(params[:id]) + user = User.find(params[:id]) + not_found!("User not found") unless user - if user && user.update_attributes(attrs) + if user.update_attributes(attrs) present user, with: Entities::User else not_found! end end + # Add ssh key to a specified user. Only available to admin users. + # + # Parameters: + # id (required) - The ID of a user + # key (required) - New SSH Key + # title (required) - New SSH Key's title + # Example Request: + # POST /users/:id/keys + post ":id/keys" do + authenticated_as_admin! + user = User.find(params[:id]) + attrs = attributes_for_keys [:title, :key] + key = user.keys.new attrs + if key.save + present key, with: Entities::SSHKey + else + not_found! + end + end + # Delete user. Available only for admin # # Example Request: @@ -99,7 +125,7 @@ module Gitlab # Example Request: # GET /user get do - present @current_user, with: Entities::User + present @current_user, with: Entities::UserLogin end # Get currently authenticated user's keys @@ -127,6 +153,8 @@ module Gitlab # Example Request: # POST /user/keys post "keys" do + required_attributes! [:title, :key] + attrs = attributes_for_keys [:title, :key] key = current_user.keys.new attrs if key.save @@ -136,15 +164,18 @@ module Gitlab end end - # Delete existed ssh key of currently authenticated user + # Delete existing ssh key of currently authenticated user # # Parameters: # id (required) - SSH Key ID # Example Request: # DELETE /user/keys/:id delete "keys/:id" do - key = current_user.keys.find params[:id] - key.delete + begin + key = current_user.keys.find params[:id] + key.destroy + rescue + end end end end diff --git a/lib/backup/database.rb b/lib/backup/database.rb new file mode 100644 index 00000000000..c4fb2e2e159 --- /dev/null +++ b/lib/backup/database.rb @@ -0,0 +1,58 @@ +require 'yaml' + +module Backup + class Database + attr_reader :config, :db_dir + + def initialize + @config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env] + @db_dir = File.join(Gitlab.config.backup.path, 'db') + FileUtils.mkdir_p(@db_dir) unless Dir.exists?(@db_dir) + end + + def dump + case config["adapter"] + when /^mysql/ then + system("mysqldump #{mysql_args} #{config['database']} > #{db_file_name}") + when "postgresql" then + pg_env + system("pg_dump #{config['database']} > #{db_file_name}") + end + end + + def restore + case config["adapter"] + when /^mysql/ then + system("mysql #{mysql_args} #{config['database']} < #{db_file_name}") + when "postgresql" then + pg_env + system("psql #{config['database']} -f #{db_file_name}") + end + end + + protected + + def db_file_name + File.join(db_dir, 'database.sql') + end + + def mysql_args + args = { + 'host' => '--host', + 'port' => '--port', + 'socket' => '--socket', + 'username' => '--user', + 'encoding' => '--default-character-set', + 'password' => '--password' + } + args.map { |opt, arg| "#{arg}='#{config[opt]}'" if config[opt] }.compact.join(' ') + end + + def pg_env + ENV['PGUSER'] = config["username"] if config["username"] + ENV['PGHOST'] = config["host"] if config["host"] + ENV['PGPORT'] = config["port"].to_s if config["port"] + ENV['PGPASSWORD'] = config["password"].to_s if config["password"] + end + end +end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb new file mode 100644 index 00000000000..258a0fb2589 --- /dev/null +++ b/lib/backup/manager.rb @@ -0,0 +1,106 @@ +module Backup + class Manager + def pack + # saving additional informations + s = {} + s[:db_version] = "#{ActiveRecord::Migrator.current_version}" + s[:backup_created_at] = Time.now + s[:gitlab_version] = %x{git rev-parse HEAD}.gsub(/\n/,"") + s[:tar_version] = %x{tar --version | head -1}.gsub(/\n/,"") + + Dir.chdir(Gitlab.config.backup.path) + + File.open("#{Gitlab.config.backup.path}/backup_information.yml", "w+") do |file| + file << s.to_yaml.gsub(/^---\n/,'') + end + + # create archive + print "Creating backup archive: #{s[:backup_created_at].to_i}_gitlab_backup.tar ... " + if Kernel.system("tar -cf #{s[:backup_created_at].to_i}_gitlab_backup.tar repositories/ db/ uploads/ backup_information.yml") + puts "done".green + else + puts "failed".red + end + end + + def cleanup + print "Deleting tmp directories ... " + if Kernel.system("rm -rf repositories/ db/ uploads/ backup_information.yml") + puts "done".green + else + puts "failed".red + end + end + + def remove_old + # delete backups + print "Deleting old backups ... " + keep_time = Gitlab.config.backup.keep_time.to_i + path = Gitlab.config.backup.path + + if keep_time > 0 + removed = 0 + file_list = Dir.glob(Rails.root.join(path, "*_gitlab_backup.tar")) + file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_backup.tar/ } + file_list.sort.each do |timestamp| + if Time.at(timestamp) < (Time.now - keep_time) + if system("rm #{timestamp}_gitlab_backup.tar") + removed += 1 + end + end + end + puts "done. (#{removed} removed)".green + else + puts "skipping".yellow + end + end + + def unpack + Dir.chdir(Gitlab.config.backup.path) + + # check for existing backups in the backup dir + file_list = Dir.glob("*_gitlab_backup.tar").each.map { |f| f.split(/_/).first.to_i } + puts "no backups found" if file_list.count == 0 + if file_list.count > 1 && ENV["BACKUP"].nil? + puts "Found more than one backup, please specify which one you want to restore:" + puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup" + exit 1 + end + + tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_backup.tar") + + unless File.exists?(tar_file) + puts "The specified backup doesn't exist!" + exit 1 + end + + print "Unpacking backup ... " + unless Kernel.system("tar -xf #{tar_file}") + puts "failed".red + exit 1 + else + puts "done".green + end + + settings = YAML.load_file("backup_information.yml") + ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 + + # backups directory is not always sub of Rails root and able to execute the git rev-parse below + begin + Dir.chdir(Rails.root) + + # restoring mismatching backups can lead to unexpected problems + if settings[:gitlab_version] != %x{git rev-parse HEAD}.gsub(/\n/, "") + puts "GitLab version mismatch:".red + puts " Your current HEAD differs from the HEAD in the backup!".red + puts " Please switch to the following revision and try again:".red + puts " revision: #{settings[:gitlab_version]}".red + exit 1 + end + ensure + # chdir back to original intended dir + Dir.chdir(Gitlab.config.backup.path) + end + end + end +end diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb new file mode 100644 index 00000000000..c5e3d049fd7 --- /dev/null +++ b/lib/backup/repository.rb @@ -0,0 +1,105 @@ +require 'yaml' + +module Backup + class Repository + attr_reader :repos_path + + def dump + prepare + + Project.find_each(batch_size: 1000) do |project| + print " * #{project.path_with_namespace} ... " + + if project.empty_repo? + puts "[SKIPPED]".cyan + next + end + + # Create namespace dir if missing + FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace + + if system("cd #{path_to_repo(project)} > /dev/null 2>&1 && git bundle create #{path_to_bundle(project)} --all > /dev/null 2>&1") + puts "[DONE]".green + else + puts "[FAILED]".red + end + + wiki = GollumWiki.new(project) + + if File.exists?(path_to_repo(wiki)) + print " * #{wiki.path_with_namespace} ... " + if system("cd #{path_to_repo(wiki)} > /dev/null 2>&1 && git bundle create #{path_to_bundle(wiki)} --all > /dev/null 2>&1") + puts " [DONE]".green + else + puts " [FAILED]".red + end + end + end + end + + def restore + if File.exists?(repos_path) + # Move repos dir to 'repositories.old' dir + bk_repos_path = File.join(repos_path, '..', 'repositories.old.' + Time.now.to_i.to_s) + FileUtils.mv(repos_path, bk_repos_path) + end + + FileUtils.mkdir_p(repos_path) + + Project.find_each(batch_size: 1000) do |project| + print "#{project.path_with_namespace} ... " + + project.namespace.ensure_dir_exist if project.namespace + + if system("git clone --bare #{path_to_bundle(project)} #{path_to_repo(project)} > /dev/null 2>&1") + puts "[DONE]".green + else + puts "[FAILED]".red + end + + wiki = GollumWiki.new(project) + + if File.exists?(path_to_bundle(wiki)) + print " * #{wiki.path_with_namespace} ... " + if system("git clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)} > /dev/null 2>&1") + puts " [DONE]".green + else + puts " [FAILED]".red + end + end + end + + print 'Put GitLab hooks in repositories dirs'.yellow + gitlab_shell_user_home = File.expand_path("~#{Gitlab.config.gitlab_shell.ssh_user}") + if system("#{gitlab_shell_user_home}/gitlab-shell/support/rewrite-hooks.sh #{Gitlab.config.gitlab_shell.repos_path}") + puts " [DONE]".green + else + puts " [FAILED]".red + end + + end + + protected + + def path_to_repo(project) + File.join(repos_path, project.path_with_namespace + '.git') + end + + def path_to_bundle(project) + File.join(backup_repos_path, project.path_with_namespace + ".bundle") + end + + def repos_path + Gitlab.config.gitlab_shell.repos_path + end + + def backup_repos_path + File.join(Gitlab.config.backup.path, "repositories") + end + + def prepare + FileUtils.rm_rf(backup_repos_path) + FileUtils.mkdir_p(backup_repos_path) + end + end +end diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb new file mode 100644 index 00000000000..462d3f1e274 --- /dev/null +++ b/lib/backup/uploads.rb @@ -0,0 +1,29 @@ +module Backup + class Uploads + attr_reader :app_uploads_dir, :backup_uploads_dir, :backup_dir + + def initialize + @app_uploads_dir = Rails.root.join('public', 'uploads') + @backup_dir = Gitlab.config.backup.path + @backup_uploads_dir = File.join(Gitlab.config.backup.path, 'uploads') + end + + # Copy uploads from public/uploads to backup/uploads + def dump + FileUtils.mkdir_p(backup_uploads_dir) + FileUtils.cp_r(app_uploads_dir, backup_dir) + end + + def restore + backup_existing_uploads_dir + + FileUtils.cp_r(backup_uploads_dir, app_uploads_dir) + end + + def backup_existing_uploads_dir + if File.exists?(app_uploads_dir) + FileUtils.mv(app_uploads_dir, Rails.root.join('public', "uploads.#{Time.now.to_i}")) + end + end + end +end diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index fb595e18b24..53bc079296a 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -8,7 +8,7 @@ module ExtractsPath included do if respond_to?(:before_filter) - before_filter :assign_ref_vars, only: [:show] + before_filter :assign_ref_vars end end @@ -33,7 +33,7 @@ module ExtractsPath # extract_ref("v2.0.0/README.md") # # => ['v2.0.0', 'README.md'] # - # extract_ref('/gitlab/vagrant/tree/master/app/models/project.rb') + # extract_ref('master/app/models/project.rb') # # => ['master', 'app/models/project.rb'] # # extract_ref('issues/1234/app/models/project.rb') @@ -45,22 +45,12 @@ module ExtractsPath # # Returns an Array where the first value is the tree-ish and the second is the # path - def extract_ref(input) + def extract_ref(id) pair = ['', ''] return pair unless @project - # Remove relative_url_root from path - input.gsub!(/^#{Gitlab.config.gitlab.relative_url_root}/, "") - # Remove project, actions and all other staff from path - input.gsub!(/^\/#{Regexp.escape(@project.path_with_namespace)}/, "") - input.gsub!(/^\/(tree|commits|blame|blob|refs|graph)\//, "") # remove actions - input.gsub!(/\?.*$/, "") # remove stamps suffix - input.gsub!(/.atom$/, "") # remove rss feed - input.gsub!(/.json$/, "") # remove json suffix - input.gsub!(/\/edit$/, "") # remove edit route part - - if input.match(/^([[:alnum:]]{40})(.+)/) + if id.match(/^([[:alnum:]]{40})(.+)/) # If the ref appears to be a SHA, we're done, just split the string pair = $~.captures else @@ -68,7 +58,6 @@ module ExtractsPath # branches and tags # Append a trailing slash if we only get a ref and no file path - id = input id += '/' unless id.ends_with?('/') valid_refs = @project.repository.ref_names @@ -96,8 +85,8 @@ module ExtractsPath # - @id - A string representing the joined ref and path # - @ref - A string representing the ref (e.g., the branch, tag, or commit SHA) # - @path - A string representing the filesystem path - # - @commit - A CommitDecorator representing the commit from the given ref - # - @tree - A TreeDecorator representing the tree at the given ref/path + # - @commit - A Commit representing the commit from the given ref + # - @tree - A Tree representing the tree at the given ref/path # # If the :id parameter appears to be requesting a specific response format, # that will be handled as well. @@ -105,28 +94,33 @@ module ExtractsPath # Automatically renders `not_found!` if a valid tree path could not be # resolved (e.g., when a user inserts an invalid path or ref). def assign_ref_vars - # Handle formats embedded in the id - if params[:id].ends_with?('.atom') - params[:id].gsub!(/\.atom$/, '') - request.format = :atom + # assign allowed options + allowed_options = ["filter_ref", "extended_sha1"] + @options = params.select {|key, value| allowed_options.include?(key) && !value.blank? } + @options = HashWithIndifferentAccess.new(@options) + + @id = get_id + @ref, @path = extract_ref(@id) + @repo = @project.repository + if @options[:extended_sha1].blank? + @commit = @repo.commit(@ref) + else + @commit = @repo.commit(@options[:extended_sha1]) end + @tree = Tree.new(@repo, @commit.id, @ref, @path) + @hex_path = Digest::SHA1.hexdigest(@path) + @logs_path = logs_file_project_ref_path(@project, @ref, @path) - path = CGI::unescape(request.fullpath.dup) - - @ref, @path = extract_ref(path) - - @id = File.join(@ref, @path) - - # It is used "@project.repository.commits(@ref, @path, 1, 0)", - # because "@project.repository.commit(@ref)" returns wrong commit when @ref is tag name. - commits = @project.repository.commits(@ref, @path, 1, 0) - @commit = CommitDecorator.decorate(commits.first) + raise InvalidPathError unless @tree.exists? + rescue RuntimeError, NoMethodError, InvalidPathError + not_found! + end - @tree = Tree.new(@commit.tree, @ref, @path) - @tree = TreeDecorator.new(@tree) + private - raise InvalidPathError if @tree.invalid? - rescue NoMethodError, InvalidPathError - not_found! + def get_id + id = params[:id] || params[:ref] + id += "/" + params[:path] unless params[:path].blank? + id end end diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb new file mode 100644 index 00000000000..87f9cfab608 --- /dev/null +++ b/lib/gitlab/access.rb @@ -0,0 +1,52 @@ +# Gitlab::Access module +# +# Define allowed roles that can be used +# in GitLab code to determine authorization level +# +module Gitlab + module Access + GUEST = 10 + REPORTER = 20 + DEVELOPER = 30 + MASTER = 40 + OWNER = 50 + + class << self + def values + options.values + end + + def options + { + "Guest" => GUEST, + "Reporter" => REPORTER, + "Developer" => DEVELOPER, + "Master" => MASTER, + } + end + + def options_with_owner + options.merge( + "Owner" => OWNER + ) + end + + def sym_options + { + guest: GUEST, + reporter: REPORTER, + developer: DEVELOPER, + master: MASTER, + } + end + end + + def human_access + Gitlab::Access.options_with_owner.key(access_field) + end + + def owner? + access_field == OWNER + end + end +end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index d0e792befbb..0f196297477 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -1,72 +1,24 @@ module Gitlab class Auth - def find_for_ldap_auth(auth, signed_in_resource = nil) - uid = auth.info.uid - provider = auth.provider - email = auth.info.email.downcase unless auth.info.email.nil? - raise OmniAuth::Error, "LDAP accounts must provide an uid and email address" if uid.nil? or email.nil? + def find(login, password) + user = User.find_by_email(login) || User.find_by_username(login) - if @user = User.find_by_extern_uid_and_provider(uid, provider) - @user - elsif @user = User.find_by_email(email) - log.info "Updating legacy LDAP user #{email} with extern_uid => #{uid}" - @user.update_attributes(:extern_uid => uid, :provider => provider) - @user - else - create_from_omniauth(auth, true) - end - end + if user.nil? || user.ldap_user? + # Second chance - try LDAP authentication + return nil unless ldap_conf.enabled - def create_from_omniauth(auth, ldap = false) - provider = auth.provider - uid = auth.info.uid || auth.uid - uid = uid.to_s.force_encoding("utf-8") - name = auth.info.name.to_s.force_encoding("utf-8") - email = auth.info.email.to_s.downcase unless auth.info.email.nil? - - ldap_prefix = ldap ? '(LDAP) ' : '' - raise OmniAuth::Error, "#{ldap_prefix}#{provider} does not provide an email"\ - " address" if auth.info.email.blank? - - log.info "#{ldap_prefix}Creating user from #{provider} login"\ - " {uid => #{uid}, name => #{name}, email => #{email}}" - password = Devise.friendly_token[0, 8].downcase - @user = User.new({ - extern_uid: uid, - provider: provider, - name: name, - username: email.match(/^[^@]*/)[0], - email: email, - password: password, - password_confirmation: password, - projects_limit: Gitlab.config.gitlab.default_projects_limit, - }, as: :admin) - if Gitlab.config.omniauth['block_auto_created_users'] && !ldap - @user.blocked = true - end - @user.save! - @user - end - - def find_or_new_for_omniauth(auth) - provider, uid = auth.provider, auth.uid - email = auth.info.email.downcase unless auth.info.email.nil? - - if @user = User.find_by_provider_and_extern_uid(provider, uid) - @user - elsif @user = User.find_by_email(email) - @user.update_attributes(:extern_uid => uid, :provider => provider) - @user + Gitlab::LDAP::User.authenticate(login, password) else - if Gitlab.config.omniauth['allow_single_sign_on'] - @user = create_from_omniauth(auth) - @user - end + 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 abbee6132d3..c522e0a413b 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -1,8 +1,11 @@ require_relative 'shell_env' +require_relative 'grack_helpers' module Grack class Auth < Rack::Auth::Basic - attr_accessor :user, :project + include Helpers + + attr_accessor :user, :project, :ref, :env def call(env) @env = env @@ -10,52 +13,73 @@ module Grack @auth = Request.new(env) # Need this patch due to the rails mount - @env['PATH_INFO'] = @request.path - @env['SCRIPT_NAME'] = "" - return render_not_found unless project - return unauthorized unless project.public || @auth.provided? - return bad_request if @auth.provided? && !@auth.basic? - - if valid? - if @auth.provided? - @env['REMOTE_USER'] = @auth.username - end - return @app.call(env) + # Need this if under RELATIVE_URL_ROOT + unless Gitlab.config.gitlab.relative_url_root.empty? + # If website is mounted using relative_url_root need to remove it first + @env['PATH_INFO'] = @request.path.sub(Gitlab.config.gitlab.relative_url_root,'') else - unauthorized + @env['PATH_INFO'] = @request.path end + + @env['SCRIPT_NAME'] = "" + + auth! end - def valid? + private + + def auth! + return render_not_found unless project + if @auth.provided? + return bad_request unless @auth.basic? + # Authentication with username and password login, password = @auth.credentials - self.user = User.find_by_email(login) || User.find_by_username(login) - return false unless user.try(:valid_password?, password) - Gitlab::ShellEnv.set_env(user) + @user = authenticate_user(login, password) + + if @user + Gitlab::ShellEnv.set_env(@user) + @env['REMOTE_USER'] = @auth.username + else + return unauthorized + end + + else + return unauthorized unless project.public end + if authorized_git_request? + @app.call(env) + else + unauthorized + end + end + + def authorized_git_request? # Git upload and receive if @request.get? - validate_get_request + authorize_request(@request.params['service']) elsif @request.post? - validate_post_request + authorize_request(File.basename(@request.path)) else false end end - def validate_get_request - project.public || can?(user, :download_code, project) + def authenticate_user(login, password) + auth = Gitlab::Auth.new + auth.find(login, password) end - def validate_post_request - if @request.path_info.end_with?('git-upload-pack') + def authorize_request(service) + case service + when 'git-upload-pack' project.public || can?(user, :download_code, project) - elsif @request.path_info.end_with?('git-receive-pack') - action = if project.protected_branch?(current_ref) + when'git-receive-pack' + action = if project.protected_branch?(ref) :push_code_to_protected_branches else :push_code @@ -67,45 +91,24 @@ module Grack end end - def can?(object, action, subject) - abilities.allowed?(object, action, subject) - end - - def current_ref - if @env["HTTP_CONTENT_ENCODING"] =~ /gzip/ - input = Zlib::GzipReader.new(@request.body).read - else - input = @request.body.read - end - # Need to reset seek point - @request.body.rewind - /refs\/heads\/([\w\.-]+)/.match(input).to_a.last - end - def project - unless instance_variable_defined? :@project - # Find project by PATH_INFO from env - if m = /^\/([\w\.\/-]+)\.git/.match(@request.path_info).to_a - @project = Project.find_with_namespace(m.last) - end - end - return @project + @project ||= project_by_path(@request.path_info) end - PLAIN_TYPE = {"Content-Type" => "text/plain"} - - def render_not_found - [404, PLAIN_TYPE, ["Not Found"]] + def ref + @ref ||= parse_ref end - protected + def parse_ref + input = if @env["HTTP_CONTENT_ENCODING"] =~ /gzip/ + Zlib::GzipReader.new(@request.body).read + else + @request.body.read + end - def abilities - @abilities ||= begin - abilities = Six.new - abilities << Ability - abilities - end + # Need to reset seek point + @request.body.rewind + /refs\/heads\/([\/\w\.-]+)/n.match(input.force_encoding('ascii-8bit')).to_a.last end - end# Auth -end# Grack + end +end diff --git a/lib/gitlab/backend/grack_helpers.rb b/lib/gitlab/backend/grack_helpers.rb new file mode 100644 index 00000000000..5ac9e9f325b --- /dev/null +++ b/lib/gitlab/backend/grack_helpers.rb @@ -0,0 +1,28 @@ +module Grack + module Helpers + def project_by_path(path) + if m = /^\/([\w\.\/-]+)\.git/.match(path).to_a + path_with_namespace = m.last + path_with_namespace.gsub!(/\.wiki$/, '') + + Project.find_with_namespace(path_with_namespace) + end + end + + def render_not_found + [404, {"Content-Type" => "text/plain"}, ["Not Found"]] + end + + def can?(object, action, subject) + abilities.allowed?(object, action, subject) + end + + def abilities + @abilities ||= begin + abilities = Six.new + abilities << Ability + abilities + end + end + end +end diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index b7b92e86a87..c819ce56ac9 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -10,7 +10,7 @@ module Gitlab # add_repository("gitlab/gitlab-ci") # def add_repository(name) - system("/home/git/gitlab-shell/bin/gitlab-projects add-project #{name}.git") + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "add-project", "#{name}.git" end # Import repository @@ -21,7 +21,43 @@ module Gitlab # import_repository("gitlab/gitlab-ci", "https://github.com/randx/six.git") # def import_repository(name, url) - system("/home/git/gitlab-shell/bin/gitlab-projects import-project #{name}.git #{url}") + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "import-project", "#{name}.git", url + end + + # Move repository + # + # path - project path with namespace + # new_path - new project path with namespace + # + # Ex. + # mv_repository("gitlab/gitlab-ci", "randx/gitlab-ci-new.git") + # + def mv_repository(path, new_path) + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "mv-project", "#{path}.git", "#{new_path}.git" + end + + # Update HEAD for repository + # + # path - project path with namespace + # branch - repository branch name + # + # Ex. + # update_repository_head("gitlab/gitlab-ci", "3-1-stable") + # + def update_repository_head(path, branch) + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "update-head", "#{path}.git", branch + end + + # Fork repository to new namespace + # + # path - project path with namespace + # fork_namespace - namespace for forked project + # + # Ex. + # fork_repository("gitlab/gitlab-ci", "randx") + # + def fork_repository(path, fork_namespace) + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "fork-project", "#{path}.git", fork_namespace end # Remove repository from file system @@ -32,7 +68,57 @@ module Gitlab # remove_repository("gitlab/gitlab-ci") # def remove_repository(name) - system("/home/git/gitlab-shell/bin/gitlab-projects rm-project #{name}.git") + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "rm-project", "#{name}.git" + end + + # Add repository branch from passed ref + # + # path - project path with namespace + # branch_name - new branch name + # ref - HEAD for new branch + # + # Ex. + # add_branch("gitlab/gitlab-ci", "4-0-stable", "master") + # + def add_branch(path, branch_name, ref) + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "create-branch", "#{path}.git", branch_name, ref + end + + # Remove repository branch + # + # path - project path with namespace + # branch_name - branch name to remove + # + # Ex. + # rm_branch("gitlab/gitlab-ci", "4-0-stable") + # + def rm_branch(path, branch_name) + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "rm-branch", "#{path}.git", branch_name + end + + # Add repository tag from passed ref + # + # path - project path with namespace + # tag_name - new tag name + # ref - HEAD for new tag + # + # Ex. + # add_tag("gitlab/gitlab-ci", "v4.0", "master") + # + def add_tag(path, tag_name, ref) + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "create-tag", "#{path}.git", tag_name, ref + end + + # Remove repository tag + # + # path - project path with namespace + # tag_name - tag name to remove + # + # Ex. + # rm_tag("gitlab/gitlab-ci", "v4.0") + # + def rm_tag(path, tag_name) + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "rm-tag", "#{path}.git", tag_name end # Add new key to gitlab-shell @@ -41,7 +127,7 @@ module Gitlab # add_key("key-42", "sha-rsa ...") # def add_key(key_id, key_content) - system("/home/git/gitlab-shell/bin/gitlab-keys add-key #{key_id} \"#{key_content}\"") + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-keys", "add-key", key_id, key_content end # Remove ssh key from gitlab shell @@ -50,11 +136,84 @@ module Gitlab # remove_key("key-342", "sha-rsa ...") # def remove_key(key_id, key_content) - system("/home/git/gitlab-shell/bin/gitlab-keys rm-key #{key_id} \"#{key_content}\"") + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-keys", "rm-key", key_id, key_content + end + + # Remove all ssh keys from gitlab shell + # + # Ex. + # remove_all_keys + # + def remove_all_keys + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-keys", "clear" + end + + # Add empty directory for storing repositories + # + # Ex. + # add_namespace("gitlab") + # + def add_namespace(name) + FileUtils.mkdir(full_path(name), mode: 0770) unless exists?(name) + end + + # Remove directory from repositories storage + # Every repository inside this directory will be removed too + # + # Ex. + # rm_namespace("gitlab") + # + def rm_namespace(name) + FileUtils.rm_r(full_path(name), force: true) + end + + # Move namespace directory inside repositories storage + # + # Ex. + # mv_namespace("gitlab", "gitlabhq") + # + def mv_namespace(old_name, new_name) + return false if exists?(new_name) || !exists?(old_name) + + FileUtils.mv(full_path(old_name), full_path(new_name)) + end + + # Remove GitLab Satellites for provided path (namespace or repo dir) + # + # Ex. + # rm_satellites("gitlab") + # + # rm_satellites("gitlab/gitlab-ci.git") + # + def rm_satellites(path) + raise ArgumentError.new("Path can't be blank") if path.blank? + + satellites_path = File.join(Gitlab.config.satellites.path, path) + FileUtils.rm_r(satellites_path, force: true) end def url_to_repo path Gitlab.config.gitlab_shell.ssh_path_prefix + "#{path}.git" end + + protected + + def gitlab_shell_user_home + File.expand_path("~#{Gitlab.config.gitlab_shell.ssh_user}") + end + + def repos_path + Gitlab.config.gitlab_shell.repos_path + end + + def full_path(dir_name) + raise ArgumentError.new("Directory name can't be blank") if dir_name.blank? + + File.join(repos_path, dir_name) + end + + def exists?(dir_name) + File.exists?(full_path(dir_name)) + end end end diff --git a/lib/gitlab/backend/shell_adapter.rb b/lib/gitlab/backend/shell_adapter.rb new file mode 100644 index 00000000000..f247f4593d7 --- /dev/null +++ b/lib/gitlab/backend/shell_adapter.rb @@ -0,0 +1,12 @@ +# == GitLab Shell mixin +# +# Provide a shortcut to Gitlab::Shell instance by gitlab_shell +# +module Gitlab + module ShellAdapter + def gitlab_shell + Gitlab::Shell.new + end + end +end + diff --git a/lib/gitlab/backend/shell_env.rb b/lib/gitlab/backend/shell_env.rb index 15721875093..044afb27f3f 100644 --- a/lib/gitlab/backend/shell_env.rb +++ b/lib/gitlab/backend/shell_env.rb @@ -1,6 +1,6 @@ module Gitlab # This module provide 2 methods - # to set specific ENV variabled for GitLab Shell + # to set specific ENV variables for GitLab Shell module ShellEnv extend self diff --git a/lib/gitlab/blacklist.rb b/lib/gitlab/blacklist.rb new file mode 100644 index 00000000000..2f9091e07df --- /dev/null +++ b/lib/gitlab/blacklist.rb @@ -0,0 +1,9 @@ +module Gitlab + module Blacklist + extend self + + def path + %w(admin dashboard groups help profile projects search public assets u s teams merge_requests issues users snippets services repository hooks notes) + end + end +end diff --git a/lib/gitlab/diff_parser.rb b/lib/gitlab/diff_parser.rb new file mode 100644 index 00000000000..fb27280c4a4 --- /dev/null +++ b/lib/gitlab/diff_parser.rb @@ -0,0 +1,77 @@ +module Gitlab + class DiffParser + include Enumerable + + attr_reader :lines, :new_path + + def initialize(diff) + @lines = diff.diff.lines.to_a + @new_path = diff.new_path + end + + def each + line_old = 1 + line_new = 1 + type = nil + + lines_arr = ::Gitlab::InlineDiff.processing lines + lines_arr.each do |line| + raw_line = line.dup + + next if line.match(/^\-\-\- \/dev\/null/) + next if line.match(/^\+\+\+ \/dev\/null/) + next if line.match(/^\-\-\- a/) + next if line.match(/^\+\+\+ b/) + + full_line = html_escape(line.gsub(/\n/, '')) + full_line = ::Gitlab::InlineDiff.replace_markers full_line + + if line.match(/^@@ -/) + type = "match" + + line_old = line.match(/\-[0-9]*/)[0].to_i.abs rescue 0 + line_new = line.match(/\+[0-9]*/)[0].to_i.abs rescue 0 + + next if line_old == 1 && line_new == 1 #top of file + yield(full_line, type, nil, nil, nil) + next + else + type = identification_type(line) + line_code = generate_line_code(new_path, line_new, line_old) + yield(full_line, type, line_code, line_new, line_old, raw_line) + end + + + if line[0] == "+" + line_new += 1 + elsif line[0] == "-" + line_old += 1 + else + line_new += 1 + line_old += 1 + end + end + end + + private + + def identification_type(line) + if line[0] == "+" + "new" + elsif line[0] == "-" + "old" + else + nil + end + end + + def generate_line_code(path, line_new, line_old) + "#{Digest::SHA1.hexdigest(path)}_#{line_old}_#{line_new}" + end + + def html_escape str + replacements = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } + str.gsub(/[&"'><]/, replacements) + end + end +end diff --git a/lib/gitlab/git_stats.rb b/lib/gitlab/git_stats.rb deleted file mode 100644 index 855bffb5dde..00000000000 --- a/lib/gitlab/git_stats.rb +++ /dev/null @@ -1,73 +0,0 @@ -module Gitlab - class GitStats - attr_accessor :repo, :ref - - def initialize repo, ref - @repo, @ref = repo, ref - end - - def authors - @authors ||= collect_authors - end - - def commits_count - @commits_count ||= repo.commit_count(ref) - end - - def files_count - args = [ref, '-r', '--name-only' ] - repo.git.run(nil, 'ls-tree', nil, {}, args).split("\n").count - end - - def authors_count - authors.size - end - - def graph - @graph ||= build_graph - end - - protected - - def collect_authors - shortlog = repo.git.shortlog({e: true, s: true }, ref) - - authors = [] - - lines = shortlog.split("\n") - - lines.each do |line| - data = line.split("\t") - commits = data.first - author = Grit::Actor.from_string(data.last) - - authors << OpenStruct.new( - name: author.name, - email: author.email, - commits: commits.to_i - ) - end - - authors.sort_by(&:commits).reverse - end - - def build_graph n = 4 - from, to = (Date.today - n.weeks), Date.today - args = ['--all', "--since=#{from.to_s(:date)}", '--format=%ad' ] - rev_list = repo.git.run(nil, 'rev-list', nil, {}, args).split("\n") - - commits_dates = rev_list.values_at(* rev_list.each_index.select {|i| i.odd?}) - commits_dates = commits_dates.map { |date_str| Time.parse(date_str).to_date.to_s(:date) } - - commits_per_day = from.upto(to).map do |day| - commits_dates.count(day.to_date.to_s(:date)) - end - - OpenStruct.new( - labels: from.upto(to).map { |day| day.stamp('Aug 23') }, - commits: commits_per_day, - weeks: n - ) - end - end -end diff --git a/lib/gitlab/graph/commit.rb b/lib/gitlab/graph/commit.rb deleted file mode 100644 index 13c8ebc9952..00000000000 --- a/lib/gitlab/graph/commit.rb +++ /dev/null @@ -1,52 +0,0 @@ -require "grit" - -module Gitlab - module Graph - class Commit - include ActionView::Helpers::TagHelper - - attr_accessor :time, :space, :refs, :parent_spaces - - def initialize(commit) - @_commit = commit - @time = -1 - @space = 0 - @parent_spaces = [] - end - - def method_missing(m, *args, &block) - @_commit.send(m, *args, &block) - end - - def to_graph_hash - h = {} - h[:parents] = self.parents.collect do |p| - [p.id,0,0] - end - h[:author] = { - name: author.name, - email: author.email - } - h[:time] = time - h[:space] = space - h[:parent_spaces] = parent_spaces - h[:refs] = refs.collect{|r|r.name}.join(" ") unless refs.nil? - h[:id] = sha - h[:date] = date - h[:message] = message - h - end - - def add_refs(ref_cache, repo) - if ref_cache.empty? - repo.refs.each do |ref| - ref_cache[ref.commit.id] ||= [] - ref_cache[ref.commit.id] << ref - end - end - @refs = ref_cache[@_commit.id] if ref_cache.include?(@_commit.id) - @refs ||= [] - end - end - end -end diff --git a/lib/gitlab/graph/json_builder.rb b/lib/gitlab/graph/json_builder.rb deleted file mode 100644 index cc971a245a7..00000000000 --- a/lib/gitlab/graph/json_builder.rb +++ /dev/null @@ -1,268 +0,0 @@ -require "grit" - -module Gitlab - module Graph - class JsonBuilder - attr_accessor :days, :commits, :ref_cache, :repo - - def self.max_count - @max_count ||= 650 - end - - def initialize project, ref, commit - @project = project - @ref = ref - @commit = commit - @repo = project.repo - @ref_cache = {} - - @commits = collect_commits - @days = index_commits - end - - def to_json(*args) - { - days: @days.compact.map { |d| [d.day, d.strftime("%b")] }, - commits: @commits.map(&:to_graph_hash) - }.to_json(*args) - end - - protected - - # Get commits from repository - # - def collect_commits - - @commits = Grit::Commit.find_all(repo, nil, {topo_order: true, max_count: self.class.max_count, skip: to_commit}).dup - - # Decorate with app/models/commit.rb - @commits.map! { |commit| ::Commit.new(commit) } - - # Decorate with lib/gitlab/graph/commit.rb - @commits.map! { |commit| Gitlab::Graph::Commit.new(commit) } - - # add refs to each commit - @commits.each { |commit| commit.add_refs(ref_cache, repo) } - - @commits - end - - # Method is adding time and space on the - # list of commits. As well as returns date list - # corelated with time set on commits. - # - # @param [Array<Graph::Commit>] commits to index - # - # @return [Array<TimeDate>] list of commit dates corelated with time on commits - def index_commits - days, times = [], [] - map = {} - - commits.reverse.each_with_index do |c,i| - c.time = i - days[i] = c.committed_date - map[c.id] = c - times[i] = c - end - - @_reserved = {} - days.each_index do |i| - @_reserved[i] = [] - end - - commits_sort_by_ref.each do |commit| - if map.include? commit.id then - place_chain(map[commit.id], map) - end - end - - # find parent spaces for not overlap lines - times.each do |c| - c.parent_spaces.concat(find_free_parent_spaces(c, map, times)) - end - - days - end - - # Skip count that the target commit is displayed in center. - def to_commit - commits = Grit::Commit.find_all(repo, nil, {topo_order: true}) - commit_index = commits.index do |c| - c.id == @commit.id - end - - if commit_index && (self.class.max_count / 2 < commit_index) then - # get max index that commit is displayed in the center. - commit_index - self.class.max_count / 2 - else - 0 - end - end - - def commits_sort_by_ref - commits.sort do |a,b| - if include_ref?(a) - -1 - elsif include_ref?(b) - 1 - else - b.committed_date <=> a.committed_date - end - end - end - - def include_ref?(commit) - heads = commit.refs.select do |ref| - ref.is_a?(Grit::Head) or ref.is_a?(Grit::Remote) or ref.is_a?(Grit::Tag) - end - - heads.map! do |head| - head.name - end - - heads.include?(@ref) - end - - def find_free_parent_spaces(commit, map, times) - spaces = [] - - commit.parents.each do |p| - if map.include?(p.id) then - parent = map[p.id] - - range = if commit.time < parent.time then - commit.time..parent.time - else - parent.time..commit.time - end - - space = if commit.space >= parent.space then - find_free_parent_space(range, parent.space, 1, commit.space, times) - else - find_free_parent_space(range, parent.space, -1, parent.space, times) - end - - mark_reserved(range, space) - spaces << space - end - end - - spaces - end - - def find_free_parent_space(range, space_base, space_step, space_default, times) - if is_overlap?(range, times, space_default) then - find_free_space(range, space_base, space_step) - else - space_default - end - end - - def is_overlap?(range, times, overlap_space) - range.each do |i| - if i != range.first && - i != range.last && - times[i].space == overlap_space then - - return true; - end - end - - false - end - - # Add space mark on commit and its parents - # - # @param [Graph::Commit] the commit object. - # @param [Hash<String,Graph::Commit>] map of commits - def place_chain(commit, map, parent_time = nil) - leaves = take_left_leaves(commit, map) - if leaves.empty? - return - end - # and mark it as reserved - min_time = leaves.last.time - max_space = 1 - parents = leaves.last.parents.collect - parents.each do |p| - if map.include? p.id - parent = map[p.id] - if parent.time < min_time - min_time = parent.time - end - if max_space < parent.space then - max_space = parent.space - end - end - end - if parent_time.nil? - max_time = leaves.first.time - else - max_time = parent_time - 1 - end - - time_range = leaves.last.time..leaves.first.time - space = find_free_space(time_range, max_space, 2) - leaves.each{|l| l.space = space} - - mark_reserved(min_time..max_time, space) - - # Visit branching chains - leaves.each do |l| - parents = l.parents.collect.select{|p| map.include? p.id and map[p.id].space.zero?} - for p in parents - place_chain(map[p.id], map, l.time) - end - end - end - - def mark_reserved(time_range, space) - for day in time_range - @_reserved[day].push(space) - end - end - - def find_free_space(time_range, space_base, space_step) - reserved = [] - for day in time_range - reserved += @_reserved[day] - end - reserved.uniq! - - space = space_base - while reserved.include?(space) do - space += space_step - if space <= 0 then - space_step *= -1 - space = space_base + space_step - end - end - - space - end - - # Takes most left subtree branch of commits - # which don't have space mark yet. - # - # @param [Graph::Commit] the commit object. - # @param [Hash<String,Graph::Commit>] map of commits - # - # @return [Array<Graph::Commit>] list of branch commits - def take_left_leaves(commit, map) - leaves = [] - leaves.push(commit) if commit.space.zero? - - while true - return leaves if commit.parents.count.zero? - return leaves unless map.include? commit.parents.first.id - - commit = map[commit.parents.first.id] - - return leaves unless commit.space.zero? - - leaves.push(commit) - end - end - end - end -end diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb new file mode 100644 index 00000000000..a1ff248a77f --- /dev/null +++ b/lib/gitlab/identifier.rb @@ -0,0 +1,23 @@ +# Detect user based on identifier like +# key-13 or user-36 or last commit +module Gitlab + module Identifier + def identify(identifier, project, newrev) + if identifier.blank? + # Local push from gitlab + email = project.repository.commit(newrev).author_email rescue nil + User.find_by_email(email) if email + + elsif identifier =~ /\Auser-\d+\Z/ + # git push over http + user_id = identifier.gsub("user-", "") + User.find_by_id(user_id) + + elsif identifier =~ /\Akey-\d+\Z/ + # git push over ssh + key_id = identifier.gsub("key-", "") + Key.find_by_id(key_id).try(:user) + end + end + end +end diff --git a/lib/gitlab/inline_diff.rb b/lib/gitlab/inline_diff.rb index 7a0a3214aa1..89c8e0680c3 100644 --- a/lib/gitlab/inline_diff.rb +++ b/lib/gitlab/inline_diff.rb @@ -4,7 +4,7 @@ module Gitlab START = "#!idiff-start!#" FINISH = "#!idiff-finish!#" - + def processing diff_arr indexes = _indexes_of_changed_lines diff_arr @@ -13,6 +13,9 @@ module Gitlab second_line = diff_arr[index+2] max_length = [first_line.size, second_line.size].max + # Skip inline diff if empty line was replaced with content + next if first_line == "-\n" + first_the_same_symbols = 0 (0..max_length + 1).each do |i| first_the_same_symbols = i - 1 @@ -20,9 +23,19 @@ module Gitlab break end end + first_token = first_line[0..first_the_same_symbols][1..-1] - diff_arr[index+1].sub!(first_token, first_token + START) - diff_arr[index+2].sub!(first_token, first_token + START) + start = first_token + START + + if first_token.empty? + # In case if we remove string of spaces in commit + diff_arr[index+1].sub!("-", "-" => "-#{START}") + diff_arr[index+2].sub!("+", "+" => "+#{START}") + else + diff_arr[index+1].sub!(first_token, first_token => start) + diff_arr[index+2].sub!(first_token, first_token => start) + end + last_the_same_symbols = 0 (1..max_length + 1).each do |i| last_the_same_symbols = -i @@ -60,8 +73,6 @@ module Gitlab line.gsub!(FINISH, "</span>") line end - end - end end diff --git a/lib/gitlab/issues_labels.rb b/lib/gitlab/issues_labels.rb new file mode 100644 index 00000000000..bc49d27b521 --- /dev/null +++ b/lib/gitlab/issues_labels.rb @@ -0,0 +1,28 @@ +module Gitlab + class IssuesLabels + class << self + def important_labels + %w(bug critical confirmed) + end + + def warning_labels + %w(documentation support) + end + + def neutral_labels + %w(discussion suggestion) + end + + def positive_labels + %w(feature enhancement) + end + + def generate(project) + labels = important_labels + warning_labels + neutral_labels + positive_labels + + project.issues_default_label_list = labels + project.save + end + end + end +end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb new file mode 100644 index 00000000000..260bacfeeb0 --- /dev/null +++ b/lib/gitlab/ldap/user.rb @@ -0,0 +1,94 @@ +require 'gitlab/oauth/user' + +# LDAP extension for User model +# +# * Find or create user from omniauth.auth data +# * Links LDAP account with existing user +# * Auth LDAP user with login and password +# +module Gitlab + module LDAP + class User < Gitlab::OAuth::User + class << self + def find_or_create(auth) + @auth = auth + + if uid.blank? || email.blank? + raise_error("Account must provide an uid and email address") + end + + user = find(auth) + + unless user + # Look for user with same emails + # + # Possible cases: + # * When user already has account and need to link his LDAP account. + # * LDAP uid changed for user with same email and we need to update his uid + # + user = find_user(email) + + if user + user.update_attributes(extern_uid: uid, provider: provider) + log.info("(LDAP) Updating legacy LDAP user #{email} with extern_uid => #{uid}") + else + # Create a new user inside GitLab database + # based on LDAP credentials + # + # + user = create(auth) + end + end + + user + end + + def find_user(email) + user = model.find_by_email(email) + + # If no user found and allow_username_or_email_login is true + # we look for user by extracting part of his email + if !user && email && ldap_conf['allow_username_or_email_login'] + uname = email.partition('@').first + user = model.find_by_username(uname) + end + + user + 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 = OmniAuth::LDAP::Adaptor.new(ldap_conf) + ldap_user = ldap.bind_as( + filter: Net::LDAP::Filter.eq(ldap.uid, login), + size: 1, + password: password + ) + + find_by_uid(ldap_user.dn) if ldap_user + end + + private + + def find_by_uid(uid) + model.where(provider: provider, extern_uid: uid).last + end + + def provider + 'ldap' + end + + def raise_error(message) + raise OmniAuth::Error, "(LDAP) " + message + end + + def ldap_conf + Gitlab.config.ldap + end + end + end + end +end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index e7d6e3e6bd9..dc9c0e0ab2c 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -7,6 +7,7 @@ module Gitlab # Supported reference formats are: # * @foo for team members # * #123 for issues + # * #JIRA-123 for Jira issues # * !123 for merge requests # * $123 for snippets # * 123456 for commits @@ -17,7 +18,7 @@ module Gitlab # Examples # # >> gfm("Hey @david, can you fix this?") - # => "Hey <a href="/gitlab/team_members/1">@david</a>, can you fix this?" + # => "Hey <a href="/u/david">@david</a>, can you fix this?" # # >> gfm("Commit 35d5f7c closes #1234") # => "Commit <a href="/gitlab/commits/35d5f7c">35d5f7c</a> closes <a href="/gitlab/issues/1234">#1234</a>" @@ -25,6 +26,8 @@ module Gitlab # >> gfm(":trollface:") # => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" /> module Markdown + include IssuesHelper + attr_reader :html_options # Public: Parse the provided text with GitLab-Flavored Markdown @@ -60,7 +63,7 @@ module Gitlab insert_piece($1) end - sanitize text.html_safe, attributes: ActionView::Base.sanitized_allowed_attributes + %w(id class) + sanitize text.html_safe, attributes: ActionView::Base.sanitized_allowed_attributes + %w(id class), tags: ActionView::Base.sanitized_allowed_tags + %w(table tr td th) end private @@ -95,10 +98,11 @@ module Gitlab (?<prefix>\W)? # Prefix ( # Reference @(?<user>[a-zA-Z][a-zA-Z0-9_\-\.]*) # User name - |\#(?<issue>\d+) # Issue ID + |\#(?<issue>([a-zA-Z]+-)?\d+) # Issue ID |!(?<merge_request>\d+) # MR ID |\$(?<snippet>\d+) # Snippet ID |(?<commit>[\h]{6,40}) # Commit ID + |(?<skip>gfm-extraction-[\h]{6,40}) # Skip gfm extractions. Otherwise will be parsed as commit ) (?<suffix>\W)? # Suffix }x.freeze @@ -111,13 +115,18 @@ module Gitlab prefix = $~[:prefix] suffix = $~[:suffix] type = TYPES.select{|t| !$~[t].nil?}.first - identifier = $~[type] - # Avoid HTML entities - if prefix && suffix && prefix[0] == '&' && suffix[-1] == ';' - match - elsif ref_link = reference_link(type, identifier) - "#{prefix}#{ref_link}#{suffix}" + if type + identifier = $~[type] + + # Avoid HTML entities + if prefix && suffix && prefix[0] == '&' && suffix[-1] == ';' + match + elsif ref_link = reference_link(type, identifier) + "#{prefix}#{ref_link}#{suffix}" + else + match + end else match end @@ -157,19 +166,22 @@ module Gitlab end def reference_user(identifier) - if member = @project.users_projects.joins(:user).where(users: { username: identifier }).first - link_to("@#{identifier}", project_team_member_url(@project, member), html_options.merge(class: "gfm gfm-team_member #{html_options[:class]}")) if member + if member = @project.team_members.find { |user| user.username == identifier } + link_to("@#{identifier}", user_url(identifier), html_options.merge(class: "gfm gfm-team_member #{html_options[:class]}")) if member end end def reference_issue(identifier) - if issue = @project.issues.where(id: identifier).first - link_to("##{identifier}", project_issue_url(@project, issue), html_options.merge(title: "Issue: #{issue.title}", class: "gfm gfm-issue #{html_options[:class]}")) + if @project.issue_exists? identifier + url = url_for_issue(identifier) + title = title_for_issue(identifier) + + link_to("##{identifier}", url, html_options.merge(title: "Issue: #{title}", class: "gfm gfm-issue #{html_options[:class]}")) end end def reference_merge_request(identifier) - if merge_request = @project.merge_requests.where(id: identifier).first + if merge_request = @project.merge_requests.where(iid: identifier).first link_to("!#{identifier}", project_merge_request_url(@project, merge_request), html_options.merge(title: "Merge Request: #{merge_request.title}", class: "gfm gfm-merge_request #{html_options[:class]}")) end end @@ -182,7 +194,7 @@ module Gitlab def reference_commit(identifier) if @project.valid_repo? && commit = @project.repository.commit(identifier) - link_to(identifier, project_commit_url(@project, commit), html_options.merge(title: CommitDecorator.new(commit).link_title, class: "gfm gfm-commit #{html_options[:class]}")) + link_to(identifier, project_commit_url(@project, commit), html_options.merge(title: commit.link_title, class: "gfm gfm-commit #{html_options[:class]}")) end end end diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/oauth/user.rb new file mode 100644 index 00000000000..1b32b99f4ba --- /dev/null +++ b/lib/gitlab/oauth/user.rb @@ -0,0 +1,85 @@ +# OAuth extension for User model +# +# * Find GitLab user based on omniauth uid and provider +# * Create new user from omniauth data +# +module Gitlab + module OAuth + class User + class << self + attr_reader :auth + + def find(auth) + @auth = auth + find_by_uid_and_provider + end + + def create(auth) + @auth = auth + password = Devise.friendly_token[0, 8].downcase + opts = { + extern_uid: uid, + provider: provider, + name: name, + username: username, + email: email, + password: password, + password_confirmation: password, + } + + user = model.build_user(opts, as: :admin) + user.save! + log.info "(OAuth) Creating user #{email} from login with extern_uid => #{uid}" + + if Gitlab.config.omniauth['block_auto_created_users'] && !ldap? + user.block + end + + user + end + + private + + def find_by_uid_and_provider + model.where(provider: provider, extern_uid: uid).last + end + + def uid + auth.info.uid || auth.uid + end + + def email + auth.info.email.downcase unless auth.info.email.nil? + end + + def name + auth.info.name.to_s.force_encoding("utf-8") + end + + def username + email.match(/^[^@]*/)[0] + end + + def provider + auth.provider + end + + def log + Gitlab::AppLogger + end + + def model + ::User + end + + def raise_error(message) + raise OmniAuth::Error, "(OAuth) " + message + end + + def ldap? + provider == 'ldap' + end + end + end + end +end diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb index f2cfd8073e3..2f30fde2078 100644 --- a/lib/gitlab/popen.rb +++ b/lib/gitlab/popen.rb @@ -2,7 +2,7 @@ module Gitlab module Popen def popen(cmd, path) vars = { "PWD" => path } - options = { :chdir => path } + options = { chdir: path } @cmd_output = "" @cmd_status = 0 diff --git a/lib/gitlab/project_mover.rb b/lib/gitlab/project_mover.rb deleted file mode 100644 index e21f45c6564..00000000000 --- a/lib/gitlab/project_mover.rb +++ /dev/null @@ -1,45 +0,0 @@ -# ProjectMover class -# -# Used for moving project repositories from one subdir to another -module Gitlab - class ProjectMover - class ProjectMoveError < StandardError; end - - attr_reader :project, :old_dir, :new_dir - - def initialize(project, old_dir, new_dir) - @project = project - @old_dir = old_dir - @new_dir = new_dir - end - - def execute - # Create new dir if missing - new_dir_path = File.join(Gitlab.config.gitlab_shell.repos_path, new_dir) - FileUtils.mkdir( new_dir_path, mode: 0770 ) unless File.exists?(new_dir_path) - - old_path = File.join(Gitlab.config.gitlab_shell.repos_path, old_dir, "#{project.path}.git") - new_path = File.join(new_dir_path, "#{project.path}.git") - - if File.exists? new_path - raise ProjectMoveError.new("Destination #{new_path} already exists") - end - - begin - FileUtils.mv( old_path, new_path ) - log_info "Project #{project.name} was moved from #{old_path} to #{new_path}" - true - rescue Exception => e - message = "Project #{project.name} cannot be moved from #{old_path} to #{new_path}" - log_info "Error! #{message} (#{e.message})" - raise ProjectMoveError.new(message) - end - end - - protected - - def log_info message - Gitlab::AppLogger.info message - end - end -end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb new file mode 100644 index 00000000000..94b01e808d9 --- /dev/null +++ b/lib/gitlab/reference_extractor.rb @@ -0,0 +1,59 @@ +module Gitlab + # Extract possible GFM references from an arbitrary String for further processing. + class ReferenceExtractor + attr_accessor :users, :issues, :merge_requests, :snippets, :commits + + include Markdown + + def initialize + @users, @issues, @merge_requests, @snippets, @commits = [], [], [], [], [] + end + + def analyze string + parse_references(string.dup) + end + + # Given a valid project, resolve the extracted identifiers of the requested type to + # model objects. + + def users_for project + users.map do |identifier| + project.users.where(username: identifier).first + end.reject(&:nil?) + end + + def issues_for project + issues.map do |identifier| + project.issues.where(iid: identifier).first + end.reject(&:nil?) + end + + def merge_requests_for project + merge_requests.map do |identifier| + project.merge_requests.where(iid: identifier).first + end.reject(&:nil?) + end + + def snippets_for project + snippets.map do |identifier| + project.snippets.where(id: identifier).first + end.reject(&:nil?) + end + + def commits_for project + repo = project.repository + return [] if repo.nil? + + commits.map do |identifier| + repo.commit(identifier) + end.reject(&:nil?) + end + + private + + def reference_link type, identifier + # Append identifier to the appropriate collection. + send("#{type}s") << identifier + end + end +end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 483042205ea..b4be46d3b42 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -7,7 +7,11 @@ module Gitlab end def project_name_regex - /\A[a-zA-Z][a-zA-Z0-9_\-\. ]*\z/ + /\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z/ + end + + def name_regex + /\A[a-zA-Z0-9_\-\. ]*\z/ end def path_regex @@ -17,7 +21,7 @@ module Gitlab protected def default_regex - /\A[a-zA-Z][a-zA-Z0-9_\-\.]*\z/ + /\A[a-zA-Z0-9][a-zA-Z0-9_\-\.]*\z/ end end end diff --git a/lib/gitlab/satellite/action.rb b/lib/gitlab/satellite/action.rb index 63303ca3de1..5ea6f956765 100644 --- a/lib/gitlab/satellite/action.rb +++ b/lib/gitlab/satellite/action.rb @@ -25,25 +25,31 @@ module Gitlab end end rescue Errno::ENOMEM => ex - Gitlab::GitLogger.error(ex.message) - return false + return handle_exception(ex) rescue Grit::Git::GitTimeout => ex - Gitlab::GitLogger.error(ex.message) - return false + return handle_exception(ex) ensure Gitlab::ShellEnv.reset_env end - # * Clears the satellite - # * Updates the satellite from Gitolite + # * Recreates the satellite # * Sets up Git variables for the user # # Note: use this within #in_locked_and_timed_satellite def prepare_satellite!(repo) project.satellite.clear_and_update! - repo.git.config({}, "user.name", user.name) - repo.git.config({}, "user.email", user.email) + repo.config['user.name'] = user.name + repo.config['user.email'] = user.email + end + + def default_options(options = {}) + {raise: true, timeout: true}.merge(options) + end + + def handle_exception(exception) + Gitlab::GitLogger.error(exception.message) + false end end end diff --git a/lib/gitlab/satellite/edit_file_action.rb b/lib/gitlab/satellite/edit_file_action.rb index e9053f904c0..d793d0ba8dc 100644 --- a/lib/gitlab/satellite/edit_file_action.rb +++ b/lib/gitlab/satellite/edit_file_action.rb @@ -13,7 +13,7 @@ module Gitlab # Updates the files content and creates a new commit for it # # Returns false if the ref has been updated while editing the file - # Returns false if commiting the change fails + # Returns false if committing the change fails # Returns false if pushing from the satellite to Gitolite failed or was rejected # Returns true otherwise def commit!(content, commit_message, last_commit) @@ -49,7 +49,7 @@ module Gitlab protected def can_edit?(last_commit) - current_last_commit = @project.repository.last_commit_for(ref, file_path).sha + current_last_commit = Gitlab::Git::Commit.last_for_path(@project.repository, ref, file_path).sha last_commit == current_last_commit end end diff --git a/lib/gitlab/satellite/merge_action.rb b/lib/gitlab/satellite/merge_action.rb index 832db6621c4..156483be8dd 100644 --- a/lib/gitlab/satellite/merge_action.rb +++ b/lib/gitlab/satellite/merge_action.rb @@ -5,48 +5,120 @@ module Gitlab attr_accessor :merge_request def initialize(user, merge_request) - super user, merge_request.project + super user, merge_request.target_project @merge_request = merge_request end # Checks if a merge request can be executed without user interaction def can_be_merged? in_locked_and_timed_satellite do |merge_repo| + prepare_satellite!(merge_repo) merge_in_satellite!(merge_repo) end end # Merges the source branch into the target branch in the satellite and - # pushes it back to Gitolite. - # It also removes the source branch if requested in the merge request. + # pushes it back to the repository. + # It also removes the source branch if requested in the merge request (and this is permitted by the merge request). # # Returns false if the merge produced conflicts - # Returns false if pushing from the satellite to Gitolite failed or was rejected + # Returns false if pushing from the satellite to the repository failed or was rejected # Returns true otherwise def merge! in_locked_and_timed_satellite do |merge_repo| + prepare_satellite!(merge_repo) if merge_in_satellite!(merge_repo) # push merge back to Gitolite # will raise CommandFailed when push fails - merge_repo.git.push({raise: true, timeout: true}, :origin, merge_request.target_branch) - + merge_repo.git.push(default_options, :origin, merge_request.target_branch) # remove source branch if merge_request.should_remove_source_branch && !project.root_ref?(merge_request.source_branch) # will raise CommandFailed when push fails - merge_repo.git.push({raise: true, timeout: true}, :origin, ":#{merge_request.source_branch}") + merge_repo.git.push(default_options, :origin, ":#{merge_request.source_branch}") end - # merge, push and branch removal successful true end end rescue Grit::Git::CommandFailed => ex - Gitlab::GitLogger.error(ex.message) - false + handle_exception(ex) end - private + # Get a raw diff of the source to the target + def diff_in_satellite + in_locked_and_timed_satellite do |merge_repo| + prepare_satellite!(merge_repo) + update_satellite_source_and_target!(merge_repo) + + if merge_request.for_fork? + diff = merge_repo.git.native(:diff, default_options, "origin/#{merge_request.target_branch}", "source/#{merge_request.source_branch}") + else + diff = merge_repo.git.native(:diff, default_options, "#{merge_request.target_branch}", "#{merge_request.source_branch}") + end + + return diff + end + rescue Grit::Git::CommandFailed => ex + handle_exception(ex) + end + + # Only show what is new in the source branch compared to the target branch, not the other way around. + # The line below with merge_base is equivalent to diff with three dots (git diff branch1...branch2) + # From the git documentation: "git diff A...B" is equivalent to "git diff $(git-merge-base A B) B" + def diffs_between_satellite + in_locked_and_timed_satellite do |merge_repo| + prepare_satellite!(merge_repo) + update_satellite_source_and_target!(merge_repo) + if merge_request.for_fork? + common_commit = merge_repo.git.native(:merge_base, default_options, ["origin/#{merge_request.target_branch}", "source/#{merge_request.source_branch}"]).strip + #this method doesn't take default options + diffs = merge_repo.diff(common_commit, "source/#{merge_request.source_branch}") + else + raise "Attempt to determine diffs between for a non forked merge request in satellite MergeRequest.id:[#{merge_request.id}]" + end + diffs = diffs.map { |diff| Gitlab::Git::Diff.new(diff) } + return diffs + end + rescue Grit::Git::CommandFailed => ex + handle_exception(ex) + end + + # Get commit as an email patch + def format_patch + in_locked_and_timed_satellite do |merge_repo| + prepare_satellite!(merge_repo) + update_satellite_source_and_target!(merge_repo) + + if (merge_request.for_fork?) + patch = merge_repo.git.format_patch(default_options({stdout: true}), "origin/#{merge_request.target_branch}..source/#{merge_request.source_branch}") + else + patch = merge_repo.git.format_patch(default_options({stdout: true}), "#{merge_request.target_branch}..#{merge_request.source_branch}") + end + + return patch + end + rescue Grit::Git::CommandFailed => ex + handle_exception(ex) + end + # Retrieve an array of commits between the source and the target + def commits_between + in_locked_and_timed_satellite do |merge_repo| + prepare_satellite!(merge_repo) + update_satellite_source_and_target!(merge_repo) + if (merge_request.for_fork?) + commits = merge_repo.commits_between("origin/#{merge_request.target_branch}", "source/#{merge_request.source_branch}") + else + raise "Attempt to determine commits between for a non forked merge request in satellite MergeRequest.id:[#{merge_request.id}]" + end + commits = commits.map { |commit| Gitlab::Git::Commit.new(commit, nil) } + return commits + end + rescue Grit::Git::CommandFailed => ex + handle_exception(ex) + end + + private # Merges the source_branch into the target_branch in the satellite. # # Note: it will clear out the satellite before doing anything @@ -54,18 +126,35 @@ module Gitlab # Returns false if the merge produced conflicts # Returns true otherwise def merge_in_satellite!(repo) - prepare_satellite!(repo) - - # create target branch in satellite at the corresponding commit from Gitolite - repo.git.checkout({raise: true, timeout: true, b: true}, merge_request.target_branch, "origin/#{merge_request.target_branch}") + update_satellite_source_and_target!(repo) - # merge the source branch from Gitolite into the satellite + # merge the source branch into the satellite # will raise CommandFailed when merge fails - repo.git.pull({raise: true, timeout: true, no_ff: true}, :origin, merge_request.source_branch) + if merge_request.for_fork? + repo.git.pull(default_options({no_ff: true}), 'source', merge_request.source_branch) + else + repo.git.pull(default_options({no_ff: true}), 'origin', merge_request.source_branch) + end rescue Grit::Git::CommandFailed => ex - Gitlab::GitLogger.error(ex.message) - false + handle_exception(ex) end + + # Assumes a satellite exists that is a fresh clone of the projects repo, prepares satellite for merges, diffs etc + def update_satellite_source_and_target!(repo) + if merge_request.for_fork? + repo.remote_add('source', merge_request.source_project.repository.path_to_repo) + repo.remote_fetch('source') + repo.git.checkout(default_options({b: true}), merge_request.target_branch, "origin/#{merge_request.target_branch}") + else + # We can't trust the input here being branch names, we can't always check it out because it could be a relative ref i.e. HEAD~3 + # we could actually remove the if true, because it should never ever happen (as long as the satellite has been prepared) + repo.git.checkout(default_options, "#{merge_request.source_branch}") + repo.git.checkout(default_options, "#{merge_request.target_branch}") + end + rescue Grit::Git::CommandFailed => ex + handle_exception(ex) + end + end end end diff --git a/lib/gitlab/satellite/satellite.rb b/lib/gitlab/satellite/satellite.rb index e7f7a7673b5..6cb7814fae5 100644 --- a/lib/gitlab/satellite/satellite.rb +++ b/lib/gitlab/satellite/satellite.rb @@ -1,5 +1,5 @@ module Gitlab - class SatelliteNotExistError < StandardError; end + class SatelliteNotExistError < StandardError; end module Satellite class Satellite @@ -24,8 +24,11 @@ module Gitlab def clear_and_update! raise_no_satellite unless exists? - delete_heads! + File.exists? path + @repo = nil clear_working_dir! + delete_heads! + remove_remotes! update_from_source! end @@ -55,16 +58,18 @@ module Gitlab raise_no_satellite unless exists? File.open(lock_file, "w+") do |f| - f.flock(File::LOCK_EX) - - Dir.chdir(path) do - return yield + begin + f.flock File::LOCK_EX + Dir.chdir(path) { return yield } + ensure + f.flock File::LOCK_UN end end end def lock_file - Rails.root.join("tmp", "satellite_#{project.id}.lock") + create_locks_dir unless File.exists?(lock_files_dir) + File.join(lock_files_dir, "satellite_#{project.id}.lock") end def path @@ -99,20 +104,44 @@ module Gitlab if heads.include? PARKING_BRANCH repo.git.checkout({}, PARKING_BRANCH) else - repo.git.checkout({b: true}, PARKING_BRANCH) + repo.git.checkout(default_options({b: true}), PARKING_BRANCH) end # remove the parking branch from the list of heads ... heads.delete(PARKING_BRANCH) # ... and delete all others - heads.each { |head| repo.git.branch({D: true}, head) } + heads.each { |head| repo.git.branch(default_options({D: true}), head) } + end + + # Deletes all remotes except origin + # + # This ensures we have no remote name clashes or issues updating branches when + # working with the satellite. + def remove_remotes! + remotes = repo.git.remote.split(' ') + remotes.delete('origin') + remotes.each { |name| repo.git.remote(default_options,'rm', name)} end # Updates the satellite from Gitolite # # Note: this will only update remote branches (i.e. origin/*) def update_from_source! - repo.git.fetch({timeout: true}, :origin) + repo.git.fetch(default_options, :origin) + end + + def default_options(options = {}) + {raise: true, timeout: true}.merge(options) + end + + # Create directory for storing + # satellites lock files + def create_locks_dir + FileUtils.mkdir_p(lock_files_dir) + end + + def lock_files_dir + @lock_files_dir ||= File.join(Gitlab.config.satellites.path, "tmp") end end end diff --git a/lib/gitlab/theme.rb b/lib/gitlab/theme.rb index 7f833867e39..89604162304 100644 --- a/lib/gitlab/theme.rb +++ b/lib/gitlab/theme.rb @@ -1,12 +1,18 @@ module Gitlab class Theme + BASIC = 1 + MARS = 2 + MODERN = 3 + GRAY = 4 + COLOR = 5 + def self.css_class_by_id(id) themes = { - 1 => "ui_basic", - 2 => "ui_mars", - 3 => "ui_modern", - 4 => "ui_gray", - 5 => "ui_color" + BASIC => "ui_basic", + MARS => "ui_mars", + MODERN => "ui_modern", + GRAY => "ui_gray", + COLOR => "ui_color" } id ||= 1 diff --git a/lib/gitlab/user_team_manager.rb b/lib/gitlab/user_team_manager.rb deleted file mode 100644 index a8ff4a3d94d..00000000000 --- a/lib/gitlab/user_team_manager.rb +++ /dev/null @@ -1,135 +0,0 @@ -# UserTeamManager class -# -# Used for manage User teams with project repositories -module Gitlab - class UserTeamManager - class << self - def assign(team, project, access) - project = Project.find(project) unless project.is_a? Project - searched_project = team.user_team_project_relationships.find_by_project_id(project.id) - - unless searched_project.present? - team.user_team_project_relationships.create(project_id: project.id, greatest_access: access) - update_team_users_access_in_project(team, project) - end - end - - def resign(team, project) - project = Project.find(project) unless project.is_a? Project - - team.user_team_project_relationships.with_project(project).destroy_all - - update_team_users_access_in_project(team, project) - end - - def update_team_user_membership(team, member, options) - updates = {} - - if options[:default_projects_access] && options[:default_projects_access] != team.default_projects_access(member) - updates[:permission] = options[:default_projects_access] - end - - if options[:group_admin].to_s != team.admin?(member).to_s - updates[:group_admin] = options[:group_admin].present? - end - - unless updates.blank? - user_team_relationship = team.user_team_user_relationships.find_by_user_id(member) - if user_team_relationship.update_attributes(updates) - if updates[:permission] - rebuild_project_permissions_to_member(team, member) - end - true - else - false - end - else - true - end - end - - def update_project_greates_access(team, project, permission) - project_relation = team.user_team_project_relationships.find_by_project_id(project) - if permission != team.max_project_access(project) - if project_relation.update_attributes(greatest_access: permission) - update_team_users_access_in_project(team, project) - true - else - false - end - else - true - end - end - - def rebuild_project_permissions_to_member(team, member) - team.projects.each do |project| - update_team_user_access_in_project(team, member, project) - end - end - - def update_team_users_access_in_project(team, project) - members = team.members - members.each do |member| - update_team_user_access_in_project(team, member, project) - end - end - - def update_team_user_access_in_project(team, user, project) - granted_access = max_teams_member_permission_in_project(user, project) - - project_team_user = UsersProject.find_by_user_id_and_project_id(user.id, project.id) - project_team_user.destroy if project_team_user.present? - - # project_team_user.project_access != granted_access - project.team << [user, granted_access] if granted_access > 0 - end - - def max_teams_member_permission_in_project(user, project, teams = nil) - result_access = 0 - - user_teams = project.user_teams.with_member(user) - - teams ||= user_teams - - if teams.any? - teams.each do |team| - granted_access = max_team_member_permission_in_project(team, user, project) - result_access = [granted_access, result_access].max - end - end - result_access - end - - def max_team_member_permission_in_project(team, user, project) - member_access = team.default_projects_access(user) - team_access = team.user_team_project_relationships.find_by_project_id(project.id).greatest_access - - [team_access, member_access].min - end - - def add_member_into_team(team, user, access, admin) - user = User.find(user) unless user.is_a? User - - team.user_team_user_relationships.create(user_id: user.id, permission: access, group_admin: admin) - team.projects.each do |project| - update_team_user_access_in_project(team, user, project) - end - end - - def remove_member_from_team(team, user) - user = User.find(user) unless user.is_a? User - - team.user_team_user_relationships.with_user(user).destroy_all - other_teams = [] - team.projects.each do |project| - other_teams << project.user_teams.with_member(user) - end - other_teams.uniq - unless other_teams.any? - UsersProject.in_projects(team.projects).with_user(user).destroy_all - end - end - end - end -end diff --git a/lib/gitlab/version_info.rb b/lib/gitlab/version_info.rb new file mode 100644 index 00000000000..6ee41e85cc9 --- /dev/null +++ b/lib/gitlab/version_info.rb @@ -0,0 +1,54 @@ +module Gitlab + class VersionInfo + include Comparable + + attr_reader :major, :minor, :patch + + def self.parse(str) + if str && m = str.match(/(\d+)\.(\d+)\.(\d+)/) + VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i) + else + VersionInfo.new + end + end + + def initialize(major = 0, minor = 0, patch = 0) + @major = major + @minor = minor + @patch = patch + end + + def <=>(other) + return unless other.is_a? VersionInfo + return unless valid? && other.valid? + + if other.major < @major + 1 + elsif @major < other.major + -1 + elsif other.minor < @minor + 1 + elsif @minor < other.minor + -1 + elsif other.patch < @patch + 1 + elsif @patch < other.patch + -1 + else + 0 + end + end + + def to_s + if valid? + "%d.%d.%d" % [@major, @minor, @patch] + else + "Unknown" + end + end + + def valid? + @major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0 + end + end +end diff --git a/lib/gitolited.rb b/lib/gitolited.rb deleted file mode 100644 index a7fc4148106..00000000000 --- a/lib/gitolited.rb +++ /dev/null @@ -1,11 +0,0 @@ -# == Gitolited mixin -# -# Provide a shortcut to Gitlab::Shell instance by gitlab_shell -# -# Used by Project, UsersProject, etc -# -module Gitolited - def gitlab_shell - Gitlab::Shell.new - end -end diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index 4f2c86e2d41..d9c2d3b626d 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -11,7 +11,8 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML def block_code(code, language) options = { options: {encoding: 'utf-8'} } - options.merge!(lexer: language.downcase) if Pygments::Lexer.find(language) + lexer = Pygments::Lexer.find(language) # language can be an alias + options.merge!(lexer: lexer.aliases[0].downcase) if lexer # downcase is required # New lines are placed to fix an rendering issue # with code wrapped inside <h1> tag for next case: diff --git a/lib/support/deploy/deploy.sh b/lib/support/deploy/deploy.sh new file mode 100755 index 00000000000..0d2f8418bcf --- /dev/null +++ b/lib/support/deploy/deploy.sh @@ -0,0 +1,44 @@ +# This is deploy script we use to update staging server +# You can always modify it for your needs :) + +# If any command return non-zero status - stop deploy +set -e + +echo 'Deploy: Stoping sidekiq..' +cd /home/git/gitlab/ && sudo -u git -H bundle exec rake sidekiq:stop RAILS_ENV=production + +echo 'Deploy: Show deploy index page' +sudo -u git -H cp /home/git/gitlab/public/deploy.html /home/git/gitlab/public/index.html + +echo 'Deploy: Starting backup...' +cd /home/git/gitlab/ && sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production + +echo 'Deploy: Stop GitLab server' +sudo service gitlab stop + +echo 'Deploy: Get latest code' +cd /home/git/gitlab/ + +# clean working directory +sudo -u git -H git stash + +# change branch to +sudo -u git -H git pull origin master + +echo 'Deploy: Bundle and migrate' + +# change it to your needs +sudo -u git -H bundle --without postgres + +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# return stashed changes (if necessary) +# sudo -u git -H git stash pop + + +echo 'Deploy: Starting GitLab server...' +sudo service gitlab start + +sleep 10 +sudo -u git -H rm /home/git/gitlab/public/index.html +echo 'Deploy: Done' diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab new file mode 100755 index 00000000000..0248284f8d5 --- /dev/null +++ b/lib/support/init.d/gitlab @@ -0,0 +1,262 @@ +#! /bin/sh + +# GITLAB +# Maintainer: @randx +# Authors: rovanion.luckey@gmail.com, @randx +# App Version: 6.0 + +### BEGIN INIT INFO +# Provides: gitlab +# Required-Start: $local_fs $remote_fs $network $syslog redis-server +# Required-Stop: $local_fs $remote_fs $network $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: GitLab git repository management +# Description: GitLab git repository management +### END INIT INFO + +### Environment variables +RAILS_ENV="production" + +# Script variable names should be lower-case not to conflict with internal +# /bin/sh variables such as PATH, EDITOR or SHELL. +app_root="/home/git/gitlab" +app_user="git" +unicorn_conf="$app_root/config/unicorn.rb" +pid_path="$app_root/tmp/pids" +socket_path="$app_root/tmp/sockets" +web_server_pid_path="$pid_path/unicorn.pid" +sidekiq_pid_path="$pid_path/sidekiq.pid" + + + +### Here ends user configuration ### + + +# Switch to the app_user if it is not he/she who is running the script. +if [ "$USER" != "$app_user" ]; then + sudo -u "$app_user" -H -i $0 "$@"; exit; +fi + +# Switch to the gitlab path, if it fails exit with an error. +if ! cd "$app_root" ; then + echo "Failed to cd into $app_root, exiting!"; exit 1 +fi + +### Init Script functions + +check_pids(){ + if ! mkdir -p "$pid_path"; then + echo "Could not create the path $pid_path needed to store the pids." + exit 1 + fi + # If there exists a file which should hold the value of the Unicorn pid: read it. + if [ -f "$web_server_pid_path" ]; then + wpid=$(cat "$web_server_pid_path") + else + wpid=0 + fi + if [ -f "$sidekiq_pid_path" ]; then + spid=$(cat "$sidekiq_pid_path") + else + spid=0 + fi +} + +# We use the pids in so many parts of the script it makes sense to always check them. +# Only after start() is run should the pids change. Sidekiq sets it's own pid. +check_pids + + +# Checks whether the different parts of the service are already running or not. +check_status(){ + check_pids + # If the web server is running kill -0 $wpid returns true, or rather 0. + # Checks of *_status should only check for == 0 or != 0, never anything else. + if [ $wpid -ne 0 ]; then + kill -0 "$wpid" 2>/dev/null + web_status="$?" + else + web_status="-1" + fi + if [ $spid -ne 0 ]; then + kill -0 "$spid" 2>/dev/null + sidekiq_status="$?" + else + sidekiq_status="-1" + fi +} + +# Check for stale pids and remove them if necessary +check_stale_pids(){ + check_status + # If there is a pid it is something else than 0, the service is running if + # *_status is == 0. + if [ "$wpid" != "0" -a "$web_status" != "0" ]; then + echo "Removing stale Unicorn web server pid. This is most likely caused by the web server crashing the last time it ran." + if ! rm "$web_server_pid_path"; then + echo "Unable to remove stale pid, exiting" + exit 1 + fi + fi + if [ "$spid" != "0" -a "$sidekiq_status" != "0" ]; then + echo "Removing stale Sidekiq web server pid. This is most likely caused by the Sidekiq crashing the last time it ran." + if ! rm "$sidekiq_pid_path"; then + echo "Unable to remove stale pid, exiting" + exit 1 + fi + fi +} + +# If no parts of the service is running, bail out. +exit_if_not_running(){ + check_stale_pids + if [ "$web_status" != "0" -a "$sidekiq_status" != "0" ]; then + echo "GitLab is not running." + exit + fi +} + +# Starts Unicorn and Sidekiq. +start() { + check_stale_pids + + # Then check if the service is running. If it is: don't start again. + if [ "$web_status" = "0" ]; then + echo "The Unicorn web server already running with pid $wpid, not restarting." + else + echo "Starting the GitLab Unicorn web server..." + # Remove old socket if it exists + rm -f "$socket_path"/gitlab.socket 2>/dev/null + # Start the webserver + bundle exec unicorn_rails -D -c "$unicorn_conf" -E "$RAILS_ENV" + fi + + # If sidekiq is already running, don't start it again. + if [ "$sidekiq_status" = "0" ]; then + echo "The Sidekiq job dispatcher is already running with pid $spid, not restarting" + else + echo "Starting the GitLab Sidekiq event dispatcher..." + RAILS_ENV=$RAILS_ENV bundle exec rake sidekiq:start + # We are sleeping a bit here because sidekiq is slow at writing it's pid + sleep 2 + fi + + # Finally check the status to tell wether or not GitLab is running + status +} + +# Asks the Unicorn and the Sidekiq if they would be so kind as to stop, if not kills them. +stop() { + exit_if_not_running + # If the Unicorn web server is running, tell it to stop; + if [ "$web_status" = "0" ]; then + kill -QUIT "$wpid" & + echo "Stopping the GitLab Unicorn web server..." + stopping=true + else + echo "The Unicorn web was not running, doing nothing." + fi + # And do the same thing for the Sidekiq. + if [ "$sidekiq_status" = "0" ]; then + printf "Stopping Sidekiq job dispatcher." + RAILS_ENV=$RAILS_ENV bundle exec rake sidekiq:stop & + stopping=true + else + echo "The Sidekiq was not running, must have run out of breath." + fi + + + # If something needs to be stopped, lets wait for it to stop. Never use SIGKILL in a script. + while [ "$stopping" = "true" ]; do + sleep 1 + check_status + if [ "$web_status" = "0" -o "$sidekiq_status" = "0" ]; then + printf "." + else + printf "\n" + break + fi + done + sleep 1 + # Cleaning up unused pids + rm "$web_server_pid_path" 2>/dev/null + # rm "$sidekiq_pid_path" # Sidekiq seems to be cleaning up it's own pid. + + status +} + +# Returns the status of GitLab and it's components +status() { + check_status + if [ "$web_status" != "0" -a "$sidekiq_status" != "0" ]; then + echo "GitLab is not running." + return + fi + if [ "$web_status" = "0" ]; then + echo "The GitLab Unicorn webserver with pid $wpid is running." + else + printf "The GitLab Unicorn webserver is \033[31mnot running\033[0m.\n" + fi + if [ "$sidekiq_status" = "0" ]; then + echo "The GitLab Sidekiq job dispatcher with pid $spid is running." + else + printf "The GitLab Sidekiq job dispatcher is \033[31mnot running\033[0m.\n" + fi + if [ "$web_status" = "0" -a "$sidekiq_status" = "0" ]; then + printf "GitLab and all its components are \033[32mup and running\033[0m.\n" + fi +} + +reload(){ + exit_if_not_running + if [ "$wpid" = "0" ];then + echo "The GitLab Unicorn Web server is not running thus its configuration can't be reloaded." + exit 1 + fi + printf "Reloading GitLab Unicorn configuration... " + kill -USR2 "$wpid" + echo "Done." + echo "Restarting GitLab Sidekiq since it isn't capable of reloading its config..." + RAILS_ENV=$RAILS_ENV bundle exec rake sidekiq:stop + echo "Starting Sidekiq..." + RAILS_ENV=$RAILS_ENV bundle exec rake sidekiq:start + # Waiting 2 seconds for sidekiq to write it. + sleep 2 + status +} + +restart(){ + check_status + if [ "$web_status" = "0" -o "$sidekiq_status" = "0" ]; then + stop + fi + start +} + + +## Finally the input handling. + +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + restart + ;; + reload|force-reload) + reload + ;; + status) + status + ;; + *) + echo "Usage: service gitlab {start|stop|restart|reload|status}" + exit 1 + ;; +esac + +exit diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab new file mode 100644 index 00000000000..3e929c52990 --- /dev/null +++ b/lib/support/nginx/gitlab @@ -0,0 +1,39 @@ +# GITLAB +# Maintainer: @randx +# App Version: 5.0 + +upstream gitlab { + server unix:/home/git/gitlab/tmp/sockets/gitlab.socket; +} + +server { + listen *:80 default_server; # e.g., listen 192.168.1.1:80; In most cases *:80 is a good idea + server_name YOUR_SERVER_FQDN; # e.g., server_name source.example.com; + server_tokens off; # don't show the version number, a security best practice + root /home/git/gitlab/public; + + # individual nginx logs for this gitlab vhost + access_log /var/log/nginx/gitlab_access.log; + error_log /var/log/nginx/gitlab_error.log; + + location / { + # serve static files from defined root folder;. + # @gitlab is a named location for the upstream fallback, see below + try_files $uri $uri/index.html $uri.html @gitlab; + } + + # if a file, which is not found in the root folder is requested, + # then the proxy pass the request to the upsteam (gitlab unicorn) + location @gitlab { + proxy_read_timeout 300; # https://github.com/gitlabhq/gitlabhq/issues/694 + proxy_connect_timeout 300; # https://github.com/gitlabhq/gitlabhq/issues/694 + proxy_redirect off; + + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + + proxy_pass http://gitlab; + } +} + diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake new file mode 100644 index 00000000000..8320b9b2576 --- /dev/null +++ b/lib/tasks/cache.rake @@ -0,0 +1,6 @@ +namespace :cache do + desc "GITLAB | Clear redis cache" + task :clear => :environment do + Rails.cache.clear + end +end diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake new file mode 100644 index 00000000000..7d3602211c1 --- /dev/null +++ b/lib/tasks/dev.rake @@ -0,0 +1,10 @@ +namespace :dev do + desc "GITLAB | Setup developer environment (db, fixtures)" + task :setup => :environment do + ENV['force'] = 'yes' + Rake::Task["db:setup"].invoke + Rake::Task["db:seed_fu"].invoke + Rake::Task["gitlab:shell:setup"].invoke + end +end + diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 214ce720e7a..2eff1260b61 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -4,210 +4,75 @@ namespace :gitlab do namespace :backup do # Create backup of GitLab system desc "GITLAB | Create a backup of the GitLab system" - task :create => :environment do + task create: :environment do warn_user_is_not_gitlab Rake::Task["gitlab:backup:db:create"].invoke Rake::Task["gitlab:backup:repo:create"].invoke + Rake::Task["gitlab:backup:uploads:create"].invoke - Dir.chdir(Gitlab.config.backup.path) - - # saving additional informations - s = {} - s[:db_version] = "#{ActiveRecord::Migrator.current_version}" - s[:backup_created_at] = "#{Time.now}" - s[:gitlab_version] = %x{git rev-parse HEAD}.gsub(/\n/,"") - s[:tar_version] = %x{tar --version | head -1}.gsub(/\n/,"") - - File.open("#{Gitlab.config.backup.path}/backup_information.yml", "w+") do |file| - file << s.to_yaml.gsub(/^---\n/,'') - end - - # create archive - print "Creating backup archive: #{Time.now.to_i}_gitlab_backup.tar ... " - if Kernel.system("tar -cf #{Time.now.to_i}_gitlab_backup.tar repositories/ db/ backup_information.yml") - puts "done".green - else - puts "failed".red - end - - # cleanup: remove tmp files - print "Deleting tmp directories ... " - if Kernel.system("rm -rf repositories/ db/ backup_information.yml") - puts "done".green - else - puts "failed".red - end - - # delete backups - print "Deleting old backups ... " - if Gitlab.config.backup.keep_time > 0 - file_list = Dir.glob("*_gitlab_backup.tar").map { |f| f.split(/_/).first.to_i } - file_list.sort.each do |timestamp| - if Time.at(timestamp) < (Time.now - Gitlab.config.backup.keep_time) - %x{rm #{timestamp}_gitlab_backup.tar} - end - end - puts "done".green - else - puts "skipping".yellow - end + backup = Backup::Manager.new + backup.pack + backup.cleanup + backup.remove_old end # Restore backup of GitLab system desc "GITLAB | Restore a previously created backup" - task :restore => :environment do + task restore: :environment do warn_user_is_not_gitlab - Dir.chdir(Gitlab.config.backup.path) - - # check for existing backups in the backup dir - file_list = Dir.glob("*_gitlab_backup.tar").each.map { |f| f.split(/_/).first.to_i } - puts "no backups found" if file_list.count == 0 - if file_list.count > 1 && ENV["BACKUP"].nil? - puts "Found more than one backup, please specify which one you want to restore:" - puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup" - exit 1 - end - - tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_backup.tar") - - unless File.exists?(tar_file) - puts "The specified backup doesn't exist!" - exit 1 - end - - print "Unpacking backup ... " - unless Kernel.system("tar -xf #{tar_file}") - puts "failed".red - exit 1 - else - puts "done".green - end - - settings = YAML.load_file("backup_information.yml") - ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 - - # restoring mismatching backups can lead to unexpected problems - if settings[:gitlab_version] != %x{git rev-parse HEAD}.gsub(/\n/,"") - puts "GitLab version mismatch:".red - puts " Your current HEAD differs from the HEAD in the backup!".red - puts " Please switch to the following revision and try again:".red - puts " revision: #{settings[:gitlab_version]}".red - exit 1 - end + backup = Backup::Manager.new + backup.unpack Rake::Task["gitlab:backup:db:restore"].invoke Rake::Task["gitlab:backup:repo:restore"].invoke + Rake::Task["gitlab:backup:uploads:restore"].invoke + Rake::Task["gitlab:shell:setup"].invoke - # cleanup: remove tmp files - print "Deleting tmp directories ... " - if Kernel.system("rm -rf repositories/ db/ backup_information.yml") - puts "done".green - else - puts "failed".red - end + backup.cleanup end - ################################################################################ - ################################# invoked tasks ################################ - - ################################# REPOSITORIES ################################# - namespace :repo do - task :create => :environment do - backup_path_repo = File.join(Gitlab.config.backup.path, "repositories") - FileUtils.mkdir_p(backup_path_repo) until Dir.exists?(backup_path_repo) + task create: :environment do puts "Dumping repositories ...".blue - - Project.find_each(:batch_size => 1000) do |project| - print " * #{project.path_with_namespace} ... " - - if project.empty_repo? - puts "[SKIPPED]".cyan - next - end - - # Create namespace dir if missing - FileUtils.mkdir_p(File.join(backup_path_repo, project.namespace.path)) if project.namespace - - # Build a destination path for backup - path_to_bundle = File.join(backup_path_repo, project.path_with_namespace + ".bundle") - - if Kernel.system("cd #{project.repository.path_to_repo} > /dev/null 2>&1 && git bundle create #{path_to_bundle} --all > /dev/null 2>&1") - puts "[DONE]".green - else - puts "[FAILED]".red - end - end + Backup::Repository.new.dump + puts "done".green end - task :restore => :environment do - backup_path_repo = File.join(Gitlab.config.backup.path, "repositories") - repos_path = Gitlab.config.gitlab_shell.repos_path - - puts "Restoring repositories ... " - - Project.find_each(:batch_size => 1000) do |project| - print "#{project.path_with_namespace} ... " - - if project.namespace - project.namespace.ensure_dir_exist - end - - # Build a backup path - path_to_bundle = File.join(backup_path_repo, project.path_with_namespace + ".bundle") - - if Kernel.system("git clone --bare #{path_to_bundle} #{project.repository.path_to_repo} > /dev/null 2>&1") - puts "[DONE]".green - else - puts "[FAILED]".red - end - end + task restore: :environment do + puts "Restoring repositories ...".blue + Backup::Repository.new.restore + puts "done".green end end - ###################################### DB ###################################### - namespace :db do - task :create => :environment do - backup_path_db = File.join(Gitlab.config.backup.path, "db") - FileUtils.mkdir_p(backup_path_db) unless Dir.exists?(backup_path_db) - - puts "Dumping database tables ... ".blue - ActiveRecord::Base.connection.tables.each do |tbl| - print " * #{tbl.yellow} ... " - count = 1 - File.open(File.join(backup_path_db, tbl + ".yml"), "w+") do |file| - ActiveRecord::Base.connection.select_all("SELECT * FROM `#{tbl}`").each do |line| - line.delete_if{|k,v| v.blank?} - output = {tbl + '_' + count.to_s => line} - file << output.to_yaml.gsub(/^---\n/,'') + "\n" - count += 1 - end - puts "done".green - end - end + task create: :environment do + puts "Dumping database ... ".blue + Backup::Database.new.dump + puts "done".green end - task :restore => :environment do - backup_path_db = File.join(Gitlab.config.backup.path, "db") + task restore: :environment do + puts "Restoring database ... ".blue + Backup::Database.new.restore + puts "done".green + end + end - puts "Restoring database tables (loading fixtures) ... " - Rake::Task["db:reset"].invoke + namespace :uploads do + task create: :environment do + puts "Dumping uploads ... ".blue + Backup::Uploads.new.dump + puts "done".green + end - Dir.glob(File.join(backup_path_db, "*.yml") ).each do |dir| - fixture_file = File.basename(dir, ".*" ) - print "#{fixture_file.yellow} ... " - if File.size(dir) > 0 - ActiveRecord::Fixtures.create_fixtures(backup_path_db, fixture_file) - puts "done".green - else - puts "skipping".yellow - end - end + task restore: :environment do + puts "Restoring uploads ... ".blue + Backup::Uploads.new.restore + puts "done".green end end - end # namespace end: backup end # namespace end: gitlab diff --git a/lib/tasks/gitlab/bulk_add_permission.rake b/lib/tasks/gitlab/bulk_add_permission.rake index eb1a7559dbd..c270232edba 100644 --- a/lib/tasks/gitlab/bulk_add_permission.rake +++ b/lib/tasks/gitlab/bulk_add_permission.rake @@ -1,9 +1,9 @@ namespace :gitlab do namespace :import do desc "GITLAB | Add all users to all projects (admin users are added as masters)" - task :all_users_to_all_projects => :environment do |t, args| - user_ids = User.where(:admin => false).pluck(:id) - admin_ids = User.where(:admin => true).pluck(:id) + task all_users_to_all_projects: :environment do |t, args| + user_ids = User.where(admin: false).pluck(:id) + admin_ids = User.where(admin: true).pluck(:id) projects_ids = Project.pluck(:id) puts "Importing #{user_ids.size} users into #{projects_ids.size} projects" @@ -21,4 +21,4 @@ namespace :gitlab do UsersProject.add_users_into_projects(project_ids, Array.wrap(user.id), UsersProject::DEVELOPER) end end -end
\ No newline at end of file +end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 6a138396087..6e2a59f62ac 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -22,7 +22,10 @@ namespace :gitlab do check_tmp_writable check_init_script_exists check_init_script_up_to_date + check_projects_have_namespace check_satellites_exist + check_redis_version + check_git_version finished_checking "GitLab" end @@ -136,13 +139,15 @@ namespace :gitlab do def check_init_script_up_to_date print "Init script up-to-date? ... " + recipe_path = Rails.root.join("lib/support/init.d/", "gitlab") script_path = "/etc/init.d/gitlab" + unless File.exists?(script_path) puts "can't check because of previous errors".magenta return end - recipe_content = `curl https://raw.github.com/gitlabhq/gitlab-recipes/master/init.d/gitlab 2>/dev/null` + recipe_content = File.read(recipe_path) script_content = File.read(script_path) if recipe_content == script_content @@ -217,7 +222,7 @@ namespace :gitlab do puts "no".red try_fixing_it( "sudo chown -R gitlab #{log_path}", - "sudo chmod -R rwX #{log_path}" + "sudo chmod -R u+rwX #{log_path}" ) for_more_information( see_installation_guide_section "GitLab" @@ -237,7 +242,7 @@ namespace :gitlab do puts "no".red try_fixing_it( "sudo chown -R gitlab #{tmp_path}", - "sudo chmod -R rwX #{tmp_path}" + "sudo chmod -R u+rwX #{tmp_path}" ) for_more_information( see_installation_guide_section "GitLab" @@ -245,6 +250,23 @@ namespace :gitlab do fix_and_rerun end end + + def check_redis_version + print "Redis version >= 2.0.0? ... " + + if run_and_match("redis-cli --version", /redis-cli 2.\d.\d/) + puts "yes".green + else + puts "no".red + try_fixing_it( + "Update your redis server to a version >= 2.0.0" + ) + for_more_information( + "gitlab-public-wiki/wiki/Trouble-Shooting-Guide in section sidekiq" + ) + fix_and_rerun + end + end end @@ -255,7 +277,6 @@ namespace :gitlab do warn_user_is_not_gitlab start_checking "Environment" - check_issue_1059_shell_profile_error check_gitlab_git_config check_python2_exists check_python2_version @@ -294,30 +315,6 @@ namespace :gitlab do end end - # see https://github.com/gitlabhq/gitlabhq/issues/1059 - def check_issue_1059_shell_profile_error - gitlab_shell_ssh_user = Gitlab.config.gitlab_shell.ssh_user - print "Has no \"-e\" in ~#{gitlab_shell_ssh_user}/.profile ... " - - profile_file = File.join(gitlab_shell_user_home, ".profile") - - unless File.read(profile_file) =~ /^-e PATH/ - puts "yes".green - else - puts "no".red - try_fixing_it( - "Open #{profile_file}", - "Find the line starting with \"-e PATH\"", - "Remove \"-e \" so the line starts with PATH" - ) - for_more_information( - see_installation_guide_section("Gitlab Shell"), - "https://github.com/gitlabhq/gitlabhq/issues/1059" - ) - fix_and_rerun - end - end - def check_python2_exists print "Has python2? ... " @@ -368,19 +365,21 @@ namespace :gitlab do namespace :gitlab_shell do - desc "GITLAB | Check the configuration of Gitlab Shell" + desc "GITLAB | Check the configuration of GitLab Shell" task check: :environment do warn_user_is_not_gitlab - start_checking "Gitlab Shell" + start_checking "GitLab Shell" + check_gitlab_shell check_repo_base_exists check_repo_base_is_not_symlink check_repo_base_user_and_group check_repo_base_permissions - check_post_receive_hook_is_up_to_date - check_repos_post_receive_hooks_is_link + check_update_hook_is_up_to_date + check_repos_update_hooks_is_link + check_gitlab_shell_self_test - finished_checking "Gitlab Shell" + finished_checking "GitLab Shell" end @@ -388,10 +387,10 @@ namespace :gitlab do ######################## - def check_post_receive_hook_is_up_to_date - print "post-receive hook up-to-date? ... " + def check_update_hook_is_up_to_date + print "update hook up-to-date? ... " - hook_file = "post-receive" + hook_file = "update" gitlab_shell_hooks_path = Gitlab.config.gitlab_shell.hooks_path gitlab_shell_hook_file = File.join(gitlab_shell_hooks_path, hook_file) gitlab_shell_ssh_user = Gitlab.config.gitlab_shell.ssh_user @@ -415,12 +414,12 @@ namespace :gitlab do puts "no".red puts "#{repo_base_path} is missing".red try_fixing_it( - "This should have been created when setting up Gitlab Shell.", + "This should have been created when setting up GitLab Shell.", "Make sure it's set correctly in config/gitlab.yml", - "Make sure Gitlab Shell is installed correctly." + "Make sure GitLab Shell is installed correctly." ) for_more_information( - see_installation_guide_section "Gitlab Shell" + see_installation_guide_section "GitLab Shell" ) fix_and_rerun end @@ -465,7 +464,7 @@ namespace :gitlab do "find #{repo_base_path} -type d -print0 | sudo xargs -0 chmod g+s" ) for_more_information( - see_installation_guide_section "Gitlab Shell" + see_installation_guide_section "GitLab Shell" ) fix_and_rerun end @@ -482,25 +481,27 @@ namespace :gitlab do return end - if File.stat(repo_base_path).uid == uid_for(gitlab_shell_ssh_user) && - File.stat(repo_base_path).gid == gid_for(gitlab_shell_owner_group) + uid = uid_for(gitlab_shell_ssh_user) + gid = gid_for(gitlab_shell_owner_group) + if File.stat(repo_base_path).uid == uid && File.stat(repo_base_path).gid == gid puts "yes".green else puts "no".red + puts " User id for #{gitlab_shell_ssh_user}: #{uid}. Groupd id for #{gitlab_shell_owner_group}: #{gid}".blue try_fixing_it( "sudo chown -R #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group} #{repo_base_path}" ) for_more_information( - see_installation_guide_section "Gitlab Shell" + see_installation_guide_section "GitLab Shell" ) fix_and_rerun end end - def check_repos_post_receive_hooks_is_link - print "post-receive hooks in repos are links: ... " + def check_repos_update_hooks_is_link + print "update hooks in repos are links: ... " - hook_file = "post-receive" + hook_file = "update" gitlab_shell_hooks_path = Gitlab.config.gitlab_shell.hooks_path gitlab_shell_hook_file = File.join(gitlab_shell_hooks_path, hook_file) gitlab_shell_ssh_user = Gitlab.config.gitlab_shell.ssh_user @@ -540,7 +541,7 @@ namespace :gitlab do File.realpath(project_hook_file) == File.realpath(gitlab_shell_hook_file) puts "ok".green else - puts "not a link to Gitlab Shell's hook".red + puts "not a link to GitLab Shell's hook".red try_fixing_it( "sudo -u #{gitlab_shell_ssh_user} ln -sf #{gitlab_shell_hook_file} #{project_hook_file}" ) @@ -553,6 +554,49 @@ namespace :gitlab do end end + def check_gitlab_shell_self_test + gitlab_shell_repo_base = File.expand_path('gitlab-shell', gitlab_shell_user_home) + check_cmd = File.expand_path('bin/check', gitlab_shell_repo_base) + puts "Running #{check_cmd}" + if system(check_cmd, chdir: gitlab_shell_repo_base) + puts 'gitlab-shell self-check successful'.green + else + puts 'gitlab-shell self-check failed'.red + try_fixing_it( + 'Make sure GitLab is running;', + 'Check the gitlab-shell configuration file:', + sudo_gitlab("editor #{File.expand_path('config.yml', gitlab_shell_repo_base)}") + ) + fix_and_rerun + end + end + + def check_projects_have_namespace + print "projects have namespace: ... " + + unless Project.count > 0 + puts "can't check, you have no projects".magenta + return + end + puts "" + + Project.find_each(batch_size: 100) do |project| + print "#{project.name_with_namespace.yellow} ... " + + if project.namespace + puts "yes".green + else + puts "no".red + try_fixing_it( + "Migrate global projects" + ) + for_more_information( + "doc/update/5.4-to-6.0.md in section \"#global-projects\"" + ) + fix_and_rerun + end + end + end # Helper methods ######################## @@ -582,6 +626,7 @@ namespace :gitlab do start_checking "Sidekiq" check_sidekiq_running + only_one_sidekiq_running finished_checking "Sidekiq" end @@ -593,7 +638,7 @@ namespace :gitlab do def check_sidekiq_running print "Running? ... " - if run_and_match("ps aux | grep -i sidekiq", /sidekiq \d\.\d\.\d.+$/) + if sidekiq_process_match puts "yes".green else puts "no".red @@ -607,6 +652,29 @@ namespace :gitlab do fix_and_rerun end end + + def only_one_sidekiq_running + sidekiq_match = sidekiq_process_match + return unless sidekiq_match + + print 'Number of Sidekiq processes ... ' + if sidekiq_match.length == 1 + puts '1'.green + else + puts "#{sidekiq_match.length}".red + try_fixing_it( + 'sudo service gitlab stop', + 'sudo pkill -f sidekiq', + 'sleep 10 && sudo pkill -9 -f sidekiq', + 'sudo service gitlab start' + ) + fix_and_rerun + end + end + + def sidekiq_process_match + run_and_match("ps ux | grep -i sidekiq", /(sidekiq \d+\.\d+\.\d+.+$)/) + end end @@ -658,4 +726,34 @@ namespace :gitlab do puts " #{step}" end end + + def check_gitlab_shell + required_version = Gitlab::VersionInfo.new(1, 7, 1) + current_version = Gitlab::VersionInfo.parse(gitlab_shell_version) + + print "GitLab Shell version >= #{required_version} ? ... " + if current_version.valid? && required_version <= current_version + puts "OK (#{current_version})".green + else + puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".red + end + end + + def check_git_version + required_version = Gitlab::VersionInfo.new(1, 7, 10) + current_version = Gitlab::VersionInfo.parse(run("#{Gitlab.config.git.bin_path} --version")) + + puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\"" + print "Git version >= #{required_version} ? ... " + + if current_version.valid? && required_version <= current_version + puts "yes (#{current_version})".green + else + puts "no".red + try_fixing_it( + "Update your git to a version >= #{required_version} from #{current_version}" + ) + fix_and_rerun + end + end end diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index d8ee56e5523..4aaab11340f 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -1,7 +1,7 @@ namespace :gitlab do namespace :cleanup do desc "GITLAB | Cleanup | Clean namespaces" - task :dirs => :environment do + task dirs: :environment do warn_user_is_not_gitlab remove_flag = ENV['REMOVE'] @@ -43,8 +43,8 @@ namespace :gitlab do end end - desc "GITLAB | Cleanup | Clean respositories" - task :repos => :environment do + desc "GITLAB | Cleanup | Clean repositories" + task repos: :environment do warn_user_is_not_gitlab remove_flag = ENV['REMOVE'] diff --git a/lib/tasks/gitlab/enable_namespaces.rake b/lib/tasks/gitlab/enable_namespaces.rake index a33639a0013..927748c0fd5 100644 --- a/lib/tasks/gitlab/enable_namespaces.rake +++ b/lib/tasks/gitlab/enable_namespaces.rake @@ -42,7 +42,7 @@ namespace :gitlab do username = user.email.match(/^[^@]*/)[0] username.gsub!("+", ".") - # return username if no mathes + # return username if no matches return username unless User.find_by_username(username) # look for same username @@ -99,7 +99,7 @@ namespace :gitlab do end begin - Gitlab::ProjectMover.new(project, '', group.path).execute + project.transfer(group.path) puts "moved to #{new_path}".green rescue puts "failed moving to #{new_path}".red diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index bddbd7ef855..8fa89270854 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -2,53 +2,76 @@ namespace :gitlab do namespace :import do # How to use: # - # 1. copy your bare repos under git base_path + # 1. copy your bare repos under git repos_path # 2. run bundle exec rake gitlab:import:repos RAILS_ENV=production # # Notes: # * project owner will be a first admin # * existing projects will be skipped # - desc "GITLAB | Import bare repositories from git_host -> base_path into GitLab project instance" - task :repos => :environment do + desc "GITLAB | Import bare repositories from gitlab_shell -> repos_path into GitLab project instance" + task repos: :environment do git_base_path = Gitlab.config.gitlab_shell.repos_path - repos_to_import = Dir.glob(git_base_path + '/*') + repos_to_import = Dir.glob(git_base_path + '/**/*.git') namespaces = Namespace.pluck(:path) repos_to_import.each do |repo_path| - repo_name = File.basename repo_path + # strip repo base path + repo_path[0..git_base_path.length] = '' - # Skip if group or user - next if namespaces.include?(repo_name) + path = repo_path.sub(/\.git$/, '') + name = File.basename path + group_name = File.dirname path + group_name = nil if group_name == '.' - # skip if not git repo - next unless repo_name =~ /.git$/ + # Skip if group or user + next if namespaces.include?(name) - next if repo_name == 'gitolite-admin.git' + puts "Processing #{repo_path}".yellow - path = repo_name.sub(/\.git$/, '') + if path =~ /.wiki\Z/ + puts " * Skipping wiki repo" + next + end project = Project.find_with_namespace(path) - puts "Processing #{repo_name}".yellow - if project - puts " * #{project.name} (#{repo_name}) exists" + puts " * #{project.name} (#{repo_path}) exists" else user = User.admins.first project_params = { - :name => path, + name: name, + path: name } + # find group namespace + if group_name + group = Group.find_by_path(group_name) + # create group namespace + if !group + group = Group.new(:name => group_name) + group.path = group_name + group.owner = user + if group.save + puts " * Created Group #{group.name} (#{group.id})".green + else + puts " * Failed trying to create group #{group.name}".red + end + end + # set project group + project_params[:namespace_id] = group.id + end + project = Projects::CreateContext.new(user, project_params).execute if project.valid? - puts " * Created #{project.name} (#{repo_name})".green + puts " * Created #{project.name} (#{repo_path})".green else - puts " * Failed trying to create #{project.name} (#{repo_name})".red + puts " * Failed trying to create #{project.name} (#{repo_path})".red end end end diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake index c44016ef6e8..ea83efcd887 100644 --- a/lib/tasks/gitlab/info.rake +++ b/lib/tasks/gitlab/info.rake @@ -40,8 +40,8 @@ namespace :gitlab do puts "" puts "GitLab information".yellow - puts "Version:\t#{Gitlab::Version}" - puts "Revision:\t#{Gitlab::Revision}" + puts "Version:\t#{Gitlab::VERSION}" + puts "Revision:\t#{Gitlab::REVISION}" puts "Directory:\t#{Rails.root}" puts "DB Adapter:\t#{database_adapter}" puts "URL:\t\t#{Gitlab.config.gitlab.url}" @@ -54,7 +54,7 @@ namespace :gitlab do # check Gitolite version - gitlab_shell_version_file = "#{Gitlab.config.gitlab_shell.repos_path}/../gitlab-shell/VERSION" + gitlab_shell_version_file = "#{Gitlab.config.gitlab_shell.hooks_path}/../VERSION" if File.readable?(gitlab_shell_version_file) gitlab_shell_version = File.read(gitlab_shell_version_file) end diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake index bc0742564d0..2b730774e06 100644 --- a/lib/tasks/gitlab/setup.rake +++ b/lib/tasks/gitlab/setup.rake @@ -1,16 +1,18 @@ namespace :gitlab do desc "GITLAB | Setup production application" - task :setup => :environment do - setup + task setup: :environment do + setup_db end - def setup + def setup_db warn_user_is_not_gitlab - puts "This will create the necessary database tables and seed the database." - puts "You will lose any previous data stored in the database." - ask_to_continue - puts "" + unless ENV['force'] == 'yes' + puts "This will create the necessary database tables and seed the database." + puts "You will lose any previous data stored in the database." + ask_to_continue + puts "" + end Rake::Task["db:setup"].invoke Rake::Task["db:seed_fu"].invoke diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index 0ab8df1d094..0d7a390bc92 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -25,12 +25,14 @@ namespace :gitlab do def setup warn_user_is_not_gitlab - puts "This will rebuild an authorized_keys file." - puts "You will lose any data stored in /home/git/.ssh/authorized_keys." - ask_to_continue - puts "" + unless ENV['force'] == 'yes' + puts "This will rebuild an authorized_keys file." + puts "You will lose any data stored in authorized_keys file." + ask_to_continue + puts "" + end - system("echo '# Managed by gitlab-shell' > /home/git/.ssh/authorized_keys") + Gitlab::Shell.new.remove_all_keys Key.find_each(batch_size: 1000) do |key| if Gitlab::Shell.new.add_key(key.shell_id, key.key) diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index cb4e34cc0d7..ac2c4577c77 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -16,7 +16,7 @@ namespace :gitlab do # Check which OS is running # # It will primarily use lsb_relase to determine the OS. - # It has fallbacks to Debian, SuSE and OS X. + # It has fallbacks to Debian, SuSE, OS X and systems running systemd. def os_name os_name = run("lsb_release -irs") os_name ||= if File.readable?('/etc/system-release') @@ -32,13 +32,16 @@ namespace :gitlab do os_name ||= if os_x_version = run("sw_vers -productVersion") "Mac OS X #{os_x_version}" end + os_name ||= if File.readable?('/etc/os-release') + File.read('/etc/os-release').match(/PRETTY_NAME=\"(.+)\"/)[1] + end os_name.try(:squish!) end # Prompt the user to input something # # message - the message to display before input - # choices - array of strings of acceptible answers or nil for any answer + # choices - array of strings of acceptable answers or nil for any answer # # Returns the user's answer def prompt(message, choices = nil) @@ -49,10 +52,10 @@ namespace :gitlab do answer end - # Runs the given command and matches the output agains the given pattern + # Runs the given command and matches the output against the given pattern # # Returns nil if nothing matched - # Retunrs the MatchData if the pattern matched + # Returns the MatchData if the pattern matched # # see also #run # see also String#match @@ -77,7 +80,11 @@ namespace :gitlab do end def gid_for(group_name) - Etc.getgrnam(group_name).gid + begin + Etc.getgrnam(group_name).gid + rescue ArgumentError # no group + "group #{group_name} doesn't exist" + end end def warn_user_is_not_gitlab diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake index ad1bfb2e4b3..03b3fc5ea20 100644 --- a/lib/tasks/gitlab/test.rake +++ b/lib/tasks/gitlab/test.rake @@ -1,4 +1,4 @@ namespace :gitlab do desc "GITLAB | Run both spinach and rspec" - task :test => ['spinach', 'spec'] + task test: ['spinach', 'spec'] end diff --git a/lib/tasks/migrate/migrate_iids.rake b/lib/tasks/migrate/migrate_iids.rake new file mode 100644 index 00000000000..33271e1a2bb --- /dev/null +++ b/lib/tasks/migrate/migrate_iids.rake @@ -0,0 +1,48 @@ +desc "GITLAB | Build internal ids for issues and merge requests" +task migrate_iids: :environment do + puts 'Issues'.yellow + Issue.where(iid: nil).find_each(batch_size: 100) do |issue| + begin + issue.set_iid + if issue.update_attribute(:iid, issue.iid) + print '.' + else + print 'F' + end + rescue + print 'F' + end + end + + puts 'done' + puts 'Merge Requests'.yellow + MergeRequest.where(iid: nil).find_each(batch_size: 100) do |mr| + begin + mr.set_iid + if mr.update_attribute(:iid, mr.iid) + print '.' + else + print 'F' + end + rescue => ex + print 'F' + end + end + + puts 'done' + puts 'Milestones'.yellow + Milestone.where(iid: nil).find_each(batch_size: 100) do |m| + begin + m.set_iid + if m.update_attribute(:iid, m.iid) + print '.' + else + print 'F' + end + rescue + print 'F' + end + end + + puts 'done' +end diff --git a/lib/tasks/sidekiq.rake b/lib/tasks/sidekiq.rake index cf99951e027..d0e9dfe46a1 100644 --- a/lib/tasks/sidekiq.rake +++ b/lib/tasks/sidekiq.rake @@ -1,19 +1,19 @@ namespace :sidekiq do desc "GITLAB | Stop sidekiq" task :stop do - run "bundle exec sidekiqctl stop #{pidfile}" + system "bundle exec sidekiqctl stop #{pidfile}" end desc "GITLAB | Start sidekiq" task :start do - run "nohup bundle exec sidekiq -q post_receive,mailer,system_hook,project_web_hook,gitlab_shell,common,default -e #{Rails.env} -P #{pidfile} >> #{Rails.root.join("log", "sidekiq.log")} 2>&1 &" + system "nohup bundle exec sidekiq -q post_receive,mailer,system_hook,project_web_hook,gitlab_shell,common,default -e #{Rails.env} -P #{pidfile} >> #{Rails.root.join("log", "sidekiq.log")} 2>&1 &" end - + desc "GITLAB | Start sidekiq with launchd on Mac OS X" task :launchd do - run "bundle exec sidekiq -q post_receive,mailer,system_hook,project_web_hook,gitlab_shell,common,default -e #{Rails.env} -P #{pidfile} >> #{Rails.root.join("log", "sidekiq.log")} 2>&1" + system "bundle exec sidekiq -q post_receive,mailer,system_hook,project_web_hook,gitlab_shell,common,default -e #{Rails.env} -P #{pidfile} >> #{Rails.root.join("log", "sidekiq.log")} 2>&1" end - + def pidfile Rails.root.join("tmp", "pids", "sidekiq.pid") end diff --git a/lib/tasks/travis.rake b/lib/tasks/travis.rake index 6b434830803..bc1b8aadbc5 100644 --- a/lib/tasks/travis.rake +++ b/lib/tasks/travis.rake @@ -1,5 +1,5 @@ desc "Travis run tests" -task :travis => [ +task travis: [ :spinach, :spec ] diff --git a/public/deploy.html b/public/deploy.html index d8c287809ea..d9c4bb5c583 100644 --- a/public/deploy.html +++ b/public/deploy.html @@ -5,7 +5,7 @@ <link href="/static.css" media="screen" rel="stylesheet" type="text/css" /> </head> <body> - <h1>Deploy in progress</h1> + <h1><center><img src="/gitlab_logo.png"/></center>Deploy in progress</h1> <h3>Please try again in few minutes or contact your administrator.</h3> </body> </html> diff --git a/public/gitlab_logo.png b/public/gitlab_logo.png Binary files differnew file mode 100644 index 00000000000..e3cda5978ab --- /dev/null +++ b/public/gitlab_logo.png diff --git a/script/check b/script/check index d2eb4a2f6d8..c907a98b5d9 100755 --- a/script/check +++ b/script/check @@ -1,2 +1,2 @@ #!/bin/sh -sudo -u gitlab -H bundle exec rake gitlab:check RAILS_ENV=production +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production diff --git a/spec/contexts/filter_context_spec.rb b/spec/contexts/filter_context_spec.rb new file mode 100644 index 00000000000..db27742b9b5 --- /dev/null +++ b/spec/contexts/filter_context_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe FilterContext do + + let(:user) { create :user } + let(:user2) { create :user } + let(:project1) { create(:project, creator_id: user.id) } + let(:project2) { create(:project, creator_id: user.id) } + let(:merge_request1) { create(:merge_request, author_id: user.id, source_project: project1, target_project: project2) } + let(:merge_request2) { create(:merge_request, author_id: user.id, source_project: project2, target_project: project1) } + let(:merge_request3) { create(:merge_request, author_id: user.id, source_project: project2, target_project: project2) } + let(:merge_request4) { create(:merge_request, author_id: user2.id, source_project: project2, target_project: project2, target_branch:"notes_refactoring") } + let(:issue1) { create(:issue, assignee_id: user.id, project: project1) } + let(:issue2) { create(:issue, assignee_id: user.id, project: project2) } + let(:issue3) { create(:issue, assignee_id: user2.id, project: project2) } + + describe 'merge requests' do + before :each do + merge_request1 + merge_request2 + merge_request3 + merge_request4 + end + + it 'should by default filter properly' do + merge_requests = user.cared_merge_requests + params ={} + merge_requests = FilterContext.new(merge_requests, params).execute + merge_requests.size.should == 3 + end + + it 'should apply blocks passed in on creation to the filters' do + merge_requests = user.cared_merge_requests + params = {:project_id => project1.id} + merge_requests = FilterContext.new(merge_requests, params).execute + merge_requests.size.should == 1 + end + end + + describe 'issues' do + before :each do + issue1 + issue2 + issue3 + end + it 'should by default filter projects properly' do + issues = user.assigned_issues + params = {} + issues = FilterContext.new(issues, params).execute + issues.size.should == 2 + end + it 'should apply blocks passed in on creation to the filters' do + issues = user.assigned_issues + params = {:project_id => project1.id} + issues = FilterContext.new(issues, params).execute + issues.size.should == 1 + end + end +end diff --git a/spec/contexts/fork_context_spec.rb b/spec/contexts/fork_context_spec.rb new file mode 100644 index 00000000000..ed51b0c3f8e --- /dev/null +++ b/spec/contexts/fork_context_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe Projects::ForkContext do + describe :fork_by_user do + before do + @from_namespace = create(:namespace) + @from_user = create(:user, namespace: @from_namespace ) + @from_project = create(:project, creator_id: @from_user.id, namespace: @from_namespace) + @to_namespace = create(:namespace) + @to_user = create(:user, namespace: @to_namespace) + end + + context 'fork project' do + + it "successfully creates project in the user namespace" do + @to_project = fork_project(@from_project, @to_user) + + @to_project.owner.should == @to_user + @to_project.namespace.should == @to_user.namespace + end + end + + context 'fork project failure' do + + it "fails due to transaction failure" do + # make the mock gitlab-shell fail + @to_project = fork_project(@from_project, @to_user, false) + + @to_project.errors.should_not be_empty + @to_project.errors[:base].should include("Fork transaction failed.") + end + + end + + context 'project already exists' do + + it "should fail due to validation, not transaction failure" do + @existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) + @to_project = fork_project(@from_project, @to_user) + + @existing_project.persisted?.should be_true + @to_project.errors[:base].should include("Invalid fork destination") + @to_project.errors[:base].should_not include("Fork transaction failed.") + end + + end + end + + def fork_project(from_project, user, fork_success = true) + context = Projects::ForkContext.new(from_project, user) + shell = mock("gitlab_shell") + shell.stub(fork_repository: fork_success) + context.stub(gitlab_shell: shell) + context.execute + end + +end diff --git a/spec/contexts/issues/bulk_update_context_spec.rb b/spec/contexts/issues/bulk_update_context_spec.rb new file mode 100644 index 00000000000..058e43ba090 --- /dev/null +++ b/spec/contexts/issues/bulk_update_context_spec.rb @@ -0,0 +1,113 @@ +require 'spec_helper' + +describe Issues::BulkUpdateContext do + before(:each) { ActiveRecord::Base.observers.enable(:user_observer) } + after(:each) { ActiveRecord::Base.observers.disable(:user_observer) } + + let(:issue) { + create(:issue, project: @project) + } + + before do + @user = create :user + opts = { + name: "GitLab", + namespace: @user.namespace + } + @project = Projects::CreateContext.new(@user, opts).execute + end + + describe :close_issue do + + before do + @issues = 5.times.collect do + create(:issue, project: @project) + end + @params = { + update: { + status: 'closed', + issues_ids: @issues.map(&:id) + } + } + end + + it { + result = Issues::BulkUpdateContext.new(@project, @user, @params).execute + result[:success].should be_true + result[:count].should == @issues.count + + @project.issues.opened.should be_empty + @project.issues.closed.should_not be_empty + } + + end + + describe :reopen_issues do + + before do + @issues = 5.times.collect do + create(:closed_issue, project: @project) + end + @params = { + update: { + status: 'reopen', + issues_ids: @issues.map(&:id) + } + } + end + + it { + result = Issues::BulkUpdateContext.new(@project, @user, @params).execute + result[:success].should be_true + result[:count].should == @issues.count + + @project.issues.closed.should be_empty + @project.issues.opened.should_not be_empty + } + + end + + describe :update_assignee do + + before do + @new_assignee = create :user + @params = { + update: { + issues_ids: [issue.id], + assignee_id: @new_assignee.id + } + } + end + + it { + result = Issues::BulkUpdateContext.new(@project, @user, @params).execute + result[:success].should be_true + result[:count].should == 1 + + @project.issues.first.assignee.should == @new_assignee + } + + end + + describe :update_milestone do + + before do + @milestone = create :milestone + @params = { + update: { + issues_ids: [issue.id], + milestone_id: @milestone.id + } + } + end + + it { + result = Issues::BulkUpdateContext.new(@project, @user, @params).execute + result[:success].should be_true + result[:count].should == 1 + + @project.issues.first.milestone.should == @milestone + } + end + +end diff --git a/spec/contexts/projects_create_context_spec.rb b/spec/contexts/projects_create_context_spec.rb index dd10dd3ede8..8b2a49dbee5 100644 --- a/spec/contexts/projects_create_context_spec.rb +++ b/spec/contexts/projects_create_context_spec.rb @@ -1,11 +1,15 @@ require 'spec_helper' describe Projects::CreateContext do + before(:each) { ActiveRecord::Base.observers.enable(:user_observer) } + after(:each) { ActiveRecord::Base.observers.disable(:user_observer) } + describe :create_by_user do before do @user = create :user @opts = { - name: "GitLab" + name: "GitLab", + namespace: @user.namespace } end @@ -21,15 +25,48 @@ describe Projects::CreateContext do context 'group namespace' do before do - @group = create :group, owner: @user + @group = create :group + @group.add_owner(@user) + @opts.merge!(namespace_id: @group.id) @project = create_project(@user, @opts) end it { @project.should be_valid } - it { @project.owner.should == @user } + it { @project.owner.should == @group } it { @project.namespace.should == @group } end + + context 'respect configured public setting' do + before(:each) do + @settings = double("settings") + @settings.stub(:issues) { true } + @settings.stub(:merge_requests) { true } + @settings.stub(:wiki) { true } + @settings.stub(:wall) { true } + @settings.stub(:snippets) { true } + stub_const("Settings", Class.new) + Settings.stub_chain(:gitlab, :default_projects_features).and_return(@settings) + end + + context 'should be public when setting is public' do + before do + @settings.stub(:public) { true } + @project = create_project(@user, @opts) + end + + it { @project.public.should be_true } + end + + context 'should be private when setting is not public' do + before do + @settings.stub(:public) { false } + @project = create_project(@user, @opts) + end + + it { @project.public.should be_false } + end + end end def create_project(user, opts) diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb new file mode 100644 index 00000000000..d528d12c66c --- /dev/null +++ b/spec/controllers/application_controller_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe ApplicationController do + describe '#check_password_expiration' do + let(:user) { create(:user) } + let(:controller) { ApplicationController.new } + + it 'should redirect if the user is over their password expiry' do + user.password_expires_at = Time.new(2002) + user.ldap_user?.should be_false + controller.stub!(:current_user).and_return(user) + controller.should_receive(:redirect_to) + controller.should_receive(:new_profile_password_path) + controller.send(:check_password_expiration) + end + + it 'should not redirect if the user is under their password expiry' do + user.password_expires_at = Time.now + 20010101 + user.ldap_user?.should be_false + controller.stub!(:current_user).and_return(user) + controller.should_not_receive(:redirect_to) + controller.send(:check_password_expiration) + end + + it 'should not redirect if the user is over their password expiry but they are an ldap user' do + user.password_expires_at = Time.new(2002) + user.stub!(:ldap_user?).and_return(true) + controller.stub!(:current_user).and_return(user) + controller.should_not_receive(:redirect_to) + controller.send(:check_password_expiration) + end + end +end
\ No newline at end of file diff --git a/spec/controllers/blob_controller_spec.rb b/spec/controllers/blob_controller_spec.rb new file mode 100644 index 00000000000..479d8fc1a1d --- /dev/null +++ b/spec/controllers/blob_controller_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Projects::BlobController do + let(:project) { create(:project_with_code) } + 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 "GET show" do + render_views + + before { get :show, project_id: project.to_param, id: id } + + context "valid branch, valid file" do + let(:id) { 'master/README.md' } + it { should respond_with(:success) } + end + + context "valid branch, invalid file" do + let(:id) { 'master/invalid-path.rb' } + it { should respond_with(:not_found) } + end + + context "invalid branch, valid file" do + let(:id) { 'invalid-branch/README.md' } + it { should respond_with(:not_found) } + end + end +end diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb index 7bf13822829..fdf0884f4e2 100644 --- a/spec/controllers/commit_controller_spec.rb +++ b/spec/controllers/commit_controller_spec.rb @@ -1,20 +1,19 @@ require 'spec_helper' -describe CommitController do - let(:project) { create(:project) } +describe Projects::CommitController do + let(:project) { create(:project_with_code) } let(:user) { create(:user) } - let(:commit) { project.repository.last_commit_for("master") } + let(:commit) { project.repository.commit("master") } before do sign_in(user) - project.team << [user, :master] end describe "#show" do shared_examples "export as" do |format| it "should generally work" do - get :show, project_id: project.code, id: commit.id, format: format + get :show, project_id: project.to_param, id: commit.id, format: format expect(response).to be_success end @@ -22,11 +21,11 @@ describe CommitController do it "should generate it" do Commit.any_instance.should_receive(:"to_#{format}") - get :show, project_id: project.code, id: commit.id, format: format + get :show, project_id: project.to_param, id: commit.id, format: format end it "should render it" do - get :show, project_id: project.code, id: commit.id, format: format + get :show, project_id: project.to_param, id: commit.id, format: format expect(response.body).to eq(commit.send(:"to_#{format}")) end @@ -34,7 +33,7 @@ describe CommitController do it "should not escape Html" do Commit.any_instance.stub(:"to_#{format}").and_return('HTML entities &<>" ') - get :show, project_id: project.code, id: commit.id, format: format + get :show, project_id: project.to_param, id: commit.id, format: format expect(response.body).to_not include('&') expect(response.body).to_not include('>') @@ -48,7 +47,7 @@ describe CommitController do let(:format) { :diff } it "should really only be a git diff" do - get :show, project_id: project.code, id: commit.id, format: format + get :show, project_id: project.to_param, id: commit.id, format: format expect(response.body).to start_with("diff --git") end @@ -59,13 +58,13 @@ describe CommitController do let(:format) { :patch } it "should really be a git email patch" do - get :show, project_id: project.code, id: commit.id, format: format + get :show, project_id: project.to_param, id: commit.id, format: format expect(response.body).to start_with("From #{commit.id}") end it "should contain a git diff" do - get :show, project_id: project.code, id: commit.id, format: format + get :show, project_id: project.to_param, id: commit.id, format: format expect(response.body).to match(/^diff --git/) end diff --git a/spec/controllers/commits_controller_spec.rb b/spec/controllers/commits_controller_spec.rb index 1d5d99df802..8263afc97a2 100644 --- a/spec/controllers/commits_controller_spec.rb +++ b/spec/controllers/commits_controller_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe CommitsController do - let(:project) { create(:project) } - let(:user) { create(:user) } +describe Projects::CommitsController do + let(:project) { create(:project_with_code) } + let(:user) { create(:user) } before do sign_in(user) @@ -13,7 +13,7 @@ describe CommitsController do describe "GET show" do context "as atom feed" do it "should render as atom" do - get :show, project_id: project.path, id: "master.atom" + get :show, project_id: project.to_param, id: "master", format: "atom" response.should be_success response.content_type.should == 'application/atom+xml' end diff --git a/spec/controllers/merge_requests_controller_spec.rb b/spec/controllers/merge_requests_controller_spec.rb index 37e36efc1ca..69708edd8b1 100644 --- a/spec/controllers/merge_requests_controller_spec.rb +++ b/spec/controllers/merge_requests_controller_spec.rb @@ -1,20 +1,20 @@ require 'spec_helper' -describe MergeRequestsController do - let(:project) { create(:project) } +describe Projects::MergeRequestsController do + let(:project) { create(:project_with_code) } let(:user) { create(:user) } - let(:merge_request) { create(:merge_request_with_diffs, project: project, target_branch: "bcf03b5d~3", source_branch: "bcf03b5d") } + let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project, target_branch: "bcf03b5d~3", source_branch: "bcf03b5d") } before do sign_in(user) project.team << [user, :master] - MergeRequestsController.any_instance.stub(validates_merge_request: true) + Projects::MergeRequestsController.any_instance.stub(validates_merge_request: true, ) end describe "#show" do - shared_examples "export as" do |format| + shared_examples "export merge as" do |format| it "should generally work" do - get :show, project_id: project.code, id: merge_request.id, format: format + get :show, project_id: project.to_param, id: merge_request.iid, format: format expect(response).to be_success end @@ -22,19 +22,19 @@ describe MergeRequestsController do it "should generate it" do MergeRequest.any_instance.should_receive(:"to_#{format}") - get :show, project_id: project.code, id: merge_request.id, format: format + get :show, project_id: project.to_param, id: merge_request.iid, format: format end it "should render it" do - get :show, project_id: project.code, id: merge_request.id, format: format + get :show, project_id: project.to_param, id: merge_request.iid, format: format - expect(response.body).to eq(merge_request.send(:"to_#{format}")) + expect(response.body).to eq((merge_request.send(:"to_#{format}",user)).to_s) end it "should not escape Html" do MergeRequest.any_instance.stub(:"to_#{format}").and_return('HTML entities &<>" ') - get :show, project_id: project.code, id: merge_request.id, format: format + get :show, project_id: project.to_param, id: merge_request.iid, format: format expect(response.body).to_not include('&') expect(response.body).to_not include('>') @@ -44,39 +44,28 @@ describe MergeRequestsController do end describe "as diff" do - include_examples "export as", :diff + include_examples "export merge as", :diff let(:format) { :diff } it "should really only be a git diff" do - get :show, project_id: project.code, id: merge_request.id, format: format + get :show, project_id: project.to_param, id: merge_request.iid, format: format expect(response.body).to start_with("diff --git") end end describe "as patch" do - include_examples "export as", :patch + include_examples "export merge as", :patch let(:format) { :patch } it "should really be a git email patch with commit" do - get :show, project_id: project.code, id: merge_request.id, format: format + get :show, project_id: project.to_param, id: merge_request.iid, format: format expect(response.body[0..100]).to start_with("From #{merge_request.commits.last.id}") end - # TODO: fix or remove - #it "should contain as many patches as there are commits" do - #get :show, project_id: project.code, id: merge_request.id, format: format - - #patch_count = merge_request.commits.count - #merge_request.commits.each_with_index do |commit, patch_num| - #expect(response.body).to match(/^From #{commit.id}/) - #expect(response.body).to match(/^Subject: \[PATCH #{patch_num}\/#{patch_count}\]/) - #end - #end - it "should contain git diffs" do - get :show, project_id: project.code, id: merge_request.id, format: format + get :show, project_id: project.to_param, id: merge_request.iid, format: format expect(response.body).to match(/^diff --git/) end diff --git a/spec/controllers/tree_controller_spec.rb b/spec/controllers/tree_controller_spec.rb index 81c7656d07a..bb1232e6264 100644 --- a/spec/controllers/tree_controller_spec.rb +++ b/spec/controllers/tree_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe TreeController do - let(:project) { create(:project) } +describe Projects::TreeController do + let(:project) { create(:project_with_code) } let(:user) { create(:user) } before do @@ -18,7 +18,7 @@ describe TreeController do # Make sure any errors accessing the tree in our views bubble up to this spec render_views - before { get :show, project_id: project.code, id: id } + before { get :show, project_id: project.to_param, id: id } context "valid branch, no path" do let(:id) { 'master' } @@ -26,17 +26,17 @@ describe TreeController do end context "valid branch, valid path" do - let(:id) { 'master/README.md' } + let(:id) { 'master/app/' } it { should respond_with(:success) } end context "valid branch, invalid path" do - let(:id) { 'master/invalid-path.rb' } + let(:id) { 'master/invalid-path/' } it { should respond_with(:not_found) } end context "invalid branch, valid path" do - let(:id) { 'invalid-branch/README.md' } + let(:id) { 'invalid-branch/app/' } it { should respond_with(:not_found) } end end diff --git a/spec/factories.rb b/spec/factories.rb index d2e9f48c47b..56561fe4595 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,3 +1,5 @@ +include ActionDispatch::TestProcess + FactoryGirl.define do sequence :sentence, aliases: [:title, :content] do Faker::Lorem.sentence @@ -26,13 +28,49 @@ FactoryGirl.define do factory :project do sequence(:name) { |n| "project#{n}" } path { name.downcase.gsub(/\s/, '_') } + namespace creator + + trait :source do + sequence(:name) { |n| "source project#{n}" } + end + trait :target do + sequence(:name) { |n| "target project#{n}" } + end + + factory :source_project, traits: [:source] + factory :target_project, traits: [:target] + end + + + factory :redmine_project, parent: :project do + issues_tracker { "redmine" } + issues_tracker_id { "project_name_in_redmine" } + end + + factory :project_with_code, parent: :project do + path { 'gitlabhq' } + + trait :source_path do + path { 'source_gitlabhq' } + end + + trait :target_path do + path { 'target_gitlabhq' } + end + + factory :source_project_with_code, traits: [:source, :source_path] + factory :target_project_with_code, traits: [:target, :target_path] + + after :create do |project| + TestEnv.clear_repo_dir(project.namespace, project.path) + TestEnv.create_repo(project.namespace, project.path) + end end factory :group do sequence(:name) { |n| "group#{n}" } path { name.downcase.gsub(/\s/, '_') } - owner type 'Group' end @@ -54,38 +92,51 @@ FactoryGirl.define do project trait :closed do - closed true + state :closed + end + + trait :reopened do + state :reopened end factory :closed_issue, traits: [:closed] + factory :reopened_issue, traits: [:reopened] end factory :merge_request do title author - project + source_project factory: :source_project_with_code + target_project factory: :target_project_with_code source_branch "master" target_branch "stable" - trait :closed do - closed true - end - # pick 3 commits "at random" (from bcf03b5d~3 to bcf03b5d) trait :with_diffs do target_branch "master" # pretend bcf03b5d~3 source_branch "stable" # pretend bcf03b5d st_commits do - [Commit.new(project.repo.commit('bcf03b5d')), - Commit.new(project.repo.commit('bcf03b5d~1')), - Commit.new(project.repo.commit('bcf03b5d~2'))] + [ + source_project.repository.commit('bcf03b5d').to_hash, + source_project.repository.commit('bcf03b5d~1').to_hash, + source_project.repository.commit('bcf03b5d~2').to_hash + ] end st_diffs do - project.repo.diff("bcf03b5d~3", "bcf03b5d") + source_project.repo.diff("bcf03b5d~3", "bcf03b5d") end end + trait :closed do + state :closed + end + + trait :reopened do + state :reopened + end + factory :closed_merge_request, traits: [:closed] + factory :reopened_merge_request, traits: [:reopened] factory :merge_request_with_diffs, traits: [:with_diffs] end @@ -99,9 +150,11 @@ FactoryGirl.define do factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note] factory :note_on_merge_request, traits: [:on_merge_request] factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff] + factory :note_on_merge_request_with_attachment, traits: [:on_merge_request, :with_attachment] trait :on_commit do - commit_id "bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a" + project factory: :project_with_code + commit_id "bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a" noteable_type "Commit" end @@ -110,14 +163,19 @@ FactoryGirl.define do end trait :on_merge_request do - noteable_id 1 + project factory: :project_with_code + noteable_id 1 noteable_type "MergeRequest" end trait :on_issue do - noteable_id 1 + noteable_id 1 noteable_type "Issue" end + + trait :with_attachment do + attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") } + end end factory :event do @@ -135,8 +193,7 @@ FactoryGirl.define do "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" end - factory :deploy_key do - project + factory :deploy_key, class: 'DeployKey' do end factory :personal_key do @@ -148,11 +205,23 @@ FactoryGirl.define do "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa ++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" end end + + factory :invalid_key do + key do + "ssh-rsa this_is_invalid_key==" + end + end end factory :milestone do title project + + trait :closed do + state :closed + end + + factory :closed_milestone, traits: [:closed] end factory :system_hook do @@ -163,14 +232,22 @@ FactoryGirl.define do url end - factory :wiki do + factory :project_snippet do + project + author title content - user + file_name + end + + factory :personal_snippet do + author + title + content + file_name end factory :snippet do - project author title content @@ -193,4 +270,9 @@ FactoryGirl.define do url service end + + factory :deploy_keys_project do + deploy_key + project + end end diff --git a/spec/factories/forked_project_links.rb b/spec/factories/forked_project_links.rb new file mode 100644 index 00000000000..2f9b91acf2c --- /dev/null +++ b/spec/factories/forked_project_links.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: forked_project_links +# +# id :integer not null, primary key +# forked_to_project_id :integer not null +# forked_from_project_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# + +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :forked_project_link do + association :forked_to_project, factory: :project + association :forked_from_project, factory: :project + end +end diff --git a/spec/factories/user_team_project_relationships.rb b/spec/factories/user_team_project_relationships.rb deleted file mode 100644 index e900d86c2e4..00000000000 --- a/spec/factories/user_team_project_relationships.rb +++ /dev/null @@ -1,21 +0,0 @@ -# == Schema Information -# -# Table name: user_team_project_relationships -# -# id :integer not null, primary key -# project_id :integer -# user_team_id :integer -# greatest_access :integer -# created_at :datetime not null -# updated_at :datetime not null -# - -# Read about factories at https://github.com/thoughtbot/factory_girl - -FactoryGirl.define do - factory :user_team_project_relationship do - project - user_team - greatest_access { UsersProject::MASTER } - end -end diff --git a/spec/factories/user_team_user_relationships.rb b/spec/factories/user_team_user_relationships.rb deleted file mode 100644 index 8c729dd8751..00000000000 --- a/spec/factories/user_team_user_relationships.rb +++ /dev/null @@ -1,23 +0,0 @@ -# == Schema Information -# -# Table name: user_team_user_relationships -# -# id :integer not null, primary key -# user_id :integer -# user_team_id :integer -# group_admin :boolean -# permission :integer -# created_at :datetime not null -# updated_at :datetime not null -# - -# Read about factories at https://github.com/thoughtbot/factory_girl - -FactoryGirl.define do - factory :user_team_user_relationship do - user - user_team - group_admin false - permission { UsersProject::MASTER } - end -end diff --git a/spec/factories/user_teams.rb b/spec/factories/user_teams.rb deleted file mode 100644 index 1a9ae8e885c..00000000000 --- a/spec/factories/user_teams.rb +++ /dev/null @@ -1,21 +0,0 @@ -# == Schema Information -# -# Table name: user_teams -# -# id :integer not null, primary key -# name :string(255) -# path :string(255) -# owner_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# - -# Read about factories at https://github.com/thoughtbot/factory_girl - -FactoryGirl.define do - factory :user_team do - sequence(:name) { |n| "team#{n}" } - path { name.downcase.gsub(/\s/, '_') } - owner - end -end diff --git a/spec/factories/users_groups.rb b/spec/factories/users_groups.rb new file mode 100644 index 00000000000..34bc0c51468 --- /dev/null +++ b/spec/factories/users_groups.rb @@ -0,0 +1,20 @@ +# == Schema Information +# +# Table name: users_groups +# +# id :integer not null, primary key +# group_access :integer not null +# group_id :integer not null +# user_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# notification_level :integer default(3), not null +# + +FactoryGirl.define do + factory :users_group do + group_access { UsersGroup::OWNER } + group + user + end +end diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb index 5ee7354688a..66bef0761c7 100644 --- a/spec/factories_spec.rb +++ b/spec/factories_spec.rb @@ -1,6 +1,9 @@ require 'spec_helper' -INVALID_FACTORIES = [:key_with_a_space_in_the_middle] +INVALID_FACTORIES = [ + :key_with_a_space_in_the_middle, + :invalid_key, +] FactoryGirl.factories.map(&:name).each do |factory_name| next if INVALID_FACTORIES.include?(factory_name) diff --git a/spec/requests/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index bc0586b2712..102a1b928f5 100644 --- a/spec/requests/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -12,7 +12,7 @@ describe "Admin::Hooks" do describe "GET /admin/hooks" do it "should be ok" do visit admin_root_path - within ".main_menu" do + within ".main-nav" do click_on "Hooks" end current_path.should == admin_hooks_path diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb new file mode 100644 index 00000000000..23370891244 --- /dev/null +++ b/spec/features/admin/admin_projects_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe "Admin::Projects" do + before do + @project = create(:project) + login_as :admin + end + + describe "GET /admin/projects" do + before do + visit admin_projects_path + end + + it "should be ok" do + current_path.should == admin_projects_path + end + + it "should have projects list" do + page.should have_content(@project.name) + end + end + + describe "GET /admin/projects/:id" do + before do + visit admin_projects_path + click_link "#{@project.name}" + end + + it "should have project info" do + page.should have_content(@project.path) + page.should have_content(@project.name) + end + end +end diff --git a/spec/requests/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 455caf4a376..8d69b595aee 100644 --- a/spec/requests/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -20,21 +20,25 @@ describe "Admin::Users" do describe "GET /admin/users/new" do before do - @password = "123ABC" visit new_admin_user_path fill_in "user_name", with: "Big Bang" fill_in "user_username", with: "bang" fill_in "user_email", with: "bigbang@mail.com" - fill_in "user_password", with: @password - fill_in "user_password_confirmation", with: @password end it "should create new user" do - expect { click_button "Save" }.to change {User.count}.by(1) + expect { click_button "Create user" }.to change {User.count}.by(1) + end + + it "should apply defaults to user" do + click_button "Create user" + user = User.last + user.projects_limit.should == Gitlab.config.gitlab.default_projects_limit + user.can_create_group.should == Gitlab.config.gitlab.default_can_create_group end it "should create user with valid data" do - click_button "Save" + click_button "Create user" user = User.last user.name.should == "Big Bang" user.email.should == "bigbang@mail.com" @@ -44,31 +48,18 @@ describe "Admin::Users" do Notify.should_receive(:new_user_email) User.observers.enable :user_observer do - click_button "Save" + click_button "Create user" end end it "should send valid email to user with email & password" do - Gitlab.config.gitlab.stub(:signup_enabled).and_return(false) User.observers.enable :user_observer do - click_button "Save" + click_button "Create user" user = User.last email = ActionMailer::Base.deliveries.last email.subject.should have_content("Account was created") - email.body.should have_content(user.email) - email.body.should have_content(@password) - end - end - - it "should send valid email to user with email without password when signup is enabled" do - Gitlab.config.gitlab.stub(:signup_enabled).and_return(true) - User.observers.enable :user_observer do - click_button "Save" - user = User.last - email = ActionMailer::Base.deliveries.last - email.subject.should have_content("Account was created") - email.body.should have_content(user.email) - email.body.should_not have_content(@password) + email.text_part.body.should have_content(user.email) + email.text_part.body.should have_content('password') end end end @@ -82,7 +73,6 @@ describe "Admin::Users" do it "should have user info" do page.should have_content(@user.email) page.should have_content(@user.name) - page.should have_content(@user.projects_limit) end end @@ -103,7 +93,7 @@ describe "Admin::Users" do fill_in "user_name", with: "Big Bang" fill_in "user_email", with: "bigbang@mail.com" check "user_admin" - click_button "Save" + click_button "Save changes" end it "should show page with new data" do @@ -118,18 +108,4 @@ describe "Admin::Users" do end end end - - describe "Add new project" do - before do - @new_project = create(:project) - visit admin_user_path(@user) - end - - it "should create new user" do - select @new_project.name, from: "project_ids" - expect { click_button "Add" }.to change { UsersProject.count }.by(1) - page.should have_content @new_project.name - current_path.should == admin_user_path(@user) - end - end end diff --git a/spec/requests/admin/security_spec.rb b/spec/features/admin/security_spec.rb index 6306832628b..6306832628b 100644 --- a/spec/requests/admin/security_spec.rb +++ b/spec/features/admin/security_spec.rb diff --git a/spec/requests/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb index 6f5d51d16b6..6f5d51d16b6 100644 --- a/spec/requests/atom/dashboard_issues_spec.rb +++ b/spec/features/atom/dashboard_issues_spec.rb diff --git a/spec/requests/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb index 6257ad5c895..6257ad5c895 100644 --- a/spec/requests/atom/dashboard_spec.rb +++ b/spec/features/atom/dashboard_spec.rb diff --git a/spec/requests/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb index 0488c1f2266..c9bbdad380f 100644 --- a/spec/requests/atom/issues_spec.rb +++ b/spec/features/atom/issues_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe "Issues Feed" do describe "GET /issues" do let!(:user) { create(:user) } - let!(:project) { create(:project, namespace: user.namespace) } + let!(:project) { create(:project) } let!(:issue) { create(:issue, author: user, project: project) } before { project.team << [user, :developer] } diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb new file mode 100644 index 00000000000..2ea569a6208 --- /dev/null +++ b/spec/features/gitlab_flavored_markdown_spec.rb @@ -0,0 +1,129 @@ +require 'spec_helper' + +describe "GitLab Flavored Markdown" do + let(:project) { create(:project_with_code) } + let(:issue) { create(:issue, project: project) } + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:fred) do + u = create(:user, name: "fred") + project.team << [u, :master] + u + end + + before do + Commit.any_instance.stub(title: "fix ##{issue.iid}\n\nask @#{fred.username} for details") + end + + let(:commit) { project.repository.commit } + + before do + login_as :user + project.team << [@user, :developer] + end + + describe "for commits" do + it "should render title in commits#index" do + visit project_commits_path(project, 'master', limit: 1) + + page.should have_link("##{issue.iid}") + end + + it "should render title in commits#show" do + visit project_commit_path(project, commit) + + page.should have_link("##{issue.iid}") + end + + it "should render description in commits#show" do + visit project_commit_path(project, commit) + + page.should have_link("@#{fred.username}") + end + + it "should render title in repositories#branches" do + visit project_branches_path(project) + + page.should have_link("##{issue.iid}") + end + end + + describe "for issues" do + before do + @other_issue = create(:issue, + author: @user, + assignee: @user, + project: project) + @issue = create(:issue, + author: @user, + assignee: @user, + project: project, + title: "fix ##{@other_issue.iid}", + description: "ask @#{fred.username} for details") + end + + it "should render subject in issues#index" do + visit project_issues_path(project) + + page.should have_link("##{@other_issue.iid}") + end + + it "should render subject in issues#show" do + visit project_issue_path(project, @issue) + + page.should have_link("##{@other_issue.iid}") + end + + it "should render details in issues#show" do + visit project_issue_path(project, @issue) + + page.should have_link("@#{fred.username}") + end + end + + + describe "for merge requests" do + before do + @merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix ##{issue.iid}") + end + + it "should render title in merge_requests#index" do + visit project_merge_requests_path(project) + + page.should have_link("##{issue.iid}") + end + + it "should render title in merge_requests#show" do + visit project_merge_request_path(project, @merge_request) + + page.should have_link("##{issue.iid}") + end + end + + + describe "for milestones" do + before do + @milestone = create(:milestone, + project: project, + title: "fix ##{issue.iid}", + description: "ask @#{fred.username} for details") + end + + it "should render title in milestones#index" do + visit project_milestones_path(project) + + page.should have_link("##{issue.iid}") + end + + it "should render title in milestones#show" do + visit project_milestone_path(project, @milestone) + + page.should have_link("##{issue.iid}") + end + + it "should render description in milestones#show" do + visit project_milestone_path(project, @milestone) + + page.should have_link("@#{fred.username}") + end + end +end diff --git a/spec/requests/issues_spec.rb b/spec/features/issues_spec.rb index 2e94ffd0020..0c09279e3dc 100644 --- a/spec/requests/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -45,41 +45,6 @@ describe "Issues" do end end - describe "Search issue", js: true do - before do - ['foobar', 'foobar2', 'gitlab'].each do |title| - create(:issue, - author: @user, - assignee: @user, - project: project, - title: title) - end - end - - it "should be able to search on different statuses" do - issue = Issue.first # with title 'foobar' - issue.closed = true - issue.save - - visit project_issues_path(project) - click_link 'Closed' - fill_in 'issue_search', with: 'foobar' - - page.should have_content 'foobar' - page.should_not have_content 'foobar2' - page.should_not have_content 'gitlab' - end - - it "should search for term and return the correct results" do - visit project_issues_path(project) - fill_in 'issue_search', with: 'foobar' - - page.should have_content 'foobar' - page.should have_content 'foobar2' - page.should_not have_content 'gitlab' - end - end - describe "Filter issue" do before do ['foobar', 'barbaz', 'gitlab'].each do |title| diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb new file mode 100644 index 00000000000..ba580d9484d --- /dev/null +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -0,0 +1,239 @@ +require 'spec_helper' + +describe "On a merge request", js: true do + let!(:project) { create(:project_with_code) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let!(:note) { create(:note_on_merge_request_with_attachment, project: project) } + + before do + login_as :user + project.team << [@user, :master] + + visit project_merge_request_path(project, merge_request) + end + + subject { page } + + describe "the note form" 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) } + end + + describe "with text" do + before do + within(".js-main-target-form") do + fill_in "note[note]", with: "This is awesome" + end + 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) } + end + end + + describe "with preview" do + before do + within(".js-main-target-form") do + fill_in "note[note]", with: "This is awesome" + find(".js-note-preview-button").trigger("click") + end + end + + it 'should have text and visible edit button' do + within(".js-main-target-form") { should have_css(".js-note-preview", text: "This is awesome", visible: true) } + within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } + within(".js-main-target-form") { should have_css(".js-note-edit-button", visible: true) } + end + end + end + + describe "when posting a note" do + before do + within(".js-main-target-form") do + fill_in "note[note]", with: "This is awsome!" + find(".js-note-preview-button").trigger("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") { should have_css(".js-note-text", visible: true) } + end + end + + describe "when editing a note", js: true do + it "should contain the hidden edit form" do + within("#note_#{note.id}") { should have_css(".note-edit-form", visible: false) } + end + + describe "editing the note" do + before do + find('.note').hover + find(".js-note-edit").click + end + + it "should show the note edit form and hide the note body" do + within("#note_#{note.id}") do + find(".note-edit-form", visible: true).should be_visible + find(".note-text", visible: false).should_not be_visible + end + end + + it "should reset the edit note form textarea with the original content of the note if cancelled" do + find('.note').hover + find(".js-note-edit").click + + within(".note-edit-form") do + fill_in "note[note]", with: "Some new content" + find(".btn-cancel").click + find(".js-note-text", visible: false).text.should == note.note + end + end + + it "appends the edited at time to the note" do + find('.note').hover + find(".js-note-edit").click + + within(".note-edit-form") do + fill_in "note[note]", with: "Some new content" + find(".btn-save").click + end + + within("#note_#{note.id}") do + should have_css(".note-last-update small") + find(".note-last-update small").text.should match(/Edited just now/) + end + end + end + + describe "deleting an attachment" do + before do + find('.note').hover + find(".js-note-edit").click + end + + it "shows the delete link" do + within(".note-attachment") do + should have_css(".js-note-attachment-delete") + end + end + + it "removes the attachment div and resets the edit form" do + find(".js-note-attachment-delete").click + should_not have_css(".note-attachment") + find(".note-edit-form", visible: false).should_not be_visible + end + end + end +end + +describe "On a merge request diff", js: true, focus: true do + let!(:project) { create(:source_project_with_code) } + let!(:merge_request) { create(:merge_request_with_diffs, source_project: project, target_project: project) } + + before do + login_as :user + project.team << [@user, :master] + visit diffs_project_merge_request_path(project, merge_request) + end + + + subject { page } + + describe "when adding a note" do + before do + find('a[data-line-code="4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185"]').click + end + + describe "the notes holder" do + it { should have_css(".js-temp-notes-holder") } + + it { within(".js-temp-notes-holder") { should have_css(".new_note") } } + end + + describe "the note form" do + it 'should be valid' do + within(".js-temp-notes-holder") { find("#note_noteable_type").value.should == "MergeRequest" } + within(".js-temp-notes-holder") { find("#note_noteable_id").value.should == merge_request.id.to_s } + within(".js-temp-notes-holder") { find("#note_commit_id").value.should == "" } + within(".js-temp-notes-holder") { find("#note_line_code").value.should == "4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185" } + should have_css(".js-close-discussion-note-form", text: "Cancel") + end + + it "shouldn't add a second form for same row" do + find('a[data-line-code="4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185"]').click + + should have_css("tr[id='4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185'] + .js-temp-notes-holder form", count: 1) + end + + it "should be removed when canceled" do + within(".file form[rel$='4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185']") do + find(".js-close-discussion-note-form").trigger("click") + end + + should have_no_css(".js-temp-notes-holder") + end + end + end + + describe "with muliple note forms" do + let!(:project) { create(:source_project_with_code) } + let!(:merge_request) { create(:merge_request_with_diffs, source_project: project, target_project: project) } + + before do + find('a[data-line-code="4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185"]').click + find('a[data-line-code="342e16cbbd482ac2047dc679b2749d248cc1428f_18_17"]').click + end + + it { should have_css(".js-temp-notes-holder", count: 2) } + + describe "previewing them separately" do + before do + # add two separate texts and trigger previews on both + within("tr[id='4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185'] + .js-temp-notes-holder") do + fill_in "note[note]", with: "One comment on line 185" + find(".js-note-preview-button").trigger("click") + end + within("tr[id='342e16cbbd482ac2047dc679b2749d248cc1428f_18_17'] + .js-temp-notes-holder") do + fill_in "note[note]", with: "Another comment on line 17" + find(".js-note-preview-button").trigger("click") + end + end + end + + describe "posting a note" do + before do + within("tr[id='342e16cbbd482ac2047dc679b2749d248cc1428f_18_17'] + .js-temp-notes-holder") do + fill_in "note[note]", with: "Another comment on line 17" + click_button("Add Comment") + end + end + + it do + within("tr[id='342e16cbbd482ac2047dc679b2749d248cc1428f_18_17'] + .js-temp-notes-holder") do + should have_no_css(".js-temp-notes-holder") + end + end + + it 'should be added as discussion' do + should have_content("Another comment on line 17") + should have_css(".notes_holder") + should have_css(".notes_holder .note", count: 1) + should have_link("Reply") + end + end + end +end + +describe "On merge request discussion", js: true do + describe "with merge request diff note" + describe "with commit note" + describe "with commit diff note" +end diff --git a/spec/requests/profile_spec.rb b/spec/features/profile_spec.rb index c18d8f921a3..80c9f5d7f14 100644 --- a/spec/requests/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe "Profile account page" do + before(:each) { enable_observers } + after(:each) {disable_observers} let(:user) { create(:user) } before do @@ -12,25 +14,12 @@ describe "Profile account page" do Gitlab.config.gitlab.stub(:signup_enabled).and_return(true) visit account_profile_path end - it { page.should have_content("Remove account") } - - it "should delete the account", js: true do - expect { click_link "Delete account" }.to change {User.count}.by(-1) - current_path.should == new_user_session_path - end - end - describe "when signup is enabled and user has a project" do - before do - Gitlab.config.gitlab.stub(:signup_enabled).and_return(true) - @project = create(:project, namespace: @user.namespace) - @project.team << [@user, :master] - visit account_profile_path - end it { page.should have_content("Remove account") } - it "should not allow user to delete the account" do - expect { click_link "Delete account" }.not_to change {User.count}.by(-1) + it "should delete the account" do + expect { click_link "Delete account" }.to change {User.count}.by(-1) + current_path.should == new_user_session_path end end @@ -45,4 +34,4 @@ describe "Profile account page" do current_path.should == account_profile_path end end -end
\ No newline at end of file +end diff --git a/spec/requests/projects_spec.rb b/spec/features/projects_spec.rb index 7bc48260935..9d5f9d5a2e2 100644 --- a/spec/requests/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe "Projects" do + before(:each) { enable_observers } + after(:each) {disable_observers} before { login_as :user } describe "DELETE /projects/:id" do @@ -11,7 +13,7 @@ describe "Projects" do end it "should be correct path" do - expect { click_link "Remove" }.to change {Project.count}.by(-1) + expect { click_link "Remove project" }.to change {Project.count}.by(-1) end end end diff --git a/spec/requests/search_spec.rb b/spec/features/search_spec.rb index e338f359f88..3ca59da493b 100644 --- a/spec/requests/search_spec.rb +++ b/spec/features/search_spec.rb @@ -2,12 +2,16 @@ require 'spec_helper' describe "Search" do before do + ActiveRecord::Base.observers.enable(:user_observer) login_as :user - @project = create(:project) + @project = create(:project, namespace: @user.namespace) @project.team << [@user, :reporter] visit search_path - fill_in "search", with: @project.name[0..3] - click_button "Search" + + within '.search-holder' do + fill_in "search", with: @project.name[0..3] + click_button "Search" + end end it "should show project in search results" do diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb new file mode 100644 index 00000000000..adec5926c6f --- /dev/null +++ b/spec/features/security/dashboard_access_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe "Dashboard access" do + describe "GET /dashboard" do + subject { dashboard_path } + + it { should be_allowed_for :admin } + it { should be_allowed_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /dashboard/issues" do + subject { issues_dashboard_path } + + it { should be_allowed_for :admin } + it { should be_allowed_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /dashboard/merge_requests" do + subject { merge_requests_dashboard_path } + + it { should be_allowed_for :admin } + it { should be_allowed_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /dashboard/projects" do + subject { projects_dashboard_path } + + it { should be_allowed_for :admin } + it { should be_allowed_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /help" do + subject { help_path } + + it { should be_allowed_for :admin } + it { should be_allowed_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /projects/new" do + it { new_project_path.should be_allowed_for :admin } + it { new_project_path.should be_allowed_for :user } + it { new_project_path.should be_denied_for :visitor } + end + + describe "GET /groups/new" do + it { new_group_path.should be_allowed_for :admin } + it { new_group_path.should be_allowed_for :user } + it { new_group_path.should be_denied_for :visitor } + end +end diff --git a/spec/features/security/group_access_spec.rb b/spec/features/security/group_access_spec.rb new file mode 100644 index 00000000000..dea957962a8 --- /dev/null +++ b/spec/features/security/group_access_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe "Group access" do + describe "GET /projects/new" do + it { new_group_path.should be_allowed_for :admin } + it { new_group_path.should be_allowed_for :user } + it { new_group_path.should be_denied_for :visitor } + end + + describe "Group" do + let(:group) { create(:group) } + + let(:owner) { create(:owner) } + let(:master) { create(:user) } + let(:reporter) { create(:user) } + let(:guest) { create(:user) } + + before do + group.add_user(owner, Gitlab::Access::OWNER) + group.add_user(master, Gitlab::Access::MASTER) + group.add_user(reporter, Gitlab::Access::REPORTER) + group.add_user(guest, Gitlab::Access::GUEST) + end + + describe "GET /groups/:path" do + subject { group_path(group) } + + it { should be_allowed_for owner } + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /groups/:path/issues" do + subject { issues_group_path(group) } + + it { should be_allowed_for owner } + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /groups/:path/merge_requests" do + subject { merge_requests_group_path(group) } + + it { should be_allowed_for owner } + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /groups/:path/members" do + subject { members_group_path(group) } + + it { should be_allowed_for owner } + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /groups/:path/edit" do + subject { edit_group_path(group) } + + it { should be_allowed_for owner } + it { should be_denied_for master } + it { should be_denied_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + end +end diff --git a/spec/requests/security/profile_access_spec.rb b/spec/features/security/profile_access_spec.rb index f854f3fb066..7754b28347a 100644 --- a/spec/requests/security/profile_access_spec.rb +++ b/spec/features/security/profile_access_spec.rb @@ -10,8 +10,8 @@ describe "Users Security" do it { new_user_session_path.should_not be_404_for :visitor } end - describe "GET /keys" do - subject { keys_path } + describe "GET /profile/keys" do + subject { profile_keys_path } it { should be_allowed_for @u1 } it { should be_allowed_for :admin } @@ -45,5 +45,32 @@ describe "Users Security" do it { should be_allowed_for :user } it { should be_denied_for :visitor } end + + describe "GET /profile/history" do + subject { history_profile_path } + + it { should be_allowed_for @u1 } + it { should be_allowed_for :admin } + it { should be_allowed_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /profile/notifications" do + subject { profile_notifications_path } + + it { should be_allowed_for @u1 } + it { should be_allowed_for :admin } + it { should be_allowed_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /profile/groups" do + subject { profile_groups_path } + + it { should be_allowed_for @u1 } + it { should be_allowed_for :admin } + it { should be_allowed_for :user } + it { should be_denied_for :visitor } + end end end diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb new file mode 100644 index 00000000000..7f3f8c50f02 --- /dev/null +++ b/spec/features/security/project/private_access_spec.rb @@ -0,0 +1,218 @@ +require 'spec_helper' + +describe "Private Project Access" do + let(:project) { create(:project_with_code) } + + let(:master) { create(:user) } + let(:guest) { create(:user) } + let(:reporter) { create(:user) } + + before do + # full access + project.team << [master, :master] + + # readonly + project.team << [reporter, :reporter] + end + + describe "GET /:project_path" do + subject { project_path(project) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/tree/master" do + subject { project_tree_path(project, project.repository.root_ref) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/commits/master" do + subject { project_commits_path(project, project.repository.root_ref, limit: 1) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/commit/:sha" do + subject { project_commit_path(project, project.repository.commit) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/compare" do + subject { project_compare_index_path(project) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/team" do + subject { project_team_index_path(project) } + + it { should be_allowed_for master } + it { should be_denied_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/wall" do + subject { project_wall_path(project) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/blob" do + before do + commit = project.repository.commit + path = commit.tree.contents.select { |i| i.is_a?(Grit::Blob) }.first.name + @blob_path = project_blob_path(project, File.join(commit.id, path)) + end + + it { @blob_path.should be_allowed_for master } + it { @blob_path.should be_allowed_for reporter } + it { @blob_path.should be_allowed_for :admin } + it { @blob_path.should be_denied_for guest } + it { @blob_path.should be_denied_for :user } + it { @blob_path.should be_denied_for :visitor } + end + + describe "GET /:project_path/edit" do + subject { edit_project_path(project) } + + it { should be_allowed_for master } + it { should be_denied_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/deploy_keys" do + subject { project_deploy_keys_path(project) } + + it { should be_allowed_for master } + it { should be_denied_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/issues" do + subject { project_issues_path(project) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/snippets" do + subject { project_snippets_path(project) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/merge_requests" do + subject { project_merge_requests_path(project) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/branches/recent" do + subject { recent_project_branches_path(project) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/branches" do + subject { project_branches_path(project) } + + before do + # Speed increase + Project.any_instance.stub(:branches).and_return([]) + end + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/tags" do + subject { project_tags_path(project) } + + before do + # Speed increase + Project.any_instance.stub(:tags).and_return([]) + end + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/hooks" do + subject { project_hooks_path(project) } + + it { should be_allowed_for master } + it { should be_denied_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end +end diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb new file mode 100644 index 00000000000..267643fd8ef --- /dev/null +++ b/spec/features/security/project/public_access_spec.rb @@ -0,0 +1,251 @@ +require 'spec_helper' + +describe "Public Project Access" do + let(:project) { create(:project_with_code) } + + let(:master) { create(:user) } + let(:guest) { create(:user) } + let(:reporter) { create(:user) } + + before do + # public project + project.public = true + project.save! + + # full access + project.team << [master, :master] + + # readonly + project.team << [reporter, :reporter] + + end + + describe "Project should be public" do + subject { project } + + its(:public?) { should be_true } + end + + describe "GET /:project_path" do + subject { project_path(project) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_allowed_for :user } + it { should be_allowed_for :visitor } + end + + describe "GET /:project_path/tree/master" do + subject { project_tree_path(project, project.repository.root_ref) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_allowed_for :user } + it { should be_allowed_for :visitor } + end + + describe "GET /:project_path/commits/master" do + subject { project_commits_path(project, project.repository.root_ref, limit: 1) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_allowed_for :user } + it { should be_allowed_for :visitor } + end + + describe "GET /:project_path/commit/:sha" do + subject { project_commit_path(project, project.repository.commit) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_allowed_for :user } + it { should be_allowed_for :visitor } + end + + describe "GET /:project_path/compare" do + subject { project_compare_index_path(project) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_allowed_for :user } + it { should be_allowed_for :visitor } + end + + describe "GET /:project_path/team" do + subject { project_team_index_path(project) } + + it { should be_allowed_for master } + it { should be_denied_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/wall" do + subject { project_wall_path(project) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_allowed_for :user } + it { should be_allowed_for :visitor } + end + + describe "GET /:project_path/blob" do + before do + commit = project.repository.commit + path = commit.tree.contents.select { |i| i.is_a?(Grit::Blob) }.first.name + @blob_path = project_blob_path(project, File.join(commit.id, path)) + end + + it { @blob_path.should be_allowed_for master } + it { @blob_path.should be_allowed_for reporter } + it { @blob_path.should be_allowed_for :admin } + it { @blob_path.should be_allowed_for guest } + it { @blob_path.should be_allowed_for :user } + it { @blob_path.should be_allowed_for :visitor } + end + + describe "GET /:project_path/edit" do + subject { edit_project_path(project) } + + it { should be_allowed_for master } + it { should be_denied_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/deploy_keys" do + subject { project_deploy_keys_path(project) } + + it { should be_allowed_for master } + it { should be_denied_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/issues" do + subject { project_issues_path(project) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_allowed_for :user } + it { should be_allowed_for :visitor } + end + + describe "GET /:project_path/snippets" do + subject { project_snippets_path(project) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_allowed_for :user } + it { should be_allowed_for :visitor } + end + + describe "GET /:project_path/snippets/new" do + subject { new_project_snippet_path(project) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/merge_requests" do + subject { project_merge_requests_path(project) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_allowed_for :user } + it { should be_allowed_for :visitor } + end + + describe "GET /:project_path/merge_requests/new" do + subject { new_project_merge_request_path(project) } + + it { should be_allowed_for master } + it { should be_denied_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end + + describe "GET /:project_path/branches/recent" do + subject { recent_project_branches_path(project) } + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_allowed_for :user } + it { should be_allowed_for :visitor } + end + + describe "GET /:project_path/branches" do + subject { project_branches_path(project) } + + before do + # Speed increase + Project.any_instance.stub(:branches).and_return([]) + end + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_allowed_for :user } + it { should be_allowed_for :visitor } + end + + describe "GET /:project_path/tags" do + subject { project_tags_path(project) } + + before do + # Speed increase + Project.any_instance.stub(:tags).and_return([]) + end + + it { should be_allowed_for master } + it { should be_allowed_for reporter } + it { should be_allowed_for :admin } + it { should be_allowed_for guest } + it { should be_allowed_for :user } + it { should be_allowed_for :visitor } + end + + describe "GET /:project_path/hooks" do + subject { project_hooks_path(project) } + + it { should be_allowed_for master } + it { should be_denied_for reporter } + it { should be_allowed_for :admin } + it { should be_denied_for guest } + it { should be_denied_for :user } + it { should be_denied_for :visitor } + end +end diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb new file mode 100644 index 00000000000..ed9e44fb47e --- /dev/null +++ b/spec/features/users_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe 'Users' do + describe "GET /users/sign_up" do + before do + Gitlab.config.gitlab.stub(:signup_enabled).and_return(true) + end + + it "should create a new user account" do + visit new_user_registration_path + 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_confirmation", with: "password1234" + expect { click_button "Sign up" }.to change {User.count}.by(1) + end + end +end diff --git a/spec/fixtures/dk.png b/spec/fixtures/dk.png Binary files differnew file mode 100644 index 00000000000..87ce25e877a --- /dev/null +++ b/spec/fixtures/dk.png diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index ba1af08421b..229f49659cf 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -83,4 +83,26 @@ describe ApplicationHelper do end end + + describe "user_color_scheme_class" do + context "with current_user is nil" do + it "should return a string" do + stub!(:current_user).and_return(nil) + user_color_scheme_class.should be_kind_of(String) + end + end + + context "with a current_user" do + (1..5).each do |color_scheme_id| + context "with color_scheme_id == #{color_scheme_id}" do + it "should return a string" do + current_user = double(:color_scheme_id => color_scheme_id) + stub!(:current_user).and_return(current_user) + user_color_scheme_class.should be_kind_of(String) + end + end + end + end + end + end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 1b067972c81..d49247accc2 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -2,14 +2,15 @@ require "spec_helper" describe GitlabMarkdownHelper do include ApplicationHelper + include IssuesHelper - let!(:project) { create(:project) } + let!(:project) { create(:project_with_code) } let(:user) { create(:user, username: 'gfm') } - let(:commit) { CommitDecorator.decorate(project.repository.commit) } + let(:commit) { project.repository.commit } let(:issue) { create(:issue, project: project) } - let(:merge_request) { create(:merge_request, project: project) } - let(:snippet) { create(:snippet, project: project) } + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:snippet) { create(:project_snippet, project: project) } let(:member) { project.users_projects.where(user_id: user).first } before do @@ -19,7 +20,7 @@ describe GitlabMarkdownHelper do describe "#gfm" do it "should return unaltered text if project is nil" do - actual = "Testing references: ##{issue.id}" + actual = "Testing references: ##{issue.iid}" gfm(actual).should_not == actual @@ -84,7 +85,7 @@ describe GitlabMarkdownHelper do describe "referencing a team member" do let(:actual) { "@#{user.username} you are right." } - let(:expected) { project_team_member_path(project, member) } + let(:expected) { user_path(user) } before do project.team << [user, :master] @@ -174,14 +175,14 @@ describe GitlabMarkdownHelper do describe "referencing an issue" do let(:object) { issue } - let(:reference) { "##{issue.id}" } + let(:reference) { "##{issue.iid}" } include_examples 'referenced object' end describe "referencing a merge request" do let(:object) { merge_request } - let(:reference) { "!#{merge_request.id}" } + let(:reference) { "!#{merge_request.iid}" } include_examples 'referenced object' end @@ -189,12 +190,47 @@ describe GitlabMarkdownHelper do describe "referencing a snippet" do let(:object) { snippet } let(:reference) { "$#{snippet.id}" } + let(:actual) { "Reference to #{reference}" } + let(:expected) { project_snippet_path(project, object) } + + it "should link using a valid id" do + gfm(actual).should match(expected) + end + + it "should link with adjacent text" do + # Wrap the reference in parenthesis + gfm(actual.gsub(reference, "(#{reference})")).should match(expected) + + # Append some text to the end of the reference + gfm(actual.gsub(reference, "#{reference}, right?")).should match(expected) + end + + it "should keep whitespace intact" do + actual = "Referenced #{reference} already." + expected = /Referenced <a.+>[^\s]+<\/a> already/ + gfm(actual).should match(expected) + end + + it "should not link with an invalid id" do + # Modify the reference string so it's still parsed, but is invalid + reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2)) + gfm(actual).should == actual + end + + it "should include a title attribute" do + title = "Snippet: #{object.title}" + gfm(actual).should match(/title="#{title}"/) + end + + it "should include standard gfm classes" do + css = object.class.to_s.underscore + gfm(actual).should match(/class="\s?gfm gfm-snippet\s?"/) + end - include_examples 'referenced object' end describe "referencing multiple objects" do - let(:actual) { "!#{merge_request.id} -> #{commit.id} -> ##{issue.id}" } + let(:actual) { "!#{merge_request.iid} -> #{commit.id} -> ##{issue.iid}" } it "should link to the merge request" do expected = project_merge_request_path(project, merge_request) @@ -251,7 +287,7 @@ describe GitlabMarkdownHelper do gfm(":invalid-emoji:").should_not match(/<img/) end - it "should work independet of reference links (i.e. without @project being set)" do + it "should work independent of reference links (i.e. without @project being set)" do @project = nil gfm(":+1:").should match(/<img/) end @@ -263,7 +299,7 @@ describe GitlabMarkdownHelper do let(:issues) { create_list(:issue, 2, project: project) } it "should handle references nested in links with all the text" do - actual = link_to_gfm("This should finally fix ##{issues[0].id} and ##{issues[1].id} for real", commit_path) + actual = link_to_gfm("This should finally fix ##{issues[0].iid} and ##{issues[1].iid} for real", commit_path) # Break the result into groups of links with their content, without # closing tags @@ -275,7 +311,7 @@ describe GitlabMarkdownHelper do # First issue link groups[1].should match(/href="#{project_issue_url(project, issues[0])}"/) - groups[1].should match(/##{issues[0].id}$/) + groups[1].should match(/##{issues[0].iid}$/) # Internal commit link groups[2].should match(/href="#{commit_path}"/) @@ -283,7 +319,7 @@ describe GitlabMarkdownHelper do # Second issue link groups[3].should match(/href="#{project_issue_url(project, issues[1])}"/) - groups[3].should match(/##{issues[1].id}$/) + groups[3].should match(/##{issues[1].iid}$/) # Trailing commit link groups[4].should match(/href="#{commit_path}"/) @@ -296,7 +332,7 @@ describe GitlabMarkdownHelper do end it "escapes HTML passed in as the body" do - actual = "This is a <h1>test</h1> - see ##{issues[0].id}" + actual = "This is a <h1>test</h1> - see ##{issues[0].iid}" link_to_gfm(actual, commit_path).should match('<h1>test</h1>') end end @@ -309,25 +345,34 @@ describe GitlabMarkdownHelper do end it "should handle references in headers" do - actual = "\n# Working around ##{issue.id}\n## Apply !#{merge_request.id}" + actual = "\n# Working around ##{issue.iid}\n## Apply !#{merge_request.iid}" - markdown(actual).should match(%r{<h1[^<]*>Working around <a.+>##{issue.id}</a></h1>}) - markdown(actual).should match(%r{<h2[^<]*>Apply <a.+>!#{merge_request.id}</a></h2>}) + markdown(actual).should match(%r{<h1[^<]*>Working around <a.+>##{issue.iid}</a></h1>}) + markdown(actual).should match(%r{<h2[^<]*>Apply <a.+>!#{merge_request.iid}</a></h2>}) end it "should handle references in lists" do project.team << [user, :master] - actual = "\n* dark: ##{issue.id}\n* light by @#{member.user.username}" + actual = "\n* dark: ##{issue.iid}\n* light by @#{member.user.username}" - markdown(actual).should match(%r{<li>dark: <a.+>##{issue.id}</a></li>}) + markdown(actual).should match(%r{<li>dark: <a.+>##{issue.iid}</a></li>}) markdown(actual).should match(%r{<li>light by <a.+>@#{member.user.username}</a></li>}) end it "should handle references in <em>" do - actual = "Apply _!#{merge_request.id}_ ASAP" + actual = "Apply _!#{merge_request.iid}_ ASAP" - markdown(actual).should match(%r{Apply <em><a.+>!#{merge_request.id}</a></em>}) + markdown(actual).should match(%r{Apply <em><a.+>!#{merge_request.iid}</a></em>}) + end + + it "should handle tables" do + actual = %Q{| header 1 | header 2 | +| -------- | -------- | +| cell 1 | cell 2 | +| cell 3 | cell 4 |} + + markdown(actual).should match(/\A<table/) end it "should leave code blocks untouched" do @@ -343,23 +388,47 @@ describe GitlabMarkdownHelper do end it "should leave ref-like autolinks untouched" do - markdown("look at http://example.tld/#!#{merge_request.id}").should == "<p>look at <a href=\"http://example.tld/#!#{merge_request.id}\">http://example.tld/#!#{merge_request.id}</a></p>\n" + markdown("look at http://example.tld/#!#{merge_request.iid}").should == "<p>look at <a href=\"http://example.tld/#!#{merge_request.iid}\">http://example.tld/#!#{merge_request.iid}</a></p>\n" end it "should leave ref-like href of 'manual' links untouched" do - markdown("why not [inspect !#{merge_request.id}](http://example.tld/#!#{merge_request.id})").should == "<p>why not <a href=\"http://example.tld/#!#{merge_request.id}\">inspect </a><a href=\"#{project_merge_request_url(project, merge_request)}\" class=\"gfm gfm-merge_request \" title=\"Merge Request: #{merge_request.title}\">!#{merge_request.id}</a><a href=\"http://example.tld/#!#{merge_request.id}\"></a></p>\n" + markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})").should == "<p>why not <a href=\"http://example.tld/#!#{merge_request.iid}\">inspect </a><a href=\"#{project_merge_request_url(project, merge_request)}\" class=\"gfm gfm-merge_request \" title=\"Merge Request: #{merge_request.title}\">!#{merge_request.iid}</a><a href=\"http://example.tld/#!#{merge_request.iid}\"></a></p>\n" end it "should leave ref-like src of images untouched" do - markdown("screen shot: ").should == "<p>screen shot: <img src=\"http://example.tld/#!#{merge_request.id}\" alt=\"some image\"></p>\n" + markdown("screen shot: ").should == "<p>screen shot: <img src=\"http://example.tld/#!#{merge_request.iid}\" alt=\"some image\"></p>\n" end it "should generate absolute urls for refs" do - markdown("##{issue.id}").should include(project_issue_url(project, issue)) + markdown("##{issue.iid}").should include(project_issue_url(project, issue)) end it "should generate absolute urls for emoji" do markdown(":smile:").should include("src=\"#{url_to_image("emoji/smile")}") end end + + describe "#render_wiki_content" do + before do + @wiki = stub('WikiPage') + @wiki.stub(:content).and_return('wiki content') + end + + it "should use GitLab Flavored Markdown for markdown files" do + @wiki.stub(:format).and_return(:markdown) + + helper.should_receive(:markdown).with('wiki content') + + helper.render_wiki_content(@wiki) + end + + it "should use the Gollum renderer for all other file types" do + @wiki.stub(:format).and_return(:rdoc) + formatted_content_stub = stub('formatted_content') + formatted_content_stub.should_receive(:html_safe) + @wiki.stub(:formatted_content).and_return(formatted_content_stub) + + helper.render_wiki_content(@wiki) + end + end end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb new file mode 100644 index 00000000000..3595af32431 --- /dev/null +++ b/spec/helpers/issues_helper_spec.rb @@ -0,0 +1,106 @@ +require "spec_helper" + +describe IssuesHelper do + let(:project) { create :project } + let(:issue) { create :issue, project: project } + let(:ext_project) { create :redmine_project } + + describe :title_for_issue do + it "should return issue title if used internal tracker" do + @project = project + title_for_issue(issue.iid).should eq issue.title + end + + it "should always return empty string if used external tracker" do + @project = ext_project + title_for_issue(rand(100)).should eq "" + end + + it "should always return empty string if project nil" do + @project = nil + + title_for_issue(rand(100)).should eq "" + end + end + + describe :url_for_project_issues do + let(:project_url) { Gitlab.config.issues_tracker.redmine.project_url} + let(:ext_expected) do + project_url.gsub(':project_id', ext_project.id.to_s) + .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s) + end + let(:int_expected) { polymorphic_path([project]) } + + it "should return internal path if used internal tracker" do + @project = project + url_for_project_issues.should match(int_expected) + end + + it "should return path to external tracker" do + @project = ext_project + + url_for_project_issues.should match(ext_expected) + end + + it "should return empty string if project nil" do + @project = nil + + url_for_project_issues.should eq "" + end + end + + describe :url_for_issue do + let(:issue_id) { 3 } + let(:issues_url) { Gitlab.config.issues_tracker.redmine.issues_url} + let(:ext_expected) do + issues_url.gsub(':id', issue_id.to_s) + .gsub(':project_id', ext_project.id.to_s) + .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s) + end + let(:int_expected) { polymorphic_path([project, issue]) } + + it "should return internal path if used internal tracker" do + @project = project + url_for_issue(issue.iid).should match(int_expected) + end + + it "should return path to external tracker" do + @project = ext_project + + url_for_issue(issue_id).should match(ext_expected) + end + + it "should return empty string if project nil" do + @project = nil + + url_for_issue(issue.iid).should eq "" + end + end + + describe :url_for_new_issue do + let(:issues_url) { Gitlab.config.issues_tracker.redmine.new_issue_url} + let(:ext_expected) do + issues_url.gsub(':project_id', ext_project.id.to_s) + .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s) + end + let(:int_expected) { new_project_issue_path(project) } + + it "should return internal path if used internal tracker" do + @project = project + url_for_new_issue.should match(int_expected) + end + + it "should return path to external tracker" do + @project = ext_project + + url_for_new_issue.should match(ext_expected) + end + + it "should return empty string if project nil" do + @project = nil + + url_for_new_issue.should eq "" + end + end + +end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb new file mode 100644 index 00000000000..f97959ee8f4 --- /dev/null +++ b/spec/helpers/notifications_helper_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +# Specs in this file have access to a helper object that includes +# the NotificationsHelper. For example: +# +# describe NotificationsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# helper.concat_strings("this","that").should == "this that" +# end +# end +# end +describe NotificationsHelper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/app/views/admin/projects/team.html.haml b/spec/javascripts/helpers/.gitkeep index e69de29bb2d..e69de29bb2d 100644 --- a/app/views/admin/projects/team.html.haml +++ b/spec/javascripts/helpers/.gitkeep diff --git a/spec/javascripts/stat_graph_contributors_graph_spec.js b/spec/javascripts/stat_graph_contributors_graph_spec.js new file mode 100644 index 00000000000..8d2e2038a55 --- /dev/null +++ b/spec/javascripts/stat_graph_contributors_graph_spec.js @@ -0,0 +1,125 @@ +describe("ContributorsGraph", function () { + describe("#set_x_domain", function () { + it("set the x_domain", function () { + ContributorsGraph.set_x_domain(20) + expect(ContributorsGraph.prototype.x_domain).toEqual(20) + }) + }) + + describe("#set_y_domain", function () { + it("sets the y_domain", function () { + ContributorsGraph.set_y_domain([{commits: 30}]) + expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]) + }) + }) + + describe("#init_x_domain", function () { + it("sets the initial x_domain", function () { + ContributorsGraph.init_x_domain([{date: "2013-01-31"}, {date: "2012-01-31"}]) + expect(ContributorsGraph.prototype.x_domain).toEqual(["2012-01-31", "2013-01-31"]) + }) + }) + + describe("#init_y_domain", function () { + it("sets the initial y_domain", function () { + ContributorsGraph.init_y_domain([{commits: 30}]) + expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]) + }) + }) + + describe("#init_domain", function () { + it("calls init_x_domain and init_y_domain", function () { + spyOn(ContributorsGraph, "init_x_domain") + spyOn(ContributorsGraph, "init_y_domain") + ContributorsGraph.init_domain() + expect(ContributorsGraph.init_x_domain).toHaveBeenCalled() + expect(ContributorsGraph.init_y_domain).toHaveBeenCalled() + }) + }) + + describe("#set_dates", function () { + it("sets the dates", function () { + ContributorsGraph.set_dates("2013-12-01") + expect(ContributorsGraph.prototype.dates).toEqual("2013-12-01") + }) + }) + + describe("#set_x_domain", function () { + it("sets the instance's x domain using the prototype's x_domain", function () { + ContributorsGraph.prototype.x_domain = 20 + var instance = new ContributorsGraph() + instance.x = d3.time.scale().range([0, 100]).clamp(true) + spyOn(instance.x, 'domain') + instance.set_x_domain() + expect(instance.x.domain).toHaveBeenCalledWith(20) + }) + }) + + describe("#set_y_domain", function () { + it("sets the instance's y domain using the prototype's y_domain", function () { + ContributorsGraph.prototype.y_domain = 30 + var instance = new ContributorsGraph() + instance.y = d3.scale.linear().range([100, 0]).nice() + spyOn(instance.y, 'domain') + instance.set_y_domain() + expect(instance.y.domain).toHaveBeenCalledWith(30) + }) + }) + + describe("#set_domain", function () { + it("calls set_x_domain and set_y_domain", function () { + var instance = new ContributorsGraph() + spyOn(instance, 'set_x_domain') + spyOn(instance, 'set_y_domain') + instance.set_domain() + expect(instance.set_x_domain).toHaveBeenCalled() + expect(instance.set_y_domain).toHaveBeenCalled() + }) + }) + + describe("#set_data", function () { + it("sets the data", function () { + var instance = new ContributorsGraph() + instance.set_data("20") + expect(instance.data).toEqual("20") + }) + }) +}) + +describe("ContributorsMasterGraph", function () { + + describe("#process_dates", function () { + it("gets and parses dates", function () { + var graph = new ContributorsMasterGraph() + var data = 'random data here' + spyOn(graph, 'parse_dates') + spyOn(graph, 'get_dates').andReturn("get") + spyOn(ContributorsGraph,'set_dates').andCallThrough() + graph.process_dates(data) + expect(graph.parse_dates).toHaveBeenCalledWith(data) + expect(graph.get_dates).toHaveBeenCalledWith(data) + expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get") + }) + }) + + describe("#get_dates", function () { + it("plucks the date field from data collection", function () { + var graph = new ContributorsMasterGraph() + var data = [{date: "2013-01-01"}, {date: "2012-12-15"}] + expect(graph.get_dates(data)).toEqual(["2013-01-01", "2012-12-15"]) + }) + }) + + describe("#parse_dates", function () { + it("parses the dates", function () { + var graph = new ContributorsMasterGraph() + var parseDate = d3.time.format("%Y-%m-%d").parse + var data = [{date: "2013-01-01"}, {date: "2012-12-15"}] + var correct = [{date: parseDate(data[0].date)}, {date: parseDate(data[1].date)}] + graph.parse_dates(data) + expect(data).toEqual(correct) + }) + }) + + +}) diff --git a/spec/javascripts/stat_graph_contributors_util_spec.js b/spec/javascripts/stat_graph_contributors_util_spec.js new file mode 100644 index 00000000000..367f0af05f8 --- /dev/null +++ b/spec/javascripts/stat_graph_contributors_util_spec.js @@ -0,0 +1,200 @@ +describe("ContributorsStatGraphUtil", function () { + + describe("#parse_log", function () { + it("returns a correctly parsed log", function () { + var fake_log = [ + {author: "Karlo Soriano", date: "2013-05-09", additions: 471}, + {author: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1}, + {author: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3}, + {author: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3}] + + var correct_parsed_log = { + total: [ + {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], + by_author: + [ + { + author: "Karlo Soriano", + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} + }, + { + author: "Dmitriy Zaporozhets", + "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} + } + ] + } + expect(ContributorsStatGraphUtil.parse_log(fake_log)).toEqual(correct_parsed_log) + }) + }) + + describe("#store_data", function () { + + var fake_entry = {author: "Karlo Soriano", date: "2013-05-09", additions: 471} + var fake_total = {} + var fake_by_author = {} + + it("calls #store_commits", function () { + spyOn(ContributorsStatGraphUtil, 'store_commits') + ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author) + expect(ContributorsStatGraphUtil.store_commits).toHaveBeenCalled() + }) + + it("calls #store_additions", function () { + spyOn(ContributorsStatGraphUtil, 'store_additions') + ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author) + expect(ContributorsStatGraphUtil.store_additions).toHaveBeenCalled() + }) + + it("calls #store_deletions", function () { + spyOn(ContributorsStatGraphUtil, 'store_deletions') + ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author) + expect(ContributorsStatGraphUtil.store_deletions).toHaveBeenCalled() + }) + + }) + + describe("#store_commits", function () { + var fake_total = "fake_total" + var fake_by_author = "fake_by_author" + + it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { + spyOn(ContributorsStatGraphUtil, 'add') + ContributorsStatGraphUtil.store_commits(fake_total, fake_by_author) + expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "commits", 1], ["fake_by_author", "commits", 1]]) + }) + }) + + describe("#add", function () { + it("adds 1 to current test_field in collection", function () { + var fake_collection = {test_field: 10} + ContributorsStatGraphUtil.add(fake_collection, "test_field", 1) + expect(fake_collection.test_field).toEqual(11) + }) + + it("inits and adds 1 if test_field in collection is not defined", function () { + var fake_collection = {} + ContributorsStatGraphUtil.add(fake_collection, "test_field", 1) + expect(fake_collection.test_field).toEqual(1) + }) + }) + + describe("#store_additions", function () { + var fake_entry = {additions: 10} + var fake_total= "fake_total" + var fake_by_author = "fake_by_author" + it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { + spyOn(ContributorsStatGraphUtil, 'add') + ContributorsStatGraphUtil.store_additions(fake_entry, fake_total, fake_by_author) + expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "additions", 10], ["fake_by_author", "additions", 10]]) + }) + }) + + describe("#store_deletions", function () { + var fake_entry = {deletions: 10} + var fake_total= "fake_total" + var fake_by_author = "fake_by_author" + it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { + spyOn(ContributorsStatGraphUtil, 'add') + ContributorsStatGraphUtil.store_deletions(fake_entry, fake_total, fake_by_author) + expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "deletions", 10], ["fake_by_author", "deletions", 10]]) + }) + }) + + describe("#add_date", function () { + it("adds a date field to the collection", function () { + var fake_date = "2013-10-02" + var fake_collection = {} + ContributorsStatGraphUtil.add_date(fake_date, fake_collection) + expect(fake_collection[fake_date].date).toEqual("2013-10-02") + }) + }) + + describe("#add_author", function () { + it("adds an author field to the collection", function () { + var fake_author = "Author" + var fake_collection = {} + ContributorsStatGraphUtil.add_author(fake_author, fake_collection) + expect(fake_collection[fake_author].author).toEqual("Author") + }) + }) + + describe("#get_total_data", function () { + it("returns the collection sorted via specified field", function () { + var fake_parsed_log = { + total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], + by_author:[ + { + author: "Karlo Soriano", + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} + }, + { + author: "Dmitriy Zaporozhets", + "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} + } + ]}; + var correct_total_data = [{date: "2013-05-08", commits: 3}, + {date: "2013-05-09", commits: 1}]; + expect(ContributorsStatGraphUtil.get_total_data(fake_parsed_log, "commits")).toEqual(correct_total_data) + }) + }) + + describe("#pick_field", function () { + it("returns the collection with only the specified field and date", function () { + var fake_parsed_log_total = [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}]; + ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits") + var correct_pick_field_data = [{date: "2013-05-09", commits: 1},{date: "2013-05-08", commits: 3}]; + expect(ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits")).toEqual(correct_pick_field_data) + }) + }) + + describe("#get_author_data", function () { + it("returns the log by author sorted by specified field", function () { + var fake_parsed_log = { + total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], + by_author:[ + { + author: "Karlo Soriano", + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} + }, + { + author: "Dmitriy Zaporozhets", + "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} + } + ]} + var correct_author_data = [{author:"Dmitriy Zaporozhets",dates:{"2013-05-08":3},deletions:7,additions:54,"commits":3}, + {author:"Karlo Soriano",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1}] + expect(ContributorsStatGraphUtil.get_author_data(fake_parsed_log, "commits")).toEqual(correct_author_data) + }) + }) + + describe("#parse_log_entry", function () { + it("adds the corresponding info from the log entry to the author", function () { + var fake_log_entry = { author: "Karlo Soriano", + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} + } + var correct_parsed_log = {author:"Karlo Soriano",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1} + expect(ContributorsStatGraphUtil.parse_log_entry(fake_log_entry, 'commits', null)).toEqual(correct_parsed_log) + }) + }) + + describe("#in_range", function () { + var date = "2013-05-09" + it("returns true if date_range is null", function () { + expect(ContributorsStatGraphUtil.in_range(date, null)).toEqual(true) + }) + it("returns true if date is in range", function () { + var date_range = [new Date("2013-01-01"), new Date("2013-12-12")] + expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(true) + }) + it("returns false if date is not in range", function () { + var date_range = [new Date("1999-12-01"), new Date("2000-12-01")] + expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(false) + }) + }) + + +})
\ No newline at end of file diff --git a/spec/javascripts/stat_graph_spec.js b/spec/javascripts/stat_graph_spec.js new file mode 100644 index 00000000000..b8881769ac1 --- /dev/null +++ b/spec/javascripts/stat_graph_spec.js @@ -0,0 +1,17 @@ +describe("StatGraph", function () { + + describe("#get_log", function () { + it("returns log", function () { + StatGraph.log = "test"; + expect(StatGraph.get_log()).toBe("test"); + }); + }); + + describe("#set_log", function () { + it("sets the log", function () { + StatGraph.set_log("test"); + expect(StatGraph.log).toBe("test"); + }) + }) + +});
\ No newline at end of file diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml new file mode 100644 index 00000000000..9bfa261a356 --- /dev/null +++ b/spec/javascripts/support/jasmine.yml @@ -0,0 +1,76 @@ +# src_files +# +# Return an array of filepaths relative to src_dir to include before jasmine specs. +# Default: [] +# +# EXAMPLE: +# +# src_files: +# - lib/source1.js +# - lib/source2.js +# - dist/**/*.js +# +src_files: + - assets/application.js + +# stylesheets +# +# Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs. +# Default: [] +# +# EXAMPLE: +# +# stylesheets: +# - css/style.css +# - stylesheets/*.css +# +stylesheets: + - stylesheets/**/*.css + +# helpers +# +# Return an array of filepaths relative to spec_dir to include before jasmine specs. +# Default: ["helpers/**/*.js"] +# +# EXAMPLE: +# +# helpers: +# - helpers/**/*.js +# +helpers: + - helpers/**/*.js + +# spec_files +# +# Return an array of filepaths relative to spec_dir to include. +# Default: ["**/*[sS]pec.js"] +# +# EXAMPLE: +# +# spec_files: +# - **/*[sS]pec.js +# +spec_files: + - '**/*[sS]pec.js' + +# src_dir +# +# Source directory path. Your src_files must be returned relative to this path. Will use root if left blank. +# Default: project root +# +# EXAMPLE: +# +# src_dir: public +# +src_dir: + +# spec_dir +# +# Spec directory path. Your spec_files must be returned relative to this path. +# Default: spec/javascripts +# +# EXAMPLE: +# +# spec_dir: spec/javascripts +# +spec_dir: spec/javascripts diff --git a/spec/javascripts/support/jasmine_helper.rb b/spec/javascripts/support/jasmine_helper.rb new file mode 100644 index 00000000000..986a4c16f3e --- /dev/null +++ b/spec/javascripts/support/jasmine_helper.rb @@ -0,0 +1,11 @@ +#Use this file to set/override Jasmine configuration options +#You can remove it if you don't need it. +#This file is loaded *after* jasmine.yml is interpreted. +# +#Example: using a different boot file. +#Jasmine.configure do |config| +# @config.boot_dir = '/absolute/path/to/boot_dir' +# @config.boot_files = lambda { ['/absolute/path/to/boot_dir/file.js'] } +#end +# + diff --git a/spec/lib/auth_spec.rb b/spec/lib/auth_spec.rb index 1e03bc591b4..e05fde95731 100644 --- a/spec/lib/auth_spec.rb +++ b/spec/lib/auth_spec.rb @@ -3,93 +3,26 @@ require 'spec_helper' describe Gitlab::Auth do let(:gl_auth) { Gitlab::Auth.new } - before do - Gitlab.config.stub(omniauth: {}) - - @info = mock( - uid: '12djsak321', - name: 'John', - email: 'john@mail.com' - ) - end - - describe :find_for_ldap_auth do - before do - @auth = mock( - uid: '12djsak321', - info: @info, - provider: 'ldap' - ) - end - - it "should find by uid & provider" do - User.should_receive :find_by_extern_uid_and_provider - gl_auth.find_for_ldap_auth(@auth) - end - - it "should update credentials by email if missing uid" do - user = double('User') - User.stub find_by_extern_uid_and_provider: nil - User.stub find_by_email: user - user.should_receive :update_attributes - gl_auth.find_for_ldap_auth(@auth) - end - - - it "should create from auth if user doesnot exist"do - User.stub find_by_extern_uid_and_provider: nil - User.stub find_by_email: nil - gl_auth.should_receive :create_from_omniauth - gl_auth.find_for_ldap_auth(@auth) - end - end - - describe :find_or_new_for_omniauth do + describe :find do before do - @auth = mock( - info: @info, - provider: 'twitter', - uid: '12djsak321', + @user = create( + :user, + username: 'john', + password: '888777', + password_confirmation: '888777' ) end - it "should find user"do - User.should_receive :find_by_provider_and_extern_uid - gl_auth.should_not_receive :create_from_omniauth - gl_auth.find_or_new_for_omniauth(@auth) - end - - it "should not create user"do - User.stub find_by_provider_and_extern_uid: nil - gl_auth.should_not_receive :create_from_omniauth - gl_auth.find_or_new_for_omniauth(@auth) + it "should find user by valid login/password" do + gl_auth.find('john', '888777').should == @user end - it "should create user if single_sing_on"do - Gitlab.config.omniauth['allow_single_sign_on'] = true - User.stub find_by_provider_and_extern_uid: nil - gl_auth.should_receive :create_from_omniauth - gl_auth.find_or_new_for_omniauth(@auth) + it "should not find user with invalid password" do + gl_auth.find('john', 'invalid').should_not == @user end - end - - describe :create_from_omniauth do - it "should create user from LDAP" do - @auth = mock(info: @info, provider: 'ldap') - user = gl_auth.create_from_omniauth(@auth, true) - - user.should be_valid - user.extern_uid.should == @info.uid - user.provider.should == 'ldap' - end - - it "should create user from Omniauth" do - @auth = mock(info: @info, provider: 'twitter') - user = gl_auth.create_from_omniauth(@auth, false) - user.should be_valid - user.extern_uid.should == @info.uid - user.provider.should == 'twitter' + it "should not find user with invalid login and password" do + gl_auth.find('jon', 'invalid').should_not == @user end end end diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index ee20ae79809..aac72c63ea5 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -54,47 +54,5 @@ describe ExtractsPath do extract_ref('stable/CHANGELOG').should == ['stable', 'CHANGELOG'] end end - - context "with a fullpath" do - it "extracts a valid branch" do - extract_ref('/gitlab/gitlab-ci/tree/foo/bar/baz/CHANGELOG').should == ['foo/bar/baz', 'CHANGELOG'] - end - - it "extracts a valid tag" do - extract_ref('/gitlab/gitlab-ci/tree/v2.0.0/CHANGELOG').should == ['v2.0.0', 'CHANGELOG'] - end - - it "extracts a valid commit SHA" do - extract_ref('/gitlab/gitlab-ci/tree/f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG').should == - ['f4b14494ef6abf3d144c28e4af0c20143383e062', 'CHANGELOG'] - end - - it "extracts a timestamp" do - extract_ref('/gitlab/gitlab-ci/tree/v2.0.0/CHANGELOG?_=12354435').should == ['v2.0.0', 'CHANGELOG'] - end - end - - context "with a fullpath and a relative_url_root" do - before do - Gitlab.config.gitlab.stub(relative_url_root: '/relative') - end - - it "extracts a valid branch with relative_url_root" do - extract_ref('/relative/gitlab/gitlab-ci/tree/foo/bar/baz/CHANGELOG').should == ['foo/bar/baz', 'CHANGELOG'] - end - - it "extracts a valid tag" do - extract_ref('/relative/gitlab/gitlab-ci/tree/v2.0.0/CHANGELOG').should == ['v2.0.0', 'CHANGELOG'] - end - - it "extracts a valid commit SHA" do - extract_ref('/relative/gitlab/gitlab-ci/tree/f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG').should == - ['f4b14494ef6abf3d144c28e4af0c20143383e062', 'CHANGELOG'] - end - - it "extracts a timestamp" do - extract_ref('/relative/gitlab/gitlab-ci/tree/v2.0.0/CHANGELOG?_=12354435').should == ['v2.0.0', 'CHANGELOG'] - end - end end end diff --git a/spec/lib/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb index 3c04f4bbeb6..f00ec0fa401 100644 --- a/spec/lib/shell_spec.rb +++ b/spec/lib/gitlab/backend/shell_spec.rb @@ -12,6 +12,7 @@ describe Gitlab::Shell do it { should respond_to :remove_key } it { should respond_to :add_repository } it { should respond_to :remove_repository } + it { should respond_to :fork_repository } it { gitlab_shell.url_to_repo('diaspora').should == Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git" } end diff --git a/spec/lib/gitlab/ldap/ldap_user_auth_spec.rb b/spec/lib/gitlab/ldap/ldap_user_auth_spec.rb new file mode 100644 index 00000000000..b1c583c0476 --- /dev/null +++ b/spec/lib/gitlab/ldap/ldap_user_auth_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe Gitlab::LDAP do + let(:gl_auth) { Gitlab::LDAP::User } + + before do + Gitlab.config.stub(omniauth: {}) + + @info = mock( + uid: '12djsak321', + name: 'John', + email: 'john@mail.com' + ) + end + + describe :find_for_ldap_auth do + before do + @auth = mock( + uid: '12djsak321', + info: @info, + provider: 'ldap' + ) + end + + it "should update credentials by email if missing uid" do + user = double('User') + User.stub find_by_extern_uid_and_provider: nil + User.stub find_by_email: user + user.should_receive :update_attributes + gl_auth.find_or_create(@auth) + end + + it "should update credentials by username if missing uid and Gitlab.config.ldap.allow_username_or_email_login is true" do + user = double('User') + value = Gitlab.config.ldap.allow_username_or_email_login + Gitlab.config.ldap['allow_username_or_email_login'] = true + User.stub find_by_extern_uid_and_provider: nil + User.stub find_by_email: nil + User.stub find_by_username: user + user.should_receive :update_attributes + gl_auth.find_or_create(@auth) + Gitlab.config.ldap['allow_username_or_email_login'] = value + end + + it "should not update credentials by username if missing uid and Gitlab.config.ldap.allow_username_or_email_login is false" do + user = double('User') + value = Gitlab.config.ldap.allow_username_or_email_login + Gitlab.config.ldap['allow_username_or_email_login'] = false + User.stub find_by_extern_uid_and_provider: nil + User.stub find_by_email: nil + User.stub find_by_username: user + user.should_not_receive :update_attributes + gl_auth.find_or_create(@auth) + Gitlab.config.ldap['allow_username_or_email_login'] = value + end + end +end diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb new file mode 100644 index 00000000000..4791be41613 --- /dev/null +++ b/spec/lib/gitlab/popen_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe 'Gitlab::Popen', no_db: true do + let (:path) { Rails.root.join('tmp').to_s } + + before do + @klass = Class.new(Object) + @klass.send(:include, Gitlab::Popen) + end + + context 'zero status' do + before do + @output, @status = @klass.new.popen('ls', path) + end + + it { @status.should be_zero } + it { @output.should include('cache') } + end + + context 'non-zero status' do + before do + @output, @status = @klass.new.popen('cat NOTHING', path) + end + + it { @status.should == 1 } + it { @output.should include('No such file or directory') } + end +end + diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb new file mode 100644 index 00000000000..7d805f8c72a --- /dev/null +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -0,0 +1,95 @@ +require 'spec_helper' + +describe Gitlab::ReferenceExtractor do + it 'extracts username references' do + subject.analyze "this contains a @user reference" + subject.users.should == ["user"] + end + + it 'extracts issue references' do + subject.analyze "this one talks about issue #1234" + subject.issues.should == ["1234"] + end + + it 'extracts merge request references' do + subject.analyze "and here's !43, a merge request" + subject.merge_requests.should == ["43"] + end + + it 'extracts snippet ids' do + subject.analyze "snippets like $12 get extracted as well" + subject.snippets.should == ["12"] + end + + it 'extracts commit shas' do + subject.analyze "commit shas 98cf0ae3 are pulled out as Strings" + subject.commits.should == ["98cf0ae3"] + end + + it 'extracts multiple references and preserves their order' do + subject.analyze "@me and @you both care about this" + subject.users.should == ["me", "you"] + end + + it 'leaves the original note unmodified' do + text = "issue #123 is just the worst, @user" + subject.analyze text + text.should == "issue #123 is just the worst, @user" + end + + it 'handles all possible kinds of references' do + accessors = Gitlab::Markdown::TYPES.map { |t| "#{t}s".to_sym } + subject.should respond_to(*accessors) + end + + context 'with a project' do + let(:project) { create(:project_with_code) } + + it 'accesses valid user objects on the project team' do + @u_foo = create(:user, username: 'foo') + @u_bar = create(:user, username: 'bar') + create(:user, username: 'offteam') + + project.team << [@u_foo, :reporter] + project.team << [@u_bar, :guest] + + subject.analyze "@foo, @baduser, @bar, and @offteam" + subject.users_for(project).should == [@u_foo, @u_bar] + end + + it 'accesses valid issue objects' do + @i0 = create(:issue, project: project) + @i1 = create(:issue, project: project) + + subject.analyze "##{@i0.iid}, ##{@i1.iid}, and #999." + subject.issues_for(project).should == [@i0, @i1] + end + + it 'accesses valid merge requests' do + @m0 = create(:merge_request, source_project: project, target_project: project, source_branch: 'aaa') + @m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'bbb') + + subject.analyze "!999, !#{@m1.iid}, and !#{@m0.iid}." + subject.merge_requests_for(project).should == [@m1, @m0] + end + + it 'accesses valid snippets' do + @s0 = create(:project_snippet, project: project) + @s1 = create(:project_snippet, project: project) + @s2 = create(:project_snippet) + + subject.analyze "$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}" + subject.snippets_for(project).should == [@s0, @s1] + end + + it 'accesses valid commits' do + commit = project.repository.commit("master") + + subject.analyze "this references commits #{commit.sha[0..6]} and 012345" + extracted = subject.commits_for(project) + extracted.should have(1).item + extracted[0].sha.should == commit.sha + extracted[0].message.should == commit.message + end + end +end diff --git a/spec/lib/gitlab/satellite/action_spec.rb b/spec/lib/gitlab/satellite/action_spec.rb new file mode 100644 index 00000000000..5e0a825c3c3 --- /dev/null +++ b/spec/lib/gitlab/satellite/action_spec.rb @@ -0,0 +1,116 @@ +require 'spec_helper' + +describe 'Gitlab::Satellite::Action' do + let(:project) { create(:project_with_code) } + let(:user) { create(:user) } + + describe '#prepare_satellite!' do + + it 'create a repository with a parking branch and one remote: origin' do + repo = project.satellite.repo + + #now lets dirty it up + + starting_remote_count = repo.git.list_remotes.size + starting_remote_count.should >= 1 + #kind of hookey way to add a second remote + origin_uri = repo.git.remote({v: true}).split(" ")[1] + begin + repo.git.remote({raise: true}, 'add', 'another-remote', origin_uri) + repo.git.branch({raise: true}, 'a-new-branch') + + repo.heads.size.should > (starting_remote_count) + repo.git.remote().split(" ").size.should > (starting_remote_count) + rescue + end + + repo.git.config({}, "user.name", "#{user.name} -- foo") + repo.git.config({}, "user.email", "#{user.email} -- foo") + repo.config['user.name'].should =="#{user.name} -- foo" + repo.config['user.email'].should =="#{user.email} -- foo" + + + #These must happen in the context of the satellite directory... + satellite_action = Gitlab::Satellite::Action.new(user, project) + project.satellite.lock { + #Now clean it up, use send to get around prepare_satellite! being protected + satellite_action.send(:prepare_satellite!, repo) + } + + #verify it's clean + heads = repo.heads.map(&:name) + heads.size.should == 1 + heads.include?(Gitlab::Satellite::Satellite::PARKING_BRANCH).should == true + remotes = repo.git.remote().split(' ') + remotes.size.should == 1 + remotes.include?('origin').should == true + repo.config['user.name'].should ==user.name + repo.config['user.email'].should ==user.email + end + end + + describe '#in_locked_and_timed_satellite' do + + it 'should make use of a lockfile' do + repo = project.satellite.repo + called = false + + #set assumptions + File.rm(project.satellite.lock_file) unless !File.exists? project.satellite.lock_file + + File.exists?(project.satellite.lock_file).should be_false + + satellite_action = Gitlab::Satellite::Action.new(user, project) + satellite_action.send(:in_locked_and_timed_satellite) do |sat_repo| + repo.should == sat_repo + (File.exists? project.satellite.lock_file).should be_true + called = true + end + + called.should be_true + + end + + it 'should be able to use the satellite after locking' do + repo = project.satellite.repo + called = false + + # Set base assumptions + if File.exists? project.satellite.lock_file + FileLockStatusChecker.new(project.satellite.lock_file).flocked?.should be_false + end + + satellite_action = Gitlab::Satellite::Action.new(user, project) + satellite_action.send(:in_locked_and_timed_satellite) do |sat_repo| + called = true + repo.should == sat_repo + (File.exists? project.satellite.lock_file).should be_true + FileLockStatusChecker.new(project.satellite.lock_file).flocked?.should be_true + end + + called.should be_true + FileLockStatusChecker.new(project.satellite.lock_file).flocked?.should be_false + + end + + class FileLockStatusChecker < File + def flocked? &block + status = flock LOCK_EX|LOCK_NB + case status + when false + return true + when 0 + begin + block ? block.call : false + ensure + flock LOCK_UN + end + else + raise SystemCallError, status + end + end + end + + end +end + diff --git a/spec/lib/gitlab/satellite/merge_action_spec.rb b/spec/lib/gitlab/satellite/merge_action_spec.rb new file mode 100644 index 00000000000..3be14383e06 --- /dev/null +++ b/spec/lib/gitlab/satellite/merge_action_spec.rb @@ -0,0 +1,148 @@ +require 'spec_helper' + +describe 'Gitlab::Satellite::MergeAction' do + before(:each) do +# TestEnv.init(mailer: false, init_repos: true, repos: true) + @master = ['master', 'bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a'] + @one_after_stable = ['stable', '6ea87c47f0f8a24ae031c3fff17bc913889ecd00'] #this commit sha is one after stable + @wiki_branch = ['wiki', '635d3e09b72232b6e92a38de6cc184147e5bcb41'] #this is the commit sha where the wiki branch goes off from master + @conflicting_metior = ['metior', '313d96e42b313a0af5ab50fa233bf43e27118b3f'] #this branch conflicts with the wiki branch + + #these commits are quite close together, itended to make string diffs/format patches small + @close_commit1 = ['2_3_notes_fix', '8470d70da67355c9c009e4401746b1d5410af2e3'] + @close_commit2 = ['scss_refactoring', 'f0f14c8eaba69ebddd766498a9d0b0e79becd633'] + end + + let(:project) { create(:project_with_code) } + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:merge_request_fork) { create(:merge_request) } + describe '#commits_between' do + def verify_commits(commits, first_commit_sha, last_commit_sha) + commits.each { |commit| commit.class.should == Gitlab::Git::Commit } + commits.first.id.should == first_commit_sha + commits.last.id.should == last_commit_sha + end + + context 'on fork' do + it 'should get proper commits between' do + merge_request_fork.target_branch = @one_after_stable[0] + merge_request_fork.source_branch = @master[0] + commits = Gitlab::Satellite::MergeAction.new(merge_request_fork.author, merge_request_fork).commits_between + verify_commits(commits, @one_after_stable[1], @master[1]) + + merge_request_fork.target_branch = @wiki_branch[0] + merge_request_fork.source_branch = @master[0] + commits = Gitlab::Satellite::MergeAction.new(merge_request_fork.author, merge_request_fork).commits_between + verify_commits(commits, @wiki_branch[1], @master[1]) + end + end + + context 'between branches' do + it 'should raise exception -- not expected to be used by non forks' do + merge_request.target_branch = @one_after_stable[0] + merge_request.source_branch = @master[0] + expect {Gitlab::Satellite::MergeAction.new(merge_request.author, merge_request).commits_between}.to raise_error + + merge_request.target_branch = @wiki_branch[0] + merge_request.source_branch = @master[0] + expect {Gitlab::Satellite::MergeAction.new(merge_request.author, merge_request).commits_between}.to raise_error + end + end + end + + describe '#format_patch' do + let(:target_commit) {['artiom-config-examples','9edbac5ac88ffa1ec9dad0097226b51e29ebc9ac']} + let(:source_commit) {['metior', '313d96e42b313a0af5ab50fa233bf43e27118b3f']} + + def verify_content(patch) + (patch.include? source_commit[1]).should be_true + (patch.include? '635d3e09b72232b6e92a38de6cc184147e5bcb41').should be_true + (patch.include? '2bb2dee057327c81978ed0aa99904bd7ff5e6105').should be_true + (patch.include? '2e83de1924ad3429b812d17498b009a8b924795d').should be_true + (patch.include? 'ee45a49c57a362305431cbf004e4590b713c910e').should be_true + (patch.include? 'a6870dd08f8f274d9a6b899f638c0c26fefaa690').should be_true + + (patch.include? 'e74fae147abc7d2ffbf93d363dbbe45b87751f6f').should be_false + (patch.include? '86f76b11c670425bbab465087f25172378d76147').should be_false + end + + context 'on fork' do + it 'should build a format patch' do + merge_request_fork.target_branch = target_commit[0] + merge_request_fork.source_branch = source_commit[0] + patch = Gitlab::Satellite::MergeAction.new(merge_request_fork.author, merge_request_fork).format_patch + verify_content(patch) + end + end + + context 'between branches' do + it 'should build a format patch' do + merge_request.target_branch = target_commit[0] + merge_request.source_branch = source_commit[0] + patch = Gitlab::Satellite::MergeAction.new(merge_request_fork.author, merge_request).format_patch + verify_content(patch) + end + end + end + + describe '#diffs_between_satellite tested against diff_in_satellite' do + + def is_a_matching_diff(diff, diffs) + diff_count = diff.scan('diff --git').size + diff_count.should >= 1 + diffs.size.should == diff_count + diffs.each do |a_diff| + a_diff.class.should == Gitlab::Git::Diff + (diff.include? a_diff.diff).should be_true + end + end + + context 'on fork' do + it 'should get proper diffs' do + merge_request_fork.target_branch = @close_commit1[0] + merge_request_fork.source_branch = @master[0] + diffs = Gitlab::Satellite::MergeAction.new(merge_request_fork.author, merge_request_fork).diffs_between_satellite + + merge_request_fork.target_branch = @close_commit1[0] + merge_request_fork.source_branch = @master[0] + diff = Gitlab::Satellite::MergeAction.new(merge_request.author, merge_request_fork).diff_in_satellite + + is_a_matching_diff(diff, diffs) + end + end + + context 'between branches' do + it 'should get proper diffs' do + merge_request.target_branch = @close_commit1[0] + merge_request.source_branch = @master[0] + expect{Gitlab::Satellite::MergeAction.new(merge_request.author, merge_request).diffs_between_satellite}.to raise_error + end + end + end + + describe '#can_be_merged?' do + context 'on fork' do + it 'return true or false depending on if something is mergable' do + merge_request_fork.target_branch = @one_after_stable[0] + merge_request_fork.source_branch = @master[0] + Gitlab::Satellite::MergeAction.new(merge_request_fork.author, merge_request_fork).can_be_merged?.should be_true + + merge_request_fork.target_branch = @conflicting_metior[0] + merge_request_fork.source_branch = @wiki_branch[0] + Gitlab::Satellite::MergeAction.new(merge_request_fork.author, merge_request_fork).can_be_merged?.should be_false + end + end + + context 'between branches' do + it 'return true or false depending on if something is mergable' do + merge_request.target_branch = @one_after_stable[0] + merge_request.source_branch = @master[0] + Gitlab::Satellite::MergeAction.new(merge_request.author, merge_request).can_be_merged?.should be_true + + merge_request.target_branch = @conflicting_metior[0] + merge_request.source_branch = @wiki_branch[0] + Gitlab::Satellite::MergeAction.new(merge_request.author, merge_request).can_be_merged?.should be_false + end + end + end +end
\ No newline at end of file diff --git a/spec/lib/gitlab/version_info_spec.rb b/spec/lib/gitlab/version_info_spec.rb new file mode 100644 index 00000000000..94dccf7a4e5 --- /dev/null +++ b/spec/lib/gitlab/version_info_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe 'Gitlab::VersionInfo', no_db: true do + before do + @unknown = Gitlab::VersionInfo.new + @v0_0_1 = Gitlab::VersionInfo.new(0, 0, 1) + @v0_1_0 = Gitlab::VersionInfo.new(0, 1, 0) + @v1_0_0 = Gitlab::VersionInfo.new(1, 0, 0) + @v1_0_1 = Gitlab::VersionInfo.new(1, 0, 1) + @v1_1_0 = Gitlab::VersionInfo.new(1, 1, 0) + @v2_0_0 = Gitlab::VersionInfo.new(2, 0, 0) + end + + context '>' do + it { @v2_0_0.should > @v1_1_0 } + it { @v1_1_0.should > @v1_0_1 } + it { @v1_0_1.should > @v1_0_0 } + it { @v1_0_0.should > @v0_1_0 } + it { @v0_1_0.should > @v0_0_1 } + end + + context '>=' do + it { @v2_0_0.should >= Gitlab::VersionInfo.new(2, 0, 0) } + it { @v2_0_0.should >= @v1_1_0 } + end + + context '<' do + it { @v0_0_1.should < @v0_1_0 } + it { @v0_1_0.should < @v1_0_0 } + it { @v1_0_0.should < @v1_0_1 } + it { @v1_0_1.should < @v1_1_0 } + it { @v1_1_0.should < @v2_0_0 } + end + + context '<=' do + it { @v0_0_1.should <= Gitlab::VersionInfo.new(0, 0, 1) } + it { @v0_0_1.should <= @v0_1_0 } + end + + context '==' do + it { @v0_0_1.should == Gitlab::VersionInfo.new(0, 0, 1) } + it { @v0_1_0.should == Gitlab::VersionInfo.new(0, 1, 0) } + it { @v1_0_0.should == Gitlab::VersionInfo.new(1, 0, 0) } + end + + context '!=' do + it { @v0_0_1.should_not == @v0_1_0 } + end + + context 'unknown' do + it { @unknown.should_not be @v0_0_1 } + it { @unknown.should_not be Gitlab::VersionInfo.new } + it { expect{@unknown > @v0_0_1}.to raise_error(ArgumentError) } + it { expect{@unknown < @v0_0_1}.to raise_error(ArgumentError) } + end + + context 'parse' do + it { Gitlab::VersionInfo.parse("1.0.0").should == @v1_0_0 } + it { Gitlab::VersionInfo.parse("1.0.0.1").should == @v1_0_0 } + it { Gitlab::VersionInfo.parse("git 1.0.0b1").should == @v1_0_0 } + it { Gitlab::VersionInfo.parse("git 1.0b1").should_not be_valid } + end + + context 'to_s' do + it { @v1_0_0.to_s.should == "1.0.0" } + it { @unknown.to_s.should == "Unknown" } + end +end + diff --git a/spec/lib/oauth_spec.rb b/spec/lib/oauth_spec.rb new file mode 100644 index 00000000000..e21074554b6 --- /dev/null +++ b/spec/lib/oauth_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe Gitlab::OAuth::User do + let(:gl_auth) { Gitlab::OAuth::User } + + before do + Gitlab.config.stub(omniauth: {}) + + @info = mock( + uid: '12djsak321', + name: 'John', + email: 'john@mail.com' + ) + end + + describe :create do + it "should create user from LDAP" do + @auth = mock(info: @info, provider: 'ldap') + user = gl_auth.create(@auth) + + user.should be_valid + user.extern_uid.should == @info.uid + user.provider.should == 'ldap' + end + + it "should create user from Omniauth" do + @auth = mock(info: @info, provider: 'twitter') + user = gl_auth.create(@auth) + + user.should be_valid + user.extern_uid.should == @info.uid + user.provider.should == 'twitter' + end + + it "should apply defaults to user" do + @auth = mock(info: @info, provider: 'ldap') + user = gl_auth.create(@auth) + + 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 + end +end diff --git a/spec/lib/project_mover_spec.rb b/spec/lib/project_mover_spec.rb deleted file mode 100644 index 9202befdcb2..00000000000 --- a/spec/lib/project_mover_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ProjectMover do - let(:base_path) { Rails.root.join('tmp', 'rspec-sandbox') } - - before do - FileUtils.rm_rf base_path if File.exists? base_path - FileUtils.mkdir_p base_path - - Gitlab.config.gitlab_shell.stub(repos_path: base_path) - - @project = create(:project) - end - - after do - FileUtils.rm_rf base_path - end - - it "should move project to subdir" do - mk_dir base_path, '', @project.path - mover = Gitlab::ProjectMover.new(@project, '', 'opensource') - - mover.execute.should be_true - moved?('opensource', @project.path).should be_true - end - - it "should move project from one subdir to another" do - mk_dir base_path, 'vsizov', @project.path - mover = Gitlab::ProjectMover.new(@project, 'vsizov', 'randx') - - mover.execute.should be_true - moved?('randx', @project.path).should be_true - end - - it "should move project from subdir to base" do - mk_dir base_path, 'vsizov', @project.path - mover = Gitlab::ProjectMover.new(@project, 'vsizov', '') - - mover.execute.should be_true - moved?('', @project.path).should be_true - end - - it "should raise if destination exists" do - mk_dir base_path, '', @project.path - mk_dir base_path, 'vsizov', @project.path - mover = Gitlab::ProjectMover.new(@project, 'vsizov', '') - - expect { mover.execute }.to raise_error(Gitlab::ProjectMover::ProjectMoveError) - end - - it "should raise if move failed" do - mk_dir base_path - mover = Gitlab::ProjectMover.new(@project, 'vsizov', '') - - expect { mover.execute }.to raise_error(Gitlab::ProjectMover::ProjectMoveError) - end - - - def mk_dir base_path, namespace = '', project_path = '' - FileUtils.mkdir_p File.join(base_path, namespace, project_path + ".git") - end - - def moved? namespace, path - File.exists?(File.join(base_path, namespace, path + '.git')) - end -end diff --git a/spec/lib/votes_spec.rb b/spec/lib/votes_spec.rb index b49ed15b5c3..a3c353d5eab 100644 --- a/spec/lib/votes_spec.rb +++ b/spec/lib/votes_spec.rb @@ -1,132 +1,136 @@ require 'spec_helper' -describe MergeRequest do - let(:merge_request) { FactoryGirl.create(:merge_request_with_diffs) } +describe Issue, 'Votes' do + let(:issue) { create(:issue) } describe "#upvotes" do it "with no notes has a 0/0 score" do - merge_request.upvotes.should == 0 + issue.upvotes.should == 0 end it "should recognize non-+1 notes" do - merge_request.notes << create(:note, note: "No +1 here") - merge_request.should have(1).note - merge_request.notes.first.upvote?.should be_false - merge_request.upvotes.should == 0 + add_note "No +1 here" + issue.should have(1).note + issue.notes.first.upvote?.should be_false + issue.upvotes.should == 0 end it "should recognize a single +1 note" do - merge_request.notes << create(:note, note: "+1 This is awesome") - merge_request.upvotes.should == 1 + add_note "+1 This is awesome" + issue.upvotes.should == 1 end it "should recognize multiple +1 notes" do - merge_request.notes << create(:note, note: "+1 This is awesome") - merge_request.notes << create(:note, note: "+1 I want this") - merge_request.upvotes.should == 2 + add_note "+1 This is awesome" + add_note "+1 I want this" + issue.upvotes.should == 2 end end describe "#downvotes" do it "with no notes has a 0/0 score" do - merge_request.downvotes.should == 0 + issue.downvotes.should == 0 end it "should recognize non--1 notes" do - merge_request.notes << create(:note, note: "Almost got a -1") - merge_request.should have(1).note - merge_request.notes.first.downvote?.should be_false - merge_request.downvotes.should == 0 + add_note "Almost got a -1" + issue.should have(1).note + issue.notes.first.downvote?.should be_false + issue.downvotes.should == 0 end it "should recognize a single -1 note" do - merge_request.notes << create(:note, note: "-1 This is bad") - merge_request.downvotes.should == 1 + add_note "-1 This is bad" + issue.downvotes.should == 1 end it "should recognize multiple -1 notes" do - merge_request.notes << create(:note, note: "-1 This is bad") - merge_request.notes << create(:note, note: "-1 Away with this") - merge_request.downvotes.should == 2 + add_note "-1 This is bad" + add_note "-1 Away with this" + issue.downvotes.should == 2 end end describe "#votes_count" do it "with no notes has a 0/0 score" do - merge_request.votes_count.should == 0 + issue.votes_count.should == 0 end it "should recognize non notes" do - merge_request.notes << create(:note, note: "No +1 here") - merge_request.should have(1).note - merge_request.votes_count.should == 0 + add_note "No +1 here" + issue.should have(1).note + issue.votes_count.should == 0 end it "should recognize a single +1 note" do - merge_request.notes << create(:note, note: "+1 This is awesome") - merge_request.votes_count.should == 1 + add_note "+1 This is awesome" + issue.votes_count.should == 1 end it "should recognize a single -1 note" do - merge_request.notes << create(:note, note: "-1 This is bad") - merge_request.votes_count.should == 1 + add_note "-1 This is bad" + issue.votes_count.should == 1 end it "should recognize multiple notes" do - merge_request.notes << create(:note, note: "+1 This is awesome") - merge_request.notes << create(:note, note: "-1 This is bad") - merge_request.notes << create(:note, note: "+1 I want this") - merge_request.votes_count.should == 3 + add_note "+1 This is awesome" + add_note "-1 This is bad" + add_note "+1 I want this" + issue.votes_count.should == 3 end end describe "#upvotes_in_percent" do it "with no notes has a 0% score" do - merge_request.upvotes_in_percent.should == 0 + issue.upvotes_in_percent.should == 0 end it "should count a single 1 note as 100%" do - merge_request.notes << create(:note, note: "+1 This is awesome") - merge_request.upvotes_in_percent.should == 100 + add_note "+1 This is awesome" + issue.upvotes_in_percent.should == 100 end it "should count multiple +1 notes as 100%" do - merge_request.notes << create(:note, note: "+1 This is awesome") - merge_request.notes << create(:note, note: "+1 I want this") - merge_request.upvotes_in_percent.should == 100 + add_note "+1 This is awesome" + add_note "+1 I want this" + issue.upvotes_in_percent.should == 100 end it "should count fractions for multiple +1 and -1 notes correctly" do - merge_request.notes << create(:note, note: "+1 This is awesome") - merge_request.notes << create(:note, note: "+1 I want this") - merge_request.notes << create(:note, note: "-1 This is bad") - merge_request.notes << create(:note, note: "+1 me too") - merge_request.upvotes_in_percent.should == 75 + add_note "+1 This is awesome" + add_note "+1 I want this" + add_note "-1 This is bad" + add_note "+1 me too" + issue.upvotes_in_percent.should == 75 end end describe "#downvotes_in_percent" do it "with no notes has a 0% score" do - merge_request.downvotes_in_percent.should == 0 + issue.downvotes_in_percent.should == 0 end it "should count a single -1 note as 100%" do - merge_request.notes << create(:note, note: "-1 This is bad") - merge_request.downvotes_in_percent.should == 100 + add_note "-1 This is bad" + issue.downvotes_in_percent.should == 100 end it "should count multiple -1 notes as 100%" do - merge_request.notes << create(:note, note: "-1 This is bad") - merge_request.notes << create(:note, note: "-1 Away with this") - merge_request.downvotes_in_percent.should == 100 + add_note "-1 This is bad" + add_note "-1 Away with this" + issue.downvotes_in_percent.should == 100 end it "should count fractions for multiple +1 and -1 notes correctly" do - merge_request.notes << create(:note, note: "+1 This is awesome") - merge_request.notes << create(:note, note: "+1 I want this") - merge_request.notes << create(:note, note: "-1 This is bad") - merge_request.notes << create(:note, note: "+1 me too") - merge_request.downvotes_in_percent.should == 25 + add_note "+1 This is awesome" + add_note "+1 I want this" + add_note "-1 This is bad" + add_note "+1 me too" + issue.downvotes_in_percent.should == 25 end end + + def add_note(text) + issue.notes << create(:note, note: text, project: issue.project) + end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index befc10594db..0787bdbea6f 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -5,7 +5,7 @@ describe Notify do include EmailSpec::Matchers let(:recipient) { create(:user, email: 'recipient@example.com') } - let(:project) { create(:project) } + let(:project) { create(:project_with_code) } shared_examples 'a multiple recipients email' do it 'is sent to the given recipient' do @@ -15,7 +15,7 @@ describe Notify do describe 'for new users, the email' do let(:example_site_path) { root_path } - let(:new_user) { create(:user, email: 'newguy@example.com') } + let(:new_user) { create(:user, email: 'newguy@example.com', created_by_id: 1) } subject { Notify.new_user_email(new_user.id, new_user.password) } @@ -32,8 +32,7 @@ describe Notify do end it 'contains the new user\'s password' do - Gitlab.config.gitlab.stub(:signup_enabled).and_return(false) - should have_body_text /#{new_user.password}/ + should have_body_text /password/ end it 'includes a link to the site' do @@ -61,8 +60,7 @@ describe Notify do end it 'should not contain the new user\'s password' do - Gitlab.config.gitlab.stub(:signup_enabled).and_return(true) - should_not have_body_text /#{new_user.password}/ + should_not have_body_text /password/ end it 'includes a link to the site' do @@ -70,6 +68,28 @@ describe Notify do end end + describe 'user added ssh key' do + let(:key) { create(:personal_key) } + + subject { Notify.new_ssh_key_email(key.id) } + + it 'is sent to the new user' do + should deliver_to key.user.email + end + + it 'has the correct subject' do + should have_subject /^gitlab \| SSH key was added to your account$/i + end + + it 'contains the new ssh key title' do + should have_body_text /#{key.title}/ + end + + it 'includes a link to ssh keys page' do + should have_body_text /#{profile_keys_path}/ + end + end + context 'for a project' do describe 'items that are assignable, the email' do let(:assignee) { create(:user, email: 'assignee@example.com') } @@ -85,12 +105,12 @@ describe Notify do let(:issue) { create(:issue, assignee: assignee, project: project ) } describe 'that are new' do - subject { Notify.new_issue_email(issue.id) } + subject { Notify.new_issue_email(issue.assignee_id, issue.id) } it_behaves_like 'an assignee email' it 'has the correct subject' do - should have_subject /#{project.name} \| new issue ##{issue.id} \| #{issue.title}/ + should have_subject /#{project.name} \| new issue ##{issue.iid} \| #{issue.title}/ end it 'contains a link to the new issue' do @@ -106,7 +126,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it 'has the correct subject' do - should have_subject /changed issue ##{issue.id} \| #{issue.title}/ + should have_subject /changed issue ##{issue.iid} \| #{issue.title}/ end it 'contains the name of the previous assignee' do @@ -128,7 +148,7 @@ describe Notify do subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user) } it 'has the correct subject' do - should have_subject /changed issue ##{issue.id} \| #{issue.title}/i + should have_subject /changed issue ##{issue.iid} \| #{issue.title}/i end it 'contains the new status' do @@ -147,15 +167,15 @@ describe Notify do end context 'for merge requests' do - let(:merge_request) { create(:merge_request, assignee: assignee, project: project) } + let(:merge_request) { create(:merge_request, assignee: assignee, source_project: project, target_project: project) } describe 'that are new' do - subject { Notify.new_merge_request_email(merge_request.id) } + subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) } it_behaves_like 'an assignee email' it 'has the correct subject' do - should have_subject /new merge request !#{merge_request.id}/ + should have_subject /new merge request !#{merge_request.iid}/ end it 'contains a link to the new merge request' do @@ -179,7 +199,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it 'has the correct subject' do - should have_subject /changed merge request !#{merge_request.id}/ + should have_subject /changed merge request !#{merge_request.iid}/ end it 'contains the name of the previous assignee' do @@ -198,6 +218,24 @@ describe Notify do end end + describe 'project was moved' do + let(:project) { create(:project) } + let(:user) { create(:user) } + subject { Notify.project_was_moved_email(project.id, user.id) } + + it 'has the correct subject' do + should have_subject /project was moved/ + end + + it 'contains name of project' do + should have_body_text /#{project.name_with_namespace}/ + end + + it 'contains new user role' do + should have_body_text /#{project.ssh_url_to_repo}/ + end + end + describe 'project access changed' do let(:project) { create(:project) } let(:user) { create(:user) } @@ -212,7 +250,7 @@ describe Notify do should have_body_text /#{project.name}/ end it 'contains new user role' do - should have_body_text /#{users_project.project_access_human}/ + should have_body_text /#{users_project.human_access}/ end end @@ -239,7 +277,7 @@ describe Notify do end describe 'on a project wall' do - let(:note_on_the_wall_path) { wall_project_path(project, anchor: "note_#{note.id}") } + let(:note_on_the_wall_path) { project_wall_path(project, anchor: "note_#{note.id}") } subject { Notify.note_wall_email(recipient.id, note.id) } @@ -255,14 +293,7 @@ describe Notify do end describe 'on a commit' do - let(:commit) do - mock(:commit).tap do |commit| - commit.stub(:id).and_return('fauxsha1') - commit.stub(:project).and_return(project) - commit.stub(:short_id).and_return('fauxsha1') - commit.stub(:safe_message).and_return('some message') - end - end + let(:commit) { project.repository.commit } before(:each) { note.stub(:noteable).and_return(commit) } @@ -275,12 +306,12 @@ describe Notify do end it 'contains a link to the commit' do - should have_body_text /fauxsha1/ + should have_body_text commit.short_id end end describe 'on a merge request' do - let(:merge_request) { create(:merge_request, project: project) } + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:note_on_merge_request_path) { project_merge_request_path(project, merge_request, anchor: "note_#{note.id}") } before(:each) { note.stub(:noteable).and_return(merge_request) } @@ -289,7 +320,7 @@ describe Notify do it_behaves_like 'a note email' it 'has the correct subject' do - should have_subject /note for merge request !#{merge_request.id}/ + should have_subject /note for merge request !#{merge_request.iid}/ end it 'contains a link to the merge request note' do @@ -307,7 +338,7 @@ describe Notify do it_behaves_like 'a note email' it 'has the correct subject' do - should have_subject /note for issue ##{issue.id}/ + should have_subject /note for issue ##{issue.iid}/ end it 'contains a link to the issue note' do @@ -316,4 +347,24 @@ describe Notify do end end end + + describe 'group access changed' do + let(:group) { create(:group) } + let(:user) { create(:user) } + let(:membership) { create(:users_group, group: group, user: user) } + + subject { Notify.group_access_granted_email(membership.id) } + + it 'has the correct subject' do + should have_subject /access to group was granted/ + end + + it 'contains name of project' do + should have_body_text /#{group.name}/ + end + + it 'contains new user role' do + should have_body_text /#{membership.human_access}/ + end + end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 91301029e89..fa556f94a1d 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -1,83 +1,35 @@ require 'spec_helper' describe Commit do - let(:commit) { create(:project).repository.commit } + let(:project) { create :project_with_code } + let(:commit) { project.repository.commit } - describe CommitDecorator do - let(:decorator) { CommitDecorator.new(commit) } - - describe '#title' do - it "returns no_commit_message when safe_message is blank" do - decorator.stub(:safe_message).and_return('') - decorator.title.should == "--no commit message" - end - - it "truncates a message without a newline at 70 characters" do - message = commit.safe_message * 10 - - decorator.stub(:safe_message).and_return(message) - decorator.title.should == "#{message[0..69]}…" - end - - it "truncates a message with a newline before 80 characters at the newline" do - message = commit.safe_message.split(" ").first - - decorator.stub(:safe_message).and_return(message + "\n" + message) - decorator.title.should == message - end - - it "truncates a message with a newline after 80 characters at 70 characters" do - message = (commit.safe_message * 10) + "\n" - - decorator.stub(:safe_message).and_return(message) - decorator.title.should == "#{message[0..69]}…" - end + describe '#title' do + it "returns no_commit_message when safe_message is blank" do + commit.stub(:safe_message).and_return('') + commit.title.should == "--no commit message" end - end - describe "Commit info" do - before do - @committer = double( - email: 'mike@smith.com', - name: 'Mike Smith' - ) + it "truncates a message without a newline at 80 characters" do + message = commit.safe_message * 10 - @author = double( - email: 'john@smith.com', - name: 'John Smith' - ) + commit.stub(:safe_message).and_return(message) + commit.title.should == "#{message[0..79]}…" + end - @raw_commit = double( - id: "bcf03b5de6abcf03b5de6c", - author: @author, - committer: @committer, - committed_date: Date.yesterday, - message: 'Refactoring specs' - ) + it "truncates a message with a newline before 80 characters at the newline" do + message = commit.safe_message.split(" ").first - @commit = Commit.new(@raw_commit) + commit.stub(:safe_message).and_return(message + "\n" + message) + commit.title.should == message end - it { @commit.short_id.should == "bcf03b5de6a" } - it { @commit.safe_message.should == @raw_commit.message } - it { @commit.created_at.should == @raw_commit.committed_date } - it { @commit.author_email.should == @author.email } - it { @commit.author_name.should == @author.name } - it { @commit.committer_name.should == @committer.name } - it { @commit.committer_email.should == @committer.email } - it { @commit.different_committer?.should be_true } - end + it "truncates a message with a newline after 80 characters at 70 characters" do + message = (commit.safe_message * 10) + "\n" - describe "Class methods" do - subject { Commit } - - it { should respond_to(:find_or_first) } - it { should respond_to(:fresh_commits) } - it { should respond_to(:commits_with_refs) } - it { should respond_to(:commits_since) } - it { should respond_to(:commits_between) } - it { should respond_to(:commits) } - it { should respond_to(:compare) } + commit.stub(:safe_message).and_return(message) + commit.title.should == "#{message[0..79]}…" + end end describe "delegation" do @@ -86,13 +38,32 @@ describe Commit do it { should respond_to(:message) } it { should respond_to(:authored_date) } it { should respond_to(:committed_date) } + it { should respond_to(:committer_email) } + it { should respond_to(:author_email) } it { should respond_to(:parents) } it { should respond_to(:date) } - it { should respond_to(:committer) } - it { should respond_to(:author) } it { should respond_to(:diffs) } it { should respond_to(:tree) } it { should respond_to(:id) } it { should respond_to(:to_patch) } end + + describe '#closes_issues' do + let(:issue) { create :issue, project: project } + + it 'detects issues that this commit is marked as closing' do + commit.stub(issue_closing_regex: /^([Cc]loses|[Ff]ixes) #\d+/, safe_message: "Fixes ##{issue.iid}") + commit.closes_issues(project).should == [issue] + end + end + + 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(:set_mentionable_text) { ->(txt){ subject.stub(safe_message: txt) } } + + # Include the subject in the repository stub. + let(:extra_commits) { [subject] } + end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index b5d4bd7b406..852146ebaec 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -11,11 +11,12 @@ describe Issue, "Issuable" do end describe "Validation" do + before { subject.stub(set_iid: false) } it { should validate_presence_of(:project) } + it { should validate_presence_of(:iid) } it { should validate_presence_of(:author) } it { should validate_presence_of(:title) } it { should ensure_length_of(:title).is_at_least(0).is_at_most(255) } - it { should ensure_inclusion_of(:closed).in_array([true, false]) } end describe "Scope" do diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb new file mode 100644 index 00000000000..b76ca660411 --- /dev/null +++ b/spec/models/deploy_key_spec.rb @@ -0,0 +1,25 @@ +# == Schema Information +# +# Table name: keys +# +# id :integer not null, primary key +# user_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# key :text +# title :string(255) +# type :string(255) +# fingerprint :string(255) +# + +require 'spec_helper' + +describe DeployKey do + let(:project) { create(:project) } + let(:deploy_key) { create(:deploy_key, projects: [project]) } + + describe "Associations" do + it { should have_many(:deploy_keys_projects) } + it { should have_many(:projects) } + end +end diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb new file mode 100644 index 00000000000..aeec1713558 --- /dev/null +++ b/spec/models/deploy_keys_project_spec.rb @@ -0,0 +1,24 @@ +# == Schema Information +# +# Table name: deploy_keys_projects +# +# id :integer not null, primary key +# deploy_key_id :integer not null +# project_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# + +require 'spec_helper' + +describe DeployKeysProject do + describe "Associations" do + it { should belong_to(:deploy_key) } + it { should belong_to(:project) } + end + + describe "Validation" do + it { should validate_presence_of(:project_id) } + it { should validate_presence_of(:deploy_key_id) } + end +end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index cc78993905f..85bdf08ae64 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -72,6 +72,7 @@ describe Event do before { Event.should_receive :create + observer.stub(notification: stub.as_null_object) } describe "Joined project team" do diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb new file mode 100644 index 00000000000..44b8c6155be --- /dev/null +++ b/spec/models/forked_project_link_spec.rb @@ -0,0 +1,67 @@ +# == Schema Information +# +# Table name: forked_project_links +# +# id :integer not null, primary key +# forked_to_project_id :integer not null +# forked_from_project_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# + +require 'spec_helper' + +describe ForkedProjectLink, "add link on fork" do + let(:project_from) { create(:project) } + let(:namespace) { create(:namespace) } + let(:user) { create(:user, namespace: namespace) } + + before do + @project_to = fork_project(project_from, user) + end + + it "project_to should know it is forked" do + @project_to.forked?.should be_true + end + + it "project should know who it is forked from" do + @project_to.forked_from_project.should == project_from + end +end + +describe :forked_from_project do + let(:forked_project_link) { build(:forked_project_link) } + let(:project_from) { create(:project) } + let(:project_to) { create(:project, forked_project_link: forked_project_link) } + + + before :each do + forked_project_link.forked_from_project = project_from + forked_project_link.forked_to_project = project_to + forked_project_link.save! + end + + + it "project_to should know it is forked" do + project_to.forked?.should be_true + end + + it "project_from should not be forked" do + project_from.forked?.should be_false + end + + it "project_to.destroy should destroy fork_link" do + forked_project_link.should_receive(:destroy) + project_to.destroy + end + +end + +def fork_project(from_project, user) + context = Projects::ForkContext.new(from_project, user) + shell = mock("gitlab_shell") + shell.stub(fork_repository: true) + context.stub(gitlab_shell: shell) + context.execute +end + diff --git a/spec/models/gitlab_ci_service_spec.rb b/spec/models/gitlab_ci_service_spec.rb index b86588af1ac..56efa9df457 100644 --- a/spec/models/gitlab_ci_service_spec.rb +++ b/spec/models/gitlab_ci_service_spec.rb @@ -11,6 +11,8 @@ # updated_at :datetime not null # active :boolean default(FALSE), not null # project_url :string(255) +# subdomain :string(255) +# room :string(255) # require 'spec_helper' diff --git a/spec/models/gollum_wiki_spec.rb b/spec/models/gollum_wiki_spec.rb new file mode 100644 index 00000000000..aa850dfd0a3 --- /dev/null +++ b/spec/models/gollum_wiki_spec.rb @@ -0,0 +1,196 @@ +require "spec_helper" + +describe GollumWiki do + + def create_temp_repo(path) + FileUtils.mkdir_p path + command = "git init --quiet #{path};" + system(command) + end + + def remove_temp_repo(path) + FileUtils.rm_rf path + end + + def commit_details + commit = {name: user.name, email: user.email, message: "test commit"} + end + + def create_page(name, content) + subject.wiki.write_page(name, :markdown, content, commit_details) + end + + def destroy_page(page) + subject.wiki.delete_page(page, commit_details) + end + + let(:project) { create(:project) } + let(:repository) { project.repository } + let(:user) { project.owner } + let(:gitlab_shell) { Gitlab::Shell.new } + + subject { GollumWiki.new(project, user) } + + before do + create_temp_repo(subject.send(:path_to_repo)) + end + + describe "#path_with_namespace" do + it "returns the project path with namespace with the .wiki extension" do + subject.path_with_namespace.should == project.path_with_namespace + ".wiki" + end + end + + describe "#url_to_repo" do + it "returns the correct ssh url to the repo" do + subject.url_to_repo.should == gitlab_shell.url_to_repo(subject.path_with_namespace) + end + end + + describe "#ssh_url_to_repo" do + it "equals #url_to_repo" do + subject.ssh_url_to_repo.should == subject.url_to_repo + end + end + + describe "#http_url_to_repo" do + it "provides the full http url to the repo" do + gitlab_url = Gitlab.config.gitlab.url + repo_http_url = "#{gitlab_url}/#{subject.path_with_namespace}.git" + subject.http_url_to_repo.should == repo_http_url + end + end + + describe "#wiki" do + it "contains a Gollum::Wiki instance" do + subject.wiki.should be_a Gollum::Wiki + end + + before do + Gitlab::Shell.any_instance.stub(:add_repository) do + create_temp_repo("#{Rails.root}/tmp/test-git-base-path/non-existant.wiki.git") + end + project.stub(:path_with_namespace).and_return("non-existant") + end + + it "creates a new wiki repo if one does not yet exist" do + wiki = GollumWiki.new(project, user) + wiki.create_page("index", "test content").should_not == false + + FileUtils.rm_rf wiki.send(:path_to_repo) + end + + it "raises CouldNotCreateWikiError if it can't create the wiki repository" do + GollumWiki.any_instance.stub(:init_repo).and_return(false) + expect { GollumWiki.new(project, user).wiki }.to raise_exception(GollumWiki::CouldNotCreateWikiError) + end + end + + describe "#pages" do + before do + create_page("index", "This is an awesome new Gollum Wiki") + @pages = subject.pages + end + + after do + destroy_page(@pages.first.page) + end + + it "returns an array of WikiPage instances" do + @pages.first.should be_a WikiPage + end + + it "returns the correct number of pages" do + @pages.count.should == 1 + end + end + + describe "#find_page" do + before do + create_page("index page", "This is an awesome Gollum Wiki") + end + + after do + destroy_page(subject.pages.first.page) + end + + it "returns the latest version of the page if it exists" do + page = subject.find_page("index page") + page.title.should == "index page" + end + + it "returns nil if the page does not exist" do + subject.find_page("non-existant").should == nil + end + + it "can find a page by slug" do + page = subject.find_page("index-page") + page.title.should == "index page" + end + + it "returns a WikiPage instance" do + page = subject.find_page("index page") + page.should be_a WikiPage + end + end + + describe "#create_page" do + after do + destroy_page(subject.pages.first.page) + end + + it "creates a new wiki page" do + subject.create_page("test page", "this is content").should_not == false + subject.pages.count.should == 1 + end + + it "returns false when a duplicate page exists" do + subject.create_page("test page", "content") + subject.create_page("test page", "content").should == false + end + + it "stores an error message when a duplicate page exists" do + 2.times { subject.create_page("test page", "content") } + subject.error_message.should =~ /Duplicate page:/ + end + + it "sets the correct commit message" do + subject.create_page("test page", "some content", :markdown, "commit message") + subject.pages.first.page.version.message.should == "commit message" + end + end + + describe "#update_page" do + before do + create_page("update-page", "some content") + @gollum_page = subject.wiki.paged("update-page") + subject.update_page(@gollum_page, "some other content", :markdown, "updated page") + @page = subject.pages.first.page + end + + after do + destroy_page(@page) + end + + it "updates the content of the page" do + @page.raw_data.should == "some other content" + end + + it "sets the correct commit message" do + @page.version.message.should == "updated page" + end + end + + describe "#delete_page" do + before do + create_page("index", "some content") + @page = subject.wiki.paged("index") + end + + it "deletes the page" do + subject.delete_page(@page) + subject.pages.count.should == 0 + end + end + +end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 108bc303540..ce1aa05bcd7 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -2,13 +2,14 @@ # # Table name: namespaces # -# id :integer not null, primary key -# name :string(255) not null -# path :string(255) not null -# owner_id :integer not null -# created_at :datetime not null -# updated_at :datetime not null -# type :string(255) +# id :integer not null, primary key +# name :string(255) not null +# path :string(255) not null +# owner_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# type :string(255) +# description :string(255) default(""), not null # require 'spec_helper' @@ -16,18 +17,29 @@ require 'spec_helper' describe Group do let!(:group) { create(:group) } - it { should have_many :projects } + describe "Associations" do + it { should have_many :projects } + it { should have_many :users_groups } + end + it { should validate_presence_of :name } it { should validate_uniqueness_of(:name) } it { should validate_presence_of :path } it { should validate_uniqueness_of(:path) } - it { should validate_presence_of :owner } + it { should_not validate_presence_of :owner } describe :users do - it { group.users.should == [group.owner] } + it { group.users.should == group.owners } end describe :human_name do it { group.human_name.should == group.name } end + + describe :add_users do + let(:user) { create(:user) } + before { group.add_user(user, UsersGroup::MASTER) } + + it { group.users_groups.masters.map(&:user).should include(user) } + end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 10db53e0745..75155d5dc1d 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -9,11 +9,12 @@ # project_id :integer # created_at :datetime not null # updated_at :datetime not null -# closed :boolean default(FALSE), not null # position :integer default(0) # branch_name :string(255) # description :text # milestone_id :integer +# state :string(255) +# iid :integer # require 'spec_helper' @@ -44,34 +45,21 @@ describe Issue do end end - describe '#is_being_closed?' do - it 'returns true if the closed attribute has changed and is now true' do - subject.closed = true - subject.is_being_closed?.should be_true - end - it 'returns false if the closed attribute has changed and is now false' do - issue = create(:closed_issue) - issue.closed = false - issue.is_being_closed?.should be_false - end - it 'returns false if the closed attribute has not changed' do - subject.is_being_closed?.should be_false - end - end + describe '#is_being_reassigned?' do + it 'returns issues assigned to user' do + user = create :user + 2.times do + issue = create :issue, assignee: user + end - describe '#is_being_reopened?' do - it 'returns true if the closed attribute has changed and is now false' do - issue = create(:closed_issue) - issue.closed = false - issue.is_being_reopened?.should be_true - end - it 'returns false if the closed attribute has changed and is now true' do - subject.closed = true - subject.is_being_reopened?.should be_false - end - it 'returns false if the closed attribute has not changed' do - subject.is_being_reopened?.should be_false + Issue.open_for(user).count.should eq 2 end end + + it_behaves_like 'an editable mentionable' do + let(:subject) { create :issue, project: mproject } + let(:backref_text) { "issue ##{subject.iid}" } + let(:set_mentionable_text) { ->(txt){ subject.description = txt } } + end end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 94b952cf932..9c872c02a53 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -2,14 +2,14 @@ # # Table name: keys # -# id :integer not null, primary key -# user_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# key :text -# title :string(255) -# identifier :string(255) -# project_id :integer +# id :integer not null, primary key +# user_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# key :text +# title :string(255) +# type :string(255) +# fingerprint :string(255) # require 'spec_helper' @@ -17,7 +17,6 @@ require 'spec_helper' describe Key do describe "Associations" do it { should belong_to(:user) } - it { should belong_to(:project) } end describe "Mass assignment" do @@ -37,44 +36,36 @@ describe Key do end context "validation of uniqueness" do + let(:user) { create(:user) } - context "as a deploy key" do - let!(:deploy_key) { create(:deploy_key) } - - it "does not accept the same key twice for a project" do - key = build(:key, project: deploy_key.project) - key.should_not be_valid - end - - it "does not accept the same key for another project" do - key = build(:key, project_id: 0) - key.should_not be_valid - end + it "accepts the key once" do + build(:key, user: user).should be_valid end - context "as a personal key" do - let(:user) { create(:user) } - - it "accepts the key once" do - build(:key, user: user).should be_valid - end + it "does not accept the exact same key twice" do + create(:key, user: user) + build(:key, user: user).should_not be_valid + end - it "does not accepts the key twice" do - create(:key, user: user) - build(:key, user: user).should_not be_valid - end + it "does not accept a duplicate key with a different comment" do + create(:key, user: user) + duplicate = build(:key, user: user) + duplicate.key << ' extra comment' + duplicate.should_not be_valid end end context "validate it is a fingerprintable key" do - let(:user) { create(:user) } - it "accepts the fingerprintable key" do - build(:key, user: user).should be_valid + build(:key).should be_valid end - it "rejects the unfingerprintable key" do + it "rejects the unfingerprintable key (contains space in middle)" do build(:key_with_a_space_in_the_middle).should_not be_valid end + + it "rejects the unfingerprintable key (not a key)" do + build(:invalid_key).should_not be_valid + end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 41f4ede5d89..18c32d4fb74 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -2,21 +2,22 @@ # # Table name: merge_requests # -# id :integer not null, primary key -# target_branch :string(255) not null -# source_branch :string(255) not null -# project_id :integer not null -# author_id :integer -# assignee_id :integer -# title :string(255) -# closed :boolean default(FALSE), not null -# created_at :datetime not null -# updated_at :datetime not null -# st_commits :text(2147483647) -# st_diffs :text(2147483647) -# merged :boolean default(FALSE), not null -# state :integer default(1), not null -# milestone_id :integer +# id :integer not null, primary key +# target_branch :string(255) not null +# source_branch :string(255) not null +# source_project_id :integer not null +# author_id :integer +# assignee_id :integer +# title :string(255) +# created_at :datetime not null +# updated_at :datetime not null +# st_commits :text(2147483647) +# st_diffs :text(2147483647) +# milestone_id :integer +# state :string(255) +# merge_status :string(255) +# target_project_id :integer not null +# iid :integer # require 'spec_helper' @@ -32,6 +33,12 @@ describe MergeRequest do it { should_not allow_mass_assignment_of(:project_id) } end + describe "Respond to" do + it { should respond_to(:unchecked?) } + it { should respond_to(:can_be_merged?) } + it { should respond_to(:cannot_be_merged?) } + end + describe 'modules' do it { should include_module(Issuable) } end @@ -40,7 +47,7 @@ describe MergeRequest do let!(:merge_request) { create(:merge_request) } before do - merge_request.stub(:commits) { [merge_request.project.repository.commit] } + merge_request.stub(:commits) { [merge_request.source_project.repository.commit] } create(:note, commit_id: merge_request.commits.first.id, noteable_type: 'Commit') create(:note, noteable: merge_request) end @@ -63,34 +70,67 @@ describe MergeRequest do end end - describe '#is_being_closed?' do - it 'returns true if the closed attribute has changed and is now true' do - subject.closed = true - subject.is_being_closed?.should be_true + describe '#for_fork?' do + it 'returns true if the merge request is for a fork' do + subject.source_project = create(:source_project) + subject.target_project = create(:target_project) + + subject.for_fork?.should be_true + end + it 'returns false if is not for a fork' do + subject.source_project = create(:source_project) + subject.target_project = subject.source_project + subject.for_fork?.should be_false + end + end + + describe '#allow_source_branch_removal?' do + it 'should not allow removal when mr is a fork' do + + subject.disallow_source_branch_removal?.should be_true end - it 'returns false if the closed attribute has changed and is now false' do - merge_request = create(:closed_merge_request) - merge_request.closed = false - merge_request.is_being_closed?.should be_false + it 'should not allow removal when the mr is not a fork, but the source branch is the root reference' do + subject.target_project = subject.source_project + subject.source_branch = subject.source_project.repository.root_ref + subject.disallow_source_branch_removal?.should be_true end - it 'returns false if the closed attribute has not changed' do - subject.is_being_closed?.should be_false + + it 'should not disallow removal when the mr is not a fork, and but source branch is not the root reference' do + subject.target_project = subject.source_project + subject.source_branch = "Something Different #{subject.source_project.repository.root_ref}" + subject.for_fork?.should be_false + subject.disallow_source_branch_removal?.should be_false end end + describe 'detection of issues to be closed' do + let(:issue0) { create :issue, project: subject.project } + let(:issue1) { create :issue, project: subject.project } + let(:commit0) { mock('commit0', closes_issues: [issue0]) } + let(:commit1) { mock('commit1', closes_issues: [issue0]) } + let(:commit2) { mock('commit2', closes_issues: [issue1]) } - describe '#is_being_reopened?' do - it 'returns true if the closed attribute has changed and is now false' do - merge_request = create(:closed_merge_request) - merge_request.closed = false - merge_request.is_being_reopened?.should be_true + before do + subject.stub(unmerged_commits: [commit0, commit1, commit2]) end - it 'returns false if the closed attribute has changed and is now true' do - subject.closed = true - subject.is_being_reopened?.should be_false + + it 'accesses the set of issues that will be closed on acceptance' do + subject.project.default_branch = subject.target_branch + + subject.closes_issues.should == [issue0, issue1].sort_by(&:id) end - it 'returns false if the closed attribute has not changed' do - subject.is_being_reopened?.should be_false + + it 'only lists issues as to be closed if it targets the default branch' do + subject.project.default_branch = 'master' + subject.target_branch = 'something-else' + + subject.closes_issues.should be_empty end end + + it_behaves_like 'an editable mentionable' do + let(:subject) { create :merge_request, source_project: mproject, target_project: mproject } + let(:backref_text) { "merge request !#{subject.iid}" } + let(:set_mentionable_text) { ->(txt){ subject.title = txt } } + end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 2ea2c56a6f7..b41012a3b8c 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -7,9 +7,10 @@ # project_id :integer not null # description :text # due_date :date -# closed :boolean default(FALSE), not null # created_at :datetime not null # updated_at :datetime not null +# state :string(255) +# iid :integer # require 'spec_helper' @@ -25,9 +26,9 @@ describe Milestone do end describe "Validation" do + before { subject.stub(set_iid: false) } it { should validate_presence_of(:title) } it { should validate_presence_of(:project) } - it { should ensure_inclusion_of(:closed).in_array([true, false]) } end let(:milestone) { create(:milestone) } @@ -40,8 +41,7 @@ describe Milestone do end it "should count closed issues" do - IssueObserver.current_user = issue.author - issue.update_attributes(closed: true) + issue.close milestone.issues << issue milestone.percent_complete.should == 100 end @@ -96,7 +96,7 @@ describe Milestone do describe :items_count do before do milestone.issues << create(:issue) - milestone.issues << create(:issue, closed: true) + milestone.issues << create(:closed_issue) milestone.merge_requests << create(:merge_request) end @@ -110,7 +110,35 @@ describe Milestone do it { milestone.can_be_closed?.should be_true } end - describe :open? do - it { milestone.open?.should be_true } + describe :is_empty? do + before do + issue = create :closed_issue, milestone: milestone + merge_request = create :merge_request, milestone: milestone + end + + it 'Should return total count of issues and merge requests assigned to milestone' do + milestone.total_items_count.should eq 2 + end + end + + describe :can_be_closed? do + before do + milestone = create :milestone + create :closed_issue, milestone: milestone + + issue = create :issue + end + + it 'should be true if milestone active and all nested issues closed' do + milestone.can_be_closed?.should be_true + end + + it 'should be false if milestone active and not all nested issues closed' do + issue.milestone = milestone + issue.save + + milestone.can_be_closed?.should be_false + end end + end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index d0de4a7b7fb..0a0509bcd27 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -2,13 +2,14 @@ # # Table name: namespaces # -# id :integer not null, primary key -# name :string(255) not null -# path :string(255) not null -# owner_id :integer not null -# created_at :datetime not null -# updated_at :datetime not null -# type :string(255) +# id :integer not null, primary key +# name :string(255) not null +# path :string(255) not null +# owner_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# type :string(255) +# description :string(255) default(""), not null # require 'spec_helper' @@ -58,8 +59,8 @@ describe Namespace do @namespace.stub(path_changed?: true) end - it "should raise error when dirtory exists" do - expect { @namespace.move_dir }.to raise_error("Already exists") + it "should raise error when directory exists" do + expect { @namespace.move_dir }.to raise_error("namespace directory cannot be moved") end it "should move dir if path changed" do diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 52e24a78eb4..ef143debcc1 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -13,6 +13,7 @@ # line_code :string(255) # commit_id :string(255) # noteable_id :integer +# st_diff :text # require 'spec_helper' @@ -71,7 +72,6 @@ describe Note do end let(:project) { create(:project) } - let(:commit) { project.repository.commit } describe "Commit notes" do let!(:note) { create(:note_on_commit, note: "+1 from me") } @@ -130,7 +130,7 @@ describe Note do describe "Merge request notes" do let!(:note) { create(:note_on_merge_request, note: "+1 from me") } - it "should not be votable" do + it "should be votable" do note.should be_votable end end @@ -144,12 +144,12 @@ describe Note do end describe '#create_status_change_note' do - let(:project) { create(:project) } - let(:thing) { create(:issue, project: project) } - let(:author) { create(:user) } - let(:status) { 'new_status' } + let(:project) { create(:project) } + let(:thing) { create(:issue, project: project) } + let(:author) { create(:user) } + let(:status) { 'new_status' } - subject { Note.create_status_change_note(thing, author, status) } + subject { Note.create_status_change_note(thing, project, author, status, nil) } it 'creates and saves a Note' do should be_a Note @@ -157,9 +157,105 @@ describe Note do end its(:noteable) { should == thing } - its(:project) { should == thing.project } - its(:author) { should == author } - its(:note) { should =~ /Status changed to #{status}/ } + its(:project) { should == thing.project } + its(:author) { should == author } + its(:note) { should =~ /Status changed to #{status}/ } + + it 'appends a back-reference if a closing mentionable is supplied' do + commit = double('commit', gfm_reference: 'commit 123456') + n = Note.create_status_change_note(thing, project, author, status, commit) + + n.note.should =~ /Status changed to #{status} by commit 123456/ + end + end + + describe '#create_cross_reference_note' do + let(:project) { create(:project_with_code) } + let(:author) { create(:user) } + let(:issue) { create(:issue, project: project) } + let(:mergereq) { create(:merge_request, target_project: project) } + let(:commit) { project.repository.commit } + + # Test all of {issue, merge request, commit} in both the referenced and referencing + # roles, to ensure that the correct information can be inferred from any argument. + + context 'issue from a merge request' do + subject { Note.create_cross_reference_note(issue, mergereq, author, project) } + + it { should be_valid } + its(:noteable) { should == issue } + its(:project) { should == issue.project } + its(:author) { should == author } + its(:note) { should == "_mentioned in merge request !#{mergereq.iid}_" } + end + + context 'issue from a commit' do + subject { Note.create_cross_reference_note(issue, commit, author, project) } + + it { should be_valid } + its(:noteable) { should == issue } + its(:note) { should == "_mentioned in commit #{commit.sha[0..5]}_" } + end + + context 'merge request from an issue' do + subject { Note.create_cross_reference_note(mergereq, issue, author, project) } + + it { should be_valid } + its(:noteable) { should == mergereq } + its(:project) { should == mergereq.project } + its(:note) { should == "_mentioned in issue ##{issue.iid}_" } + end + + context 'commit from a merge request' do + subject { Note.create_cross_reference_note(commit, mergereq, author, project) } + + it { should be_valid } + its(:noteable) { should == commit } + its(:project) { should == project } + its(:note) { should == "_mentioned in merge request !#{mergereq.iid}_" } + end + end + + describe '#cross_reference_exists?' do + let(:project) { create :project } + let(:author) { create :user } + let(:issue) { create :issue } + let(:commit0) { double 'commit0', gfm_reference: 'commit 123456' } + let(:commit1) { double 'commit1', gfm_reference: 'commit 654321' } + + before do + Note.create_cross_reference_note(issue, commit0, author, project) + end + + it 'detects if a mentionable has already been mentioned' do + Note.cross_reference_exists?(issue, commit0).should be_true + end + + it 'detects if a mentionable has not already been mentioned' do + Note.cross_reference_exists?(issue, commit1).should be_false + end + end + + describe '#system?' do + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project) } + let(:other) { create(:issue, project: project) } + let(:author) { create(:user) } + + it 'should recognize user-supplied notes as non-system' do + @note = create(:note_on_issue) + @note.should_not be_system + end + + it 'should identify status-change notes as system notes' do + @note = Note.create_status_change_note(issue, project, author, 'closed', nil) + @note.should be_system + end + + it 'should identify cross-reference notes as system notes' do + @note = Note.create_cross_reference_note(issue, other, author, project) + @note.should be_system + end end describe :authorization do @@ -207,4 +303,11 @@ describe Note do it { @abilities.allowed?(@u3, :admin_note, @p1).should be_false } end end + + it_behaves_like 'an editable mentionable' do + let(:issue) { create :issue, project: project } + let(:subject) { create :note, noteable: issue, project: project } + let(:backref_text) { issue.gfm_reference } + let(:set_mentionable_text) { ->(txt) { subject.note = txt } } + end end diff --git a/spec/models/project_hooks_spec.rb b/spec/models/project_hooks_spec.rb deleted file mode 100644 index 65205538213..00000000000 --- a/spec/models/project_hooks_spec.rb +++ /dev/null @@ -1,128 +0,0 @@ -require 'spec_helper' - -describe Project, "Hooks" do - let(:project) { create(:project) } - - before do - @key = create(:key, user: project.owner) - @user = @key.user - @key_id = @key.identifier - end - - describe "Post Receive Event" do - it "should create push event" do - oldrev, newrev, ref = '00000000000000000000000000000000', 'newrev', 'refs/heads/master' - data = project.post_receive_data(oldrev, newrev, ref, @user) - - project.observe_push(data) - event = Event.last - - event.should_not be_nil - event.project.should == project - event.action.should == Event::PUSHED - event.data.should == data - end - end - - describe "Project hooks" do - context "with no web hooks" do - it "raises no errors" do - lambda { - project.execute_hooks({}) - }.should_not raise_error - end - end - - context "with web hooks" do - before do - @project_hook = create(:project_hook) - @project_hook_2 = create(:project_hook) - project.hooks << [@project_hook, @project_hook_2] - - stub_request(:post, @project_hook.url) - stub_request(:post, @project_hook_2.url) - end - - it "executes multiple web hook" do - @project_hook.should_receive(:async_execute).once - @project_hook_2.should_receive(:async_execute).once - - project.trigger_post_receive('oldrev', 'newrev', 'refs/heads/master', @user) - end - end - - context "does not execute web hooks" do - before do - @project_hook = create(:project_hook) - project.hooks << [@project_hook] - end - - it "when pushing a branch for the first time" do - @project_hook.should_not_receive(:execute) - project.trigger_post_receive('00000000000000000000000000000000', 'newrev', 'refs/heads/master', @user) - end - - it "when pushing tags" do - @project_hook.should_not_receive(:execute) - project.trigger_post_receive('oldrev', 'newrev', 'refs/tags/v1.0.0', @user) - end - end - - context "when pushing new branches" do - - end - - context "when gathering commit data" do - before do - @oldrev, @newrev, @ref = project.repository.fresh_commits(2).last.sha, - project.repository.fresh_commits(2).first.sha, 'refs/heads/master' - @commit = project.repository.fresh_commits(2).first - - # Fill nil/empty attributes - project.description = "This is a description" - - @data = project.post_receive_data(@oldrev, @newrev, @ref, @user) - end - - subject { @data } - - it { should include(before: @oldrev) } - it { should include(after: @newrev) } - it { should include(ref: @ref) } - it { should include(user_id: project.owner.id) } - it { should include(user_name: project.owner.name) } - - context "with repository data" do - subject { @data[:repository] } - - it { should include(name: project.name) } - it { should include(url: project.url_to_repo) } - it { should include(description: project.description) } - it { should include(homepage: project.web_url) } - end - - context "with commits" do - subject { @data[:commits] } - - it { should be_an(Array) } - it { should have(1).element } - - context "the commit" do - subject { @data[:commits].first } - - it { should include(id: @commit.id) } - it { should include(message: @commit.safe_message) } - it { should include(timestamp: @commit.date.xmlschema) } - it { should include(url: "#{Gitlab.config.gitlab.url}/#{project.code}/commit/#{@commit.id}") } - - context "with a author" do - subject { @data[:commits].first[:author] } - - it { should include(name: @commit.author_name) } - it { should include(email: @commit.author_email) } - end - end - end - end - end -end diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb new file mode 100644 index 00000000000..d3a46ebbb84 --- /dev/null +++ b/spec/models/project_snippet_spec.rb @@ -0,0 +1,32 @@ +# == Schema Information +# +# Table name: snippets +# +# id :integer not null, primary key +# title :string(255) +# content :text(2147483647) +# author_id :integer not null +# project_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# file_name :string(255) +# expires_at :datetime +# private :boolean default(TRUE), not null +# type :string(255) +# + +require 'spec_helper' + +describe ProjectSnippet do + describe "Associations" do + it { should belong_to(:project) } + end + + describe "Mass assignment" do + it { should_not allow_mass_assignment_of(:project_id) } + end + + describe "Validation" do + it { should validate_presence_of(:project) } + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 4b620a2fa3e..47ae760a7ed 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -16,11 +16,20 @@ # wiki_enabled :boolean default(TRUE), not null # namespace_id :integer # public :boolean default(FALSE), not null +# issues_tracker :string(255) default("gitlab"), not null +# issues_tracker_id :string(255) +# snippets_enabled :boolean default(TRUE), not null +# last_activity_at :datetime +# imported :boolean default(FALSE), not null +# import_url :string(255) # require 'spec_helper' describe Project do + before(:each) { enable_observers } + after(:each) { disable_observers } + describe "Associations" do it { should belong_to(:group) } it { should belong_to(:namespace) } @@ -32,11 +41,12 @@ describe Project do it { should have_many(:milestones).dependent(:destroy) } it { should have_many(:users_projects).dependent(:destroy) } it { should have_many(:notes).dependent(:destroy) } - it { should have_many(:snippets).dependent(:destroy) } - it { should have_many(:deploy_keys).dependent(:destroy) } + it { should have_many(:snippets).class_name('ProjectSnippet').dependent(:destroy) } + it { should have_many(:deploy_keys_projects).dependent(:destroy) } + it { should have_many(:deploy_keys) } it { should have_many(:hooks).dependent(:destroy) } - it { should have_many(:wikis).dependent(:destroy) } it { should have_many(:protected_branches).dependent(:destroy) } + it { should have_one(:forked_project_link).dependent(:destroy) } end describe "Mass assignment" do @@ -48,23 +58,22 @@ describe Project do let!(:project) { create(:project) } it { should validate_presence_of(:name) } - it { should validate_uniqueness_of(:name) } + it { should validate_uniqueness_of(:name).scoped_to(:namespace_id) } it { should ensure_length_of(:name).is_within(0..255) } it { should validate_presence_of(:path) } - it { should validate_uniqueness_of(:path) } + it { should validate_uniqueness_of(:path).scoped_to(:namespace_id) } it { should ensure_length_of(:path).is_within(0..255) } it { should ensure_length_of(:description).is_within(0..2000) } it { should validate_presence_of(:creator) } - it { should ensure_inclusion_of(:issues_enabled).in_array([true, false]) } - it { should ensure_inclusion_of(:wall_enabled).in_array([true, false]) } - it { should ensure_inclusion_of(:merge_requests_enabled).in_array([true, false]) } - it { should ensure_inclusion_of(:wiki_enabled).in_array([true, false]) } + it { should ensure_length_of(:issues_tracker_id).is_within(0..255) } + it { should validate_presence_of(:namespace) } it "should not allow new projects beyond user limits" do - project.stub(:creator).and_return(double(can_create_project?: false, projects_limit: 1)) - project.should_not be_valid - project.errors[:base].first.should match(/Your own projects limit is 1/) + project2 = build(:project) + project2.stub(:creator).and_return(double(can_create_project?: false, projects_limit: 0)) + project2.should_not be_valid + project2.errors[:limit_reached].first.should match(/Your own projects limit is 0/) end end @@ -72,14 +81,10 @@ describe Project do it { should respond_to(:url_to_repo) } it { should respond_to(:repo_exists?) } it { should respond_to(:satellite) } - it { should respond_to(:observe_push) } it { should respond_to(:update_merge_requests) } it { should respond_to(:execute_hooks) } - it { should respond_to(:post_receive_data) } - it { should respond_to(:trigger_post_receive) } it { should respond_to(:transfer) } it { should respond_to(:name_with_namespace) } - it { should respond_to(:namespace_owner) } it { should respond_to(:owner) } it { should respond_to(:path_with_namespace) } end @@ -95,11 +100,11 @@ describe Project do end describe "last_activity methods" do - let(:project) { create(:project) } + let(:project) { create(:project) } let(:last_event) { double(created_at: Time.now) } describe "last_activity" do - it "should alias last_activity to last_event"do + it "should alias last_activity to last_event" do project.stub(last_event: last_event) project.last_activity.should == last_event end @@ -107,8 +112,8 @@ describe Project do describe 'last_activity_date' do it 'returns the creation date of the project\'s last event if present' do - project.stub(last_event: last_event) - project.last_activity_date.should == last_event.created_at + last_activity_event = create(:event, project: project) + project.last_activity_at.to_i.should == last_event.created_at.to_i end it 'returns the project\'s last update date if it has no events' do @@ -118,13 +123,10 @@ describe Project do end describe :update_merge_requests do - let(:project) { create(:project) } + let(:project) { create(:project_with_code) } before do - @merge_request = create(:merge_request, - project: project, - merged: false, - closed: false) + @merge_request = create(:merge_request, source_project: project, target_project: project) @key = create(:key, user_id: project.owner.id) end @@ -133,8 +135,7 @@ describe Project do @merge_request.last_commit.id.should == "bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a" project.update_merge_requests("8716fc78f3c65bbf7bcf7b574febd583bc5d2812", "bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a", "refs/heads/stable", @key.user) @merge_request.reload - @merge_request.merged.should be_true - @merge_request.closed.should be_true + @merge_request.merged?.should be_true end it "should update merge request commits with new one if pushed to source branch" do @@ -156,15 +157,6 @@ describe Project do it { Project.find_with_namespace('gitlab/gitlab-ci').should == @project } it { Project.find_with_namespace('gitlab-ci').should be_nil } end - - context 'w/o namespace' do - before do - @project = create(:project, name: 'gitlab-ci') - end - - it { Project.find_with_namespace('gitlab-ci').should == @project } - it { Project.find_with_namespace('gitlab/gitlab-ci').should be_nil } - end end describe :to_param do @@ -176,14 +168,6 @@ describe Project do it { @project.to_param.should == "gitlab/gitlab-ci" } end - - context 'w/o namespace' do - before do - @project = create(:project, name: 'gitlab-ci') - end - - it { @project.to_param.should == "gitlab-ci" } - end end describe :repository do @@ -192,9 +176,69 @@ describe Project do it "should return valid repo" do project.repository.should be_kind_of(Repository) end + end + + describe :issue_exists? do + let(:project) { create(:project) } + let(:existed_issue) { create(:issue, project: project) } + let(:not_existed_issue) { create(:issue) } + let(:ext_project) { create(:redmine_project) } + + it "should be true or if used internal tracker and issue exists" do + project.issue_exists?(existed_issue.iid).should be_true + end + + it "should be false or if used internal tracker and issue not exists" do + project.issue_exists?(not_existed_issue.iid).should be_false + end + + it "should always be true if used other tracker" do + ext_project.issue_exists?(rand(100)).should be_true + end + end + + describe :used_default_issues_tracker? do + let(:project) { create(:project) } + let(:ext_project) { create(:redmine_project) } + + it "should be true if used internal tracker" do + project.used_default_issues_tracker?.should be_true + end + + it "should be false if used other tracker" do + ext_project.used_default_issues_tracker?.should be_false + end + end + + describe :can_have_issues_tracker_id? do + let(:project) { create(:project) } + let(:ext_project) { create(:redmine_project) } + + it "should be true for projects with external issues tracker if issues enabled" do + ext_project.can_have_issues_tracker_id?.should be_true + end + + it "should be false for projects with internal issue tracker if issues enabled" do + project.can_have_issues_tracker_id?.should be_false + end + + it "should be always false if issues disabled" do + project.issues_enabled = false + ext_project.issues_enabled = false - it "should return nil" do - Project.new(path: "empty").repository.should be_nil + project.can_have_issues_tracker_id?.should be_false + ext_project.can_have_issues_tracker_id?.should be_false end end + + describe :open_branches do + let(:project) { create(:project_with_code) } + + before do + project.protected_branches.create(name: 'master') + end + + it { project.open_branches.map(&:name).should include('bootstrap') } + it { project.open_branches.map(&:name).should_not include('master') } + end end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 7803811f395..3e3543e85e1 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -10,9 +10,6 @@ describe ProjectTeam do it { should respond_to(:masters) } it { should respond_to(:reporters) } it { should respond_to(:guests) } - it { should respond_to(:repository_writers) } - it { should respond_to(:repository_masters) } - it { should respond_to(:repository_readers) } end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb deleted file mode 100644 index 71f9b964e70..00000000000 --- a/spec/models/repository_spec.rb +++ /dev/null @@ -1,105 +0,0 @@ -require "spec_helper" - -describe Repository do - let(:project) { create(:project) } - let(:repository) { project.repository } - - describe "Respond to" do - subject { repository } - - it { should respond_to(:repo) } - it { should respond_to(:tree) } - it { should respond_to(:root_ref) } - it { should respond_to(:tags) } - it { should respond_to(:commit) } - it { should respond_to(:commits) } - it { should respond_to(:commits_between) } - it { should respond_to(:commits_with_refs) } - it { should respond_to(:commits_since) } - it { should respond_to(:commits_between) } - end - - - describe "#discover_default_branch" do - let(:master) { 'master' } - let(:stable) { 'stable' } - - it "returns 'master' when master exists" do - repository.should_receive(:branch_names).at_least(:once).and_return([stable, master]) - repository.discover_default_branch.should == 'master' - end - - it "returns non-master when master exists but default branch is set to something else" do - repository.root_ref = 'stable' - repository.should_receive(:branch_names).at_least(:once).and_return([stable, master]) - repository.discover_default_branch.should == 'stable' - end - - it "returns a non-master branch when only one exists" do - repository.should_receive(:branch_names).at_least(:once).and_return([stable]) - repository.discover_default_branch.should == 'stable' - end - - it "returns nil when no branch exists" do - repository.should_receive(:branch_names).at_least(:once).and_return([]) - repository.discover_default_branch.should be_nil - end - end - - describe :commit do - it "should return first head commit if without params" do - repository.commit.id.should == repository.repo.commits.first.id - end - - it "should return valid commit" do - repository.commit(ValidCommit::ID).should be_valid_commit - end - - it "should return nil" do - repository.commit("+123_4532530XYZ").should be_nil - end - end - - describe :tree do - before do - @commit = repository.commit(ValidCommit::ID) - end - - it "should raise error w/o arguments" do - lambda { repository.tree }.should raise_error - end - - it "should return root tree for commit" do - tree = repository.tree(@commit) - tree.contents.size.should == ValidCommit::FILES_COUNT - tree.contents.map(&:name).should == ValidCommit::FILES - end - - it "should return root tree for commit with correct path" do - tree = repository.tree(@commit, ValidCommit::C_FILE_PATH) - tree.contents.map(&:name).should == ValidCommit::C_FILES - end - - it "should return root tree for commit with incorrect path" do - repository.tree(@commit, "invalid_path").should be_nil - end - end - - describe "fresh commits" do - it { repository.fresh_commits(3).count.should == 3 } - it { repository.fresh_commits.first.id.should == "bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a" } - it { repository.fresh_commits.last.id.should == "f403da73f5e62794a0447aca879360494b08f678" } - end - - describe "commits_between" do - subject do - commits = repository.commits_between("3a4b4fb4cde7809f033822a171b9feae19d41fff", - "8470d70da67355c9c009e4401746b1d5410af2e3") - commits.map { |c| c.id } - end - - it { should have(3).elements } - it { should include("f0f14c8eaba69ebddd766498a9d0b0e79becd633") } - it { should_not include("bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a") } - end -end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 1a58f680baf..667c80bcf19 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -11,11 +11,14 @@ # updated_at :datetime not null # active :boolean default(FALSE), not null # project_url :string(255) +# subdomain :string(255) +# room :string(255) # require 'spec_helper' describe Service do + describe "Associations" do it { should belong_to :project } it { should have_one :service_hook } @@ -24,4 +27,40 @@ describe Service do describe "Mass assignment" do it { should_not allow_mass_assignment_of(:project_id) } end + + describe "Test Button" do + before do + @service = Service.new + end + + describe "Testable" do + let (:project) { create :project } + + before do + @service.stub( + project: project + ) + @testable = @service.can_test? + end + + describe :can_test do + it { @testable.should == false } + end + end + + describe "With commits" do + let (:project) { create :project_with_code } + + before do + @service.stub( + project: project + ) + @testable = @service.can_test? + end + + describe :can_test do + it { @testable.should == true } + end + end + end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index e4d1934829f..5fa397207c7 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -4,32 +4,31 @@ # # id :integer not null, primary key # title :string(255) -# content :text +# content :text(2147483647) # author_id :integer not null -# project_id :integer not null +# project_id :integer # created_at :datetime not null # updated_at :datetime not null # file_name :string(255) # expires_at :datetime +# private :boolean default(TRUE), not null +# type :string(255) # require 'spec_helper' describe Snippet do describe "Associations" do - it { should belong_to(:project) } it { should belong_to(:author).class_name('User') } it { should have_many(:notes).dependent(:destroy) } end describe "Mass assignment" do it { should_not allow_mass_assignment_of(:author_id) } - it { should_not allow_mass_assignment_of(:project_id) } end describe "Validation" do it { should validate_presence_of(:author) } - it { should validate_presence_of(:project) } it { should validate_presence_of(:title) } it { should ensure_length_of(:title).is_within(0..255) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8ab0a0343bb..f6c9f82c4ee 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -22,10 +22,8 @@ # linkedin :string(255) default(""), not null # twitter :string(255) default(""), not null # authentication_token :string(255) -# dark_scheme :boolean default(FALSE), not null # theme_id :integer default(1), not null # bio :string(255) -# blocked :boolean default(FALSE), not null # failed_attempts :integer default(0) # locked_at :datetime # extern_uid :string(255) @@ -33,6 +31,11 @@ # username :string(255) # can_create_group :boolean default(TRUE), not null # can_create_team :boolean default(TRUE), not null +# state :string(255) +# color_scheme_id :integer default(1), not null +# notification_level :integer default(1), not null +# password_expires_at :datetime +# created_by_id :integer # require 'spec_helper' @@ -40,6 +43,7 @@ require 'spec_helper' describe User do describe "Associations" do it { should have_one(:namespace) } + it { should have_many(:snippets).class_name('Snippet').dependent(:destroy) } it { should have_many(:users_projects).dependent(:destroy) } it { should have_many(:groups) } it { should have_many(:keys).dependent(:destroy) } @@ -69,28 +73,10 @@ describe User do describe "Respond to" do it { should respond_to(:is_admin?) } - it { should respond_to(:identifier) } it { should respond_to(:name) } it { should respond_to(:private_token) } end - describe '#identifier' do - it "should return valid identifier" do - user = build(:user, email: "test@mail.com") - user.identifier.should == "test_mail_com" - end - - it "should return identifier without + sign" do - user = build(:user, email: "test+foo@mail.com") - user.identifier.should == "test_foo_mail_com" - end - - it "should conform to Gitolite's required identifier pattern" do - user = build(:user, email: "_test@example.com") - user.identifier.should == 'test_example_com' - end - end - describe '#generate_password' do it "should execute callback when force_random_password specified" do user = build(:user, force_random_password: true) @@ -122,26 +108,52 @@ describe User do ActiveRecord::Base.observers.enable(:user_observer) @user = create :user @project = create :project, namespace: @user.namespace + @project_2 = create :project, group: create(:group) # Grant MASTER access to the user + @project_3 = create :project, group: create(:group) # Grant DEVELOPER access to the user + + @project_2.team << [@user, :master] + @project_3.team << [@user, :developer] end it { @user.authorized_projects.should include(@project) } + it { @user.authorized_projects.should include(@project_2) } + it { @user.authorized_projects.should include(@project_3) } it { @user.owned_projects.should include(@project) } + it { @user.owned_projects.should_not include(@project_2) } + it { @user.owned_projects.should_not include(@project_3) } it { @user.personal_projects.should include(@project) } + it { @user.personal_projects.should_not include(@project_2) } + it { @user.personal_projects.should_not include(@project_3) } end describe 'groups' do before do ActiveRecord::Base.observers.enable(:user_observer) @user = create :user - @group = create :group, owner: @user + @group = create :group + @group.add_owner(@user) end it { @user.several_namespaces?.should be_true } - it { @user.namespaces.should == [@user.namespace, @group] } + it { @user.namespaces.should include(@user.namespace) } it { @user.authorized_groups.should == [@group] } it { @user.owned_groups.should == [@group] } end + describe 'group multiple owners' do + before do + ActiveRecord::Base.observers.enable(:user_observer) + @user = create :user + @user2 = create :user + @group = create :group + @group.add_owner(@user) + + @group.add_user(@user2, UsersGroup::OWNER) + end + + it { @user2.several_namespaces?.should be_true } + end + describe 'namespaced' do before do ActiveRecord::Base.observers.enable(:user_observer) @@ -158,7 +170,7 @@ describe User do it "should block user" do user.block - user.blocked.should be_true + user.blocked?.should be_true end end @@ -167,13 +179,13 @@ describe User do User.delete_all @user = create :user @admin = create :user, admin: true - @blocked = create :user, blocked: true + @blocked = create :user, state: :blocked end it { User.filter("admins").should == [@admin] } it { User.filter("blocked").should == [@blocked] } - it { User.filter("wop").should == [@user, @admin, @blocked] } - it { User.filter(nil).should == [@user, @admin] } + it { User.filter("wop").should include(@user, @admin, @blocked) } + it { User.filter(nil).should include(@user, @admin) } end describe :not_in_project do @@ -183,16 +195,85 @@ describe User do @project = create :project end - it { User.not_in_project(@project).should == [@user, @project.owner] } + it { User.not_in_project(@project).should include(@user, @project.owner) } end - describe 'normal user' do - let(:user) { create(:user, name: 'John Smith') } + describe 'user creation' do + describe 'normal user' do + let(:user) { create(:user, name: 'John Smith') } - it { user.is_admin?.should be_false } - it { user.require_ssh_key?.should be_true } - it { user.can_create_group?.should be_true } - it { user.can_create_project?.should be_true } - it { user.first_name.should == 'John' } + it { user.is_admin?.should be_false } + it { user.require_ssh_key?.should be_true } + it { user.can_create_group?.should be_true } + it { user.can_create_project?.should be_true } + it { user.first_name.should == 'John' } + end + + describe 'without defaults' do + let(:user) { User.new } + + it "should not apply defaults to user" do + user.projects_limit.should == 10 + user.can_create_group.should be_true + user.theme_id.should == Gitlab::Theme::BASIC + end + end + context 'as admin' do + describe 'with defaults' do + let(:user) { User.build_user({}, as: :admin) } + + it "should apply defaults to user" do + user.projects_limit.should == Gitlab.config.gitlab.default_projects_limit + user.can_create_group.should == Gitlab.config.gitlab.default_can_create_group + user.theme_id.should == Gitlab.config.gitlab.default_theme + end + end + + describe 'with default overrides' do + let(:user) { User.build_user({projects_limit: 123, can_create_group: true, can_create_team: true, theme_id: Gitlab::Theme::BASIC}, as: :admin) } + + it "should apply defaults to user" do + Gitlab.config.gitlab.default_projects_limit.should_not == 123 + Gitlab.config.gitlab.default_can_create_group.should_not be_true + Gitlab.config.gitlab.default_theme.should_not == Gitlab::Theme::BASIC + user.projects_limit.should == 123 + user.can_create_group.should be_true + user.theme_id.should == Gitlab::Theme::BASIC + end + end + end + + context 'as user' do + describe 'with defaults' do + let(:user) { User.build_user } + + it "should apply defaults to user" do + user.projects_limit.should == Gitlab.config.gitlab.default_projects_limit + user.can_create_group.should == Gitlab.config.gitlab.default_can_create_group + user.theme_id.should == Gitlab.config.gitlab.default_theme + end + end + + describe 'with default overrides' do + let(:user) { User.build_user(projects_limit: 123, can_create_group: true, theme_id: Gitlab::Theme::BASIC) } + + it "should apply defaults to user" do + user.projects_limit.should == Gitlab.config.gitlab.default_projects_limit + user.can_create_group.should == Gitlab.config.gitlab.default_can_create_group + user.theme_id.should == Gitlab.config.gitlab.default_theme + end + end + end + end + + describe 'by_username_or_id' do + let(:user1) { create(:user, username: 'foo') } + + it "should get the correct user" do + User.by_username_or_id(user1.id).should == user1 + User.by_username_or_id('foo').should == user1 + User.by_username_or_id(-1).should be_nil + User.by_username_or_id('bar').should be_nil + end end end diff --git a/spec/models/user_team_project_relationship_spec.rb b/spec/models/user_team_project_relationship_spec.rb deleted file mode 100644 index 86150cf305f..00000000000 --- a/spec/models/user_team_project_relationship_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# == Schema Information -# -# Table name: user_team_project_relationships -# -# id :integer not null, primary key -# project_id :integer -# user_team_id :integer -# greatest_access :integer -# created_at :datetime not null -# updated_at :datetime not null -# - -require 'spec_helper' - -describe UserTeamProjectRelationship do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/user_team_spec.rb b/spec/models/user_team_spec.rb deleted file mode 100644 index 76d47f41498..00000000000 --- a/spec/models/user_team_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# == Schema Information -# -# Table name: user_teams -# -# id :integer not null, primary key -# name :string(255) -# path :string(255) -# owner_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# - -require 'spec_helper' - -describe UserTeam do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/user_team_user_relationship_spec.rb b/spec/models/user_team_user_relationship_spec.rb deleted file mode 100644 index 981ad1e8873..00000000000 --- a/spec/models/user_team_user_relationship_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# == Schema Information -# -# Table name: user_team_user_relationships -# -# id :integer not null, primary key -# user_id :integer -# user_team_id :integer -# group_admin :boolean -# permission :integer -# created_at :datetime not null -# updated_at :datetime not null -# - -require 'spec_helper' - -describe UserTeamUserRelationship do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/users_group_spec.rb b/spec/models/users_group_spec.rb new file mode 100644 index 00000000000..9264f2bc034 --- /dev/null +++ b/spec/models/users_group_spec.rb @@ -0,0 +1,40 @@ +# == Schema Information +# +# Table name: users_groups +# +# id :integer not null, primary key +# group_access :integer not null +# group_id :integer not null +# user_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# notification_level :integer default(3), not null +# + +require 'spec_helper' + +describe UsersGroup do + describe "Associations" do + it { should belong_to(:group) } + it { should belong_to(:user) } + end + + describe "Mass assignment" do + it { should_not allow_mass_assignment_of(:group_id) } + end + + describe "Validation" do + let!(:users_group) { create(:users_group) } + + it { should validate_presence_of(:user_id) } + it { should validate_uniqueness_of(:user_id).scoped_to(:group_id).with_message(/already exists/) } + + it { should validate_presence_of(:group_id) } + it { should ensure_inclusion_of(:group_access).in_array(UsersGroup.group_access_roles.values) } + end + + describe "Delegate methods" do + it { should respond_to(:user_name) } + it { should respond_to(:user_email) } + end +end diff --git a/spec/models/users_project_spec.rb b/spec/models/users_project_spec.rb index e8f5b647ce0..e289a592b03 100644 --- a/spec/models/users_project_spec.rb +++ b/spec/models/users_project_spec.rb @@ -2,12 +2,13 @@ # # Table name: users_projects # -# id :integer not null, primary key -# user_id :integer not null -# project_id :integer not null -# created_at :datetime not null -# updated_at :datetime not null -# project_access :integer default(0), not null +# id :integer not null, primary key +# user_id :integer not null +# project_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# project_access :integer default(0), not null +# notification_level :integer default(3), not null # require 'spec_helper' diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb new file mode 100644 index 00000000000..67f2a6da42d --- /dev/null +++ b/spec/models/wiki_page_spec.rb @@ -0,0 +1,164 @@ +require "spec_helper" + +describe WikiPage do + + def create_temp_repo(path) + FileUtils.mkdir_p path + command = "git init --quiet #{path};" + system(command) + end + + def remove_temp_repo(path) + FileUtils.rm_rf path + end + + def commit_details + commit = {name: user.name, email: user.email, message: "test commit"} + end + + def create_page(name, content) + wiki.wiki.write_page(name, :markdown, content, commit_details) + end + + def destroy_page(title) + page = wiki.wiki.paged(title) + wiki.wiki.delete_page(page, commit_details) + end + + let(:project) { create(:project) } + let(:repository) { project.repository } + let(:user) { project.owner } + let(:wiki) { GollumWiki.new(project, user) } + + subject { WikiPage.new(wiki) } + + before do + create_temp_repo(wiki.send(:path_to_repo)) + end + + describe "#initialize" do + context "when initialized with an existing gollum page" do + before do + create_page("test page", "test content") + @page = wiki.wiki.paged("test page") + @wiki_page = WikiPage.new(wiki, @page, true) + end + + it "sets the slug attribute" do + @wiki_page.slug.should == "test-page" + end + + it "sets the title attribute" do + @wiki_page.title.should == "test page" + end + + it "sets the formatted content attribute" do + @wiki_page.content.should == "test content" + end + + it "sets the format attribute" do + @wiki_page.format.should == :markdown + end + + it "sets the message attribute" do + @wiki_page.message.should == "test commit" + end + + it "sets the version attribute" do + @wiki_page.version.should be_a Commit + end + end + end + + describe "validations" do + before do + subject.attributes = {title: 'title', content: 'content'} + end + + it "validates presence of title" do + subject.attributes.delete(:title) + subject.valid?.should be_false + end + + it "validates presence of content" do + subject.attributes.delete(:content) + subject.valid?.should be_false + end + end + + before do + @wiki_attr = {title: "Index", content: "Home Page", format: "markdown"} + end + + describe "#create" do + after do + destroy_page("Index") + end + + context "with valid attributes" do + it "saves the wiki page" do + subject.create(@wiki_attr) + wiki.find_page("Index").should_not be_nil + end + + it "returns true" do + subject.create(@wiki_attr).should == true + end + end + end + + describe "#update" do + before do + create_page("Update", "content") + @page = wiki.find_page("Update") + end + + after do + destroy_page("Update") + end + + context "with valid attributes" do + it "updates the content of the page" do + @page.update("new content") + @page = wiki.find_page("Update") + end + + it "returns true" do + @page.update("more content").should be_true + end + end + end + + describe "#destroy" do + before do + create_page("Delete Page", "content") + @page = wiki.find_page("Delete Page") + end + + it "should delete the page" do + @page.delete + wiki.pages.should be_empty + end + + it "should return true" do + @page.delete.should == true + end + end + + describe "#versions" do + before do + create_page("Update", "content") + @page = wiki.find_page("Update") + end + + after do + destroy_page("Update") + end + + it "returns an array of all commits for the page" do + 3.times { |i| @page.update("content #{i}") } + @page.versions.count.should == 4 + end + end + +end diff --git a/spec/models/wiki_spec.rb b/spec/models/wiki_spec.rb deleted file mode 100644 index 9750b81d303..00000000000 --- a/spec/models/wiki_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# == Schema Information -# -# Table name: wikis -# -# id :integer not null, primary key -# title :string(255) -# content :text -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# slug :string(255) -# user_id :integer -# - -require 'spec_helper' - -describe Wiki do - describe "Associations" do - it { should belong_to(:project) } - it { should belong_to(:user) } - it { should have_many(:notes).dependent(:destroy) } - end - - describe "Mass assignment" do - it { should_not allow_mass_assignment_of(:project_id) } - it { should_not allow_mass_assignment_of(:user_id) } - end - - describe "Validation" do - it { should validate_presence_of(:title) } - it { should ensure_length_of(:title).is_within(1..250) } - it { should validate_presence_of(:content) } - it { should validate_presence_of(:user) } - end -end diff --git a/spec/observers/activity_observer_spec.rb b/spec/observers/activity_observer_spec.rb index 3d503027360..dc14ab86b6d 100644 --- a/spec/observers/activity_observer_spec.rb +++ b/spec/observers/activity_observer_spec.rb @@ -3,24 +3,13 @@ require 'spec_helper' describe ActivityObserver do let(:project) { create(:project) } + before { Thread.current[:current_user] = create(:user) } + def self.it_should_be_valid_event it { @event.should_not be_nil } it { @event.project.should == project } end - describe "Merge Request created" do - before do - MergeRequest.observers.enable :activity_observer do - @merge_request = create(:merge_request, project: project) - @event = Event.last - end - end - - it_should_be_valid_event - it { @event.action.should == Event::CREATED } - it { @event.target.should == @merge_request } - end - describe "Issue created" do before do Issue.observers.enable :activity_observer do @@ -47,4 +36,26 @@ describe ActivityObserver do it { @event.action.should == Event::COMMENTED } it { @event.target.should == @note } end + + describe "Ignore system notes" do + let(:author) { create(:user) } + let!(:issue) { create(:issue, project: project) } + let!(:other) { create(:issue) } + + it "should not create events for status change notes" do + expect do + Note.observers.enable :activity_observer do + Note.create_status_change_note(issue, project, author, 'reopened', nil) + end + end.to_not change { Event.count } + end + + it "should not create events for cross-reference notes" do + expect do + Note.observers.enable :activity_observer do + Note.create_cross_reference_note(issue, other, author, issue.project) + end + end.to_not change { Event.count } + end + end end diff --git a/spec/observers/issue_observer_spec.rb b/spec/observers/issue_observer_spec.rb index 700c9a3a69b..4155ff31193 100644 --- a/spec/observers/issue_observer_spec.rb +++ b/spec/observers/issue_observer_spec.rb @@ -1,198 +1,98 @@ require 'spec_helper' describe IssueObserver do - let(:some_user) { double(:user, id: 1) } - let(:assignee) { double(:user, id: 2) } - let(:author) { double(:user, id: 3) } - let(:issue) { double(:issue, id: 42, assignee: assignee, author: author) } + let(:some_user) { create :user } + let(:assignee) { create :user } + let(:author) { create :user } + let(:mock_issue) { create(:issue, assignee: assignee, author: author) } - before(:each) { subject.stub(:current_user).and_return(some_user) } + + before { subject.stub(:current_user).and_return(some_user) } + before { subject.stub(:current_commit).and_return(nil) } + before { subject.stub(notification: mock('NotificationService').as_null_object) } + before { mock_issue.project.stub_chain(:repository, :commit).and_return(nil) } subject { IssueObserver.instance } describe '#after_create' do + it 'trigger notification to send emails' do + subject.should_receive(:notification) - it 'is called when an issue is created' do - subject.should_receive(:after_create) - - Issue.observers.enable :issue_observer do - create(:issue, project: create(:project)) - end + subject.after_create(mock_issue) end - it 'sends an email to the assignee' do - Notify.should_receive(:new_issue_email).with(issue.id) + it 'should create cross-reference notes' do + other_issue = create(:issue) + mock_issue.stub(references: [other_issue]) - subject.after_create(issue) - end - - it 'does not send an email to the assignee if assignee created the issue' do - subject.stub(:current_user).and_return(assignee) - Notify.should_not_receive(:new_issue_email) - - subject.after_create(issue) + Note.should_receive(:create_cross_reference_note).with(other_issue, mock_issue, + some_user, mock_issue.project) + subject.after_create(mock_issue) end end - context '#after_update' do - before(:each) do - issue.stub(:is_being_reassigned?).and_return(false) - issue.stub(:is_being_closed?).and_return(false) - issue.stub(:is_being_reopened?).and_return(false) - end - - it 'is called when an issue is changed' do - changed = create(:issue, project: create(:project)) - subject.should_receive(:after_update) - - Issue.observers.enable :issue_observer do - changed.description = 'I changed' - changed.save - end - end - - context 'a reassigned email' do - it 'is sent if the issue is being reassigned' do - issue.should_receive(:is_being_reassigned?).and_return(true) - subject.should_receive(:send_reassigned_email).with(issue) - - subject.after_update(issue) - end - - it 'is not sent if the issue is not being reassigned' do - issue.should_receive(:is_being_reassigned?).and_return(false) - subject.should_not_receive(:send_reassigned_email) - - subject.after_update(issue) - end - end - + context '#after_close' do context 'a status "closed"' do + before { mock_issue.stub(state: 'closed') } + it 'note is created if the issue is being closed' do - issue.should_receive(:is_being_closed?).and_return(true) - Notify.should_receive(:issue_status_changed_email).twice - Note.should_receive(:create_status_change_note).with(issue, some_user, 'closed') + Note.should_receive(:create_status_change_note).with(mock_issue, mock_issue.project, some_user, 'closed', nil) - subject.after_update(issue) + subject.after_close(mock_issue, nil) end - it 'note is not created if the issue is not being closed' do - issue.should_receive(:is_being_closed?).and_return(false) - Note.should_not_receive(:create_status_change_note).with(issue, some_user, 'closed') - - subject.after_update(issue) + it 'trigger notification to send emails' do + subject.notification.should_receive(:close_issue).with(mock_issue, some_user) + subject.after_close(mock_issue, nil) end - it 'notification is delivered if the issue being closed' do - issue.stub(:is_being_closed?).and_return(true) - Notify.should_receive(:issue_status_changed_email).twice - Note.should_receive(:create_status_change_note).with(issue, some_user, 'closed') - - subject.after_update(issue) - end + it 'appends a mention to the closing commit if one is present' do + commit = double('commit', gfm_reference: 'commit 123456') + subject.stub(current_commit: commit) - it 'notification is not delivered if the issue not being closed' do - issue.stub(:is_being_closed?).and_return(false) - Notify.should_not_receive(:issue_status_changed_email) - Note.should_not_receive(:create_status_change_note).with(issue, some_user, 'closed') + Note.should_receive(:create_status_change_note).with(mock_issue, mock_issue.project, some_user, 'closed', commit) - subject.after_update(issue) - end - - it 'notification is delivered only to author if the issue being closed' do - issue_without_assignee = double(:issue, id: 42, author: author, assignee: nil) - issue_without_assignee.stub(:is_being_reassigned?).and_return(false) - issue_without_assignee.stub(:is_being_closed?).and_return(true) - issue_without_assignee.stub(:is_being_reopened?).and_return(false) - Notify.should_receive(:issue_status_changed_email).once - Note.should_receive(:create_status_change_note).with(issue_without_assignee, some_user, 'closed') - - subject.after_update(issue_without_assignee) + subject.after_close(mock_issue, nil) end end context 'a status "reopened"' do - it 'note is created if the issue is being reopened' do - Notify.should_receive(:issue_status_changed_email).twice - issue.should_receive(:is_being_reopened?).and_return(true) - Note.should_receive(:create_status_change_note).with(issue, some_user, 'reopened') - - subject.after_update(issue) - end - - it 'note is not created if the issue is not being reopened' do - issue.should_receive(:is_being_reopened?).and_return(false) - Note.should_not_receive(:create_status_change_note).with(issue, some_user, 'reopened') - - subject.after_update(issue) - end - - it 'notification is delivered if the issue being reopened' do - issue.stub(:is_being_reopened?).and_return(true) - Notify.should_receive(:issue_status_changed_email).twice - Note.should_receive(:create_status_change_note).with(issue, some_user, 'reopened') - - subject.after_update(issue) - end + before { mock_issue.stub(state: 'reopened') } - it 'notification is not delivered if the issue not being reopened' do - issue.stub(:is_being_reopened?).and_return(false) - Notify.should_not_receive(:issue_status_changed_email) - Note.should_not_receive(:create_status_change_note).with(issue, some_user, 'reopened') - - subject.after_update(issue) - end - - it 'notification is delivered only to author if the issue being reopened' do - issue_without_assignee = double(:issue, id: 42, author: author, assignee: nil) - issue_without_assignee.stub(:is_being_reassigned?).and_return(false) - issue_without_assignee.stub(:is_being_closed?).and_return(false) - issue_without_assignee.stub(:is_being_reopened?).and_return(true) - Notify.should_receive(:issue_status_changed_email).once - Note.should_receive(:create_status_change_note).with(issue_without_assignee, some_user, 'reopened') + it 'note is created if the issue is being reopened' do + Note.should_receive(:create_status_change_note).with(mock_issue, mock_issue.project, some_user, 'reopened', nil) - subject.after_update(issue_without_assignee) + subject.after_reopen(mock_issue, nil) end end end - describe '#send_reassigned_email' do - let(:previous_assignee) { double(:user, id: 3) } - + context '#after_update' do before(:each) do - issue.stub(:assignee_id).and_return(assignee.id) - issue.stub(:assignee_id_was).and_return(previous_assignee.id) + mock_issue.stub(:is_being_reassigned?).and_return(false) end - def it_sends_a_reassigned_email_to(recipient) - Notify.should_receive(:reassigned_issue_email).with(recipient, issue.id, previous_assignee.id) - end + context 'notification' do + it 'triggered if the issue is being reassigned' do + mock_issue.should_receive(:is_being_reassigned?).and_return(true) + subject.should_receive(:notification) - def it_does_not_send_a_reassigned_email_to(recipient) - Notify.should_not_receive(:reassigned_issue_email).with(recipient, issue.id, previous_assignee.id) - end + subject.after_update(mock_issue) + end - it 'sends a reassigned email to the previous and current assignees' do - it_sends_a_reassigned_email_to assignee.id - it_sends_a_reassigned_email_to previous_assignee.id + it 'is not triggered if the issue is not being reassigned' do + mock_issue.should_receive(:is_being_reassigned?).and_return(false) + subject.should_not_receive(:notification) - subject.send(:send_reassigned_email, issue) + subject.after_update(mock_issue) + end end - context 'does not send an email to the user who made the reassignment' do - it 'if the user is the assignee' do - subject.stub(:current_user).and_return(assignee) - it_sends_a_reassigned_email_to previous_assignee.id - it_does_not_send_a_reassigned_email_to assignee.id - - subject.send(:send_reassigned_email, issue) - end - it 'if the user is the previous assignee' do - subject.stub(:current_user).and_return(previous_assignee) - it_sends_a_reassigned_email_to assignee.id - it_does_not_send_a_reassigned_email_to previous_assignee.id + context 'cross-references' do + it 'notices added references' do + mock_issue.should_receive(:notice_added_references) - subject.send(:send_reassigned_email, issue) + subject.after_update(mock_issue) end end end diff --git a/spec/observers/key_observer_spec.rb b/spec/observers/key_observer_spec.rb index e1412f52e83..b304bf1fcb7 100644 --- a/spec/observers/key_observer_spec.rb +++ b/spec/observers/key_observer_spec.rb @@ -2,20 +2,15 @@ require 'spec_helper' describe KeyObserver do before do - @key = double('Key', - shell_id: 'key-32', - key: '== a vaild ssh key', - projects: [], - is_deploy_key: false - ) + @key = create(:personal_key) @observer = KeyObserver.instance end - context :after_save do + context :after_create do it do GitlabShellWorker.should_receive(:perform_async).with(:add_key, @key.shell_id, @key.key) - @observer.after_save(@key) + @observer.after_create(@key) end end diff --git a/spec/observers/merge_request_observer_spec.rb b/spec/observers/merge_request_observer_spec.rb index 4841bf88fc5..3f5250a0040 100644 --- a/spec/observers/merge_request_observer_spec.rb +++ b/spec/observers/merge_request_observer_spec.rb @@ -1,47 +1,52 @@ require 'spec_helper' describe MergeRequestObserver do - let(:some_user) { double(:user, id: 1) } - let(:assignee) { double(:user, id: 2) } - let(:author) { double(:user, id: 3) } - let(:mr) { double(:merge_request, id: 42, assignee: assignee, author: author) } - - before(:each) { subject.stub(:current_user).and_return(some_user) } + let(:some_user) { create :user } + let(:assignee) { create :user } + let(:author) { create :user } + let(:mr_mock) { double(:merge_request, id: 42, assignee: assignee, author: author) } + let(:assigned_mr) { create(:merge_request, assignee: assignee, author: author, target_project: create(:project)) } + let(:unassigned_mr) { create(:merge_request, author: author, target_project: create(:project)) } + let(:closed_assigned_mr) { create(:closed_merge_request, assignee: assignee, author: author, target_project: create(:project)) } + let(:closed_unassigned_mr) { create(:closed_merge_request, author: author, target_project: create(:project)) } + + before { subject.stub(:current_user).and_return(some_user) } + before { subject.stub(notification: mock('NotificationService').as_null_object) } + before { mr_mock.stub(:author_id) } + before { mr_mock.stub(:target_project) } + before { mr_mock.stub(:source_project) } + before { mr_mock.stub(:project) } + before { mr_mock.stub(:create_cross_references!).and_return(true) } + before { Repository.any_instance.stub(commit: nil) } + + before(:each) { enable_observers } + after(:each) { disable_observers } subject { MergeRequestObserver.instance } describe '#after_create' do - - it 'is called when a merge request is created' do - subject.should_receive(:after_create) - - MergeRequest.observers.enable :merge_request_observer do - create(:merge_request, project: create(:project)) - end + it 'trigger notification service' do + subject.should_receive(:notification) + subject.after_create(mr_mock) end - it 'sends an email to the assignee' do - Notify.should_receive(:new_merge_request_email).with(mr.id) - subject.after_create(mr) - end + it 'creates cross-reference notes' do + project = create :project + mr_mock.stub(title: "this mr references !#{assigned_mr.id}", project: project) + mr_mock.should_receive(:create_cross_references!).with(project, some_user) - it 'does not send an email to the assignee if assignee created the merge request' do - subject.stub(:current_user).and_return(assignee) - Notify.should_not_receive(:new_merge_request_email) - - subject.after_create(mr) + subject.after_create(mr_mock) end end context '#after_update' do before(:each) do - mr.stub(:is_being_reassigned?).and_return(false) - mr.stub(:is_being_closed?).and_return(false) - mr.stub(:is_being_reopened?).and_return(false) + mr_mock.stub(:is_being_reassigned?).and_return(false) + mr_mock.stub(:notice_added_references) end it 'is called when a merge request is changed' do - changed = create(:merge_request, project: create(:project)) + changed = create(:merge_request, source_project: create(:project)) subject.should_receive(:after_update) MergeRequest.observers.enable :merge_request_observer do @@ -50,141 +55,83 @@ describe MergeRequestObserver do end end - context 'a reassigned email' do + it 'checks for new references' do + mr_mock.should_receive(:notice_added_references) + + subject.after_update(mr_mock) + end + + context 'a notification' do it 'is sent if the merge request is being reassigned' do - mr.should_receive(:is_being_reassigned?).and_return(true) - subject.should_receive(:send_reassigned_email).with(mr) + mr_mock.should_receive(:is_being_reassigned?).and_return(true) + subject.should_receive(:notification) - subject.after_update(mr) + subject.after_update(mr_mock) end it 'is not sent if the merge request is not being reassigned' do - mr.should_receive(:is_being_reassigned?).and_return(false) - subject.should_not_receive(:send_reassigned_email) + mr_mock.should_receive(:is_being_reassigned?).and_return(false) + subject.should_not_receive(:notification) - subject.after_update(mr) + subject.after_update(mr_mock) end end + end + context '#after_close' do context 'a status "closed"' do it 'note is created if the merge request is being closed' do - mr.should_receive(:is_being_closed?).and_return(true) - Note.should_receive(:create_status_change_note).with(mr, some_user, 'closed') + Note.should_receive(:create_status_change_note).with(assigned_mr, assigned_mr.target_project, some_user, 'closed', nil) - subject.after_update(mr) - end - - it 'note is not created if the merge request is not being closed' do - mr.should_receive(:is_being_closed?).and_return(false) - Note.should_not_receive(:create_status_change_note).with(mr, some_user, 'closed') - - subject.after_update(mr) - end - - it 'notification is delivered if the merge request being closed' do - mr.stub(:is_being_closed?).and_return(true) - Note.should_receive(:create_status_change_note).with(mr, some_user, 'closed') - - subject.after_update(mr) - end - - it 'notification is not delivered if the merge request not being closed' do - mr.stub(:is_being_closed?).and_return(false) - Note.should_not_receive(:create_status_change_note).with(mr, some_user, 'closed') - - subject.after_update(mr) + assigned_mr.close end it 'notification is delivered only to author if the merge request is being closed' do - mr_without_assignee = double(:merge_request, id: 42, author: author, assignee: nil) - mr_without_assignee.stub(:is_being_reassigned?).and_return(false) - mr_without_assignee.stub(:is_being_closed?).and_return(true) - mr_without_assignee.stub(:is_being_reopened?).and_return(false) - Note.should_receive(:create_status_change_note).with(mr_without_assignee, some_user, 'closed') + Note.should_receive(:create_status_change_note).with(unassigned_mr, unassigned_mr.target_project, some_user, 'closed', nil) - subject.after_update(mr_without_assignee) + unassigned_mr.close end end + end + context '#after_reopen' do context 'a status "reopened"' do it 'note is created if the merge request is being reopened' do - mr.should_receive(:is_being_reopened?).and_return(true) - Note.should_receive(:create_status_change_note).with(mr, some_user, 'reopened') + Note.should_receive(:create_status_change_note).with(closed_assigned_mr, closed_assigned_mr.target_project, some_user, 'reopened', nil) - subject.after_update(mr) - end - - it 'note is not created if the merge request is not being reopened' do - mr.should_receive(:is_being_reopened?).and_return(false) - Note.should_not_receive(:create_status_change_note).with(mr, some_user, 'reopened') - - subject.after_update(mr) - end - - it 'notification is delivered if the merge request being reopened' do - mr.stub(:is_being_reopened?).and_return(true) - Note.should_receive(:create_status_change_note).with(mr, some_user, 'reopened') - - subject.after_update(mr) - end - - it 'notification is not delivered if the merge request is not being reopened' do - mr.stub(:is_being_reopened?).and_return(false) - Note.should_not_receive(:create_status_change_note).with(mr, some_user, 'reopened') - - subject.after_update(mr) + closed_assigned_mr.reopen end it 'notification is delivered only to author if the merge request is being reopened' do - mr_without_assignee = double(:merge_request, id: 42, author: author, assignee: nil) - mr_without_assignee.stub(:is_being_reassigned?).and_return(false) - mr_without_assignee.stub(:is_being_closed?).and_return(false) - mr_without_assignee.stub(:is_being_reopened?).and_return(true) - Note.should_receive(:create_status_change_note).with(mr_without_assignee, some_user, 'reopened') + Note.should_receive(:create_status_change_note).with(closed_unassigned_mr, closed_unassigned_mr.target_project, some_user, 'reopened', nil) - subject.after_update(mr_without_assignee) + closed_unassigned_mr.reopen end end end - describe '#send_reassigned_email' do - let(:previous_assignee) { double(:user, id: 3) } - - before(:each) do - mr.stub(:assignee_id).and_return(assignee.id) - mr.stub(:assignee_id_was).and_return(previous_assignee.id) + describe "Merge Request created" do + def self.it_should_be_valid_event + it { @event.should_not be_nil } + it { @event.should_not be_nil } + it { @event.project.should == project } + it { @event.project.should == project } end - def it_sends_a_reassigned_email_to(recipient) - Notify.should_receive(:reassigned_merge_request_email).with(recipient, mr.id, previous_assignee.id) + let(:project) { create(:project) } + before do + TestEnv.enable_observers + @merge_request = create(:merge_request, source_project: project, target_project: project) + @event = Event.last end - def it_does_not_send_a_reassigned_email_to(recipient) - Notify.should_not_receive(:reassigned_merge_request_email).with(recipient, mr.id, previous_assignee.id) + after do + TestEnv.disable_observers end - it 'sends a reassigned email to the previous and current assignees' do - it_sends_a_reassigned_email_to assignee.id - it_sends_a_reassigned_email_to previous_assignee.id - - subject.send(:send_reassigned_email, mr) - end - - context 'does not send an email to the user who made the reassignment' do - it 'if the user is the assignee' do - subject.stub(:current_user).and_return(assignee) - it_sends_a_reassigned_email_to previous_assignee.id - it_does_not_send_a_reassigned_email_to assignee.id - - subject.send(:send_reassigned_email, mr) - end - it 'if the user is the previous assignee' do - subject.stub(:current_user).and_return(previous_assignee) - it_sends_a_reassigned_email_to assignee.id - it_does_not_send_a_reassigned_email_to previous_assignee.id - - subject.send(:send_reassigned_email, mr) - end - end + it_should_be_valid_event + it { @event.action.should == Event::CREATED } + it { @event.target.should == @merge_request } end + end diff --git a/spec/observers/note_observer_spec.rb b/spec/observers/note_observer_spec.rb index 8ad42c21d2c..f9b96c255c1 100644 --- a/spec/observers/note_observer_spec.rb +++ b/spec/observers/note_observer_spec.rb @@ -2,11 +2,12 @@ require 'spec_helper' describe NoteObserver do subject { NoteObserver.instance } + before { subject.stub(notification: mock('NotificationService').as_null_object) } let(:team_without_author) { (1..2).map { |n| double :user, id: n } } + let(:note) { double(:note).as_null_object } describe '#after_create' do - let(:note) { double :note } it 'is called after a note is created' do subject.should_receive :after_create @@ -17,116 +18,39 @@ describe NoteObserver do end it 'sends out notifications' do - subject.should_receive(:send_notify_mails).with(note) + subject.should_receive(:notification) subject.after_create(note) end - end - - describe "#send_notify_mails" do - let(:note) { double :note, notify: false, notify_author: false } - - it 'notifies team of new note when flagged to notify' do - note.stub(:notify).and_return(true) - subject.should_receive(:notify_team).with(note) - - subject.after_create(note) - end - - it 'does not notify team of new note when not flagged to notify' do - subject.should_not_receive(:notify_team).with(note) - - subject.after_create(note) - end - - it 'notifies the author of a commit when flagged to notify the author' do - note.stub(:notify_author).and_return(true) - note.stub(:noteable).and_return(double(author_email: 'test@test.com')) - note.stub(:id).and_return(42) - author = double :user, id: 1, email: 'test@test.com' - note.stub(:commit_author).and_return(author) - Notify.should_receive(:note_commit_email) - - subject.after_create(note) - end - - it 'does not notify the author of a commit when not flagged to notify the author' do - notify.should_not_receive(:note_commit_email) - - subject.after_create(note) - end - - it 'does nothing if no notify flags are set' do - subject.after_create(note).should be_nil - end - end - describe '#notify_team' do - let(:note) { double :note, id: 1 } + it 'creates cross-reference notes as appropriate' do + @p = create(:project) + @referenced = create(:issue, project: @p) + @referencer = create(:issue, project: @p) + @author = create(:user) - before :each do - subject.stub(:team_without_note_author).with(note).and_return(team_without_author) - end - - context 'notifies team of a new note on' do - it 'a commit' do - note.stub(:noteable_type).and_return('Commit') - notify.should_receive(:note_commit_email).twice - - subject.send(:notify_team, note) - end - - it 'an issue' do - note.stub(:noteable_type).and_return('Issue') - notify.should_receive(:note_issue_email).twice - - subject.send(:notify_team, note) - end - - it 'a wiki page' do - note.stub(:noteable_type).and_return('Wiki') - notify.should_receive(:note_wiki_email).twice - - subject.send(:notify_team, note) - end - - it 'a merge request' do - note.stub(:noteable_type).and_return('MergeRequest') - notify.should_receive(:note_merge_request_email).twice + Note.should_receive(:create_cross_reference_note).with(@referenced, @referencer, @author, @p) - subject.send(:notify_team, note) - end - - it 'a wall' do - # Note: wall posts have #noteable_type of nil - note.stub(:noteable_type).and_return(nil) - notify.should_receive(:note_wall_email).twice - - subject.send(:notify_team, note) + Note.observers.enable :note_observer do + create(:note, project: @p, author: @author, noteable: @referencer, + note: "Duplicate of ##{@referenced.iid}") end end - it 'does nothing for a new note on a snippet' do - note.stub(:noteable_type).and_return('Snippet') + it "doesn't cross-reference system notes" do + Note.should_receive(:create_cross_reference_note).once - subject.send(:notify_team, note).should be_nil + Note.observers.enable :note_observer do + Note.create_cross_reference_note(create(:issue), create(:issue)) + end end end + describe '#after_update' do + it 'checks for new cross-references' do + note.should_receive(:notice_added_references) - describe '#team_without_note_author' do - let(:author) { double :user, id: 4 } - - let(:users) { team_without_author + [author] } - let(:project) { double :project, users: users } - let(:note) { double :note, project: project, author: author } - - it 'returns the projects user without the note author included' do - subject.send(:team_without_note_author, note).should == team_without_author + subject.after_update(note) end end - - def notify - Notify - end end diff --git a/spec/observers/user_observer_spec.rb b/spec/observers/user_observer_spec.rb index bffa5fcfd69..b74fceb98b1 100644 --- a/spec/observers/user_observer_spec.rb +++ b/spec/observers/user_observer_spec.rb @@ -1,7 +1,10 @@ require 'spec_helper' describe UserObserver do + before(:each) { enable_observers } + after(:each) {disable_observers} subject { UserObserver.instance } + before { subject.stub(notification: mock('NotificationService').as_null_object) } it 'calls #after_create when new users are created' do new_user = build(:user) @@ -11,11 +14,12 @@ describe UserObserver do context 'when a new user is created' do it 'sends an email' do - Notify.should_receive(:new_user_email) + subject.should_receive(:notification) create(:user) end it 'trigger logger' do + user = double(:user, id: 42, password: 'P@ssword!', name: 'John', email: 'u@mail.local', extern_uid?: false) Gitlab::AppLogger.should_receive(:info) create(:user) end diff --git a/spec/observers/users_group_observer_spec.rb b/spec/observers/users_group_observer_spec.rb new file mode 100644 index 00000000000..3bf562edbb7 --- /dev/null +++ b/spec/observers/users_group_observer_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe UsersGroupObserver do + before(:each) { enable_observers } + after(:each) { disable_observers } + + subject { UsersGroupObserver.instance } + before { subject.stub(notification: mock('NotificationService').as_null_object) } + + describe "#after_create" do + it "should send email to user" do + subject.should_receive(:notification) + create(:users_group) + end + end + + describe "#after_update" do + before do + @membership = create :users_group + end + + it "should send email to user" do + subject.should_receive(:notification) + @membership.update_attribute(:group_access, UsersGroup::MASTER) + end + end +end diff --git a/spec/observers/users_project_observer_spec.rb b/spec/observers/users_project_observer_spec.rb index 068688b0d1d..e33d8cc50fd 100644 --- a/spec/observers/users_project_observer_spec.rb +++ b/spec/observers/users_project_observer_spec.rb @@ -1,9 +1,13 @@ require 'spec_helper' describe UsersProjectObserver do + before(:each) { enable_observers } + after(:each) { disable_observers } + let(:user) { create(:user) } let(:project) { create(:project) } subject { UsersProjectObserver.instance } + before { subject.stub(notification: mock('NotificationService').as_null_object) } describe "#after_commit" do it "should called when UsersProject created" do @@ -12,8 +16,8 @@ describe UsersProjectObserver do end it "should send email to user" do - Notify.should_receive(:project_access_granted_email).and_return(double(deliver: true)) - Event.stub(:create => true) + subject.should_receive(:notification) + Event.stub(create: true) create(:users_project) end @@ -36,7 +40,7 @@ describe UsersProjectObserver do end it "should send email to user" do - Notify.should_receive(:project_access_granted_email) + subject.should_receive(:notification) @users_project.update_attribute(:project_access, UsersProject::MASTER) end diff --git a/spec/requests/admin/admin_projects_spec.rb b/spec/requests/admin/admin_projects_spec.rb deleted file mode 100644 index c9ddf1f4534..00000000000 --- a/spec/requests/admin/admin_projects_spec.rb +++ /dev/null @@ -1,76 +0,0 @@ -require 'spec_helper' - -describe "Admin::Projects" do - before do - @project = create(:project) - login_as :admin - end - - describe "GET /admin/projects" do - before do - visit admin_projects_path - end - - it "should be ok" do - current_path.should == admin_projects_path - end - - it "should have projects list" do - page.should have_content(@project.name) - end - end - - describe "GET /admin/projects/:id" do - before do - visit admin_projects_path - click_link "#{@project.name}" - end - - it "should have project info" do - page.should have_content(@project.path) - page.should have_content(@project.name) - end - end - - describe "GET /admin/projects/:id/edit" do - before do - visit admin_projects_path - click_link "edit_project_#{@project.id}" - end - - it "should have project edit page" do - page.should have_content("Edit project") - page.should have_button("Save Project") - end - - describe "Update project" do - before do - fill_in "project_name", with: "Big Bang" - click_button "Save Project" - @project.reload - end - - it "should show page with new data" do - page.should have_content("Big Bang") - end - - it "should change project entry" do - @project.name.should == "Big Bang" - end - end - end - - describe "Add new team member" do - before do - @new_user = create(:user) - visit admin_project_path(@project) - end - - it "should create new user" do - select @new_user.name, from: "user_ids" - expect { click_button "Add" }.to change { UsersProject.count }.by(1) - page.should have_content @new_user.name - current_path.should == admin_project_path(@project) - end - end -end diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb new file mode 100644 index 00000000000..2fc78a7e390 --- /dev/null +++ b/spec/requests/api/api_helpers_spec.rb @@ -0,0 +1,161 @@ +require 'spec_helper' + +describe API do + include API::APIHelpers + include ApiHelpers + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:key) { create(:key, user: user) } + + let(:params) { {} } + let(:env) { {} } + + def set_env(token_usr, identifier) + clear_env + clear_param + env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token + env[API::APIHelpers::SUDO_HEADER] = identifier + end + + def set_param(token_usr, identifier) + clear_env + clear_param + params[API::APIHelpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token + params[API::APIHelpers::SUDO_PARAM] = identifier + end + + def clear_env + env.delete(API::APIHelpers::PRIVATE_TOKEN_HEADER) + env.delete(API::APIHelpers::SUDO_HEADER) + end + + def clear_param + params.delete(API::APIHelpers::PRIVATE_TOKEN_PARAM) + params.delete(API::APIHelpers::SUDO_PARAM) + end + + def error!(message, status) + raise Exception + end + + describe ".current_user" do + it "should leave user as is when sudo not specified" do + env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = user.private_token + current_user.should == user + clear_env + params[API::APIHelpers::PRIVATE_TOKEN_PARAM] = user.private_token + current_user.should == user + end + + it "should change current user to sudo when admin" do + set_env(admin, user.id) + current_user.should == user + set_param(admin, user.id) + current_user.should == user + set_env(admin, user.username) + current_user.should == user + set_param(admin, user.username) + current_user.should == user + end + + it "should throw an error when the current user is not an admin and attempting to sudo" do + set_env(user, admin.id) + expect { current_user }.to raise_error + set_param(user, admin.id) + expect { current_user }.to raise_error + set_env(user, admin.username) + expect { current_user }.to raise_error + set_param(user, admin.username) + expect { current_user }.to raise_error + end + + it "should throw an error when the user cannot be found for a given id" do + id = user.id + admin.id + user.id.should_not == id + admin.id.should_not == id + set_env(admin, id) + expect { current_user }.to raise_error + + set_param(admin, id) + expect { current_user }.to raise_error + end + + it "should throw an error when the user cannot be found for a given username" do + username = "#{user.username}#{admin.username}" + user.username.should_not == username + admin.username.should_not == username + set_env(admin, username) + expect { current_user }.to raise_error + + set_param(admin, username) + expect { current_user }.to raise_error + end + + it "should handle sudo's to oneself" do + set_env(admin, admin.id) + current_user.should == admin + set_param(admin, admin.id) + current_user.should == admin + set_env(admin, admin.username) + current_user.should == admin + set_param(admin, admin.username) + current_user.should == admin + end + + it "should handle multiple sudo's to oneself" do + set_env(admin, user.id) + current_user.should == user + current_user.should == user + set_env(admin, user.username) + current_user.should == user + current_user.should == user + + set_param(admin, user.id) + current_user.should == user + current_user.should == user + set_param(admin, user.username) + current_user.should == user + current_user.should == user + end + + it "should handle multiple sudo's to oneself using string ids" do + set_env(admin, user.id.to_s) + current_user.should == user + current_user.should == user + + set_param(admin, user.id.to_s) + current_user.should == user + current_user.should == user + end + end + + describe '.sudo_identifier' do + it "should return integers when input is an int" do + set_env(admin, '123') + sudo_identifier.should == 123 + set_env(admin, '0001234567890') + sudo_identifier.should == 1234567890 + + set_param(admin, '123') + sudo_identifier.should == 123 + set_param(admin, '0001234567890') + sudo_identifier.should == 1234567890 + end + + it "should return string when input is an is not an int" do + set_env(admin, '12.30') + sudo_identifier.should == "12.30" + set_env(admin, 'hello') + sudo_identifier.should == 'hello' + set_env(admin, ' 123') + sudo_identifier.should == ' 123' + + set_param(admin, '12.30') + sudo_identifier.should == "12.30" + set_param(admin, 'hello') + sudo_identifier.should == 'hello' + set_param(admin, ' 123') + sudo_identifier.should == ' 123' + end + end +end
\ No newline at end of file diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index c39a4228408..a6ce72e11e9 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -1,13 +1,18 @@ require 'spec_helper' -describe Gitlab::API do +describe API::API do include ApiHelpers - let(:user1) { create(:user) } - let(:user2) { create(:user) } + let(:user1) { create(:user) } + let(:user2) { create(:user) } let(:admin) { create(:admin) } - let!(:group1) { create(:group, owner: user1) } - let!(:group2) { create(:group, owner: user2) } + let!(:group1) { create(:group) } + let!(:group2) { create(:group) } + + before do + group1.add_owner(user1) + group2.add_owner(user2) + end describe "GET /groups" do context "when unauthenticated" do @@ -26,7 +31,7 @@ describe Gitlab::API do json_response.first['name'].should == group1.name end end - + context "when authenticated as admin" do it "admin: should return an array of all groups" do get api("/groups", admin) @@ -36,7 +41,7 @@ describe Gitlab::API do end end end - + describe "GET /groups/:id" do context "when authenticated as user" do it "should return one of user1's groups" do @@ -44,32 +49,32 @@ describe Gitlab::API do response.status.should == 200 json_response['name'] == group1.name end - + it "should not return a non existing group" do get api("/groups/1328", user1) response.status.should == 404 end - + it "should not return a group not attached to user1" do get api("/groups/#{group2.id}", user1) - response.status.should == 404 + response.status.should == 403 end end - + context "when authenticated as admin" do it "should return any existing group" do get api("/groups/#{group2.id}", admin) response.status.should == 200 json_response['name'] == group2.name end - + it "should not return a non existing group" do get api("/groups/1328", admin) response.status.should == 404 end end end - + describe "POST /groups" do context "when authenticated as user" do it "should not create group" do @@ -77,7 +82,7 @@ describe Gitlab::API do response.status.should == 403 end end - + context "when authenticated as admin" do it "should create group" do post api("/groups", admin), attributes_for(:group) @@ -85,9 +90,151 @@ describe Gitlab::API do end it "should not create group, duplicate" do - post api("/groups", admin), {:name => "Duplicate Test", :path => group2.path} + post api("/groups", admin), {name: "Duplicate Test", path: group2.path} response.status.should == 404 end + + it "should return 400 bad request error if name not given" do + post api("/groups", admin), {path: group2.path} + response.status.should == 400 + end + + it "should return 400 bad request error if path not given" do + post api("/groups", admin), { name: 'test' } + response.status.should == 400 + end + end + end + + describe "POST /groups/:id/projects/:project_id" do + let(:project) { create(:project) } + before(:each) do + project.stub!(:transfer).and_return(true) + Project.stub(:find).and_return(project) + end + + context "when authenticated as user" do + it "should not transfer project to group" do + post api("/groups/#{group1.id}/projects/#{project.id}", user2) + response.status.should == 403 + end + end + + context "when authenticated as admin" do + it "should transfer project to group" do + project.should_receive(:transfer) + post api("/groups/#{group1.id}/projects/#{project.id}", admin) + 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], UsersGroup::REPORTER) + group.add_users([developer.id], UsersGroup::DEVELOPER) + group.add_users([master.id], UsersGroup::MASTER) + group.add_users([guest.id], UsersGroup::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 == UsersGroup::OWNER + json_response.find { |e| e['id']==reporter.id }['access_level'].should == UsersGroup::REPORTER + json_response.find { |e| e['id']==developer.id }['access_level'].should == UsersGroup::DEVELOPER + json_response.find { |e| e['id']==master.id }['access_level'].should == UsersGroup::MASTER + json_response.find { |e| e['id']==guest.id }['access_level'].should == UsersGroup::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: UsersGroup::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.users_groups.count + new_user = create(:user) + post api("/groups/#{group_no_members.id}/members", owner), user_id: new_user.id, access_level: UsersGroup::MASTER + response.status.should == 201 + json_response['name'].should == new_user.name + json_response['access_level'].should == UsersGroup::MASTER + group_no_members.users_groups.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: UsersGroup::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: UsersGroup::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.users_groups.count + delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner) + response.status.should == 200 + group_with_members.users_groups.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 new file mode 100644 index 00000000000..e8870f4d5d8 --- /dev/null +++ b/spec/requests/api/internal_spec.rb @@ -0,0 +1,162 @@ +require 'spec_helper' + +describe API::API do + include ApiHelpers + before(:each) { ActiveRecord::Base.observers.enable(:user_observer) } + after(:each) { ActiveRecord::Base.observers.disable(:user_observer) } + + let(:user) { create(:user) } + let(:key) { create(:key, user: user) } + let(:project) { create(:project) } + + describe "GET /internal/check", no_db: true do + it do + get api("/internal/check") + + response.status.should == 200 + json_response['api_version'].should == API::API.version + end + end + + describe "GET /internal/discover" do + it do + get(api("/internal/discover"), key_id: key.id) + + response.status.should == 200 + + json_response['name'].should == user.name + end + end + + describe "GET /internal/allowed" do + context "access granted" do + before do + project.team << [user, :developer] + end + + context "git pull" do + it do + pull(key, project) + + response.status.should == 200 + response.body.should == 'true' + end + end + + context "git push" do + it do + push(key, project) + + response.status.should == 200 + response.body.should == 'true' + end + end + end + + context "access denied" do + before do + project.team << [user, :guest] + end + + context "git pull" do + it do + pull(key, project) + + response.status.should == 200 + response.body.should == 'false' + end + end + + context "git push" do + it do + push(key, project) + + response.status.should == 200 + response.body.should == 'false' + end + end + end + + context "blocked user" do + let(:personal_project) { create(:project, namespace: user.namespace) } + + before do + user.block + end + + context "git pull" do + it do + pull(key, personal_project) + + response.status.should == 200 + response.body.should == 'false' + end + end + + context "git push" do + it do + push(key, personal_project) + + response.status.should == 200 + response.body.should == 'false' + end + end + end + + context "deploy key" do + let(:key) { create(:deploy_key) } + + context "added to project" do + before do + key.projects << project + end + + it do + archive(key, project) + + response.status.should == 200 + response.body.should == 'true' + end + end + + context "not added to project" do + it do + archive(key, project) + + response.status.should == 200 + response.body.should == 'false' + end + end + end + end + + def pull(key, project) + get( + api("/internal/allowed"), + ref: 'master', + key_id: key.id, + project: project.path_with_namespace, + action: 'git-upload-pack' + ) + end + + def push(key, project) + get( + api("/internal/allowed"), + ref: 'master', + key_id: key.id, + project: project.path_with_namespace, + action: 'git-receive-pack' + ) + end + + def archive(key, project) + get( + api("/internal/allowed"), + ref: 'master', + key_id: key.id, + project: project.path_with_namespace, + action: 'git-upload-archive' + ) + end +end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 781ebab026b..a97d6a282a9 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' -describe Gitlab::API do +describe API::API do include ApiHelpers + before(:each) { ActiveRecord::Base.observers.enable(:user_observer) } + after(:each) { ActiveRecord::Base.observers.disable(:user_observer) } let(:user) { create(:user) } let!(:project) { create(:project, namespace: user.namespace ) } @@ -41,6 +43,11 @@ describe Gitlab::API do response.status.should == 200 json_response['title'].should == issue.title end + + it "should return 404 if issue id not found" do + get api("/projects/#{project.id}/issues/54321", user) + response.status.should == 404 + end end describe "POST /projects/:id/issues" do @@ -52,16 +59,37 @@ describe Gitlab::API do json_response['description'].should be_nil json_response['labels'].should == ['label', 'label2'] end + + it "should return a 400 bad request if title not given" do + post api("/projects/#{project.id}/issues", user), labels: 'label, label2' + response.status.should == 400 + end end - describe "PUT /projects/:id/issues/:issue_id" do + describe "PUT /projects/:id/issues/:issue_id to update only title" do it "should update a project issue" do put api("/projects/#{project.id}/issues/#{issue.id}", user), - title: 'updated title', labels: 'label2', closed: 1 + title: 'updated title' response.status.should == 200 + json_response['title'].should == 'updated title' + end + + it "should return 404 error if issue id not found" do + put api("/projects/#{project.id}/issues/44444", user), + title: 'updated title' + response.status.should == 404 + end + end + + describe "PUT /projects/:id/issues/:issue_id to update state and label" do + it "should update a project issue" do + put api("/projects/#{project.id}/issues/#{issue.id}", user), + labels: 'label2', state_event: "close" + response.status.should == 200 + json_response['labels'].should == ['label2'] - json_response['closed'].should be_true + json_response['state'].should eq "closed" end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 5da54154a81..2f11f562aa1 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -1,12 +1,15 @@ require "spec_helper" -describe Gitlab::API do +describe API::API do include ApiHelpers - - let(:user) { create(:user ) } - let!(:project) { create(:project, namespace: user.namespace ) } - let!(:merge_request) { create(:merge_request, author: user, assignee: user, project: project, title: "Test") } - before { project.team << [user, :reporters] } + before(:each) { ActiveRecord::Base.observers.enable(:user_observer) } + after(:each) { ActiveRecord::Base.observers.disable(:user_observer) } + let(:user) { create(:user) } + let!(:project) {create(:project_with_code, creator_id: user.id, namespace: user.namespace) } + let!(:merge_request) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "Test") } + before { + project.team << [user, :reporters] + } describe "GET /projects/:id/merge_requests" do context "when unauthenticated" do @@ -32,14 +35,128 @@ describe Gitlab::API do response.status.should == 200 json_response['title'].should == merge_request.title end + + it "should return a 404 error if merge_request_id not found" do + get api("/projects/#{project.id}/merge_request/999", user) + response.status.should == 404 + end end describe "POST /projects/:id/merge_requests" do + context 'between branches projects' do + it "should return merge_request" do + post api("/projects/#{project.id}/merge_requests", user), + title: 'Test merge_request', source_branch: "stable", target_branch: "master", author: user + response.status.should == 201 + json_response['title'].should == 'Test merge_request' + end + + it "should return 422 when source_branch equals target_branch" do + post api("/projects/#{project.id}/merge_requests", user), + title: "Test merge_request", source_branch: "master", target_branch: "master", author: user + response.status.should == 422 + end + + it "should return 400 when source_branch is missing" do + post api("/projects/#{project.id}/merge_requests", user), + title: "Test merge_request", target_branch: "master", author: user + response.status.should == 400 + end + + it "should return 400 when target_branch is missing" do + post api("/projects/#{project.id}/merge_requests", user), + title: "Test merge_request", source_branch: "stable", author: user + response.status.should == 400 + end + + it "should return 400 when title is missing" do + post api("/projects/#{project.id}/merge_requests", user), + target_branch: 'master', source_branch: 'stable' + response.status.should == 400 + end + end + + context 'forked projects' do + let!(:user2) {create(:user)} + let!(:forked_project_link) { build(:forked_project_link) } + let!(:fork_project) { create(:source_project_with_code, forked_project_link: forked_project_link, namespace: user2.namespace, creator_id: user2.id) } + let!(:unrelated_project) { create(:target_project_with_code, namespace: user2.namespace, creator_id: user2.id) } + + before :each do |each| + fork_project.team << [user2, :reporters] + forked_project_link.forked_from_project = project + forked_project_link.forked_to_project = fork_project + forked_project_link.save! + end + + it "should return merge_request" do + post api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', source_branch: "stable", target_branch: "master", author: user2, target_project_id: project.id + response.status.should == 201 + json_response['title'].should == 'Test merge_request' + end + + it "should not return 422 when source_branch equals target_branch" do + project.id.should_not == fork_project.id + fork_project.forked?.should be_true + fork_project.forked_from_project.should == project + post api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id + response.status.should == 201 + json_response['title'].should == 'Test merge_request' + end + + it "should return 400 when source_branch is missing" do + post api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id + response.status.should == 400 + end + + it "should return 400 when target_branch is missing" do + post api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id + response.status.should == 400 + end + + it "should return 400 when title is missing" do + post api("/projects/#{fork_project.id}/merge_requests", user2), + target_branch: 'master', source_branch: 'stable', author: user2, target_project_id: project.id + response.status.should == 400 + end + + it "should return 400 when target_branch is specified and not a forked project" do + post api("/projects/#{project.id}/merge_requests", user), + title: 'Test merge_request', target_branch: 'master', source_branch: 'stable', author: user, target_project_id: fork_project.id + response.status.should == 400 + end + + it "should return 400 when target_branch is specified and for a different fork" do + post api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: 'master', source_branch: 'stable', author: user2, target_project_id: unrelated_project.id + response.status.should == 400 + end + + it "should return 201 when target_branch is specified and for the same project" do + post api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: 'master', source_branch: 'stable', author: user2, target_project_id: fork_project.id + response.status.should == 201 + end + end + end + + describe "PUT /projects/:id/merge_request/:merge_request_id to close MR" do it "should return merge_request" do - post api("/projects/#{project.id}/merge_requests", user), - title: 'Test merge_request', source_branch: "stable", target_branch: "master", author: user - response.status.should == 201 - json_response['title'].should == 'Test merge_request' + put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), state_event: "close" + response.status.should == 200 + json_response['state'].should == 'closed' + end + end + + describe "PUT /projects/:id/merge_request/:merge_request_id to merge MR" do + it "should return merge_request" do + put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), state_event: "merge" + response.status.should == 200 + json_response['state'].should == 'merged' end end @@ -49,6 +166,18 @@ describe Gitlab::API do response.status.should == 200 json_response['title'].should == 'New title' end + + it "should return 422 when source_branch and target_branch are renamed the same" do + put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), + source_branch: "master", target_branch: "master" + response.status.should == 422 + end + + it "should return merge_request with renamed target_branch" do + put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), target_branch: "wiki" + response.status.should == 200 + json_response['target_branch'].should == 'wiki' + end end describe "POST /projects/:id/merge_request/:merge_request_id/comments" do @@ -57,6 +186,16 @@ describe Gitlab::API do response.status.should == 201 json_response['note'].should == 'My comment' end + + it "should return 400 if note is missing" do + post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user) + response.status.should == 400 + end + + it "should return 404 if note is attached to non existent merge request" do + post api("/projects/#{project.id}/merge_request/111/comments", user), note: "My comment" + response.status.should == 404 + end end end diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index 80696671462..246fe262ce8 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' -describe Gitlab::API do +describe API::API do include ApiHelpers + before(:each) { enable_observers } + after(:each) {disable_observers} let(:user) { create(:user) } let!(:project) { create(:project, namespace: user.namespace ) } @@ -16,6 +18,11 @@ describe Gitlab::API do json_response.should be_an Array json_response.first['title'].should == milestone.title end + + it "should return a 401 error if user not authenticated" do + get api("/projects/#{project.id}/milestones") + response.status.should == 401 + end end describe "GET /projects/:id/milestones/:milestone_id" do @@ -24,16 +31,38 @@ describe Gitlab::API do response.status.should == 200 json_response['title'].should == milestone.title end + + it "should return 401 error if user not authenticated" do + get api("/projects/#{project.id}/milestones/#{milestone.id}") + response.status.should == 401 + end + + it "should return a 404 error if milestone id not found" do + get api("/projects/#{project.id}/milestones/1234", user) + response.status.should == 404 + end end describe "POST /projects/:id/milestones" do it "should create a new project milestone" do - post api("/projects/#{project.id}/milestones", user), - title: 'new milestone' + post api("/projects/#{project.id}/milestones", user), title: 'new milestone' response.status.should == 201 json_response['title'].should == 'new milestone' json_response['description'].should be_nil end + + it "should create a new project milestone with description and due date" do + post api("/projects/#{project.id}/milestones", user), + title: 'new milestone', description: 'release', due_date: '2013-03-02' + response.status.should == 201 + json_response['description'].should == 'release' + json_response['due_date'].should == '2013-03-02' + end + + it "should return a 400 error if title is missing" do + post api("/projects/#{project.id}/milestones", user) + response.status.should == 400 + end end describe "PUT /projects/:id/milestones/:milestone_id" do @@ -43,5 +72,21 @@ describe Gitlab::API do response.status.should == 200 json_response['title'].should == 'updated title' end + + it "should return a 404 error if milestone id not found" do + put api("/projects/#{project.id}/milestones/1234", user), + title: 'updated title' + response.status.should == 404 + end + end + + describe "PUT /projects/:id/milestones/:milestone_id to close milestone" do + it "should update a project milestone" do + put api("/projects/#{project.id}/milestones/#{milestone.id}", user), + state_event: 'close' + response.status.should == 200 + + json_response['state'].should == 'closed' + end end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index ee99d85df4d..ba18b123039 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -1,13 +1,15 @@ require 'spec_helper' -describe Gitlab::API do +describe API::API do include ApiHelpers + before(:each) { ActiveRecord::Base.observers.enable(:user_observer) } + after(:each) { ActiveRecord::Base.observers.disable(:user_observer) } let(:user) { create(:user) } let!(:project) { create(:project, namespace: user.namespace ) } let!(:issue) { create(:issue, project: project, author: user) } - let!(:merge_request) { create(:merge_request, project: project, author: user) } - let!(:snippet) { create(:snippet, project: project, author: user) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } + let!(:snippet) { create(:project_snippet, project: project, author: user) } let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) } let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) } let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) } @@ -38,6 +40,11 @@ describe Gitlab::API do response.status.should == 200 json_response['body'].should == wall_note.note end + + it "should return a 404 error if note not found" do + get api("/projects/#{project.id}/notes/123", user) + response.status.should == 404 + end end describe "POST /projects/:id/notes" do @@ -46,6 +53,16 @@ describe Gitlab::API do response.status.should == 201 json_response['body'].should == 'hi!' end + + it "should return 401 unauthorized error" do + post api("/projects/#{project.id}/notes") + response.status.should == 401 + end + + it "should return a 400 bad request if body is missing" do + post api("/projects/#{project.id}/notes", user) + response.status.should == 400 + end end describe "GET /projects/:id/noteable/:noteable_id/notes" do @@ -56,6 +73,11 @@ describe Gitlab::API do json_response.should be_an Array json_response.first['body'].should == issue_note.note end + + it "should return a 404 error when issue id not found" do + get api("/projects/#{project.id}/issues/123/notes", user) + response.status.should == 404 + end end context "when noteable is a Snippet" do @@ -65,6 +87,11 @@ describe Gitlab::API do json_response.should be_an Array json_response.first['body'].should == snippet_note.note end + + it "should return a 404 error when snippet id not found" do + get api("/projects/#{project.id}/snippets/42/notes", user) + response.status.should == 404 + end end context "when noteable is a Merge Request" do @@ -74,6 +101,11 @@ describe Gitlab::API do json_response.should be_an Array json_response.first['body'].should == merge_request_note.note end + + it "should return a 404 error if merge request id not found" do + get api("/projects/#{project.id}/merge_requests/4444/notes", user) + response.status.should == 404 + end end end @@ -84,6 +116,11 @@ describe Gitlab::API do response.status.should == 200 json_response['body'].should == issue_note.note end + + it "should return a 404 error if issue note not found" do + get api("/projects/#{project.id}/issues/#{issue.id}/notes/123", user) + response.status.should == 404 + end end context "when noteable is a Snippet" do @@ -92,6 +129,11 @@ describe Gitlab::API do response.status.should == 200 json_response['body'].should == snippet_note.note end + + it "should return a 404 error if snippet note not found" do + get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/123", user) + response.status.should == 404 + end end end @@ -103,6 +145,16 @@ describe Gitlab::API do json_response['body'].should == 'hi!' json_response['author']['email'].should == user.email end + + it "should return a 400 bad request error if body not given" do + post api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + response.status.should == 400 + end + + it "should return a 401 unauthorized error if user not authenticated" do + post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' + response.status.should == 401 + end end context "when noteable is a Snippet" do @@ -112,6 +164,16 @@ describe Gitlab::API do json_response['body'].should == 'hi!' json_response['author']['email'].should == user.email end + + it "should return a 400 bad request error if body not given" do + post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) + response.status.should == 400 + end + + it "should return a 401 unauthorized error if user not authenticated" do + post api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!' + response.status.should == 401 + end end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 16fd1b9307c..b8c0b6f33ed 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1,16 +1,20 @@ require 'spec_helper' -describe Gitlab::API do +describe API::API do include ApiHelpers + before(:each) { enable_observers } + after(:each) { disable_observers } let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } + let(:admin) { create(:admin) } + let!(:project) { create(:project_with_code, creator_id: user.id, namespace: user.namespace) } let!(:hook) { create(:project_hook, project: project, url: "http://example.com") } - let!(:project) { create(:project, namespace: user.namespace ) } - let!(:snippet) { create(:snippet, author: user, project: project, title: 'example') } + let!(:snippet) { create(:project_snippet, author: user, project: project, title: 'example') } let!(:users_project) { create(:users_project, user: user, project: project, project_access: UsersProject::MASTER) } let!(:users_project2) { create(:users_project, user: user3, project: project, project_access: UsersProject::DEVELOPER) } + before { project.team << [user, :reporter] } describe "GET /projects" do @@ -33,6 +37,20 @@ describe Gitlab::API do end describe "POST /projects" do + context "maximum number of projects reached" do + before do + (1..user2.projects_limit).each do |project| + post api("/projects", user2), name: "foo#{project}" + end + end + + it "should not create new project" do + expect { + post api("/projects", user2), name: 'foo' + }.to change {Project.count}.by(0) + end + end + it "should create new project without path" do expect { post api("/projects", user), name: 'foo' }.to change {Project.count}.by(1) end @@ -41,14 +59,33 @@ describe Gitlab::API do expect { post api("/projects", user) }.to_not change {Project.count} end + it "should return a 400 error if name not given" do + post api("/projects", user) + response.status.should == 400 + end + + it "should create last project before reaching project limit" do + (1..user2.projects_limit-1).each { |p| post api("/projects", user2), name: "foo#{p}" } + post api("/projects", user2), name: "foo" + response.status.should == 201 + end + it "should respond with 201 on success" do post api("/projects", user), name: 'foo' response.status.should == 201 end - it "should respond with 404 on failure" do + it "should respond with 400 if name is not given" do post api("/projects", user) - response.status.should == 404 + response.status.should == 400 + end + + it "should return a 403 error if project limit reached" do + (1..user.projects_limit).each do |p| + post api("/projects", user), name: "foo#{p}" + end + post api("/projects", user), name: 'bar' + response.status.should == 403 end it "should assign attributes to project" do @@ -68,6 +105,76 @@ describe Gitlab::API do json_response[k.to_s].should == v end end + + it "should set a project as public" do + project = attributes_for(:project, { public: true }) + post api("/projects", user), project + json_response['public'].should be_true + + end + + it "should set a project as private" do + project = attributes_for(:project, { public: false }) + post api("/projects", user), project + json_response['public'].should be_false + + end + + end + + describe "POST /projects/user/:id" do + before { admin } + + it "should create new project without path" do + expect { post api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1) + end + + it "should not create new project without name" do + expect { post api("/projects/user/#{user.id}", admin) }.to_not change {Project.count} + end + + it "should respond with 201 on success" do + post api("/projects/user/#{user.id}", admin), name: 'foo' + response.status.should == 201 + end + + it "should respond with 404 on failure" do + post api("/projects/user/#{user.id}", admin) + response.status.should == 404 + end + + it "should assign attributes to project" do + project = attributes_for(:project, { + description: Faker::Lorem.sentence, + default_branch: 'stable', + issues_enabled: false, + wall_enabled: false, + merge_requests_enabled: false, + wiki_enabled: false + }) + + post api("/projects/user/#{user.id}", admin), project + + project.each_pair do |k,v| + next if k == :path + json_response[k.to_s].should == v + end + end + + it "should set a project as public" do + project = attributes_for(:project, { public: true }) + post api("/projects/user/#{user.id}", admin), project + json_response['public'].should be_true + + end + + it "should set a project as private" do + project = attributes_for(:project, { public: false }) + post api("/projects/user/#{user.id}", admin), project + json_response['public'].should be_false + + end + end describe "GET /projects/:id" do @@ -89,52 +196,34 @@ describe Gitlab::API do response.status.should == 404 json_response['message'].should == '404 Not Found' end - end - describe "GET /projects/:id/repository/branches" do - it "should return an array of project branches" do - get api("/projects/#{project.id}/repository/branches", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['name'].should == project.repo.heads.sort_by(&:name).first.name + it "should return a 404 error if user is not a member" do + other_user = create(:user) + get api("/projects/#{project.id}", other_user) + response.status.should == 404 end end - describe "GET /projects/:id/repository/branches/:branch" do - it "should return the branch information for a single branch" do - get api("/projects/#{project.id}/repository/branches/new_design", user) + describe "GET /projects/:id/events" do + it "should return a project events" do + get api("/projects/#{project.id}/events", user) response.status.should == 200 + json_event = json_response.first - json_response['name'].should == 'new_design' - json_response['commit']['id'].should == '621491c677087aa243f165eab467bfdfbee00be1' - json_response['protected'].should == false + json_event['action_name'].should == 'joined' + json_event['project_id'].to_i.should == project.id end - it "should return a 404 error if branch is not available" do - get api("/projects/#{project.id}/repository/branches/unknown", user) + it "should return a 404 error if not found" do + get api("/projects/42/events", user) response.status.should == 404 + json_response['message'].should == '404 Not Found' end - end - - describe "PUT /projects/:id/repository/branches/:branch/protect" do - it "should protect a single branch" do - put api("/projects/#{project.id}/repository/branches/new_design/protect", user) - response.status.should == 200 - - json_response['name'].should == 'new_design' - json_response['commit']['id'].should == '621491c677087aa243f165eab467bfdfbee00be1' - json_response['protected'].should == true - end - end - describe "PUT /projects/:id/repository/branches/:branch/unprotect" do - it "should unprotect a single branch" do - put api("/projects/#{project.id}/repository/branches/new_design/unprotect", user) - response.status.should == 200 - - json_response['name'].should == 'new_design' - json_response['commit']['id'].should == '621491c677087aa243f165eab467bfdfbee00be1' - json_response['protected'].should == false + it "should return a 404 error if user is not a member" do + other_user = create(:user) + get api("/projects/#{project.id}/events", other_user) + response.status.should == 404 end end @@ -144,7 +233,7 @@ describe Gitlab::API do response.status.should == 200 json_response.should be_an Array json_response.count.should == 2 - json_response.first['email'].should == user.email + json_response.map { |u| u['email'] }.should include user.email end it "finds team members with query string" do @@ -154,6 +243,11 @@ describe Gitlab::API do json_response.count.should == 1 json_response.first['email'].should == user.email end + + it "should return a 404 error if id not found" do + get api("/projects/9999/members", user) + response.status.should == 404 + end end describe "GET /projects/:id/members/:user_id" do @@ -163,6 +257,11 @@ describe Gitlab::API do json_response['email'].should == user.email json_response['access_level'].should == UsersProject::MASTER end + + it "should return a 404 error if user id not found" do + get api("/projects/#{project.id}/members/1234", user) + response.status.should == 404 + end end describe "POST /projects/:id/members" do @@ -176,6 +275,34 @@ describe Gitlab::API do json_response['email'].should == user2.email json_response['access_level'].should == UsersProject::DEVELOPER end + + it "should return a 201 status if user is already project member" do + post api("/projects/#{project.id}/members", user), user_id: user2.id, + access_level: UsersProject::DEVELOPER + expect { + post api("/projects/#{project.id}/members", user), user_id: user2.id, + access_level: UsersProject::DEVELOPER + }.not_to change { UsersProject.count }.by(1) + + response.status.should == 201 + json_response['email'].should == user2.email + json_response['access_level'].should == UsersProject::DEVELOPER + end + + it "should return a 400 error when user id is not given" do + post api("/projects/#{project.id}/members", user), access_level: UsersProject::MASTER + response.status.should == 400 + end + + it "should return a 400 error when access level is not given" do + post api("/projects/#{project.id}/members", user), user_id: user2.id + response.status.should == 400 + end + + it "should return a 422 error when access level is not known" do + post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234 + response.status.should == 422 + end end describe "PUT /projects/:id/members/:user_id" do @@ -185,6 +312,21 @@ describe Gitlab::API do json_response['email'].should == user3.email json_response['access_level'].should == UsersProject::MASTER end + + it "should return a 404 error if user_id is not found" do + put api("/projects/#{project.id}/members/1234", user), access_level: UsersProject::MASTER + response.status.should == 404 + end + + it "should return a 400 error when access level is not given" do + put api("/projects/#{project.id}/members/#{user3.id}", user) + response.status.should == 400 + end + + it "should return a 422 error when access level is not known" do + put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123 + response.status.should == 422 + end end describe "DELETE /projects/:id/members/:user_id" do @@ -193,25 +335,76 @@ describe Gitlab::API do delete api("/projects/#{project.id}/members/#{user3.id}", user) }.to change { UsersProject.count }.by(-1) end + + it "should return 200 if team member is not part of a project" do + delete api("/projects/#{project.id}/members/#{user3.id}", user) + expect { + delete api("/projects/#{project.id}/members/#{user3.id}", user) + }.to_not change { UsersProject.count }.by(1) + end + + it "should return 200 if team member already removed" do + delete api("/projects/#{project.id}/members/#{user3.id}", user) + delete api("/projects/#{project.id}/members/#{user3.id}", user) + response.status.should == 200 + end + end + + describe "DELETE /projects/:id/members/:user_id" do + it "should return 200 OK when the user was not member" do + expect { + delete api("/projects/#{project.id}/members/1000000", user) + }.to change { UsersProject.count }.by(0) + response.status.should == 200 + json_response['message'].should == "Access revoked" + json_response['id'].should == 1000000 + end end describe "GET /projects/:id/hooks" do - it "should return project hooks" do - get api("/projects/#{project.id}/hooks", user) + context "authorized user" do + it "should return project hooks" do + get api("/projects/#{project.id}/hooks", user) + response.status.should == 200 - response.status.should == 200 + json_response.should be_an Array + json_response.count.should == 1 + json_response.first['url'].should == "http://example.com" + end + end - json_response.should be_an Array - json_response.count.should == 1 - json_response.first['url'].should == "http://example.com" + context "unauthorized user" do + it "should not access project hooks" do + get api("/projects/#{project.id}/hooks", user3) + response.status.should == 403 + end end end describe "GET /projects/:id/hooks/:hook_id" do - it "should return a project hook" do - get api("/projects/#{project.id}/hooks/#{hook.id}", user) - response.status.should == 200 - json_response['url'].should == hook.url + context "authorized user" do + it "should return a project hook" do + get api("/projects/#{project.id}/hooks/#{hook.id}", user) + response.status.should == 200 + json_response['url'].should == hook.url + end + + it "should return a 404 error if hook id is not available" do + get api("/projects/#{project.id}/hooks/1234", user) + response.status.should == 404 + end + end + + context "unauthorized user" do + it "should not access an existing hook" do + get api("/projects/#{project.id}/hooks/#{hook.id}", user3) + response.status.should == 403 + end + end + + it "should return a 404 error if hook id is not available" do + get api("/projects/#{project.id}/hooks/1234", user) + response.status.should == 404 end end @@ -219,8 +412,19 @@ describe Gitlab::API do it "should add hook to project" do expect { post api("/projects/#{project.id}/hooks", user), - "url" => "http://example.com" + url: "http://example.com" }.to change {project.hooks.count}.by(1) + response.status.should == 201 + end + + it "should return a 400 error if url not given" do + post api("/projects/#{project.id}/hooks", user) + response.status.should == 400 + end + + it "should return a 422 error if url not valid" do + post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com" + response.status.should == 422 end end @@ -231,45 +435,44 @@ describe Gitlab::API do response.status.should == 200 json_response['url'].should == 'http://example.org' end - end + it "should return 404 error if hook id not found" do + put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org' + response.status.should == 404 + end + + it "should return 400 error if url is not given" do + put api("/projects/#{project.id}/hooks/#{hook.id}", user) + response.status.should == 400 + end + + it "should return a 422 error if url is not valid" do + put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com' + response.status.should == 422 + end + end - describe "DELETE /projects/:id/hooks" do + describe "DELETE /projects/:id/hooks/:hook_id" do it "should delete hook from project" do expect { - delete api("/projects/#{project.id}/hooks", user), - hook_id: hook.id + delete api("/projects/#{project.id}/hooks/#{hook.id}", user) }.to change {project.hooks.count}.by(-1) + response.status.should == 200 end - end - describe "GET /projects/:id/repository/tags" do - it "should return an array of project tags" do - get api("/projects/#{project.id}/repository/tags", user) + it "should return success when deleting hook" do + delete api("/projects/#{project.id}/hooks/#{hook.id}", user) response.status.should == 200 - json_response.should be_an Array - json_response.first['name'].should == project.repo.tags.sort_by(&:name).reverse.first.name end - end - describe "GET /projects/:id/repository/commits" do - context "authorized user" do - before { project.team << [user2, :reporter] } - - it "should return project commits" do - get api("/projects/#{project.id}/repository/commits", user) - response.status.should == 200 - - json_response.should be_an Array - json_response.first['id'].should == project.repository.commit.id - end + it "should return success when deleting non existent hook" do + delete api("/projects/#{project.id}/hooks/42", user) + response.status.should == 200 end - context "unauthorized user" do - it "should not return project commits" do - get api("/projects/#{project.id}/repository/commits") - response.status.should == 401 - end + it "should return a 405 error if hook id not given" do + delete api("/projects/#{project.id}/hooks", user) + response.status.should == 405 end end @@ -288,6 +491,11 @@ describe Gitlab::API do response.status.should == 200 json_response['title'].should == snippet.title end + + it "should return a 404 error if snippet id not found" do + get api("/projects/#{project.id}/snippets/1234", user) + response.status.should == 404 + end end describe "POST /projects/:id/snippets" do @@ -297,6 +505,24 @@ describe Gitlab::API do response.status.should == 201 json_response['title'].should == 'api test' end + + it "should return a 400 error if title is not given" do + post api("/projects/#{project.id}/snippets", user), + file_name: 'sample.rb', code: 'test' + response.status.should == 400 + end + + it "should return a 400 error if file_name not given" do + post api("/projects/#{project.id}/snippets", user), + title: 'api test', code: 'test' + response.status.should == 400 + end + + it "should return a 400 error if code not given" do + post api("/projects/#{project.id}/snippets", user), + title: 'api test', file_name: 'sample.rb' + response.status.should == 400 + end end describe "PUT /projects/:id/snippets/:shippet_id" do @@ -307,6 +533,13 @@ describe Gitlab::API do json_response['title'].should == 'example' snippet.reload.content.should == 'updated code' end + + it "should update an existing project snippet with new title" do + put api("/projects/#{project.id}/snippets/#{snippet.id}", user), + title: 'other api test' + response.status.should == 200 + json_response['title'].should == 'other api test' + end end describe "DELETE /projects/:id/snippets/:snippet_id" do @@ -314,6 +547,12 @@ describe Gitlab::API do expect { delete api("/projects/#{project.id}/snippets/#{snippet.id}", user) }.to change { Snippet.count }.by(-1) + response.status.should == 200 + end + + it "should return success when deleting unknown snippet id" do + delete api("/projects/#{project.id}/snippets/1234", user) + response.status.should == 200 end end @@ -322,22 +561,173 @@ describe Gitlab::API do get api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user) response.status.should == 200 end + + it "should return a 404 error if raw project snippet not found" do + get api("/projects/#{project.id}/snippets/5555/raw", user) + response.status.should == 404 + end end - describe "GET /projects/:id/:sha/blob" do - it "should get the raw file contents" do - get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user) - response.status.should == 200 + describe :deploy_keys do + let(:deploy_keys_project) { create(:deploy_keys_project, project: project) } + let(:deploy_key) { deploy_keys_project.deploy_key } + + describe "GET /projects/:id/keys" do + before { deploy_key } + + it "should return array of ssh keys" do + get api("/projects/#{project.id}/keys", user) + response.status.should == 200 + json_response.should be_an Array + json_response.first['title'].should == deploy_key.title + end end - it "should return 404 for invalid branch_name" do - get api("/projects/#{project.id}/repository/commits/invalid_branch_name/blob?filepath=README.md", user) - response.status.should == 404 + describe "GET /projects/:id/keys/:key_id" do + it "should return a single key" do + get api("/projects/#{project.id}/keys/#{deploy_key.id}", user) + response.status.should == 200 + json_response['title'].should == deploy_key.title + end + + it "should return 404 Not Found with invalid ID" do + get api("/projects/#{project.id}/keys/404", user) + response.status.should == 404 + end end - it "should return 404 for invalid file" do - get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.invalid", user) - response.status.should == 404 + describe "POST /projects/:id/keys" do + it "should not create an invalid ssh key" do + post api("/projects/#{project.id}/keys", user), { title: "invalid key" } + response.status.should == 404 + end + + it "should create new ssh key" do + key_attrs = attributes_for :key + expect { + post api("/projects/#{project.id}/keys", user), key_attrs + }.to change{ project.deploy_keys.count }.by(1) + end + end + + describe "DELETE /projects/:id/keys/:key_id" do + before { deploy_key } + + it "should delete existing key" do + expect { + delete api("/projects/#{project.id}/keys/#{deploy_key.id}", user) + }.to change{ project.deploy_keys.count }.by(-1) + end + + it "should return 404 Not Found with invalid ID" do + delete api("/projects/#{project.id}/keys/404", user) + response.status.should == 404 + end + end + end + + describe :fork_admin do + let(:project_fork_target) { create(:project) } + let(:project_fork_source) { create(:project, public: true) } + + describe "POST /projects/:id/fork/:forked_from_id" do + let(:new_project_fork_source) { create(:project, public: true) } + + it "shouldn't available for non admin users" do + post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user) + response.status.should == 403 + end + + it "should allow project to be forked from an existing project" do + project_fork_target.forked?.should_not be_true + post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + response.status.should == 201 + project_fork_target.reload + project_fork_target.forked_from_project.id.should == project_fork_source.id + project_fork_target.forked_project_link.should_not be_nil + project_fork_target.forked?.should be_true + end + + it "should fail if forked_from project which does not exist" do + post api("/projects/#{project_fork_target.id}/fork/9999", admin) + response.status.should == 404 + end + + it "should fail with 409 if already forked" do + post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + project_fork_target.reload + project_fork_target.forked_from_project.id.should == project_fork_source.id + post api("/projects/#{project_fork_target.id}/fork/#{new_project_fork_source.id}", admin) + response.status.should == 409 + project_fork_target.reload + project_fork_target.forked_from_project.id.should == project_fork_source.id + project_fork_target.forked?.should be_true + end + end + + describe "DELETE /projects/:id/fork" do + + it "shouldn't available for non admin users" do + delete api("/projects/#{project_fork_target.id}/fork", user) + response.status.should == 403 + end + + it "should make forked project unforked" do + post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + project_fork_target.reload + project_fork_target.forked_from_project.should_not be_nil + project_fork_target.forked?.should be_true + delete api("/projects/#{project_fork_target.id}/fork", admin) + response.status.should == 200 + project_fork_target.reload + project_fork_target.forked_from_project.should be_nil + project_fork_target.forked?.should_not be_true + end + + it "should be idempotent if not forked" do + project_fork_target.forked_from_project.should be_nil + delete api("/projects/#{project_fork_target.id}/fork", admin) + response.status.should == 200 + project_fork_target.reload.forked_from_project.should be_nil + end + end + end + + describe "GET /projects/search/:query" do + let!(:query) { 'query'} + let!(:search) { create(:project, name: query, creator_id: user.id, namespace: user.namespace) } + let!(:pre) { create(:project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) } + let!(:post) { create(:project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) } + let!(:pre_post) { create(:project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) } + let!(:unfound) { create(:project, name: 'unfound', creator_id: user.id, namespace: user.namespace) } + let!(:public) { create(:project, name: "another #{query}",public: true) } + let!(:unfound_public) { create(:project, name: 'unfound public', public: true) } + + context "when unauthenticated" do + it "should return authentication error" do + get api("/projects/search/#{query}") + response.status.should == 401 + end + end + + context "when authenticated" do + it "should return an array of projects" do + get api("/projects/search/#{query}",user) + response.status.should == 200 + json_response.should be_an Array + json_response.size.should == 5 + json_response.each {|project| project['name'].should =~ /.*query.*/} + end + end + + context "when authenticated as a different user" do + it "should return matching public projects" do + get api("/projects/search/#{query}", user2) + response.status.should == 200 + json_response.should be_an Array + json_response.size.should == 1 + json_response.first['name'].should == "another #{query}" + end end end end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb new file mode 100644 index 00000000000..f15abdd3581 --- /dev/null +++ b/spec/requests/api/repositories_spec.rb @@ -0,0 +1,217 @@ +require 'spec_helper' + +describe API::API do + include ApiHelpers + before(:each) { enable_observers } + after(:each) {disable_observers} + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:project) { create(:project_with_code, creator_id: user.id) } + let!(:users_project) { create(:users_project, user: user, project: project, project_access: UsersProject::MASTER) } + + before { project.team << [user, :reporter] } + + + describe "GET /projects/:id/repository/branches" do + it "should return an array of project branches" do + get api("/projects/#{project.id}/repository/branches", user) + response.status.should == 200 + json_response.should be_an Array + json_response.first['name'].should == project.repo.heads.sort_by(&:name).first.name + end + end + + describe "GET /projects/:id/repository/branches/:branch" do + it "should return the branch information for a single branch" do + get api("/projects/#{project.id}/repository/branches/new_design", user) + response.status.should == 200 + + json_response['name'].should == 'new_design' + json_response['commit']['id'].should == '621491c677087aa243f165eab467bfdfbee00be1' + json_response['protected'].should == false + end + + it "should return a 404 error if branch is not available" do + get api("/projects/#{project.id}/repository/branches/unknown", user) + response.status.should == 404 + end + end + + describe "PUT /projects/:id/repository/branches/:branch/protect" do + it "should protect a single branch" do + put api("/projects/#{project.id}/repository/branches/new_design/protect", user) + response.status.should == 200 + + json_response['name'].should == 'new_design' + json_response['commit']['id'].should == '621491c677087aa243f165eab467bfdfbee00be1' + json_response['protected'].should == true + end + + it "should return a 404 error if branch not found" do + put api("/projects/#{project.id}/repository/branches/unknown/protect", user) + response.status.should == 404 + end + + it "should return success when protect branch again" do + put api("/projects/#{project.id}/repository/branches/new_design/protect", user) + put api("/projects/#{project.id}/repository/branches/new_design/protect", user) + response.status.should == 200 + end + end + + describe "PUT /projects/:id/repository/branches/:branch/unprotect" do + it "should unprotect a single branch" do + put api("/projects/#{project.id}/repository/branches/new_design/unprotect", user) + response.status.should == 200 + + json_response['name'].should == 'new_design' + json_response['commit']['id'].should == '621491c677087aa243f165eab467bfdfbee00be1' + json_response['protected'].should == false + end + + it "should return success when unprotect branch" do + put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user) + response.status.should == 404 + end + + it "should return success when unprotect branch again" do + put api("/projects/#{project.id}/repository/branches/new_design/unprotect", user) + put api("/projects/#{project.id}/repository/branches/new_design/unprotect", user) + response.status.should == 200 + end + end + + describe "GET /projects/:id/repository/tags" do + it "should return an array of project tags" do + get api("/projects/#{project.id}/repository/tags", user) + response.status.should == 200 + json_response.should be_an Array + json_response.first['name'].should == project.repo.tags.sort_by(&:name).reverse.first.name + end + end + + describe "GET /projects/:id/repository/commits" do + context "authorized user" do + before { project.team << [user2, :reporter] } + + it "should return project commits" do + get api("/projects/#{project.id}/repository/commits", user) + response.status.should == 200 + + json_response.should be_an Array + json_response.first['id'].should == project.repository.commit.id + end + end + + context "unauthorized user" do + it "should not return project commits" do + get api("/projects/#{project.id}/repository/commits") + response.status.should == 401 + end + end + end + + describe "GET /projects:id/repository/commits/:sha" do + context "authorized user" do + it "should return a commit by sha" do + get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + response.status.should == 200 + json_response['id'].should == project.repository.commit.id + json_response['title'].should == project.repository.commit.title + end + + it "should return a 404 error if not found" do + get api("/projects/#{project.id}/repository/commits/invalid_sha", user) + response.status.should == 404 + end + end + + context "unauthorized user" do + it "should not return the selected commit" do + get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}") + response.status.should == 401 + end + end + end + + describe "GET /projects:id/repository/commits/:sha/diff" do + context "authorized user" do + before { project.team << [user2, :reporter] } + + it "should return the diff of the selected commit" do + get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user) + response.status.should == 200 + + json_response.should be_an Array + json_response.length.should >= 1 + json_response.first.keys.should include "diff" + end + + it "should return a 404 error if invalid commit" do + get api("/projects/#{project.id}/repository/commits/invalid_sha/diff", 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/#{project.repository.commit.id}/diff") + response.status.should == 401 + end + end + end + + describe "GET /projects/:id/repository/tree" do + context "authorized user" do + before { project.team << [user2, :reporter] } + + it "should return project commits" do + get api("/projects/#{project.id}/repository/tree", user) + response.status.should == 200 + + json_response.should be_an Array + json_response.first['name'].should == 'app' + json_response.first['type'].should == 'tree' + json_response.first['mode'].should == '040000' + end + end + + context "unauthorized user" do + it "should not return project commits" do + get api("/projects/#{project.id}/repository/tree") + response.status.should == 401 + end + end + end + + describe "GET /projects/:id/repository/blobs/:sha" do + it "should get the raw file contents" do + get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user) + response.status.should == 200 + end + + it "should return 404 for invalid branch_name" do + get api("/projects/#{project.id}/repository/blobs/invalid_branch_name?filepath=README.md", user) + response.status.should == 404 + end + + it "should return 404 for invalid file" do + get api("/projects/#{project.id}/repository/blobs/master?filepath=README.invalid", user) + response.status.should == 404 + end + + it "should return a 400 error if filepath is missing" do + get api("/projects/#{project.id}/repository/blobs/master", user) + response.status.should == 400 + end + end + + describe "GET /projects/:id/repository/commits/:sha/blob" do + it "should get the raw file contents" do + get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user) + response.status.should == 200 + end + end + +end diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb index afae8be8cbc..88c17f26a69 100644 --- a/spec/requests/api/session_spec.rb +++ b/spec/requests/api/session_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::API do +describe API::API do include ApiHelpers let(:user) { create(:user) } @@ -13,6 +13,10 @@ describe Gitlab::API do json_response['email'].should == user.email json_response['private_token'].should == user.private_token + json_response['is_admin'].should == user.is_admin? + json_response['can_create_team'].should == user.can_create_team? + json_response['can_create_project'].should == user.can_create_project? + json_response['can_create_group'].should == user.can_create_group? end end @@ -35,5 +39,15 @@ describe Gitlab::API do json_response['private_token'].should be_nil end end + + context "when empty name" do + it "should return authentication error" do + post api("/session"), password: user.password + response.status.should == 401 + + json_response['email'].should be_nil + json_response['private_token'].should be_nil + end + end end end diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb new file mode 100644 index 00000000000..b1df3cb7886 --- /dev/null +++ b/spec/requests/api/system_hooks_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe API::API do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let!(:hook) { create(:system_hook, url: "http://example.com") } + + before { stub_request(:post, hook.url) } + + describe "GET /hooks" do + context "when no user" do + it "should return authentication error" do + get api("/hooks") + response.status.should == 401 + end + end + + context "when not an admin" do + it "should return forbidden error" do + get api("/hooks", user) + response.status.should == 403 + end + end + + context "when authenticated as admin" do + it "should return an array of hooks" do + get api("/hooks", admin) + response.status.should == 200 + json_response.should be_an Array + json_response.first['url'].should == hook.url + end + end + end + + describe "POST /hooks" do + it "should create new hook" do + expect { + post api("/hooks", admin), url: 'http://example.com' + }.to change { SystemHook.count }.by(1) + end + + it "should respond with 400 if url not given" do + post api("/hooks", admin) + response.status.should == 400 + end + + it "should not create new hook without url" do + expect { + post api("/hooks", admin) + }.to_not change { SystemHook.count } + end + end + + describe "GET /hooks/:id" do + it "should return hook by id" do + get api("/hooks/#{hook.id}", admin) + response.status.should == 200 + json_response['event_name'].should == 'project_create' + end + + it "should return 404 on failure" do + get api("/hooks/404", admin) + response.status.should == 404 + end + end + + describe "DELETE /hooks/:id" do + it "should delete a hook" do + expect { + delete api("/hooks/#{hook.id}", admin) + }.to change { SystemHook.count }.by(-1) + end + + it "should return success if hook id not found" do + delete api("/hooks/12345", admin) + response.status.should == 200 + end + end +end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 1645117e231..2fced3ec945 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::API do +describe API::API do include ApiHelpers let(:user) { create(:user) } @@ -31,15 +31,20 @@ describe Gitlab::API do response.status.should == 200 json_response['email'].should == user.email end - end - describe "POST /users" do - before{ admin } + it "should return a 401 if unauthenticated" do + get api("/users/9998") + response.status.should == 401 + end - it "should not create invalid user" do - post api("/users", admin), { email: "invalid email" } + it "should return a 404 error if user id not found" do + get api("/users/9999", user) response.status.should == 404 end + end + + describe "POST /users" do + before{ admin } it "should create user" do expect { @@ -47,46 +52,92 @@ describe Gitlab::API do }.to change { User.count }.by(1) end + it "should return 201 Created on success" do + post api("/users", admin), attributes_for(:user, projects_limit: 3) + response.status.should == 201 + end + + it "creating a user should respect default project limit" do + limit = 123456 + Gitlab.config.gitlab.stub(:default_projects_limit).and_return(limit) + attr = attributes_for(:user ) + expect { + post api("/users", admin), attr + }.to change { User.count }.by(1) + user = User.find_by_username(attr[:username]) + user.projects_limit.should == limit + user.theme_id.should == Gitlab::Theme::MARS + Gitlab.config.gitlab.unstub(:default_projects_limit) + end + + it "should not create user with invalid email" do + post api("/users", admin), { email: "invalid email", password: 'password' } + response.status.should == 400 + end + + it "should return 400 error if password not given" do + post api("/users", admin), { email: 'test@example.com' } + response.status.should == 400 + end + + it "should return 400 error if email not given" do + post api("/users", admin), { password: 'pass1234' } + response.status.should == 400 + end + it "shouldn't available for non admin users" do post api("/users", user), attributes_for(:user) response.status.should == 403 end - end - describe "GET /users/sign_up" do - before do - Gitlab.config.gitlab.stub(:signup_enabled).and_return(false) - end - it "should redirect to sign in page if signup is disabled" do - get "/users/sign_up" - response.status.should == 302 - response.should redirect_to(new_user_session_path) + context "with existing user" do + before { post api("/users", admin), { email: 'test@example.com', password: 'password', username: 'test' } } + + it "should not create user with same email" do + expect { + post api("/users", admin), { email: 'test@example.com', password: 'password' } + }.to change { User.count }.by(0) + end + + it "should return 409 conflict error if user with email exists" do + post api("/users", admin), { email: 'test@example.com', password: 'password' } + end + + it "should return 409 conflict error if same username exists" do + post api("/users", admin), { email: 'foo@example.com', password: 'pass', username: 'test' } + end end end describe "GET /users/sign_up" do - before do - Gitlab.config.gitlab.stub(:signup_enabled).and_return(true) - end - it "should return sign up page if signup is enabled" do - get "/users/sign_up" - response.status.should == 200 + context 'enabled' do + before do + Gitlab.config.gitlab.stub(:signup_enabled).and_return(true) + end + + it "should return sign up page if signup is enabled" do + get "/users/sign_up" + response.status.should == 200 + end end - it "should create a new user account" do - visit new_user_registration_path - 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_confirmation", with: "password1234" - expect { click_button "Sign up" }.to change {User.count}.by(1) + + context 'disabled' do + before do + Gitlab.config.gitlab.stub(:signup_enabled).and_return(false) + end + + it "should redirect to sign in page if signup is disabled" do + get "/users/sign_up" + response.status.should == 302 + response.should redirect_to(new_user_session_path) + end end end describe "PUT /users/:id" do before { admin } - it "should update user" do + it "should update user with new bio" do put api("/users/#{user.id}", admin), {bio: 'new test bio'} response.status.should == 200 json_response['bio'].should == 'new test bio' @@ -108,6 +159,41 @@ describe Gitlab::API do put api("/users/999999", admin), {bio: 'update should fail'} response.status.should == 404 end + + context "with existing user" do + before { + post api("/users", admin), { email: 'test@example.com', password: 'password', username: 'test', name: 'test' } + post api("/users", admin), { email: 'foo@bar.com', password: 'password', username: 'john', name: 'john' } + @user_id = User.all.last.id + } + +# it "should return 409 conflict error if email address exists" do +# put api("/users/#{@user_id}", admin), { email: 'test@example.com' } +# response.status.should == 409 +# end +# +# it "should return 409 conflict error if username taken" do +# @user_id = User.all.last.id +# put api("/users/#{@user_id}", admin), { username: 'test' } +# response.status.should == 409 +# end + end + end + + describe "POST /users/:id/keys" do + before { admin } + + it "should not create invalid ssh key" do + post api("/users/#{user.id}/keys", admin), { title: "invalid key" } + response.status.should == 404 + end + + it "should create ssh key" do + key_attrs = attributes_for :key + expect { + post api("/users/#{user.id}/keys", admin), key_attrs + }.to change{ user.keys.count }.by(1) + end end describe "DELETE /users/:id" do @@ -120,6 +206,11 @@ describe Gitlab::API do json_response['email'].should == user.email end + it "should not delete for unauthenticated user" do + delete api("/users/#{user.id}") + response.status.should == 401 + end + it "shouldn't available for non admin users" do delete api("/users/#{user.id}", user) response.status.should == 403 @@ -136,6 +227,15 @@ describe Gitlab::API do get api("/user", user) response.status.should == 200 json_response['email'].should == user.email + json_response['is_admin'].should == user.is_admin? + json_response['can_create_team'].should == user.can_create_team? + json_response['can_create_project'].should == user.can_create_project? + json_response['can_create_group'].should == user.can_create_group? + end + + it "should return 401 error if user is unauthenticated" do + get api("/user") + response.status.should == 401 end end @@ -160,7 +260,7 @@ describe Gitlab::API do end describe "GET /user/keys/:id" do - it "should returm single key" do + it "should return single key" do user.keys << key user.save get api("/user/keys/#{key.id}", user) @@ -172,19 +272,38 @@ describe Gitlab::API do get api("/user/keys/42", user) response.status.should == 404 end - end - describe "POST /user/keys" do - it "should not create invalid ssh key" do - post api("/user/keys", user), { title: "invalid key" } + it "should return 404 error if admin accesses user's ssh key" do + user.keys << key + user.save + admin + get api("/user/keys/#{key.id}", admin) response.status.should == 404 end + end + describe "POST /user/keys" do it "should create ssh key" do key_attrs = attributes_for :key expect { post api("/user/keys", user), key_attrs }.to change{ user.keys.count }.by(1) + response.status.should == 201 + end + + it "should return a 401 error if unauthorized" do + post api("/user/keys"), title: 'some title', key: 'some key' + response.status.should == 401 + end + + it "should not create ssh key without key" do + post api("/user/keys", user), title: 'title' + response.status.should == 400 + end + + it "should not create ssh key without title" do + post api("/user/keys", user), key: "somekey" + response.status.should == 400 end end @@ -195,11 +314,19 @@ describe Gitlab::API do expect { delete api("/user/keys/#{key.id}", user) }.to change{user.keys.count}.by(-1) + response.status.should == 200 end - it "should return 404 Not Found within invalid ID" do + it "should return success if key ID not found" do delete api("/user/keys/42", user) - response.status.should == 404 + response.status.should == 200 + end + + it "should return 401 error if unauthorized" do + user.keys << key + user.save + delete api("/user/keys/#{key.id}") + response.status.should == 401 end end end diff --git a/spec/requests/gitlab_flavored_markdown_spec.rb b/spec/requests/gitlab_flavored_markdown_spec.rb deleted file mode 100644 index 9a568511fa0..00000000000 --- a/spec/requests/gitlab_flavored_markdown_spec.rb +++ /dev/null @@ -1,223 +0,0 @@ -require 'spec_helper' - -describe "Gitlab Flavored Markdown" do - let(:project) { create(:project) } - let(:issue) { create(:issue, project: project) } - let(:merge_request) { create(:merge_request, project: project) } - let(:fred) do - u = create(:user, name: "fred") - project.team << [u, :master] - u - end - - before do - # add test branch - @branch_name = "gfm-test" - r = project.repo - i = r.index - # add test file - @test_file = "gfm_test_file" - i.add(@test_file, "foo\nbar\n") - # add commit with gfm - i.commit("fix ##{issue.id}\n\nask @#{fred.username} for details", head: @branch_name) - - # add test tag - @tag_name = "gfm-test-tag" - r.git.native(:tag, {}, @tag_name, commit.id) - end - - after do - # delete test branch and tag - project.repo.git.native(:branch, {D: true}, @branch_name) - project.repo.git.native(:tag, {d: true}, @tag_name) - project.repo.gc_auto - end - - let(:commit) { project.repository.commits(@branch_name).first } - - before do - login_as :user - project.team << [@user, :developer] - end - - describe "for commits" do - it "should render title in commits#index" do - visit project_commits_path(project, @branch_name, limit: 1) - - page.should have_link("##{issue.id}") - end - - it "should render title in commits#show" do - visit project_commit_path(project, commit) - - page.should have_link("##{issue.id}") - end - - it "should render description in commits#show" do - visit project_commit_path(project, commit) - - page.should have_link("@#{fred.username}") - end - - it "should render title in refs#tree", js: true do - visit project_tree_path(project, @branch_name) - - within(".tree_commit") do - page.should have_link("##{issue.id}") - end - end - - # @wip - #it "should render title in refs#blame" do - #visit project_blame_path(project, File.join(@branch_name, @test_file)) - - #within(".blame_commit") do - #page.should have_link("##{issue.id}") - #end - #end - - it "should render title in repositories#branches" do - visit branches_project_repository_path(project) - - page.should have_link("##{issue.id}") - end - end - - describe "for issues" do - before do - @other_issue = create(:issue, - author: @user, - assignee: @user, - project: project) - @issue = create(:issue, - author: @user, - assignee: @user, - project: project, - title: "fix ##{@other_issue.id}", - description: "ask @#{fred.username} for details") - end - - it "should render subject in issues#index" do - visit project_issues_path(project) - - page.should have_link("##{@other_issue.id}") - end - - it "should render subject in issues#show" do - visit project_issue_path(project, @issue) - - page.should have_link("##{@other_issue.id}") - end - - it "should render details in issues#show" do - visit project_issue_path(project, @issue) - - page.should have_link("@#{fred.username}") - end - end - - - describe "for merge requests" do - before do - @merge_request = create(:merge_request, - project: project, - title: "fix ##{issue.id}") - end - - it "should render title in merge_requests#index" do - visit project_merge_requests_path(project) - - page.should have_link("##{issue.id}") - end - - it "should render title in merge_requests#show" do - visit project_merge_request_path(project, @merge_request) - - page.should have_link("##{issue.id}") - end - end - - - describe "for milestones" do - before do - @milestone = create(:milestone, - project: project, - title: "fix ##{issue.id}", - description: "ask @#{fred.username} for details") - end - - it "should render title in milestones#index" do - visit project_milestones_path(project) - - page.should have_link("##{issue.id}") - end - - it "should render title in milestones#show" do - visit project_milestone_path(project, @milestone) - - page.should have_link("##{issue.id}") - end - - it "should render description in milestones#show" do - visit project_milestone_path(project, @milestone) - - page.should have_link("@#{fred.username}") - end - end - - - describe "for notes" do - it "should render in commits#show", js: true do - visit project_commit_path(project, commit) - fill_in "note_note", with: "see ##{issue.id}" - click_button "Add Comment" - - page.should have_link("##{issue.id}") - end - - it "should render in issue#show", js: true do - visit project_issue_path(project, issue) - fill_in "note_note", with: "see ##{issue.id}" - click_button "Add Comment" - - page.should have_link("##{issue.id}") - end - - it "should render in merge_request#show", js: true do - visit project_merge_request_path(project, merge_request) - fill_in "note_note", with: "see ##{issue.id}" - click_button "Add Comment" - - page.should have_link("##{issue.id}") - end - - it "should render in projects#wall", js: true do - visit wall_project_path(project) - fill_in "note_note", with: "see ##{issue.id}" - click_button "Add Comment" - - page.should have_link("##{issue.id}") - end - end - - - describe "for wikis" do - before do - visit project_wiki_path(project, :index) - fill_in "Title", with: "Circumvent ##{issue.id}" - fill_in "Content", with: "# Other pages\n\n* [Foo](foo)\n* [Bar](bar)\n\nAlso look at ##{issue.id} :-)" - click_on "Save" - end - - it "should NOT render title in wikis#show" do - within(".content h3") do # page title - page.should have_content("Circumvent ##{issue.id}") - page.should_not have_link("##{issue.id}") - end - end - - it "should render content in wikis#show" do - page.should have_link("##{issue.id}") - end - end -end diff --git a/spec/requests/notes_on_merge_requests_spec.rb b/spec/requests/notes_on_merge_requests_spec.rb deleted file mode 100644 index 0111cf42ac7..00000000000 --- a/spec/requests/notes_on_merge_requests_spec.rb +++ /dev/null @@ -1,232 +0,0 @@ -require 'spec_helper' - -describe "On a merge request", js: true do - let!(:project) { create(:project) } - let!(:merge_request) { create(:merge_request, project: project) } - - before do - login_as :user - project.team << [@user, :master] - - visit project_merge_request_path(project, merge_request) - end - - subject { page } - - describe "the note form" do - # main target form creation - it { should have_css(".js-main-target-form", visible: true, count: 1) } - - # button initalization - it { within(".js-main-target-form") { should have_button("Add Comment") } } - it { within(".js-main-target-form") { should_not have_link("Cancel") } } - - # notifiactions - it { within(".js-main-target-form") { should have_checked_field("Notify team via email") } } - it { within(".js-main-target-form") { should_not have_checked_field("Notify commit author") } } - it { within(".js-main-target-form") { should_not have_unchecked_field("Notify commit author") } } - - describe "without text" do - it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } } - end - - describe "with text" do - before do - within(".js-main-target-form") do - fill_in "note[note]", with: "This is awesome" - end - end - - it { within(".js-main-target-form") { should_not have_css(".js-comment-button[disabled]") } } - - it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: true) } } - end - - describe "with preview" do - before do - within(".js-main-target-form") do - fill_in "note[note]", with: "This is awesome" - find(".js-note-preview-button").trigger("click") - end - end - - it { within(".js-main-target-form") { should have_css(".js-note-preview", text: "This is awesome", visible: true) } } - - it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } } - it { within(".js-main-target-form") { should have_css(".js-note-edit-button", visible: true) } } - end - end - - describe "when posting a note" do - before do - within(".js-main-target-form") do - fill_in "note[note]", with: "This is awsome!" - find(".js-note-preview-button").trigger("click") - click_button "Add Comment" - end - end - - # note added - it { within(".js-main-target-form") { should have_content("This is awsome!") } } - - # reset form - it { within(".js-main-target-form") { should have_no_field("note[note]", with: "This is awesome!") } } - - # return from preview - it { within(".js-main-target-form") { should have_css(".js-note-preview", visible: false) } } - it { within(".js-main-target-form") { should have_css(".js-note-text", visible: true) } } - - - it "should be removable" do - find(".js-note-delete").trigger("click") - - should_not have_css(".note") - end - end -end - - - -describe "On a merge request diff", js: true, focus: true do - let!(:project) { create(:project) } - let!(:merge_request) { create(:merge_request_with_diffs, project: project) } - - before do - login_as :user - project.team << [@user, :master] - - visit diffs_project_merge_request_path(project, merge_request) - - click_link("Diff") - end - - subject { page } - - describe "when adding a note" do - before do - find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder .js-add-diff-note-button").trigger("click") - end - - describe "the notes holder" do - it { should have_css("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder + .js-temp-notes-holder") } - - it { within(".js-temp-notes-holder") { should have_css(".new_note") } } - end - - describe "the note form" do - # set up hidden fields correctly - it { within(".js-temp-notes-holder") { find("#note_noteable_type").value.should == "MergeRequest" } } - it { within(".js-temp-notes-holder") { find("#note_noteable_id").value.should == merge_request.id.to_s } } - it { within(".js-temp-notes-holder") { find("#note_commit_id").value.should == "" } } - it { within(".js-temp-notes-holder") { find("#note_line_code").value.should == "4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185" } } - - # buttons - it { should have_button("Add Comment") } - it { should have_css(".js-close-discussion-note-form", text: "Cancel") } - - # notification options - it { should have_checked_field("Notify team via email") } - - it "shouldn't add a second form for same row" do - find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder .js-add-diff-note-button").trigger("click") - - should have_css("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder + .js-temp-notes-holder form", count: 1) - end - - it "should be removed when canceled" do - find(".js-close-discussion-note-form").trigger("click") - - should have_no_css(".js-temp-notes-holder") - end - end - end - - describe "with muliple note forms" do - before do - find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder .js-add-diff-note-button").trigger("click") - find("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder .js-add-diff-note-button").trigger("click") - end - - # has two line forms - it { should have_css(".js-temp-notes-holder", count: 2) } - - describe "previewing them separately" do - before do - # add two separate texts and trigger previews on both - within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder + .js-temp-notes-holder") do - fill_in "note[note]", with: "One comment on line 185" - find(".js-note-preview-button").trigger("click") - end - within("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .js-temp-notes-holder") do - fill_in "note[note]", with: "Another comment on line 17" - find(".js-note-preview-button").trigger("click") - end - end - - # check if previews were rendered separately - it { within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder + .js-temp-notes-holder") { should have_css(".js-note-preview", text: "One comment on line 185") } } - it { within("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .js-temp-notes-holder") { should have_css(".js-note-preview", text: "Another comment on line 17") } } - end - - describe "posting a note" do - before do - within("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .js-temp-notes-holder") do - fill_in "note[note]", with: "Another comment on line 17" - click_button("Add Comment") - end - end - - # removed form after submit - it { should have_no_css("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .js-temp-notes-holder") } - - # added discussion - it { should have_content("Another comment on line 17") } - it { should have_css("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .notes_holder") } - it { should have_css("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .notes_holder .note", count: 1) } - it { should have_link("Reply") } - - it "should remove last note of a discussion" do - within("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .notes_holder") do - find(".js-note-delete").trigger("click") - end - - # removed whole discussion - should_not have_css(".note_holder") - should have_css("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + #342e16cbbd482ac2047dc679b2749d248cc1428f_18_18.line_holder") - end - end - end - - describe "when replying to a note" do - before do - # create first note - find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder .js-add-diff-note-button").trigger("click") - within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder + .js-temp-notes-holder") do - fill_in "note[note]", with: "One comment on line 184" - click_button("Add Comment") - end - # create second note - within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder + .notes_holder") do - find(".js-discussion-reply-button").trigger("click") - fill_in "note[note]", with: "An additional comment in reply" - click_button("Add Comment") - end - end - - # inserted note - it { should have_content("An additional comment in reply") } - it { within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder + .notes_holder") { should have_css(".note", count: 2) } } - - # removed form after reply - it { within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder + .notes_holder") { should have_no_css("form") } } - it { within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder + .notes_holder") { should have_link("Reply") } } - end -end - - - -describe "On merge request discussion", js: true do - describe "with merge request diff note" - describe "with commit note" - describe "with commit diff note" -end diff --git a/spec/requests/notes_on_wall_spec.rb b/spec/requests/notes_on_wall_spec.rb deleted file mode 100644 index 4adcf74e0b6..00000000000 --- a/spec/requests/notes_on_wall_spec.rb +++ /dev/null @@ -1,85 +0,0 @@ -require 'spec_helper' - -describe "On the project wall", js: true do - let!(:project) { create(:project) } - let!(:commit) { project.repository.commit("bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a") } - - before do - login_as :user - project.team << [@user, :master] - visit wall_project_path(project) - end - - subject { page } - - describe "the note form" do - # main target form creation - it { should have_css(".js-main-target-form", visible: true, count: 1) } - - # button initalization - it { within(".js-main-target-form") { should have_button("Add Comment") } } - it { within(".js-main-target-form") { should_not have_link("Cancel") } } - - # notifiactions - it { within(".js-main-target-form") { should have_checked_field("Notify team via email") } } - it { within(".js-main-target-form") { should_not have_checked_field("Notify commit author") } } - it { within(".js-main-target-form") { should_not have_unchecked_field("Notify commit author") } } - - describe "without text" do - it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } } - end - - describe "with text" do - before do - within(".js-main-target-form") do - fill_in "note[note]", with: "This is awesome" - end - end - - it { within(".js-main-target-form") { should_not have_css(".js-comment-button[disabled]") } } - - it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: true) } } - end - - describe "with preview" do - before do - within(".js-main-target-form") do - fill_in "note[note]", with: "This is awesome" - find(".js-note-preview-button").trigger("click") - end - end - - it { within(".js-main-target-form") { should have_css(".js-note-preview", text: "This is awesome", visible: true) } } - - it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } } - it { within(".js-main-target-form") { should have_css(".js-note-edit-button", visible: true) } } - end - end - - describe "when posting a note" do - before do - within(".js-main-target-form") do - fill_in "note[note]", with: "This is awsome!" - find(".js-note-preview-button").trigger("click") - click_button "Add Comment" - end - end - - # note added - it { within(".js-main-target-form") { should have_content("This is awsome!") } } - - # reset form - it { within(".js-main-target-form") { should have_no_field("note[note]", with: "This is awesome!") } } - - # return from preview - it { within(".js-main-target-form") { should have_css(".js-note-preview", visible: false) } } - it { within(".js-main-target-form") { should have_css(".js-note-text", visible: true) } } - - - it "should be removable" do - find(".js-note-delete").trigger("click") - - should_not have_css(".note") - end - end -end diff --git a/spec/requests/projects_deploy_keys_spec.rb b/spec/requests/projects_deploy_keys_spec.rb deleted file mode 100644 index 25b1da9ebd8..00000000000 --- a/spec/requests/projects_deploy_keys_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'spec_helper' - -describe "Projects", "DeployKeys" do - let(:project) { create(:project) } - - before do - login_as :user - project.team << [@user, :master] - end - - describe "GET /keys" do - before do - @key = create(:key, project: project) - visit project_deploy_keys_path(project) - end - - subject { page } - - it { should have_content(@key.title) } - - describe "Destroy" do - before { visit project_deploy_key_path(project, @key) } - - it "should remove entry" do - expect { - click_link "Remove" - }.to change { project.deploy_keys.count }.by(-1) - end - end - end - - describe "New key" do - before do - visit project_deploy_keys_path(project) - click_link "New Deploy Key" - end - - it "should open new key popup" do - page.should have_content("New Deploy key") - end - - describe "fill in" do - before do - fill_in "key_title", with: "laptop" - fill_in "key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop" - end - - it { expect { click_button "Save" }.to change {Key.count}.by(1) } - - it "should add new key to table" do - click_button "Save" - - page.should have_content "laptop" - end - end - end - - describe "Show page" do - before do - @key = create(:key, project: project) - visit project_deploy_key_path(project, @key) - end - - it { page.should have_content @key.title } - it { page.should have_content @key.key[0..10] } - end -end diff --git a/spec/requests/security/project_access_spec.rb b/spec/requests/security/project_access_spec.rb deleted file mode 100644 index a35175102ec..00000000000 --- a/spec/requests/security/project_access_spec.rb +++ /dev/null @@ -1,243 +0,0 @@ -require 'spec_helper' - -describe "Application access" do - describe "GET /" do - it { root_path.should be_allowed_for :admin } - it { root_path.should be_allowed_for :user } - it { root_path.should be_denied_for :visitor } - end - - describe "GET /projects/new" do - it { new_project_path.should be_allowed_for :admin } - it { new_project_path.should be_allowed_for :user } - it { new_project_path.should be_denied_for :visitor } - end - - describe "Project" do - let(:project) { create(:project) } - - let(:master) { create(:user) } - let(:guest) { create(:user) } - let(:reporter) { create(:user) } - - before do - # full access - project.team << [master, :master] - - # readonly - project.team << [reporter, :reporter] - end - - describe "GET /project_code" do - subject { project_path(project) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/tree/master" do - subject { project_tree_path(project, project.repository.root_ref) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/commits/master" do - subject { project_commits_path(project, project.repository.root_ref, limit: 1) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/commit/:sha" do - subject { project_commit_path(project, project.repository.commit) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/compare" do - subject { project_compare_index_path(project) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/team" do - subject { project_team_index_path(project) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/wall" do - subject { wall_project_path(project) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/blob" do - before do - commit = project.repository.commit - path = commit.tree.contents.select { |i| i.is_a?(Grit::Blob)}.first.name - @blob_path = project_blob_path(project, File.join(commit.id, path)) - end - - it { @blob_path.should be_allowed_for master } - it { @blob_path.should be_allowed_for reporter } - it { @blob_path.should be_denied_for :admin } - it { @blob_path.should be_denied_for guest } - it { @blob_path.should be_denied_for :user } - it { @blob_path.should be_denied_for :visitor } - end - - describe "GET /project_code/edit" do - subject { edit_project_path(project) } - - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/deploy_keys" do - subject { project_deploy_keys_path(project) } - - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/issues" do - subject { project_issues_path(project) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/snippets" do - subject { project_snippets_path(project) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/merge_requests" do - subject { project_merge_requests_path(project) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/repository" do - subject { project_repository_path(project) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/repository/branches" do - subject { branches_project_repository_path(project) } - - before do - # Speed increase - Project.any_instance.stub(:branches).and_return([]) - end - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/repository/tags" do - subject { tags_project_repository_path(project) } - - before do - # Speed increase - Project.any_instance.stub(:tags).and_return([]) - end - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/hooks" do - subject { project_hooks_path(project) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - - describe "GET /project_code/files" do - subject { files_project_path(project) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_denied_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - end -end diff --git a/spec/requests/snippets_spec.rb b/spec/requests/snippets_spec.rb deleted file mode 100644 index 770e34dc07c..00000000000 --- a/spec/requests/snippets_spec.rb +++ /dev/null @@ -1,99 +0,0 @@ -require 'spec_helper' - -describe "Snippets" do - let(:project) { create(:project) } - - before do - login_as :user - project.team << [@user, :developer] - end - - describe "GET /snippets" do - before do - @snippet = create(:snippet, - author: @user, - project: project) - - visit project_snippets_path(project) - end - - subject { page } - - it { should have_content(@snippet.title[0..10]) } - it { should have_content(@snippet.project.name) } - - describe "Destroy" do - before do - # admin access to remove snippet - @user.users_projects.destroy_all - project.team << [@user, :master] - visit edit_project_snippet_path(project, @snippet) - end - - it "should remove entry" do - expect { - click_link "destroy_snippet_#{@snippet.id}" - }.to change { Snippet.count }.by(-1) - end - end - end - - describe "New snippet" do - before do - visit project_snippets_path(project) - click_link "New Snippet" - end - - it "should open new snippet popup" do - page.current_path.should == new_project_snippet_path(project) - end - - describe "fill in", js: true do - before do - fill_in "snippet_title", with: "login function" - fill_in "snippet_file_name", with: "test.rb" - page.execute_script("editor.insert('def login; end');") - end - - it { expect { click_button "Save" }.to change {Snippet.count}.by(1) } - - it "should add new snippet to table" do - click_button "Save" - page.current_path.should == project_snippet_path(project, Snippet.last) - page.should have_content "login function" - page.should have_content "test.rb" - end - end - end - - describe "Edit snippet" do - before do - @snippet = create(:snippet, - author: @user, - project: project) - visit project_snippet_path(project, @snippet) - click_link "Edit" - end - - it "should open edit page" do - page.current_path.should == edit_project_snippet_path(project, @snippet) - end - - describe "fill in" do - before do - fill_in "snippet_title", with: "login function" - fill_in "snippet_file_name", with: "test.rb" - end - - it { expect { click_button "Save" }.to_not change {Snippet.count} } - - it "should update snippet fields" do - click_button "Save" - - page.current_path.should == project_snippet_path(project, @snippet) - page.should have_content "login function" - page.should have_content "test.rb" - end - end - end -end diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 3e0e4bb3883..7fe18ff47c3 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -56,60 +56,23 @@ describe Admin::UsersController, "routing" do end end -# team_admin_project GET /admin/projects/:id/team(.:format) admin/projects#team {:id=>/[^\/]+/} -# team_update_admin_project PUT /admin/projects/:id/team_update(.:format) admin/projects#team_update {:id=>/[^\/]+/} -# admin_projects GET /admin/projects(.:format) admin/projects#index {:id=>/[^\/]+/} -# POST /admin/projects(.:format) admin/projects#create {:id=>/[^\/]+/} -# new_admin_project GET /admin/projects/new(.:format) admin/projects#new {:id=>/[^\/]+/} -# edit_admin_project GET /admin/projects/:id/edit(.:format) admin/projects#edit {:id=>/[^\/]+/} -# admin_project GET /admin/projects/:id(.:format) admin/projects#show {:id=>/[^\/]+/} -# PUT /admin/projects/:id(.:format) admin/projects#update {:id=>/[^\/]+/} -# DELETE /admin/projects/:id(.:format) admin/projects#destroy {:id=>/[^\/]+/} +# team_admin_project GET /admin/projects/:id/team(.:format) admin/projects#team {id: /[^\/]+/} +# team_update_admin_project PUT /admin/projects/:id/team_update(.:format) admin/projects#team_update {id: /[^\/]+/} +# admin_projects GET /admin/projects(.:format) admin/projects#index {id: /[^\/]+/} +# POST /admin/projects(.:format) admin/projects#create {id: /[^\/]+/} +# new_admin_project GET /admin/projects/new(.:format) admin/projects#new {id: /[^\/]+/} +# edit_admin_project GET /admin/projects/:id/edit(.:format) admin/projects#edit {id: /[^\/]+/} +# admin_project GET /admin/projects/:id(.:format) admin/projects#show {id: /[^\/]+/} +# PUT /admin/projects/:id(.:format) admin/projects#update {id: /[^\/]+/} +# DELETE /admin/projects/:id(.:format) admin/projects#destroy {id: /[^\/]+/} describe Admin::ProjectsController, "routing" do - it "to #team" do - get("/admin/projects/gitlab/team").should route_to('admin/projects#team', id: 'gitlab') - end - - it "to #team_update" do - put("/admin/projects/gitlab/team_update").should route_to('admin/projects#team_update', id: 'gitlab') - end - it "to #index" do get("/admin/projects").should route_to('admin/projects#index') end - it "to #edit" do - get("/admin/projects/gitlab/edit").should route_to('admin/projects#edit', id: 'gitlab') - end - it "to #show" do get("/admin/projects/gitlab").should route_to('admin/projects#show', id: 'gitlab') end - - it "to #update" do - put("/admin/projects/gitlab").should route_to('admin/projects#update', id: 'gitlab') - end - - it "to #destroy" do - delete("/admin/projects/gitlab").should route_to('admin/projects#destroy', id: 'gitlab') - end -end - -# edit_admin_project_member GET /admin/projects/:project_id/members/:id/edit(.:format) admin/projects/members#edit {:id=>/[^\/]+/, :project_id=>/[^\/]+/} -# admin_project_member PUT /admin/projects/:project_id/members/:id(.:format) admin/projects/members#update {:id=>/[^\/]+/, :project_id=>/[^\/]+/} -# DELETE /admin/projects/:project_id/members/:id(.:format) admin/projects/members#destroy {:id=>/[^\/]+/, :project_id=>/[^\/]+/} -describe Admin::Projects::MembersController, "routing" do - it "to #edit" do - get("/admin/projects/test/members/1/edit").should route_to('admin/projects/members#edit', project_id: 'test', id: '1') - end - - it "to #update" do - put("/admin/projects/test/members/1").should route_to('admin/projects/members#update', project_id: 'test', id: '1') - end - - it "to #destroy" do - delete("/admin/projects/test/members/1").should route_to('admin/projects/members#destroy', project_id: 'test', id: '1') - end end # admin_hook_test GET /admin/hooks/:hook_id/test(.:format) admin/hooks#test @@ -142,10 +105,10 @@ describe Admin::LogsController, "routing" do end end -# admin_resque GET /admin/resque(.:format) admin/resque#show -describe Admin::ResqueController, "routing" do +# admin_background_jobs GET /admin/background_jobs(.:format) admin/background_jobs#show +describe Admin::BackgroundJobsController, "routing" do it "to #show" do - get("/admin/resque").should route_to('admin/resque#show') + get("/admin/background_jobs").should route_to('admin/background_jobs#show') end end diff --git a/spec/routing/notifications_routing_spec.rb b/spec/routing/notifications_routing_spec.rb new file mode 100644 index 00000000000..112b825e023 --- /dev/null +++ b/spec/routing/notifications_routing_spec.rb @@ -0,0 +1,13 @@ +require "spec_helper" + +describe Profiles::NotificationsController do + describe "routing" do + it "routes to #show" do + get("/profile/notifications").should route_to("profiles/notifications#show") + end + + it "routes to #update" do + put("/profile/notifications").should route_to("profiles/notifications#update") + end + end +end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 9cf5d91349f..20c04e94c24 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -25,38 +25,38 @@ shared_examples "RESTful project resources" do let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] } it "to #index" do - get("/gitlabhq/#{controller}").should route_to("#{controller}#index", project_id: 'gitlabhq') if actions.include?(:index) + get("/gitlab/gitlabhq/#{controller}").should route_to("projects/#{controller}#index", project_id: 'gitlab/gitlabhq') if actions.include?(:index) end it "to #create" do - post("/gitlabhq/#{controller}").should route_to("#{controller}#create", project_id: 'gitlabhq') if actions.include?(:create) + post("/gitlab/gitlabhq/#{controller}").should route_to("projects/#{controller}#create", project_id: 'gitlab/gitlabhq') if actions.include?(:create) end it "to #new" do - get("/gitlabhq/#{controller}/new").should route_to("#{controller}#new", project_id: 'gitlabhq') if actions.include?(:new) + get("/gitlab/gitlabhq/#{controller}/new").should route_to("projects/#{controller}#new", project_id: 'gitlab/gitlabhq') if actions.include?(:new) end it "to #edit" do - get("/gitlabhq/#{controller}/1/edit").should route_to("#{controller}#edit", project_id: 'gitlabhq', id: '1') if actions.include?(:edit) + get("/gitlab/gitlabhq/#{controller}/1/edit").should route_to("projects/#{controller}#edit", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:edit) end it "to #show" do - get("/gitlabhq/#{controller}/1").should route_to("#{controller}#show", project_id: 'gitlabhq', id: '1') if actions.include?(:show) + get("/gitlab/gitlabhq/#{controller}/1").should route_to("projects/#{controller}#show", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:show) end it "to #update" do - put("/gitlabhq/#{controller}/1").should route_to("#{controller}#update", project_id: 'gitlabhq', id: '1') if actions.include?(:update) + put("/gitlab/gitlabhq/#{controller}/1").should route_to("projects/#{controller}#update", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:update) end it "to #destroy" do - delete("/gitlabhq/#{controller}/1").should route_to("#{controller}#destroy", project_id: 'gitlabhq', id: '1') if actions.include?(:destroy) + delete("/gitlab/gitlabhq/#{controller}/1").should route_to("projects/#{controller}#destroy", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:destroy) end end # projects POST /projects(.:format) projects#create # new_project GET /projects/new(.:format) projects#new +# fork_project POST /:id/fork(.:format) projects#fork # wall_project GET /:id/wall(.:format) projects#wall -# graph_project GET /:id/graph(.:format) projects#graph # files_project GET /:id/files(.:format) projects#files # edit_project GET /:id/edit(.:format) projects#edit # project GET /:id(.:format) projects#show @@ -71,48 +71,48 @@ describe ProjectsController, "routing" do get("/projects/new").should route_to('projects#new') end - it "to #wall" do - get("/gitlabhq/wall").should route_to('projects#wall', id: 'gitlabhq') + it "to #fork" do + post("/gitlab/gitlabhq/fork").should route_to('projects#fork', id: 'gitlab/gitlabhq') end - it "to #graph" do - get("/gitlabhq/graph/master").should route_to('graph#show', project_id: 'gitlabhq', id: 'master') + it "to #wall" do + get("/gitlab/gitlabhq/wall").should route_to('projects/walls#show', project_id: 'gitlab/gitlabhq') end - it "to #files" do - get("/gitlabhq/files").should route_to('projects#files', id: 'gitlabhq') + it "to #edit" do + get("/gitlab/gitlabhq/edit").should route_to('projects#edit', id: 'gitlab/gitlabhq') end - it "to #edit" do - get("/gitlabhq/edit").should route_to('projects#edit', id: 'gitlabhq') + it "to #autocomplete_sources" do + get('/gitlab/gitlabhq/autocomplete_sources').should route_to('projects#autocomplete_sources', id: "gitlab/gitlabhq") end it "to #show" do - get("/gitlabhq").should route_to('projects#show', id: 'gitlabhq') + get("/gitlab/gitlabhq").should route_to('projects#show', id: 'gitlab/gitlabhq') end it "to #update" do - put("/gitlabhq").should route_to('projects#update', id: 'gitlabhq') + put("/gitlab/gitlabhq").should route_to('projects#update', id: 'gitlab/gitlabhq') end it "to #destroy" do - delete("/gitlabhq").should route_to('projects#destroy', id: 'gitlabhq') + delete("/gitlab/gitlabhq").should route_to('projects#destroy', id: 'gitlab/gitlabhq') end end -# pages_project_wikis GET /:project_id/wikis/pages(.:format) wikis#pages -# history_project_wiki GET /:project_id/wikis/:id/history(.:format) wikis#history -# project_wikis POST /:project_id/wikis(.:format) wikis#create -# edit_project_wiki GET /:project_id/wikis/:id/edit(.:format) wikis#edit -# project_wiki GET /:project_id/wikis/:id(.:format) wikis#show -# DELETE /:project_id/wikis/:id(.:format) wikis#destroy -describe WikisController, "routing" do +# pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages +# history_project_wiki GET /:project_id/wikis/:id/history(.:format) projects/wikis#history +# project_wikis POST /:project_id/wikis(.:format) projects/wikis#create +# edit_project_wiki GET /:project_id/wikis/:id/edit(.:format) projects/wikis#edit +# project_wiki GET /:project_id/wikis/:id(.:format) projects/wikis#show +# DELETE /:project_id/wikis/:id(.:format) projects/wikis#destroy +describe Projects::WikisController, "routing" do it "to #pages" do - get("/gitlabhq/wikis/pages").should route_to('wikis#pages', project_id: 'gitlabhq') + get("/gitlab/gitlabhq/wikis/pages").should route_to('projects/wikis#pages', project_id: 'gitlab/gitlabhq') end it "to #history" do - get("/gitlabhq/wikis/1/history").should route_to('wikis#history', project_id: 'gitlabhq', id: '1') + get("/gitlab/gitlabhq/wikis/1/history").should route_to('projects/wikis#history', project_id: 'gitlab/gitlabhq', id: '1') end it_behaves_like "RESTful project resources" do @@ -121,53 +121,33 @@ describe WikisController, "routing" do end end -# branches_project_repository GET /:project_id/repository/branches(.:format) repositories#branches -# tags_project_repository GET /:project_id/repository/tags(.:format) repositories#tags -# archive_project_repository GET /:project_id/repository/archive(.:format) repositories#archive -# project_repository POST /:project_id/repository(.:format) repositories#create -# new_project_repository GET /:project_id/repository/new(.:format) repositories#new -# edit_project_repository GET /:project_id/repository/edit(.:format) repositories#edit -# GET /:project_id/repository(.:format) repositories#show -# PUT /:project_id/repository(.:format) repositories#update -# DELETE /:project_id/repository(.:format) repositories#destroy -describe RepositoriesController, "routing" do - it "to #branches" do - get("/gitlabhq/repository/branches").should route_to('repositories#branches', project_id: 'gitlabhq') - end - - it "to #tags" do - get("/gitlabhq/repository/tags").should route_to('repositories#tags', project_id: 'gitlabhq') - end - +# branches_project_repository GET /:project_id/repository/branches(.:format) projects/repositories#branches +# tags_project_repository GET /:project_id/repository/tags(.:format) projects/repositories#tags +# archive_project_repository GET /:project_id/repository/archive(.:format) projects/repositories#archive +# edit_project_repository GET /:project_id/repository/edit(.:format) projects/repositories#edit +describe Projects::RepositoriesController, "routing" do it "to #archive" do - get("/gitlabhq/repository/archive").should route_to('repositories#archive', project_id: 'gitlabhq') - end - - it "to #create" do - post("/gitlabhq/repository").should route_to('repositories#create', project_id: 'gitlabhq') - end - - it "to #new" do - get("/gitlabhq/repository/new").should route_to('repositories#new', project_id: 'gitlabhq') - end - - it "to #edit" do - get("/gitlabhq/repository/edit").should route_to('repositories#edit', project_id: 'gitlabhq') + get("/gitlab/gitlabhq/repository/archive").should route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq') end it "to #show" do - get("/gitlabhq/repository").should route_to('repositories#show', project_id: 'gitlabhq') + get("/gitlab/gitlabhq/repository").should route_to('projects/repositories#show', project_id: 'gitlab/gitlabhq') end +end - it "to #update" do - put("/gitlabhq/repository").should route_to('repositories#update', project_id: 'gitlabhq') +describe Projects::BranchesController, "routing" do + it "to #branches" do + get("/gitlab/gitlabhq/branches").should route_to('projects/branches#index', project_id: 'gitlab/gitlabhq') end +end - it "to #destroy" do - delete("/gitlabhq/repository").should route_to('repositories#destroy', project_id: 'gitlabhq') +describe Projects::TagsController, "routing" do + it "to #tags" do + get("/gitlab/gitlabhq/tags").should route_to('projects/tags#index', project_id: 'gitlab/gitlabhq') end end + # project_deploy_keys GET /:project_id/deploy_keys(.:format) deploy_keys#index # POST /:project_id/deploy_keys(.:format) deploy_keys#create # new_project_deploy_key GET /:project_id/deploy_keys/new(.:format) deploy_keys#new @@ -175,7 +155,7 @@ end # project_deploy_key GET /:project_id/deploy_keys/:id(.:format) deploy_keys#show # PUT /:project_id/deploy_keys/:id(.:format) deploy_keys#update # DELETE /:project_id/deploy_keys/:id(.:format) deploy_keys#destroy -describe DeployKeysController, "routing" do +describe Projects::DeployKeysController, "routing" do it_behaves_like "RESTful project resources" do let(:controller) { 'deploy_keys' } end @@ -184,7 +164,7 @@ end # project_protected_branches GET /:project_id/protected_branches(.:format) protected_branches#index # POST /:project_id/protected_branches(.:format) protected_branches#create # project_protected_branch DELETE /:project_id/protected_branches/:id(.:format) protected_branches#destroy -describe ProtectedBranchesController, "routing" do +describe Projects::ProtectedBranchesController, "routing" do it_behaves_like "RESTful project resources" do let(:actions) { [:index, :create, :destroy] } let(:controller) { 'protected_branches' } @@ -194,53 +174,58 @@ end # switch_project_refs GET /:project_id/refs/switch(.:format) refs#switch # logs_tree_project_ref GET /:project_id/refs/:id/logs_tree(.:format) refs#logs_tree # logs_file_project_ref GET /:project_id/refs/:id/logs_tree/:path(.:format) refs#logs_tree -describe RefsController, "routing" do +describe Projects::RefsController, "routing" do it "to #switch" do - get("/gitlabhq/refs/switch").should route_to('refs#switch', project_id: 'gitlabhq') + get("/gitlab/gitlabhq/refs/switch").should route_to('projects/refs#switch', project_id: 'gitlab/gitlabhq') end it "to #logs_tree" do - get("/gitlabhq/refs/stable/logs_tree").should route_to('refs#logs_tree', project_id: 'gitlabhq', id: 'stable') - get("/gitlabhq/refs/stable/logs_tree/foo/bar/baz").should route_to('refs#logs_tree', project_id: 'gitlabhq', id: 'stable', path: 'foo/bar/baz') + get("/gitlab/gitlabhq/refs/stable/logs_tree").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable') + get("/gitlab/gitlabhq/refs/feature%2345/logs_tree").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature#45') + get("/gitlab/gitlabhq/refs/feature%2B45/logs_tree").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature+45') + get("/gitlab/gitlabhq/refs/stable/logs_tree/foo/bar/baz").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable', path: 'foo/bar/baz') + get("/gitlab/gitlabhq/refs/feature%2345/logs_tree/foo/bar/baz").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature#45', path: 'foo/bar/baz') + get("/gitlab/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature+45', path: 'foo/bar/baz') + get("/gitlab/gitlabhq/refs/stable/logs_tree/files.scss").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable', path: 'files.scss') end end -# diffs_project_merge_request GET /:project_id/merge_requests/:id/diffs(.:format) merge_requests#diffs -# automerge_project_merge_request GET /:project_id/merge_requests/:id/automerge(.:format) merge_requests#automerge -# automerge_check_project_merge_request GET /:project_id/merge_requests/:id/automerge_check(.:format) merge_requests#automerge_check -# branch_from_project_merge_requests GET /:project_id/merge_requests/branch_from(.:format) merge_requests#branch_from -# branch_to_project_merge_requests GET /:project_id/merge_requests/branch_to(.:format) merge_requests#branch_to -# project_merge_requests GET /:project_id/merge_requests(.:format) merge_requests#index -# POST /:project_id/merge_requests(.:format) merge_requests#create -# new_project_merge_request GET /:project_id/merge_requests/new(.:format) merge_requests#new -# edit_project_merge_request GET /:project_id/merge_requests/:id/edit(.:format) merge_requests#edit -# project_merge_request GET /:project_id/merge_requests/:id(.:format) merge_requests#show -# PUT /:project_id/merge_requests/:id(.:format) merge_requests#update -# DELETE /:project_id/merge_requests/:id(.:format) merge_requests#destroy -describe MergeRequestsController, "routing" do +# diffs_project_merge_request GET /:project_id/merge_requests/:id/diffs(.:format) projects/merge_requests#diffs +# automerge_project_merge_request GET /:project_id/merge_requests/:id/automerge(.:format) projects/merge_requests#automerge +# automerge_check_project_merge_request GET /:project_id/merge_requests/:id/automerge_check(.:format) projects/merge_requests#automerge_check +# branch_from_project_merge_requests GET /:project_id/merge_requests/branch_from(.:format) projects/merge_requests#branch_from +# branch_to_project_merge_requests GET /:project_id/merge_requests/branch_to(.:format) projects/merge_requests#branch_to +# project_merge_requests GET /:project_id/merge_requests(.:format) projects/merge_requests#index +# POST /:project_id/merge_requests(.:format) projects/merge_requests#create +# new_project_merge_request GET /:project_id/merge_requests/new(.:format) projects/merge_requests#new +# edit_project_merge_request GET /:project_id/merge_requests/:id/edit(.:format) projects/merge_requests#edit +# project_merge_request GET /:project_id/merge_requests/:id(.:format) projects/merge_requests#show +# PUT /:project_id/merge_requests/:id(.:format) projects/merge_requests#update +# DELETE /:project_id/merge_requests/:id(.:format) projects/merge_requests#destroy +describe Projects::MergeRequestsController, "routing" do it "to #diffs" do - get("/gitlabhq/merge_requests/1/diffs").should route_to('merge_requests#diffs', project_id: 'gitlabhq', id: '1') + get("/gitlab/gitlabhq/merge_requests/1/diffs").should route_to('projects/merge_requests#diffs', project_id: 'gitlab/gitlabhq', id: '1') end it "to #automerge" do - get("/gitlabhq/merge_requests/1/automerge").should route_to('merge_requests#automerge', project_id: 'gitlabhq', id: '1') + get("/gitlab/gitlabhq/merge_requests/1/automerge").should route_to('projects/merge_requests#automerge', project_id: 'gitlab/gitlabhq', id: '1') end it "to #automerge_check" do - get("/gitlabhq/merge_requests/1/automerge_check").should route_to('merge_requests#automerge_check', project_id: 'gitlabhq', id: '1') + get("/gitlab/gitlabhq/merge_requests/1/automerge_check").should route_to('projects/merge_requests#automerge_check', project_id: 'gitlab/gitlabhq', id: '1') end it "to #branch_from" do - get("/gitlabhq/merge_requests/branch_from").should route_to('merge_requests#branch_from', project_id: 'gitlabhq') + get("/gitlab/gitlabhq/merge_requests/branch_from").should route_to('projects/merge_requests#branch_from', project_id: 'gitlab/gitlabhq') end it "to #branch_to" do - get("/gitlabhq/merge_requests/branch_to").should route_to('merge_requests#branch_to', project_id: 'gitlabhq') + get("/gitlab/gitlabhq/merge_requests/branch_to").should route_to('projects/merge_requests#branch_to', project_id: 'gitlab/gitlabhq') end it "to #show" do - get("/gitlabhq/merge_requests/1.diff").should route_to('merge_requests#show', project_id: 'gitlabhq', id: '1', format: 'diff') - get("/gitlabhq/merge_requests/1.patch").should route_to('merge_requests#show', project_id: 'gitlabhq', id: '1', format: 'patch') + get("/gitlab/gitlabhq/merge_requests/1.diff").should route_to('projects/merge_requests#show', project_id: 'gitlab/gitlabhq', id: '1', format: 'diff') + get("/gitlab/gitlabhq/merge_requests/1.patch").should route_to('projects/merge_requests#show', project_id: 'gitlab/gitlabhq', id: '1', format: 'patch') end it_behaves_like "RESTful project resources" do @@ -259,11 +244,35 @@ end # DELETE /:project_id/snippets/:id(.:format) snippets#destroy describe SnippetsController, "routing" do it "to #raw" do - get("/gitlabhq/snippets/1/raw").should route_to('snippets#raw', project_id: 'gitlabhq', id: '1') + get("/gitlab/gitlabhq/snippets/1/raw").should route_to('projects/snippets#raw', project_id: 'gitlab/gitlabhq', id: '1') end - it_behaves_like "RESTful project resources" do - let(:controller) { 'snippets' } + it "to #index" do + get("/gitlab/gitlabhq/snippets").should route_to("projects/snippets#index", project_id: 'gitlab/gitlabhq') + end + + it "to #create" do + post("/gitlab/gitlabhq/snippets").should route_to("projects/snippets#create", project_id: 'gitlab/gitlabhq') + end + + it "to #new" do + get("/gitlab/gitlabhq/snippets/new").should route_to("projects/snippets#new", project_id: 'gitlab/gitlabhq') + end + + it "to #edit" do + get("/gitlab/gitlabhq/snippets/1/edit").should route_to("projects/snippets#edit", project_id: 'gitlab/gitlabhq', id: '1') + end + + it "to #show" do + get("/gitlab/gitlabhq/snippets/1").should route_to("projects/snippets#show", project_id: 'gitlab/gitlabhq', id: '1') + end + + it "to #update" do + put("/gitlab/gitlabhq/snippets/1").should route_to("projects/snippets#update", project_id: 'gitlab/gitlabhq', id: '1') + end + + it "to #destroy" do + delete("/gitlab/gitlabhq/snippets/1").should route_to("projects/snippets#destroy", project_id: 'gitlab/gitlabhq', id: '1') end end @@ -271,9 +280,9 @@ end # project_hooks GET /:project_id/hooks(.:format) hooks#index # POST /:project_id/hooks(.:format) hooks#create # project_hook DELETE /:project_id/hooks/:id(.:format) hooks#destroy -describe HooksController, "routing" do +describe Projects::HooksController, "routing" do it "to #test" do - get("/gitlabhq/hooks/1/test").should route_to('hooks#test', project_id: 'gitlabhq', id: '1') + get("/gitlab/gitlabhq/hooks/1/test").should route_to('projects/hooks#test', project_id: 'gitlab/gitlabhq', id: '1') end it_behaves_like "RESTful project resources" do @@ -282,13 +291,13 @@ describe HooksController, "routing" do end end -# project_commit GET /:project_id/commit/:id(.:format) commit#show {:id=>/[[:alnum:]]{6,40}/, :project_id=>/[^\/]+/} -describe CommitController, "routing" do +# project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /[[:alnum:]]{6,40}/, project_id: /[^\/]+/} +describe Projects::CommitController, "routing" do it "to #show" do - get("/gitlabhq/commit/4246fb").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fb') - get("/gitlabhq/commit/4246fb.diff").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fb', format: 'diff') - get("/gitlabhq/commit/4246fb.patch").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fb', format: 'patch') - get("/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') + get("/gitlab/gitlabhq/commit/4246fb").should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb') + get("/gitlab/gitlabhq/commit/4246fb.diff").should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb', format: 'diff') + get("/gitlab/gitlabhq/commit/4246fb.patch").should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb', format: 'patch') + get("/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5").should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') end end @@ -296,11 +305,15 @@ end # project_commits GET /:project_id/commits(.:format) commits#index # POST /:project_id/commits(.:format) commits#create # project_commit GET /:project_id/commits/:id(.:format) commits#show -describe CommitsController, "routing" do +describe Projects::CommitsController, "routing" do it_behaves_like "RESTful project resources" do let(:actions) { [:show] } let(:controller) { 'commits' } end + + it "to #show" do + get("/gitlab/gitlabhq/commits/master.atom").should route_to('projects/commits#show', project_id: 'gitlab/gitlabhq', id: "master", format: "atom") + end end # project_team_members GET /:project_id/team_members(.:format) team_members#index @@ -310,8 +323,9 @@ end # project_team_member GET /:project_id/team_members/:id(.:format) team_members#show # PUT /:project_id/team_members/:id(.:format) team_members#update # DELETE /:project_id/team_members/:id(.:format) team_members#destroy -describe TeamMembersController, "routing" do +describe Projects::TeamMembersController, "routing" do it_behaves_like "RESTful project resources" do + let(:actions) { [:new, :create, :update, :destroy] } let(:controller) { 'team_members' } end end @@ -323,7 +337,7 @@ end # project_milestone GET /:project_id/milestones/:id(.:format) milestones#show # PUT /:project_id/milestones/:id(.:format) milestones#update # DELETE /:project_id/milestones/:id(.:format) milestones#destroy -describe MilestonesController, "routing" do +describe Projects::MilestonesController, "routing" do it_behaves_like "RESTful project resources" do let(:controller) { 'milestones' } let(:actions) { [:index, :create, :new, :edit, :show, :update] } @@ -331,9 +345,9 @@ describe MilestonesController, "routing" do end # project_labels GET /:project_id/labels(.:format) labels#index -describe LabelsController, "routing" do +describe Projects::LabelsController, "routing" do it "to #index" do - get("/gitlabhq/labels").should route_to('labels#index', project_id: 'gitlabhq') + get("/gitlab/gitlabhq/labels").should route_to('projects/labels#index', project_id: 'gitlab/gitlabhq') end end @@ -347,17 +361,9 @@ end # project_issue GET /:project_id/issues/:id(.:format) issues#show # PUT /:project_id/issues/:id(.:format) issues#update # DELETE /:project_id/issues/:id(.:format) issues#destroy -describe IssuesController, "routing" do - it "to #sort" do - post("/gitlabhq/issues/sort").should route_to('issues#sort', project_id: 'gitlabhq') - end - +describe Projects::IssuesController, "routing" do it "to #bulk_update" do - post("/gitlabhq/issues/bulk_update").should route_to('issues#bulk_update', project_id: 'gitlabhq') - end - - it "to #search" do - get("/gitlabhq/issues/search").should route_to('issues#search', project_id: 'gitlabhq') + post("/gitlab/gitlabhq/issues/bulk_update").should route_to('projects/issues#bulk_update', project_id: 'gitlab/gitlabhq') end it_behaves_like "RESTful project resources" do @@ -370,9 +376,9 @@ end # 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 NotesController, "routing" do +describe Projects::NotesController, "routing" do it "to #preview" do - post("/gitlabhq/notes/preview").should route_to('notes#preview', project_id: 'gitlabhq') + post("/gitlab/gitlabhq/notes/preview").should route_to('projects/notes#preview', project_id: 'gitlab/gitlabhq') end it_behaves_like "RESTful project resources" do @@ -381,42 +387,58 @@ describe NotesController, "routing" do end end -# project_blame GET /:project_id/blame/:id(.:format) blame#show {:id=>/.+/, :project_id=>/[^\/]+/} -describe BlameController, "routing" do +# project_blame GET /:project_id/blame/:id(.:format) blame#show {id: /.+/, project_id: /[^\/]+/} +describe Projects::BlameController, "routing" do it "to #show" do - get("/gitlabhq/blame/master/app/models/project.rb").should route_to('blame#show', project_id: 'gitlabhq', id: 'master/app/models/project.rb') + get("/gitlab/gitlabhq/blame/master/app/models/project.rb").should route_to('projects/blame#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') + get("/gitlab/gitlabhq/blame/master/files.scss").should route_to('projects/blame#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') end end -# project_blob GET /:project_id/blob/:id(.:format) blob#show {:id=>/.+/, :project_id=>/[^\/]+/} -describe BlobController, "routing" do +# project_blob GET /:project_id/blob/:id(.:format) blob#show {id: /.+/, project_id: /[^\/]+/} +describe Projects::BlobController, "routing" do it "to #show" do - get("/gitlabhq/blob/master/app/models/project.rb").should route_to('blob#show', project_id: 'gitlabhq', id: 'master/app/models/project.rb') - get("/gitlabhq/blob/master/app/models/compare.rb").should route_to('blob#show', project_id: 'gitlabhq', id: 'master/app/models/compare.rb') + 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/files.scss").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') end end -# project_tree GET /:project_id/tree/:id(.:format) tree#show {:id=>/.+/, :project_id=>/[^\/]+/} -describe TreeController, "routing" do +# project_tree GET /:project_id/tree/:id(.:format) tree#show {id: /.+/, project_id: /[^\/]+/} +describe Projects::TreeController, "routing" do it "to #show" do - get("/gitlabhq/tree/master/app/models/project.rb").should route_to('tree#show', project_id: 'gitlabhq', id: 'master/app/models/project.rb') + get("/gitlab/gitlabhq/tree/master/app/models/project.rb").should route_to('projects/tree#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') + get("/gitlab/gitlabhq/tree/master/files.scss").should route_to('projects/tree#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') end end -# project_compare_index GET /:project_id/compare(.:format) compare#index {:id=>/[^\/]+/, :project_id=>/[^\/]+/} -# POST /:project_id/compare(.:format) compare#create {:id=>/[^\/]+/, :project_id=>/[^\/]+/} -# project_compare /:project_id/compare/:from...:to(.:format) compare#show {:from=>/.+/, :to=>/.+/, :id=>/[^\/]+/, :project_id=>/[^\/]+/} -describe CompareController, "routing" do +# project_compare_index GET /:project_id/compare(.:format) compare#index {id: /[^\/]+/, project_id: /[^\/]+/} +# POST /:project_id/compare(.:format) compare#create {id: /[^\/]+/, project_id: /[^\/]+/} +# project_compare /:project_id/compare/:from...:to(.:format) compare#show {from: /.+/, to: /.+/, id: /[^\/]+/, project_id: /[^\/]+/} +describe Projects::CompareController, "routing" do it "to #index" do - get("/gitlabhq/compare").should route_to('compare#index', project_id: 'gitlabhq') + get("/gitlab/gitlabhq/compare").should route_to('projects/compare#index', project_id: 'gitlab/gitlabhq') end it "to #compare" do - post("/gitlabhq/compare").should route_to('compare#create', project_id: 'gitlabhq') + post("/gitlab/gitlabhq/compare").should route_to('projects/compare#create', project_id: 'gitlab/gitlabhq') end it "to #show" do - get("/gitlabhq/compare/master...stable").should route_to('compare#show', project_id: 'gitlabhq', from: 'master', to: 'stable') - get("/gitlabhq/compare/issue/1234...stable").should route_to('compare#show', project_id: 'gitlabhq', from: 'issue/1234', to: 'stable') + get("/gitlab/gitlabhq/compare/master...stable").should route_to('projects/compare#show', project_id: 'gitlab/gitlabhq', from: 'master', to: 'stable') + get("/gitlab/gitlabhq/compare/issue/1234...stable").should route_to('projects/compare#show', project_id: 'gitlab/gitlabhq', from: 'issue/1234', to: 'stable') + end +end + +describe Projects::NetworkController, "routing" do + it "to #show" do + get("/gitlab/gitlabhq/network/master").should route_to('projects/network#show', project_id: 'gitlab/gitlabhq', id: 'master') + get("/gitlab/gitlabhq/network/master.json").should route_to('projects/network#show', project_id: 'gitlab/gitlabhq', id: 'master', format: "json") + end +end + +describe Projects::GraphsController, "routing" do + it "to #show" do + get("/gitlab/gitlabhq/graphs/master").should route_to('projects/graphs#show', project_id: 'gitlab/gitlabhq', id: 'master') end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 5ad8165ecce..946ef7c28cb 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -7,21 +7,60 @@ describe SearchController, "routing" do end end -# gitlab_api /api Gitlab::API -# resque /info/resque Resque::Server +# gitlab_api /api API::API # /:path Grack describe "Mounted Apps", "routing" do it "to API" do - get("/api").should be_routable + get("/api/issues").should be_routable end - it "to Resque" do - pending - get("/info/resque").should be_routable + it "to Grack" do + get("/gitlab/gitlabhq.git").should be_routable end +end - it "to Grack" do - get("/gitlabhq.git").should be_routable +# snippets GET /snippets(.:format) snippets#index +# POST /snippets(.:format) snippets#create +# new_snippet GET /snippets/new(.:format) snippets#new +# edit_snippet GET /snippets/:id/edit(.:format) snippets#edit +# snippet GET /snippets/:id(.:format) snippets#show +# PUT /snippets/:id(.:format) snippets#update +# DELETE /snippets/:id(.:format) snippets#destroy +describe SnippetsController, "routing" do + it "to #user_index" do + get("/s/User").should route_to('snippets#user_index', username: 'User') + end + + it "to #raw" do + get("/snippets/1/raw").should route_to('snippets#raw', id: '1') + end + + it "to #index" do + get("/snippets").should route_to('snippets#index') + end + + it "to #create" do + post("/snippets").should route_to('snippets#create') + end + + it "to #new" do + get("/snippets/new").should route_to('snippets#new') + end + + it "to #edit" do + get("/snippets/1/edit").should route_to('snippets#edit', id: '1') + end + + it "to #show" do + get("/snippets/1").should route_to('snippets#show', id: '1') + end + + it "to #update" do + put("/snippets/1").should route_to('snippets#update', id: '1') + end + + it "to #destroy" do + delete("/snippets/1").should route_to('snippets#destroy', id: '1') end end @@ -116,33 +155,33 @@ end # key GET /keys/:id(.:format) keys#show # PUT /keys/:id(.:format) keys#update # DELETE /keys/:id(.:format) keys#destroy -describe KeysController, "routing" do +describe Profiles::KeysController, "routing" do it "to #index" do - get("/keys").should route_to('keys#index') + get("/profile/keys").should route_to('profiles/keys#index') end it "to #create" do - post("/keys").should route_to('keys#create') + post("/profile/keys").should route_to('profiles/keys#create') end it "to #new" do - get("/keys/new").should route_to('keys#new') + get("/profile/keys/new").should route_to('profiles/keys#new') end it "to #edit" do - get("/keys/1/edit").should route_to('keys#edit', id: '1') + get("/profile/keys/1/edit").should route_to('profiles/keys#edit', id: '1') end it "to #show" do - get("/keys/1").should route_to('keys#show', id: '1') + get("/profile/keys/1").should route_to('profiles/keys#show', id: '1') end it "to #update" do - put("/keys/1").should route_to('keys#update', id: '1') + put("/profile/keys/1").should route_to('profiles/keys#update', id: '1') end it "to #destroy" do - delete("/keys/1").should route_to('keys#destroy', id: '1') + delete("/profile/keys/1").should route_to('profiles/keys#destroy', id: '1') end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb new file mode 100644 index 00000000000..e3c25fa0469 --- /dev/null +++ b/spec/services/git_push_service_spec.rb @@ -0,0 +1,215 @@ +require 'spec_helper' + +describe GitPushService do + let (:user) { create :user } + let (:project) { create :project_with_code } + let (:service) { GitPushService.new } + + before do + @blankrev = '0000000000000000000000000000000000000000' + @oldrev = 'b98a310def241a6fd9c9a9a3e7934c48e498fe81' + @newrev = 'b19a04f53caeebf4fe5ec2327cb83e9253dc91bb' + @ref = 'refs/heads/master' + end + + describe "Git Push Data" do + before do + service.execute(project, user, @oldrev, @newrev, @ref) + @push_data = service.push_data + @commit = project.repository.commit(@newrev) + end + + subject { @push_data } + + it { should include(before: @oldrev) } + it { should include(after: @newrev) } + it { should include(ref: @ref) } + it { should include(user_id: user.id) } + it { should include(user_name: user.name) } + + context "with repository data" do + subject { @push_data[:repository] } + + it { should include(name: project.name) } + it { should include(url: project.url_to_repo) } + it { should include(description: project.description) } + it { should include(homepage: project.web_url) } + end + + context "with commits" do + subject { @push_data[:commits] } + + it { should be_an(Array) } + it { should have(1).element } + + context "the commit" do + subject { @push_data[:commits].first } + + it { should include(id: @commit.id) } + it { should include(message: @commit.safe_message) } + it { should include(timestamp: @commit.date.xmlschema) } + it { should include(url: "#{Gitlab.config.gitlab.url}/#{project.to_param}/commit/#{@commit.id}") } + + context "with a author" do + subject { @push_data[:commits].first[:author] } + + it { should include(name: @commit.author_name) } + it { should include(email: @commit.author_email) } + end + end + end + end + + describe "Push Event" do + before do + service.execute(project, user, @oldrev, @newrev, @ref) + @event = Event.last + end + + it { @event.should_not be_nil } + it { @event.project.should == project } + it { @event.action.should == Event::PUSHED } + it { @event.data.should == service.push_data } + end + + describe "Web Hooks" do + context "with web hooks" do + before do + @project_hook = create(:project_hook) + @project_hook_2 = create(:project_hook) + project.hooks << [@project_hook, @project_hook_2] + + stub_request(:post, @project_hook.url) + stub_request(:post, @project_hook_2.url) + end + + it "executes multiple web hook" do + @project_hook.should_receive(:async_execute).once + @project_hook_2.should_receive(:async_execute).once + + service.execute(project, user, @oldrev, @newrev, @ref) + end + end + + context "execute web hooks" do + before do + @project_hook = create(:project_hook) + project.hooks << [@project_hook] + stub_request(:post, @project_hook.url) + end + + it "when pushing a branch for the first time" do + @project_hook.should_receive(:async_execute) + service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') + end + + it "when pushing tags" do + @project_hook.should_not_receive(:async_execute) + service.execute(project, user, 'newrev', 'newrev', 'refs/tags/v1.0.0') + end + end + end + + describe "cross-reference notes" do + let(:issue) { create :issue, project: project } + let(:commit_author) { create :user } + let(:commit) { project.repository.commit } + + before do + commit.stub({ + safe_message: "this commit \n mentions ##{issue.id}", + references: [issue], + author_name: commit_author.name, + author_email: commit_author.email + }) + project.repository.stub(commits_between: [commit]) + end + + it "creates a note if a pushed commit mentions an issue" do + Note.should_receive(:create_cross_reference_note).with(issue, commit, commit_author, project) + + service.execute(project, user, @oldrev, @newrev, @ref) + end + + it "only creates a cross-reference note if one doesn't already exist" do + Note.create_cross_reference_note(issue, commit, user, project) + + Note.should_not_receive(:create_cross_reference_note).with(issue, commit, commit_author, project) + + service.execute(project, user, @oldrev, @newrev, @ref) + end + + it "defaults to the pushing user if the commit's author is not known" do + commit.stub(author_name: 'unknown name', author_email: 'unknown@email.com') + Note.should_receive(:create_cross_reference_note).with(issue, commit, user, project) + + service.execute(project, user, @oldrev, @newrev, @ref) + end + + it "finds references in the first push to a non-default branch" do + project.repository.stub(:commits_between).with(@blankrev, @newrev).and_return([]) + project.repository.stub(:commits_between).with("master", @newrev).and_return([commit]) + + Note.should_receive(:create_cross_reference_note).with(issue, commit, commit_author, project) + + service.execute(project, user, @blankrev, @newrev, 'refs/heads/other') + end + + it "finds references in the first push to a default branch" do + project.repository.stub(:commits_between).with(@blankrev, @newrev).and_return([]) + project.repository.stub(:commits).with(@newrev).and_return([commit]) + + Note.should_receive(:create_cross_reference_note).with(issue, commit, commit_author, project) + + service.execute(project, user, @blankrev, @newrev, 'refs/heads/master') + end + end + + describe "closing issues from pushed commits" do + let(:issue) { create :issue, project: project } + let(:other_issue) { create :issue, project: project } + let(:commit_author) { create :user } + let(:closing_commit) { project.repository.commit } + + before do + closing_commit.stub({ + issue_closing_regex: /^([Cc]loses|[Ff]ixes) #\d+/, + safe_message: "this is some work.\n\ncloses ##{issue.iid}", + author_name: commit_author.name, + author_email: commit_author.email + }) + + project.repository.stub(commits_between: [closing_commit]) + end + + it "closes issues with commit messages" do + service.execute(project, user, @oldrev, @newrev, @ref) + + Issue.find(issue.id).should be_closed + end + + it "passes the closing commit as a thread-local" do + service.execute(project, user, @oldrev, @newrev, @ref) + + Thread.current[:current_commit].should == closing_commit + end + + it "doesn't create cross-reference notes for a closing reference" do + expect { + service.execute(project, user, @oldrev, @newrev, @ref) + }.not_to change { Note.where(project_id: project.id, system: true).count } + end + + it "doesn't close issues when pushed to non-default branches" do + project.stub(default_branch: 'durf') + + # The push still shouldn't create cross-reference notes. + expect { + service.execute(project, user, @oldrev, @newrev, 'refs/heads/hurf') + }.not_to change { Note.where(project_id: project.id, system: true).count } + + Issue.find(issue.id).should be_opened + end + end +end + diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb new file mode 100644 index 00000000000..a112835d4d0 --- /dev/null +++ b/spec/services/notification_service_spec.rb @@ -0,0 +1,249 @@ +require 'spec_helper' + +describe NotificationService do + let(:notification) { NotificationService.new } + + describe 'Keys' do + describe :new_key do + let(:key) { create(:personal_key) } + + it { notification.new_key(key).should be_true } + + it 'should sent email to key owner' do + Notify.should_receive(:new_ssh_key_email).with(key.id) + notification.new_key(key) + end + end + end + + describe 'Notes' do + context 'issue note' do + let(:issue) { create(:issue, assignee: create(:user)) } + let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@mention referenced') } + + before do + build_team(note.project) + end + + describe :new_note do + it do + should_email(@u_watcher.id) + should_email(note.noteable.author_id) + should_email(note.noteable.assignee_id) + should_email(@u_mentioned.id) + should_not_email(note.author_id) + should_not_email(@u_participating.id) + should_not_email(@u_disabled.id) + notification.new_note(note) + end + + def should_email(user_id) + Notify.should_receive(:note_issue_email).with(user_id, note.id) + end + + def should_not_email(user_id) + Notify.should_not_receive(:note_issue_email).with(user_id, note.id) + end + end + end + + context 'commit note' do + let(:note) { create(:note_on_commit) } + + before do + build_team(note.project) + note.stub(:commit_author => @u_committer) + end + + describe :new_note do + it do + should_email(@u_committer.id, note) + should_email(@u_watcher.id, note) + should_not_email(@u_mentioned.id, note) + should_not_email(note.author_id, note) + should_not_email(@u_participating.id, note) + should_not_email(@u_disabled.id, note) + notification.new_note(note) + end + + it do + note.update_attribute(:note, '@mention referenced') + should_email(@u_committer.id, note) + should_email(@u_watcher.id, note) + should_email(@u_mentioned.id, note) + should_not_email(note.author_id, note) + should_not_email(@u_participating.id, note) + should_not_email(@u_disabled.id, note) + notification.new_note(note) + end + + def should_email(user_id, n) + Notify.should_receive(:note_commit_email).with(user_id, n.id) + end + + def should_not_email(user_id, n) + Notify.should_not_receive(:note_commit_email).with(user_id, n.id) + end + end + end + end + + describe 'Issues' do + let(:issue) { create :issue, assignee: create(:user) } + + before do + build_team(issue.project) + end + + describe :new_issue do + it do + should_email(issue.assignee_id) + should_email(@u_watcher.id) + should_not_email(@u_participating.id) + should_not_email(@u_disabled.id) + notification.new_issue(issue, @u_disabled) + end + + def should_email(user_id) + Notify.should_receive(:new_issue_email).with(user_id, issue.id) + end + + def should_not_email(user_id) + Notify.should_not_receive(:new_issue_email).with(user_id, issue.id) + end + end + + describe :reassigned_issue do + it 'should email new assignee' do + should_email(issue.assignee_id) + should_email(@u_watcher.id) + should_not_email(@u_participating.id) + should_not_email(@u_disabled.id) + + notification.reassigned_issue(issue, @u_disabled) + end + + def should_email(user_id) + Notify.should_receive(:reassigned_issue_email).with(user_id, issue.id, issue.assignee_id) + end + + def should_not_email(user_id) + Notify.should_not_receive(:reassigned_issue_email).with(user_id, issue.id, issue.assignee_id) + end + end + + describe :close_issue do + it 'should sent email to issue assignee and issue author' do + should_email(issue.assignee_id) + should_email(issue.author_id) + should_email(@u_watcher.id) + should_not_email(@u_participating.id) + should_not_email(@u_disabled.id) + + notification.close_issue(issue, @u_disabled) + end + + def should_email(user_id) + Notify.should_receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id) + end + + def should_not_email(user_id) + Notify.should_not_receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id) + end + end + end + + describe 'Merge Requests' do + let(:merge_request) { create :merge_request, assignee: create(:user) } + + before do + build_team(merge_request.target_project) + end + + describe :new_merge_request do + it do + should_email(merge_request.assignee_id) + should_email(@u_watcher.id) + should_not_email(@u_participating.id) + should_not_email(@u_disabled.id) + notification.new_merge_request(merge_request, @u_disabled) + end + + def should_email(user_id) + Notify.should_receive(:new_merge_request_email).with(user_id, merge_request.id) + end + + def should_not_email(user_id) + Notify.should_not_receive(:new_merge_request_email).with(user_id, merge_request.id) + end + end + + describe :reassigned_merge_request do + it do + should_email(merge_request.assignee_id) + should_email(@u_watcher.id) + should_not_email(@u_participating.id) + should_not_email(@u_disabled.id) + notification.reassigned_merge_request(merge_request, merge_request.author) + end + + def should_email(user_id) + Notify.should_receive(:reassigned_merge_request_email).with(user_id, merge_request.id, merge_request.assignee_id) + end + + def should_not_email(user_id) + Notify.should_not_receive(:reassigned_merge_request_email).with(user_id, merge_request.id, merge_request.assignee_id) + end + end + + describe :closed_merge_request do + it do + should_email(merge_request.assignee_id) + should_email(@u_watcher.id) + should_not_email(@u_participating.id) + should_not_email(@u_disabled.id) + notification.close_mr(merge_request, @u_disabled) + end + + def should_email(user_id) + Notify.should_receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) + end + + def should_not_email(user_id) + Notify.should_not_receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) + end + end + + describe :merged_merge_request do + it do + should_email(merge_request.assignee_id) + should_email(@u_watcher.id) + should_not_email(@u_participating.id) + should_not_email(@u_disabled.id) + notification.merge_mr(merge_request) + end + + def should_email(user_id) + Notify.should_receive(:merged_merge_request_email).with(user_id, merge_request.id) + end + + def should_not_email(user_id) + Notify.should_not_receive(:merged_merge_request_email).with(user_id, merge_request.id) + end + end + end + + def build_team(project) + @u_watcher = create(:user, notification_level: Notification::N_WATCH) + @u_participating = create(:user, notification_level: Notification::N_PARTICIPATING) + @u_disabled = create(:user, notification_level: Notification::N_DISABLED) + @u_mentioned = create(:user, username: 'mention', notification_level: Notification::N_PARTICIPATING) + @u_committer = create(:user, username: 'committer') + + project.team << [@u_watcher, :master] + project.team << [@u_participating, :master] + project.team << [@u_disabled, :master] + project.team << [@u_mentioned, :master] + project.team << [@u_committer, :master] + end +end diff --git a/spec/services/project_transfer_service_spec.rb b/spec/services/project_transfer_service_spec.rb new file mode 100644 index 00000000000..109b429967e --- /dev/null +++ b/spec/services/project_transfer_service_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe ProjectTransferService do + before(:each) { enable_observers } + after(:each) {disable_observers} + + context 'namespace -> namespace' do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:project) { create(:project, namespace: user.namespace) } + + before do + @result = service.transfer(project, group) + end + + it { @result.should be_true } + it { project.namespace.should == group } + end + + context 'namespace -> no namespace' do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + + it { lambda{service.transfer(project, nil)}.should raise_error(ActiveRecord::RecordInvalid) } + end + + def service + service = ProjectTransferService.new + service.gitlab_shell.stub(mv_repository: true) + service + end +end + diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb new file mode 100644 index 00000000000..7f1590f559e --- /dev/null +++ b/spec/services/system_hooks_service_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe SystemHooksService do + let (:user) { create :user } + let (:project) { create :project } + let (:users_project) { create :users_project } + + context 'it should build event data' do + it 'should build event data for user' do + SystemHooksService.build_event_data(user, :create).should include(:event_name, :name, :created_at, :email) + end + + it 'should build event data for project' do + SystemHooksService.build_event_data(project, :create).should include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email) + end + + it 'should build event data for users project' do + SystemHooksService.build_event_data(users_project, :create).should include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :project_access) + end + end + + context 'it should build event names' do + it 'should build event names for user' do + SystemHooksService.build_event_name(user, :create).should eq "user_create" + + SystemHooksService.build_event_name(user, :destroy).should eq "user_destroy" + end + + it 'should build event names for project' do + SystemHooksService.build_event_name(project, :create).should eq "project_create" + + SystemHooksService.build_event_name(project, :destroy).should eq "project_destroy" + end + + it 'should build event names for users project' do + SystemHooksService.build_event_name(users_project, :create).should eq "user_add_to_team" + + SystemHooksService.build_event_name(users_project, :destroy).should eq "user_remove_from_team" + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 77497991f99..dd008ed02ad 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,42 +1,63 @@ -require 'simplecov' unless ENV['CI'] - - -# This file is copied to spec/ when you run 'rails generate rspec:install' -ENV["RAILS_ENV"] ||= 'test' -require File.expand_path("../../config/environment", __FILE__) -require 'rspec/rails' -require 'capybara/rails' -require 'capybara/rspec' -require 'webmock/rspec' -require 'email_spec' -require 'sidekiq/testing/inline' - -# Requires supporting ruby files with custom matchers and macros, etc, -# in spec/support/ and its subdirectories. -Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} - -require 'capybara/poltergeist' -Capybara.javascript_driver = :poltergeist - -WebMock.disable_net_connect!(allow_localhost: true) - -RSpec.configure do |config| - config.mock_with :rspec - - config.include LoginHelpers, type: :request - config.include FactoryGirl::Syntax::Methods - config.include Devise::TestHelpers, type: :controller - - # If you're not using ActiveRecord, or you'd prefer not to run each of your - # examples within a transaction, remove the following line or assign false - # instead of true. - config.use_transactional_fixtures = false - - config.before do - # Use tmp dir for FS manipulations - temp_repos_path = Rails.root.join('tmp', 'test-git-base-path') - Gitlab.config.gitlab_shell.stub(repos_path: temp_repos_path) - FileUtils.rm_rf temp_repos_path - FileUtils.mkdir_p temp_repos_path +require 'rubygems' +require 'spork' + +Spork.prefork do + require 'simplecov' unless ENV['CI'] + + if ENV['TRAVIS'] + require 'coveralls' + Coveralls.wear! + end + + # This file is copied to spec/ when you run 'rails generate rspec:install' + ENV["RAILS_ENV"] ||= 'test' + require File.expand_path("../../config/environment", __FILE__) + require 'rspec/rails' + require 'capybara/rails' + require 'capybara/rspec' + require 'webmock/rspec' + require 'email_spec' + require 'sidekiq/testing/inline' + require 'capybara/poltergeist' + + # Loading more in this block will cause your tests to run faster. However, + + # if you change any configuration or code from libraries loaded here, you'll + # need to restart spork for it take effect. + Capybara.javascript_driver = :poltergeist + Capybara.default_wait_time = 10 + + # Requires supporting ruby files with custom matchers and macros, etc, + # in spec/support/ and its subdirectories. + Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} + + WebMock.disable_net_connect!(allow_localhost: true) + + RSpec.configure do |config| + config.mock_with :rspec + + config.include LoginHelpers, type: :feature + config.include LoginHelpers, type: :request + config.include FactoryGirl::Syntax::Methods + config.include Devise::TestHelpers, type: :controller + + config.include TestEnv + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = false + + config.before(:suite) do + TestEnv.init(observers: false, init_repos: true, repos: false) + end + config.before(:each) do + TestEnv.setup_stubs + end end end + +Spork.each_run do + # This code will be run each time you run your specs. + +end diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index c4514bf38be..ec9a326a1ea 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -18,7 +18,7 @@ module ApiHelpers # # Returns the relative path to the requested API resource def api(path, user = nil) - "/api/#{Gitlab::API.version}#{path}" + + "/api/#{API::API.version}#{path}" + # Normalize query string (path.index('?') ? '' : '?') + diff --git a/spec/support/big_commits.rb b/spec/support/big_commits.rb new file mode 100644 index 00000000000..69daa709dd9 --- /dev/null +++ b/spec/support/big_commits.rb @@ -0,0 +1,8 @@ +module BigCommits + HUGE_COMMIT_ID = "7f92534f767fa20357a11c63f973ae3b79cc5b85" + HUGE_COMMIT_MESSAGE = "pybments.rb version up. gitignore improved" + + BIG_COMMIT_ID = "d62200cad430565bd9f80befaf329297120330b5" + BIG_COMMIT_MESSAGE = "clean-up code" +end + diff --git a/spec/support/chosen_helper.rb b/spec/support/chosen_helper.rb new file mode 100644 index 00000000000..42c9342c77a --- /dev/null +++ b/spec/support/chosen_helper.rb @@ -0,0 +1,21 @@ +# Chosen programmatic helper +# It allows you to select value from chosen select +# +# Params +# value - real value of selected item +# opts - options containing css selector +# +# Usage: +# +# chosen(2, from: '#user_ids') +# + +module ChosenHelper + def chosen(value, options={}) + raise "Must pass a hash containing 'from'" if not options.is_a?(Hash) or not options.has_key?(:from) + + selector = options[:from] + + page.execute_script("$('#{selector}').val('#{value}').trigger('chosen:updated');") + end +end diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index f1e072aa15f..8c9c74f14bd 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -9,10 +9,14 @@ RSpec.configure do |config| DatabaseCleaner.strategy = :transaction end - DatabaseCleaner.start + unless example.metadata[:no_db] + DatabaseCleaner.start + end end config.after do - DatabaseCleaner.clean + unless example.metadata[:no_db] + DatabaseCleaner.clean + end end end diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index 4579c971d47..025534a900d 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -3,7 +3,10 @@ module LoginHelpers # # role - User role (e.g., :admin, :user) def login_as(role) - @user = create(role) + ActiveRecord::Base.observers.enable(:user_observer) do + @user = create(role) + end + login_with(@user) end @@ -12,9 +15,10 @@ module LoginHelpers # user - User instance to login with def login_with(user) visit new_user_session_path - fill_in "user_email", with: user.email + fill_in "user_login", with: user.email fill_in "user_password", with: "123456" click_button "Sign in" + Thread.current[:current_user] = user end def logout diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index 29d16ecbac7..15fb47004e9 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -3,7 +3,7 @@ RSpec::Matchers.define :be_valid_commit do actual != nil actual.id == ValidCommit::ID actual.message == ValidCommit::MESSAGE - actual.author.name == ValidCommit::AUTHOR_FULL_NAME + actual.author_name == ValidCommit::AUTHOR_FULL_NAME end end diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb new file mode 100644 index 00000000000..a7f189777c8 --- /dev/null +++ b/spec/support/mentionable_shared_examples.rb @@ -0,0 +1,94 @@ +# Specifications for behavior common to all Mentionable implementations. +# Requires a shared context containing: +# - let(:subject) { "the mentionable implementation" } +# - let(:backref_text) { "the way that +subject+ should refer to itself in backreferences " } +# - let(:set_mentionable_text) { lambda { |txt| "block that assigns txt to the subject's mentionable_text" } } + +def common_mentionable_setup + # Avoid name collisions with let(:project) or let(:author) in the surrounding scope. + let(:mproject) { create :project } + let(:mauthor) { subject.author } + + let(:mentioned_issue) { create :issue, project: mproject } + let(:other_issue) { create :issue, project: mproject } + let(:mentioned_mr) { create :merge_request, target_project: mproject, source_branch: 'different' } + let(:mentioned_commit) { mock('commit', sha: '1234567890abcdef').as_null_object } + + # Override to add known commits to the repository stub. + let(:extra_commits) { [] } + + # A string that mentions each of the +mentioned_.*+ objects above. Mentionables should add a self-reference + # to this string and place it in their +mentionable_text+. + let(:ref_string) do + "mentions ##{mentioned_issue.iid} twice ##{mentioned_issue.iid}, !#{mentioned_mr.iid}, " + + "#{mentioned_commit.sha[0..5]} 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 } + + repo = mock('repository') + repo.stub(:commit) { |sha| commitmap[sha] } + mproject.stub(repository: repo) + + set_mentionable_text.call(ref_string) + end +end + +shared_examples 'a mentionable' do + common_mentionable_setup + + it 'generates a descriptive back-reference' do + subject.gfm_reference.should == backref_text + end + + it "extracts references from its reference property" do + # De-duplicate and omit itself + refs = subject.references(mproject) + + refs.should have(3).items + refs.should include(mentioned_issue) + refs.should include(mentioned_mr) + refs.should include(mentioned_commit) + end + + it 'creates cross-reference notes' do + [mentioned_issue, mentioned_mr, mentioned_commit].each do |referenced| + Note.should_receive(:create_cross_reference_note).with(referenced, subject.local_reference, mauthor, mproject) + end + + subject.create_cross_references!(mproject, mauthor) + end + + it 'detects existing cross-references' do + Note.create_cross_reference_note(mentioned_issue, subject.local_reference, mauthor, mproject) + + subject.has_mentioned?(mentioned_issue).should be_true + subject.has_mentioned?(mentioned_mr).should be_false + end +end + +shared_examples 'an editable mentionable' do + common_mentionable_setup + + it_behaves_like 'a mentionable' + + it 'creates new cross-reference notes when the mentionable text is edited' do + new_text = "this text still mentions ##{mentioned_issue.iid} and #{mentioned_commit.sha[0..5]}, " + + "but now it mentions ##{other_issue.iid}, too." + + [mentioned_issue, mentioned_commit].each do |oldref| + Note.should_not_receive(:create_cross_reference_note).with(oldref, subject.local_reference, + mauthor, mproject) + end + + Note.should_receive(:create_cross_reference_note).with(other_issue, subject.local_reference, mauthor, mproject) + + subject.save + set_mentionable_text.call(new_text) + subject.notice_added_references(mproject, mauthor) + end +end diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb new file mode 100644 index 00000000000..32c1ded2e9d --- /dev/null +++ b/spec/support/select2_helper.rb @@ -0,0 +1,25 @@ +# Select2 ajax programmatic helper +# It allows you to select value from select2 +# +# Params +# value - real value of selected item +# opts - options containing css selector +# +# Usage: +# +# select2(2, from: '#user_ids') +# + +module Select2Helper + def select2(value, options={}) + raise "Must pass a hash containing 'from'" if not options.is_a?(Hash) or not options.has_key?(:from) + + selector = options[:from] + + if options[:multiple] + page.execute_script("$('#{selector}').select2('val', ['#{value}']);") + else + page.execute_script("$('#{selector}').select2('val', '#{value}');") + end + end +end diff --git a/spec/support/stubbed_repository.rb b/spec/support/stubbed_repository.rb deleted file mode 100644 index 434cab6516e..00000000000 --- a/spec/support/stubbed_repository.rb +++ /dev/null @@ -1,59 +0,0 @@ -require "repository" -require "project" -require "shell" - -# Stubs out all Git repository access done by models so that specs can run -# against fake repositories without Grit complaining that they don't exist. -class Project - def repository - if path == "empty" || !path - nil - else - GitLabTestRepo.new(path_with_namespace) - end - end - - def satellite - FakeSatellite.new - end - - class FakeSatellite - def exists? - true - end - - def destroy - true - end - - def create - true - end - end -end - -class GitLabTestRepo < Repository - def repo - @repo ||= Grit::Repo.new(Rails.root.join('tmp', 'repositories', 'gitlabhq')) - end -end - -module Gitlab - class Shell - def add_repository name - true - end - - def remove_repository name - true - end - - def add_key id, key - true - end - - def remove_key id, key - true - end - end -end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb new file mode 100644 index 00000000000..203e661f514 --- /dev/null +++ b/spec/support/test_env.rb @@ -0,0 +1,173 @@ +require 'rspec/mocks' + +module TestEnv + extend self + + # Test environment + # + # all repositories and namespaces stored at + # RAILS_APP/tmp/test-git-base-path + # + # Next shell methods are stubbed and return true + # - mv_repository + # - remove_repository + # - add_key + # - remove_key + # + def init(opts = {}) + RSpec::Mocks::setup(self) + + # Disable observers to improve test speed + # + # You can enable it in whole test case where needed by next string: + # + # before(:each) { enable_observers } + # + disable_observers if opts[:observers] == false + + # Disable mailer for spinach tests + disable_mailer if opts[:mailer] == false + setup_stubs + + + clear_test_repo_dir if opts[:init_repos] == true + setup_test_repos(opts) if opts[:repos] == true + end + + def enable_observers + ActiveRecord::Base.observers.enable(:all) + end + + def disable_observers + ActiveRecord::Base.observers.disable(:all) + end + + def disable_mailer + NotificationService.any_instance.stub(mailer: double.as_null_object) + end + def enable_mailer + NotificationService.any_instance.unstub(:mailer) + end + + def setup_stubs() + # Use tmp dir for FS manipulations + repos_path = testing_path() + GollumWiki.any_instance.stub(:init_repo) do |path| + create_temp_repo(File.join(repos_path, "#{path}.git")) + end + + Gitlab.config.gitlab_shell.stub(repos_path: repos_path) + + Gitlab.config.satellites.stub(path: satellite_path) + + Gitlab::Git::Repository.stub(repos_path: repos_path) + + Gitlab::Shell.any_instance.stub( + add_repository: true, + mv_repository: true, + remove_repository: true, + update_repository_head: true, + add_key: true, + remove_key: true + ) + + Gitlab::Satellite::Satellite.any_instance.stub( + exists?: true, + destroy: true, + create: true, + lock_files_dir: repos_path + ) + + MergeRequest.any_instance.stub( + check_if_can_be_merged: true + ) + Repository.any_instance.stub( + size: 12.45 + ) + end + + def clear_repo_dir(namespace, name) + setup_stubs + # Clean any .wiki.git that may have been created + FileUtils.rm_rf File.join(testing_path(), "#{name}.wiki.git") + end + + # Create a repo and it's satellite + def create_repo(namespace, name) + setup_stubs + repo = repo(namespace, name) + + # Symlink tmp/repositories/gitlabhq to tmp/test-git-base-path/gitlabhq + system("ln -s -f #{seed_repo_path()} #{repo}") + create_satellite(repo, namespace, name) + end + + private + + def testing_path + Rails.root.join('tmp', 'test-git-base-path') + end + + def seed_repo_path + Rails.root.join('tmp', 'repositories', 'gitlabhq') + end + + def seed_satellite_path + Rails.root.join('tmp', 'satellite', 'gitlabhq') + end + + def satellite_path + "#{testing_path()}/satellite" + end + + def repo(namespace, name) + unless (namespace.nil? || namespace.path.nil? || namespace.path.strip.empty?) + repo = File.join(testing_path(), "#{namespace.path}/#{name}.git") + else + repo = File.join(testing_path(), "#{name}.git") + end + end + + def satellite(namespace, name) + unless (namespace.nil? || namespace.path.nil? || namespace.path.strip.empty?) + satellite_repo = File.join(satellite_path, namespace.path, name) + else + satellite_repo = File.join(satellite_path, name) + end + end + + def setup_test_repos(opts ={}) + create_repo(nil, 'gitlabhq') #unless opts[:repo].nil? || !opts[:repo].include?('') + create_repo(nil, 'source_gitlabhq') #unless opts[:repo].nil? || !opts[:repo].include?('source_') + create_repo(nil, 'target_gitlabhq') #unless opts[:repo].nil? || !opts[:repo].include?('target_') + end + + def clear_test_repo_dir + setup_stubs + # Use tmp dir for FS manipulations + repos_path = testing_path() + # Remove tmp/test-git-base-path + FileUtils.rm_rf Gitlab.config.gitlab_shell.repos_path + + # Recreate tmp/test-git-base-path + FileUtils.mkdir_p Gitlab.config.gitlab_shell.repos_path + + # Since much more is happening in satellites + FileUtils.mkdir_p Gitlab.config.satellites.path + end + + # Create a testing satellite, and clone the source repo into it + def create_satellite(source_repo, namespace, satellite_name) + satellite_repo = satellite(namespace, satellite_name) + # Symlink tmp/satellite/gitlabhq to tmp/test-git-base-path/satellite/gitlabhq, create the directory if it doesn't exist already + satellite_dir = File.dirname(satellite_repo) + FileUtils.mkdir_p(satellite_dir) unless File.exists?(satellite_dir) + system("ln -s -f #{seed_satellite_path} #{satellite_repo}") + end + + def create_temp_repo(path) + FileUtils.mkdir_p path + command = "git init --quiet --bare #{path};" + system(command) + end +end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index babbf2916f8..cba243226db 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -22,21 +22,21 @@ describe 'gitlab:app namespace rake task' do context 'gitlab version' do before do - Dir.stub :glob => [] + Dir.stub glob: [] Dir.stub :chdir - File.stub :exists? => true - Kernel.stub :system => true + File.stub exists?: true + Kernel.stub system: true end let(:gitlab_version) { %x{git rev-parse HEAD}.gsub(/\n/,"") } - it 'should fail on mismach' do - YAML.stub :load_file => {:gitlab_version => gitlab_version.reverse} + it 'should fail on mismatch' do + YAML.stub load_file: {gitlab_version: gitlab_version.reverse} expect { run_rake_task }.to raise_error SystemExit end it 'should invoke restoration on mach' do - YAML.stub :load_file => {:gitlab_version => gitlab_version} + YAML.stub load_file: {gitlab_version: gitlab_version} Rake::Task["gitlab:backup:db:restore"].should_receive :invoke Rake::Task["gitlab:backup:repo:restore"].should_receive :invoke expect { run_rake_task }.to_not raise_error SystemExit diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index d38cd59efa7..46e86dbe00a 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -9,7 +9,7 @@ describe PostReceive do end context "web hook" do - let(:project) { create(:project) } + let(:project) { create(:project_with_code) } let(:key) { create(:key, user: project.owner) } let(:key_id) { key.shell_id } @@ -21,7 +21,6 @@ describe PostReceive do it "does not run if the author is not in the project" do Key.stub(find_by_id: nil) - project.should_not_receive(:observe_push) project.should_not_receive(:execute_hooks) PostReceive.new.perform(pwd(project), 'sha-old', 'sha-new', 'refs/heads/master', key_id).should be_false @@ -32,7 +31,6 @@ describe PostReceive do project.should_receive(:execute_hooks) project.should_receive(:execute_services) project.should_receive(:update_merge_requests) - project.should_receive(:observe_push) PostReceive.new.perform(pwd(project), 'sha-old', 'sha-new', 'refs/heads/master', key_id) end diff --git a/vendor/assets/javascripts/ace-src-noconflict/ace.js b/vendor/assets/javascripts/ace-src-noconflict/ace.js index 11b4bbb8da3..8bd2d9a6051 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/ace.js +++ b/vendor/assets/javascripts/ace-src-noconflict/ace.js @@ -4800,7 +4800,7 @@ var SearchHighlight = require("./search_highlight").SearchHighlight; /** * new EditSession(text, mode) * - text (Document | String): If `text` is a `Document`, it associates the `EditSession` with it. Otherwise, a new `Document` is created, with the initial text - * - mode (TextMode): The inital language mode to use for the document + * - mode (TextMode): The initial language mode to use for the document * * Sets up a new `EditSession` and associates it with the given `Document` and `TextMode`. * @@ -10068,7 +10068,7 @@ ace.define('ace/token_iterator', ['require', 'exports', 'module' ], function(req * - initialRow (Number): The row to start the tokenizing at * - initialColumn (Number): The column to start the tokenizing at * - * Creates a new token iterator object. The inital token index is set to the provided row and column coordinates. + * Creates a new token iterator object. The initial token index is set to the provided row and column coordinates. * **/ var TokenIterator = function(session, initialRow, initialColumn) { @@ -11946,7 +11946,7 @@ var VirtualRenderer = function(container, theme) { this.$horizScroll = horizScroll; if (horizScrollChanged) { this.scroller.style.overflowX = horizScroll ? "scroll" : "hidden"; - // when we hide scrollbar scroll event isn't emited + // when we hide scrollbar scroll event isn't emitted // leaving session with wrong scrollLeft value if (!horizScroll) this.session.setScrollLeft(0); @@ -13029,7 +13029,7 @@ var Text = function(parentEl) { var html = []; // Get the tokens per line as there might be some lines in between - // beeing folded. + // being folded. this.$renderLine(html, row, false, row == foldStart ? foldLine : false); // don't use setInnerHtml since we are working with an empty DIV @@ -14529,7 +14529,7 @@ var Editor = require("./editor").Editor; * - dir (Number): The direction of lines to select: -1 for up, 1 for down * - skip (Boolean): If `true`, removes the active selection range * - * Finds the next occurence of text in an active selection and adds it to the selections. + * Finds the next occurrence of text in an active selection and adds it to the selections. **/ this.selectMore = function(dir, skip) { var session = this.session; @@ -15878,4 +15878,4 @@ ace.define("text!ace/theme/textmate.css", [], ".ace-tm .ace_editor {\n" + ace[key] = a[key]; }); })(); -
\ No newline at end of file + diff --git a/vendor/assets/javascripts/ace-src-noconflict/mode-coldfusion.js b/vendor/assets/javascripts/ace-src-noconflict/mode-coldfusion.js index 1b41d001a25..aea4214560e 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/mode-coldfusion.js +++ b/vendor/assets/javascripts/ace-src-noconflict/mode-coldfusion.js @@ -1170,7 +1170,7 @@ var JavaScriptHighlightRules = function() { } ], // regular expressions are only allowed after certain tokens. This - // makes sure we don't mix up regexps with the divison operator + // makes sure we don't mix up regexps with the division operator "regex_allowed": [ DocCommentHighlightRules.getStartRule("doc-start"), { diff --git a/vendor/assets/javascripts/ace-src-noconflict/mode-groovy.js b/vendor/assets/javascripts/ace-src-noconflict/mode-groovy.js index ad7bdf58855..80d153d8fbd 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/mode-groovy.js +++ b/vendor/assets/javascripts/ace-src-noconflict/mode-groovy.js @@ -337,7 +337,7 @@ var JavaScriptHighlightRules = function() { } ], // regular expressions are only allowed after certain tokens. This - // makes sure we don't mix up regexps with the divison operator + // makes sure we don't mix up regexps with the division operator "regex_allowed": [ DocCommentHighlightRules.getStartRule("doc-start"), { diff --git a/vendor/assets/javascripts/ace-src-noconflict/mode-html.js b/vendor/assets/javascripts/ace-src-noconflict/mode-html.js index 0ea36845d75..5ec11f141c0 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/mode-html.js +++ b/vendor/assets/javascripts/ace-src-noconflict/mode-html.js @@ -389,7 +389,7 @@ var JavaScriptHighlightRules = function() { } ], // regular expressions are only allowed after certain tokens. This - // makes sure we don't mix up regexps with the divison operator + // makes sure we don't mix up regexps with the division operator "regex_allowed": [ DocCommentHighlightRules.getStartRule("doc-start"), { diff --git a/vendor/assets/javascripts/ace-src-noconflict/mode-jade.js b/vendor/assets/javascripts/ace-src-noconflict/mode-jade.js index c01a359835a..32aafd17c9e 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/mode-jade.js +++ b/vendor/assets/javascripts/ace-src-noconflict/mode-jade.js @@ -659,7 +659,7 @@ var JavaScriptHighlightRules = function() { } ], // regular expressions are only allowed after certain tokens. This - // makes sure we don't mix up regexps with the divison operator + // makes sure we don't mix up regexps with the division operator "regex_allowed": [ DocCommentHighlightRules.getStartRule("doc-start"), { diff --git a/vendor/assets/javascripts/ace-src-noconflict/mode-java.js b/vendor/assets/javascripts/ace-src-noconflict/mode-java.js index 23f9e60396b..c05bf0f9e75 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/mode-java.js +++ b/vendor/assets/javascripts/ace-src-noconflict/mode-java.js @@ -338,7 +338,7 @@ var JavaScriptHighlightRules = function() { } ], // regular expressions are only allowed after certain tokens. This - // makes sure we don't mix up regexps with the divison operator + // makes sure we don't mix up regexps with the division operator "regex_allowed": [ DocCommentHighlightRules.getStartRule("doc-start"), { diff --git a/vendor/assets/javascripts/ace-src-noconflict/mode-javascript.js b/vendor/assets/javascripts/ace-src-noconflict/mode-javascript.js index d266bffb19a..90cf57eb5f4 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/mode-javascript.js +++ b/vendor/assets/javascripts/ace-src-noconflict/mode-javascript.js @@ -342,7 +342,7 @@ var JavaScriptHighlightRules = function() { } ], // regular expressions are only allowed after certain tokens. This - // makes sure we don't mix up regexps with the divison operator + // makes sure we don't mix up regexps with the division operator "regex_allowed": [ DocCommentHighlightRules.getStartRule("doc-start"), { diff --git a/vendor/assets/javascripts/ace-src-noconflict/mode-jsp.js b/vendor/assets/javascripts/ace-src-noconflict/mode-jsp.js index c96d25e936e..e4f4bcffed3 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/mode-jsp.js +++ b/vendor/assets/javascripts/ace-src-noconflict/mode-jsp.js @@ -597,7 +597,7 @@ var JavaScriptHighlightRules = function() { } ], // regular expressions are only allowed after certain tokens. This - // makes sure we don't mix up regexps with the divison operator + // makes sure we don't mix up regexps with the division operator "regex_allowed": [ DocCommentHighlightRules.getStartRule("doc-start"), { diff --git a/vendor/assets/javascripts/ace-src-noconflict/mode-liquid.js b/vendor/assets/javascripts/ace-src-noconflict/mode-liquid.js index 2623396ca0a..3886e739ba4 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/mode-liquid.js +++ b/vendor/assets/javascripts/ace-src-noconflict/mode-liquid.js @@ -641,7 +641,7 @@ var JavaScriptHighlightRules = function() { } ], // regular expressions are only allowed after certain tokens. This - // makes sure we don't mix up regexps with the divison operator + // makes sure we don't mix up regexps with the division operator "regex_allowed": [ DocCommentHighlightRules.getStartRule("doc-start"), { diff --git a/vendor/assets/javascripts/ace-src-noconflict/mode-luahtml.js b/vendor/assets/javascripts/ace-src-noconflict/mode-luahtml.js index 89bc2ec783f..d8a1b653f3d 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/mode-luahtml.js +++ b/vendor/assets/javascripts/ace-src-noconflict/mode-luahtml.js @@ -466,7 +466,7 @@ var JavaScriptHighlightRules = function() { } ], // regular expressions are only allowed after certain tokens. This - // makes sure we don't mix up regexps with the divison operator + // makes sure we don't mix up regexps with the division operator "regex_allowed": [ DocCommentHighlightRules.getStartRule("doc-start"), { @@ -2412,4 +2412,4 @@ oop.inherits(LuaHtmlHighlightRules, HtmlHighlightRules); exports.LuaHtmlHighlightRules = LuaHtmlHighlightRules; -});
\ No newline at end of file +}); diff --git a/vendor/assets/javascripts/ace-src-noconflict/mode-luapage.js b/vendor/assets/javascripts/ace-src-noconflict/mode-luapage.js index 8f03866ddaf..9be136de978 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/mode-luapage.js +++ b/vendor/assets/javascripts/ace-src-noconflict/mode-luapage.js @@ -382,7 +382,7 @@ var JavaScriptHighlightRules = function() { } ], // regular expressions are only allowed after certain tokens. This - // makes sure we don't mix up regexps with the divison operator + // makes sure we don't mix up regexps with the division operator "regex_allowed": [ DocCommentHighlightRules.getStartRule("doc-start"), { @@ -2477,4 +2477,4 @@ oop.inherits(LuaPageHighlightRules, HtmlHighlightRules); exports.LuaPageHighlightRules = LuaPageHighlightRules; -});
\ No newline at end of file +}); diff --git a/vendor/assets/javascripts/ace-src-noconflict/mode-markdown.js b/vendor/assets/javascripts/ace-src-noconflict/mode-markdown.js index 179376aa403..e1c269eefc2 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/mode-markdown.js +++ b/vendor/assets/javascripts/ace-src-noconflict/mode-markdown.js @@ -384,7 +384,7 @@ var JavaScriptHighlightRules = function() { } ], // regular expressions are only allowed after certain tokens. This - // makes sure we don't mix up regexps with the divison operator + // makes sure we don't mix up regexps with the division operator "regex_allowed": [ DocCommentHighlightRules.getStartRule("doc-start"), { diff --git a/vendor/assets/javascripts/ace-src-noconflict/mode-php.js b/vendor/assets/javascripts/ace-src-noconflict/mode-php.js index d3deefc6618..40710ceef20 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/mode-php.js +++ b/vendor/assets/javascripts/ace-src-noconflict/mode-php.js @@ -1685,7 +1685,7 @@ var JavaScriptHighlightRules = function() { } ], // regular expressions are only allowed after certain tokens. This - // makes sure we don't mix up regexps with the divison operator + // makes sure we don't mix up regexps with the division operator "regex_allowed": [ DocCommentHighlightRules.getStartRule("doc-start"), { diff --git a/vendor/assets/javascripts/ace-src-noconflict/mode-scala.js b/vendor/assets/javascripts/ace-src-noconflict/mode-scala.js index ee8ebb8fc0c..87b8e12b8e0 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/mode-scala.js +++ b/vendor/assets/javascripts/ace-src-noconflict/mode-scala.js @@ -338,7 +338,7 @@ var JavaScriptHighlightRules = function() { } ], // regular expressions are only allowed after certain tokens. This - // makes sure we don't mix up regexps with the divison operator + // makes sure we don't mix up regexps with the division operator "regex_allowed": [ DocCommentHighlightRules.getStartRule("doc-start"), { diff --git a/vendor/assets/javascripts/ace-src-noconflict/mode-svg.js b/vendor/assets/javascripts/ace-src-noconflict/mode-svg.js index 29c2b17bdd3..96b965ba112 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/mode-svg.js +++ b/vendor/assets/javascripts/ace-src-noconflict/mode-svg.js @@ -1177,7 +1177,7 @@ var JavaScriptHighlightRules = function() { } ], // regular expressions are only allowed after certain tokens. This - // makes sure we don't mix up regexps with the divison operator + // makes sure we don't mix up regexps with the division operator "regex_allowed": [ DocCommentHighlightRules.getStartRule("doc-start"), { diff --git a/vendor/assets/javascripts/ace-src-noconflict/worker-javascript.js b/vendor/assets/javascripts/ace-src-noconflict/worker-javascript.js index 923bac6f661..671d7bec547 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/worker-javascript.js +++ b/vendor/assets/javascripts/ace-src-noconflict/worker-javascript.js @@ -2912,7 +2912,7 @@ var JSHINT = (function () { immed : true, // if immediate invocations must be wrapped in parens iterator : true, // if the `__iterator__` property should be allowed jquery : true, // if jQuery globals should be predefined - lastsemic : true, // if semicolons may be ommitted for the trailing + lastsemic : true, // if semicolons may be omitted for the trailing // statements inside of a one-line blocks. latedef : true, // if the use before definition should not be tolerated laxbreak : true, // if line breaks should not be checked @@ -3674,7 +3674,7 @@ var JSHINT = (function () { line += 1; // If smarttabs option is used check for spaces followed by tabs only. - // Otherwise check for any occurence of mixed tabs and spaces. + // Otherwise check for any occurrence of mixed tabs and spaces. if (option.smarttabs) at = s.search(/ \t/); else diff --git a/vendor/assets/javascripts/ace-src-noconflict/worker-xquery.js b/vendor/assets/javascripts/ace-src-noconflict/worker-xquery.js index 494066fd73f..1d8c512ee77 100644 --- a/vendor/assets/javascripts/ace-src-noconflict/worker-xquery.js +++ b/vendor/assets/javascripts/ace-src-noconflict/worker-xquery.js @@ -7609,7 +7609,7 @@ org.antlr.runtime.BaseRecognizer.prototype = { * * Until then I'll leave this unimplemented. If there is enough clamor * it would be possible to keep track of the invocation stack using an - * auxillary array, but that will definitely be a performance hit. + * auxiliary array, but that will definitely be a performance hit. */ getRuleInvocationStack: function(e, recognizerClassName) { diff --git a/vendor/assets/javascripts/branch-graph.js b/vendor/assets/javascripts/branch-graph.js deleted file mode 100644 index fb22953acd2..00000000000 --- a/vendor/assets/javascripts/branch-graph.js +++ /dev/null @@ -1,385 +0,0 @@ -!function(){ - - var BranchGraph = function(element, options){ - this.element = element; - this.options = options; - - this.preparedCommits = {}; - this.mtime = 0; - this.mspace = 0; - this.parents = {}; - this.colors = ["#000"]; - - this.load(); - }; - - BranchGraph.prototype.load = function(){ - $.ajax({ - url: this.options.url, - method: 'get', - dataType: 'json', - success: $.proxy(function(data){ - $('.loading', this.element).hide(); - this.prepareData(data.days, data.commits); - this.buildGraph(); - }, this) - }); - }; - - BranchGraph.prototype.prepareData = function(days, commits){ - this.days = days; - this.dayCount = days.length; - this.commits = commits; - this.commitCount = commits.length; - - this.collectParents(); - - this.mtime += 4; - this.mspace += 10; - for (var i = 0; i < this.commitCount; i++) { - if (this.commits[i].id in this.parents) { - this.commits[i].isParent = true; - } - this.preparedCommits[this.commits[i].id] = this.commits[i]; - } - this.collectColors(); - }; - - BranchGraph.prototype.collectParents = function(){ - for (var i = 0; i < this.commitCount; i++) { - for (var j = 0, jj = this.commits[i].parents.length; j < jj; j++) { - this.parents[this.commits[i].parents[j][0]] = true; - } - this.mtime = Math.max(this.mtime, this.commits[i].time); - this.mspace = Math.max(this.mspace, this.commits[i].space); - } - }; - - BranchGraph.prototype.collectColors = function(){ - for (var k = 0; k < this.mspace; k++) { - this.colors.push(Raphael.getColor(.8)); - // Skipping a few colors in the spectrum to get more contrast between colors - Raphael.getColor();Raphael.getColor(); - } - }; - - BranchGraph.prototype.buildGraph = function(){ - var graphWidth = $(this.element).width() - , ch = this.mspace * 20 + 100 - , cw = Math.max(graphWidth, this.mtime * 20 + 260) - , r = Raphael(this.element.get(0), cw, ch) - , top = r.set() - , cuday = 0 - , cumonth = "" - , offsetX = 20 - , offsetY = 60 - , barWidth = Math.max(graphWidth, this.dayCount * 20 + 320) - , scrollLeft = cw; - - this.raphael = r; - - r.rect(0, 0, barWidth, 20).attr({fill: "#222"}); - r.rect(0, 20, barWidth, 20).attr({fill: "#444"}); - - for (mm = 0; mm < this.dayCount; mm++) { - if(this.days[mm] != null){ - if(cuday != this.days[mm][0]){ - // Dates - r.text(offsetX + mm * 20, 31, this.days[mm][0]).attr({ - font: "12px Monaco, monospace", - fill: "#DDD" - }); - cuday = this.days[mm][0]; - } - if(cumonth != this.days[mm][1]){ - // Months - r.text(offsetX + mm * 20, 11, this.days[mm][1]).attr({ - font: "12px Monaco, monospace", - fill: "#EEE" - }); - cumonth = this.days[mm][1]; - } - } - } - - for (i = 0; i < this.commitCount; i++) { - var x = offsetX + 20 * this.commits[i].time - , y = offsetY + 10 * this.commits[i].space - , c - , ps; - - // Draw dot - r.circle(x, y, 3).attr({ - fill: this.colors[this.commits[i].space], - stroke: "none" - }); - - // Draw lines - for (var j = 0, jj = this.commits[i].parents.length; j < jj; j++) { - c = this.preparedCommits[this.commits[i].parents[j][0]]; - ps = this.commits[i].parent_spaces[j]; - if (c) { - var cx = offsetX + 20 * c.time - , cy = offsetY + 10 * c.space - , psy = offsetY + 10 * ps; - if (c.space == this.commits[i].space && c.space == ps) { - r.path([ - "M", x, y, - "L", cx, cy - ]).attr({ - stroke: this.colors[c.space], - "stroke-width": 2 - }); - - } else if (c.space < this.commits[i].space) { - r.path([ - "M", x - 5, y, - "l-5-2,0,4,5,-2", - "L", x - 10, y, - "L", x - 15, psy, - "L", cx + 5, psy, - "L", cx, cy]) - .attr({ - stroke: this.colors[this.commits[i].space], - "stroke-width": 2 - }); - } else { - r.path([ - "M", x - 3, y + 6, - "l-4,3,4,2,0,-5", - "L", x - 5, y + 10, - "L", x - 10, psy, - "L", cx + 5, psy, - "L", cx, cy]) - .attr({ - stroke: this.colors[c.space], - "stroke-width": 2 - }); - } - } - } - - if (this.commits[i].refs) { - this.appendLabel(x, y, this.commits[i].refs); - } - - // mark commit and displayed in the center - if (this.commits[i].id == this.options.commit_id) { - r.path([ - 'M', x, y - 5, - 'L', x + 4, y - 15, - 'L', x - 4, y - 15, - 'Z' - ]).attr({ - "fill": "#000", - "fill-opacity": .7, - "stroke": "none" - }); - scrollLeft = x - graphWidth / 2; - } - - this.appendAnchor(top, this.commits[i], x, y); - } - top.toFront(); - this.element.scrollLeft(scrollLeft); - this.bindEvents(); - }; - - BranchGraph.prototype.bindEvents = function(){ - var drag = {} - , element = this.element; - - var dragger = function(event){ - element.scrollLeft(drag.sl - (event.clientX - drag.x)); - element.scrollTop(drag.st - (event.clientY - drag.y)); - }; - - element.on({ - mousedown: function (event) { - drag = { - x: event.clientX, - y: event.clientY, - st: element.scrollTop(), - sl: element.scrollLeft() - }; - $(window).on('mousemove', dragger); - } - }); - $(window).on({ - mouseup: function(){ - //bars.animate({opacity: 0}, 300); - $(window).off('mousemove', dragger); - }, - keydown: function(event){ - if(event.keyCode == 37){ - // left - element.scrollLeft( element.scrollLeft() - 50); - } - if(event.keyCode == 38){ - // top - element.scrollTop( element.scrollTop() - 50); - } - if(event.keyCode == 39){ - // right - element.scrollLeft( element.scrollLeft() + 50); - } - if(event.keyCode == 40){ - // bottom - element.scrollTop( element.scrollTop() + 50); - } - } - }); - }; - - BranchGraph.prototype.appendLabel = function(x, y, refs){ - var r = this.raphael - , shortrefs = refs - , text, textbox, rect; - - if (shortrefs.length > 17){ - // Truncate if longer than 15 chars - shortrefs = shortrefs.substr(0,15) + "…"; - } - - text = r.text(x+5, y+8 + 10, shortrefs).attr({ - font: "10px Monaco, monospace", - fill: "#FFF", - title: refs - }); - - textbox = text.getBBox(); - text.transform([ - 't', textbox.height/-4, textbox.width/2 + 5, - 'r90' - ]); - - // Create rectangle based on the size of the textbox - rect = r.rect(x, y, textbox.width + 15, textbox.height + 5, 4).attr({ - "fill": "#000", - "fill-opacity": .7, - "stroke": "none" - }); - - triangle = r.path([ - 'M', x, y + 5, - 'L', x + 4, y + 15, - 'L', x - 4, y + 15, - 'Z' - ]).attr({ - "fill": "#000", - "fill-opacity": .7, - "stroke": "none" - }); - - // Rotate and reposition rectangle over text - rect.transform([ - 'r', 90, x, y, - 't', 15, -9 - ]); - - // Set text to front - text.toFront(); - }; - - BranchGraph.prototype.appendAnchor = function(top, commit, x, y) { - var r = this.raphael - , options = this.options - , anchor; - anchor = r.circle(x, y, 10).attr({ - fill: "#000", - opacity: 0, - cursor: "pointer" - }) - .click(function(){ - window.open(options.commit_url.replace('%s', commit.id), '_blank'); - }) - .hover(function(){ - this.tooltip = r.commitTooltip(x, y + 5, commit); - top.push(this.tooltip.insertBefore(this)); - }, function(){ - this.tooltip && this.tooltip.remove() && delete this.tooltip; - }); - top.push(anchor); - }; - - this.BranchGraph = BranchGraph; - -}(this); -Raphael.fn.commitTooltip = function(x, y, commit){ - var nameText, idText, messageText - , boxWidth = 300 - , boxHeight = 200; - - nameText = this.text(x, y + 10, commit.author.name); - idText = this.text(x, y + 35, commit.id); - messageText = this.text(x, y + 50, commit.message); - - textSet = this.set(nameText, idText, messageText).attr({ - "text-anchor": "start", - "font": "12px Monaco, monospace" - }); - - nameText.attr({ - "font": "14px Arial", - "font-weight": "bold" - }); - - idText.attr({ - "fill": "#AAA" - }); - - textWrap(messageText, boxWidth - 50); - - var rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({ - "fill": "#FFF", - "stroke": "#000", - "stroke-linecap": "round", - "stroke-width": 2 - }); - var tooltip = this.set(rect, textSet); - - rect.attr({ - "height" : tooltip.getBBox().height + 10, - "width" : tooltip.getBBox().width + 10 - }); - - tooltip.transform([ - 't', 20, 20 - ]); - - return tooltip; -}; - -function textWrap(t, width) { - var content = t.attr("text"); - var abc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - t.attr({ - "text" : abc - }); - var letterWidth = t.getBBox().width / abc.length; - - t.attr({ - "text" : content - }); - - var words = content.split(" "); - var x = 0, s = []; - for ( var i = 0; i < words.length; i++) { - - var l = words[i].length; - if (x + (l * letterWidth) > width) { - s.push("\n"); - x = 0; - } - x += l * letterWidth; - s.push(words[i] + " "); - } - t.attr({ - "text" : s.join("") - }); - var b = t.getBBox() - , h = Math.abs(b.y2) - Math.abs(b.y) + 1; - t.attr({ - "y": b.y + h - }); -} diff --git a/vendor/assets/javascripts/jquery.blockUI.js b/vendor/assets/javascripts/jquery.blockUI.js new file mode 100644 index 00000000000..c8702d79b65 --- /dev/null +++ b/vendor/assets/javascripts/jquery.blockUI.js @@ -0,0 +1,590 @@ +/*! + * jQuery blockUI plugin + * Version 2.60.0-2013.04.05 + * @requires jQuery v1.7 or later + * + * Examples at: http://malsup.com/jquery/block/ + * Copyright (c) 2007-2013 M. Alsup + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * Thanks to Amir-Hossein Sobhi for some excellent contributions! + */ + +;(function() { +/*jshint eqeqeq:false curly:false latedef:false */ +"use strict"; + + function setup($) { + $.fn._fadeIn = $.fn.fadeIn; + + var noOp = $.noop || function() {}; + + // this bit is to ensure we don't call setExpression when we shouldn't (with extra muscle to handle + // retarded userAgent strings on Vista) + var msie = /MSIE/.test(navigator.userAgent); + var ie6 = /MSIE 6.0/.test(navigator.userAgent) && ! /MSIE 8.0/.test(navigator.userAgent); + var mode = document.documentMode || 0; + var setExpr = $.isFunction( document.createElement('div').style.setExpression ); + + // global $ methods for blocking/unblocking the entire page + $.blockUI = function(opts) { install(window, opts); }; + $.unblockUI = function(opts) { remove(window, opts); }; + + // convenience method for quick growl-like notifications (http://www.google.com/search?q=growl) + $.growlUI = function(title, message, timeout, onClose) { + var $m = $('<div class="growlUI"></div>'); + if (title) $m.append('<h1>'+title+'</h1>'); + if (message) $m.append('<h2>'+message+'</h2>'); + if (timeout === undefined) timeout = 3000; + $.blockUI({ + message: $m, fadeIn: 700, fadeOut: 1000, centerY: false, + timeout: timeout, showOverlay: false, + onUnblock: onClose, + css: $.blockUI.defaults.growlCSS + }); + }; + + // plugin method for blocking element content + $.fn.block = function(opts) { + if ( this[0] === window ) { + $.blockUI( opts ); + return this; + } + var fullOpts = $.extend({}, $.blockUI.defaults, opts || {}); + this.each(function() { + var $el = $(this); + if (fullOpts.ignoreIfBlocked && $el.data('blockUI.isBlocked')) + return; + $el.unblock({ fadeOut: 0 }); + }); + + return this.each(function() { + if ($.css(this,'position') == 'static') { + this.style.position = 'relative'; + $(this).data('blockUI.static', true); + } + this.style.zoom = 1; // force 'hasLayout' in ie + install(this, opts); + }); + }; + + // plugin method for unblocking element content + $.fn.unblock = function(opts) { + if ( this[0] === window ) { + $.unblockUI( opts ); + return this; + } + return this.each(function() { + remove(this, opts); + }); + }; + + $.blockUI.version = 2.60; // 2nd generation blocking at no extra cost! + + // override these in your code to change the default behavior and style + $.blockUI.defaults = { + // message displayed when blocking (use null for no message) + message: '<h1>Please wait...</h1>', + + title: null, // title string; only used when theme == true + draggable: true, // only used when theme == true (requires jquery-ui.js to be loaded) + + theme: false, // set to true to use with jQuery UI themes + + // styles for the message when blocking; if you wish to disable + // these and use an external stylesheet then do this in your code: + // $.blockUI.defaults.css = {}; + css: { + padding: 0, + margin: 0, + width: '30%', + top: '40%', + left: '35%', + textAlign: 'center', + color: '#000', + border: '3px solid #aaa', + backgroundColor:'#fff', + cursor: 'wait' + }, + + // minimal style set used when themes are used + themedCSS: { + width: '30%', + top: '40%', + left: '35%' + }, + + // styles for the overlay + overlayCSS: { + backgroundColor: '#000', + opacity: 0.6, + cursor: 'wait' + }, + + // style to replace wait cursor before unblocking to correct issue + // of lingering wait cursor + cursorReset: 'default', + + // styles applied when using $.growlUI + growlCSS: { + width: '350px', + top: '10px', + left: '', + right: '10px', + border: 'none', + padding: '5px', + opacity: 0.6, + cursor: 'default', + color: '#fff', + backgroundColor: '#000', + '-webkit-border-radius':'10px', + '-moz-border-radius': '10px', + 'border-radius': '10px' + }, + + // IE issues: 'about:blank' fails on HTTPS and javascript:false is s-l-o-w + // (hat tip to Jorge H. N. de Vasconcelos) + /*jshint scripturl:true */ + iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank', + + // force usage of iframe in non-IE browsers (handy for blocking applets) + forceIframe: false, + + // z-index for the blocking overlay + baseZ: 1000, + + // set these to true to have the message automatically centered + centerX: true, // <-- only effects element blocking (page block controlled via css above) + centerY: true, + + // allow body element to be stetched in ie6; this makes blocking look better + // on "short" pages. disable if you wish to prevent changes to the body height + allowBodyStretch: true, + + // enable if you want key and mouse events to be disabled for content that is blocked + bindEvents: true, + + // be default blockUI will supress tab navigation from leaving blocking content + // (if bindEvents is true) + constrainTabKey: true, + + // fadeIn time in millis; set to 0 to disable fadeIn on block + fadeIn: 200, + + // fadeOut time in millis; set to 0 to disable fadeOut on unblock + fadeOut: 400, + + // time in millis to wait before auto-unblocking; set to 0 to disable auto-unblock + timeout: 0, + + // disable if you don't want to show the overlay + showOverlay: true, + + // if true, focus will be placed in the first available input field when + // page blocking + focusInput: true, + + // elements that can receive focus + focusableElements: ':input:enabled:visible', + + // suppresses the use of overlay styles on FF/Linux (due to performance issues with opacity) + // no longer needed in 2012 + // applyPlatformOpacityRules: true, + + // callback method invoked when fadeIn has completed and blocking message is visible + onBlock: null, + + // callback method invoked when unblocking has completed; the callback is + // passed the element that has been unblocked (which is the window object for page + // blocks) and the options that were passed to the unblock call: + // onUnblock(element, options) + onUnblock: null, + + // callback method invoked when the overlay area is clicked. + // setting this will turn the cursor to a pointer, otherwise cursor defined in overlayCss will be used. + onOverlayClick: null, + + // don't ask; if you really must know: http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493 + quirksmodeOffsetHack: 4, + + // class name of the message block + blockMsgClass: 'blockMsg', + + // if it is already blocked, then ignore it (don't unblock and reblock) + ignoreIfBlocked: false + }; + + // private data and functions follow... + + var pageBlock = null; + var pageBlockEls = []; + + function install(el, opts) { + var css, themedCSS; + var full = (el == window); + var msg = (opts && opts.message !== undefined ? opts.message : undefined); + opts = $.extend({}, $.blockUI.defaults, opts || {}); + + if (opts.ignoreIfBlocked && $(el).data('blockUI.isBlocked')) + return; + + opts.overlayCSS = $.extend({}, $.blockUI.defaults.overlayCSS, opts.overlayCSS || {}); + css = $.extend({}, $.blockUI.defaults.css, opts.css || {}); + if (opts.onOverlayClick) + opts.overlayCSS.cursor = 'pointer'; + + themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, opts.themedCSS || {}); + msg = msg === undefined ? opts.message : msg; + + // remove the current block (if there is one) + if (full && pageBlock) + remove(window, {fadeOut:0}); + + // if an existing element is being used as the blocking content then we capture + // its current place in the DOM (and current display style) so we can restore + // it when we unblock + if (msg && typeof msg != 'string' && (msg.parentNode || msg.jquery)) { + var node = msg.jquery ? msg[0] : msg; + var data = {}; + $(el).data('blockUI.history', data); + data.el = node; + data.parent = node.parentNode; + data.display = node.style.display; + data.position = node.style.position; + if (data.parent) + data.parent.removeChild(node); + } + + $(el).data('blockUI.onUnblock', opts.onUnblock); + var z = opts.baseZ; + + // blockUI uses 3 layers for blocking, for simplicity they are all used on every platform; + // layer1 is the iframe layer which is used to supress bleed through of underlying content + // layer2 is the overlay layer which has opacity and a wait cursor (by default) + // layer3 is the message content that is displayed while blocking + var lyr1, lyr2, lyr3, s; + if (msie || opts.forceIframe) + lyr1 = $('<iframe class="blockUI" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;position:absolute;width:100%;height:100%;top:0;left:0" src="'+opts.iframeSrc+'"></iframe>'); + else + lyr1 = $('<div class="blockUI" style="display:none"></div>'); + + if (opts.theme) + lyr2 = $('<div class="blockUI blockOverlay ui-widget-overlay" style="z-index:'+ (z++) +';display:none"></div>'); + else + lyr2 = $('<div class="blockUI blockOverlay" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;width:100%;height:100%;top:0;left:0"></div>'); + + if (opts.theme && full) { + s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:fixed">'; + if ( opts.title ) { + s += '<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || ' ')+'</div>'; + } + s += '<div class="ui-widget-content ui-dialog-content"></div>'; + s += '</div>'; + } + else if (opts.theme) { + s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:absolute">'; + if ( opts.title ) { + s += '<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || ' ')+'</div>'; + } + s += '<div class="ui-widget-content ui-dialog-content"></div>'; + s += '</div>'; + } + else if (full) { + s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage" style="z-index:'+(z+10)+';display:none;position:fixed"></div>'; + } + else { + s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement" style="z-index:'+(z+10)+';display:none;position:absolute"></div>'; + } + lyr3 = $(s); + + // if we have a message, style it + if (msg) { + if (opts.theme) { + lyr3.css(themedCSS); + lyr3.addClass('ui-widget-content'); + } + else + lyr3.css(css); + } + + // style the overlay + if (!opts.theme /*&& (!opts.applyPlatformOpacityRules)*/) + lyr2.css(opts.overlayCSS); + lyr2.css('position', full ? 'fixed' : 'absolute'); + + // make iframe layer transparent in IE + if (msie || opts.forceIframe) + lyr1.css('opacity',0.0); + + //$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el); + var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') : $(el); + $.each(layers, function() { + this.appendTo($par); + }); + + if (opts.theme && opts.draggable && $.fn.draggable) { + lyr3.draggable({ + handle: '.ui-dialog-titlebar', + cancel: 'li' + }); + } + + // ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling) + var expr = setExpr && (!$.support.boxModel || $('object,embed', full ? null : el).length > 0); + if (ie6 || expr) { + // give body 100% height + if (full && opts.allowBodyStretch && $.support.boxModel) + $('html,body').css('height','100%'); + + // fix ie6 issue when blocked element has a border width + if ((ie6 || !$.support.boxModel) && !full) { + var t = sz(el,'borderTopWidth'), l = sz(el,'borderLeftWidth'); + var fixT = t ? '(0 - '+t+')' : 0; + var fixL = l ? '(0 - '+l+')' : 0; + } + + // simulate fixed position + $.each(layers, function(i,o) { + var s = o[0].style; + s.position = 'absolute'; + if (i < 2) { + if (full) + s.setExpression('height','Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.support.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"'); + else + s.setExpression('height','this.parentNode.offsetHeight + "px"'); + if (full) + s.setExpression('width','jQuery.support.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"'); + else + s.setExpression('width','this.parentNode.offsetWidth + "px"'); + if (fixL) s.setExpression('left', fixL); + if (fixT) s.setExpression('top', fixT); + } + else if (opts.centerY) { + if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"'); + s.marginTop = 0; + } + else if (!opts.centerY && full) { + var top = (opts.css && opts.css.top) ? parseInt(opts.css.top, 10) : 0; + var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + '+top+') + "px"'; + s.setExpression('top',expression); + } + }); + } + + // show the message + if (msg) { + if (opts.theme) + lyr3.find('.ui-widget-content').append(msg); + else + lyr3.append(msg); + if (msg.jquery || msg.nodeType) + $(msg).show(); + } + + if ((msie || opts.forceIframe) && opts.showOverlay) + lyr1.show(); // opacity is zero + if (opts.fadeIn) { + var cb = opts.onBlock ? opts.onBlock : noOp; + var cb1 = (opts.showOverlay && !msg) ? cb : noOp; + var cb2 = msg ? cb : noOp; + if (opts.showOverlay) + lyr2._fadeIn(opts.fadeIn, cb1); + if (msg) + lyr3._fadeIn(opts.fadeIn, cb2); + } + else { + if (opts.showOverlay) + lyr2.show(); + if (msg) + lyr3.show(); + if (opts.onBlock) + opts.onBlock(); + } + + // bind key and mouse events + bind(1, el, opts); + + if (full) { + pageBlock = lyr3[0]; + pageBlockEls = $(opts.focusableElements,pageBlock); + if (opts.focusInput) + setTimeout(focus, 20); + } + else + center(lyr3[0], opts.centerX, opts.centerY); + + if (opts.timeout) { + // auto-unblock + var to = setTimeout(function() { + if (full) + $.unblockUI(opts); + else + $(el).unblock(opts); + }, opts.timeout); + $(el).data('blockUI.timeout', to); + } + } + + // remove the block + function remove(el, opts) { + var count; + var full = (el == window); + var $el = $(el); + var data = $el.data('blockUI.history'); + var to = $el.data('blockUI.timeout'); + if (to) { + clearTimeout(to); + $el.removeData('blockUI.timeout'); + } + opts = $.extend({}, $.blockUI.defaults, opts || {}); + bind(0, el, opts); // unbind events + + if (opts.onUnblock === null) { + opts.onUnblock = $el.data('blockUI.onUnblock'); + $el.removeData('blockUI.onUnblock'); + } + + var els; + if (full) // crazy selector to handle odd field errors in ie6/7 + els = $('body').children().filter('.blockUI').add('body > .blockUI'); + else + els = $el.find('>.blockUI'); + + // fix cursor issue + if ( opts.cursorReset ) { + if ( els.length > 1 ) + els[1].style.cursor = opts.cursorReset; + if ( els.length > 2 ) + els[2].style.cursor = opts.cursorReset; + } + + if (full) + pageBlock = pageBlockEls = null; + + if (opts.fadeOut) { + count = els.length; + els.fadeOut(opts.fadeOut, function() { + if ( --count === 0) + reset(els,data,opts,el); + }); + } + else + reset(els, data, opts, el); + } + + // move blocking element back into the DOM where it started + function reset(els,data,opts,el) { + var $el = $(el); + els.each(function(i,o) { + // remove via DOM calls so we don't lose event handlers + if (this.parentNode) + this.parentNode.removeChild(this); + }); + + if (data && data.el) { + data.el.style.display = data.display; + data.el.style.position = data.position; + if (data.parent) + data.parent.appendChild(data.el); + $el.removeData('blockUI.history'); + } + + if ($el.data('blockUI.static')) { + $el.css('position', 'static'); // #22 + } + + if (typeof opts.onUnblock == 'function') + opts.onUnblock(el,opts); + + // fix issue in Safari 6 where block artifacts remain until reflow + var body = $(document.body), w = body.width(), cssW = body[0].style.width; + body.width(w-1).width(w); + body[0].style.width = cssW; + } + + // bind/unbind the handler + function bind(b, el, opts) { + var full = el == window, $el = $(el); + + // don't bother unbinding if there is nothing to unbind + if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked'))) + return; + + $el.data('blockUI.isBlocked', b); + + // don't bind events when overlay is not in use or if bindEvents is false + if (!full || !opts.bindEvents || (b && !opts.showOverlay)) + return; + + // bind anchors and inputs for mouse and key events + var events = 'mousedown mouseup keydown keypress keyup touchstart touchend touchmove'; + if (b) + $(document).bind(events, opts, handler); + else + $(document).unbind(events, handler); + + // former impl... + // var $e = $('a,:input'); + // b ? $e.bind(events, opts, handler) : $e.unbind(events, handler); + } + + // event handler to suppress keyboard/mouse events when blocking + function handler(e) { + // allow tab navigation (conditionally) + if (e.keyCode && e.keyCode == 9) { + if (pageBlock && e.data.constrainTabKey) { + var els = pageBlockEls; + var fwd = !e.shiftKey && e.target === els[els.length-1]; + var back = e.shiftKey && e.target === els[0]; + if (fwd || back) { + setTimeout(function(){focus(back);},10); + return false; + } + } + } + var opts = e.data; + var target = $(e.target); + if (target.hasClass('blockOverlay') && opts.onOverlayClick) + opts.onOverlayClick(); + + // allow events within the message content + if (target.parents('div.' + opts.blockMsgClass).length > 0) + return true; + + // allow events for content that is not being blocked + return target.parents().children().filter('div.blockUI').length === 0; + } + + function focus(back) { + if (!pageBlockEls) + return; + var e = pageBlockEls[back===true ? pageBlockEls.length-1 : 0]; + if (e) + e.focus(); + } + + function center(el, x, y) { + var p = el.parentNode, s = el.style; + var l = ((p.offsetWidth - el.offsetWidth)/2) - sz(p,'borderLeftWidth'); + var t = ((p.offsetHeight - el.offsetHeight)/2) - sz(p,'borderTopWidth'); + if (x) s.left = l > 0 ? (l+'px') : '0'; + if (y) s.top = t > 0 ? (t+'px') : '0'; + } + + function sz(el, p) { + return parseInt($.css(el,p),10)||0; + } + + } + + + /*global define:true */ + if (typeof define === 'function' && define.amd && define.amd.jQuery) { + define(['jquery'], setup); + } else { + setup(jQuery); + } + +})(); |