diff options
388 files changed, 6579 insertions, 2076 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index 7290d627d24..03b78d68840 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -652,7 +652,7 @@ Style/SymbolProc: Style/Tab: Description: 'No hard tabs.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' - Enabled: false + Enabled: true Style/TrailingBlankLines: Description: 'Checks trailing blank lines and final newline.' diff --git a/.ruby-version b/.ruby-version index cd57a8b95d6..399088bf465 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.1.5 +2.1.6 diff --git a/CHANGELOG b/CHANGELOG index 242d2c773c6..b82392c4e2b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,29 +1,60 @@ Please view this file on the master branch, on stable branches it's out of date. +v 7.11.0 (unreleased) + - Fix clone URL field and X11 Primary selection (Dmitry Medvinsky) + - Ignore invalid lines in .gitmodules + - + - + - + - + - + - + - + v 7.10.0 (unreleased) + - Allow projects to be imported from Google Code. + - Allow users to be invited by email to join a group or project. + - Don't crash when project repository doesn't exist. + - Add config var to block auto-created LDAP users. + - Don't use HTML ellipsis in EmailsOnPush subject truncated commit message. + - Set EmailsOnPush reply-to address to committer email when enabled. + - Fix broken file browsing with a submodule that contains a relative link (Stan Hu) + - Fix persistent XSS vulnerability around profile website URLs. + - Fix project import URL regex to prevent arbitary local repos from being imported. + - Fix directory traversal vulnerability around uploads routes. + - Fix directory traversal vulnerability around help pages. + - Don't leak existence of project via search autocomplete. + - Don't leak existence of group or project via search. + - Fix bug where Wiki pages that included a '/' were no longer accessible (Stan Hu) + - Fix bug where error messages from Dropzone would not be displayed on the issues page (Stan Hu) + - Add a rake task to check repository integrity with `git fsck` + - Add ability to configure Reply-To address in gitlab.yml (Stan Hu) + - Move current user to the top of the list in assignee/author filters (Stan Hu) - Fix broken side-by-side diff view on merge request page (Stan Hu) - Set Application controller default URL options to ensure all url_for calls are consistent (Stan Hu) - Allow HTML tags in Markdown input - Fix code unfold not working on Compare commits page (Stan Hu) + - Fix generating SSH key fingerprints with OpenSSH 6.8. (SaÅ¡o Stanovnik) - Include missing events and fix save functionality in admin service template settings form (Stan Hu) - Fix "Import projects from" button to show the correct instructions (Stan Hu) - Fix dots in Wiki slugs causing errors (Stan Hu) - - Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu) + - Make maximum attachment size configurable via Application Settings (Stan Hu) - Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg) - Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu) - Disable reference creation for comments surrounded by code/preformatted blocks (Stan Hu) - Reduce Rack Attack false positives causing 403 errors during HTTP authentication (Stan Hu) - enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger) - - extend the commit calendar to show the actual commits made on a date (Hannes Rosenögger) - Fix a link in the patch update guide - Add a service to support external wikis (Hannes Rosenögger) + - Omit the "email patches" link and fix plain diff view for merge commits - List new commits for newly pushed branch in activity view. - Add sidetiq gem dependency to match EE - - Add changelog, license and contribution guide links to project sidebar. + - Add changelog, license and contribution guide links to project tab bar. - Improve diff UI - Fix alignment of navbar toggle button (Cody Mize) - Fix checkbox rendering for nested task lists - Identical look of selectboxes in UI + - Upgrade the gitlab_git gem to version 7.1.3 - Move "Import existing repository by URL" option to button. - Improve error message when save profile has error. - Passing the name of pushed ref to CI service (requires GitLab CI 7.9+) @@ -37,18 +68,69 @@ v 7.10.0 (unreleased) - Add ability to unlink connected accounts - Replace commits calendar with faster contribution calendar that includes issues and merge requests - Add inifinite scroll to user page activity - - Don't show commit comment button when user is not signed in. - Don't include system notes in issue/MR comment count. - Don't mark merge request as updated when merge status relative to target branch changes. - Link note avatar to user. - Make Git-over-SSH errors more descriptive. - Fix EmailsOnPush. - -v 7.9.0 - - Send EmailsOnPush email when branch or tag is created or deleted. - - Faster merge request processing for large repository - - Prevent doubling AJAX request with each commit visit via Turbolink - - Prevent unnecessary doubling of js events on import pages and user calendar + - Refactor issue filtering + - AJAX selectbox for issue assignee and author filters + - Fix issue with missing options in issue filtering dropdown if selected one + - Prevent holding Control-Enter or Command-Enter from posting comment multiple times. + - Prevent note form from being cleared when submitting failed. + - Improve file icons rendering on tree (Sullivan Sénéchal) + - API: Add pagination to project events + - Get issue links in notification mail to work again. + - Don't show commit comment button when user is not signed in. + - Fix admin user projects lists. + - Don't leak private group existence by redirecting from namespace controller to group controller. + - Ability to skip some items from backup (database, respositories or uploads) + - Fix "Hello @username." references not working by no longer allowing usernames to end in period. + - Archive repositories in background worker. + - Import GitHub, Bitbucket or GitLab.com projects owned by authenticated user into current namespace. + - Project labels are now available over the API under the "tag_list" field (Cristian Medina) + - Fixed link paths for HTTP and SSH on the admin project view (Jeremy Maziarz) + - Fix and improve help rendering (Sullivan Sénéchal) + - Fix final line in EmailsOnPush email diff being rendered as error. + - Authometic setup GitLab CI project for forks if origin project has GitLab CI enabled + - Prevent duplicate Buildkite service creation. + - Fix git over ssh errors 'fatal: protocol error: bad line length character' + - Automatically setup GitLab CI project for forks if origin project has GitLab CI enabled + - Bust group page project list cache when namespace name or path changes. + - Explicitly set image alt-attribute to prevent graphical glitches if gravatars could not be loaded + - Allow user to choose a public email to show on public profile + - Remove truncation from issue titles on milestone page (Jason Blanchard) + - Fix stuck Merge Request merging events from old installations (Ben Bodenmiller) + - Fix merge request comments on files with multiple commits + - Fix Resource Owner Password Authentication Flow + +v 7.9.4 + - Security: Fix project import URL regex to prevent arbitary local repos from being imported + - Fixed issue where only 25 commits would load in file listings + - Fix LDAP identities after config update + +v 7.9.3 + - Contains no changes + - Add icons to Add dropdown items. + - Allow admin to create public deploy keys that are accessible to any project. + - Warn when gitlab-shell version doesn't match requirement. + - Skip email confirmation when set by admin or via LDAP. + - Only allow users to reference groups, projects, issues, MRs, commits they have access to. + +v 7.9.3 + - Contains no changes + +v 7.9.2 + - Contains no changes + +v 7.9.1 + - Include missing events and fix save functionality in admin service template settings form (Stan Hu) + - Fix "Import projects from" button to show the correct instructions (Stan Hu) + - Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu) + - Fix for LDAP with commas in DN + - Fix missing events and in admin Slack service template settings form (Stan Hu) + - Don't show commit comment button when user is not signed in. + - Downgrade gemnasium-gitlab-service gem v 7.9.0 - Add HipChat integration documentation (Stan Hu) @@ -129,6 +211,10 @@ v 7.9.0 - Fix invalid Atom feeds when using emoji, horizontal rules, or images (Christian Walther) - Backup of repositories with tar instead of git bundle (only now are git-annex files included in the backup) - Add canceled status for CI + - Send EmailsOnPush email when branch or tag is created or deleted. + - Faster merge request processing for large repository + - Prevent doubling AJAX request with each commit visit via Turbolink + - Prevent unnecessary doubling of js events on import pages and user calendar v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42b5ce22e32..3165b7379d3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,7 +71,7 @@ If you can, please submit a merge request with the fix or improvements including 1. Fork the project on GitLab Cloud 1. Create a feature branch -1. Write [tests](README.md#run-the-tests) and code +1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code 1. Add your changes to the [CHANGELOG](CHANGELOG) 1. If you are changing the README, some documentation or other things which have no effect on the tests, add `[ci skip]` somewhere in the commit message 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) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index e70b4523ae7..097a15a2af3 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.6.0 +2.6.2 @@ -39,7 +39,7 @@ gem "browser" # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.1.2' +gem "gitlab_git", '~> 7.1.9' # Ruby/Rack Git Smart-HTTP Server Handler gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack' @@ -94,7 +94,7 @@ gem 'html-pipeline-gitlab', '~> 0.1' gem "github-markup" # Required markup gems by github-markdown -gem 'redcarpet', '~> 3.1.2' +gem 'redcarpet', '~> 3.2.3' gem 'RedCloth' gem 'rdoc', '~>3.6' gem 'org-ruby', '= 0.9.12' @@ -115,7 +115,7 @@ end gem "state_machine" # Issue tags -gem "acts-as-taggable-on" +gem 'acts-as-taggable-on', '~> 3.4' # Background jobs gem 'slim' @@ -208,7 +208,6 @@ group :development do gem "letter_opener" gem 'quiet_assets', '~> 1.0.1' gem 'rack-mini-profiler', require: false - gem "byebug" # Better errors handler gem 'better_errors' @@ -257,6 +256,8 @@ group :development, :test do gem "spring", '~> 1.3.1' gem "spring-commands-rspec", '1.0.4' gem "spring-commands-spinach", '1.0.0' + + gem "byebug" end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 7da4d3c3583..bfe626521e7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,8 +33,8 @@ GEM minitest (~> 5.1) thread_safe (~> 0.1) tzinfo (~> 1.1) - acts-as-taggable-on (2.4.1) - rails (>= 3, < 5) + acts-as-taggable-on (3.5.0) + activerecord (>= 3.2, < 5) addressable (2.3.5) annotate (2.6.0) activerecord (>= 2.3.0) @@ -188,7 +188,7 @@ GEM dotenv (>= 0.7) thor (>= 0.13.6) formatador (0.2.4) - gemnasium-gitlab-service (0.2.4) + gemnasium-gitlab-service (0.2.6) rugged (~> 0.21) gemojione (2.0.0) json @@ -212,7 +212,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.1.0) gemojione (~> 2.0) - gitlab_git (7.1.2) + gitlab_git (7.1.9) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -457,7 +457,7 @@ GEM ffi (>= 0.5.0) rdoc (3.12.2) json (~> 1.4) - redcarpet (3.1.2) + redcarpet (3.2.3) redis (3.1.0) redis-actionpack (4.0.0) actionpack (~> 4) @@ -662,7 +662,7 @@ PLATFORMS DEPENDENCIES RedCloth ace-rails-ap - acts-as-taggable-on + acts-as-taggable-on (~> 3.4) addressable annotate (~> 2.6.0.beta2) asana (~> 0.0.6) @@ -703,7 +703,7 @@ DEPENDENCIES gitlab-grack (~> 2.0.0.rc2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.1.2) + gitlab_git (~> 7.1.9) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.1) gollum-lib (~> 4.0.2) @@ -755,7 +755,7 @@ DEPENDENCIES rb-fsevent rb-inotify rdoc (~> 3.6) - redcarpet (~> 3.1.2) + redcarpet (~> 3.2.3) redis-rails request_store rspec-rails (= 2.99) @@ -1,4 +1,4 @@ -Copyright (c) 2011-2014 GitLab B.V. +Copyright (c) 2011-2015 GitLab B.V. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -1,2 +1,2 @@ web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"} -worker: bundle exec sidekiq -q post_receive -q mailer -q system_hook -q project_web_hook -q gitlab_shell -q common -q default +worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q common -q default diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index 27d04e7cac6..9e5d594c861 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -1,57 +1,7 @@ @Api = groups_path: "/api/:version/groups.json" group_path: "/api/:version/groups/:id.json" - users_path: "/api/:version/users.json" - user_path: "/api/:version/users/:id.json" - notes_path: "/api/:version/projects/:id/notes.json" namespaces_path: "/api/:version/namespaces.json" - project_users_path: "/api/:version/projects/:id/users.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) group: (group_id, callback) -> url = Api.buildUrl(Api.group_path) @@ -80,23 +30,6 @@ ).done (groups) -> callback(groups) - # Return project users list. Filtered by query - # Only active users retrieved - projectUsers: (project_id, query, callback) -> - url = Api.buildUrl(Api.project_users_path) - url = url.replace(':id', project_id) - - $.ajax( - url: url - data: - private_token: gon.api_token - search: query - per_page: 20 - active: true - dataType: "json" - ).done (users) -> - callback(users) - # Return namespaces list. Filtered by query namespaces: (query, callback) -> url = Api.buildUrl(Api.namespaces_path) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index fda142293bc..bd52d3d4d70 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -115,8 +115,8 @@ if location.hash window.addEventListener "hashchange", shiftWindow $ -> - # Click a .one_click_select field, select the contents - $(".one_click_select").on 'click', -> $(@).select() + # Click a .js-select-on-focus field, select the contents + $(".js-select-on-focus").on "focusin", -> $(this).select() $('.remove-row').bind 'ajax:success', -> $(this).closest('li').fadeOut() diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index 37b7ba2cc10..44d75bd694f 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -20,9 +20,9 @@ class @Calendar position: "top" legend: [ 0 - 1 - 4 - 7 + 10 + 20 + 30 ] legendCellPadding: 3 onClick: (date, count) -> diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 3535d8c2cfc..330ebac6f75 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -100,6 +100,8 @@ class Dispatcher when 'users:show' new User() new Activities() + when 'admin:users:show' + new ProjectsList() switch path.first() when 'admin' @@ -127,7 +129,7 @@ class Dispatcher when 'show' new ProjectShow() when 'issues', 'merge_requests' - new ProjectUsersSelect() + new UsersSelect() when 'wikis' new Wikis() shortcut_handler = new ShortcutsNavigation() diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index 06e9f0001ae..fca2a290e2d 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -10,6 +10,7 @@ class @DropzoneInput iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>" btnAlert = "<button type=\"button\"" + alertAttr + ">×</button>" project_uploads_path = window.project_uploads_path or null + max_file_size = gon.max_file_size or 10 form_textarea = $(form).find("textarea.markdown-area") form_textarea.wrap "<div class=\"div-dropzone\"></div>" @@ -76,7 +77,7 @@ class @DropzoneInput dictDefaultMessage: "" clickable: true paramName: "file" - maxFilesize: 10 + maxFilesize: max_file_size uploadMultiple: false headers: "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") @@ -108,9 +109,10 @@ class @DropzoneInput return error: (temp, errorMessage) -> - checkIfMsgExists = $(".error-alert").children().length + errorAlert = $(form).find('.error-alert') + checkIfMsgExists = errorAlert.children().length if checkIfMsgExists is 0 - $(".error-alert").append divAlert + errorAlert.append divAlert $(".div-dropzone-alert").append btnAlert + errorMessage return @@ -221,9 +223,10 @@ class @DropzoneInput "display": "none" showError = (message) -> - checkIfMsgExists = $(".error-alert").children().length + errorAlert = $(form).find('.error-alert') + checkIfMsgExists = errorAlert.children().length if checkIfMsgExists is 0 - $(".error-alert").append divAlert + errorAlert.append divAlert $(".div-dropzone-alert").append btnAlert + message closeAlertMessage = -> @@ -237,4 +240,4 @@ class @DropzoneInput formatLink: (link) -> text = "[#{link.alt}](#{link.url})" text = "!#{text}" if link.is_image - text
\ No newline at end of file + text diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index bf71c144eaf..4e2e6550eb2 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -9,12 +9,8 @@ class @Issue if $("a.btn-close").length $("li.task-list-item input:checkbox").prop("disabled", false) - $(".task-list-item input:checkbox").on( - "click" - null - "issue" - updateTaskState - ) + $('.task-list-item input:checkbox').off('change') + $('.task-list-item input:checkbox').change('issue', updateTaskState) $('.issue-details').waitForImages -> $('.issuable-affix').affix offset: @@ -22,3 +18,7 @@ class @Issue @top = ($('.issuable-affix').offset().top - 70) bottom: -> @bottom = $('.footer').outerHeight(true) + $('.issuable-affix').on 'affix.bs.affix', -> + $(@).width($(@).outerWidth()) + .on 'affixed-top.bs.affix affixed-bottom.bs.affix', -> + $(@).width('') diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 6127d2bb480..ae5d088d593 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -26,6 +26,10 @@ class @MergeRequest @top = ($('.issuable-affix').offset().top - 70) bottom: -> @bottom = $('.footer').outerHeight(true) + $('.issuable-affix').on 'affix.bs.affix', -> + $(@).width($(@).outerWidth()) + .on 'affixed-top.bs.affix affixed-bottom.bs.affix', -> + $(@).width('') # Local jQuery finder $: (selector) -> @@ -54,14 +58,6 @@ class @MergeRequest , 'json' bindEvents: -> - this.$('.merge-request-tabs').on 'click', 'a', (event) => - a = $(event.currentTarget) - - href = a.attr('href') - History.replaceState {path: href}, document.title, href - - event.preventDefault() - this.$('.merge-request-tabs').on 'click', 'li', (event) => this.activateTab($(event.currentTarget).data('action')) @@ -81,12 +77,8 @@ class @MergeRequest this.$('.remove_source_branch_in_progress').hide() this.$('.remove_source_branch_widget.failed').show() - $(".task-list-item input:checkbox").on( - "click" - null - "merge_request" - updateTaskState - ) + $('.task-list-item input:checkbox').off('change') + $('.task-list-item input:checkbox').change('merge_request', updateTaskState) activateTab: (action) -> this.$('.merge-request-tabs li').removeClass 'active' @@ -164,4 +156,3 @@ class @MergeRequest else setTimeout(merge_request.mergeInProgress, 3000) dataType: 'json' - diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index c366c98cf54..6dfe10f0006 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -37,7 +37,8 @@ class @Notes $(document).on "click", ".js-note-attachment-delete", @removeAttachment # reset main target form after submit - $(document).on "ajax:complete", ".js-main-target-form", @resetMainTargetForm + $(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton + $(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm # update the file name when an attachment is selected $(document).on "change", ".js-note-attachment-input", @updateFormAttachment @@ -57,6 +58,7 @@ class @Notes @notes_forms = '.js-main-target-form textarea, .js-discussion-note-form textarea' # Chrome doesn't fire keypress or keyup for Command+Enter, so we need keydown. $(document).on('keydown', @notes_forms, (e) -> + return if e.originalEvent.repeat if e.keyCode == 10 || ((e.metaKey || e.ctrlKey) && e.keyCode == 13) $(@).parents('form').submit() ) @@ -70,6 +72,7 @@ class @Notes $(document).off "click", ".js-note-delete" $(document).off "click", ".js-note-attachment-delete" $(document).off "ajax:complete", ".js-main-target-form" + $(document).off "ajax:success", ".js-main-target-form" $(document).off "click", ".js-discussion-reply-button" $(document).off "click", ".js-add-diff-note-button" $(document).off "visibilitychange" @@ -169,6 +172,11 @@ class @Notes form.find(".js-note-text").data("autosave").reset() + reenableTargetFormSubmitButton: -> + form = $(".js-main-target-form") + + form.find(".js-note-text").trigger "input" + ### Shows the main form and does some setup on it. diff --git a/app/assets/javascripts/project_users_select.js.coffee b/app/assets/javascripts/project_users_select.js.coffee deleted file mode 100644 index 80ab1a61ab9..00000000000 --- a/app/assets/javascripts/project_users_select.js.coffee +++ /dev/null @@ -1,56 +0,0 @@ -class @ProjectUsersSelect - constructor: -> - $('.ajax-project-users-select').each (i, select) => - project_id = $(select).data('project-id') || $('body').data('project-id') - - $(select).select2 - placeholder: $(select).data('placeholder') || "Search for a user" - multiple: $(select).hasClass('multiselect') - minimumInputLength: 0 - query: (query) -> - Api.projectUsers project_id, query.term, (users) -> - data = { results: users } - - if query.term.length == 0 - nullUser = { - name: 'Unassigned', - avatar: null, - username: 'none', - id: -1 - } - - data.results.unshift(nullUser) - - query.callback(data) - - initSelection: (element, callback) -> - id = $(element).val() - if id != "" && id != "-1" - Api.user(id, callback) - - - formatResult: (args...) => - @formatResult(args...) - formatSelection: (args...) => - @formatSelection(args...) - dropdownCssClass: "ajax-project-users-dropdown" - dropdownAutoWidth: true - escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results - m - - formatResult: (user) -> - if user.avatar_url - avatar = user.avatar_url - else - avatar = gon.default_avatar_url - - avatarMarkup = "<div class='user-image'><img class='avatar s24' src='#{avatar}'></div>" - - "<div class='user-result'> - #{avatarMarkup} - <div class='user-name'>#{user.name}</div> - <div class='user-username'>#{user.username}</div> - </div>" - - formatSelection: (user) -> - user.name diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee index d522d9f3b90..4a05bdccdb3 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee @@ -3,12 +3,12 @@ class @ShortcutsDashboardNavigation extends Shortcuts constructor: -> super() - Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-activity')) - Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-projects')) - Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-issues')) - Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-merge_requests')) + Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-activity')) + Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-issues')) + Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-merge_requests')) + Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-projects')) - @findAndollowLink: (selector) -> + @findAndFollowLink: (selector) -> link = $(selector).attr('href') if link window.location = link diff --git a/app/assets/javascripts/shortcuts_navigation.coffee b/app/assets/javascripts/shortcuts_navigation.coffee index e592b700e7c..31895fbf2bc 100644 --- a/app/assets/javascripts/shortcuts_navigation.coffee +++ b/app/assets/javascripts/shortcuts_navigation.coffee @@ -3,18 +3,18 @@ class @ShortcutsNavigation extends Shortcuts constructor: -> super() - Mousetrap.bind('g p', -> ShortcutsNavigation.findAndollowLink('.shortcuts-project')) - Mousetrap.bind('g f', -> ShortcutsNavigation.findAndollowLink('.shortcuts-tree')) - Mousetrap.bind('g c', -> ShortcutsNavigation.findAndollowLink('.shortcuts-commits')) - Mousetrap.bind('g n', -> ShortcutsNavigation.findAndollowLink('.shortcuts-network')) - Mousetrap.bind('g g', -> ShortcutsNavigation.findAndollowLink('.shortcuts-graphs')) - Mousetrap.bind('g i', -> ShortcutsNavigation.findAndollowLink('.shortcuts-issues')) - Mousetrap.bind('g m', -> ShortcutsNavigation.findAndollowLink('.shortcuts-merge_requests')) - Mousetrap.bind('g w', -> ShortcutsNavigation.findAndollowLink('.shortcuts-wiki')) - Mousetrap.bind('g s', -> ShortcutsNavigation.findAndollowLink('.shortcuts-snippets')) + Mousetrap.bind('g p', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-project')) + Mousetrap.bind('g f', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-tree')) + Mousetrap.bind('g c', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-commits')) + Mousetrap.bind('g n', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-network')) + Mousetrap.bind('g g', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs')) + Mousetrap.bind('g i', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-issues')) + Mousetrap.bind('g m', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests')) + Mousetrap.bind('g w', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki')) + Mousetrap.bind('g s', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-snippets')) @enabledHelp.push('.hidden-shortcut.project') - - @findAndollowLink: (selector) -> + + @findAndFollowLink: (selector) -> link = $(selector).attr('href') if link window.location = link diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index 7febcba0e94..2e3f5608257 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -3,12 +3,7 @@ $(document).on("click", '.toggle-nav-collapse', (e) -> collapsed = 'page-sidebar-collapsed' expanded = 'page-sidebar-expanded' - if $('.page-with-sidebar').hasClass(collapsed) - $('.page-with-sidebar').removeClass(collapsed).addClass(expanded) - $('.toggle-nav-collapse i').removeClass('fa-angle-right').addClass('fa-angle-left') - $.cookie("collapsed_nav", "false", { path: '/' }) - else - $('.page-with-sidebar').removeClass(expanded).addClass(collapsed) - $('.toggle-nav-collapse i').removeClass('fa-angle-left').addClass('fa-angle-right') - $.cookie("collapsed_nav", "true", { path: '/' }) + $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") + $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") + $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) ) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 9eee7406511..aeeed9ca3cc 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -1,20 +1,66 @@ class @UsersSelect constructor: -> + @usersPath = "/autocomplete/users.json" + @userPath = "/autocomplete/users/:id.json" + $('.ajax-users-select').each (i, select) => + @projectId = $(select).data('project-id') + @groupId = $(select).data('group-id') + showNullUser = $(select).data('null-user') + showAnyUser = $(select).data('any-user') + showEmailUser = $(select).data('email-user') + firstUser = $(select).data('first-user') + $(select).select2 placeholder: "Search for a user" multiple: $(select).hasClass('multiselect') minimumInputLength: 0 - query: (query) -> - Api.users query.term, (users) -> + query: (query) => + @users query.term, (users) => data = { results: users } + + if query.term.length == 0 + if firstUser + # Move current user to the front of the list + for obj, index in data.results + if obj.username == firstUser + data.results.splice(index, 1) + data.results.unshift(obj) + break + + if showNullUser + nullUser = { + name: 'Unassigned', + avatar: null, + username: 'none', + id: 0 + } + data.results.unshift(nullUser) + + if showAnyUser + anyUser = { + name: 'Any', + avatar: null, + username: 'none', + id: null + } + data.results.unshift(anyUser) + + if showEmailUser && data.results.length == 0 && query.term.match(/^[^@]+@[^@]+$/) + emailUser = { + name: "Invite \"#{query.term}\"", + avatar: null, + username: query.term, + id: query.term + } + data.results.unshift(emailUser) + query.callback(data) - initSelection: (element, callback) -> + initSelection: (element, callback) => id = $(element).val() - if id isnt "" - Api.user(id, callback) - + if id != "" && id != "0" + @user(id, callback) formatResult: (args...) => @formatResult(args...) @@ -38,3 +84,34 @@ class @UsersSelect formatSelection: (user) -> user.name + + user: (user_id, callback) => + url = @buildUrl(@userPath) + url = url.replace(':id', user_id) + + $.ajax( + url: url + dataType: "json" + ).done (user) -> + callback(user) + + # Return users list. Filtered by query + # Only active users retrieved + users: (query, callback) => + url = @buildUrl(@usersPath) + + $.ajax( + url: url + data: + search: query + per_page: 20 + active: true + project_id: @projectId + group_id: @groupId + dataType: "json" + ).done (users) -> + callback(users) + + buildUrl: (url) -> + url = gon.relative_url_root + url if gon.relative_url_root? + return url diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss index ccba65e3fd5..216f25cdcd5 100644 --- a/app/assets/stylesheets/base/mixins.scss +++ b/app/assets/stylesheets/base/mixins.scss @@ -119,6 +119,22 @@ li { line-height: 1.5; } + + a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] { + &:before { + margin-right: 4px; + + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + content: "\f0c6"; + } + + &:hover:before { + text-decoration: none; + } + } } @mixin str-truncated($max_width: 82%) { diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index db393e08819..7c3021989a8 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -246,7 +246,7 @@ li.note { .milestone { &.milestone-closed { - background: #eee; + background: #f9f9f9; } .progress { margin-bottom: 0; diff --git a/app/assets/stylesheets/generic/filters.scss b/app/assets/stylesheets/generic/filters.scss new file mode 100644 index 00000000000..bd93a79722d --- /dev/null +++ b/app/assets/stylesheets/generic/filters.scss @@ -0,0 +1,55 @@ +.filter-item { + margin-right: 15px; +} + +.issues-state-filters { + li.active a { + border-color: #DDD !important; + + &, &:hover, &:active, &.active { + background: #f5f5f5 !important; + border-bottom: 1px solid #f5f5f5 !important; + } + } +} + +.issues-details-filters { + font-size: 13px; + background: #f5f5f5; + margin: -10px 0; + padding: 10px 15px; + margin-top: -15px; + border-left: 1px solid #DDD; + border-right: 1px solid #DDD; + + .btn { + font-size: 13px; + } +} + +@media (min-width: 800px) { + .issues-filters, + .issues_bulk_update { + select, .select2-container { + width: 120px !important; + display: inline-block; + } + } +} + +@media (min-width: 1200px) { + .issues-filters, + .issues_bulk_update { + select, .select2-container { + width: 150px !important; + display: inline-block; + } + } +} + +.issues-filters, +.issues_bulk_update { + .select2-container .select2-choice { + color: #444 !important; + } +} diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss index 1b0e056216f..71a1fc4493f 100644 --- a/app/assets/stylesheets/generic/mobile.scss +++ b/app/assets/stylesheets/generic/mobile.scss @@ -24,13 +24,6 @@ display: none !important; } - .project-home-panel { - .star-fork-buttons { - padding-top: 10px; - padding-right: 15px; - } - } - .project-home-links { display: none; } diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss index 7557f411111..d8e0dc028d1 100644 --- a/app/assets/stylesheets/generic/selects.scss +++ b/app/assets/stylesheets/generic/selects.scss @@ -2,20 +2,25 @@ .select2-container, .select2-container.select2-drop-above { .select2-choice { background: #FFF; - border-color: #CCC; + border-color: #DDD; + height: 34px; padding: 6px 14px; + font-size: 14px; line-height: 1.42857143; - height: auto; + + @include border-radius(4px); .select2-arrow { background: #FFF; - border-left: 1px solid #DDD; + border-left: none; + padding-top: 3px; } } } .select2-container-multi .select2-choices { - @include border-radius(4px) + @include border-radius(4px); + border-color: #CCC; } .select2-container-multi .select2-choices .select2-search-field input { @@ -28,6 +33,7 @@ .select2-drop-active { border: 1px solid #BBB !important; margin-top: 4px; + font-size: 13px; &.select2-drop-above { margin-bottom: 8px; @@ -106,3 +112,7 @@ font-weight: bolder; } } + +.ajax-users-dropdown { + min-width: 225px !important; +} diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 83daea10ed5..d4af7506d5b 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -45,7 +45,8 @@ padding: 12px 0px; border-bottom: 1px solid #eee; .event-title { - @include str-truncated(72%); + max-width: 70%; + @include str-truncated(calc(100% - 174px)); font-weight: 500; font-size: 14px; .author_name { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 6c1dd4f7e9f..cd86a9be8b2 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -41,12 +41,9 @@ } .check-all-holder { - height: 36px; + line-height: 36px; float: left; - margin-right: 12px; - padding: 6px 15px; - border: 1px solid #ccc; - @include border-radius(4px); + margin-right: 15px; } .issues_content { @@ -59,30 +56,6 @@ } } -@media (min-width: 800px) { - .issues_bulk_update { - select, .select2-container { - width: 120px !important; - display: inline-block; - } - } -} - -@media (min-width: 1200px) { - .issues_bulk_update { - select, .select2-container { - width: 160px !important; - display: inline-block; - } - } -} - -.issues_bulk_update { - .select2-container .select2-choice { - color: #444 !important; - } -} - .participants { margin-bottom: 20px; } @@ -120,12 +93,12 @@ form.edit-issue { } &.closed { - background: #F5f5f5; + background: #F9F9F9; border-color: #E5E5E5; } &.merged { - background: #F5f5f5; + background: #F9F9F9; border-color: #E5E5E5; } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index d66093bc2e5..facd7e19314 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -62,20 +62,8 @@ ul.notes { word-wrap: break-word; @include md-typography; - a[href*="/uploads/"] { - &:before { - margin-right: 4px; - - font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - content: "\f0c6"; - } - - &:hover:before { - text-decoration: none; - } + hr { + margin: 10px 0; } } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 6d55a5fa66e..c005470355e 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -15,7 +15,7 @@ } .project-home-panel { - margin-bottom: 15px; + margin-bottom: 20px; position: relative; padding-left: 85px; @@ -38,6 +38,11 @@ font-size: 45px; line-height: 1.6; } + + .avatar, .identicon { + @include border-radius(4px); + box-shadow: 0 1px 2px #ddd; + } } .project-home-dropdown { @@ -49,44 +54,49 @@ @extend .clearfix; margin-bottom: 15px; - .project-home-desc, - .star-fork-buttons { + &.project-home-row-top { + margin-bottom: 15px; + } + + .project-home-desc { font-size: 16px; line-height: 1.3; + margin-right: 215px; } .project-home-desc { float: left; - color: #666; - } - - .star-fork-buttons { - float: right; - min-width: 200px; - font-weight: bold; - - .star-buttons, .fork-buttons { - float: right; - margin-left: 20px; - - a:hover { - text-decoration: none; - } - - .count { - margin-left: 5px; - } - } + color: $gray; } } .visibility-level-label { - color: #555; - font-weight: bold; + color: $gray; i { color: inherit; } } + + .project-repo-buttons { + margin-top: -3px; + position: absolute; + right: 0; + width: 260px; + text-align: right; + + .btn { + font-weight: bold; + font-size: 14px; + line-height: 16px; + + .count { + padding-left: 10px; + border-left: 1px solid #ccc; + display: inline-block; + margin-left: 10px; + } + } + } } .project-home-links { @@ -109,6 +119,10 @@ background: #FAFAFA; width: 100%; } + + .input-group-addon { + background: #FAFAFA; + } } .project-visibility-level-holder { @@ -127,7 +141,7 @@ .option-descr { margin-left: 24px; - color: #666; + color: $gray; } } } @@ -206,8 +220,10 @@ ul.nav.nav-projects-tabs { white-space: normal; text-align: left; padding: 10px 15px; - background-color: #F9F9F9; - border-color: #DDD; + + &.dropdown-toggle { + text-align: center; + } &:hover { background-color: #eee; diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 9a5685877f8..b5fda196bf0 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -38,6 +38,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :twitter_sharing_enabled, :sign_in_text, :home_page_url, + :max_attachment_size, restricted_visibility_levels: [] ) end diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb new file mode 100644 index 00000000000..e93603bef36 --- /dev/null +++ b/app/controllers/admin/deploy_keys_controller.rb @@ -0,0 +1,49 @@ +class Admin::DeployKeysController < Admin::ApplicationController + before_filter :deploy_keys, only: [:index] + before_filter :deploy_key, only: [:show, :destroy] + + def index + + end + + def show + + end + + def new + @deploy_key = deploy_keys.new + end + + def create + @deploy_key = deploy_keys.new(deploy_key_params) + + if @deploy_key.save + redirect_to admin_deploy_keys_path + else + render "new" + end + end + + def destroy + deploy_key.destroy + + respond_to do |format| + format.html { redirect_to admin_deploy_keys_path } + format.json { head :ok } + end + end + + protected + + def deploy_key + @deploy_key ||= deploy_keys.find(params[:id]) + end + + def deploy_keys + @deploy_keys ||= DeployKey.are_public + end + + def deploy_key_params + params.require(:deploy_key).permit(:key, :title) + end +end diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 9d9adaa467f..22d045fc388 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -41,7 +41,7 @@ class Admin::GroupsController < Admin::ApplicationController end def members_update - @group.add_users(params[:user_ids].split(','), params[:access_level]) + @group.add_users(params[:user_ids].split(','), params[:access_level], current_user) redirect_to [:admin, @group], notice: 'Users were successfully added.' end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 693970e5349..b4c011f213c 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -72,8 +72,8 @@ class Admin::UsersController < Admin::ApplicationController end respond_to do |format| + user.skip_reconfirmation! if user.update_attributes(user_params_with_pass) - user.confirm! format.html { redirect_to [:admin, user], notice: 'User was successfully updated.' } format.json { head :ok } else diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2809f90c0d5..920a981e7c9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -126,7 +126,7 @@ class ApplicationController < ActionController::Base def repository @repository ||= project.repository - rescue Grit::NoSuchPathError(e) + rescue Grit::NoSuchPathError => e log_exception(e) nil end @@ -153,7 +153,7 @@ class ApplicationController < ActionController::Base end def method_missing(method_sym, *arguments, &block) - if method_sym.to_s =~ /^authorize_(.*)!$/ + if method_sym.to_s =~ /\Aauthorize_(.*)!\z/ authorize_project!($1.to_sym) else super @@ -203,6 +203,7 @@ class ApplicationController < ActionController::Base gon.api_version = API::API.version gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s + gon.max_file_size = current_application_settings.max_attachment_size; if current_user gon.current_user_id = current_user.id diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb new file mode 100644 index 00000000000..11af9895261 --- /dev/null +++ b/app/controllers/autocomplete_controller.rb @@ -0,0 +1,30 @@ +class AutocompleteController < ApplicationController + def users + @users = + if params[:project_id].present? + project = Project.find(params[:project_id]) + + if can?(current_user, :read_project, project) + project.team.users + end + elsif params[:group_id] + group = Group.find(params[:group_id]) + + if can?(current_user, :read_group, group) + group.users + end + else + User.all + end + + @users = @users.search(params[:search]) if params[:search].present? + @users = @users.active + @users = @users.page(params[:page]).per(PER_PAGE) + render json: @users, only: [:name, :username, :id], methods: [:avatar_url] + end + + def user + @user = User.find(params[:id]) + render json: @user, only: [:name, :username, :id], methods: [:avatar_url] + end +end diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index bc98eab133c..af1faca93f6 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -4,11 +4,11 @@ class ConfirmationsController < Devise::ConfirmationsController def after_confirmation_path_for(resource_name, resource) if signed_in?(resource_name) - signed_in_root_path(resource) + after_sign_in_path_for(resource) else sign_in(resource) if signed_in?(resource_name) - signed_in_root_path(resource) + after_sign_in_path_for(resource) else new_session_path(resource_name) end diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index a73b8fa212a..469a6813ee2 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -13,7 +13,7 @@ class Groups::ApplicationController < ApplicationController end def authorize_admin_group! - unless can?(current_user, :manage_group, group) + unless can?(current_user, :admin_group, group) return render_404 end end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 2df51c97a22..265cf4f0f4a 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -11,6 +11,7 @@ class Groups::GroupMembersController < Groups::ApplicationController def index @project = @group.projects.find(params[:project_id]) if params[:project_id] @members = @group.group_members + @members = @members.non_invite unless can?(current_user, :admin_group, @group) if params[:search].present? users = @group.users.search(params[:search]).to_a @@ -22,7 +23,7 @@ class Groups::GroupMembersController < Groups::ApplicationController end def create - @group.add_users(params[:user_ids].split(','), params[:access_level]) + @group.add_users(params[:user_ids].split(','), params[:access_level], current_user) redirect_to group_group_members_path(@group), notice: 'Users were successfully added.' end @@ -38,7 +39,7 @@ class Groups::GroupMembersController < Groups::ApplicationController if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner. @group_member.destroy respond_to do |format| - format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } + format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } format.js { render nothing: true } end else @@ -46,12 +47,26 @@ class Groups::GroupMembersController < Groups::ApplicationController end end + def resend_invite + redirect_path = group_group_members_path(@group) + + @group_member = @group.group_members.find(params[:id]) + + if @group_member.invite? + @group_member.resend_invite + + redirect_to redirect_path, notice: 'The invitation was successfully resent.' + else + redirect_to redirect_path, alert: 'The invitation has already been accepted.' + end + end + def leave @group_member = @group.group_members.where(user_id: current_user.id).first if can?(current_user, :destroy_group_member, @group_member) @group_member.destroy - redirect_to(dashboard_groups_path, info: "You left #{group.name} group.") + redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.") else return render_403 end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index c46b8fff88f..546ff2cc71f 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -51,6 +51,6 @@ class Groups::MilestonesController < ApplicationController end def authorize_group_milestone! - return render_404 unless can?(current_user, :manage_group, group) + return render_404 unless can?(current_user, :admin_group, group) end end diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index c4d620d87b1..35ece5b270b 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -3,13 +3,36 @@ class HelpController < ApplicationController end def show - @category = params[:category] - @file = params[:file] + category = clean_path_info(path_params[:category]) + file = path_params[:file] - if File.exists?(Rails.root.join('doc', @category, @file + '.md')) - render 'show' - else - not_found! + respond_to do |format| + format.any(:markdown, :md, :html) do + path = Rails.root.join('doc', category, "#{file}.md") + + if File.exist?(path) + @markdown = File.read(path) + + render 'show.html.haml' + else + # Force template to Haml + render 'errors/not_found.html.haml', layout: 'errors', status: 404 + end + end + + # Allow access to images in the doc folder + format.any(:png, :gif, :jpeg) do + path = Rails.root.join('doc', category, "#{file}.#{params[:format]}") + + if File.exist?(path) + send_file(path, disposition: 'inline') + else + head :not_found + end + end + + # Any other format we don't recognize, just respond 404 + format.any { head :not_found } end end @@ -18,4 +41,44 @@ class HelpController < ApplicationController def ui end + + private + + def path_params + params.require(:category) + params.require(:file) + + params + end + + PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact) + + # Taken from ActionDispatch::FileHandler + # Cleans up the path, to prevent directory traversal outside the doc folder. + def clean_path_info(path_info) + parts = path_info.split(PATH_SEPS) + + clean = [] + + # Walk over each part of the path + parts.each do |part| + # Turn `one//two` or `one/./two` into `one/two`. + next if part.empty? || part == '.' + + if part == '..' + # Turn `one/two/../` into `one` + clean.pop + else + # Add simple folder names to the clean path. + clean << part + end + end + + # If the path was an absolute path (i.e. `/` or `/one/two`), + # add `/` to the front of the clean path. + clean.unshift '/' if parts.empty? || parts.first.empty? + + # Join all the clean path parts by the path separator. + ::File.join(*clean) + end end diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index edb8bd4160b..93a7ace3530 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -8,7 +8,7 @@ class Import::BaseController < ApplicationController namespace.add_owner(current_user) rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid namespace = Namespace.find_by_path_or_name(@target_namespace) - unless namespace.owner == current_user + unless current_user.can?(:create_projects, namespace) @already_been_taken = true return false end diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 83ebc5fddca..bb8d7e0235c 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -36,8 +36,11 @@ class Import::BitbucketController < Import::BaseController def create @repo_id = params[:repo_id] || "" repo = client.project(@repo_id.gsub("___", "/")) - @target_namespace = params[:new_namespace].presence || repo["owner"] @project_name = repo["slug"] + + repo_owner = repo["owner"] + repo_owner = current_user.username if repo_owner == client.user["user"]["username"] + @target_namespace = params[:new_namespace].presence || repo_owner namespace = get_or_create_namespace || (render and return) diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 8650b6464dc..87b41454c77 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -31,8 +31,11 @@ class Import::GithubController < Import::BaseController def create @repo_id = params[:repo_id].to_i repo = client.repo(@repo_id) - @target_namespace = params[:new_namespace].presence || repo.owner.login @project_name = repo.name + + repo_owner = repo.owner.login + repo_owner = current_user.username if repo_owner == client.user.login + @target_namespace = params[:new_namespace].presence || repo_owner namespace = get_or_create_namespace || (render and return) diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index e979dad4b11..bddbfded812 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -28,8 +28,11 @@ class Import::GitlabController < Import::BaseController def create @repo_id = params[:repo_id].to_i repo = client.project(@repo_id) - @target_namespace = params[:new_namespace].presence || repo["namespace"]["path"] @project_name = repo["name"] + + repo_owner = repo["namespace"]["path"] + repo_owner = current_user.username if repo_owner == client.user["username"] + @target_namespace = params[:new_namespace].presence || repo_owner namespace = get_or_create_namespace || (render and return) diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb new file mode 100644 index 00000000000..73c912e285b --- /dev/null +++ b/app/controllers/import/google_code_controller.rb @@ -0,0 +1,116 @@ +class Import::GoogleCodeController < Import::BaseController + before_filter :user_map, only: [:new_user_map, :create_user_map] + + def new + + end + + def callback + dump_file = params[:dump_file] + + unless dump_file.respond_to?(:read) + return redirect_to :back, alert: "You need to upload a Google Takeout archive." + end + + begin + dump = JSON.parse(dump_file.read) + rescue + return redirect_to :back, alert: "The uploaded file is not a valid Google Takeout archive." + end + + client = Gitlab::GoogleCodeImport::Client.new(dump) + unless client.valid? + return redirect_to :back, alert: "The uploaded file is not a valid Google Takeout archive." + end + + session[:google_code_dump] = dump + + if params[:create_user_map] == "1" + redirect_to new_user_map_import_google_code_path + else + redirect_to status_import_google_code_path + end + end + + def new_user_map + + end + + def create_user_map + user_map_json = params[:user_map] + user_map_json = "{}" if user_map_json.blank? + + begin + user_map = JSON.parse(user_map_json) + rescue + flash.now[:alert] = "The entered user map is not a valid JSON user map." + + render "new_user_map" and return + end + + unless user_map.is_a?(Hash) && user_map.all? { |k, v| k.is_a?(String) && v.is_a?(String) } + flash.now[:alert] = "The entered user map is not a valid JSON user map." + + render "new_user_map" and return + end + + # This is the default, so let's not save it into the database. + user_map.reject! do |key, value| + value == Gitlab::GoogleCodeImport::Client.mask_email(key) + end + + session[:google_code_user_map] = user_map + + flash[:notice] = "The user map has been saved. Continue by selecting the projects you want to import." + + redirect_to status_import_google_code_path + end + + def status + unless client.valid? + return redirect_to new_import_google_path + end + + @repos = client.repos + + @already_added_projects = current_user.created_projects.where(import_type: "google_code") + already_added_projects_names = @already_added_projects.pluck(:import_source) + + @repos.reject! { |repo| already_added_projects_names.include? repo.name } + end + + def jobs + jobs = current_user.created_projects.where(import_type: "google_code").to_json(only: [:id, :import_status]) + render json: jobs + end + + def create + @repo_id = params[:repo_id] + repo = client.repo(@repo_id) + @target_namespace = current_user.namespace + @project_name = repo.name + + namespace = @target_namespace + + user_map = session[:google_code_user_map] + + @project = Gitlab::GoogleCodeImport::ProjectCreator.new(repo, namespace, current_user, user_map).execute + end + + private + + def client + @client ||= Gitlab::GoogleCodeImport::Client.new(session[:google_code_dump]) + end + + def user_map + @user_map ||= begin + user_map = client.user_map + + stored_user_map = session[:google_code_user_map] + user_map.update(stored_user_map) if stored_user_map + + Hash[user_map.sort] + end + end +end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb new file mode 100644 index 00000000000..1f97ff16c55 --- /dev/null +++ b/app/controllers/invites_controller.rb @@ -0,0 +1,83 @@ +class InvitesController < ApplicationController + before_filter :member + skip_before_filter :authenticate_user!, only: :decline + + respond_to :html + + layout 'navless' + + def show + + end + + def accept + if member.accept_invite!(current_user) + label, path = source_info(member.source) + + redirect_to path, notice: "You have been granted #{member.human_access} access to #{label}." + else + redirect_to :back, alert: "The invitation could not be accepted." + end + end + + def decline + if member.decline_invite! + label, _ = source_info(member.source) + + path = + if current_user + dashboard_path + else + new_user_session_path + end + + redirect_to path, notice: "You have declined the invitation to join #{label}." + else + redirect_to :back, alert: "The invitation could not be declined." + end + end + + private + + def member + return @member if defined?(@member) + + @token = params[:id] + @member = Member.find_by_invite_token(@token) + + unless @member + render_404 and return + end + + @member + end + + def authenticate_user! + return if current_user + + notice = "To accept this invitation, sign in" + notice << " or create an account" if current_application_settings.signup_enabled? + notice << "." + + store_location_for :user, request.fullpath + redirect_to new_user_session_path, notice: notice + end + + def source_info(source) + case source + when Project + project = member.source + label = "project #{project.name_with_namespace}" + path = namespace_project_path(project.namespace, project) + when Group + group = member.source + label = "group #{group.name}" + path = group_path(group) + else + label = "who knows what" + path = dashboard_path + end + + [label, path] + end +end diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb index b7a9d8c1291..386d103ee5a 100644 --- a/app/controllers/namespaces_controller.rb +++ b/app/controllers/namespaces_controller.rb @@ -4,14 +4,22 @@ class NamespacesController < ApplicationController def show namespace = Namespace.find_by(path: params[:id]) - unless namespace - return render_404 + if namespace + if namespace.is_a?(Group) + group = namespace + else + user = namespace.owner + end end - if namespace.type == "Group" - redirect_to group_path(namespace) + if user + redirect_to user_path(user) + elsif group && can?(current_user, :read_group, group) + redirect_to group_path(group) + elsif current_user.nil? + authenticate_user! else - redirect_to user_path(namespace.owner) + render_404 end end end diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb index 4a65c978e5c..954c98c0d9f 100644 --- a/app/controllers/profiles/emails_controller.rb +++ b/app/controllers/profiles/emails_controller.rb @@ -3,6 +3,7 @@ class Profiles::EmailsController < ApplicationController def index @primary = current_user.email + @public_email = current_user.public_email @emails = current_user.emails end @@ -19,7 +20,8 @@ class Profiles::EmailsController < ApplicationController @email.destroy current_user.set_notification_email - current_user.save if current_user.notification_email_changed? + current_user.set_public_email + current_user.save if current_user.notification_email_changed? or current_user.public_email_changed? respond_to do |format| format.html { redirect_to profile_emails_url } diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 9252e85e8cc..7f76906066d 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -67,9 +67,10 @@ class ProfilesController < ApplicationController def user_params params.require(:user).permit( - :email, :password, :password_confirmation, :bio, :name, :username, - :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, - :avatar, :hide_no_ssh_key, :hide_no_password, :location + :email, :password, :password_confirmation, :bio, :name, + :username, :skype, :linkedin, :twitter, :website_url, + :color_scheme_id, :theme_id, :avatar, :hide_no_ssh_key, + :hide_no_password, :location, :public_email ) end end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 679a5d76ec0..6fba3ce299b 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -8,7 +8,14 @@ class Projects::DeployKeysController < Projects::ApplicationController def index @enabled_keys = @project.deploy_keys - @available_keys = available_keys - @enabled_keys + + @available_keys = accessible_keys - @enabled_keys + @available_project_keys = current_user.project_deploy_keys - @enabled_keys + @available_public_keys = DeployKey.are_public - @enabled_keys + + # Public keys that are already used by another accessible project are already + # in @available_project_keys. + @available_public_keys -= @available_project_keys end def show @@ -32,18 +39,9 @@ class Projects::DeployKeysController < Projects::ApplicationController end end - def destroy - @key = @project.deploy_keys.find(params[:id]) - @key.destroy - - respond_to do |format| - format.html { redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) } - format.js { render nothing: true } - end - end - def enable - @project.deploy_keys << available_keys.find(params[:id]) + @key = accessible_keys.find(params[:id]) + @project.deploy_keys << @key redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) @@ -52,14 +50,13 @@ class Projects::DeployKeysController < Projects::ApplicationController def disable @project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy - redirect_to namespace_project_deploy_keys_path(@project.namespace, - @project) + redirect_to :back end protected - def available_keys - @available_keys ||= current_user.accessible_deploy_keys + def accessible_keys + @accessible_keys ||= current_user.accessible_deploy_keys end def deploy_key_params diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 4ab15db01f7..72967a26ff1 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -6,6 +6,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController def index @project_members = @project.project_members + @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) if params[:search].present? users = @project.users.search(params[:search]).to_a @@ -17,6 +18,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController @group = @project.group if @group @group_members = @group.group_members + @group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group) if params[:search].present? users = @group.users.search(params[:search]).to_a @@ -34,30 +36,42 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def create - users = User.where(id: params[:user_ids].split(',')) - @project.team << [users, params[:access_level]] + @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user) redirect_to namespace_project_project_members_path(@project.namespace, @project) end def update - @project_member = @project.project_members.find_by(user_id: member) + @project_member = @project.project_members.find(params[:id]) @project_member.update_attributes(member_params) end def destroy - @project_member = @project.project_members.find_by(user_id: member) + @project_member = @project.project_members.find(params[:id]) @project_member.destroy respond_to do |format| format.html do - redirect_to namespace_project_project_members_path(@project.namespace, - @project) + redirect_to namespace_project_project_members_path(@project.namespace, @project) end format.js { render nothing: true } end end + def resend_invite + redirect_path = namespace_project_project_members_path(@project.namespace, @project) + + @project_member = @project.project_members.find(params[:id]) + + if @project_member.invite? + @project_member.resend_invite + + redirect_to redirect_path, notice: 'The invitation was successfully resent.' + else + redirect_to redirect_path, alert: 'The invitation has already been accepted.' + end + end + def leave @project.project_members.find_by(user_id: current_user).destroy @@ -69,7 +83,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController def apply_import giver = Project.find(params[:source_project_id]) - status = @project.team.import(giver) + status = @project.team.import(giver, current_user) notice = status ? "Successfully imported" : "Import failed" redirect_to(namespace_project_project_members_path(project.namespace, project), @@ -78,10 +92,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController protected - def member - @member ||= User.find_by(username: params[:id]) - end - def member_params params.require(:project_member).permit(:user_id, :access_level) end diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 67acf45ab7f..ec3b2b8d75a 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -55,5 +55,10 @@ class Projects::RefsController < Projects::ApplicationController commit: last_commit } end + + respond_to do |format| + format.html { render_404 } + format.js + end end end diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index cbb888b25e8..96defb0c721 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -11,18 +11,18 @@ class Projects::RepositoriesController < Projects::ApplicationController end def archive - unless can?(current_user, :download_code, @project) - render_404 and return + begin + file_path = ArchiveRepositoryService.new(@project, params[:ref], params[:format]).execute + rescue + return head :not_found end - file_path = ArchiveRepositoryService.new.execute(@project, params[:ref], params[:format]) - if file_path # Send file to user response.headers["Content-Length"] = File.open(file_path).size.to_s send_file file_path else - render_404 + redirect_to request.fullpath end end end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 643167947b9..aeb7f0699f5 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -5,6 +5,7 @@ class Projects::WikisController < Projects::ApplicationController before_filter :authorize_write_wiki!, only: [:edit, :create, :history] before_filter :authorize_admin_wiki!, only: :destroy before_filter :load_project_wiki + include WikiHelper def pages @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]).per(PER_PAGE) @@ -45,7 +46,10 @@ class Projects::WikisController < Projects::ApplicationController return render('empty') unless can?(current_user, :write_wiki, @project) if @page.update(content, format, message) - redirect_to [@project.namespace.becomes(Namespace), @project, @page], notice: 'Wiki was successfully updated.' + redirect_to( + namespace_project_wiki_path(@project.namespace, @project, @page), + notice: 'Wiki was successfully updated.' + ) else render 'edit' end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index a3284c82d3f..c5828d0b2df 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -3,15 +3,22 @@ class SearchController < ApplicationController def show return if params[:search].nil? || params[:search].blank? - @project = Project.find_by(id: params[:project_id]) if params[:project_id].present? - @group = Group.find_by(id: params[:group_id]) if params[:group_id].present? + + if params[:project_id].present? + @project = Project.find_by(id: params[:project_id]) + @project = nil unless can?(current_user, :download_code, @project) + end + + if params[:group_id].present? + @group = Group.find_by(id: params[:group_id]) + @group = nil unless can?(current_user, :read_group, @group) + end + @scope = params[:scope] @show_snippets = params[:snippets].eql? 'true' @search_results = if @project - return access_denied! unless can?(current_user, :download_code, @project) - unless %w(blobs notes issues merge_requests wiki_blobs). include?(@scope) @scope = 'blobs' @@ -35,7 +42,12 @@ class SearchController < ApplicationController def autocomplete term = params[:term] - @project = Project.find(params[:project_id]) if params[:project_id].present? + + if params[:project_id].present? + @project = Project.find_by(id: params[:project_id]) + @project = nil unless can?(current_user, :read_project, @project) + end + @ref = params[:project_ref] if params[:project_ref].present? render json: search_autocomplete_opts(term).to_json diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 7b6982c5074..3f11d7afe6f 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -26,6 +26,12 @@ class SessionsController < Devise::SessionsController end def create - super + super do |resource| + # User has successfully signed in, so clear any unused reset tokens + if resource.reset_password_token.present? + resource.update_attributes(reset_password_token: nil, + reset_password_sent_at: nil) + end + end end end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 088a766ed3a..2c0702073d4 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -19,6 +19,8 @@ require_relative 'projects_finder' class IssuableFinder + NONE = '0' + attr_accessor :current_user, :params def execute(current_user, params) @@ -112,7 +114,7 @@ class IssuableFinder def by_milestone(items) if params[:milestone_id].present? - items = items.where(milestone_id: (params[:milestone_id] == '0' ? nil : params[:milestone_id])) + items = items.where(milestone_id: (params[:milestone_id] == NONE ? nil : params[:milestone_id])) end items @@ -120,7 +122,7 @@ class IssuableFinder def by_assignee(items) if params[:assignee_id].present? - items = items.where(assignee_id: (params[:assignee_id] == '0' ? nil : params[:assignee_id])) + items = items.where(assignee_id: (params[:assignee_id] == NONE ? nil : params[:assignee_id])) end items @@ -128,7 +130,7 @@ class IssuableFinder def by_author(items) if params[:author_id].present? - items = items.where(author_id: (params[:author_id] == '0' ? nil : params[:author_id])) + items = items.where(author_id: (params[:author_id] == NONE ? nil : params[:author_id])) end items diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 38b5fc4a011..20457572a08 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -125,7 +125,7 @@ module ApplicationHelper # If reference is commit id - we should add it to branch/tag selectbox if(@ref && !options.flatten.include?(@ref) && - @ref =~ /^[0-9a-zA-Z]{6,52}$/) + @ref =~ /\A[0-9a-zA-Z]{6,52}\z/) options << ['Commit', [@ref]] end @@ -236,33 +236,35 @@ module ApplicationHelper Gitlab::MarkdownHelper.gitlab_markdown?(filename) end - def link_to(name = nil, options = nil, html_options = nil, &block) - begin - uri = URI(options) - host = uri.host - absolute_uri = uri.absolute? - rescue URI::InvalidURIError, ArgumentError - host = nil - absolute_uri = nil - end - - # Add 'nofollow' only to external links - if host && host != Gitlab.config.gitlab.host && absolute_uri - if html_options - if html_options[:rel] - html_options[:rel] << ' nofollow' - else - html_options.merge!(rel: 'nofollow') - end - else - html_options = Hash.new - html_options[:rel] = 'nofollow' + # Overrides ActionView::Helpers::UrlHelper#link_to to add `rel="nofollow"` to + # external links + def link_to(name = nil, options = nil, html_options = {}) + if options.kind_of?(String) + if !options.start_with?('#', '/') + html_options = add_nofollow(options, html_options) end end super end + # Add `"rel=nofollow"` to external links + # + # link - String link to check + # html_options - Hash of `html_options` passed to `link_to` + # + # Returns `html_options`, adding `rel: nofollow` for external links + def add_nofollow(link, html_options = {}) + uri = URI(link) + + if uri && uri.absolute? && uri.host != Gitlab.config.gitlab.host + rel = html_options.fetch(:rel, '') + html_options[:rel] = (rel + ' nofollow').strip + end + + html_options + end + def escaped_autolink(text) auto_link ERB::Util.html_escape(text), link: :urls end @@ -275,7 +277,9 @@ module ApplicationHelper 'https://' + promo_host end - def page_filter_path(options={}) + def page_filter_path(options = {}) + without = options.delete(:without) + exist_opts = { state: params[:state], scope: params[:scope], @@ -288,6 +292,12 @@ module ApplicationHelper options = exist_opts.merge(options) + if without.present? + without.each do |key| + options.delete(key) + end + end + path = request.path path << "?#{options.to_param}" path diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 798d62b3a09..4ea838ca447 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -61,4 +61,12 @@ module BlobHelper 'Preview changes' end end + + # Return an image icon depending on the file mode and extension + # + # mode - File unix mode + # mode - File name + def blob_icon(mode, name) + icon("#{file_type_icon_class('file', mode, name)} fw") + end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 5aae697e2f0..d13d80be293 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -134,12 +134,13 @@ module CommitsHelper # avatar: true will prepend the avatar image # size: size of the avatar image in px def commit_person_link(commit, options = {}) + user = commit.send(options[:source]) + source_name = clean(commit.send "#{options[:source]}_name".to_sym) source_email = clean(commit.send "#{options[:source]}_email".to_sym) - user = User.find_for_commit(source_email, source_name) - person_name = user.nil? ? source_name : user.name - person_email = user.nil? ? source_email : user.email + person_name = user.try(:name) || source_name + person_email = user.try(:email) || source_email text = if options[:avatar] diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index b56f21c7a18..4f42972a4dd 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -101,7 +101,7 @@ module DiffHelper end def line_comments - @line_comments ||= @line_notes.group_by(&:line_code) + @line_comments ||= @line_notes.select(&:active?).group_by(&:line_code) end def organize_comments(type_left, type_right, line_code_left, line_code_right) diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 08476f8516e..0df3ecc90b7 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -1,6 +1,3 @@ -require 'html/pipeline' -require 'html/pipeline/gitlab' - module EmailsHelper # Google Actions @@ -33,35 +30,9 @@ module EmailsHelper end end - def add_email_highlight_css - Rugments::Themes::Github.render(scope: '.highlight') - end - def color_email_diff(diffcontent) - formatter = Rugments::Formatters::HTML.new(cssclass: 'highlight') + formatter = Rugments::Formatters::HTML.new(cssclass: "highlight", inline_theme: :github) lexer = Rugments::Lexers::Diff.new raw formatter.format(lexer.lex(diffcontent)) end - - def replace_image_links_with_base64(text, project) - # Used pipelines in GitLab: - # GitlabEmailImageFilter - replaces images that have been uploaded as attachments with inline images in emails. - # - # see https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters - filters = [ - HTML::Pipeline::Gitlab::GitlabEmailImageFilter - ] - - context = { - base_url: File.join(Gitlab.config.gitlab.url, project.path_with_namespace, 'uploads'), - upload_path: File.join(Rails.root, 'public', 'uploads', project.path_with_namespace), - } - - pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline - - result = pipeline.call(text, context) - text = result[:output].to_html(save_with: 0) - - text.html_safe - end end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 7ca3f058636..aa1de2f50ef 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -13,7 +13,7 @@ module GitlabMarkdownHelper def link_to_gfm(body, url, html_options = {}) return "" if body.blank? - escaped_body = if body =~ /^\<img/ + escaped_body = if body =~ /\A\<img/ body else escape_once(body) @@ -31,24 +31,28 @@ module GitlabMarkdownHelper def markdown(text, options={}) unless @markdown && options == @options @options = options - gitlab_renderer = Redcarpet::Render::GitlabHTML.new(self, - user_color_scheme_class, - { - # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch- - with_toc_data: true, - safe_links_only: true - }.merge(options)) - @markdown = Redcarpet::Markdown.new(gitlab_renderer, - # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use - no_intra_emphasis: true, - tables: true, - fenced_code_blocks: true, - autolink: true, - strikethrough: true, - lax_spacing: true, - space_after_headers: true, - superscript: true) + + # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch + rend = Redcarpet::Render::GitlabHTML.new(self, user_color_scheme_class, { + with_toc_data: true, + safe_links_only: true, + # Handled further down the line by HTML::Pipeline::SanitizationFilter + escape_html: false + }.merge(options)) + + # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use + @markdown = Redcarpet::Markdown.new(rend, + no_intra_emphasis: true, + tables: true, + fenced_code_blocks: true, + autolink: true, + strikethrough: true, + lax_spacing: true, + space_after_headers: true, + superscript: true + ) end + @markdown.render(text).html_safe end @@ -135,7 +139,7 @@ module GitlabMarkdownHelper @project.path_with_namespace, path_with_ref(file_path), file_path - ].compact.join("/").gsub(/^\/*|\/*$/, '') + id + ].compact.join("/").gsub(/\A\/*|\/*\z/, '') + id end def sanitize_slashes(path) diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 3386fac8657..9703c8d9e9c 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -29,6 +29,10 @@ module GitlabRoutingHelper namespace_project_merge_request_path(entity.project.namespace, entity.project, entity, *args) end + def milestone_path(entity, *args) + namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args) + end + def project_url(project, *args) namespace_project_url(project.namespace, project, *args) end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 2d0d0b494f6..add0a776a63 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,6 +1,10 @@ module GroupsHelper - def remove_user_from_group_message(group, user) - "Are you sure you want to remove \"#{user.name}\" from \"#{group.name}\"?" + def remove_user_from_group_message(group, member) + if member.user + "Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?" + else + "Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?" + end end def leave_group_message(group) diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 18260f0ed4d..a9030729b48 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -36,4 +36,48 @@ module IconsHelper def private_icon icon('lock') end + + def file_type_icon_class(type, mode, name) + if type == 'folder' + icon_class = 'folder' + elsif mode == '120000' + icon_class = 'share' + else + # Guess which icon to choose based on file extension. + # If you think a file extension is missing, feel free to add it on PR + + case File.extname(name).downcase + when '.pdf' + icon_class = 'file-pdf-o' + when '.jpg', '.jpeg', '.jif', '.jfif', + '.jp2', '.jpx', '.j2k', '.j2c', + '.png', '.gif', '.tif', '.tiff', + '.svg', '.ico', '.bmp' + icon_class = 'file-image-o' + when '.zip', '.zipx', '.tar', '.gz', '.bz', '.bzip', + '.xz', '.rar', '.7z' + icon_class = 'file-archive-o' + when '.mp3', '.wma', '.ogg', '.oga', '.wav', '.flac', '.aac' + icon_class = 'file-audio-o' + when '.mp4', '.m4p', '.m4v', + '.mpg', '.mp2', '.mpeg', '.mpe', '.mpv', + '.mpg', '.mpeg', '.m2v', + '.avi', '.mkv', '.flv', '.ogv', '.mov', + '.3gp', '.3g2' + icon_class = 'file-video-o' + when '.doc', '.dot', '.docx', '.docm', '.dotx', '.dotm', '.docb' + icon_class = 'file-word-o' + when '.xls', '.xlt', '.xlm', '.xlsx', '.xlsm', '.xltx', '.xltm', + '.xlsb', '.xla', '.xlam', '.xll', '.xlw' + icon_class = 'file-excel-o' + when '.ppt', '.pot', '.pps', '.pptx', '.pptm', '.potx', '.potm', + '.ppam', '.ppsx', '.ppsm', '.sldx', '.sldm' + icon_class = 'file-powerpoint-o' + else + icon_class = 'file-text-o' + end + end + + icon_class + end end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index a4bd4d30215..ad4a7612724 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -13,22 +13,34 @@ module IssuesHelper OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned') end - def url_for_project_issues(project = @project) + def url_for_project_issues(project = @project, options = {}) return '' if project.nil? - project.issues_tracker.project_url + if options[:only_path] + project.issues_tracker.project_path + else + project.issues_tracker.project_url + end end - def url_for_new_issue(project = @project) + def url_for_new_issue(project = @project, options = {}) return '' if project.nil? - project.issues_tracker.new_issue_url + if options[:only_path] + project.issues_tracker.new_issue_path + else + project.issues_tracker.new_issue_url + end end - def url_for_issue(issue_iid, project = @project) + def url_for_issue(issue_iid, project = @project, options = {}) return '' if project.nil? - project.issues_tracker.issue_url(issue_iid) + if options[:only_path] + project.issues_tracker.issue_path(issue_iid) + else + project.issues_tracker.issue_url(issue_iid) + end end def title_for_issue(issue_iid, project = @project) diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 49063491abf..32ef2e7ca84 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -47,4 +47,8 @@ module LabelsHelper "#FFF" end end + + def project_labels_options(project) + options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name]) + end end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 59fdc0d49cc..282bdf744d2 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -19,4 +19,15 @@ module MilestonesHelper content_tag :div, nil, options end end + + def projects_milestones_options + milestones = + if @project + @project.milestones + else + Milestone.where(project_id: @projects) + end.active + + options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id]) + end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 7bf51b5b8e8..c2a7732e6f0 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -1,6 +1,10 @@ module ProjectsHelper - def remove_from_project_team_message(project, user) - "You are going to remove #{user.name} from #{project.name} project team. Are you sure?" + def remove_from_project_team_message(project, member) + if member.user + "You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?" + else + "You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?" + end end def link_to_project(project) @@ -80,17 +84,17 @@ module ProjectsHelper @project.milestones.active.order("due_date, title ASC") end - def link_to_toggle_star(title, starred, signed_in) - cls = 'star-btn' - cls << ' disabled' unless signed_in + def link_to_toggle_star(title, starred) + cls = 'star-btn btn btn-sm btn-default' - toggle_html = content_tag('span', class: 'toggle') do - toggle_text = if starred - ' Unstar' - else - ' Star' - end + toggle_text = + if starred + ' Unstar' + else + ' Star' + end + toggle_html = content_tag('span', class: 'toggle') do icon('star') + toggle_text end @@ -106,23 +110,33 @@ module ProjectsHelper data: { type: 'json' } } + path = toggle_star_namespace_project_path(@project.namespace, @project) content_tag 'span', class: starred ? 'turn-on' : 'turn-off' do - link_to( - toggle_star_namespace_project_path(@project.namespace, @project), - link_opts - ) do + link_to(path, link_opts) do toggle_html + ' ' + count_html end end end def link_to_toggle_fork - out = icon('code-fork') - out << ' Fork' - out << content_tag(:span, class: 'count') do + html = content_tag('span') do + icon('code-fork') + ' Fork' + end + + count_html = content_tag(:span, class: 'count') do @project.forks_count.to_s end + + html + count_html + end + + def project_for_deploy_key(deploy_key) + if deploy_key.projects.include?(@project) + @project + else + deploy_key.projects.find { |project| can?(current_user, :read_project, project) } + end end private diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 796d805f219..bec8f2f1aa7 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -4,18 +4,31 @@ module SelectsHelper css_class << "multiselect " if opts[:multiple] css_class << (opts[:class] || '') value = opts[:selected] || '' + placeholder = opts[:placeholder] || 'Search for a user' - hidden_field_tag(id, value, class: css_class) - end + null_user = opts[:null_user] || false + any_user = opts[:any_user] || false + email_user = opts[:email_user] || false + first_user = opts[:first_user] && current_user ? current_user.username : false - def project_users_select_tag(id, opts = {}) - css_class = "ajax-project-users-select " - css_class << "multiselect " if opts[:multiple] - css_class << (opts[:class] || '') - value = opts[:selected] || '' - placeholder = opts[:placeholder] || 'Select user' - project_id = opts[:project_id] || @project.id - hidden_field_tag(id, value, class: css_class, 'data-placeholder' => placeholder, 'data-project-id' => project_id) + html = { + class: css_class, + 'data-placeholder' => placeholder, + 'data-null-user' => null_user, + 'data-any-user' => any_user, + 'data-email-user' => email_user, + 'data-first-user' => first_user + } + + unless opts[:scope] == :all + if @project + html['data-project-id'] = @project.id + elsif @group + html['data-group-id'] = @group.id + end + end + + hidden_field_tag(id, value, html) end def groups_select_tag(id, opts = {}) diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index 241462e5e4c..9954617c762 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -44,7 +44,7 @@ module SubmoduleHelper def relative_self_url?(url) # (./)?(../repo.git) || (./)?(../../project/repo.git) ) - url =~ /^((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\Z/ || url =~ /^((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\Z/ + url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\z/ end def standard_links(host, namespace, project, commit) @@ -53,15 +53,22 @@ module SubmoduleHelper end def relative_self_links(url, commit) - if url.scan(/(\.\.\/)/).size == 2 - base = url[/([^\/]*\/[^\/]*)\.git/, 1] - else - base = [ @project.group.path, '/', url[/([^\/]*)\.git/, 1] ].join('') + # Map relative links to a namespace and project + # For example: + # ../bar.git -> same namespace, repo bar + # ../foo/bar.git -> namespace foo, repo bar + # ../../foo/bar/baz.git -> namespace bar, repo baz + components = url.split('/') + base = components.pop.gsub(/.git$/, '') + namespace = components.pop.gsub(/^\.\.$/, '') + + if namespace.empty? + namespace = @project.group.path end [ - namespace_project_path(base.namespace, base), - namespace_project_tree_path(base.namespace, base, commit) + namespace_project_path(namespace, base), + namespace_project_tree_path(namespace, base, commit) ] end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index bf6726574ec..6dd9b6f017c 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -34,12 +34,13 @@ module TreeHelper end end - # Return an image icon depending on the file type + # Return an image icon depending on the file type and mode # # type - String type of the tree item; either 'folder' or 'file' - def tree_icon(type) - icon_class = type == 'folder' ? 'folder' : 'file-o' - icon(icon_class) + # mode - File unix mode + # name - File name + def tree_icon(type, mode, name) + icon("#{file_type_icon_class(type, mode, name)} fw") end def tree_hex_class(content) diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb new file mode 100644 index 00000000000..a3bc64c010e --- /dev/null +++ b/app/helpers/wiki_helper.rb @@ -0,0 +1,22 @@ +module WikiHelper + # Rails v4.1.9+ escapes all model IDs, converting slashes into %2F. The + # only way around this is to implement our own path generators. + def namespace_project_wiki_path(namespace, project, wiki_page, *args) + slug = + case wiki_page + when Symbol + wiki_page + else + wiki_page.slug + end + namespace_project_path(namespace, project) + "/wikis/#{slug}" + end + + def edit_namespace_project_wiki_path(namespace, project, wiki_page, *args) + namespace_project_wiki_path(namespace, project, wiki_page) + '/edit' + end + + def history_namespace_project_wiki_path(namespace, project, wiki_page, *args) + namespace_project_wiki_path(namespace, project, wiki_page) + '/history' + end +end diff --git a/app/mailers/emails/groups.rb b/app/mailers/emails/groups.rb index 26f43bf955e..1c43f95dc8c 100644 --- a/app/mailers/emails/groups.rb +++ b/app/mailers/emails/groups.rb @@ -3,9 +3,50 @@ module Emails def group_access_granted_email(group_member_id) @group_member = GroupMember.find(group_member_id) @group = @group_member.group + @target_url = group_url(@group) - mail(to: @group_member.user.email, + @current_user = @group_member.user + + mail(to: @group_member.user.notification_email, subject: subject("Access to group was granted")) end + + def group_member_invited_email(group_member_id, token) + @group_member = GroupMember.find group_member_id + @group = @group_member.group + @token = token + + @target_url = group_url(@group) + @current_user = @group_member.user + + mail(to: @group_member.invite_email, + subject: "Invitation to join group #{@group.name}") + end + + def group_invite_accepted_email(group_member_id) + @group_member = GroupMember.find group_member_id + return if @group_member.created_by.nil? + + @group = @group_member.group + + @target_url = group_url(@group) + @current_user = @group_member.created_by + + mail(to: @group_member.created_by.notification_email, + subject: subject("Invitation accepted")) + end + + def group_invite_declined_email(group_id, invite_email, access_level, created_by_id) + return if created_by_id.nil? + + @group = Group.find(group_id) + @current_user = @created_by = User.find(created_by_id) + @access_level = access_level + @invite_email = invite_email + + @target_url = group_url(@group) + mail(to: @created_by.notification_email, + subject: subject("Invitation declined")) + end end end diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index ab5b0765352..3a83b083109 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -1,7 +1,7 @@ module Emails module Profile def new_user_email(user_id, token = nil) - @user = User.find(user_id) + @current_user = @user = User.find(user_id) @target_url = user_url(@user) @token = token mail(to: @user.notification_email, subject: subject("Account was created for you")) @@ -9,13 +9,13 @@ module Emails def new_email_email(email_id) @email = Email.find(email_id) - @user = @email.user + @current_user = @user = @email.user mail(to: @user.notification_email, subject: subject("Email was added to your account")) end def new_ssh_key_email(key_id) @key = Key.find(key_id) - @user = @key.user + @current_user = @user = @key.user @target_url = user_url(@user) mail(to: @user.notification_email, subject: subject("SSH key was added to your account")) end diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 3cd812825e2..0dbb2939bb3 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -1,15 +1,57 @@ module Emails module Projects - def project_access_granted_email(user_project_id) - @project_member = ProjectMember.find user_project_id + def project_access_granted_email(project_member_id) + @project_member = ProjectMember.find project_member_id @project = @project_member.project + @target_url = namespace_project_url(@project.namespace, @project) - mail(to: @project_member.user.email, + @current_user = @project_member.user + + mail(to: @project_member.user.notification_email, subject: subject("Access to project was granted")) end + def project_member_invited_email(project_member_id, token) + @project_member = ProjectMember.find project_member_id + @project = @project_member.project + @token = token + + @target_url = namespace_project_url(@project.namespace, @project) + @current_user = @project_member.user + + mail(to: @project_member.invite_email, + subject: "Invitation to join project #{@project.name_with_namespace}") + end + + def project_invite_accepted_email(project_member_id) + @project_member = ProjectMember.find project_member_id + return if @project_member.created_by.nil? + + @project = @project_member.project + + @target_url = namespace_project_url(@project.namespace, @project) + @current_user = @project_member.created_by + + mail(to: @project_member.created_by.notification_email, + subject: subject("Invitation accepted")) + end + + def project_invite_declined_email(project_id, invite_email, access_level, created_by_id) + return if created_by_id.nil? + + @project = Project.find(project_id) + @current_user = @created_by = User.find(created_by_id) + @access_level = access_level + @invite_email = invite_email + + @target_url = namespace_project_url(@project.namespace, @project) + + mail(to: @created_by.notification_email, + subject: subject("Invitation declined")) + end + def project_was_moved_email(project_id, user_id) - @user = User.find user_id + @current_user = @user = User.find user_id @project = Project.find project_id @target_url = namespace_project_url(@project.namespace, @project) mail(to: @user.notification_email, @@ -28,7 +70,7 @@ module Emails end @project = Project.find(project_id) - @author = User.find(author_id) + @current_user = @author = User.find(author_id) @reverse_compare = reverse_compare @compare = compare @ref_name = Gitlab::Git.ref_name(ref) @@ -83,9 +125,17 @@ module Emails @disable_footer = true - mail(from: sender(author_id, send_from_committer_email), - to: recipient, - subject: @subject) + reply_to = + if send_from_committer_email && can_send_from_user_email?(@author) + @author.email + else + Gitlab.config.gitlab.email_reply_to + end + + mail(from: sender(author_id, send_from_committer_email), + reply_to: reply_to, + to: recipient, + subject: @subject) end end end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 8fcdd3bc853..2c0d451511f 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -13,13 +13,16 @@ class Notify < ActionMailer::Base add_template_helper MergeRequestsHelper add_template_helper EmailsHelper + attr_accessor :current_user + helper_method :current_user, :can? + default_url_options[:host] = Gitlab.config.gitlab.host default_url_options[:protocol] = Gitlab.config.gitlab.protocol default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port? default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root default from: Proc.new { default_sender_address.format } - default reply_to: "noreply@#{Gitlab.config.gitlab.host}" + default reply_to: Gitlab.config.gitlab.email_reply_to # Just send email with 2 seconds delay def self.delay @@ -57,20 +60,24 @@ class Notify < ActionMailer::Base address end + def can_send_from_user_email?(sender) + sender_domain = sender.email.split("@").last + self.class.allowed_email_domains.include?(sender_domain) + end + # Return an email address that displays the name of the sender. # Only the displayed name changes; the actual email address is always the same. def sender(sender_id, send_from_user_email = false) - if sender = User.find(sender_id) - address = default_sender_address - address.display_name = sender.name - - sender_domain = sender.email.split("@").last - if send_from_user_email && self.class.allowed_email_domains.include?(sender_domain) - address.address = sender.email - end + return unless sender = User.find(sender_id) + + address = default_sender_address + address.display_name = sender.name - address.format + if send_from_user_email && can_send_from_user_email?(sender) + address.address = sender.email end + + address.format end # Look up a User by their ID and return their email address @@ -79,9 +86,8 @@ class Notify < ActionMailer::Base # # Returns a String containing the User's email address. def recipient(recipient_id) - if recipient = User.find(recipient_id) - recipient.notification_email - end + @current_user = User.find(recipient_id) + @current_user.notification_email end # Set the References header field @@ -154,4 +160,8 @@ class Notify < ActionMailer::Base mail(headers, &block) end + + def can? + Ability.abilities.allowed?(user, action, subject) + end end diff --git a/app/models/ability.rb b/app/models/ability.rb index d2b39f667f2..85a15596f8d 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -198,11 +198,11 @@ class Ability ]) end - # Only group owner and administrators can manage group + # Only group owner and administrators can admin group if group.has_owner?(user) || user.admin? rules.push(*[ - :manage_group, - :manage_namespace + :admin_group, + :admin_namespace ]) end @@ -212,11 +212,11 @@ class Ability def namespace_abilities(user, namespace) rules = [] - # Only namespace owner and administrators can manage it + # Only namespace owner and administrators can admin it if namespace.owner == user || user.admin? rules.push(*[ :create_projects, - :manage_namespace + :admin_namespace ]) end @@ -254,7 +254,7 @@ class Ability rules = [] target_user = subject.user group = subject.group - can_manage = group_abilities(user, group).include?(:manage_group) + can_manage = group_abilities(user, group).include?(:admin_group) if can_manage && (user != target_user) rules << :modify_group_member rules << :destroy_group_member diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 1c87db613ae..0d8365c4ff2 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -16,6 +16,7 @@ # default_branch_protection :integer default(2) # twitter_sharing_enabled :boolean default(TRUE) # restricted_visibility_levels :text +# max_attachment_size :integer default(10) # class ApplicationSetting < ActiveRecord::Base @@ -23,7 +24,7 @@ class ApplicationSetting < ActiveRecord::Base validates :home_page_url, allow_blank: true, - format: { with: URI::regexp(%w(http https)), message: "should be a valid url" }, + format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :home_page_url_column_exist validates_each :restricted_visibility_levels do |record, attr, value| @@ -49,7 +50,8 @@ class ApplicationSetting < ActiveRecord::Base twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'], gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], - restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'] + restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], + max_attachment_size: Settings.gitlab['max_attachment_size'] ) end diff --git a/app/models/commit.rb b/app/models/commit.rb index e0461809e10..006fa62c8f9 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -77,7 +77,7 @@ class Commit title_end = title.index("\n") if (!title_end && title.length > 100) || (title_end && title_end > 100) - title[0..79] << "…".html_safe + title[0..79] << "…" else title.split("\n", 2).first end @@ -90,7 +90,7 @@ class Commit title_end = safe_message.index("\n") @description ||= if (!title_end && safe_message.length > 100) || (title_end && title_end > 100) - "…".html_safe << safe_message[80..-1] + "…" << safe_message[80..-1] else safe_message.split("\n", 2)[1].try(:chomp) end @@ -117,8 +117,8 @@ class Commit # Discover issues should be closed when this commit is pushed to a project's # default branch. - def closes_issues(project) - Gitlab::ClosingIssueExtractor.closed_by_message_in_project(safe_message, project) + def closes_issues(project, current_user = self.committer) + Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message) end # Mentionable override. @@ -126,6 +126,14 @@ class Commit "commit #{id}" end + def author + User.find_for_commit(author_email, author_name) + end + + def committer + User.find_for_commit(committer_email, committer_name) + end + def method_missing(m, *args, &block) @raw.send(m, *args, &block) end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 88ac83744df..478134dff68 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -118,16 +118,16 @@ module Issuable end # Return all users participating on the discussion - def participants + def participants(current_user = self.author) users = [] users << author users << assignee if is_assigned? mentions = [] - mentions << self.mentioned_users + mentions << self.mentioned_users(current_user) notes.each do |note| users << note.author - mentions << note.mentioned_users + mentions << note.mentioned_users(current_user) end users.concat(mentions.reduce([], :|)).uniq @@ -140,7 +140,7 @@ module Issuable return subscription.subscribed end - participants.include?(user) + participants(user).include?(user) end def toggle_subscription(user) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 74900d4675d..b7882a2bb16 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -42,35 +42,22 @@ module Mentionable 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 identifier == "all" - users.push(*project.team.members.flatten) - elsif namespace = Namespace.find_by(path: identifier) - if namespace.type == "Group" - users.push(*namespace.users) - else - users << namespace.owner - end - end - end - users.uniq + def mentioned_users(current_user = nil) + return [] if mentionable_text.blank? + + ext = Gitlab::ReferenceExtractor.new(self.project, current_user) + ext.analyze(mentionable_text) + ext.users.uniq end # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. - def references(p = project, text = mentionable_text) + def references(p = project, current_user = self.author, text = mentionable_text) return [] if text.blank? - ext = Gitlab::ReferenceExtractor.new - ext.analyze(text, p) - (ext.issues_for(p) + - ext.merge_requests_for(p) + - ext.commits_for(p)).uniq - [local_reference] + ext = Gitlab::ReferenceExtractor.new(p, current_user) + ext.analyze(text) + + (ext.issues + ext.merge_requests + ext.commits).uniq - [local_reference] end # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. @@ -96,7 +83,7 @@ module Mentionable # Only proceed if the saved changes actually include a chance to an attr_mentionable field. return unless mentionable_changed - preexisting = references(p, original) + preexisting = references(p, self.author, original) create_cross_references!(p, a, preexisting) end end diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index 570f5e91c13..85d52d558cd 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -7,6 +7,7 @@ # created_at :datetime # updated_at :datetime # key :text +# public :boolean default(FALSE) # title :string(255) # type :string(255) # fingerprint :string(255) @@ -17,4 +18,21 @@ class DeployKey < Key has_many :projects, through: :deploy_keys_projects scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where('deploy_keys_projects.project_id in (?)', projects) } + scope :are_public, -> { where(public: true) } + + def private? + !public? + end + + def orphaned? + self.deploy_keys_projects.length == 0 + end + + def almost_orphaned? + self.deploy_keys_projects.length == 1 + end + + def destroyed_when_orphaned? + self.private? + end end diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb index 7e88903b9af..18db521741f 100644 --- a/app/models/deploy_keys_project.rb +++ b/app/models/deploy_keys_project.rb @@ -22,6 +22,8 @@ class DeployKeysProject < ActiveRecord::Base private def destroy_orphaned_deploy_key - self.deploy_key.destroy if self.deploy_key.deploy_keys_projects.length == 0 + return unless self.deploy_key.destroyed_when_orphaned? && self.deploy_key.orphaned? + + self.deploy_key.destroy end end diff --git a/app/models/event.rb b/app/models/event.rb index 57f6d5cd4e0..c9a88ffa8e0 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -183,7 +183,11 @@ class Event < ActiveRecord::Base elsif commented? "commented on" elsif created_project? - "created" + if project.import? + "imported" + else + "created" + end else "opened" end diff --git a/app/models/group.rb b/app/models/group.rb index da9621a2a1a..1386a9eccc9 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -46,19 +46,18 @@ class Group < Namespace @owners ||= group_members.owners.map(&:user) end - def add_users(user_ids, access_level) - user_ids.compact.each do |user_id| - user = self.group_members.find_or_initialize_by(user_id: user_id) - user.update_attributes(access_level: access_level) + def add_users(user_ids, access_level, current_user = nil) + user_ids.each do |user_id| + Member.add_user(self.group_members, user_id, access_level, current_user) end end - def add_user(user, access_level) - self.group_members.create(user_id: user.id, access_level: access_level) + def add_user(user, access_level, current_user = nil) + add_users([user], access_level, current_user) end - def add_owner(user) - self.add_user(user, Gitlab::Access::OWNER) + def add_owner(user, current_user = nil) + self.add_user(user, Gitlab::Access::OWNER, current_user) end def has_owner?(user) diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index defef7216f2..315d96af1b9 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -28,7 +28,7 @@ class WebHook < ActiveRecord::Base default_timeout Gitlab.config.gitlab.webhook_timeout validates :url, presence: true, - format: { with: URI::regexp(%w(http https)), message: "should be a valid url" } + format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" } def execute(data) parsed_url = URI.parse(url) diff --git a/app/models/identity.rb b/app/models/identity.rb index 440fcd0d052..756d19adec7 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -15,4 +15,5 @@ class Identity < ActiveRecord::Base belongs_to :user validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider } + validates :user_id, uniqueness: { scope: :provider } end diff --git a/app/models/key.rb b/app/models/key.rb index e2e59296eed..016eee86992 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -16,7 +16,6 @@ require 'digest/md5' class Key < ActiveRecord::Base include Sortable - include Gitlab::Popen belongs_to :user @@ -79,20 +78,9 @@ class Key < ActiveRecord::Base def generate_fingerprint 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(%W(ssh-keygen -lf #{file.path}), '/tmp') - end - - if cmd_status.zero? - cmd_output.gsub /(\h{2}:)+\h{2}/ do |match| - self.fingerprint = match - end - end + + return unless self.key.present? + + self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint end end diff --git a/app/models/member.rb b/app/models/member.rb index fe3d2f40e87..d151c7b2390 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -11,6 +11,10 @@ # type :string(255) # created_at :datetime # updated_at :datetime +# created_by_id :integer +# invite_email :string +# invite_token :string +# invite_accepted_at :datetime # class Member < ActiveRecord::Base @@ -18,19 +22,151 @@ class Member < ActiveRecord::Base include Notifiable include Gitlab::Access + attr_accessor :raw_invite_token + + belongs_to :created_by, class_name: "User" belongs_to :user belongs_to :source, polymorphic: true - validates :user, presence: true + validates :user, presence: true, unless: :invite? validates :source, presence: true - validates :user_id, uniqueness: { scope: [:source_type, :source_id], message: "already exists in source" } + validates :user_id, uniqueness: { scope: [:source_type, :source_id], + message: "already exists in source", + allow_nil: true } validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true + validates :invite_email, presence: { if: :invite? }, + email: { strict_mode: true, allow_nil: true }, + uniqueness: { scope: [:source_type, :source_id], allow_nil: true } + scope :invite, -> { where(user_id: nil) } + scope :non_invite, -> { where("user_id IS NOT NULL") } scope :guests, -> { where(access_level: GUEST) } scope :reporters, -> { where(access_level: REPORTER) } scope :developers, -> { where(access_level: DEVELOPER) } scope :masters, -> { where(access_level: MASTER) } scope :owners, -> { where(access_level: OWNER) } + before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } + after_create :send_invite, if: :invite? + after_create :post_create_hook, unless: :invite? + after_update :post_update_hook, unless: :invite? + after_destroy :post_destroy_hook, unless: :invite? + delegate :name, :username, :email, to: :user, prefix: true + + class << self + def find_by_invite_token(invite_token) + invite_token = Devise.token_generator.digest(self, :invite_token, invite_token) + find_by(invite_token: invite_token) + end + + # This method is used to find users that have been entered into the "Add members" field. + # These can be the User objects directly, their IDs, their emails, or new emails to be invited. + def user_for_id(user_id) + return user_id if user_id.is_a?(User) + + user = User.find_by(id: user_id) + user ||= User.find_by(email: user_id) + user ||= user_id + user + end + + def add_user(members, user_id, access_level, current_user = nil) + user = user_for_id(user_id) + + # `user` can be either a User object or an email to be invited + if user.is_a?(User) + member = members.find_or_initialize_by(user_id: user.id) + else + member = members.build + member.invite_email = user + end + + member.created_by ||= current_user + member.access_level = access_level + + member.save + end + end + + def invite? + self.invite_token.present? + end + + def accept_invite!(new_user) + return false unless invite? + + self.invite_token = nil + self.invite_accepted_at = Time.now.utc + + self.user = new_user + + saved = self.save + + after_accept_invite if saved + + saved + end + + def decline_invite! + return false unless invite? + + destroyed = self.destroy + + after_decline_invite if destroyed + + destroyed + end + + def generate_invite_token + raw, enc = Devise.token_generator.generate(self.class, :invite_token) + @raw_invite_token = raw + self.invite_token = enc + end + + def generate_invite_token! + generate_invite_token && save(validate: false) + end + + def resend_invite + return unless invite? + + generate_invite_token! unless @raw_invite_token + + send_invite + end + + private + + def send_invite + # override in subclass + end + + def post_create_hook + system_hook_service.execute_hooks_for(self, :create) + end + + def post_update_hook + # override in subclass + end + + def post_destroy_hook + system_hook_service.execute_hooks_for(self, :destroy) + end + + def after_accept_invite + post_create_hook + end + + def after_decline_invite + # override in subclass + end + + def system_hook_service + SystemHooksService.new + end + + def notification_service + NotificationService.new + end end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 28d0b4483b4..84c91372b3f 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -27,10 +27,6 @@ class GroupMember < Member scope :with_group, ->(group) { where(source_id: group.id) } scope :with_user, ->(user) { where(user_id: user.id) } - after_create :post_create_hook - after_update :notify_update - after_destroy :post_destroy_hook - def self.access_level_roles Gitlab::Access.options_with_owner end @@ -43,26 +39,37 @@ class GroupMember < Member access_level end + private + + def send_invite + notification_service.invite_group_member(self, @raw_invite_token) + + super + end + def post_create_hook notification_service.new_group_member(self) - system_hook_service.execute_hooks_for(self, :create) + + super end - def notify_update + def post_update_hook if access_level_changed? notification_service.update_group_member(self) end - end - def post_destroy_hook - system_hook_service.execute_hooks_for(self, :destroy) + super end - def system_hook_service - SystemHooksService.new + def after_accept_invite + notification_service.accept_group_invite(self) + + super end - def notification_service - NotificationService.new + def after_decline_invite + notification_service.decline_group_invite(self) + + super end end diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 6b13e0ff30b..0a3b4d2182b 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -27,10 +27,6 @@ class ProjectMember < Member validates_format_of :source_type, with: /\AProject\z/ default_scope { where(source_type: SOURCE_TYPE) } - after_create :post_create_hook - after_update :post_update_hook - after_destroy :post_destroy_hook - scope :in_project, ->(project) { where(source_id: project.id) } scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) } scope :with_user, ->(user) { where(user_id: user.id) } @@ -55,7 +51,7 @@ class ProjectMember < Member # :master # ) # - def add_users_into_projects(project_ids, user_ids, access) + def add_users_into_projects(project_ids, user_ids, access, current_user = nil) access_level = if roles_hash.has_key?(access) roles_hash[access] elsif roles_hash.values.include?(access.to_i) @@ -64,12 +60,14 @@ class ProjectMember < Member raise "Non valid access" end + users = user_ids.map { |user_id| Member.user_for_id(user_id) } + ProjectMember.transaction do project_ids.each do |project_id| - user_ids.each do |user_id| - member = ProjectMember.new(access_level: access_level, user_id: user_id) - member.source_id = project_id - member.save + project = Project.find(project_id) + + users.each do |user| + Member.add_user(project.project_members, user, access_level, current_user) end end end @@ -82,6 +80,7 @@ class ProjectMember < Member def truncate_teams(project_ids) ProjectMember.transaction do members = ProjectMember.where(source_id: project_ids) + members.each do |member| member.destroy end @@ -109,41 +108,58 @@ class ProjectMember < Member access_level end + def project + source + end + def owner? project.owner == user end + private + + def send_invite + notification_service.invite_project_member(self, @raw_invite_token) + + super + end + def post_create_hook unless owner? event_service.join_project(self.project, self.user) notification_service.new_project_member(self) end - system_hook_service.execute_hooks_for(self, :create) + super end def post_update_hook - notification_service.update_project_member(self) if self.access_level_changed? + if access_level_changed? + notification_service.update_project_member(self) + end + + super end def post_destroy_hook event_service.leave_project(self.project, self.user) - system_hook_service.execute_hooks_for(self, :destroy) - end - def event_service - EventCreateService.new + super end - def notification_service - NotificationService.new + def after_accept_invite + notification_service.accept_project_invite(self) + + super end - def system_hook_service - SystemHooksService.new + def after_decline_invite + notification_service.decline_project_invite(self) + + super end - def project - source + def event_service + EventCreateService.new end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 5634f9a686e..9c9e2762507 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -257,11 +257,11 @@ class MergeRequest < ActiveRecord::Base end # Return the set of issues that will be closed if this merge request is accepted. - def closes_issues + def closes_issues(current_user = self.author) if target_branch == project.default_branch - issues = commits.flat_map { |c| c.closes_issues(project) } - issues.push(*Gitlab::ClosingIssueExtractor. - closed_by_message_in_project(description, project)) + issues = commits.flat_map { |c| c.closes_issues(project, current_user) } + issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user). + closed_by_message(description)) issues.uniq.sort_by(&:id) else [] @@ -361,6 +361,8 @@ class MergeRequest < ActiveRecord::Base end def locked_long_ago? - locked_at && locked_at < (Time.now - 1.day) + return false unless locked? + + locked_at.nil? || locked_at < (Time.now - 1.day) end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 35280889a86..e1de114375e 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -24,8 +24,8 @@ class Namespace < ActiveRecord::Base validates :name, presence: true, uniqueness: true, length: { within: 0..255 }, - format: { with: Gitlab::Regex.name_regex, - message: Gitlab::Regex.name_regex_message } + format: { with: Gitlab::Regex.namespace_name_regex, + message: Gitlab::Regex.namespace_name_regex_message } validates :description, length: { within: 0..255 } validates :path, @@ -33,8 +33,8 @@ class Namespace < ActiveRecord::Base presence: true, length: { within: 1..255 }, exclusion: { in: Gitlab::Blacklist.path }, - format: { with: Gitlab::Regex.path_regex, - message: Gitlab::Regex.path_regex_message } + format: { with: Gitlab::Regex.namespace_regex, + message: Gitlab::Regex.namespace_regex_message } delegate :name, to: :owner, allow_nil: true, prefix: true @@ -44,21 +44,37 @@ class Namespace < ActiveRecord::Base scope :root, -> { where('type IS NULL') } - def self.by_path(path) - where('lower(path) = :value', value: path.downcase).first - end + class << self + def by_path(path) + where('lower(path) = :value', value: path.downcase).first + end - # Case insensetive search for namespace by path or name - def self.find_by_path_or_name(path) - find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) - end + # Case insensetive search for namespace by path or name + def find_by_path_or_name(path) + find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) + end - def self.search(query) - where("name LIKE :query OR path LIKE :query", query: "%#{query}%") - end + def search(query) + where("name LIKE :query OR path LIKE :query", query: "%#{query}%") + end + + def clean_path(path) + path = path.dup + path.gsub!(/@.*\z/, "") + path.gsub!(/\.git\z/, "") + path.gsub!(/\A-+/, "") + path.gsub!(/\.+\z/, "") + path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + + counter = 0 + base = path + while Namespace.by_path(path).present? + counter += 1 + path = "#{base}#{counter}" + end - def self.global_id - 'GLN' + path + end end def to_param diff --git a/app/models/note.rb b/app/models/note.rb index e86160e7cd9..2cf3fac2def 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -22,6 +22,7 @@ require 'file_size_validator' class Note < ActiveRecord::Base include Mentionable + include Gitlab::CurrentSettings default_value_for :system, false @@ -36,7 +37,8 @@ class Note < ActiveRecord::Base validates :note, :project, presence: true validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true - validates :attachment, file_size: { maximum: 10.megabytes.to_i } + # Attachments are deprecated and are handled by Markdown uploader + validates :attachment, file_size: { maximum: :max_attachment_size } validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' } validates :commit_id, presence: true, if: ->(n) { n.noteable_type == 'Commit' } @@ -321,6 +323,10 @@ class Note < ActiveRecord::Base end end + def max_attachment_size + current_application_settings.max_attachment_size.megabytes.to_i + end + def commit_author @commit_author ||= project.team.users.find_by(email: noteable.author_email) || @@ -348,7 +354,7 @@ class Note < ActiveRecord::Base 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 = diff_for_line_code unless for_merge_request? diff ||= find_diff self.st_diff = diff.to_hash if diff @@ -358,6 +364,10 @@ class Note < ActiveRecord::Base @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) end + def diff_for_line_code + Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff) + end + # Check if such line of code exists in merge request diff # If exists - its active discussion # If not - its outdated diff @@ -451,7 +461,7 @@ class Note < ActiveRecord::Base prev_match_line = line else prev_lines << line - + break if generate_line_code(line) == self.line_code prev_lines.shift if prev_lines.length >= max_number_of_lines diff --git a/app/models/project.rb b/app/models/project.rb index c50b8a12621..64ee2c2212b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -81,7 +81,7 @@ class Project < ActiveRecord::Base has_one :asana_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy has_one :slack_service, dependent: :destroy - has_one :buildbox_service, dependent: :destroy + has_one :buildkite_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy has_one :teamcity_service, dependent: :destroy has_one :pushover_service, dependent: :destroy @@ -114,6 +114,8 @@ class Project < ActiveRecord::Base has_many :users_star_projects, dependent: :destroy has_many :starrers, through: :users_star_projects, source: :user + has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" + delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true @@ -124,12 +126,12 @@ class Project < ActiveRecord::Base presence: true, length: { within: 0..255 }, format: { with: Gitlab::Regex.project_name_regex, - message: Gitlab::Regex.project_regex_message } + message: Gitlab::Regex.project_name_regex_message } validates :path, presence: true, length: { within: 0..255 }, - format: { with: Gitlab::Regex.path_regex, - message: Gitlab::Regex.path_regex_message } + format: { with: Gitlab::Regex.project_path_regex, + message: Gitlab::Regex.project_path_regex_message } validates :issues_enabled, :merge_requests_enabled, :wiki_enabled, inclusion: { in: [true, false] } validates :issues_tracker_id, length: { maximum: 255 }, allow_blank: true @@ -137,7 +139,7 @@ class Project < ActiveRecord::Base validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :path, scope: :namespace_id validates :import_url, - format: { with: URI::regexp(%w(ssh git http https)), message: 'should be a valid url' }, + format: { with: /\A#{URI.regexp(%w(ssh git http https))}\z/, message: 'should be a valid url' }, if: :import? validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create @@ -185,6 +187,7 @@ class Project < ActiveRecord::Base state :failed after_transition any => :started, do: :add_import_job + after_transition any => :finished, do: :clear_import_data end class << self @@ -262,6 +265,10 @@ class Project < ActiveRecord::Base RepositoryImportWorker.perform_in(2.seconds, id) end + def clear_import_data + self.import_data.destroy if self.import_data + end + def import? import_url.present? end diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb new file mode 100644 index 00000000000..6a8a8a56eb5 --- /dev/null +++ b/app/models/project_import_data.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: project_import_datas +# +# id :integer not null, primary key +# project_id :integer +# data :text +# + +require 'carrierwave/orm/activerecord' +require 'file_size_validator' + +class ProjectImportData < ActiveRecord::Base + belongs_to :project + + serialize :data, JSON + + validates :project, presence: true +end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index f968afe9fa8..d8aedbd2ab4 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -25,7 +25,7 @@ class BambooService < CiService validates :bamboo_url, presence: true, - format: { with: URI::regexp }, + format: { with: /\A#{URI.regexp}\z/ }, if: :activated? validates :build_key, presence: true, if: :activated? validates :username, diff --git a/app/models/project_services/buildbox_service.rb b/app/models/project_services/buildkite_service.rb index 3a381ff11b8..a714bc82246 100644 --- a/app/models/project_services/buildbox_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -20,9 +20,7 @@ require "addressable/uri" -# Buildbox renamed to Buildkite, but for backwards compatability with the STI -# of Services, the class name is kept as "Buildbox" -class BuildboxService < CiService +class BuildkiteService < CiService ENDPOINT = "https://buildkite.com" prop_accessor :project_url, :token diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index e521186798c..a199d0e86f2 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -18,7 +18,7 @@ class ExternalWikiService < Service prop_accessor :external_wiki_url validates :external_wiki_url, presence: true, - format: { with: URI::regexp }, + format: { with: /\A#{URI.regexp}\z/ }, if: :activated? def title diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index edaeeffc228..0f9838a575d 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -18,6 +18,8 @@ # class GitlabCiService < CiService + API_PREFIX = "api/v1" + prop_accessor :project_url, :token validates :project_url, presence: true, if: :activated? validates :token, presence: true, if: :activated? @@ -59,6 +61,26 @@ class GitlabCiService < CiService end end + def fork_registration(new_project, private_token) + params = { + id: new_project.id, + name_with_namespace: new_project.name_with_namespace, + web_url: new_project.web_url, + default_branch: new_project.default_branch, + ssh_url_to_repo: new_project.ssh_url_to_repo + } + + HTTParty.post( + fork_registration_path, + body: { + project_id: project.id, + project_token: token, + private_token: private_token, + data: params }, + verify: false + ) + end + def commit_coverage(sha, ref) response = get_ci_build(sha, ref) @@ -97,4 +119,10 @@ class GitlabCiService < CiService { type: 'text', name: 'project_url', placeholder: 'http://ci.gitlabhq.com/projects/3' } ] end + + private + + def fork_registration_path + project_url.sub(/projects\/\d*/, "#{API_PREFIX}/forks") + end end diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 84346350a6c..5f0553f3b0b 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -20,8 +20,13 @@ class GitlabIssueTrackerService < IssueTrackerService include Rails.application.routes.url_helpers - prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + default_url_options[:host] = Gitlab.config.gitlab.host + default_url_options[:protocol] = Gitlab.config.gitlab.protocol + default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port? + default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root + + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url def default? true @@ -32,20 +37,26 @@ class GitlabIssueTrackerService < IssueTrackerService end def project_url - "#{gitlab_url}#{namespace_project_issues_path(project.namespace, project)}" + namespace_project_issues_url(project.namespace, project) end def new_issue_url - "#{gitlab_url}#{new_namespace_project_issue_path(namespace_id: project.namespace, project_id: project)}" + new_namespace_project_issue_url(namespace_id: project.namespace, project_id: project) end def issue_url(iid) - "#{gitlab_url}#{namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: iid)}" + namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: iid) end - private + def project_path + namespace_project_issues_path(project.namespace, project) + end + + def new_issue_path + new_namespace_project_issue_path(namespace_id: project.namespace, project_id: project) + end - def gitlab_url - Gitlab.config.gitlab.relative_url_root.chomp("/") if Gitlab.config.gitlab.relative_url_root + def issue_path(iid) + namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: iid) end end diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 2bddb7b881c..e9e1e276e7d 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -148,7 +148,7 @@ class IrkerService < Service def consider_uri(uri) # Authorize both irc://domain.com/#chan and irc://domain.com/chan - if uri.is_a?(URI) && uri.scheme[/^ircs?$/] && !uri.path.nil? + if uri.is_a?(URI) && uri.scheme[/^ircs?\z/] && !uri.path.nil? # Do not authorize irc://domain.com/ if uri.fragment.nil? && uri.path.length > 1 uri.to_s diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 8e90c44d103..c8ab9d63b74 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -34,6 +34,18 @@ class IssueTrackerService < Service self.issues_url.gsub(':id', iid.to_s) end + def project_path + project_url + end + + def new_issue_path + new_issue_url + end + + def issue_path(iid) + issue_url(iid) + end + def fields [ { type: 'text', name: 'description', placeholder: description }, diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index c26bc551352..3c002a1634b 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -25,7 +25,7 @@ class TeamcityService < CiService validates :teamcity_url, presence: true, - format: { with: URI::regexp }, if: :activated? + format: { with: /\A#{URI.regexp}\z/ }, if: :activated? validates :build_type, presence: true, if: :activated? validates :username, presence: true, diff --git a/app/models/project_team.rb b/app/models/project_team.rb index d4a07caf9ef..56e49af2324 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -12,12 +12,12 @@ class ProjectTeam # @team << [@users, :master] # def <<(args) - users = args.first + users, access, current_user = *args if users.respond_to?(:each) - add_users(users, args.second) + add_users(users, access, current_user) else - add_user(users, args.second) + add_user(users, access, current_user) end end @@ -43,22 +43,19 @@ class ProjectTeam member end - def add_user(user, access) - add_users_ids([user.id], access) - end - - def add_users(users, access) - add_users_ids(users.map(&:id), access) - end - - def add_users_ids(user_ids, access) + def add_users(users, access, current_user = nil) ProjectMember.add_users_into_projects( [project.id], - user_ids, - access + users, + access, + current_user ) end + def add_user(user, access, current_user = nil) + add_users([user], access, current_user) + end + # Remove all users from project team def truncate ProjectMember.truncate_team(project) @@ -88,7 +85,7 @@ class ProjectTeam @masters ||= fetch_members(:masters) end - def import(source_project) + def import(source_project, current_user = nil) target_project = project source_members = source_project.project_members.to_a @@ -96,13 +93,14 @@ class ProjectTeam source_members.reject! do |member| # Skip if user already present in team - target_user_ids.include?(member.user_id) + !member.invite? && target_user_ids.include?(member.user_id) end source_members.map! do |member| new_member = member.dup new_member.id = nil new_member.source = target_project + new_member.created_by = current_user new_member end diff --git a/app/models/repository.rb b/app/models/repository.rb index 77765cae1a0..263a436d521 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -199,7 +199,7 @@ class Repository def changelog cache.fetch(:changelog) do tree(:head).blobs.find do |file| - file.name =~ /^(changelog|history)/i + file.name =~ /\A(changelog|history)/i end end end @@ -207,7 +207,7 @@ class Repository def license cache.fetch(:license) do tree(:head).blobs.find do |file| - file.name =~ /^license/i + file.name =~ /\Alicense/i end end end @@ -267,6 +267,9 @@ class Repository # Remove archives older than 2 hours def clean_old_archives repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path + + return unless File.directory?(repository_downloads_path) + Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete)) end diff --git a/app/models/service.rb b/app/models/service.rb index f54ad19666b..393cf55a69f 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -122,25 +122,25 @@ class Service < ActiveRecord::Base def self.available_services_names %w( - gitlab_ci - campfire - hipchat - pivotaltracker - flowdock - assembla asana + assembla + bamboo + buildkite + campfire + custom_issue_tracker emails_on_push + external_wiki + flowdock gemnasium - slack - pushover - buildbox - bamboo - teamcity + gitlab_ci + hipchat + irker jira + pivotaltracker + pushover redmine - custom_issue_tracker - irker - external_wiki + slack + teamcity ) end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 3fb2ec1d66c..b35e72c4bdb 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -33,8 +33,8 @@ class Snippet < ActiveRecord::Base validates :file_name, presence: true, length: { within: 0..255 }, - format: { with: Gitlab::Regex.path_regex, - message: Gitlab::Regex.path_regex_message } + format: { with: Gitlab::Regex.file_name_regex, + message: Gitlab::Regex.file_name_regex_message } validates :content, presence: true validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values } diff --git a/app/models/user.rb b/app/models/user.rb index 979150b4d68..d6b93afe739 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -49,6 +49,7 @@ # password_automatically_set :boolean default(FALSE) # bitbucket_access_token :string(255) # bitbucket_access_token_secret :string(255) +# public_email :string(255) default(""), not null # require 'carrierwave/orm/activerecord' @@ -123,14 +124,15 @@ class User < ActiveRecord::Base validates :name, presence: true validates :email, presence: true, email: { strict_mode: true }, uniqueness: true validates :notification_email, presence: true, email: { strict_mode: true } + validates :public_email, presence: true, email: { strict_mode: true }, allow_blank: true, uniqueness: true validates :bio, length: { maximum: 255 }, allow_blank: true validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :username, presence: true, uniqueness: { case_sensitive: false }, exclusion: { in: Gitlab::Blacklist.path }, - format: { with: Gitlab::Regex.username_regex, - message: Gitlab::Regex.username_regex_message } + format: { with: Gitlab::Regex.namespace_regex, + message: Gitlab::Regex.namespace_regex_message } validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true validate :namespace_uniq, if: ->(user) { user.username_changed? } @@ -142,6 +144,7 @@ class User < ActiveRecord::Base before_validation :generate_password, on: :create before_validation :sanitize_attrs before_validation :set_notification_email, if: ->(user) { user.email_changed? } + before_validation :set_public_email, if: ->(user) { user.public_email_changed? } before_save :ensure_authentication_token after_save :ensure_namespace_correct @@ -229,22 +232,6 @@ class User < ActiveRecord::Base def build_user(attrs = {}) User.new(attrs) end - - def clean_username(username) - username.gsub!(/@.*\z/, "") - username.gsub!(/\.git\z/, "") - username.gsub!(/\A-/, "") - username.gsub!(/[^a-zA-Z0-9_\-\.]/, "") - - counter = 0 - base = username - while User.by_login(username).present? || Namespace.by_path(username).present? - counter += 1 - username = "#{base}#{counter}" - end - - username - end end # @@ -430,8 +417,16 @@ class User < ActiveRecord::Base @ldap_identity ||= identities.find_by(["provider LIKE ?", "ldap%"]) end + def project_deploy_keys + DeployKey.in_projects(self.authorized_projects.pluck(:id)) + end + def accessible_deploy_keys - DeployKey.in_projects(self.authorized_projects.pluck(:id)).uniq + @accessible_deploy_keys ||= begin + key_ids = project_deploy_keys.pluck(:id) + key_ids.push(*DeployKey.are_public.pluck(:id)) + DeployKey.where(id: key_ids) + end end def created_by @@ -451,6 +446,12 @@ class User < ActiveRecord::Base end end + def set_public_email + if self.public_email.blank? || !self.all_emails.include?(self.public_email) + self.public_email = '' + end + end + def set_projects_limit connection_default_value_defined = new_record? && !projects_limit_changed? return unless self.projects_limit.nil? || connection_default_value_defined @@ -502,13 +503,13 @@ class User < ActiveRecord::Base end def full_website_url - return "http://#{website_url}" if website_url !~ /^https?:\/\// + return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\// website_url end def short_website_url - website_url.gsub(/https?:\/\//, '') + website_url.sub(/\Ahttps?:\/\//, '') end def all_ssh_keys diff --git a/app/services/archive_repository_service.rb b/app/services/archive_repository_service.rb index 8823f6fdc67..e1b41527d8d 100644 --- a/app/services/archive_repository_service.rb +++ b/app/services/archive_repository_service.rb @@ -1,14 +1,62 @@ class ArchiveRepositoryService - def execute(project, ref, format) - storage_path = Gitlab.config.gitlab.repository_downloads_path + attr_reader :project, :ref, :format - unless File.directory?(storage_path) - FileUtils.mkdir_p(storage_path) + def initialize(project, ref, format) + format ||= 'tar.gz' + @project, @ref, @format = project, ref, format.downcase + end + + def execute(options = {}) + project.repository.clean_old_archives + + raise "No archive file path" unless file_path + + return file_path if archived? + + unless archiving? + RepositoryArchiveWorker.perform_async(project.id, ref, format) end - format ||= 'tar.gz' - repository = project.repository - repository.clean_old_archives - repository.archive_repo(ref, storage_path, format.downcase) + archived = wait_until_archived(options[:timeout] || 5.0) + + file_path if archived + end + + private + + def storage_path + Gitlab.config.gitlab.repository_downloads_path + end + + def file_path + @file_path ||= project.repository.archive_file_path(ref, storage_path, format) + end + + def pid_file_path + @pid_file_path ||= project.repository.archive_pid_file_path(ref, storage_path, format) + end + + def archived? + File.exist?(file_path) + end + + def archiving? + File.exist?(pid_file_path) + end + + def wait_until_archived(timeout = 5.0) + return archived? if timeout == 0.0 + + t1 = Time.now + + begin + sleep 0.1 + + success = archived? + + t2 = Time.now + end until success || t2 - t1 >= timeout + + success end end diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index 4115d689925..25f9e203246 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -13,9 +13,7 @@ class CreateTagService < BaseService return error('Tag already exists') end - if message - message.gsub!(/^\s+|\s+$/, '') - end + message.strip! if message repository.add_tag(tag_name, ref, message) new_tag = repository.find_tag(tag_name) diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index eeafefc25af..23833aa78ec 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -12,10 +12,10 @@ module Files file_name = File.basename(path) file_path = path - unless file_name =~ Gitlab::Regex.path_regex + unless file_name =~ Gitlab::Regex.file_name_regex return error( 'Your changes could not be committed, because the file name ' + - Gitlab::Regex.path_regex_message + Gitlab::Regex.file_name_regex_message ) end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 1f0b29dff5e..31e0167d247 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -70,7 +70,7 @@ class GitPushService # 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) + issues_to_close = commit.closes_issues(project, user) # Load commit author only if needed. # For push with 1k commits it prevents 900+ requests in database @@ -87,7 +87,7 @@ class GitPushService # 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 = commit.references(project, user) - issues_to_close refs.reject! { |r| commit.has_mentioned?(r) } if refs.present? @@ -127,6 +127,6 @@ class GitPushService end def commit_user(commit) - User.find_for_commit(commit.author_email, commit.author_name) || user + commit.author || user end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 3371fe7d5ef..8f04a69287a 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -14,8 +14,8 @@ module Issues issue.update_nth_task(params[:task_num].to_i, false) end - params[:assignee_id] = "" if params[:assignee_id] == "-1" - params[:milestone_id] = "" if params[:milestone_id] == "-1" + params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE + params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE old_labels = issue.labels.to_a diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 0ac6dfea6fd..23af2656c37 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -23,8 +23,8 @@ module MergeRequests merge_request.update_nth_task(params[:task_num].to_i, false) end - params[:assignee_id] = "" if params[:assignee_id] == "-1" - params[:milestone_id] = "" if params[:milestone_id] == "-1" + params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE + params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE old_labels = merge_request.labels.to_a diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index cc5853144c5..cfed7964c37 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -123,30 +123,30 @@ class NotificationService return true if note.note.start_with?('Status changed to closed') return true if note.cross_reference? && note.system == true - opts = { noteable_type: note.noteable_type, project_id: note.project_id } - target = note.noteable - if target.respond_to?(:participants) - recipients = target.participants - else - recipients = note.mentioned_users - end + recipients = [] 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))) + + # Add all users participating in the thread (author, assignee, comment authors) + participants = + if target.respond_to?(:participants) + target.participants + elsif target.is_a?(Commit) + author_ids = Note.for_commit_id(target.id).pluck(:author_id).uniq + User.where(id: author_ids) + else + note.mentioned_users + end + recipients = recipients.concat(participants) # Merge project watchers recipients = recipients.concat(project_watchers(note.project)).compact.uniq - # Reject mention users unless mentioned in comment + # Reject users with Mention notification level, except those mentioned in _this_ note. recipients = reject_mention_users(recipients - note.mentioned_users, note.project) recipients = recipients + note.mentioned_users @@ -168,6 +168,18 @@ class NotificationService end end + def invite_project_member(project_member, token) + mailer.project_member_invited_email(project_member.id, token) + end + + def accept_project_invite(project_member) + mailer.project_invite_accepted_email(project_member.id) + end + + def decline_project_invite(project_member) + mailer.project_invite_declined_email(project_member.project.id, project_member.invite_email, project_member.access_level, project_member.created_by_id) + end + def new_project_member(project_member) mailer.project_access_granted_email(project_member.id) end @@ -176,6 +188,18 @@ class NotificationService mailer.project_access_granted_email(project_member.id) end + def invite_group_member(group_member, token) + mailer.group_member_invited_email(group_member.id, token) + end + + def accept_group_invite(group_member) + mailer.group_invite_accepted_email(group_member.id) + end + + def decline_group_invite(group_member) + mailer.group_invite_declined_email(group_member.group.id, group_member.invite_email, group_member.access_level, group_member.created_by_id) + end + def new_group_member(group_member) mailer.group_access_granted_email(group_member.id) end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 7ffd0b3882a..a7afcf8f64b 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -83,7 +83,7 @@ module Projects system_hook_service.execute_hooks_for(@project, :create) unless @project.group - @project.team << [current_user, :master] + @project.team << [current_user, :master, current_user] end @project.update_column(:last_activity_at, @project.created_at) diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 6b0d4aca3e1..1e4deb6ed39 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -38,14 +38,20 @@ module Projects #First save the DB entries as they can be rolled back if the repo fork fails project.build_forked_project_link(forked_to_project_id: project.id, forked_from_project_id: @from_project.id) if project.save - project.team << [@current_user, :master] + project.team << [@current_user, :master, @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 + + if @from_project.gitlab_ci? + ForkRegistrationWorker.perform_async(@from_project.id, project.id, @current_user.private_token) + end rescue => ex project.errors.add(:base, 'Fork transaction failed.') project.destroy diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index bcbacbff562..ae6260bcdab 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -1,10 +1,5 @@ module Projects class ParticipantsService < BaseService - def initialize(project, user) - @project = project - @user = user - end - def execute(note_type, note_id) participating = if note_type && note_id @@ -12,7 +7,7 @@ module Projects else [] end - project_members = sorted(@project.team.members) + project_members = sorted(project.team.members) participants = all_members + groups + project_members + participating participants.uniq end @@ -20,11 +15,11 @@ module Projects def participants_in(type, id) users = case type when "Issue" - issue = @project.issues.find_by_iid(id) - issue ? issue.participants : [] + issue = project.issues.find_by_iid(id) + issue ? issue.participants(current_user) : [] when "MergeRequest" - merge_request = @project.merge_requests.find_by_iid(id) - merge_request ? merge_request.participants : [] + merge_request = project.merge_requests.find_by_iid(id) + merge_request ? merge_request.participants(current_user) : [] when "Commit" author_ids = Note.for_commit_id(id).pluck(:author_id).uniq User.where(id: author_ids) @@ -41,14 +36,14 @@ module Projects end def groups - @user.authorized_groups.sort_by(&:path).map do |group| + current_user.authorized_groups.sort_by(&:path).map do |group| count = group.users.count { username: group.path, name: "#{group.name} (#{count})" } end end def all_members - count = @project.team.members.flatten.count + count = project.team.members.flatten.count [{ username: "all", name: "All Project and Group Members (#{count})" }] end end diff --git a/app/services/projects/upload_service.rb b/app/services/projects/upload_service.rb index a186c97628f..992a7a7a1dc 100644 --- a/app/services/projects/upload_service.rb +++ b/app/services/projects/upload_service.rb @@ -5,7 +5,7 @@ module Projects end def execute - return nil unless @file + return nil unless @file and @file.size <= max_attachment_size uploader = FileUploader.new(@project) uploader.store!(@file) @@ -18,5 +18,11 @@ module Projects 'is_image' => uploader.image? } end + + private + + def max_attachment_size + current_application_settings.max_attachment_size.megabytes.to_i + end end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index edfcccfcf4c..4f3565c67eb 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -60,5 +60,10 @@ .col-sm-10 = f.text_area :sign_in_text, class: 'form-control', rows: 4 .help-block Markdown enabled + .form-group + = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :max_attachment_size, class: 'form-control' + .form-actions = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index b77d188a38d..fa4e6335c73 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -1,13 +1,15 @@ = form_for [:admin, @application], url: @url, html: {class: 'form-horizontal', role: 'form'} do |f| - if application.errors.any? - .alert.alert-danger{"data-alert" => ""} - %p Whoops! Check your form for possible errors - = content_tag :div, class: "form-group#{' has-error' if application.errors[:name].present?}" do + .alert.alert-danger + %button{ type: "button", class: "close", "data-dismiss" => "alert"} × + - application.errors.full_messages.each do |msg| + %p= msg + = content_tag :div, class: 'form-group' do = f.label :name, class: 'col-sm-2 control-label' .col-sm-10 = f.text_field :name, class: 'form-control' = doorkeeper_errors_for application, :name - = content_tag :div, class: "form-group#{' has-error' if application.errors[:redirect_uri].present?}" do + = content_tag :div, class: 'form-group' do = f.label :redirect_uri, class: 'col-sm-2 control-label' .col-sm-10 = f.text_area :redirect_uri, class: 'form-control' diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index 8db2b2a709c..4ef8e878a7f 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -41,4 +41,4 @@ .panel.panel-default - %iframe{src: sidekiq_path, width: '100%', height: 900, style: "border: none"} + %iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"} diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml new file mode 100644 index 00000000000..2ae83ab95f7 --- /dev/null +++ b/app/views/admin/deploy_keys/index.html.haml @@ -0,0 +1,27 @@ +.panel.panel-default + .panel-heading + Public deploy keys (#{@deploy_keys.count}) + .panel-head-actions + = link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm" + - if @deploy_keys.any? + %table.table + %thead.panel-heading + %tr + %th Title + %th Fingerprint + %th Added at + %th + %tbody + - @deploy_keys.each do |deploy_key| + %tr + %td + = link_to admin_deploy_key_path(deploy_key) do + %strong= deploy_key.title + %td + %span + (#{deploy_key.fingerprint}) + %td + %span.cgray + added #{time_ago_with_tooltip(deploy_key.created_at)} + %td + = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right" diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml new file mode 100644 index 00000000000..c00049424c5 --- /dev/null +++ b/app/views/admin/deploy_keys/new.html.haml @@ -0,0 +1,26 @@ +%h3.page-title New public deploy key +%hr + +%div + = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f| + -if @deploy_key.errors.any? + .alert.alert-danger + %ul + - @deploy_key.errors.full_messages.each do |msg| + %li= msg + + .form-group + = f.label :title, class: "control-label" + .col-sm-10= f.text_field :title, class: 'form-control' + .form-group + = f.label :key, class: "control-label" + .col-sm-10 + %p.light + Paste a machine public key here. Read more about how to generate it + = link_to "here", help_page_path("ssh", "README") + = f.text_area :key, class: "form-control thin_area", rows: 5 + + .form-actions + = f.submit 'Create', class: "btn-create btn" + = link_to "Cancel", admin_deploy_keys_path, class: "btn btn-cancel" + diff --git a/app/views/admin/deploy_keys/show.html.haml b/app/views/admin/deploy_keys/show.html.haml new file mode 100644 index 00000000000..cfa2adf92ee --- /dev/null +++ b/app/views/admin/deploy_keys/show.html.haml @@ -0,0 +1,34 @@ +.row + .col-md-4 + .panel.panel-default + .panel-heading + Deploy Key + %ul.well-list + %li + %span.light Title: + %strong= @deploy_key.title + %li + %span.light Created on: + %strong= @deploy_key.created_at.stamp("Aug 21, 2011") + + .panel.panel-default + .panel-heading Projects (#{@deploy_key.deploy_keys_projects.count}) + - if @deploy_key.deploy_keys_projects.any? + %ul.well-list + - @deploy_key.projects.each do |project| + %li + %span + %strong + = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] + .pull-right + = link_to disable_namespace_project_deploy_key_path(project.namespace, project, @deploy_key), data: { confirm: "Are you sure?" }, method: :put, class: "btn-xs btn btn-remove", title: 'Remove deploy key from project' do + %i.fa.fa-times.fa-inverse + + .col-md-8 + %p + %span.light Fingerprint: + %strong= @deploy_key.fingerprint + %pre.well-pre + = @deploy_key.key + .pull-right + = link_to 'Remove', admin_deploy_key_path(@deploy_key), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key" diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 7d292118075..14996dcd6a2 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -60,7 +60,7 @@ = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do %div - = users_select_tag(:user_ids, multiple: true) + = users_select_tag(:user_ids, multiple: true, email_user: true) %div.prepend-top-10 = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" %hr @@ -74,13 +74,18 @@ %ul.well-list.group-users-list - @members.each do |member| - user = member.user - %li{class: dom_class(member), id: dom_id(user)} + %li{class: dom_class(member), id: (dom_id(user) if user)} .list-item-name - %strong - = link_to user.name, admin_user_path(user) + - if user + %strong + = link_to user.name, admin_user_path(user) + - else + %strong + = member.invite_email + (invited) %span.pull-right.light = member.human_access - = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do + = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse .panel-footer = paginate @members, param_name: 'members_page', theme: 'gitlab' diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 05372f4124f..78684c692c7 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -42,11 +42,11 @@ %li %span.light http: %strong - = link_to @project.http_url_to_repo + = link_to @project.http_url_to_repo, project_path(@project) %li %span.light ssh: %strong - = link_to @project.ssh_url_to_repo + = link_to @project.ssh_url_to_repo, project_path(@project) - if @project.repository.exists? %li %span.light fs: @@ -124,14 +124,19 @@ - user = project_member.user %li.project_member .list-item-name - %strong - = link_to user.name, admin_user_path(user) + - if user + %strong + = link_to user.name, admin_user_path(user) + - else + %strong + = project_member.invite_email + (invited) .pull-right - if project_member.owner? %span.light Owner - else %span.light= project_member.human_access - = link_to namespace_project_project_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do + = link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_from_project_team_message(@project, project_member)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do %i.fa.fa-times .panel-footer = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab' diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 0a2934d3bda..3524f04c5ed 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -182,7 +182,7 @@ .pull-right %span.light= group_member.human_access - unless group_member.owner? - = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, @user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do + = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-times.fa-inverse - else .nothing-here-block This user has no groups. @@ -221,7 +221,7 @@ %span.light= member.human_access - if member.respond_to? :project - = link_to namespace_project_project_member_path(project.namespace, project, @user), data: { confirm: remove_from_project_team_message(project, @user) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do + = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_from_project_team_message(project, member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do %i.fa.fa-times #ssh-keys.tab-pane = render 'profiles/keys/key_table', admin: true diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 165db214d75..0cb7f764fab 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -17,7 +17,7 @@ - group = group_member.group %li .pull-right - - if can?(current_user, :manage_group, group) + - if can?(current_user, :admin_group, group) = link_to edit_group_path(group), class: "btn-sm btn btn-grouped" do %i.fa.fa-cogs Settings diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml new file mode 100644 index 00000000000..21e730bb7ff --- /dev/null +++ b/app/views/dashboard/milestones/_milestone.html.haml @@ -0,0 +1,20 @@ +%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } + %h4 + = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title) + .row + .col-sm-6 + = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do + = pluralize milestone.issue_count, 'Issue' + + = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do + = pluralize milestone.merge_requests_count, 'Merge Request' + + %span.light #{milestone.percent_complete}% complete + + .col-sm-6 + = milestone_progress_bar(milestone) + %div + - milestone.milestones.each do |milestone| + = link_to milestone_path(milestone) do + %span.label.label-gray + = milestone.project.name_with_namespace diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index caf3b685864..9944c0df815 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -16,23 +16,5 @@ .nothing-here-block No milestones to show - else - @dashboard_milestones.each do |milestone| - %li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } - %h4 - = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title) - %div - %div - = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do - = pluralize milestone.issue_count, 'Issue' - - = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do - = pluralize milestone.merge_requests_count, 'Merge Request' - - %span.light #{milestone.percent_complete}% complete - = milestone_progress_bar(milestone) - %div - %br - - milestone.milestones.each do |milestone| - = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) do - %span.label.label-default - = milestone.project.name_with_namespace + = render 'milestone', milestone: milestone = paginate @dashboard_milestones, theme: "gitlab" diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index 670f5ac7af7..f4ad2b294b3 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -19,5 +19,5 @@ %i.fa.fa-angle-left - else - %h3 You dont have starred projects yet + %h3 You don't have starred projects yet %p.slead Visit project page and press on star icon and it will appear on this page. diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index dcf60c90430..9dc6aeffd59 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -22,5 +22,6 @@ .clearfix.prepend-top-20 %p - %span.light Did not receive confirmation email? - = link_to "Send again", new_confirmation_path(resource_name)
\ No newline at end of file + %span.light Didn't receive a confirmation email? + = succeed '.' do + = link_to "Request a new one", new_confirmation_path(resource_name) diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index 3c7153d235f..552525f4a07 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -18,10 +18,10 @@ %a.twitter-share-button{ | href: "https://twitter.com/share", | "data-url" => event.project.web_url, | - "data-text" => "I just created a new project in GitLab! GitLab is version control on your server.", | + "data-text" => "I just #{event.project.imported? ? "imported" : "created"} a new project in GitLab! GitLab is version control on your server.", | "data-size" => "medium", | "data-related" => "gitlab", | "data-hashtags" => "gitlab", | "data-count" => "none"} Tweet - %script{src: "//platform.twitter.com/widgets.js"}
\ No newline at end of file + %script{src: "//platform.twitter.com/widgets.js"} diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 30e5faf822e..56b1948a474 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -1,17 +1,32 @@ - user = member.user -- return unless user +- return unless user || member.invite? - show_roles = true if show_roles.nil? %li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)} %span{class: ("list-item-name" if show_controls)} - = image_tag avatar_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 - - if user.blocked? - %label.label.label-danger - %strong Blocked + - if member.user + = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: '' + %strong= user.name + %span.cgray= user.username + - if user == current_user + %span.label.label-success It's you + - if user.blocked? + %label.label.label-danger + %strong Blocked + - else + = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: '' + %strong + = member.invite_email + %span.cgray + invited + - if member.created_by + by + = link_to member.created_by.name, user_path(member.created_by) + = time_ago_with_tooltip(member.created_at) + + - if show_controls && can?(current_user, :admin_group, @group) + = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do + Resend invite - if show_roles %span.pull-right @@ -27,7 +42,7 @@ = link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse - else - = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do + = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse .edit-member.hide.js-toggle-content diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml index c4c29bb2e8d..3361d7e2a8d 100644 --- a/app/views/groups/group_members/_new_group_member.html.haml +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -1,7 +1,10 @@ = form_for @group_member, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f| .form-group = f.label :user_ids, "People", class: 'control-label' - .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large') + .col-sm-10 + = users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true) + .help-block + Search for existing users or invite new ones using their email address. .form-group = f.label :access_level, "Group Access", class: 'control-label' diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 0d501fe7bd3..c0c9cd170ad 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -16,7 +16,7 @@ = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input input-mn-300' } = button_tag 'Search', class: 'btn' - - if current_user && current_user.can?(:manage_group, @group) + - if current_user && current_user.can?(:admin_group, @group) .pull-right = button_tag class: 'btn btn-new js-toggle-button', type: 'button' do Add members diff --git a/app/views/groups/milestones/_issue.html.haml b/app/views/groups/milestones/_issue.html.haml index 27d0c62df8c..09f9b4b8969 100644 --- a/app/views/groups/milestones/_issue.html.haml +++ b/app/views/groups/milestones/_issue.html.haml @@ -7,4 +7,4 @@ = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title .pull-right.assignee-icon - if issue.assignee - = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16" + = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16", alt: '' diff --git a/app/views/groups/milestones/_merge_request.html.haml b/app/views/groups/milestones/_merge_request.html.haml index b2d2097dfab..d0d1426762b 100644 --- a/app/views/groups/milestones/_merge_request.html.haml +++ b/app/views/groups/milestones/_merge_request.html.haml @@ -7,4 +7,4 @@ = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title .pull-right.assignee-icon - if merge_request.assignee - = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16" + = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16", alt: '' diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml new file mode 100644 index 00000000000..30093d2d05d --- /dev/null +++ b/app/views/groups/milestones/_milestone.html.haml @@ -0,0 +1,25 @@ +%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } + .pull-right + - if can?(current_user, :admin_group, @group) + - if milestone.closed? + = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" + - else + = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close" + %h4 + = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title) + .row + .col-sm-6 + = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do + = pluralize milestone.issue_count, 'Issue' + + = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do + = pluralize milestone.merge_requests_count, 'Merge Request' + + %span.light #{milestone.percent_complete}% complete + .col-sm-6 + = milestone_progress_bar(milestone) + %div + - milestone.milestones.each do |milestone| + = link_to milestone_path(milestone) do + %span.label.label-gray + = milestone.project.name diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 57dc235f5bb..008d5a6bd22 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -18,29 +18,5 @@ .nothing-here-block No milestones to show - else - @group_milestones.each do |milestone| - %li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } - .pull-right - - if can?(current_user, :manage_group, @group) - - if milestone.closed? - = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" - - else - = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close" - %h4 - = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title) - %div - %div - = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do - = pluralize milestone.issue_count, 'Issue' - - = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do - = pluralize milestone.merge_requests_count, 'Merge Request' - - %span.light #{milestone.percent_complete}% complete - = milestone_progress_bar(milestone) - %div - %br - - milestone.milestones.each do |milestone| - = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) do - %span.label.label-default - = milestone.project.name + = render 'milestone', milestone: milestone = paginate @group_milestones, theme: "gitlab" diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index fea70f5cbc3..fb32f2caa4c 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -6,7 +6,7 @@ Open Milestone #{@group_milestone.title} .pull-right - - if can?(current_user, :manage_group, @group) + - if can?(current_user, :admin_group, @group) - if @group_milestone.active? = link_to 'Close Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close" - else diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index dd1fa3840d5..0d547984cc9 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -2,7 +2,7 @@ .panel-heading %strong= @group.name projects: - - if can? current_user, :manage_group, @group + - if can? current_user, :admin_group, @group .panel-head-actions = link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do %i.fa.fa-plus diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml index eca34dbff06..cc1be6a717a 100644 --- a/app/views/help/show.html.haml +++ b/app/views/help/show.html.haml @@ -1,2 +1,2 @@ .documentation.wiki - = markdown File.read(Rails.root.join('doc', @category, @file + '.md')).gsub("$your_email", current_user.email) + = markdown @markdown.gsub('$your_email', current_user.email) diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml index 8d10722628f..90a6f5f9d2d 100644 --- a/app/views/import/base/create.js.haml +++ b/app/views/import/base/create.js.haml @@ -13,7 +13,7 @@ - elsif @access_denied :plain job = $("tr#repo_#{@repo_id}") - job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>"") + job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>") - else :plain job = $("tr#repo_#{@repo_id}") diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml new file mode 100644 index 00000000000..ce78fec205f --- /dev/null +++ b/app/views/import/google_code/new.html.haml @@ -0,0 +1,60 @@ +%h3.page-title + %i.fa.fa-google + Import projects from Google Code +%hr + += form_tag callback_import_google_code_path, class: 'form-horizontal', multipart: true do + %p + Follow the steps below to export your Google Code project data. + In the next step, you'll be able to select the projects you want to import. + %ol + %li + %p + Go to + #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: "_blank"}. + %li + %p + Make sure you're logged into the account that owns the projects you'd like to import. + %li + %p + Click the <strong>Select none</strong> button on the right, since we only need "Google Code Project Hosting". + %li + %p + Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right. + %li + %p + Choose <strong>Next</strong> at the bottom of the page. + %li + %p + Leave the "File type" and "Delivery method" options on their default values. + %li + %p + Choose <strong>Create archive</strong> and wait for archiving to complete. + %li + %p + Click the <strong>Download</strong> button and wait for downloading to complete. + %li + %p + Find the downloaded ZIP file and decompress it. + %li + %p + Find the newly extracted <code>Takeout/Google Code Project Hosting/GoogleCodeProjectHosting.json</code> file. + %li + %p + Upload <code>GoogleCodeProjectHosting.json</code> here: + %p + %input{type: "file", name: "dump_file", id: "dump_file"} + %li + %p + Do you want to customize how Google Code email addresses and usernames are imported into GitLab? + %p + = label_tag :create_user_map_0 do + = radio_button_tag :create_user_map, 0, true + No, directly import the existing email addresses and usernames. + %p + = label_tag :create_user_map_1 do + = radio_button_tag :create_user_map, 1, false + Yes, let me map Google Code users to full names or GitLab users. + %li + %p + = submit_tag 'Continue to the next step', class: "btn btn-create" diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml new file mode 100644 index 00000000000..d55fcfc97a8 --- /dev/null +++ b/app/views/import/google_code/new_user_map.html.haml @@ -0,0 +1,20 @@ +%h3.page-title + %i.fa.fa-google + Import projects from Google Code +%hr + += form_tag create_user_map_import_google_code_path, class: 'form-horizontal' do + %p + Customize how Google Code email addresses and usernames are imported into GitLab. + In the next step, you'll be able to select the projects you want to import. + %p + The user map is a JSON document mapping Google Code users (as keys) to the way they will be imported into GitLab (as values). By default the username is masked to ensure users' privacy. + %p + To map a Google Code user to a full name or GitLab user, simply replace the value, e.g. <code>"johnsmith@gmail.com": "John Smith"</code> or <code>"johnsmith@gmail.com": "@johnsmith"</code>. Be sure to preserve the surrounding double quotes and other punctuation. + + .form-group + .col-sm-12 + = text_area_tag :user_map, JSON.pretty_generate(@user_map), class: 'form-control', rows: 15 + + .form-actions + = submit_tag 'Continue to the next step', class: "btn btn-create" diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml new file mode 100644 index 00000000000..2013b8c03c6 --- /dev/null +++ b/app/views/import/google_code/status.html.haml @@ -0,0 +1,49 @@ +%h3.page-title + %i.fa.fa-google + Import projects from Google Code + +%p.light + Select projects you want to import. +%p.light + Optionally, you can + = link_to "customize", new_user_map_import_google_code_path + how Google Code email addresses and usernames are imported into GitLab. +%hr +%p + = button_tag 'Import all projects', class: "btn btn-success js-import-all" + +%table.table.import-jobs + %thead + %tr + %th From Google Code + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank" + %td + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" + %td.import-target + = "#{current_user.username}/#{repo.name}" + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" + +:coffeescript + new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}") diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml new file mode 100644 index 00000000000..ab0ecffe4d2 --- /dev/null +++ b/app/views/invites/show.html.haml @@ -0,0 +1,29 @@ +%h3.page-title Invitation + +%p + You have been invited + - if inviter = @member.created_by + by + = link_to inviter.name, user_url(inviter) + to join + - case @member.source + - when Project + - project = @member.source + project + %strong + = link_to project.name_with_namespace, namespace_project_url(project.namespace, project) + - when Group + - group = @member.source + group + %strong + = link_to group.name, group_url(group) + as #{@member.human_access}. + +- if @member.source.users.include?(current_user) + %p + However, you are already a member of this #{@member.source.is_a?(Group) ? "group" : "project"}. + Sign in using a different account to accept the invitation. +- else + .actions + = link_to "Accept invitation", accept_invite_url(@token), method: :post, class: "btn btn-success" + = link_to "Decline", decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10" diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml index b1c2e1a7b19..d58582c107a 100644 --- a/app/views/layouts/_head_panel.html.haml +++ b/app/views/layouts/_head_panel.html.haml @@ -39,7 +39,7 @@ = link_to profile_path, title: "Profile settings", class: 'has_bottom_tooltip', 'data-original-title' => 'Profile settings"' do %i.fa.fa-user %li - = link_to destroy_user_session_path, class: "logout", method: :delete, title: "Logout", class: 'has_bottom_tooltip', 'data-original-title' => 'Logout' do + = link_to destroy_user_session_path, class: "logout", method: :delete, title: "Sign out", class: 'has_bottom_tooltip', 'data-original-title' => 'Sign out' do %i.fa.fa-sign-out %li.hidden-xs = link_to current_user, class: "profile-pic has_bottom_tooltip", id: 'profile-pic', 'data-original-title' => 'Your profile' do diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 2f38d596c65..34efceb37d1 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -19,6 +19,11 @@ %i.fa.fa-group %span Groups + = nav_link(controller: :deploy_keys) do + = link_to admin_deploy_keys_path, title: 'Deploy Keys' do + %i.fa.fa-key + %span + Deploy Keys = nav_link(controller: :logs) do = link_to admin_logs_path, title: 'Logs' do %i.fa.fa-file-text diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 32fe0e37df8..f0d92b7a12c 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -30,7 +30,7 @@ %span Members - - if can?(current_user, :manage_group, @group) + - if can?(current_user, :admin_group, @group) = nav_link(html_options: { class: "#{"active" if group_settings_page?} separate-item" }) do = link_to edit_group_path(@group), title: 'Settings', class: "tab no-highlight" do %i.fa.fa-cogs diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 52681865d64..6c13f30f627 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -53,7 +53,7 @@ - if project_nav_tab? :issues = nav_link(controller: :issues) do - = link_to url_for_project_issues, title: 'Issues', class: 'shortcuts-issues' do + = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do %i.fa.fa-exclamation-circle %span Issues diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 7eec93abdf6..00c7cedce40 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -27,8 +27,7 @@ } .file-stats .deleted-file { color: #B00; - } - #{add_email_highlight_css} + }} %body %div.content = yield diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml index 778a78acf56..3fd4b04ac84 100644 --- a/app/views/notify/_note_message.html.haml +++ b/app/views/notify/_note_message.html.haml @@ -1,2 +1,2 @@ %div - = replace_image_links_with_base64(markdown(@note.note), @note.project) + = markdown(@note.note, reference_only_path: false) diff --git a/app/views/notify/group_invite_accepted_email.html.haml b/app/views/notify/group_invite_accepted_email.html.haml new file mode 100644 index 00000000000..55efad384a7 --- /dev/null +++ b/app/views/notify/group_invite_accepted_email.html.haml @@ -0,0 +1,6 @@ +%p + #{@group_member.invite_email}, now known as + #{link_to @group_member.user.name, user_url(@group_member.user)}, + has accepted your invitation to join group + #{link_to @group.name, group_url(@group)}. + diff --git a/app/views/notify/group_invite_accepted_email.text.erb b/app/views/notify/group_invite_accepted_email.text.erb new file mode 100644 index 00000000000..f8b70f7a5a6 --- /dev/null +++ b/app/views/notify/group_invite_accepted_email.text.erb @@ -0,0 +1,3 @@ +<%= @group_member.invite_email %>, now known as <%= @group_member.user.name %>, has accepted your invitation to join group <%= @group.name %>. + +<%= group_url(@group) %> diff --git a/app/views/notify/group_invite_declined_email.html.haml b/app/views/notify/group_invite_declined_email.html.haml new file mode 100644 index 00000000000..f9525d84fac --- /dev/null +++ b/app/views/notify/group_invite_declined_email.html.haml @@ -0,0 +1,5 @@ +%p + #{@invite_email} + has declined your invitation to join group + #{link_to @group.name, group_url(@group)}. + diff --git a/app/views/notify/group_invite_declined_email.text.erb b/app/views/notify/group_invite_declined_email.text.erb new file mode 100644 index 00000000000..6c19a288d15 --- /dev/null +++ b/app/views/notify/group_invite_declined_email.text.erb @@ -0,0 +1,3 @@ +<%= @invite_email %> has declined your invitation to join group <%= @group.name %>. + +<%= group_url(@group) %> diff --git a/app/views/notify/group_member_invited_email.html.haml b/app/views/notify/group_member_invited_email.html.haml new file mode 100644 index 00000000000..163e88bfea3 --- /dev/null +++ b/app/views/notify/group_member_invited_email.html.haml @@ -0,0 +1,14 @@ +%p + You have been invited + - if inviter = @group_member.created_by + by + = link_to inviter.name, user_url(inviter) + to join group + = link_to @group.name, group_url(@group) + as #{@group_member.human_access}. + +%p + = link_to 'Accept invitation', invite_url(@token) + or + = link_to 'decline', decline_invite_url(@token) + diff --git a/app/views/notify/group_member_invited_email.text.erb b/app/views/notify/group_member_invited_email.text.erb new file mode 100644 index 00000000000..28ce4819b14 --- /dev/null +++ b/app/views/notify/group_member_invited_email.text.erb @@ -0,0 +1,4 @@ +You have been invited <%= "by #{@group_member.created_by.name} " if @group_member.created_by %>to join group <%= @group.name %> as <%= @group_member.human_access %>. + +Accept invitation: <%= invite_url(@token) %> +Decline invitation: <%= decline_invite_url(@token) %> diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index 03cbee94608..53a068be52e 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -1,5 +1,5 @@ -if @issue.description - = replace_image_links_with_base64(markdown(@issue.description), @issue.project) + = markdown(@issue.description, reference_only_path: false) - if @issue.assignee_id.present? %p diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index 729a7bb505d..5b7dd117c16 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -6,4 +6,4 @@ Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} -if @merge_request.description - = replace_image_links_with_base64(markdown(@merge_request.description), @merge_request.project) + = markdown(@merge_request.description, reference_only_path: false) diff --git a/app/views/notify/project_invite_accepted_email.html.haml b/app/views/notify/project_invite_accepted_email.html.haml new file mode 100644 index 00000000000..7e58d30b10a --- /dev/null +++ b/app/views/notify/project_invite_accepted_email.html.haml @@ -0,0 +1,6 @@ +%p + #{@project_member.invite_email}, now known as + #{link_to @project_member.user.name, user_url(@project_member.user)}, + has accepted your invitation to join project + #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}. + diff --git a/app/views/notify/project_invite_accepted_email.text.erb b/app/views/notify/project_invite_accepted_email.text.erb new file mode 100644 index 00000000000..fcbe752114d --- /dev/null +++ b/app/views/notify/project_invite_accepted_email.text.erb @@ -0,0 +1,3 @@ +<%= @project_member.invite_email %>, now known as <%= @project_member.user.name %>, has accepted your invitation to join project <%= @project.name_with_namespace %>. + +<%= namespace_project_url(@project.namespace, @project) %> diff --git a/app/views/notify/project_invite_declined_email.html.haml b/app/views/notify/project_invite_declined_email.html.haml new file mode 100644 index 00000000000..c2d7e6f6e3a --- /dev/null +++ b/app/views/notify/project_invite_declined_email.html.haml @@ -0,0 +1,5 @@ +%p + #{@invite_email} + has declined your invitation to join project + #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}. + diff --git a/app/views/notify/project_invite_declined_email.text.erb b/app/views/notify/project_invite_declined_email.text.erb new file mode 100644 index 00000000000..484687fa51c --- /dev/null +++ b/app/views/notify/project_invite_declined_email.text.erb @@ -0,0 +1,3 @@ +<%= @invite_email %> has declined your invitation to join project <%= @project.name_with_namespace %>. + +<%= namespace_project_url(@project.namespace, @project) %> diff --git a/app/views/notify/project_member_invited_email.html.haml b/app/views/notify/project_member_invited_email.html.haml new file mode 100644 index 00000000000..79eb89616de --- /dev/null +++ b/app/views/notify/project_member_invited_email.html.haml @@ -0,0 +1,13 @@ +%p + You have been invited + - if inviter = @project_member.created_by + by + = link_to inviter.name, user_url(inviter) + to join project + = link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project) + as #{@project_member.human_access}. + +%p + = link_to 'Accept invitation', invite_url(@token) + or + = link_to 'decline', decline_invite_url(@token) diff --git a/app/views/notify/project_member_invited_email.text.erb b/app/views/notify/project_member_invited_email.text.erb new file mode 100644 index 00000000000..e0706272115 --- /dev/null +++ b/app/views/notify/project_member_invited_email.text.erb @@ -0,0 +1,4 @@ +You have been invited <%= "by #{@project_member.created_by.name} " if @project_member.created_by %>to join project <%= @project.name_with_namespace %> as <%= @project_member.human_access %>. + +Accept invitation: <%= invite_url(@token) %> +Decline invitation: <%= decline_invite_url(@token) %> diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index bbf7004c906..a374a662333 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -59,8 +59,7 @@ %strong = diff.new_path %hr - %pre - = color_email_diff(diff.diff) + = color_email_diff(diff.diff) %br - if @compare.timeout diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 9d8f33cbbaa..09f290429ea 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -20,9 +20,13 @@ %li %strong= @primary %span.label.label-success Primary Email + - if @primary === @public_email + %span.label.label-info Public Email - @emails.each do |email| %li %strong= email.email + - if email.email === @public_email + %span.label.label-info Public Email %span.cgray added #{time_ago_with_tooltip(email.created_at)} = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove pull-right' diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 5a501e43149..6c745e69e40 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -42,6 +42,11 @@ - else %span.help-block We also use email for avatar detection if no avatar is uploaded. .form-group + = f.label :public_email, class: "control-label" + .col-sm-10 + = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show in profile'}, class: "form-control" + %span.help-block This email will be displayed on your public profile. + .form-group = f.label :skype, class: "control-label" .col-sm-10= f.text_field :skype, class: "form-control" .form-group diff --git a/app/views/projects/_dropdown.html.haml b/app/views/projects/_dropdown.html.haml index f4f4c2662cf..3036f11bb2d 100644 --- a/app/views/projects/_dropdown.html.haml +++ b/app/views/projects/_dropdown.html.haml @@ -5,29 +5,33 @@ %ul.dropdown-menu - if @project.issues_enabled && can?(current_user, :write_issue, @project) %li - = link_to url_for_new_issue, title: "New Issue" do + = link_to url_for_new_issue(@project, only_path: true), title: "New Issue" do + %i.fa.fa-fw.fa-exclamation-circle New issue - if @project.merge_requests_enabled && can?(current_user, :write_merge_request, @project) %li = link_to new_namespace_project_merge_request_path(@project.namespace, @project), title: "New Merge Request" do + %i.fa.fa-fw.fa-tasks New merge request - if @project.snippets_enabled && can?(current_user, :write_snippet, @project) %li = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New Snippet" do + %i.fa.fa-fw.fa-file-text-o New snippet - if can?(current_user, :admin_project_member, @project) %li = link_to namespace_project_project_members_path(@project.namespace, @project), title: "New project member" do + %i.fa.fa-fw.fa-users New project member - if can? current_user, :push_code, @project %li.divider %li = link_to new_namespace_project_branch_path(@project.namespace, @project) do - %i.fa.fa-code-fork - Git branch + %i.fa.fa-fw.fa-code-fork + New branch %li = link_to new_namespace_project_tag_path(@project.namespace, @project) do - %i.fa.fa-tag - Git tag + %i.fa.fa-fw.fa-tag + New tag diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index a295a0d6cdc..5689bdee1c6 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -2,7 +2,7 @@ .project-home-panel{:class => ("empty-project" if empty_repo)} .project-identicon-holder = project_icon(@project, alt: '', class: 'avatar project-avatar') - .project-home-row + .project-home-row.project-home-row-top .project-home-desc - if @project.description.present? = escaped_autolink(@project.description) @@ -14,31 +14,30 @@ – = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do = readme.name - .star-fork-buttons + .project-repo-buttons + .inline.star.js-toggler-container{class: @show_star ? 'on' : ''} + - if current_user + = link_to_toggle_star('Star this project.', false) + = link_to_toggle_star('Unstar this project.', true) + - else + = link_to new_user_session_path, class: 'btn star-btn has_tooltip', title: 'You must sign in to star a project' do + %span + = icon('star') + Star + %span.count + = @project.star_count - unless @project.empty_repo? - .fork-buttons - - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace + - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace + .inline.fork-buttons.prepend-left-10 - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork' do + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-sm btn-default' do = link_to_toggle_fork - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project" do + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-sm btn-default' do = link_to_toggle_fork - .star-buttons - %span.star.js-toggler-container{class: @show_star ? 'on' : ''} - - if current_user - = link_to_toggle_star('Star this project.', false, true) - = link_to_toggle_star('Unstar this project.', true, true) - - else - = link_to_toggle_star('You must sign in to star a project.', false, false) - .project-home-row.hidden-xs - if current_user && !empty_repo .project-home-dropdown = render "dropdown" - - unless @project.empty_repo? - - if can? current_user, :download_code, @project - .pull-right.prepend-left-10 - = render 'projects/repositories/download_archive', split_button: true = render "shared/clone_panel" diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml index 7fd5fe8a6e1..e321a84974e 100644 --- a/app/views/projects/_issuable_form.html.haml +++ b/app/views/projects/_issuable_form.html.haml @@ -35,8 +35,8 @@ %i.fa.fa-user Assign to .col-sm-10 - = project_users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", - placeholder: 'Select a user', class: 'custom-form-control', + = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", + placeholder: 'Select a user', class: 'custom-form-control', null_user: true, selected: issuable.assignee_id) = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index ba60bd92869..65c3ab10e02 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -22,7 +22,7 @@ %div#tree-content-holder.tree-content-holder %article.file-holder .file-title - %i.fa.fa-file + = blob_icon blob.mode, blob.name %strong = blob.name %small diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 0de8c509f2b..4e7415be4aa 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -11,14 +11,14 @@ protected .pull-right - if can?(current_user, :download_code, @project) - = render 'projects/repositories/download_archive', ref: branch.name, btn_class: 'btn-grouped btn-group-sm' + = render 'projects/repositories/download_archive', ref: branch.name, btn_class: 'btn-grouped btn-group-xs' - if branch.name != @repository.root_ref - = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-sm', method: :post, title: "Compare" do + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-xs', method: :post, title: "Compare" do %i.fa.fa-files-o Compare - if can_remove_branch?(@project, branch.name) - = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-sm btn-remove remove-row', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do + = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do %i.fa.fa-trash-o - if commit diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 2579f2cac92..3f645b81397 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -10,7 +10,8 @@ Download as %span.caret %ul.dropdown-menu - %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) + - unless @commit.parents.length > 1 + %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) %li= link_to "Plain Diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff) = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-primary btn-grouped" do %span Browse Code » diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 83e4d24cf5f..a714f5f79e0 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,6 +1,8 @@ %ul.nav.nav-tabs = nav_link(controller: [:commit, :commits]) do - = link_to 'Commits', namespace_project_commits_path(@project.namespace, @project, @repository.root_ref) + = link_to namespace_project_commits_path(@project.namespace, @project, @repository.root_ref) do + Commits + %span.badge= number_with_precision(@repository.commit_count, precision: 0, delimiter: ',') = nav_link(controller: :compare) do = link_to 'Compare', namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref) diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml index a2faa9d5e25..c577dfa8d55 100644 --- a/app/views/projects/deploy_keys/_deploy_key.html.haml +++ b/app/views/projects/deploy_keys/_deploy_key.html.haml @@ -5,21 +5,32 @@ %i.fa.fa-plus Enable - else - - if deploy_key.projects.count > 1 + - if deploy_key.destroyed_when_orphaned? && deploy_key.almost_orphaned? + = link_to 'Remove', disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: 'You are going to remove deploy key. Are you sure?'}, method: :put, class: "btn btn-remove delete-key btn-sm pull-right" + - else = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-sm', method: :put do %i.fa.fa-power-off Disable - - else - = link_to 'Remove', namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: 'You are going to remove deploy key. Are you sure?'}, method: :delete, class: "btn btn-remove delete-key btn-sm pull-right" - - - key_project = deploy_key.projects.include?(@project) ? @project : deploy_key.projects.first - = link_to namespace_project_deploy_key_path(key_project.namespace, key_project, deploy_key) do + - if project = project_for_deploy_key(deploy_key) + = link_to namespace_project_deploy_key_path(project.namespace, project, deploy_key) do + %i.fa.fa-key + %strong= deploy_key.title + - else %i.fa.fa-key %strong= deploy_key.title + %p.light.prepend-top-10 - - deploy_key.projects.map(&:name_with_namespace).each do |project_name| - %span.label.label-gray.deploy-project-label= project_name + - if deploy_key.public? + %span.label.label-info.deploy-project-label + Public deploy key + + - deploy_key.projects.each do |project| + - if can?(current_user, :read_project, project) + %span.label.label-gray.deploy-project-label + = link_to namespace_project_path(project.namespace, project) do + = project.name_with_namespace + %small.pull-right Created #{time_ago_with_tooltip(deploy_key.created_at)} diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml index c02a18146eb..472a13a8524 100644 --- a/app/views/projects/deploy_keys/index.html.haml +++ b/app/views/projects/deploy_keys/index.html.haml @@ -22,11 +22,20 @@ .light-well .nothing-here-block Create a #{link_to 'new deploy key', new_namespace_project_deploy_key_path(@project.namespace, @project)} or add an existing one .col-md-6.available-keys - %h5 - %strong Deploy keys - from projects available to you - %ul.bordered-list - = render @available_keys - - if @available_keys.blank? - .light-well - .nothing-here-block Deploy keys from projects you have access to will be displayed here + - # If there are available public deploy keys but no available project deploy keys, only public deploy keys are shown. + - if @available_project_keys.any? || @available_public_keys.blank? + %h5 + %strong Deploy keys + from projects you have access to + %ul.bordered-list + = render @available_project_keys + - if @available_project_keys.blank? + .light-well + .nothing-here-block Deploy keys from projects you have access to will be displayed here + + - if @available_public_keys.any? + %h5 + %strong Public deploy keys + available to any project + %ul.bordered-list + = render @available_public_keys diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 2b9b6599a7d..b49aee504fe 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -20,4 +20,4 @@ Maybe diff is really big and operation failed with timeout. Try to get diff locally :coffeescript - $('.files .diff-header').stick_in_parent(recalc_every: 1, offset_top: $('.navbar').height()) + $('.files .diff-header').stick_in_parent(offset_top: $('.navbar').height()) diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index e691db9c08e..e6dfbfd6511 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -23,7 +23,7 @@ %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line.text) - if @reply_allowed - - comments = @line_notes.select { |n| n.line_code == line_code }.sort_by(&:created_at) + - comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at) - unless comments.empty? = render "projects/notes/diff_notes_with_reply", notes: comments, line: line.text diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index 4a5d09b9503..78b4c1923dd 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -54,7 +54,7 @@ } ctx = $("#hour-chart").get(0).getContext("2d"); - new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true}); + new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2}) data = { labels : #{@commits_per_week_days.keys.to_json}, @@ -68,7 +68,7 @@ } ctx = $("#weekday-chart").get(0).getContext("2d"); - new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true}); + new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2}) data = { labels : #{@commits_per_month.keys.to_json}, @@ -82,4 +82,4 @@ } ctx = $("#month-chart").get(0).getContext("2d"); - new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true}); + new Chart(ctx).Line(data, {"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2}) diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 0d3028d50b4..288b48f4583 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -9,8 +9,8 @@ .votes-holder.pull-right #votes= render 'votes/votes_block', votable: @issue .participants - %span= pluralize(@issue.participants.count, 'participant') - - @issue.participants.each do |participant| + %span= pluralize(@issue.participants(current_user).count, 'participant') + - @issue.participants(current_user).each do |participant| = link_to_member(@project, participant, name: false, size: 24) .voting_notes#notes= render "projects/notes/notes_with_form" %aside.col-md-3 diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index c3d6dc2e50b..9228074d833 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -8,7 +8,7 @@ - else none - if can?(current_user, :modify_issue, @issue) - = project_users_select_tag('issue[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @issue.assignee_id) + = users_select_tag('issue[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @issue.assignee_id, null_user: true, first_user: true) %div.prepend-top-20.clearfix .issuable-context-title @@ -44,5 +44,3 @@ :coffeescript new Subscription("#{toggle_subscription_namespace_project_issue_path(@issue.project.namespace, @project, @issue)}") - - diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 54e3009cca2..d3c7ae24a75 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -15,15 +15,5 @@ = render 'shared/issuable_filter' - .clearfix - .issues_bulk_update.hide - = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do - = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status", class: 'form-control') - = project_users_select_tag('update[assignee_id]', placeholder: 'Assignee') - = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") - = hidden_field_tag 'update[issues_ids]', [] - = hidden_field_tag :state_event, params[:state_event] - = button_tag "Update issues", class: "btn update_selected_issues btn-save" - .issues-holder = render "issues" diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml index 82c0e653759..1d38662bff8 100644 --- a/app/views/projects/issues/update.js.haml +++ b/app/views/projects/issues/update.js.haml @@ -13,5 +13,5 @@ $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) $('.edit-issue.inline-update input[type="submit"]').hide(); -new ProjectUsersSelect(); +new UsersSelect() new Issue(); diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 1d8eef4e8ce..d986ce67c0c 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -39,7 +39,7 @@ %i.fa.fa-user Assign to .col-sm-10 - = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control', selected: @merge_request.assignee_id, project_id: @merge_request.target_project_id) + = users_select_tag('merge_request[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control', selected: @merge_request.assignee_id, project_id: @merge_request.target_project_id) = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' .form-group diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index a74aede4e6b..cec02de84ca 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -36,17 +36,17 @@ - if @commits.present? %ul.nav.nav-tabs.merge-request-tabs - %li.notes-tab{data: {action: 'notes'}} + %li.notes-tab{data: {action: 'notes', toggle: 'tab'}} = link_to merge_request_path(@merge_request) do %i.fa.fa-comments Discussion %span.badge= @merge_request.mr_and_commit_notes.user.count - %li.commits-tab{data: {action: 'commits'}} + %li.commits-tab{data: {action: 'commits', toggle: 'tab'}} = link_to merge_request_path(@merge_request), title: 'Commits' do %i.fa.fa-history Commits %span.badge= @commits.size - %li.diffs-tab{data: {action: 'diffs'}} + %li.diffs-tab{data: {action: 'diffs', toggle: 'tab'}} = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) do %i.fa.fa-list-alt Changes diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml index 80e5c223d60..105562fb05e 100644 --- a/app/views/projects/merge_requests/show/_context.html.haml +++ b/app/views/projects/merge_requests/show/_context.html.haml @@ -9,7 +9,7 @@ none .issuable-context-selectbox - if can?(current_user, :modify_merge_request, @merge_request) - = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @merge_request.assignee_id) + = users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @merge_request.assignee_id, null_user: true) %div.prepend-top-20.clearfix .issuable-context-title diff --git a/app/views/projects/merge_requests/show/_participants.html.haml b/app/views/projects/merge_requests/show/_participants.html.haml index 4f34af1737d..9c93fa55fe6 100644 --- a/app/views/projects/merge_requests/show/_participants.html.haml +++ b/app/views/projects/merge_requests/show/_participants.html.haml @@ -1,4 +1,4 @@ .participants - %span #{@merge_request.participants.count} participants - - @merge_request.participants.each do |participant| + %span #{@merge_request.participants(current_user).count} participants + - @merge_request.participants(current_user).each do |participant| = link_to_member(@project, participant, name: false, size: 24) diff --git a/app/views/projects/merge_requests/show/_state_widget.html.haml b/app/views/projects/merge_requests/show/_state_widget.html.haml index a4f2a890969..44bd9347f51 100644 --- a/app/views/projects/merge_requests/show/_state_widget.html.haml +++ b/app/views/projects/merge_requests/show/_state_widget.html.haml @@ -29,7 +29,7 @@ %h4 Merge in progress... %p - GitLab tries to merge it right now. During this time merge request is locked and can not be closed. + Merging is in progress. While merging this request is locked and cannot be closed. - unless @commits.any? %h4 Nothing to merge diff --git a/app/views/projects/merge_requests/update.js.haml b/app/views/projects/merge_requests/update.js.haml index f5cc98c7fa4..b4df1d20737 100644 --- a/app/views/projects/merge_requests/update.js.haml +++ b/app/views/projects/merge_requests/update.js.haml @@ -2,7 +2,7 @@ $('.context').html("#{escape_javascript(render partial: 'projects/merge_requests/show/context', locals: { issue: @issue })}"); $('.context').effect('highlight'); - new ProjectUsersSelect(); + new UsersSelect() $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}); merge_request = new MergeRequest(); diff --git a/app/views/projects/milestones/_issue.html.haml b/app/views/projects/milestones/_issue.html.haml index 26c83841a22..88fccfe4981 100644 --- a/app/views/projects/milestones/_issue.html.haml +++ b/app/views/projects/milestones/_issue.html.haml @@ -1,9 +1,9 @@ %li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) } - %span.str-truncated + .pull-right.assignee-icon + - if issue.assignee + = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16", alt: '' + %span = link_to [@project.namespace.becomes(Namespace), @project, issue] do %span.cgray ##{issue.iid} = link_to_gfm issue.title, [@project.namespace.becomes(Namespace), @project, issue], title: issue.title - .pull-right.assignee-icon - - if issue.assignee - = image_tag avatar_icon(issue.assignee.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 index 42fbd0cd2ca..0d7a118569a 100644 --- a/app/views/projects/milestones/_merge_request.html.haml +++ b/app/views/projects/milestones/_merge_request.html.haml @@ -5,4 +5,4 @@ = link_to_gfm merge_request.title, [@project.namespace.becomes(Namespace), @project, merge_request], title: merge_request.title .pull-right.assignee-icon - if merge_request.assignee - = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16" + = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16", alt: '' diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index 7039c85bb2c..62360158ff9 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -11,16 +11,14 @@ %span.cred (Expired) %small = milestone.expires_at - - if milestone.is_empty? - %span.muted Empty - - else - %div - %div - = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do - = pluralize milestone.issues.count, 'Issue' - - = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do - = pluralize milestone.merge_requests.count, 'Merge Request' - - %span.light #{milestone.percent_complete}% complete + .row + .col-sm-6 + = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do + = pluralize milestone.issues.count, 'Issue' + + = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do + = pluralize milestone.merge_requests.count, 'Merge Request' + + %span.light #{milestone.percent_complete}% complete + .col-sm-6 = milestone_progress_bar(milestone) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 69909a8554e..a06c85b4251 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -62,6 +62,10 @@ %i.icon-gitorious.icon-gitorious-small Gitorious.org + = link_to new_import_google_code_path, class: 'btn' do + %i.fa.fa-google + Google Code + = link_to "#", class: 'btn js-toggle-button' do %i.fa.fa-git %span Any repo by URL diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml index 3561ca49f81..b8068835b3a 100644 --- a/app/views/projects/notes/_discussion.html.haml +++ b/app/views/projects/notes/_discussion.html.haml @@ -6,9 +6,8 @@ = image_tag avatar_icon(note.author_email), class: "avatar s40" .timeline-content - if note.for_merge_request? - - if note.outdated? - = render "projects/notes/discussions/outdated", discussion_notes: discussion_notes - - else - = render "projects/notes/discussions/active", discussion_notes: discussion_notes + - (active_notes, outdated_notes) = discussion_notes.partition(&:active?) + = render "projects/notes/discussions/active", discussion_notes: active_notes if active_notes.length > 0 + = render "projects/notes/discussions/outdated", discussion_notes: outdated_notes if outdated_notes.length > 0 - else = render "projects/notes/discussions/commit", discussion_notes: discussion_notes diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index be96c302143..2ada6cb6700 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -12,7 +12,7 @@ .comment-hints.clearfix .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 }} .pull-right Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }. - + .error-alert .note-form-actions .buttons diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 71bdf5c8f2a..0728f8fa42b 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -5,7 +5,7 @@ %span.fa.fa-circle - else = link_to user_path(note.author) do - = image_tag avatar_icon(note.author_email), class: "avatar s40" + = image_tag avatar_icon(note.author_email), class: "avatar s40", alt: '' .timeline-content .note-header .note-actions @@ -23,7 +23,7 @@ Remove - if note.system = link_to user_path(note.author) do - = image_tag avatar_icon(note.author_email), class: "avatar s16" + = image_tag avatar_icon(note.author_email), class: "avatar s16", alt: '' = link_to_member(@project, note.author, avatar: false) %span.author-username = '@' + note.author.username diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 0f824bdabf8..d708b01a114 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -1,7 +1,10 @@ = form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'form-horizontal users-project-form' } do |f| .form-group = f.label :user_ids, "People", class: 'control-label' - .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large') + .col-sm-10 + = users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true) + .help-block + Search for existing users or invite new ones using their email address. .form-group = f.label :access_level, "Project Access", class: 'control-label' diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml index a07d0762334..635e4d70941 100644 --- a/app/views/projects/project_members/_project_member.html.haml +++ b/app/views/projects/project_members/_project_member.html.haml @@ -1,16 +1,32 @@ - user = member.user -- return unless user +- return unless user || member.invite? %li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)} %span.list-item-name - = image_tag avatar_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 - - if user.blocked? - %label.label.label-danger - %strong Blocked + - if member.user + = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: '' + %strong + = link_to user.name, user_path(user) + %span.cgray= user.username + - if user == current_user + %span.label.label-success It's you + - if user.blocked? + %label.label.label-danger + %strong Blocked + - else + = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: '' + %strong + = member.invite_email + %span.cgray + invited + - if member.created_by + by + = link_to member.created_by.name, user_path(member.created_by) + = time_ago_with_tooltip(member.created_at) + + - if current_user_can_admin_project + = link_to resend_invite_namespace_project_project_member_path(@project.namespace, @project, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do + Resend invite - if current_user_can_admin_project - unless @project.personal? && user == current_user @@ -25,12 +41,12 @@ = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: "Leave project?"}, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do %i.fa.fa-minus.fa-inverse - else - = link_to namespace_project_project_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do + = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do %i.fa.fa-minus.fa-inverse .edit-member.hide.js-toggle-content %br - = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member.user), remote: true do |f| + = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member), remote: true do |f| .prepend-top-10 = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: 'form-control' .prepend-top-10 diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml index 49ce6c0888e..35c15cf3a9e 100644 --- a/app/views/projects/refs/logs_tree.js.haml +++ b/app/views/projects/refs/logs_tree.js.haml @@ -15,5 +15,5 @@ if(current_url == log_url) { // Load 10 more commit log for each file in tree // if we still on the same page - ajaxGet('#{logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '/', offset: (@offset + @limit))}'); + ajaxGet('#{logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: (@offset + @limit))}'); } diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index 26669fb00a9..b9486a9b492 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -3,14 +3,14 @@ - split_button = split_button || false - if split_button == true %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do %i.fa.fa-download %span Download zip - %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } + %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } %span.caret %span.sr-only Select Archive Format - %ul.dropdown-menu{ role: 'menu' } + %ul.col-xs-10.dropdown-menu{ role: 'menu' } %li = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do %i.fa.fa-download diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index cfa6f558dd6..4464c51744a 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -4,15 +4,27 @@ = render "home_panel" -- readme = @repository.readme %ul.nav.nav-tabs %li.active = link_to '#tab-activity', 'data-toggle' => 'tab' do Activity - - if readme + - if @repository.readme %li = link_to '#tab-readme', 'data-toggle' => 'tab' do Readme + - if @repository.changelog + %li + = link_to changelog_url(@project) do + Changelog + - if @repository.contribution_guide + %li + = link_to contribution_guide_url(@project) do + Contribution guide + - if @repository.license + %li + = link_to license_url(@project) do + License + .project-home-links - unless @project.empty_repo? = link_to pluralize(number_with_delimiter(@repository.commit_count), 'commit'), namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) @@ -48,27 +60,21 @@ - unless @project.empty_repo? = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-block' do + %i.fa.fa-exchange Compare code + - if can?(current_user, :download_code, @project) + = render 'projects/repositories/download_archive', split_button: true, btn_class: 'btn-block' + - if version = @repository.version - detail_url = changelog_url(@project) || version_url(@project) = link_to detail_url, class: 'btn btn-block' do + %i.fa.fa-file-text-o Version: %span.count = @repository.blob_by_oid(version.id).data - - elsif @repository.changelog - = link_to changelog_url(@project), class: 'btn btn-block' do - View changelog - - - if @repository.contribution_guide - = link_to contribution_guide_url(@project), class: 'btn btn-block' do - View contribution guide - - if @repository.license - = link_to license_url(@project), class: 'btn btn-block' do - View license - - .prepend-top-10 + .prepend-top-10.append-bottom-10 %p %span.light Created on #{@project.created_at.stamp('Aug 22, 2013')} @@ -79,8 +85,11 @@ - else #{link_to @project.owner_name, @project.owner} + + .prepend-top-10 - @project.ci_services.each do |ci_service| - if ci_service.active? && ci_service.respond_to?(:builds_path) + %hr - if ci_service.respond_to?(:status_img_path) = link_to ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' do = image_tag ci_service.status_img_path, alt: "build status" @@ -88,7 +97,7 @@ %span.light CI provided by = link_to ci_service.title, ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' - - if readme + - if readme = @repository.readme .tab-pane#tab-readme %article.readme-holder#README = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do @@ -97,4 +106,3 @@ = readme.name .wiki = render_readme(readme) - diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index f22308e54b0..28ad272322f 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -9,9 +9,9 @@ = strip_gpg_signature(tag.message) .pull-right - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-sm' + = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-xs' - if can?(current_user, :admin_project, @project) - = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-sm btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do + = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-xs btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do %i.fa.fa-trash-o - if commit diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml index b253fe896e3..02ecbade219 100644 --- a/app/views/projects/tree/_blob_item.html.haml +++ b/app/views/projects/tree/_blob_item.html.haml @@ -1,6 +1,6 @@ %tr{ class: "tree-item #{tree_hex_class(blob_item)}" } %td.tree-item-file-name - = tree_icon(type) + = tree_icon(type, blob_item.mode, blob_item.name) %span.str-truncated = link_to blob_item.name, namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)) %td.tree_time_ago.cgray diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml index 20c70cac699..2b5f671c09e 100644 --- a/app/views/projects/tree/_submodule_item.html.haml +++ b/app/views/projects/tree/_submodule_item.html.haml @@ -1,6 +1,6 @@ %tr{ class: "tree-item" } %td.tree-item-file-name - %i.fa.fa-archive + %i.fa.fa-archive.fa-fw = submodule_link(submodule_item, @ref) %td %td.hidden-xs diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml index 94342bc9b2b..e87138bf980 100644 --- a/app/views/projects/tree/_tree_item.html.haml +++ b/app/views/projects/tree/_tree_item.html.haml @@ -1,6 +1,6 @@ %tr{ class: "tree-item #{tree_hex_class(tree_item)}" } %td.tree-item-file-name - = tree_icon(type) + = tree_icon(type, tree_item.mode, tree_item.name) %span.str-truncated - path = flatten_tree(tree_item) = link_to path, namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)) diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index a1121750ca3..8b1e3a6dd5e 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -15,7 +15,7 @@ :"data-html" => "true", :"data-container" => "body"} = gitlab_config.protocol.upcase - = text_field_tag :project_clone, default_url_to_repo(project), class: "one_click_select form-control", readonly: true + = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true - if project.kind_of?(Project) .input-group-addon .visibility-level-label.has_tooltip{'data-title' => "#{visibility_level_label(project.visibility_level)} project" } diff --git a/app/views/shared/_issuable_filter.html.haml b/app/views/shared/_issuable_filter.html.haml index 5412b9ef0f4..83f5a3a8015 100644 --- a/app/views/shared/_issuable_filter.html.haml +++ b/app/views/shared/_issuable_filter.html.haml @@ -14,106 +14,45 @@ %i.fa.fa-compass All - %div - - if controller.controller_name == 'issues' - .check-all-holder - = check_box_tag "check_all_issues", nil, false, - class: "check_all_issues left", - disabled: !can?(current_user, :modify_issue, @project) - .issues-other-filters - .dropdown.inline.assignee-filter - %button.dropdown-toggle.btn{type: 'button', "data-toggle" => "dropdown"} - %i.fa.fa-user - %span.light assignee: - - if @assignee.present? - %strong= @assignee.name - - elsif params[:assignee_id] == "0" - Unassigned - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to page_filter_path(assignee_id: nil) do - Any - = link_to page_filter_path(assignee_id: 0) do - Unassigned - - @assignees.sort_by(&:name).each do |user| - %li - = link_to page_filter_path(assignee_id: user.id) do - = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' - = user.name + .issues-details-filters + = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_id, :label_name]), method: :get, class: 'filter-form' do + - if controller.controller_name == 'issues' + .check-all-holder + = check_box_tag "check_all_issues", nil, false, + class: "check_all_issues left", + disabled: !can?(current_user, :modify_issue, @project) + .issues-other-filters + .filter-item.inline + = users_select_tag(:assignee_id, selected: params[:assignee_id], + placeholder: 'Assignee', class: 'trigger-submit', any_user: true, null_user: true, first_user: true) + + .filter-item.inline + = users_select_tag(:author_id, selected: params[:author_id], + placeholder: 'Author', class: 'trigger-submit', any_user: true, first_user: true) + + .filter-item.inline.milestone-filter + = select_tag('milestone_id', projects_milestones_options, class: "select2 trigger-submit", prompt: 'Milestone') - .dropdown.inline.prepend-left-10.author-filter - %button.dropdown-toggle.btn{type: 'button', "data-toggle" => "dropdown"} - %i.fa.fa-user - %span.light author: - - if @author.present? - %strong= @author.name - - elsif params[:author_id] == "0" - Unassigned - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to page_filter_path(author_id: nil) do - Any - = link_to page_filter_path(author_id: 0) do - Unassigned - - @authors.sort_by(&:name).each do |user| - %li - = link_to page_filter_path(author_id: user.id) do - = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' - = user.name + - if @project + .filter-item.inline.labels-filter + = select_tag('label_name', project_labels_options(@project), class: "select2 trigger-submit", prompt: 'Label') - .dropdown.inline.prepend-left-10.milestone-filter - %button.dropdown-toggle.btn{type: 'button', "data-toggle" => "dropdown"} - %i.fa.fa-clock-o - %span.light milestone: - - if @milestone.present? - %strong= @milestone.title - - elsif params[:milestone_id] == "0" - None (backlog) - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to page_filter_path(milestone_id: nil) do - Any - = link_to page_filter_path(milestone_id: 0) do - None (backlog) - - @milestones.each do |milestone| - %li - = link_to page_filter_path(milestone_id: milestone.id) do - %strong= milestone.title - %small.light= milestone.expires_at + .pull-right + = render 'shared/sort_dropdown' + + - if controller.controller_name == 'issues' + .issues_bulk_update.hide + = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do + = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status", class: 'form-control') + = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true) + = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") + = hidden_field_tag 'update[issues_ids]', [] + = hidden_field_tag :state_event, params[:state_event] + = button_tag "Update issues", class: "btn update_selected_issues btn-save" - - if @project - .dropdown.inline.prepend-left-10.labels-filter - %button.dropdown-toggle.btn{type: 'button', "data-toggle" => "dropdown"} - %i.fa.fa-tags - %span.light label: - - if params[:label_name].present? - %strong= params[:label_name] - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to page_filter_path(label_name: nil) do - Any - - if @project.labels.any? - - @project.labels.each do |label| - %li - = link_to page_filter_path(label_name: label.name) do - = render_colored_label(label) - - else - %li - = link_to generate_namespace_project_labels_path(@project.namespace, @project, redirect: request.original_url), method: :post do - %i.fa.fa-plus-circle - Create default labels +:coffeescript + new UsersSelect() - .pull-right - = render 'shared/sort_dropdown' + $('form.filter-form').on 'submit', (event) -> + event.preventDefault() + Turbolinks.visit @.action + '&' + $(@).serialize() diff --git a/app/views/shared/_project.html.haml b/app/views/shared/_project.html.haml index 8746970c239..722a7f7ce0f 100644 --- a/app/views/shared/_project.html.haml +++ b/app/views/shared/_project.html.haml @@ -1,4 +1,4 @@ -= cache [project, controller.controller_name, controller.action_name] do += cache [project.namespace, project, controller.controller_name, controller.action_name] do = link_to project_path(project), class: dom_class(project) do - if avatar .dash-project-avatar diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 55a990c94ed..5204fb9a907 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -17,7 +17,7 @@ %span.light by = link_to user_snippets_path(@snippet.author) do - = image_tag avatar_icon(@snippet.author_email), class: "avatar avatar-inline s16" + = image_tag avatar_icon(@snippet.author_email), class: "avatar avatar-inline s16", alt: '' = @snippet.author_name .back-link diff --git a/app/views/users/_profile.html.haml b/app/views/users/_profile.html.haml index bca71444956..90d9980c85c 100644 --- a/app/views/users/_profile.html.haml +++ b/app/views/users/_profile.html.haml @@ -5,6 +5,10 @@ %li %span.light Member since %strong= user.created_at.stamp("Aug 21, 2011") + - unless user.public_email.blank? + %li + %span.light E-mail: + %strong= link_to user.public_email, "mailto:#{user.public_email}" - unless user.skype.blank? %li %span.light Skype: diff --git a/app/views/users/_projects.html.haml b/app/views/users/_projects.html.haml index b7383d5594e..297fa537394 100644 --- a/app/views/users/_projects.html.haml +++ b/app/views/users/_projects.html.haml @@ -1,13 +1,13 @@ -- if @contributed_projects.present? +- if local_assigns.has_key?(:contributed_projects) && contributed_projects.present? .panel.panel-default.contributed-projects .panel-heading Projects contributed to = render 'shared/projects_list', - projects: @contributed_projects.sort_by(&:star_count).reverse, + projects: contributed_projects.sort_by(&:star_count).reverse, projects_limit: 5, stars: true, avatar: false -- if @projects.present? +- if local_assigns.has_key?(:projects) && projects.present? .panel.panel-default .panel-heading Personal projects = render 'shared/projects_list', - projects: @projects.sort_by(&:star_count).reverse, + projects: projects.sort_by(&:star_count).reverse, projects_limit: 10, stars: true, avatar: false diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 5e1d65e2ed8..9dd8cb0738c 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -44,7 +44,7 @@ = spinner %aside.col-md-4 = render 'profile', user: @user - = render 'projects' + = render 'projects', projects: @projects, contributed_projects: @contributed_projects :coffeescript $(".user-calendar").load("#{user_calendar_path}") diff --git a/app/workers/fork_registration_worker.rb b/app/workers/fork_registration_worker.rb new file mode 100644 index 00000000000..fffa8b3a659 --- /dev/null +++ b/app/workers/fork_registration_worker.rb @@ -0,0 +1,12 @@ +class ForkRegistrationWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform(from_project_id, to_project_id, private_token) + from_project = Project.find(from_project_id) + to_project = Project.find(to_project_id) + + from_project.gitlab_ci_service.fork_registration(to_project, private_token) + end +end diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 0c3ee6ba4ff..33d8cc8861b 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -11,8 +11,8 @@ class PostReceive 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$/, "") - repo_path.gsub!(/^\//, "") + repo_path.gsub!(/\.git\z/, "") + repo_path.gsub!(/\A\//, "") project = Project.find_with_namespace(repo_path) diff --git a/app/workers/repository_archive_worker.rb b/app/workers/repository_archive_worker.rb new file mode 100644 index 00000000000..021c1139568 --- /dev/null +++ b/app/workers/repository_archive_worker.rb @@ -0,0 +1,43 @@ +class RepositoryArchiveWorker + include Sidekiq::Worker + + sidekiq_options queue: :archive_repo + + attr_accessor :project, :ref, :format + + def perform(project_id, ref, format) + @project = Project.find(project_id) + @ref, @format = ref, format.downcase + + repository = project.repository + + repository.clean_old_archives + + return unless file_path + return if archived? || archiving? + + repository.archive_repo(ref, storage_path, format) + end + + private + + def storage_path + Gitlab.config.gitlab.repository_downloads_path + end + + def file_path + @file_path ||= project.repository.archive_file_path(ref, storage_path, format) + end + + def pid_file_path + @pid_file_path ||= project.repository.archive_pid_file_path(ref, storage_path, format) + end + + def archived? + File.exist?(file_path) + end + + def archiving? + File.exist?(pid_file_path) + end +end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 437640d2305..e6a50afedb1 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -18,6 +18,8 @@ class RepositoryImportWorker Gitlab::GitlabImport::Importer.new(project).execute elsif project.import_type == 'bitbucket' Gitlab::BitbucketImport::Importer.new(project).execute + elsif project.import_type == 'google_code' + Gitlab::GoogleCodeImport::Importer.new(project).execute else true end diff --git a/bin/background_jobs b/bin/background_jobs index 59a51c5c868..a041a4b0433 100755 --- a/bin/background_jobs +++ b/bin/background_jobs @@ -37,7 +37,7 @@ start_no_deamonize() start_sidekiq() { - bundle exec sidekiq -q post_receive -q mailer -q system_hook -q project_web_hook -q gitlab_shell -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 + bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 } load_ok() diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 11278ef40dc..ba40671b162 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -49,6 +49,7 @@ production: &base # Email address used in the "From" field in mails sent by GitLab email_from: example@example.com email_display_name: GitLab + email_reply_to: noreply@example.com # Email server smtp settings are in config/initializers/smtp_settings.rb.sample @@ -66,8 +67,8 @@ production: &base # If a commit message matches this regular expression, all issues referenced from the matched text will be closed. # This happens when the commit is pushed or merged into the default branch of a project. # When not specified the default issue_closing_pattern as specified below will be used. - # Tip: you can test your closing pattern at http://rubular.com - # issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' + # Tip: you can test your closing pattern at http://rubular.com. + # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' ## Default project features settings default_projects_features: @@ -86,35 +87,6 @@ production: &base # The default is 'tmp/repositories' relative to the root of the Rails app. # repository_downloads_path: tmp/repositories - ## External issues trackers - issues_tracker: - # redmine: - # title: "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, links 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: - # title: "Atlassian 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 ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html gravatar: @@ -133,6 +105,15 @@ production: &base ldap: enabled: false servers: + ########################################################################## + # + # Since GitLab 7.4, LDAP servers get ID's (below the ID is 'main'). GitLab + # Enterprise Edition now supports connecting to multiple LDAP servers. + # + # If you are updating from the old (pre-7.4) syntax, you MUST give your + # old server the ID 'main'. + # + ########################################################################## main: # 'main' is the GitLab 'provider ID' of this LDAP server ## label # @@ -165,6 +146,11 @@ production: &base # disable this setting, because the userPrincipalName contains an '@'. allow_username_or_email_login: false + # To maintain tight control over the number of active users on your GitLab installation, + # enable this setting to keep new users blocked until they have been cleared by the admin + # (default: false). + block_auto_created_users: false + # Base where we can search for users # # Ex. ou=People,dc=gitlab,dc=example diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 15c1ae9466f..0abd34fc3e0 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -1,3 +1,5 @@ +require 'gitlab' # Load lib/gitlab.rb as soon as possible + class Settings < Settingslogic source ENV.fetch('GITLAB_CONFIG') { "#{Rails.root}/config/gitlab.yml" } namespace Rails.env @@ -64,15 +66,17 @@ Settings.ldap['enabled'] = false if Settings.ldap['enabled'].nil? # backwards compatibility, we only have one host if Settings.ldap['enabled'] || Rails.env.test? if Settings.ldap['host'].present? + # We detected old LDAP configuration syntax. Update the config to make it + # look like it was entered with the new syntax. server = Settings.ldap.except('sync_time') - server['provider_name'] = 'ldap' Settings.ldap['servers'] = { - 'ldap' => server + 'main' => server } end Settings.ldap['servers'].each do |key, server| server['label'] ||= 'LDAP' + server['block_auto_created_users'] = false if server['block_auto_created_users'].nil? server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil? server['active_directory'] = true if server['active_directory'].nil? server['provider_name'] ||= "ldap#{key}".downcase @@ -80,6 +84,7 @@ if Settings.ldap['enabled'] || Rails.env.test? end end + Settings['omniauth'] ||= Settingslogic.new({}) Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? Settings.omniauth['providers'] ||= [] @@ -103,6 +108,7 @@ Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil? Settings.gitlab['email_from'] ||= "gitlab@#{Settings.gitlab.host}" Settings.gitlab['email_display_name'] ||= "GitLab" +Settings.gitlab['email_reply_to'] ||= "noreply@#{Settings.gitlab.host}" Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url) Settings.gitlab['user'] ||= 'git' Settings.gitlab['user_home'] ||= begin @@ -119,6 +125,7 @@ Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' if Settings.gitlab['issue_closing_pattern'].nil? Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 +Settings.gitlab['max_attachment_size'] ||= 10 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? diff --git a/config/initializers/2_app.rb b/config/initializers/2_app.rb index 655590dff0b..688cdf5f4b0 100644 --- a/config/initializers/2_app.rb +++ b/config/initializers/2_app.rb @@ -6,8 +6,3 @@ module Gitlab Settings end end - -# -# Load all libs for threadsafety -# -Dir["#{Rails.root}/lib/**/*.rb"].each { |file| require file } diff --git a/config/initializers/5_backend.rb b/config/initializers/5_backend.rb index 7c2e7f39000..80d641d73a3 100644 --- a/config/initializers/5_backend.rb +++ b/config/initializers/5_backend.rb @@ -6,3 +6,10 @@ require Rails.root.join("lib", "gitlab", "backend", "shell") # GitLab shell adapter require Rails.root.join("lib", "gitlab", "backend", "shell_adapter") + +required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required) +current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.new.version) + +unless current_version.valid? && required_version <= current_version + warn "WARNING: This version of GitLab depends on gitlab-shell #{required_version}, but you're running #{current_version}. Please update gitlab-shell." +end diff --git a/config/initializers/acts_as_taggable_on_patch.rb b/config/initializers/acts_as_taggable_on_patch.rb deleted file mode 100644 index 0d535cb5cac..00000000000 --- a/config/initializers/acts_as_taggable_on_patch.rb +++ /dev/null @@ -1,131 +0,0 @@ -# This is a patch to address the issue in https://github.com/mbleigh/acts-as-taggable-on/issues/427 caused by -# https://github.com/rails/rails/commit/31a43ebc107fbd50e7e62567e5208a05909ec76c -# gem 'acts-as-taggable-on' has the fix included https://github.com/mbleigh/acts-as-taggable-on/commit/89bbed3864a9252276fb8dd7d535fce280454b90 -# but not in the currently used version of gem ('2.4.1') -# With replacement of 'acts-as-taggable-on' gem this file will become obsolete - -module ActsAsTaggableOn::Taggable - module Core - module ClassMethods - def tagged_with(tags, options = {}) - tag_list = ActsAsTaggableOn::TagList.from(tags) - empty_result = where("1 = 0") - - return empty_result if tag_list.empty? - - joins = [] - conditions = [] - having = [] - select_clause = [] - - context = options.delete(:on) - owned_by = options.delete(:owned_by) - alias_base_name = undecorated_table_name.gsub('.','_') - quote = ActsAsTaggableOn::Tag.using_postgresql? ? '"' : '' - - if options.delete(:exclude) - if options.delete(:wild) - tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ") - else - tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ") - end - - conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)})" - - if owned_by - joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" + - " ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}" + - " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{owned_by.id}" + - " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}" - end - - elsif options.delete(:any) - # get tags, drop out if nothing returned (we need at least one) - tags = - if options.delete(:wild) - ActsAsTaggableOn::Tag.named_like_any(tag_list) - else - ActsAsTaggableOn::Tag.named_any(tag_list) - end - - return empty_result unless tags.length > 0 - - # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123 - # avoid ambiguous column name - taggings_context = context ? "_#{context}" : '' - - taggings_alias = adjust_taggings_alias( - "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}" - ) - - tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" + - " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" - tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context - - # don't need to sanitize sql, map all ids and join with OR logic - conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ") - select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one? - - if owned_by - tagging_join << " AND " + - sanitize_sql([ - "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?", - owned_by.id, - owned_by.class.base_class.to_s - ]) - end - - joins << tagging_join - else - tags = ActsAsTaggableOn::Tag.named_any(tag_list) - - return empty_result unless tags.length == tag_list.length - - tags.each do |tag| - taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}") - tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" + - " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" + - " AND #{taggings_alias}.tag_id = #{tag.id}" - - tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context - - if owned_by - tagging_join << " AND " + - sanitize_sql([ - "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?", - owned_by.id, - owned_by.class.base_class.to_s - ]) - end - - joins << tagging_join - end - end - - taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group" - - if options.delete(:match_all) - joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" + - " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" - - - group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}" - group = group_columns - having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}" - end - - select(select_clause) \ - .joins(joins.join(" ")) \ - .where(conditions.join(" AND ")) \ - .group(group) \ - .having(having) \ - .order(options[:order]) \ - .readonly(false) - end - end - end -end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 79abe3c695d..9dce495106f 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -208,7 +208,7 @@ Devise.setup do |config| if Gitlab::LDAP::Config.enabled? Gitlab.config.ldap.servers.values.each do |server| if server['allow_username_or_email_login'] - email_stripping_proc = ->(name) {name.gsub(/@.*$/,'')} + email_stripping_proc = ->(name) {name.gsub(/@.*\z/,'')} else email_stripping_proc = ->(name) {name} end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 9da7ebf4290..d422acb31d6 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -11,7 +11,7 @@ Doorkeeper.configure do end resource_owner_from_credentials do |routes| - u = User.find_by(email: params[:username]) + u = User.find_by(email: params[:username]) || User.find_by(username: params[:username]) u if u && u.valid_password?(params[:password]) end @@ -83,7 +83,7 @@ Doorkeeper.configure do # # If not specified, Doorkeeper enables all the four grant flows. # - # grant_flows %w(authorization_code implicit password client_credentials) + grant_flows %w(authorization_code password client_credentials) # Under some circumstances you might want to have applications auto-approved, # so that the user skips the authorization step. diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 8f8bef42bef..ca58ae92d1b 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -6,3 +6,5 @@ Mime::Type.register_alias "text/plain", :diff Mime::Type.register_alias "text/plain", :patch +Mime::Type.register_alias 'text/html', :markdown +Mime::Type.register_alias 'text/html', :md diff --git a/config/routes.rb b/config/routes.rb index c30cd768572..744a99feded 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,6 +8,11 @@ Gitlab::Application.routes.draw do authorizations: 'oauth/authorizations' end + # Autocomplete + get '/autocomplete/users' => 'autocomplete#users' + get '/autocomplete/users/:id' => 'autocomplete#user' + + # Search get 'search' => 'search#show' get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete @@ -34,7 +39,7 @@ Gitlab::Application.routes.draw do # Help get 'help' => 'help#index' - get 'help/:category/:file' => 'help#show', as: :help_page + get 'help/:category/:file' => 'help#show', as: :help_page, constraints: { category: /.*/, file: /[^\/\.]+/ } get 'help/shortcuts' get 'help/ui' => 'help#ui' @@ -48,6 +53,16 @@ Gitlab::Application.routes.draw do end get '/s/:username' => 'snippets#user_index', as: :user_snippets, constraints: { username: /.*/ } + # + # Invites + # + + resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do + member do + post :accept + match :decline, via: [:get, :post] + end + end # # Import @@ -76,6 +91,15 @@ Gitlab::Application.routes.draw do get :callback get :jobs end + + resource :google_code, only: [:create, :new], controller: :google_code do + get :status + post :callback + get :jobs + + get :new_user_map, path: :user_map + post :create_user_map, path: :user_map + end end # @@ -86,18 +110,18 @@ Gitlab::Application.routes.draw do # Note attachments and User/Group/Project avatars get ":model/:mounted_as/:id/:filename", to: "uploads#show", - constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ } + constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ } # Project markdown uploads get ":namespace_id/:project_id/:secret/:filename", to: "projects/uploads#show", - constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /.+/ } + constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ } end # Redirect old note attachments path to new uploads path. get "files/note/:id/:filename", to: redirect("uploads/note/attachment/%{id}/%{filename}"), - constraints: { filename: /.+/ } + constraints: { filename: /[^\/]+/ } # # Explore area @@ -140,6 +164,8 @@ Gitlab::Application.routes.draw do end end + resources :deploy_keys, only: [:index, :show, :new, :create, :destroy] + resources :hooks, only: [:index, :create, :destroy] do get :test end @@ -244,6 +270,7 @@ Gitlab::Application.routes.draw do scope module: :groups do resources :group_members, only: [:index, :create, :update, :destroy] do + post :resend_invite, on: :member delete :leave, on: :collection end @@ -388,7 +415,7 @@ Gitlab::Application.routes.draw do end end - resources :deploy_keys, constraints: { id: /\d+/ } do + resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :show, :new, :create] do member do put :enable put :disable @@ -470,6 +497,10 @@ Gitlab::Application.routes.draw do get :import post :apply_import end + + member do + post :resend_invite + end end resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do @@ -480,7 +511,7 @@ Gitlab::Application.routes.draw do resources :uploads, only: [:create] do collection do - get ":secret/:filename", action: :show, as: :show, constraints: { filename: /.+/ } + get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ } end end end diff --git a/db/migrate/20150324133047_remove_periods_at_ends_of_usernames.rb b/db/migrate/20150324133047_remove_periods_at_ends_of_usernames.rb new file mode 100644 index 00000000000..dc38b0eceb7 --- /dev/null +++ b/db/migrate/20150324133047_remove_periods_at_ends_of_usernames.rb @@ -0,0 +1,76 @@ +class RemovePeriodsAtEndsOfUsernames < ActiveRecord::Migration + include Gitlab::ShellAdapter + + class Namespace < ActiveRecord::Base + class << self + def find_by_path_or_name(path) + find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) + end + + def clean_path(path) + path = path.dup + path.gsub!(/@.*\z/, "") + path.gsub!(/\.git\z/, "") + path.gsub!(/\A-+/, "") + path.gsub!(/\.+\z/, "") + path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + + counter = 0 + base = path + while Namespace.find_by_path_or_name(path) + counter += 1 + path = "#{base}#{counter}" + end + + path + end + end + end + + def up + changed_paths = {} + + select_all("SELECT id, username FROM users WHERE username LIKE '%.'").each do |user| + username_was = user["username"] + username = Namespace.clean_path(username_was) + changed_paths[username_was] = username + + username = quote_string(username) + execute "UPDATE users SET username = '#{username}' WHERE id = #{user["id"]}" + execute "UPDATE namespaces SET path = '#{username}', name = '#{username}' WHERE type IS NULL AND owner_id = #{user["id"]}" + end + + select_all("SELECT id, path FROM namespaces WHERE type = 'Group' AND path LIKE '%.'").each do |group| + path_was = group["path"] + path = Namespace.clean_path(path_was) + changed_paths[path_was] = path + + path = quote_string(path) + execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{group["id"]}" + end + + changed_paths.each do |path_was, path| + 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 + gitlab_shell.rm_satellites(path_was) + # We cannot send update instructions since models and mailers + # can't safely be used from migrations as they may be written for + # later versions of the database. + # send_update_instructions + 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 + end +end diff --git a/db/migrate/20150327122227_add_public_to_key.rb b/db/migrate/20150327122227_add_public_to_key.rb new file mode 100644 index 00000000000..6ffbf4cda19 --- /dev/null +++ b/db/migrate/20150327122227_add_public_to_key.rb @@ -0,0 +1,5 @@ +class AddPublicToKey < ActiveRecord::Migration + def change + add_column :keys, :public, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20150327150017_add_import_data_to_project.rb b/db/migrate/20150327150017_add_import_data_to_project.rb new file mode 100644 index 00000000000..12c00339eec --- /dev/null +++ b/db/migrate/20150327150017_add_import_data_to_project.rb @@ -0,0 +1,5 @@ +class AddImportDataToProject < ActiveRecord::Migration + def change + add_column :projects, :import_data, :text + end +end diff --git a/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb b/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb new file mode 100644 index 00000000000..1d161674a9a --- /dev/null +++ b/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddMaxAttachmentSizeToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :max_attachment_size, :integer, default: 10, null: false + end +end diff --git a/db/migrate/20150406133311_add_invite_data_to_member.rb b/db/migrate/20150406133311_add_invite_data_to_member.rb new file mode 100644 index 00000000000..3452fd45c4f --- /dev/null +++ b/db/migrate/20150406133311_add_invite_data_to_member.rb @@ -0,0 +1,12 @@ +class AddInviteDataToMember < ActiveRecord::Migration + def change + add_column :members, :created_by_id, :integer + add_column :members, :invite_email, :string + add_column :members, :invite_token, :string + add_column :members, :invite_accepted_at, :datetime + + change_column :members, :user_id, :integer, null: true + + add_index :members, :invite_token, unique: true + end +end diff --git a/db/migrate/20150411000035_fix_identities.rb b/db/migrate/20150411000035_fix_identities.rb new file mode 100644 index 00000000000..d9051f9fffd --- /dev/null +++ b/db/migrate/20150411000035_fix_identities.rb @@ -0,0 +1,45 @@ +class FixIdentities < ActiveRecord::Migration + def up + # Up until now, legacy 'ldap' references in the database were charitably + # interpreted to point to the first LDAP server specified in the GitLab + # configuration. So if the database said 'provider: ldap' but the first + # LDAP server was called 'ldapmain', then we would try to interpret + # 'provider: ldap' as if it said 'provider: ldapmain'. This migration (and + # accompanying changes in the GitLab LDAP code) get rid of this complicated + # behavior. Any database references to 'provider: ldap' get rewritten to + # whatever the code would have interpreted it as, i.e. as a reference to + # the first LDAP server specified in gitlab.yml / gitlab.rb. + new_provider = if Gitlab.config.ldap.enabled + first_ldap_server = Gitlab.config.ldap.servers.values.first + first_ldap_server['provider_name'] + else + 'ldapmain' + end + + # Delete duplicate identities + # We use a sort of self-join to find rows in identities which match on + # user_id but where one has provider 'ldap'. We delete the duplicate row + # with provider 'ldap'. + delete_statement = '' + case adapter_name.downcase + when /^mysql/ + delete_statement << 'DELETE FROM id1 USING identities AS id1, identities AS id2' + when 'postgresql' + delete_statement << 'DELETE FROM identities AS id1 USING identities AS id2' + else + raise "Unknown DB adapter: #{adapter_name}" + end + delete_statement << " WHERE id1.user_id = id2.user_id AND id1.provider = 'ldap' AND id2.provider = '#{new_provider}'" + execute delete_statement + + # Update legacy identities + execute "UPDATE identities SET provider = '#{new_provider}' WHERE provider = 'ldap'" + + if table_exists?('ldap_group_links') + execute "UPDATE ldap_group_links SET provider = '#{new_provider}' WHERE provider IS NULL OR provider = 'ldap'" + end + end + + def down + end +end diff --git a/db/migrate/20150411180045_rename_buildbox_service.rb b/db/migrate/20150411180045_rename_buildbox_service.rb new file mode 100644 index 00000000000..5a0b5d07e50 --- /dev/null +++ b/db/migrate/20150411180045_rename_buildbox_service.rb @@ -0,0 +1,9 @@ +class RenameBuildboxService < ActiveRecord::Migration + def up + execute "UPDATE services SET type = 'BuildkiteService' WHERE type = 'BuildboxService';" + end + + def down + execute "UPDATE services SET type = 'BuildboxService' WHERE type = 'BuildkiteService';" + end +end diff --git a/db/migrate/20150413192223_add_public_email_to_users.rb b/db/migrate/20150413192223_add_public_email_to_users.rb new file mode 100644 index 00000000000..700e9f343a6 --- /dev/null +++ b/db/migrate/20150413192223_add_public_email_to_users.rb @@ -0,0 +1,5 @@ +class AddPublicEmailToUsers < ActiveRecord::Migration + def change + add_column :users, :public_email, :string, default: "", null: false + end +end diff --git a/db/migrate/20150417121913_create_project_import_data.rb b/db/migrate/20150417121913_create_project_import_data.rb new file mode 100644 index 00000000000..c78f5fde85e --- /dev/null +++ b/db/migrate/20150417121913_create_project_import_data.rb @@ -0,0 +1,8 @@ +class CreateProjectImportData < ActiveRecord::Migration + def change + create_table :project_import_data do |t| + t.references :project + t.text :data + end + end +end diff --git a/db/migrate/20150417122318_remove_import_data_from_project.rb b/db/migrate/20150417122318_remove_import_data_from_project.rb new file mode 100644 index 00000000000..c275b49d228 --- /dev/null +++ b/db/migrate/20150417122318_remove_import_data_from_project.rb @@ -0,0 +1,5 @@ +class RemoveImportDataFromProject < ActiveRecord::Migration + def change + remove_column :projects, :import_data + end +end diff --git a/db/schema.rb b/db/schema.rb index 4a445ae5832..1aee37b2e61 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150324155957) do +ActiveRecord::Schema.define(version: 20150417122318) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -28,6 +28,7 @@ ActiveRecord::Schema.define(version: 20150324155957) do t.integer "default_branch_protection", default: 2 t.boolean "twitter_sharing_enabled", default: true t.text "restricted_visibility_levels" + t.integer "max_attachment_size", default: 10, null: false end create_table "broadcast_messages", force: true do |t| @@ -131,6 +132,7 @@ ActiveRecord::Schema.define(version: 20150324155957) do t.string "title" t.string "type" t.string "fingerprint" + t.boolean "public", default: false, null: false end add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree @@ -161,15 +163,20 @@ ActiveRecord::Schema.define(version: 20150324155957) do t.integer "access_level", null: false t.integer "source_id", null: false t.string "source_type", null: false - t.integer "user_id", null: false + t.integer "user_id" t.integer "notification_level", null: false t.string "type" t.datetime "created_at" t.datetime "updated_at" + t.integer "created_by_id" + t.string "invite_email" + t.string "invite_token" + t.datetime "invite_accepted_at" end add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree add_index "members", ["created_at", "id"], name: "index_members_on_created_at_and_id", using: :btree + add_index "members", ["invite_token"], name: "index_members_on_invite_token", unique: true, using: :btree add_index "members", ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree add_index "members", ["type"], name: "index_members_on_type", using: :btree add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree @@ -316,6 +323,11 @@ ActiveRecord::Schema.define(version: 20150324155957) do add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree + create_table "project_import_data", force: true do |t| + t.integer "project_id" + t.text "data" + end + create_table "projects", force: true do |t| t.string "name" t.string "path" @@ -458,6 +470,7 @@ ActiveRecord::Schema.define(version: 20150324155957) do t.integer "notification_level", default: 1, null: false t.datetime "password_expires_at" t.integer "created_by_id" + t.datetime "last_credential_check_at" t.string "avatar" t.string "confirmation_token" t.datetime "confirmed_at" @@ -465,7 +478,6 @@ ActiveRecord::Schema.define(version: 20150324155957) do t.string "unconfirmed_email" t.boolean "hide_no_ssh_key", default: false t.string "website_url", default: "", null: false - t.datetime "last_credential_check_at" t.string "github_access_token" t.string "gitlab_access_token" t.string "notification_email" @@ -474,6 +486,7 @@ ActiveRecord::Schema.define(version: 20150324155957) do t.string "bitbucket_access_token" t.string "bitbucket_access_token_secret" t.string "location" + t.string "public_email", default: "", null: false end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/doc/api/projects.md b/doc/api/projects.md index 7fe244477db..971fe96fb8e 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -44,6 +44,10 @@ Parameters: "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git", "web_url": "http://example.com/diaspora/diaspora-client", + "tag_list": [ + "example", + "disapora client" + ], "owner": { "id": 3, "name": "Diaspora", @@ -59,6 +63,7 @@ Parameters: "snippets_enabled": false, "created_at": "2013-09-30T13: 46: 02Z", "last_activity_at": "2013-09-30T13: 46: 02Z", + "creator_id": 3, "namespace": { "created_at": "2013-09-30T13: 46: 02Z", "description": "", @@ -80,6 +85,10 @@ Parameters: "ssh_url_to_repo": "git@example.com:brightbox/puppet.git", "http_url_to_repo": "http://example.com/brightbox/puppet.git", "web_url": "http://example.com/brightbox/puppet", + "tag_list": [ + "example", + "puppet" + ], "owner": { "id": 4, "name": "Brightbox", @@ -95,6 +104,7 @@ Parameters: "snippets_enabled": false, "created_at": "2013-09-30T13:46:02Z", "last_activity_at": "2013-09-30T13:46:02Z", + "creator_id": 3, "namespace": { "created_at": "2013-09-30T13:46:02Z", "description": "", @@ -163,6 +173,10 @@ Parameters: "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", "web_url": "http://example.com/diaspora/diaspora-project-site", + "tag_list": [ + "example", + "disapora project" + ], "owner": { "id": 3, "name": "Diaspora", @@ -178,6 +192,7 @@ Parameters: "snippets_enabled": false, "created_at": "2013-09-30T13: 46: 02Z", "last_activity_at": "2013-09-30T13: 46: 02Z", + "creator_id": 3, "namespace": { "created_at": "2013-09-30T13: 46: 02Z", "description": "", diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md index ddc0c8eac2b..aa65a082a53 100644 --- a/doc/customization/issue_closing.md +++ b/doc/customization/issue_closing.md @@ -1,5 +1,36 @@ # Issue closing pattern -By default you can close issues from commit messages by saying 'Closes #12' or 'Fixed #101'. +If a commit message matches the regular expression below, all issues referenced from +the matched text will be closed. This happens when the commit is pushed or merged +into the default branch of a project. -If you want to customize the message please do so in [gitlab.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/73b92f85bcd6c213b845cc997843a969cf0906cf/config/gitlab.yml.example#L73) +When not specified, the default issue_closing_pattern as shown below will be used: + +```bash +((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+) +``` + +For example: + +``` +git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes #22). This commit is also related to #17 and fixes #18, #19 and #23." +``` + +will close `#20`, `#21`, `#22`, `#18`, `#19` and `#23`, but `#17` won't be closed +as it does not match the pattern. It also works with multiline commit messages. + +Tip: you can test this closing pattern at [http://rubular.com][1]. Use this site +to test your own patterns. + +## Change the pattern + +For Omnibus installs you can change the default pattern in `/etc/gitlab/gitlab.rb`: + +``` +issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' +``` + +For manual installs you can customize the pattern in [gitlab.yml][0]. + +[0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/40c3675372320febf5264061c9bcd63db2dfd13c/config/gitlab.yml.example#L65 +[1]: http://rubular.com/r/Xmbexed1OJ diff --git a/doc/install/installation.md b/doc/install/installation.md index d6208bb0797..a61a40ebd16 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -103,8 +103,8 @@ Remove the old Ruby 1.8 if present Download Ruby and compile it: mkdir /tmp/ruby && cd /tmp/ruby - curl -L --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.5.tar.gz | tar xz - cd ruby-2.1.5 + curl -L --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz + cd ruby-2.1.6 ./configure --disable-install-rdoc make sudo make install @@ -183,9 +183,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-9-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-10-stable gitlab -**Note:** You can change `7-9-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `7-10-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -280,7 +280,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da GitLab Shell is an SSH access and repository management software developed specially for GitLab. # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.0] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production + sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.2] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production # By default, the gitlab-shell config is generated from your main GitLab config. # You can review (and modify) the gitlab-shell config as follows: diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index 125ce31b521..b67f793c591 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -51,6 +51,11 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server # disable this setting, because the userPrincipalName contains an '@'. allow_username_or_email_login: false + # To maintain tight control over the number of active users on your GitLab installation, + # enable this setting to keep new users blocked until they have been cleared by the admin + # (default: false). + block_auto_created_users: false + # Base where we can search for users # # Ex. ou=People,dc=gitlab,dc=example diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 965d8fc313f..1d5fd4c8b0d 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -421,7 +421,7 @@ Quote break. You can also use raw HTML in your Markdown, and it'll mostly work pretty well. -Note that inline HTML is disabled in the default Gitlab configuration, although it is [possible](https://github.com/gitlabhq/gitlabhq/pull/8007/commits) for the system administrator to enable it. +See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows the `class`, `id`, and `style` attributes. ```no-highlight <dl> @@ -441,8 +441,6 @@ Note that inline HTML is disabled in the default Gitlab configuration, although <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd> </dl> -See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows the `class`, `id`, and `style` attributes. - ## Horizontal Rule ``` diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md index 7c5a6c04639..bd439f7c6f3 100644 --- a/doc/public_access/public_access.md +++ b/doc/public_access/public_access.md @@ -41,4 +41,4 @@ When visiting the public page of an user, you will only see listed projects whic ## Restricting the use of public or internal projects -In [gitlab.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/dbd88d453b8e6c78a423fa7e692004b1db6ea069/config/gitlab.yml.example#L64) you can disable public projects or public and internal projects for the entire GitLab installation to prevent people making code public by accident. The restricted visibility settings do not apply to admin users. +In the Admin area under Settings you can disable public projects or public and internal projects for the entire GitLab installation to prevent people making code public by accident. The restricted visibility settings do not apply to admin users. diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 99cdfff0ac6..2e41fad89e7 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -17,6 +17,13 @@ sudo gitlab-rake gitlab:backup:create sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` +Also you can choose what should be backed up by adding environment variable SKIP. Available options: db, +uploads (attachments), repositories. Use a comma to specify several options at the same time. + +``` +sudo gitlab-rake gitlab:backup:create SKIP=db,uploads +``` + Example output: ``` diff --git a/doc/release/patch.md b/doc/release/patch.md index 68156ae9c0e..4c7b471785f 100644 --- a/doc/release/patch.md +++ b/doc/release/patch.md @@ -35,16 +35,13 @@ git clone git@dev.gitlab.org:gitlab/release-tools.git cd release-tools ``` -Bump version in stable branch, create release tag and push to remotes: +Bump all versions in stable branch, even if the changes affect only EE, CE, or CI. Since all the versions are synced now, +it doesn't make sense to say upgrade CE to 7.2, EE to 7.3 and CI to 7.1. -``` -bundle exec rake release["x.x.x"] -``` - -Or if you need to release only EE: +Create release tag and push to remotes: ``` -CE=false be rake release['x.x.x'] +bundle exec rake release["x.x.x"] ``` ### Release diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 66941521c2e..0acf92fbf54 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -68,6 +68,12 @@ You can't add the same deploy key twice with the 'New Deploy Key' option. If you want to add the same key to another project, please enable it in the list that says 'Deploy keys from projects available to you'. All the deploy keys of all the projects you have access to are available. This project -access can happen through being a direct member of the projecti, or through +access can happen through being a direct member of the project, or through a group. See `def accessible_deploy_keys` in `app/models/user.rb` for more information. + +## Applications + +### Eclipse + +How to add your ssh key to Eclipse: http://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration diff --git a/doc/update/6.6-to-6.7.md b/doc/update/6.6-to-6.7.md index 5622a7001ed..b4298c93429 100644 --- a/doc/update/6.6-to-6.7.md +++ b/doc/update/6.6-to-6.7.md @@ -71,6 +71,9 @@ sudo -u git -H gzip /home/git/gitlab-shell/gitlab-shell.log.1 # Close access to gitlab-satellites for others sudo chmod u+rwx,g=rx,o-rwx /home/git/gitlab-satellites + +# Add directory for uploads +sudo -u git -H mkdir -p /home/git/gitlab/public/uploads ``` ## 5. Start application diff --git a/doc/update/6.x-or-7.x-to-7.9.md b/doc/update/6.x-or-7.x-to-7.10.md index bd6eb6b211f..2ee9a07cee3 100644 --- a/doc/update/6.x-or-7.x-to-7.9.md +++ b/doc/update/6.x-or-7.x-to-7.10.md @@ -1,7 +1,7 @@ -# From 6.x or 7.x to 7.9 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.9.md) for the most up to date instructions.* +# From 6.x or 7.x to 7.10 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.10.md) for the most up to date instructions.* -This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.9. +This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.10. ## Global issue numbers @@ -35,7 +35,7 @@ You can check which version you are running with `ruby -v`. If you are you running Ruby 2.0.x, you do not need to upgrade ruby, but can consider doing so for performance reasons. -If you are running Ruby 2.1.1 consider upgrading to 2.1.5, because of the high memory usage of Ruby 2.1.1. +If you are running Ruby 2.1.1 consider upgrading to 2.1.6, because of the high memory usage of Ruby 2.1.1. Install, update dependencies: @@ -47,8 +47,8 @@ Download and compile Ruby: ```bash mkdir /tmp/ruby && cd /tmp/ruby -curl --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.5.tar.gz | tar xz -cd ruby-2.1.5 +curl --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz +cd ruby-2.1.6 ./configure --disable-install-rdoc make sudo make install @@ -71,7 +71,7 @@ sudo -u git -H git checkout -- db/schema.rb # local changes will be restored aut For GitLab Community Edition: ```bash -sudo -u git -H git checkout 7-9-stable +sudo -u git -H git checkout 7-10-stable ``` OR @@ -79,7 +79,7 @@ OR For GitLab Enterprise Edition: ```bash -sudo -u git -H git checkout 7-9-stable-ee +sudo -u git -H git checkout 7-10-stable-ee ``` ## 4. Install additional packages @@ -161,11 +161,11 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab TIP: to see what changed in `gitlab.yml.example` in this release use next command: ``` -git diff 6-0-stable:config/gitlab.yml.example 7-9-stable:config/gitlab.yml.example +git diff 6-0-stable:config/gitlab.yml.example 7-10-stable:config/gitlab.yml.example ``` -* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-9-stable/config/gitlab.yml.example but with your settings. -* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-9-stable/config/unicorn.rb.example but with your settings. +* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/config/unicorn.rb.example but with your settings. * Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.6.0/config.yml.example but with your settings. * Copy rack attack middleware config @@ -181,8 +181,8 @@ sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab ### Change Nginx settings -* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-9-stable/lib/support/nginx/gitlab but with your settings. -* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-9-stable/lib/support/nginx/gitlab-ssl but with your settings. +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/lib/support/nginx/gitlab but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/lib/support/nginx/gitlab-ssl but with your settings. * A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section. ## 9. Start application diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 6e70235f5b8..7e996dc47d4 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -1,6 +1,7 @@ # Workflow - [Feature branch workflow](workflow.md) +- [Project forking workflow](forking_workflow.md) - [Project Features](project_features.md) - [Authorization for merge requests](authorization_for_merge_requests.md) - [Groups](groups.md) diff --git a/doc/workflow/forking/branch_select.png b/doc/workflow/forking/branch_select.png Binary files differnew file mode 100644 index 00000000000..275f64d113b --- /dev/null +++ b/doc/workflow/forking/branch_select.png diff --git a/doc/workflow/forking/fork_button.png b/doc/workflow/forking/fork_button.png Binary files differnew file mode 100644 index 00000000000..def4266476a --- /dev/null +++ b/doc/workflow/forking/fork_button.png diff --git a/doc/workflow/forking/groups.png b/doc/workflow/forking/groups.png Binary files differnew file mode 100644 index 00000000000..3ac64b3c8e7 --- /dev/null +++ b/doc/workflow/forking/groups.png diff --git a/doc/workflow/forking/merge_request.png b/doc/workflow/forking/merge_request.png Binary files differnew file mode 100644 index 00000000000..2dc00ed08a1 --- /dev/null +++ b/doc/workflow/forking/merge_request.png diff --git a/doc/workflow/forking_workflow.md b/doc/workflow/forking_workflow.md new file mode 100644 index 00000000000..8edf7c6ab3d --- /dev/null +++ b/doc/workflow/forking_workflow.md @@ -0,0 +1,36 @@ +# Project forking workflow + +Forking a project to your own namespace is useful if you have no write access to the project you want to contribute +to. If you do have write access or can request it we recommend working together in the same repository since it is simpler. +See our **[GitLab Flow](https://about.gitlab.com/2014/09/29/gitlab-flow/)** article for more information about using +branches to work together. + +## Creating a fork + +In order to create a fork of a project, all you need to do is click on the fork button located on the top right side +of the screen, close to the project's URL and right next to the stars button. + + + +Once you do that you'll be presented with a screen where you can choose the namespace to fork to. Only namespaces +(groups and your own namespace) where you have write access to, will be shown. Click on the namespace to create your +fork there. + + + +After the forking is done, you can start working on the newly created repository. There you will have full +[Owner](../permissions/permissions.md) access, so you can set it up as you please. + +## Merging upstream + +Once you are ready to send your code back to the main project, you need to create a merge request. Choose your forked +project's main branch as the source and the original project's main branch as the destination and create the merge request. + + + +You can then assign the merge request to someone to have them review your changes. Upon pressing the 'Accept Merge Request' +button, your changes will be added to the repository and branch you're merging into. + + + + diff --git a/docker/Dockerfile b/docker/Dockerfile index b228a66832a..bb25bb677ca 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,7 +11,7 @@ RUN apt-get update -q \ # If the Omnibus package version below is outdated please contribute a merge request to update it. # If you run GitLab Enterprise Edition point it to a location where you have downloaded it. RUN TMP_FILE=$(mktemp); \ - wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.9.0-omnibus.2-1_amd64.deb \ + wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.9.2-omnibus-1_amd64.deb \ && dpkg -i $TMP_FILE \ && rm -f $TMP_FILE diff --git a/features/admin/deploy_keys.feature b/features/admin/deploy_keys.feature new file mode 100644 index 00000000000..9df47eb51fd --- /dev/null +++ b/features/admin/deploy_keys.feature @@ -0,0 +1,21 @@ +@admin +Feature: Admin Deploy Keys + Background: + Given I sign in as an admin + And there are public deploy keys in system + + Scenario: Deploy Keys list + When I visit admin deploy keys page + Then I should see all public deploy keys + + Scenario: Deploy Keys show + When I visit admin deploy keys page + And I click on first deploy key + Then I should see deploy key details + + Scenario: Deploy Keys new + When I visit admin deploy keys page + And I click 'New Deploy Key' + And I submit new deploy key + Then I should be on admin deploy keys page + And I should see newly created deploy key diff --git a/features/dashboard/issues.feature b/features/dashboard/issues.feature index 72627e43e05..99dad88a402 100644 --- a/features/dashboard/issues.feature +++ b/features/dashboard/issues.feature @@ -10,10 +10,12 @@ Feature: Dashboard Issues Scenario: I should see assigned issues Then I should see issues assigned to me + @javascript Scenario: I should see authored issues When I click "Authored by me" link Then I should see issues authored by me + @javascript Scenario: I should see all issues When I click "All" link Then I should see all issues diff --git a/features/dashboard/merge_requests.feature b/features/dashboard/merge_requests.feature index dcef1290e7e..4a2c997d707 100644 --- a/features/dashboard/merge_requests.feature +++ b/features/dashboard/merge_requests.feature @@ -10,10 +10,12 @@ Feature: Dashboard Merge Requests Scenario: I should see assigned merge_requests Then I should see merge requests assigned to me + @javascript Scenario: I should see authored merge_requests When I click "Authored by me" link Then I should see merge requests authored by me + @javascript Scenario: I should see all merge_requests When I click "All" link Then I should see all merge requests diff --git a/features/groups.feature b/features/groups.feature index 05546e0d6ef..415e43d6ae7 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -47,6 +47,21 @@ Feature: Groups Then I should not see group "Owned" avatar And I should not see the "Remove avatar" button + @javascript + Scenario: Add user to group + Given gitlab user "Mike" + When I visit group "Owned" members page + And I click link "Add members" + When I select "Mike" as "Reporter" + Then I should see "Mike" in team list as "Reporter" + + @javascript + Scenario: Invite user to group + When I visit group "Owned" members page + And I click link "Add members" + When I select "sjobs@apple.com" as "Reporter" + Then I should see "sjobs@apple.com" in team list as invited "Reporter" + # Leave @javascript diff --git a/features/invites.feature b/features/invites.feature new file mode 100644 index 00000000000..dc8eefaeaed --- /dev/null +++ b/features/invites.feature @@ -0,0 +1,45 @@ +Feature: Invites + Background: + Given "John Doe" is owner of group "Owned" + And "John Doe" has invited "user@example.com" to group "Owned" + + Scenario: Viewing invitation when signed out + When I visit the invitation page + Then I should be redirected to the sign in page + And I should see a notice telling me to sign in + + Scenario: Signing in to view invitation + When I visit the invitation page + And I sign in as "Mary Jane" + Then I should be redirected to the invitation page + + Scenario: Viewing invitation when signed in + Given I sign in as "Mary Jane" + And I visit the invitation page + Then I should see the invitation details + And I should see an "Accept invitation" button + And I should see a "Decline" button + + Scenario: Viewing invitation as an existing member + Given I sign in as "John Doe" + And I visit the invitation page + Then I should see a message telling me I'm already a member + + Scenario: Accepting the invitation + Given I sign in as "Mary Jane" + And I visit the invitation page + And I click the "Accept invitation" button + Then I should be redirected to the group page + And I should see a notice telling me I have access + + Scenario: Declining the application when signed in + Given I sign in as "Mary Jane" + And I visit the invitation page + And I click the "Decline" button + Then I should be redirected to the dashboard + And I should see a notice telling me I have declined + + Scenario: Declining the application when signed out + When I visit the invitation's decline page + Then I should be redirected to the sign in page + And I should see a notice telling me I have declined diff --git a/features/project/deploy_keys.feature b/features/project/deploy_keys.feature index 13e3b9bbd2e..a71f6124d9c 100644 --- a/features/project/deploy_keys.feature +++ b/features/project/deploy_keys.feature @@ -6,7 +6,17 @@ Feature: Project Deploy Keys 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 + Then I should see project deploy key + + Scenario: I should see project deploy keys + Given other project has deploy key + When I visit project deploy keys page + Then I should see other project deploy key + + Scenario: I should see public deploy keys + Given public deploy key exists + When I visit project deploy keys page + Then I should see public deploy key Scenario: I add new deploy key Given I visit project deploy keys page @@ -15,9 +25,16 @@ Feature: Project Deploy Keys Then I should be on deploy keys page And I should see newly created deploy key - Scenario: I attach deploy key to project + Scenario: I attach other project 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 + + Scenario: I attach public deploy key to project + Given public deploy key exists + 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/issues/filter_labels.feature b/features/project/issues/filter_labels.feature index 2c69a78a749..e316f519861 100644 --- a/features/project/issues/filter_labels.feature +++ b/features/project/issues/filter_labels.feature @@ -8,11 +8,7 @@ Feature: Project Issues Filter Labels And project "Shop" has issue "Feature1" with labels: "feature" Given I visit project "Shop" issues page - Scenario: I should see project issues - Then I should see "bug" in labels filter - And I should see "feature" in labels filter - And I should see "enhancement" in labels filter - + @javascript Scenario: I filter by one label Given I click link "bug" Then I should see "Bugfix1" in issues list diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index b9031f6f32b..eb813884d1e 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -25,6 +25,12 @@ Feature: Project Issues Given I click link "Release 0.4" Then I should see issue "Release 0.4" + @javascript + Scenario: I visit issue page + Given I add a user to project "Shop" + And I click "author" dropdown + Then I see current user as the first user + Scenario: I submit new unassigned issue Given I click link "New Issue" And I submit new issue "500 error on profile" @@ -42,6 +48,7 @@ Feature: Project Issues Given I visit issue page "Release 0.4" And I leave a comment like "XML attached" Then I should see comment "XML attached" + And I should see an error alert section within the comment form @javascript Scenario: I search issue diff --git a/features/project/star.feature b/features/project/star.feature index 3322f891805..a45f9c470ea 100644 --- a/features/project/star.feature +++ b/features/project/star.feature @@ -13,7 +13,7 @@ Feature: Project Star Given public project "Community" And I visit project "Community" page When I click on the star toggle button - Then The project has 0 stars + Then I redirected to sign in page @javascript Scenario: Signed in users can toggle star diff --git a/features/project/team_management.feature b/features/project/team_management.feature index 22393622bb9..6cda225ea7b 100644 --- a/features/project/team_management.feature +++ b/features/project/team_management.feature @@ -18,6 +18,12 @@ Feature: Project Team Management Then I should see "Mike" in team list as "Reporter" @javascript + Scenario: Invite user to project + Given I click link "Add members" + And I select "sjobs@apple.com" as "Reporter" + Then I should see "sjobs@apple.com" in team list as invited "Reporter" + + @javascript Scenario: Update user access Given I should see "Sam" in team list as "Developer" And I change "Sam" role to "Reporter" diff --git a/features/project/wiki.feature b/features/project/wiki.feature index 4a8c771ddac..977cd609a11 100644 --- a/features/project/wiki.feature +++ b/features/project/wiki.feature @@ -62,3 +62,27 @@ Feature: Project Wiki And I browse to wiki page with images And I click on image link Then I should see the new wiki page form + + @javascript + Scenario: New Wiki page that has a path + Given I create a New page with paths + And I click on the "Pages" button + Then I should see non-escaped link in the pages list + + @javascript + Scenario: Edit Wiki page that has a path + Given I create a New page with paths + And I click on the "Pages" button + And I edit the Wiki page with a path + Then I should see a non-escaped path + And I should see the Editing page + And I change the content + Then I should see the updated content + + @javascript + Scenario: View the page history of a Wiki page that has a path + Given I create a New page with paths + And I click on the "Pages" button + And I view the page history of a Wiki page that has a path + Then I should see a non-escaped path + And I should see the page history diff --git a/features/steps/admin/deploy_keys.rb b/features/steps/admin/deploy_keys.rb new file mode 100644 index 00000000000..fb0b611762e --- /dev/null +++ b/features/steps/admin/deploy_keys.rb @@ -0,0 +1,57 @@ +class Spinach::Features::AdminDeployKeys < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedAdmin + + step 'there are public deploy keys in system' do + create(:deploy_key, public: true) + create(:another_deploy_key, public: true) + end + + step 'I should see all public deploy keys' do + DeployKey.are_public.each do |p| + page.should have_content p.title + end + end + + step 'I click on first deploy key' do + click_link DeployKey.are_public.first.title + end + + step 'I should see deploy key details' do + deploy_key = DeployKey.are_public.first + current_path.should == admin_deploy_key_path(deploy_key) + page.should have_content(deploy_key.title) + page.should have_content(deploy_key.key) + end + + step 'I visit admin deploy key page' do + visit admin_deploy_key_path(deploy_key) + end + + step 'I visit admin deploy keys page' do + visit admin_deploy_keys_path + 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 admin deploy keys page' do + current_path.should == admin_deploy_keys_path + end + + step 'I should see newly created deploy key' do + page.should have_content(deploy_key.title) + end + + def deploy_key + @deploy_key ||= DeployKey.are_public.first + end +end diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb index b77113e3974..60da36e86de 100644 --- a/features/steps/dashboard/issues.rb +++ b/features/steps/dashboard/issues.rb @@ -1,6 +1,7 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps include SharedAuthentication include SharedPaths + include Select2Helper step 'I should see issues assigned to me' do should_see(assigned_issue) @@ -35,21 +36,13 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps end step 'I click "Authored by me" link' do - within ".assignee-filter" do - click_link "Any" - end - within ".author-filter" do - click_link current_user.name - end + select2(current_user.id, from: "#author_id") + select2(nil, from: "#assignee_id") end step 'I click "All" link' do - within ".author-filter" do - click_link "Any" - end - within ".assignee-filter" do - click_link "Any" - end + select2(nil, from: "#author_id") + select2(nil, from: "#assignee_id") end def should_see(issue) diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb index 6261c89924c..9d92082bb83 100644 --- a/features/steps/dashboard/merge_requests.rb +++ b/features/steps/dashboard/merge_requests.rb @@ -1,6 +1,7 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps include SharedAuthentication include SharedPaths + include Select2Helper step 'I should see merge requests assigned to me' do should_see(assigned_merge_request) @@ -39,21 +40,13 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps end step 'I click "Authored by me" link' do - within ".assignee-filter" do - click_link "Any" - end - within ".author-filter" do - click_link current_user.name - end + select2(current_user.id, from: "#author_id") + select2(nil, from: "#assignee_id") end step 'I click "All" link' do - within ".author-filter" do - click_link "Any" - end - within ".assignee-filter" do - click_link "Any" - end + select2(nil, from: "#author_id") + select2(nil, from: "#assignee_id") end def should_see(merge_request) diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 91921f5e21c..228b83e5fd0 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -5,6 +5,49 @@ class Spinach::Features::Groups < Spinach::FeatureSteps include SharedUser include Select2Helper + step 'gitlab user "Mike"' do + create(:user, name: "Mike") + end + + step 'I click link "Add members"' do + find(:css, 'button.btn-new').click + end + + step 'I select "Mike" as "Reporter"' do + user = User.find_by(name: "Mike") + + within ".users-group-form" do + select2(user.id, from: "#user_ids", multiple: true) + select "Reporter", from: "access_level" + end + + click_button "Add users to group" + end + + step 'I should see "Mike" in team list as "Reporter"' do + within '.well-list' do + page.should have_content('Mike') + page.should have_content('Reporter') + end + end + + step 'I select "sjobs@apple.com" as "Reporter"' do + within ".users-group-form" do + select2("sjobs@apple.com", from: "#user_ids", multiple: true) + select "Reporter", from: "access_level" + end + + click_button "Add users to group" + end + + step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do + within '.well-list' do + page.should have_content('sjobs@apple.com') + page.should have_content('invited') + page.should have_content('Reporter') + end + end + step 'I should see group "Owned" projects list' do Group.find_by(name: "Owned").projects.each do |project| page.should have_link project.name diff --git a/features/steps/invites.rb b/features/steps/invites.rb new file mode 100644 index 00000000000..d051cc3edc8 --- /dev/null +++ b/features/steps/invites.rb @@ -0,0 +1,80 @@ +class Spinach::Features::Invites < Spinach::FeatureSteps + include SharedAuthentication + include SharedUser + include SharedGroup + + step '"John Doe" has invited "user@example.com" to group "Owned"' do + user = User.find_by(name: "John Doe") + group = Group.find_by(name: "Owned") + group.add_user("user@example.com", Gitlab::Access::DEVELOPER, user) + end + + step 'I visit the invitation page' do + group = Group.find_by(name: "Owned") + invite = group.group_members.invite.last + invite.generate_invite_token! + @raw_invite_token = invite.raw_invite_token + visit invite_path(@raw_invite_token) + end + + step 'I should be redirected to the sign in page' do + expect(current_path).to eq(new_user_session_path) + end + + step 'I should see a notice telling me to sign in' do + expect(page).to have_content "To accept this invitation, sign in" + end + + step 'I should be redirected to the invitation page' do + expect(current_path).to eq(invite_path(@raw_invite_token)) + end + + step 'I should see the invitation details' do + expect(page).to have_content("You have been invited by John Doe to join group Owned as Developer.") + end + + step "I should see a message telling me I'm already a member" do + expect(page).to have_content("However, you are already a member of this group.") + end + + step 'I should see an "Accept invitation" button' do + expect(page).to have_link("Accept invitation") + end + + step 'I should see a "Decline" button' do + expect(page).to have_link("Decline") + end + + step 'I click the "Accept invitation" button' do + page.click_link "Accept invitation" + end + + step 'I should be redirected to the group page' do + group = Group.find_by(name: "Owned") + expect(current_path).to eq(group_path(group)) + end + + step 'I should see a notice telling me I have access' do + expect(page).to have_content("You have been granted Developer access to group Owned.") + end + + step 'I click the "Decline" button' do + page.click_link "Decline" + end + + step 'I should be redirected to the dashboard' do + expect(current_path).to eq(dashboard_path) + end + + step 'I should see a notice telling me I have declined' do + expect(page).to have_content("You have declined the invitation to join group Owned.") + end + + step "I visit the invitation's decline page" do + group = Group.find_by(name: "Owned") + invite = group.group_members.invite.last + invite.generate_invite_token! + @raw_invite_token = invite.raw_invite_token + visit decline_invite_path(@raw_invite_token) + end +end diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb index 4bf5cb5fa40..50e14513a7a 100644 --- a/features/steps/project/deploy_keys.rb +++ b/features/steps/project/deploy_keys.rb @@ -7,12 +7,24 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps create(:deploy_keys_project, project: @project) end - step 'I should see project deploy keys' do + step 'I should see project deploy key' do within '.enabled-keys' do page.should have_content deploy_key.title end end + step 'I should see other project deploy key' do + within '.available-keys' do + page.should have_content other_deploy_key.title + end + end + + step 'I should see public deploy key' do + within '.available-keys' do + page.should have_content public_deploy_key.title + end + end + step 'I click \'New Deploy Key\'' do click_link 'New Deploy Key' end @@ -39,6 +51,10 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps create(:deploy_keys_project, project: @second_project) end + step 'public deploy key exists' do + create(:deploy_key, public: true) + end + step 'I click attach deploy key' do within '.available-keys' do click_link 'Enable' @@ -50,4 +66,12 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps def deploy_key @project.deploy_keys.last end + + def other_deploy_key + @second_project.deploy_keys.last + end + + def public_deploy_key + DeployKey.are_public.last + end end diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb index e62fa9c84c8..5740bd12837 100644 --- a/features/steps/project/issues/filter_labels.rb +++ b/features/steps/project/issues/filter_labels.rb @@ -2,24 +2,7 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps include SharedAuthentication include SharedProject include SharedPaths - - step 'I should see "bug" in labels filter' do - within ".labels-filter" do - page.should have_content "bug" - end - end - - step 'I should see "feature" in labels filter' do - within ".labels-filter" do - page.should have_content "feature" - end - end - - step 'I should see "enhancement" in labels filter' do - within ".labels-filter" do - page.should have_content "enhancement" - end - end + include Select2Helper step 'I should see "Bugfix1" in issues list' do within ".issues-list" do @@ -46,9 +29,7 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps end step 'I click link "bug"' do - within ".labels-filter" do - click_link "bug" - end + select2('bug', from: "#label_name") end step 'I click link "feature"' do diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index e8ca3f7c176..b8e282b2029 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -59,6 +59,18 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps click_link "New Issue" end + step 'I click "author" dropdown' do + first('.ajax-users-select').click + end + + step 'I see current user as the first user' do + expect(page).to have_selector('.user-result', visible: true, count: 4) + users = page.all('.user-name') + users[0].text.should == 'Any' + users[1].text.should == 'Unassigned' + users[2].text.should == current_user.name + end + step 'I submit new issue "500 error on profile"' do fill_in "issue_title", with: "500 error on profile" click_button "Submit new issue" @@ -204,6 +216,12 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end end + step 'I should see an error alert section within the comment form' do + within(".js-main-target-form") do + find(".error-alert") + end + end + step 'The code block should be unchanged' do page.should have_content("```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```") end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 40c102833a4..bb1f9f129c0 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -61,8 +61,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see that I am unsubscribed' do - sleep 0.2 - find(".subscribe-button span").text.should == "Subscribe" + find(".subscribe-button span").should have_content("Subscribe") end step 'I click button "Unsubscribe"' do diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 557555aee58..caf6c73ee06 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -74,7 +74,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I fill the new file name with an illegal name' do - fill_in :file_name, with: '.git' + fill_in :file_name, with: 'Spaces Not Allowed' end step 'I fill the commit message' do diff --git a/features/steps/project/star.rb b/features/steps/project/star.rb index ae2e4c7a201..50cdfd73c34 100644 --- a/features/steps/project/star.rb +++ b/features/steps/project/star.rb @@ -22,12 +22,16 @@ class Spinach::Features::ProjectStar < Spinach::FeatureSteps # Requires @javascript step "I click on the star toggle button" do - find(".star .toggle", visible: true).click + find(".star-btn", visible: true).click + end + + step 'I redirected to sign in page' do + current_path.should == new_user_session_path end protected def has_n_stars(n) - expect(page).to have_css(".star .count", text: /^#{n}$/, visible: true) + expect(page).to have_css(".star-btn .count", text: n, visible: true) end end diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index 0eefe2b5688..e95621071c4 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -35,6 +35,22 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps end end + step 'I select "sjobs@apple.com" as "Reporter"' do + within ".users-project-form" do + select2("sjobs@apple.com", from: "#user_ids", multiple: true) + select "Reporter", from: "access_level" + end + click_button "Add users to project" + end + + step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do + within ".access-reporter" do + page.should have_content('sjobs@apple.com') + page.should have_content('invited') + page.should have_content('Reporter') + end + end + step 'I should see "Sam" in team list as "Developer"' do within ".access-developer" do page.should have_content('Sam') diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index cd7d5eac243..bb93e582a1f 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -3,6 +3,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps include SharedProject include SharedNote include SharedPaths + include WikiHelper step 'I click on the Cancel button' do within(:css, ".form-actions") do @@ -123,6 +124,41 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps page.should have_content('Editing - image.jpg') end + step 'I create a New page with paths' do + click_on 'New Page' + fill_in 'Page slug', with: 'one/two/three' + click_on 'Build' + fill_in "wiki_content", with: 'wiki content' + click_on "Create page" + current_path.should include 'one/two/three' + end + + step 'I should see non-escaped link in the pages list' do + page.should have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three']") + end + + step 'I edit the Wiki page with a path' do + click_on 'three' + click_on 'Edit' + end + + step 'I should see a non-escaped path' do + current_path.should include 'one/two/three' + end + + step 'I should see the Editing page' do + page.should have_content('Editing') + end + + step 'I view the page history of a Wiki page that has a path' do + click_on 'three' + click_on 'Page History' + end + + step 'I should see the page history' do + page.should have_content('History for') + end + def wiki @project_wiki = ProjectWiki.new(project, current_user) end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 41f71ae29cb..b60ac5e3423 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -14,6 +14,13 @@ module SharedProject @project.team << [@user, :master] end + # Add another user to project "Shop" + step 'I add a user to project "Shop"' do + @project = Project.find_by(name: "Shop") + other_user = create(:user, name: 'Alpha') + @project.team << [other_user, :master] + end + # Create another specific project called "Forum" step 'I own project "Forum"' do @project = Project.find_by(name: "Forum") diff --git a/lib/api/branches.rb b/lib/api/branches.rb index edfdf842f85..592100a7045 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -1,5 +1,4 @@ require 'mime/types' -require 'uri' module API # Projects API @@ -101,10 +100,11 @@ module API # branch (required) - The name of the branch # Example Request: # DELETE /projects/:id/repository/branches/:branch - delete ":id/repository/branches/:branch" do + delete ":id/repository/branches/:branch", + requirements: { branch: /.*/ } do authorize_push_project result = DeleteBranchService.new(user_project, current_user). - execute(URI.unescape(params[:branch])) + execute(params[:branch]) if result[:status] == :success { diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 489be210784..36332bc6514 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -46,7 +46,7 @@ module API end class Project < Grape::Entity - expose :id, :description, :default_branch + expose :id, :description, :default_branch, :tag_list expose :public?, as: :public expose :archived?, as: :archived expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url @@ -54,6 +54,7 @@ module API expose :name, :name_with_namespace expose :path, :path_with_namespace expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :snippets_enabled, :created_at, :last_activity_at + expose :creator_id expose :namespace expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? } expose :avatar_url diff --git a/lib/api/group_members.rb b/lib/api/group_members.rb index ed54c7f6ff0..ab9b7c602b5 100644 --- a/lib/api/group_members.rb +++ b/lib/api/group_members.rb @@ -9,8 +9,7 @@ module API # GET /groups/:id/members get ":id/members" do group = find_group(params[:id]) - members = group.group_members - users = (paginate members).collect(&:user) + users = group.users present users, with: Entities::GroupMember, group: group end @@ -24,7 +23,7 @@ module API # POST /groups/:id/members post ":id/members" do group = find_group(params[:id]) - authorize! :manage_group, group + authorize! :admin_group, group required_attributes! [:user_id, :access_level] unless validate_access_level?(params[:access_level]) @@ -35,7 +34,7 @@ module API render_api_error!("Already exists", 409) end - group.add_users([params[:user_id]], params[:access_level]) + group.add_users([params[:user_id]], params[:access_level], current_user) member = group.group_members.find_by(user_id: params[:user_id]) present member.user, with: Entities::GroupMember, group: group end @@ -50,7 +49,7 @@ module API # PUT /groups/:id/members/:user_id put ':id/members/:user_id' do group = find_group(params[:id]) - authorize! :manage_group, group + authorize! :admin_group, group required_attributes! [:access_level] group_member = group.group_members.find_by(user_id: params[:user_id]) @@ -74,7 +73,7 @@ module API # DELETE /groups/:id/members/:user_id delete ":id/members/:user_id" do group = find_group(params[:id]) - authorize! :manage_group, group + authorize! :admin_group, group member = group.group_members.find_by(user_id: params[:user_id]) if member.nil? diff --git a/lib/api/groups.rb b/lib/api/groups.rb index a92abd4b690..8cb9f920975 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -61,7 +61,7 @@ module API # DELETE /groups/:id delete ":id" do group = find_group(params[:id]) - authorize! :manage_group, group + authorize! :admin_group, group group.destroy end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 83f65eec6cc..e3fff79d68f 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -88,17 +88,14 @@ module API present user_project, with: Entities::ProjectWithAccess, user: current_user end - # Get a single project events + # Get events for a single project # # Parameters: # id (required) - The ID of a project # Example Request: # GET /projects/:id/events 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) - + events = paginate user_project.events.recent present events, with: Entities::Event end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index b259914a01c..1fbf3dca3c6 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -133,10 +133,11 @@ module API authorize! :download_code, user_project begin - file_path = ArchiveRepositoryService.new.execute( - user_project, - params[:sha], - params[:format]) + file_path = ArchiveRepositoryService.new( + user_project, + params[:sha], + params[:format] + ).execute rescue not_found!('File') end @@ -149,7 +150,7 @@ module API env['api.format'] = :binary present data else - not_found!('File') + redirect request.fullpath end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index afd05897509..b69aebf9fe1 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -1,7 +1,5 @@ module Backup class Manager - BACKUP_CONTENTS = %w{repositories/ db/ uploads/ backup_information.yml} - def pack # saving additional informations s = {} @@ -9,6 +7,7 @@ module Backup s[:backup_created_at] = Time.now s[:gitlab_version] = Gitlab::VERSION s[:tar_version] = tar_version + s[:skipped] = ENV["SKIP"] tar_file = "#{s[:backup_created_at].to_i}_gitlab_backup.tar" Dir.chdir(Gitlab.config.backup.path) do @@ -17,12 +16,12 @@ module Backup file << s.to_yaml.gsub(/^---\n/,'') end - FileUtils.chmod_R(0700, %w{db uploads repositories}) + FileUtils.chmod(0700, folders_to_backup) # create archive $progress.print "Creating backup archive: #{tar_file} ... " orig_umask = File.umask(0077) - if Kernel.system('tar', '-cf', tar_file, *BACKUP_CONTENTS) + if Kernel.system('tar', '-cf', tar_file, *backup_contents) $progress.puts "done".green else puts "creating archive #{tar_file} failed".red @@ -46,6 +45,7 @@ module Backup connection = ::Fog::Storage.new(connection_settings) directory = connection.directories.get(remote_directory) + if directory.files.create(key: tar_file, body: File.open(tar_file), public: false) $progress.puts "done".green else @@ -56,7 +56,10 @@ module Backup def cleanup $progress.print "Deleting tmp directories ... " - BACKUP_CONTENTS.each do |dir| + + backup_contents.each do |dir| + next unless File.exist?(File.join(Gitlab.config.backup.path, dir)) + if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir)) $progress.puts "done".green else @@ -73,6 +76,7 @@ module Backup if keep_time > 0 removed = 0 + Dir.chdir(Gitlab.config.backup.path) do file_list = Dir.glob('*_gitlab_backup.tar') file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_backup.tar/ } @@ -84,6 +88,7 @@ module Backup end end end + $progress.puts "done. (#{removed} removed)".green else $progress.puts "skipping".yellow @@ -96,6 +101,7 @@ module Backup # 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" @@ -110,6 +116,7 @@ module Backup end $progress.print "Unpacking backup ... " + unless Kernel.system(*%W(tar -xf #{tar_file})) puts "unpacking backup failed".red exit 1 @@ -117,7 +124,6 @@ module Backup $progress.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 @@ -136,5 +142,29 @@ module Backup tar_version, _ = Gitlab::Popen.popen(%W(tar --version)) tar_version.force_encoding('locale').split("\n").first end + + def skipped?(item) + settings[:skipped] && settings[:skipped].include?(item) + end + + private + + def backup_contents + folders_to_backup + ["backup_information.yml"] + end + + def folders_to_backup + folders = %w{repositories db uploads} + + if ENV["SKIP"] + return folders.reject{ |folder| ENV["SKIP"].include?(folder) } + end + + folders + end + + def settings + @settings ||= YAML.load_file("backup_information.yml") + end end end diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb index 42970c1be59..2eae55e534b 100644 --- a/lib/file_size_validator.rb +++ b/lib/file_size_validator.rb @@ -25,8 +25,8 @@ class FileSizeValidator < ActiveModel::EachValidator keys.each do |key| value = options[key] - unless value.is_a?(Integer) && value >= 0 - raise ArgumentError, ":#{key} must be a nonnegative Integer" + unless (value.is_a?(Integer) && value >= 0) || value.is_a?(Symbol) + raise ArgumentError, ":#{key} must be a nonnegative Integer or symbol" end end end @@ -39,6 +39,14 @@ class FileSizeValidator < ActiveModel::EachValidator CHECKS.each do |key, validity_check| next unless check_value = options[key] + check_value = + case check_value + when Integer + check_value + when Symbol + record.send(check_value) + end + value ||= [] if key == :maximum value_size = value.size diff --git a/lib/gitlab.rb b/lib/gitlab.rb new file mode 100644 index 00000000000..5fc1862c3e9 --- /dev/null +++ b/lib/gitlab.rb @@ -0,0 +1,5 @@ +require 'gitlab/git' + +module Gitlab + autoload :Satellite, 'gitlab/satellite/satellite' +end diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index aabc7f1e69a..530f9d93de4 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -240,7 +240,7 @@ module Gitlab gitlab_shell_version_file = "#{gitlab_shell_path}/VERSION" if File.readable?(gitlab_shell_version_file) - File.read(gitlab_shell_version_file) + File.read(gitlab_shell_version_file).chomp end end diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb index db33af2c2da..54420e62c90 100644 --- a/lib/gitlab/bitbucket_import/project_creator.rb +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -10,29 +10,16 @@ module Gitlab end def execute - @project = Project.new( + ::Projects::CreateService.new(current_user, name: repo["name"], path: repo["slug"], description: repo["description"], - namespace: namespace, - creator: current_user, + namespace_id: namespace.id, visibility_level: repo["is_private"] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, import_type: "bitbucket", import_source: "#{repo["owner"]}/#{repo["slug"]}", import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git" - ) - - if @project.save! - @project.reload - - if @project.import_failed? - @project.import_retry - else - @project.import_start - end - end - - @project + ).execute end end end diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb index a9fd59f03d9..ab184d95c05 100644 --- a/lib/gitlab/closing_issue_extractor.rb +++ b/lib/gitlab/closing_issue_extractor.rb @@ -1,21 +1,20 @@ module Gitlab - module ClosingIssueExtractor + class ClosingIssueExtractor ISSUE_CLOSING_REGEX = Regexp.new(Gitlab.config.gitlab.issue_closing_pattern) - def self.closed_by_message_in_project(message, project) - issues = [] + def initialize(project, current_user = nil) + @extractor = Gitlab::ReferenceExtractor.new(project, current_user) + end - unless message.nil? - md = message.scan(ISSUE_CLOSING_REGEX) + def closed_by_message(message) + return [] if message.nil? + + closing_statements = message.scan(ISSUE_CLOSING_REGEX). + map { |ref| ref[0] }.join(" ") - md.each do |ref| - extractor = Gitlab::ReferenceExtractor.new - extractor.analyze(ref[0], project) - issues += extractor.issues_for(project) - end - end + @extractor.analyze(closing_statements) - issues.uniq + @extractor.issues end end end diff --git a/lib/gitlab/contributors.rb b/lib/gitlab/contributor.rb index c41e92b620f..c41e92b620f 100644 --- a/lib/gitlab/contributors.rb +++ b/lib/gitlab/contributor.rb diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 0ebebfa09c4..d8f696d247b 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -20,7 +20,8 @@ module Gitlab signin_enabled: Settings.gitlab['signin_enabled'], gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], - restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'] + restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], + max_attachment_size: Settings.gitlab['max_attachment_size'] ) end end diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb index 9439ca6cbf4..2723eec933e 100644 --- a/lib/gitlab/github_import/project_creator.rb +++ b/lib/gitlab/github_import/project_creator.rb @@ -10,29 +10,16 @@ module Gitlab end def execute - @project = Project.new( + ::Projects::CreateService.new(current_user, name: repo.name, path: repo.name, description: repo.description, - namespace: namespace, - creator: current_user, + namespace_id: namespace.id, visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, import_type: "github", import_source: repo.full_name, import_url: repo.clone_url.sub("https://", "https://#{current_user.github_access_token}@") - ) - - if @project.save! - @project.reload - - if @project.import_failed? - @project.import_retry - else - @project.import_start - end - end - - @project + ).execute end end end diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb index f48ede9d067..9c00896c913 100644 --- a/lib/gitlab/gitlab_import/client.rb +++ b/lib/gitlab/gitlab_import/client.rb @@ -28,6 +28,10 @@ module Gitlab client.auth_code.get_token(code, redirect_uri: redirect_uri).token end + def user + api.get("/api/v3/user").parsed + end + def issues(project_identifier) lazy_page_iterator(PER_PAGE) do |page| api.get("/api/v3/projects/#{project_identifier}/issues?per_page=#{PER_PAGE}&page=#{page}").parsed diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb index 6424d56f8f1..f0d7141bf56 100644 --- a/lib/gitlab/gitlab_import/project_creator.rb +++ b/lib/gitlab/gitlab_import/project_creator.rb @@ -10,29 +10,16 @@ module Gitlab end def execute - @project = Project.new( + ::Projects::CreateService.new(current_user, name: repo["name"], path: repo["path"], description: repo["description"], - namespace: namespace, - creator: current_user, + namespace_id: namespace.id, visibility_level: repo["visibility_level"], import_type: "gitlab", import_source: repo["path_with_namespace"], import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{current_user.gitlab_access_token}@") - ) - - if @project.save! - @project.reload - - if @project.import_failed? - @project.import_retry - else - @project.import_start - end - end - - @project + ).execute end end end diff --git a/lib/gitlab/gitorious_import/client.rb b/lib/gitlab/gitorious_import/client.rb index 5043f6a2ebd..8cdc3d4afae 100644 --- a/lib/gitlab/gitorious_import/client.rb +++ b/lib/gitlab/gitorious_import/client.rb @@ -27,37 +27,5 @@ module Gitlab repo_list.to_s.split(',').map(&:strip).reject(&:blank?) end end - - Repository = Struct.new(:full_name) do - def id - Digest::SHA1.hexdigest(full_name) - end - - def namespace - segments.first - end - - def path - segments.last - end - - def name - path.titleize - end - - def description - "" - end - - def import_url - "#{GITORIOUS_HOST}/#{full_name}.git" - end - - private - - def segments - full_name.split('/') - end - end end end diff --git a/lib/gitlab/gitorious_import/project_creator.rb b/lib/gitlab/gitorious_import/project_creator.rb index 3cbebe53997..cc9a91c91f4 100644 --- a/lib/gitlab/gitorious_import/project_creator.rb +++ b/lib/gitlab/gitorious_import/project_creator.rb @@ -10,29 +10,16 @@ module Gitlab end def execute - @project = Project.new( + ::Projects::CreateService.new(current_user, name: repo.name, path: repo.path, description: repo.description, - namespace: namespace, - creator: current_user, + namespace_id: namespace.id, visibility_level: Gitlab::VisibilityLevel::PUBLIC, import_type: "gitorious", import_source: repo.full_name, import_url: repo.import_url - ) - - if @project.save! - @project.reload - - if @project.import_failed? - @project.import_retry - else - @project.import_start - end - end - - @project + ).execute end end end diff --git a/lib/gitlab/gitorious_import/repository.rb b/lib/gitlab/gitorious_import/repository.rb new file mode 100644 index 00000000000..f702797dc6e --- /dev/null +++ b/lib/gitlab/gitorious_import/repository.rb @@ -0,0 +1,37 @@ +module Gitlab + module GitoriousImport + GITORIOUS_HOST = "https://gitorious.org" + + Repository = Struct.new(:full_name) do + def id + Digest::SHA1.hexdigest(full_name) + end + + def namespace + segments.first + end + + def path + segments.last + end + + def name + path.titleize + end + + def description + "" + end + + def import_url + "#{GITORIOUS_HOST}/#{full_name}.git" + end + + private + + def segments + full_name.split('/') + end + end + end +end diff --git a/lib/gitlab/google_code_import/client.rb b/lib/gitlab/google_code_import/client.rb new file mode 100644 index 00000000000..02f31e45f88 --- /dev/null +++ b/lib/gitlab/google_code_import/client.rb @@ -0,0 +1,48 @@ +module Gitlab + module GoogleCodeImport + class Client + attr_reader :raw_data + + def self.mask_email(author) + parts = author.split("@", 2) + parts[0] = "#{parts[0][0...-3]}..." + parts.join("@") + end + + def initialize(raw_data) + @raw_data = raw_data + end + + def valid? + raw_data.is_a?(Hash) && raw_data["kind"] == "projecthosting#user" && raw_data.has_key?("projects") + end + + def repos + @repos ||= raw_data["projects"].map { |raw_repo| GoogleCodeImport::Repository.new(raw_repo) }.select(&:git?) + end + + def repo(id) + repos.find { |repo| repo.id == id } + end + + def user_map + user_map = Hash.new { |hash, user| hash[user] = self.class.mask_email(user) } + + repos.each do |repo| + next unless repo.valid? && repo.issues + + repo.issues.each do |raw_issue| + # Touching is enough to add the entry and masked email. + user_map[raw_issue["author"]["name"]] + + raw_issue["comments"]["items"].each do |raw_comment| + user_map[raw_comment["author"]["name"]] + end + end + end + + Hash[user_map.sort] + end + end + end +end diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb new file mode 100644 index 00000000000..b5e82563ff1 --- /dev/null +++ b/lib/gitlab/google_code_import/importer.rb @@ -0,0 +1,360 @@ +module Gitlab + module GoogleCodeImport + class Importer + attr_reader :project, :repo + + def initialize(project) + @project = project + + import_data = project.import_data.try(:data) + repo_data = import_data["repo"] if import_data + @repo = GoogleCodeImport::Repository.new(repo_data) + + @closed_statuses = [] + @known_labels = Set.new + end + + def execute + return true unless repo.valid? + + import_status_labels + + import_labels + + import_issues + + true + end + + private + + def user_map + @user_map ||= begin + user_map = Hash.new { |hash, user| Client.mask_email(user) } + + import_data = project.import_data.try(:data) + stored_user_map = import_data["user_map"] if import_data + user_map.update(stored_user_map) if stored_user_map + + user_map + end + end + + def import_status_labels + repo.raw_data["issuesConfig"]["statuses"].each do |status| + closed = !status["meansOpen"] + @closed_statuses << status["status"] if closed + + name = nice_status_name(status["status"]) + create_label(name) + @known_labels << name + end + end + + def import_labels + repo.raw_data["issuesConfig"]["labels"].each do |label| + name = nice_label_name(label["label"]) + create_label(name) + @known_labels << name + end + end + + def import_issues + return unless repo.issues + + while raw_issue = repo.issues.shift + author = user_map[raw_issue["author"]["name"]] + date = DateTime.parse(raw_issue["published"]).to_formatted_s(:long) + + comments = raw_issue["comments"]["items"] + issue_comment = comments.shift + + content = format_content(issue_comment["content"]) + attachments = format_attachments(raw_issue["id"], 0, issue_comment["attachments"]) + + body = format_issue_body(author, date, content, attachments) + + labels = [] + raw_issue["labels"].each do |label| + name = nice_label_name(label) + labels << name + + unless @known_labels.include?(name) + create_label(name) + @known_labels << name + end + end + labels << nice_status_name(raw_issue["status"]) + + assignee_id = nil + if raw_issue.has_key?("owner") + username = user_map[raw_issue["owner"]["name"]] + + if username.start_with?("@") + username = username[1..-1] + + if user = User.find_by(username: username) + assignee_id = user.id + end + end + end + + issue = Issue.create!( + project_id: project.id, + title: raw_issue["title"], + description: body, + author_id: project.creator_id, + assignee_id: assignee_id, + state: raw_issue["state"] == "closed" ? "closed" : "opened" + ) + issue.add_labels_by_names(labels) + + if issue.iid != raw_issue["id"] + issue.update_attribute(:iid, raw_issue["id"]) + end + + import_issue_comments(issue, comments) + end + end + + def import_issue_comments(issue, comments) + Note.transaction do + while raw_comment = comments.shift + next if raw_comment.has_key?("deletedBy") + + content = format_content(raw_comment["content"]) + updates = format_updates(raw_comment["updates"]) + attachments = format_attachments(issue.iid, raw_comment["id"], raw_comment["attachments"]) + + next if content.blank? && updates.blank? && attachments.blank? + + author = user_map[raw_comment["author"]["name"]] + date = DateTime.parse(raw_comment["published"]).to_formatted_s(:long) + + body = format_issue_comment_body( + raw_comment["id"], + author, + date, + content, + updates, + attachments + ) + + # Needs to match order of `comment_columns` below. + Note.create!( + project_id: project.id, + noteable_type: "Issue", + noteable_id: issue.id, + author_id: project.creator_id, + note: body + ) + end + end + end + + def nice_label_color(name) + case name + when /\AComponent:/ + "#fff39e" + when /\AOpSys:/ + "#e2e2e2" + when /\AMilestone:/ + "#fee3ff" + + when *@closed_statuses.map { |s| nice_status_name(s) } + "#cfcfcf" + when "Status: New" + "#428bca" + when "Status: Accepted" + "#5cb85c" + when "Status: Started" + "#8e44ad" + + when "Priority: Critical" + "#ffcfcf" + when "Priority: High" + "#deffcf" + when "Priority: Medium" + "#fff5cc" + when "Priority: Low" + "#cfe9ff" + + when "Type: Defect" + "#d9534f" + when "Type: Enhancement" + "#44ad8e" + when "Type: Task" + "#4b6dd0" + when "Type: Review" + "#8e44ad" + when "Type: Other" + "#7f8c8d" + else + "#e2e2e2" + end + end + + def nice_label_name(name) + name.sub("-", ": ") + end + + def nice_status_name(name) + "Status: #{name}" + end + + def linkify_issues(s) + s.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2') + end + + def escape_for_markdown(s) + s = s.gsub("*", "\\*") + s = s.gsub("#", "\\#") + s = s.gsub("`", "\\`") + s = s.gsub(":", "\\:") + s = s.gsub("-", "\\-") + s = s.gsub("+", "\\+") + s = s.gsub("_", "\\_") + s = s.gsub("(", "\\(") + s = s.gsub(")", "\\)") + s = s.gsub("[", "\\[") + s = s.gsub("]", "\\]") + s = s.gsub("<", "\\<") + s = s.gsub(">", "\\>") + s = s.gsub("\r", "") + s = s.gsub("\n", " \n") + s + end + + def create_label(name) + color = nice_label_color(name) + Label.create!(project_id: project.id, name: name, color: color) + end + + def format_content(raw_content) + linkify_issues(escape_for_markdown(raw_content)) + end + + def format_updates(raw_updates) + updates = [] + + if raw_updates.has_key?("status") + updates << "*Status: #{raw_updates["status"]}*" + end + + if raw_updates.has_key?("owner") + updates << "*Owner: #{user_map[raw_updates["owner"]]}*" + end + + if raw_updates.has_key?("cc") + cc = raw_updates["cc"].map do |l| + deleted = l.start_with?("-") + l = l[1..-1] if deleted + l = user_map[l] + l = "~~#{l}~~" if deleted + l + end + + updates << "*Cc: #{cc.join(", ")}*" + end + + if raw_updates.has_key?("labels") + labels = raw_updates["labels"].map do |l| + deleted = l.start_with?("-") + l = l[1..-1] if deleted + l = nice_label_name(l) + l = "~~#{l}~~" if deleted + l + end + + updates << "*Labels: #{labels.join(", ")}*" + end + + if raw_updates.has_key?("mergedInto") + updates << "*Merged into: ##{raw_updates["mergedInto"]}*" + end + + if raw_updates.has_key?("blockedOn") + blocked_ons = raw_updates["blockedOn"].map do |raw_blocked_on| + name, id = raw_blocked_on.split(":", 2) + if name == project.import_source + "##{id}" + else + "#{project.namespace.path}/#{name}##{id}" + end + end + updates << "*Blocked on: #{blocked_ons.join(", ")}*" + end + + if raw_updates.has_key?("blocking") + blockings = raw_updates["blocking"].map do |raw_blocked_on| + name, id = raw_blocked_on.split(":", 2) + if name == project.import_source + "##{id}" + else + "#{project.namespace.path}/#{name}##{id}" + end + end + updates << "*Blocking: #{blockings.join(", ")}*" + end + + updates + end + + def format_attachments(issue_id, comment_id, raw_attachments) + return [] unless raw_attachments + + raw_attachments.map do |attachment| + next if attachment["isDeleted"] + + filename = attachment["fileName"] + link = "https://storage.googleapis.com/google-code-attachments/#{@repo.name}/issue-#{issue_id}/comment-#{comment_id}/#{filename}" + + text = "[#{filename}](#{link})" + text = "!#{text}" if filename =~ /\.(png|jpg|jpeg|gif|bmp|tiff)\z/ + text + end.compact + end + + def format_issue_comment_body(id, author, date, content, updates, attachments) + body = [] + body << "*Comment #{id} by #{author} on #{date}*" + body << "---" + + if content.blank? + content = "*(No comment has been entered for this change)*" + end + body << content + + if updates.any? + body << "---" + body += updates + end + + if attachments.any? + body << "---" + body += attachments + end + + body.join("\n\n") + end + + def format_issue_body(author, date, content, attachments) + body = [] + body << "*By #{author} on #{date}*" + body << "---" + + if content.blank? + content = "*(No description has been entered for this issue)*" + end + body << content + + if attachments.any? + body << "---" + body += attachments + end + + body.join("\n\n") + end + end + end +end diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb new file mode 100644 index 00000000000..0cfeaf9d61c --- /dev/null +++ b/lib/gitlab/google_code_import/project_creator.rb @@ -0,0 +1,37 @@ +module Gitlab + module GoogleCodeImport + class ProjectCreator + attr_reader :repo, :namespace, :current_user, :user_map + + def initialize(repo, namespace, current_user, user_map = nil) + @repo = repo + @namespace = namespace + @current_user = current_user + @user_map = user_map + end + + def execute + project = ::Projects::CreateService.new(current_user, + name: repo.name, + path: repo.name, + description: repo.summary, + namespace: namespace, + creator: current_user, + visibility_level: Gitlab::VisibilityLevel::PUBLIC, + import_type: "google_code", + import_source: repo.name, + import_url: repo.import_url + ).execute + + import_data = project.create_import_data( + data: { + "repo" => repo.raw_data, + "user_map" => user_map + } + ) + + project + end + end + end +end diff --git a/lib/gitlab/google_code_import/repository.rb b/lib/gitlab/google_code_import/repository.rb new file mode 100644 index 00000000000..ad33fc2cad2 --- /dev/null +++ b/lib/gitlab/google_code_import/repository.rb @@ -0,0 +1,43 @@ +module Gitlab + module GoogleCodeImport + class Repository + attr_accessor :raw_data + + def initialize(raw_data) + @raw_data = raw_data + end + + def valid? + raw_data.is_a?(Hash) && raw_data["kind"] == "projecthosting#project" + end + + def id + raw_data["externalId"] + end + + def name + raw_data["name"] + end + + def summary + raw_data["summary"] + end + + def description + raw_data["description"] + end + + def git? + raw_data["versionControlSystem"] == "git" + end + + def import_url + raw_data["repositoryUrls"].first + end + + def issues + raw_data["issues"] && raw_data["issues"]["items"] + end + end + end +end diff --git a/lib/gitlab/key_fingerprint.rb b/lib/gitlab/key_fingerprint.rb new file mode 100644 index 00000000000..baf52ff750d --- /dev/null +++ b/lib/gitlab/key_fingerprint.rb @@ -0,0 +1,55 @@ +module Gitlab + class KeyFingerprint + include Gitlab::Popen + + attr_accessor :key + + def initialize(key) + @key = key + end + + def fingerprint + cmd_status = 0 + cmd_output = '' + + Tempfile.open('gitlab_key_file') do |file| + file.puts key + file.rewind + + cmd = [] + cmd.push *%W(ssh-keygen) + cmd.push *%W(-E md5) if explicit_fingerprint_algorithm? + cmd.push *%W(-lf #{file.path}) + + cmd_output, cmd_status = popen(cmd, '/tmp') + end + + return nil unless cmd_status.zero? + + # 16 hex bytes separated by ':', optionally starting with "MD5:" + fingerprint_matches = cmd_output.match(/(MD5:)?(?<fingerprint>(\h{2}:){15}\h{2})/) + return nil unless fingerprint_matches + + fingerprint_matches[:fingerprint] + end + + private + + def explicit_fingerprint_algorithm? + # OpenSSH 6.8 introduces a new default output format for fingerprints. + # Check the version and decide which command to use. + + version_output, version_status = popen(%W(ssh -V)) + return false unless version_status.zero? + + version_matches = version_output.match(/OpenSSH_(?<major>\d+)\.(?<minor>\d+)/) + return false unless version_matches + + version_info = Gitlab::VersionInfo.new(version_matches[:major].to_i, version_matches[:minor].to_i) + + required_version_info = Gitlab::VersionInfo.new(6, 8) + + version_info >= required_version_info + end + end +end diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index 0cb24d0ccc1..d2ffa2e1fe8 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -27,8 +27,6 @@ module Gitlab def initialize(provider) if self.class.valid_provider?(provider) @provider = provider - elsif provider == 'ldap' - @provider = self.class.providers.first else self.class.invalid_provider(provider) end @@ -82,6 +80,10 @@ module Gitlab options['active_directory'] end + def block_auto_created_users + options['block_auto_created_users'] + end + protected def base_config Gitlab.config.ldap diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index cfa8692659d..f7f3ba9ad7d 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -1,4 +1,4 @@ -require 'gitlab/oauth/user' +require 'gitlab/o_auth/user' # LDAP extension for User model # @@ -13,7 +13,7 @@ module Gitlab def find_by_uid_and_provider(uid, provider) # LDAP distinguished name is case-insensitive identity = ::Identity. - where(provider: [provider, :ldap]). + where(provider: provider). where('lower(extern_uid) = ?', uid.downcase).last identity && identity.user end @@ -39,6 +39,9 @@ module Gitlab end def update_user_attributes + return unless persisted? + + gl_user.skip_reconfirmation! gl_user.email = auth_hash.email # Build new identity only if we dont have have same one @@ -52,13 +55,17 @@ module Gitlab gl_user.changed? || gl_user.identities.any?(&:changed?) end - def needs_blocking? - false + def block_after_signup? + ldap_config.block_auto_created_users end def allowed? Gitlab::LDAP::Access.allowed?(gl_user) end + + def ldap_config + Gitlab::LDAP::Config.new(auth_hash.provider) + end end end end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index b10e85e8a8c..47c456d8dc7 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -32,12 +32,12 @@ module Gitlab module Markdown include IssuesHelper - attr_reader :html_options + attr_reader :options, :html_options # Public: Parse the provided text with GitLab-Flavored Markdown # # text - the source text - # project - extra options for the reference links as given to link_to + # project - the project # html_options - extra options for the reference links as given to link_to def gfm(text, project = @project, html_options = {}) gfm_with_options(text, {}, project, html_options) @@ -46,9 +46,10 @@ module Gitlab # Public: Parse the provided text with GitLab-Flavored Markdown # # text - the source text - # options - parse_tasks: true - render tasks - # - xhtml: true - output XHTML instead of HTML - # project - extra options for the reference links as given to link_to + # options - parse_tasks - render tasks + # - xhtml - output XHTML instead of HTML + # - reference_only_path - Use relative path for reference links + # project - the project # html_options - extra options for the reference links as given to link_to def gfm_with_options(text, options = {}, project = @project, html_options = {}) return text if text.nil? @@ -58,6 +59,13 @@ module Gitlab # for gsub calls to work as we need them to. text = text.dup.to_str + options.reverse_merge!( + parse_tasks: false, + xhtml: false, + reference_only_path: true + ) + + @options = options @html_options = html_options # TODO: add popups with additional information @@ -98,12 +106,13 @@ module Gitlab markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline result = markdown_pipeline.call(text, markdown_context) - saveoptions = 0 + + save_options = 0 if options[:xhtml] - saveoptions |= Nokogiri::XML::Node::SaveOptions::AS_XHTML + save_options |= Nokogiri::XML::Node::SaveOptions::AS_XHTML end - text = result[:output].to_html(save_with: saveoptions) + text = result[:output].to_html(save_with: save_options) # Extract pre blocks so they are not altered # from http://github.github.com/github-flavored-markdown/ @@ -152,7 +161,7 @@ module Gitlab text end - NAME_STR = '[a-zA-Z0-9_][a-zA-Z0-9_\-\.]*' + NAME_STR = Gitlab::Regex::NAMESPACE_REGEX_STR PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})" REFERENCE_PATTERN = %r{ @@ -183,6 +192,7 @@ module Gitlab project_path = $LAST_MATCH_INFO[:project] if project_path actual_project = ::Project.find_with_namespace(project_path) + actual_project = nil unless can?(current_user, :read_project, actual_project) project_prefix = project_path end @@ -229,33 +239,38 @@ module Gitlab end def reference_user(identifier, project = @project, _ = nil) - options = html_options.merge( + link_options = html_options.merge( class: "gfm gfm-project_member #{html_options[:class]}" ) if identifier == "all" - link_to("@all", namespace_project_url(project.namespace, project), options) + link_to( + "@all", + namespace_project_url(project.namespace, project, only_path: options[:reference_only_path]), + link_options + ) elsif namespace = Namespace.find_by(path: identifier) url = - if namespace.type == "Group" - group_url(identifier) - else - user_url(identifier) + if namespace.is_a?(Group) + return nil unless can?(current_user, :read_group, namespace) + group_url(identifier, only_path: options[:reference_only_path]) + else + user_url(identifier, only_path: options[:reference_only_path]) end - - link_to("@#{identifier}", url, options) + + link_to("@#{identifier}", url, link_options) end end def reference_label(identifier, project = @project, _ = nil) if label = project.labels.find_by(id: identifier) - options = html_options.merge( + link_options = html_options.merge( class: "gfm gfm-label #{html_options[:class]}" ) link_to( render_colored_label(label), namespace_project_issues_path(project.namespace, project, label_name: label.name), - options + link_options ) end end @@ -263,14 +278,14 @@ module Gitlab def reference_issue(identifier, project = @project, prefix_text = nil) if project.default_issues_tracker? if project.issue_exists? identifier - url = url_for_issue(identifier, project) + url = url_for_issue(identifier, project, only_path: options[:reference_only_path]) title = title_for_issue(identifier, project) - options = html_options.merge( + link_options = html_options.merge( title: "Issue: #{title}", class: "gfm gfm-issue #{html_options[:class]}" ) - link_to("#{prefix_text}##{identifier}", url, options) + link_to("#{prefix_text}##{identifier}", url, link_options) end else if project.external_issue_tracker.present? @@ -280,44 +295,46 @@ module Gitlab end end - def reference_merge_request(identifier, project = @project, - prefix_text = nil) + def reference_merge_request(identifier, project = @project, prefix_text = nil) if merge_request = project.merge_requests.find_by(iid: identifier) - options = html_options.merge( + link_options = html_options.merge( title: "Merge Request: #{merge_request.title}", class: "gfm gfm-merge_request #{html_options[:class]}" ) url = namespace_project_merge_request_url(project.namespace, project, - merge_request) - link_to("#{prefix_text}!#{identifier}", url, options) + merge_request, + only_path: options[:reference_only_path]) + link_to("#{prefix_text}!#{identifier}", url, link_options) end end def reference_snippet(identifier, project = @project, _ = nil) if snippet = project.snippets.find_by(id: identifier) - options = html_options.merge( + link_options = html_options.merge( title: "Snippet: #{snippet.title}", class: "gfm gfm-snippet #{html_options[:class]}" ) link_to( "$#{identifier}", - namespace_project_snippet_url(project.namespace, project, snippet), - options + namespace_project_snippet_url(project.namespace, project, snippet, + only_path: options[:reference_only_path]), + link_options ) end end def reference_commit(identifier, project = @project, prefix_text = nil) if project.valid_repo? && commit = project.repository.commit(identifier) - options = html_options.merge( + link_options = html_options.merge( title: commit.link_title, class: "gfm gfm-commit #{html_options[:class]}" ) prefix_text = "#{prefix_text}@" if prefix_text link_to( "#{prefix_text}#{identifier}", - namespace_project_commit_url(project.namespace, project, commit), - options + namespace_project_commit_url( project.namespace, project, commit, + only_path: options[:reference_only_path]), + link_options ) end end @@ -328,11 +345,11 @@ module Gitlab inclusive = identifier !~ /\.{3}/ from_id << "^" if inclusive - if project.valid_repo? && - from = project.repository.commit(from_id) && + if project.valid_repo? && + from = project.repository.commit(from_id) && to = project.repository.commit(to_id) - options = html_options.merge( + link_options = html_options.merge( title: "Commits #{from_id} through #{to_id}", class: "gfm gfm-commit_range #{html_options[:class]}" ) @@ -340,22 +357,23 @@ module Gitlab link_to( "#{prefix_text}#{identifier}", - namespace_project_compare_url(project.namespace, project, from: from_id, to: to_id), - options + namespace_project_compare_url(project.namespace, project, + from: from_id, to: to_id, + only_path: options[:reference_only_path]), + link_options ) end end - def reference_external_issue(identifier, project = @project, - prefix_text = nil) - url = url_for_issue(identifier, project) + def reference_external_issue(identifier, project = @project, prefix_text = nil) + url = url_for_issue(identifier, project, only_path: options[:reference_only_path]) title = project.external_issue_tracker.title - options = html_options.merge( + link_options = html_options.merge( title: "Issue in #{title}", class: "gfm gfm-issue #{html_options[:class]}" ) - link_to("#{prefix_text}##{identifier}", url, options) + link_to("#{prefix_text}##{identifier}", url, link_options) end # Turn list items that start with "[ ]" into HTML checkbox inputs. diff --git a/lib/gitlab/oauth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb index ce52beec78e..ce52beec78e 100644 --- a/lib/gitlab/oauth/auth_hash.rb +++ b/lib/gitlab/o_auth/auth_hash.rb diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/o_auth/user.rb index c023d275703..2f5c217d764 100644 --- a/lib/gitlab/oauth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -86,7 +86,7 @@ module Gitlab def user_attributes { name: auth_hash.name, - username: ::User.clean_username(auth_hash.username), + username: ::Namespace.clean_path(auth_hash.username), email: auth_hash.email, password: auth_hash.password, password_confirmation: auth_hash.password, diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 1058d4c43d9..a502a8fe9cd 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -1,94 +1,94 @@ module Gitlab # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor - attr_accessor :users, :labels, :issues, :merge_requests, :snippets, :commits, :commit_ranges + attr_accessor :project, :current_user, :references - include Markdown + include ::Gitlab::Markdown - def initialize - @users, @labels, @issues, @merge_requests, @snippets, @commits, @commit_ranges = - [], [], [], [], [], [], [] + def initialize(project, current_user = nil) + @project = project + @current_user = current_user end - def analyze(string, project) - text = string.dup + def can?(user, action, subject) + Ability.abilities.allowed?(user, action, subject) + end + + def analyze(text) + text = text.dup # Remove preformatted/code blocks so that references are not included text.gsub!(%r{<pre>.*?</pre>|<code>.*?</code>}m) { |match| '' } text.gsub!(%r{^```.*?^```}m) { |match| '' } - parse_references(text, project) + @references = Hash.new { |hash, type| hash[type] = [] } + parse_references(text) end # Given a valid project, resolve the extracted identifiers of the requested type to # model objects. - def users_for(project) - users.map do |entry| - project.users.where(username: entry[:id]).first - end.reject(&:nil?) + def users + references[:user].uniq.map do |project, identifier| + if identifier == "all" + project.team.members.flatten + elsif namespace = Namespace.find_by(path: identifier) + if namespace.is_a?(Group) + namespace.users + else + namespace.owner + end + end + end.flatten.compact.uniq end - def labels_for(project = nil) - labels.map do |entry| - project.labels.where(id: entry[:id]).first - end.reject(&:nil?) + def labels + references[:label].uniq.map do |project, identifier| + project.labels.where(id: identifier).first + end.compact.uniq end - def issues_for(project = nil) - issues.map do |entry| - if should_lookup?(project, entry[:project]) - entry[:project].issues.where(iid: entry[:id]).first + def issues + references[:issue].uniq.map do |project, identifier| + if project.default_issues_tracker? + project.issues.where(iid: identifier).first end - end.reject(&:nil?) + end.compact.uniq end - def merge_requests_for(project = nil) - merge_requests.map do |entry| - if should_lookup?(project, entry[:project]) - entry[:project].merge_requests.where(iid: entry[:id]).first - end - end.reject(&:nil?) + def merge_requests + references[:merge_request].uniq.map do |project, identifier| + project.merge_requests.where(iid: identifier).first + end.compact.uniq end - def snippets_for(project) - snippets.map do |entry| - project.snippets.where(id: entry[:id]).first - end.reject(&:nil?) + def snippets + references[:snippet].uniq.map do |project, identifier| + project.snippets.where(id: identifier).first + end.compact.uniq end - def commits_for(project = nil) - commits.map do |entry| - repo = entry[:project].repository if entry[:project] - if should_lookup?(project, entry[:project]) - repo.commit(entry[:id]) if repo - end - end.reject(&:nil?) + def commits + references[:commit].uniq.map do |project, identifier| + repo = project.repository + repo.commit(identifier) if repo + end.compact.uniq end - def commit_ranges_for(project = nil) - commit_ranges.map do |entry| - repo = entry[:project].repository if entry[:project] - if repo && should_lookup?(project, entry[:project]) - from_id, to_id = entry[:id].split(/\.{2,3}/, 2) + def commit_ranges + references[:commit_range].uniq.map do |project, identifier| + repo = project.repository + if repo + from_id, to_id = identifier.split(/\.{2,3}/, 2) [repo.commit(from_id), repo.commit(to_id)] end - end.reject(&:nil?) + end.compact.uniq end private def reference_link(type, identifier, project, _) - # Append identifier to the appropriate collection. - send("#{type}s") << { project: project, id: identifier } - end - - def should_lookup?(project, entry_project) - if entry_project.nil? - false - else - project.nil? || entry_project.default_issues_tracker? - end + references[type] << [project, identifier] end end end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index cf6e260f257..0571574aa4f 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -2,49 +2,66 @@ module Gitlab module Regex extend self - def username_regex - default_regex + NAMESPACE_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])'.freeze + + def namespace_regex + @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze + end + + def namespace_regex_message + "can contain only letters, digits, '_', '-' and '.'. " \ + "Cannot start with '-' or end in '.'." \ + end + + + def namespace_name_regex + @namespace_name_regex ||= /\A[a-zA-Z0-9_\-\. ]*\z/.freeze end - def username_regex_message - default_regex_message + def namespace_name_regex_message + "can contain only letters, digits, '_', '-', '.' and space." end + def project_name_regex - /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\. ]*\z/ + @project_name_regex ||= /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\. ]*\z/.freeze end - def project_regex_message - "can contain only letters, digits, '_', '-' and '.' and space. " \ + def project_name_regex_message + "can contain only letters, digits, '_', '-', '.' and space. " \ "It must start with letter, digit or '_'." end - def name_regex - /\A[a-zA-Z0-9_\-\. ]*\z/ + + def project_path_regex + @project_path_regex ||= /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\.]*(?<!\.git)\z/.freeze end - def name_regex_message - "can contain only letters, digits, '_', '-' and '.' and space." + def project_path_regex_message + "can contain only letters, digits, '_', '-' and '.'. " \ + "Cannot start with '-' or end in '.git'" \ end - def path_regex - default_regex + + def file_name_regex + @file_name_regex ||= /\A[a-zA-Z0-9_\-\.]*\z/.freeze end - def path_regex_message - default_regex_message + def file_name_regex_message + "can contain only letters, digits, '_', '-' and '.'. " end + def archive_formats_regex - #|zip|tar| tar.gz | tar.bz2 | - /(zip|tar|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/ + # |zip|tar| tar.gz | tar.bz2 | + @archive_formats_regex ||= /(zip|tar|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/.freeze end def git_reference_regex # Valid git ref regex, see: # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html - %r{ + @git_reference_regex ||= %r{ (?! (?# doesn't begins with) \/| (?# rule #6) @@ -60,18 +77,7 @@ module Gitlab (?# doesn't end with) (?<!\.lock) (?# rule #1) (?<![\/.]) (?# rule #6-7) - }x - end - - protected - - def default_regex_message - "can contain only letters, digits, '_', '-' and '.'. " \ - "Cannot start with '-' or end in '.git'" \ - end - - def default_regex - /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\.]*(?<!\.git)\z/ + }x.freeze end end end diff --git a/lib/gitlab/satellite/satellite.rb b/lib/gitlab/satellite/satellite.rb index f24c6199c44..398643d68de 100644 --- a/lib/gitlab/satellite/satellite.rb +++ b/lib/gitlab/satellite/satellite.rb @@ -1,5 +1,10 @@ module Gitlab module Satellite + autoload :DeleteFileAction, 'gitlab/satellite/files/delete_file_action' + autoload :EditFileAction, 'gitlab/satellite/files/edit_file_action' + autoload :FileAction, 'gitlab/satellite/files/file_action' + autoload :NewFileAction, 'gitlab/satellite/files/new_file_action' + class CheckoutFailed < StandardError; end class CommitFailed < StandardError; end class PushFailed < StandardError; end diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 0230fbb010b..84445b3bf2f 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -27,9 +27,9 @@ namespace :gitlab do 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:backup:db:restore"].invoke unless backup.skipped?("db") + Rake::Task["gitlab:backup:repo:restore"].invoke unless backup.skipped?("repositories") + Rake::Task["gitlab:backup:uploads:restore"].invoke unless backup.skipped?("uploads") Rake::Task["gitlab:shell:setup"].invoke backup.cleanup @@ -38,8 +38,13 @@ namespace :gitlab do namespace :repo do task create: :environment do $progress.puts "Dumping repositories ...".blue - Backup::Repository.new.dump - $progress.puts "done".green + + if ENV["SKIP"] && ENV["SKIP"].include?("repositories") + $progress.puts "[SKIPPED]".cyan + else + Backup::Repository.new.dump + $progress.puts "done".green + end end task restore: :environment do @@ -52,8 +57,13 @@ namespace :gitlab do namespace :db do task create: :environment do $progress.puts "Dumping database ... ".blue - Backup::Database.new.dump - $progress.puts "done".green + + if ENV["SKIP"] && ENV["SKIP"].include?("db") + $progress.puts "[SKIPPED]".cyan + else + Backup::Database.new.dump + $progress.puts "done".green + end end task restore: :environment do @@ -66,8 +76,13 @@ namespace :gitlab do namespace :uploads do task create: :environment do $progress.puts "Dumping uploads ... ".blue - Backup::Uploads.new.dump - $progress.puts "done".green + + if ENV["SKIP"] && ENV["SKIP"].include?("uploads") + $progress.puts "[SKIPPED]".cyan + else + Backup::Uploads.new.dump + $progress.puts "done".green + end end task restore: :environment do diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index d791b7155f9..04a2eb12db0 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -687,6 +687,23 @@ namespace :gitlab do end end + namespace :repo do + desc "GITLAB | Check the integrity of the repositories managed by GitLab" + task check: :environment do + namespace_dirs = Dir.glob( + File.join(Gitlab.config.gitlab_shell.repos_path, '*') + ) + + namespace_dirs.each do |namespace_dir| + repo_dirs = Dir.glob(File.join(namespace_dir, '*')) + repo_dirs.each do |dir| + puts "\nChecking repo at #{dir}" + system(*%w(git fsck), chdir: dir) + end + end + end + end + # Helper methods ########################## diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake index b4076f8238f..b4c0ae3ff79 100644 --- a/lib/tasks/gitlab/test.rake +++ b/lib/tasks/gitlab/test.rake @@ -2,6 +2,7 @@ namespace :gitlab do desc "GITLAB | Run all tests" task :test do cmds = [ + %W(rake brakeman), %W(rake rubocop), %W(rake spinach), %W(rake spec), diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb new file mode 100644 index 00000000000..a0909cec3bd --- /dev/null +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe AutocompleteController do + let!(:project) { create(:project) } + let!(:user) { create(:user) } + let!(:user2) { create(:user) } + + context 'project members' do + before do + sign_in(user) + project.team << [user, :master] + + get(:users, project_id: project.id) + end + + let(:body) { JSON.parse(response.body) } + + it { body.should be_kind_of(Array) } + it { body.size.should eq(1) } + it { body.first["username"].should == user.username } + end + + context 'group members' do + let(:group) { create(:group) } + + before do + sign_in(user) + group.add_owner(user) + + get(:users, group_id: group.id) + end + + let(:body) { JSON.parse(response.body) } + + it { body.should be_kind_of(Array) } + it { body.size.should eq(1) } + it { body.first["username"].should == user.username } + end + + context 'all users' do + before do + sign_in(user) + get(:users) + end + + let(:body) { JSON.parse(response.body) } + + it { body.should be_kind_of(Array) } + it { body.size.should eq(User.count) } + end +end diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb new file mode 100644 index 00000000000..93535ced7ae --- /dev/null +++ b/spec/controllers/help_controller_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe HelpController do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + describe 'GET #show' do + context 'for Markdown formats' do + context 'when requested file exists' do + before do + get :show, category: 'ssh', file: 'README', format: :md + end + + it 'assigns to @markdown' do + expect(assigns[:markdown]).not_to be_empty + end + + it 'renders HTML' do + expect(response).to render_template('show.html.haml') + expect(response.content_type).to eq 'text/html' + end + end + + context 'when requested file is missing' do + it 'renders not found' do + get :show, category: 'foo', file: 'bar', format: :md + expect(response).to be_not_found + end + end + end + + context 'for image formats' do + context 'when requested file exists' do + it 'renders the raw file' do + get :show, category: 'workflow/protected_branches', + file: 'protected_branches1', format: :png + expect(response).to be_success + expect(response.content_type).to eq 'image/png' + expect(response.headers['Content-Disposition']).to match(/^inline;/) + end + end + + context 'when requested file is missing' do + it 'renders not found' do + get :show, category: 'foo', file: 'bar', format: :png + expect(response).to be_not_found + end + end + end + + context 'for other formats' do + it 'always renders not found' do + get :show, category: 'ssh', file: 'README', format: :foo + expect(response).to be_not_found + end + end + end +end diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 5dd4124061c..c31563e6d77 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -55,24 +55,109 @@ describe Import::BitbucketController do end describe "POST create" do - before do - @repo = { - slug: 'vim', - owner: "john" + let(:bitbucket_username) { user.username } + + let(:bitbucket_user) { + { + user: { + username: bitbucket_username + } }.with_indifferent_access - end + } - it "takes already existing namespace" do - namespace = create(:namespace, name: "john", owner: user) - expect(Gitlab::BitbucketImport::KeyAdder). - to receive(:new).with(@repo, user). - and_return(double(execute: true)) - expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(@repo, namespace, user). + let(:bitbucket_repo) { + { + slug: "vim", + owner: bitbucket_username + }.with_indifferent_access + } + + before do + allow(Gitlab::BitbucketImport::KeyAdder). + to receive(:new).with(bitbucket_repo, user). and_return(double(execute: true)) - controller.stub_chain(:client, :project).and_return(@repo) - post :create, format: :js + controller.stub_chain(:client, :user).and_return(bitbucket_user) + controller.stub_chain(:client, :project).and_return(bitbucket_repo) + end + + context "when the repository owner is the Bitbucket user" do + context "when the Bitbucket user and GitLab user's usernames match" do + it "takes the current user's namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).with(bitbucket_repo, user.namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the Bitbucket user and GitLab user's usernames don't match" do + let(:bitbucket_username) { "someone_else" } + + it "takes the current user's namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).with(bitbucket_repo, user.namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context "when the repository owner is not the Bitbucket user" do + let(:other_username) { "someone_else" } + + before do + bitbucket_repo["owner"] = other_username + end + + context "when a namespace with the Bitbucket user's username already exists" do + let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } + + context "when the namespace is owned by the GitLab user" do + it "takes the existing namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).with(bitbucket_repo, existing_namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the namespace is not owned by the GitLab user" do + before do + existing_namespace.owner = create(:user) + existing_namespace.save + end + + it "doesn't create a project" do + expect(Gitlab::BitbucketImport::ProjectCreator). + not_to receive(:new) + + post :create, format: :js + end + end + end + + context "when a namespace with the Bitbucket user's username doesn't exist" do + it "creates the namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + post :create, format: :js + + expect(Namespace.where(name: other_username).first).not_to be_nil + end + + it "takes the new namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).with(bitbucket_repo, an_instance_of(Group), user). + and_return(double(execute: true)) + + post :create, format: :js + end + end end end end diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 5b967bfcc0c..3d3846b2e3a 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -56,18 +56,98 @@ describe Import::GithubController do end describe "POST create" do + let(:github_username) { user.username } + + let(:github_user) { + OpenStruct.new(login: github_username) + } + + let(:github_repo) { + OpenStruct.new(name: 'vim', full_name: "#{github_username}/vim", owner: OpenStruct.new(login: github_username)) + } + before do - @repo = OpenStruct.new(login: 'vim', full_name: 'asd/vim', owner: OpenStruct.new(login: "john")) + controller.stub_chain(:client, :user).and_return(github_user) + controller.stub_chain(:client, :repo).and_return(github_repo) + end + + context "when the repository owner is the GitHub user" do + context "when the GitHub user and GitLab user's usernames match" do + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(github_repo, user.namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the GitHub user and GitLab user's usernames don't match" do + let(:github_username) { "someone_else" } + + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(github_repo, user.namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end end - it "takes already existing namespace" do - namespace = create(:namespace, name: "john", owner: user) - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(@repo, namespace, user). - and_return(double(execute: true)) - controller.stub_chain(:client, :repo).and_return(@repo) + context "when the repository owner is not the GitHub user" do + let(:other_username) { "someone_else" } + + before do + github_repo.owner = OpenStruct.new(login: other_username) + end + + context "when a namespace with the GitHub user's username already exists" do + let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } + + context "when the namespace is owned by the GitLab user" do + it "takes the existing namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(github_repo, existing_namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the namespace is not owned by the GitLab user" do + before do + existing_namespace.owner = create(:user) + existing_namespace.save + end + + it "doesn't create a project" do + expect(Gitlab::GithubImport::ProjectCreator). + not_to receive(:new) + + post :create, format: :js + end + end + end + + context "when a namespace with the GitHub user's username doesn't exist" do + it "creates the namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + post :create, format: :js + + expect(Namespace.where(name: other_username).first).not_to be_nil + end + + it "takes the new namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(github_repo, an_instance_of(Group), user). + and_return(double(execute: true)) - post :create, format: :js + post :create, format: :js + end + end end end end diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index b6b86b1bcee..112e51d431e 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -48,23 +48,105 @@ describe Import::GitlabController do end describe "POST create" do - before do - @repo = { + let(:gitlab_username) { user.username } + + let(:gitlab_user) { + { + username: gitlab_username + }.with_indifferent_access + } + + let(:gitlab_repo) { + { path: 'vim', - path_with_namespace: 'asd/vim', - owner: {name: "john"}, - namespace: {path: "john"} + path_with_namespace: "#{gitlab_username}/vim", + owner: { name: gitlab_username }, + namespace: { path: gitlab_username } }.with_indifferent_access + } + + before do + controller.stub_chain(:client, :user).and_return(gitlab_user) + controller.stub_chain(:client, :project).and_return(gitlab_repo) + end + + context "when the repository owner is the GitLab.com user" do + context "when the GitLab.com user and GitLab server user's usernames match" do + it "takes the current user's namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).with(gitlab_repo, user.namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the GitLab.com user and GitLab server user's usernames don't match" do + let(:gitlab_username) { "someone_else" } + + it "takes the current user's namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).with(gitlab_repo, user.namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end end - it "takes already existing namespace" do - namespace = create(:namespace, name: "john", owner: user) - expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(@repo, namespace, user). - and_return(double(execute: true)) - controller.stub_chain(:client, :project).and_return(@repo) + context "when the repository owner is not the GitLab.com user" do + let(:other_username) { "someone_else" } + + before do + gitlab_repo["namespace"]["path"] = other_username + end + + context "when a namespace with the GitLab.com user's username already exists" do + let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } + + context "when the namespace is owned by the GitLab server user" do + it "takes the existing namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).with(gitlab_repo, existing_namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the namespace is not owned by the GitLab server user" do + before do + existing_namespace.owner = create(:user) + existing_namespace.save + end + + it "doesn't create a project" do + expect(Gitlab::GitlabImport::ProjectCreator). + not_to receive(:new) + + post :create, format: :js + end + end + end + + context "when a namespace with the GitLab.com user's username doesn't exist" do + it "creates the namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + post :create, format: :js + + expect(Namespace.where(name: other_username).first).not_to be_nil + end + + it "takes the new namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).with(gitlab_repo, an_instance_of(Group), user). + and_return(double(execute: true)) - post :create, format: :js + post :create, format: :js + end + end end end end diff --git a/spec/controllers/import/google_code_controller_spec.rb b/spec/controllers/import/google_code_controller_spec.rb new file mode 100644 index 00000000000..037cddb4600 --- /dev/null +++ b/spec/controllers/import/google_code_controller_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Import::GoogleCodeController do + let(:user) { create(:user) } + let(:dump_file) { fixture_file_upload(Rails.root + 'spec/fixtures/GoogleCodeProjectHosting.json', 'application/json') } + + before do + sign_in(user) + end + + describe "POST callback" do + it "stores Google Takeout dump list in session" do + post :callback, dump_file: dump_file + + expect(session[:google_code_dump]).to be_a(Hash) + expect(session[:google_code_dump]["kind"]).to eq("projecthosting#user") + expect(session[:google_code_dump]).to have_key("projects") + end + end + + describe "GET status" do + before do + @repo = OpenStruct.new(name: 'vim') + controller.stub_chain(:client, :valid?).and_return(true) + end + + it "assigns variables" do + @project = create(:project, import_type: 'google_code', creator_id: user.id) + controller.stub_chain(:client, :repos).and_return([@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([@repo]) + end + + it "does not show already added project" do + @project = create(:project, import_type: 'google_code', creator_id: user.id, import_source: 'vim') + controller.stub_chain(:client, :repos).and_return([@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([]) + end + end +end diff --git a/spec/controllers/namespaces_controller_spec.rb b/spec/controllers/namespaces_controller_spec.rb new file mode 100644 index 00000000000..9c8619722cd --- /dev/null +++ b/spec/controllers/namespaces_controller_spec.rb @@ -0,0 +1,121 @@ +require 'spec_helper' + +describe NamespacesController do + let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + + describe "GET show" do + context "when the namespace belongs to a user" do + let!(:other_user) { create(:user) } + + it "redirects to the user's page" do + get :show, id: other_user.username + + expect(response).to redirect_to(user_path(other_user)) + end + end + + context "when the namespace belongs to a group" do + let!(:group) { create(:group) } + let!(:project) { create(:project, namespace: group) } + + context "when the group has public projects" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + context "when not signed in" do + it "redirects to the group's page" do + get :show, id: group.path + + expect(response).to redirect_to(group_path(group)) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + it "redirects to the group's page" do + get :show, id: group.path + + expect(response).to redirect_to(group_path(group)) + end + end + end + + context "when the project doesn't have public projects" do + context "when not signed in" do + it "redirects to the sign in page" do + get :show, id: group.path + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + context "when the user has access to the project" do + before do + project.team << [user, :master] + end + + context "when the user is blocked" do + before do + user.block + project.team << [user, :master] + end + + it "redirects to the sign in page" do + get :show, id: group.path + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when the user isn't blocked" do + it "redirects to the group's page" do + get :show, id: group.path + + expect(response).to redirect_to(group_path(group)) + end + end + end + + context "when the user doesn't have access to the project" do + it "responds with status 404" do + get :show, id: group.path + + expect(response.status).to eq(404) + end + end + end + end + end + + context "when the namespace doesn't exist" do + context "when signed in" do + before do + sign_in(user) + end + + it "responds with status 404" do + get :show, id: "doesntexist" + + expect(response.status).to eq(404) + end + end + + context "when not signed in" do + it "redirects to the sign in page" do + get :show, id: "doesntexist" + + expect(response).to redirect_to(new_user_session_path) + end + end + end + end +end diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb new file mode 100644 index 00000000000..c254ab7cb6e --- /dev/null +++ b/spec/controllers/projects/refs_controller_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Projects::RefsController do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + sign_in(user) + project.team << [user, :developer] + end + + describe 'GET #logs_tree' do + def default_get(format = :html) + get :logs_tree, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: 'master', + path: 'foo/bar/baz.html', format: format + end + + def xhr_get(format = :html) + xhr :get, :logs_tree, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: 'master', + path: 'foo/bar/baz.html', format: format + end + + it 'never throws MissingTemplate' do + expect { default_get }.not_to raise_error + expect { xhr_get }.not_to raise_error + end + + it 'renders 404 for non-JS requests' do + xhr_get + + expect(response).to be_not_found + end + + it 'renders JS' do + xhr_get(:js) + expect(response).to be_success + end + end +end diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb new file mode 100644 index 00000000000..91856ed0cc0 --- /dev/null +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -0,0 +1,65 @@ +require "spec_helper" + +describe Projects::RepositoriesController do + let(:project) { create(:project) } + let(:user) { create(:user) } + + describe "GET archive" do + before do + sign_in(user) + project.team << [user, :developer] + + allow(ArchiveRepositoryService).to receive(:new).and_return(service) + end + + let(:service) { ArchiveRepositoryService.new(project, "master", "zip") } + + it "executes ArchiveRepositoryService" do + expect(ArchiveRepositoryService).to receive(:new).with(project, "master", "zip") + expect(service).to receive(:execute) + + get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + end + + context "when the service raises an error" do + + before do + allow(service).to receive(:execute).and_raise("Archive failed") + end + + it "renders Not Found" do + get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + + expect(response.status).to eq(404) + end + end + + context "when the service doesn't return a path" do + + before do + allow(service).to receive(:execute).and_return(nil) + end + + it "reloads the page" do + get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + + expect(response).to redirect_to(archive_namespace_project_repository_path(project.namespace, project, ref: "master", format: "zip")) + end + end + + context "when the service returns a path" do + + let(:path) { Rails.root.join("spec/fixtures/dk.png").to_s } + + before do + allow(service).to receive(:execute).and_return(path) + end + + it "sends the file" do + get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + + expect(response.body).to eq(File.binread(path)) + end + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index fc103e5b133..a5c335c82bc 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -101,21 +101,12 @@ FactoryGirl.define do user end - factory :key_with_a_space_in_the_middle do - key do - "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa ++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" - end - end - factory :another_key do key do "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmTillFzNTrrGgwaCKaSj+QCz81E6jBc/s9av0+3b1Hwfxgkqjl4nAK/OD2NjgyrONDTDfR8cRN4eAAy6nY8GLkOyYBDyuc5nTMqs5z3yVuTwf3koGm/YQQCmo91psZ2BgDFTor8SVEE5Mm1D1k3JDMhDFxzzrOtRYFPci9lskTJaBjpqWZ4E9rDTD2q/QZntCqbC3wE9uSemRQB5f8kik7vD/AD8VQXuzKladrZKkzkONCPWsXDspUitjM8HkQdOf0PsYn1CMUC1xKYbCxkg5TkEosIwGv6CoEArUrdu/4+10LVslq494mAvEItywzrluCLCnwELfW+h/m8UHoVhZ" end - end - factory :invalid_key do - key do - "ssh-rsa this_is_invalid_key==" + factory :another_deploy_key, class: 'DeployKey' do end end end diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb index c8e218d4d03..457859dedaf 100644 --- a/spec/factories_spec.rb +++ b/spec/factories_spec.rb @@ -1,12 +1,6 @@ require 'spec_helper' -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) describe "#{factory_name} factory" do it 'should be valid' do expect(build(factory_name)).to be_valid diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index 41088ce8271..8c6b669ce78 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -6,7 +6,7 @@ describe 'Help Pages', feature: true do login_as :user end it 'replace the variable $your_email with the email of the user' do - visit help_page_path(category: 'ssh', file: 'README.md') + visit help_page_path('ssh', 'README') expect(page).to have_content("ssh-keygen -t rsa -C \"#{@user.email}\"") end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index a2db57ad908..e5f33d5a25a 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -95,7 +95,7 @@ describe 'Issues', feature: true do let(:issue) { @issue } it 'should allow filtering by issues with no specified milestone' do - visit namespace_project_issues_path(project.namespace, project, milestone_id: '0') + visit namespace_project_issues_path(project.namespace, project, milestone_id: IssuableFinder::NONE) expect(page).not_to have_content 'foobar' expect(page).to have_content 'barbaz' @@ -111,7 +111,7 @@ describe 'Issues', feature: true do end it 'should allow filtering by issues with no specified assignee' do - visit namespace_project_issues_path(project.namespace, project, assignee_id: '0') + visit namespace_project_issues_path(project.namespace, project, assignee_id: IssuableFinder::NONE) expect(page).to have_content 'foobar' expect(page).not_to have_content 'barbaz' diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index 21a3a4bf937..4cfaab03caf 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -1,14 +1,37 @@ require 'spec_helper' -describe 'Users', feature: true do - describe "GET /users/sign_in" do - it "should create a new user account" do - visit new_user_session_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_sign_up", with: "password1234" - expect { click_button "Sign up" }.to change { User.count }.by(1) - end +feature 'Users' do + around do |ex| + old_url_options = Rails.application.routes.default_url_options + Rails.application.routes.default_url_options = { host: 'example.foo' } + ex.run + Rails.application.routes.default_url_options = old_url_options + end + + scenario 'GET /users/sign_in creates a new user account' do + visit new_user_session_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_sign_up', with: 'password1234' + expect { click_button 'Sign up' }.to change { User.count }.by(1) + end + + scenario 'Successful user signin invalidates password reset token' do + user = create(:user) + expect(user.reset_password_token).to be_nil + + visit new_user_password_path + fill_in 'user_email', with: user.email + click_button 'Reset password' + + user.reload + expect(user.reset_password_token).not_to be_nil + + login_with(user) + expect(current_path).to eq root_path + + user.reload + expect(user.reset_password_token).to be_nil end end diff --git a/spec/fixtures/GoogleCodeProjectHosting.json b/spec/fixtures/GoogleCodeProjectHosting.json new file mode 100644 index 00000000000..d05e77271ae --- /dev/null +++ b/spec/fixtures/GoogleCodeProjectHosting.json @@ -0,0 +1,407 @@ +{ + "kind" : "projecthosting#user", + "id" : "@WRRVSlFXARlCVgB6", + "projects" : [ { + "kind" : "projecthosting#project", + "name" : "pmn", + "externalId" : "pmn", + "htmlLink" : "/p/pmn/", + "summary" : "Shows an icon in the system tray when you have new emails", + "description" : "IMAP client that shows an icon in the system tray when you have new emails.", + "labels" : [ "Mail" ], + "versionControlSystem" : "svn", + "repositoryUrls" : [ "https://pmn.googlecode.com/svn/" ], + "issuesConfig" : { + "kind" : "projecthosting#projectIssueConfig", + "statuses" : [ { + "status" : "New", + "meansOpen" : true, + "description" : "Issue has not had initial review yet" + }, { + "status" : "Accepted", + "meansOpen" : true, + "description" : "Problem reproduced / Need acknowledged" + }, { + "status" : "Started", + "meansOpen" : true, + "description" : "Work on this issue has begun" + }, { + "status" : "Fixed", + "meansOpen" : false, + "description" : "Developer made source code changes, QA should verify" + }, { + "status" : "Verified", + "meansOpen" : false, + "description" : "QA has verified that the fix worked" + }, { + "status" : "Invalid", + "meansOpen" : false, + "description" : "This was not a valid issue report" + }, { + "status" : "Duplicate", + "meansOpen" : false, + "description" : "This report duplicates an existing issue" + }, { + "status" : "WontFix", + "meansOpen" : false, + "description" : "We decided to not take action on this issue" + }, { + "status" : "Done", + "meansOpen" : false, + "description" : "The requested non-coding task was completed" + } ], + "labels" : [ { + "label" : "Type-Defect", + "description" : "Report of a software defect" + }, { + "label" : "Type-Enhancement", + "description" : "Request for enhancement" + }, { + "label" : "Type-Task", + "description" : "Work item that doesn't change the code or docs" + }, { + "label" : "Type-Review", + "description" : "Request for a source code review" + }, { + "label" : "Type-Other", + "description" : "Some other kind of issue" + }, { + "label" : "Priority-Critical", + "description" : "Must resolve in the specified milestone" + }, { + "label" : "Priority-High", + "description" : "Strongly want to resolve in the specified milestone" + }, { + "label" : "Priority-Medium", + "description" : "Normal priority" + }, { + "label" : "Priority-Low", + "description" : "Might slip to later milestone" + }, { + "label" : "OpSys-All", + "description" : "Affects all operating systems" + }, { + "label" : "OpSys-Windows", + "description" : "Affects Windows users" + }, { + "label" : "OpSys-Linux", + "description" : "Affects Linux users" + }, { + "label" : "OpSys-OSX", + "description" : "Affects Mac OS X users" + }, { + "label" : "Milestone-Release1.0", + "description" : "All essential functionality working" + }, { + "label" : "Component-UI", + "description" : "Issue relates to program UI" + }, { + "label" : "Component-Logic", + "description" : "Issue relates to application logic" + }, { + "label" : "Component-Persistence", + "description" : "Issue relates to data storage components" + }, { + "label" : "Component-Scripts", + "description" : "Utility and installation scripts" + }, { + "label" : "Component-Docs", + "description" : "Issue relates to end-user documentation" + }, { + "label" : "Security", + "description" : "Security risk to users" + }, { + "label" : "Performance", + "description" : "Performance issue" + }, { + "label" : "Usability", + "description" : "Affects program usability" + }, { + "label" : "Maintainability", + "description" : "Hinders future changes" + } ], + "prompts" : [ { + "name" : "Defect report from user", + "title" : "Enter one-line summary", + "description" : "What steps will reproduce the problem?\n1. \n2. \n3. \n\nWhat is the expected output? What do you see instead?\n\n\nWhat version of the product are you using? On what operating system?\n\n\nPlease provide any additional information below.\n", + "titleMustBeEdited" : true, + "status" : "New", + "labels" : [ "Type-Defect", "Priority-Medium" ] + }, { + "name" : "Defect report from developer", + "title" : "Enter one-line summary", + "description" : "What steps will reproduce the problem?\n1. \n2. \n3. \n\nWhat is the expected output? What do you see instead?\n\n\nPlease use labels and text to provide additional information.\n", + "titleMustBeEdited" : true, + "status" : "Accepted", + "labels" : [ "Type-Defect", "Priority-Medium" ], + "membersOnly" : true + }, { + "name" : "Review request", + "title" : "Code review request", + "description" : "Branch name:\n\nPurpose of code changes on this branch:\n\n\nWhen reviewing my code changes, please focus on:\n\n\nAfter the review, I'll merge this branch into:\n/trunk\n", + "status" : "New", + "labels" : [ "Type-Review", "Priority-Medium" ], + "membersOnly" : true, + "defaultToMember" : false + } ], + "defaultPromptForMembers" : 1, + "defaultPromptForNonMembers" : 0 + }, + "role" : "owner", + "members" : [ { + "kind" : "projecthosting#issuePerson", + "name" : "mrovi9000", + "htmlLink" : "https://code.google.com/u/106736353629303906862/" + } ], + "issues" : { + "kind" : "projecthosting#issueList", + "totalResults" : 0, + "items" : [ ] + } + }, { + "kind" : "projecthosting#project", + "name" : "tint2", + "externalId" : "tint2", + "htmlLink" : "/p/tint2/", + "summary" : "tint2 is a lightweight panel/taskbar.", + "description" : "tint2 is a simple _*panel/taskbar*_ unintrusive and light (memory / cpu / aestetic). <br>We follow freedesktop specifications.\r\n \r\n=== 0.11 features ===\r\n * panel with taskbar, systray, clock and battery status\r\n * easy to customize : color/transparency on font, icon, border and background\r\n * pager like capability : send task from one workspace to another, switch workspace\r\n * multi-monitor capability : one panel per monitor, show task from current monitor\r\n * customize mouse event\r\n * window manager's menu\r\n * tooltip\r\n * autohide\r\n * clock timezones\r\n * real & fake transparency with autodetection of composite manager\r\n * panel's theme switcher 'tint2conf' \r\n\r\n=== Other project ===\r\n * Lightweight volume control http://softwarebakery.com/maato/volumeicon.html\r\n * Lightweight calendar http://code.google.com/p/gsimplecal/\r\n * Graphical config tool http://code.google.com/p/tintwizard/\r\n * Command line theme switcher http://github.com/dbbolton/scripts/blob/master/tint2theme\r\n\r\n\r\n=== Snapshot SVN ===\r\n\r\nhttp://img252.imageshack.us/img252/1433/wallpaper2td.jpg\r\n\r\n\r\n", + "labels" : [ "taskbar", "panel", "lightweight", "desktop", "openbox", "pager", "tint2" ], + "versionControlSystem" : "git", + "repositoryUrls" : [ "https://tint2.googlecode.com/git/" ], + "issuesConfig" : { + "kind" : "projecthosting#projectIssueConfig", + "defaultColumns" : [ "ID", "Status", "Type", "Milestone", "Priority", "Component", "Owner", "Summary", "Modified", "Stars" ], + "defaultSorting" : [ "-ID" ], + "statuses" : [ { + "status" : "New", + "meansOpen" : true, + "description" : "Issue has not had initial review yet" + }, { + "status" : "NeedInfo", + "meansOpen" : true, + "description" : "More information is needed before deciding what action should be taken" + }, { + "status" : "Accepted", + "meansOpen" : true, + "description" : "A Defect that a developer has reproduced or an Enhancement that a developer has committed to addressing" + }, { + "status" : "Wishlist", + "meansOpen" : true, + "description" : "An Enhancement which is valid, but no developers have committed to addressing" + }, { + "status" : "Started", + "meansOpen" : true, + "description" : "Work on this issue has begun" + }, { + "status" : "Fixed", + "meansOpen" : false, + "description" : "Work has completed" + }, { + "status" : "Invalid", + "meansOpen" : false, + "description" : "This was not a valid issue report" + }, { + "status" : "Duplicate", + "meansOpen" : false, + "description" : "This report duplicates an existing issue" + }, { + "status" : "WontFix", + "meansOpen" : false, + "description" : "We decided to not take action on this issue" + }, { + "status" : "Incomplete", + "meansOpen" : false, + "description" : "Not enough information and no activity for a long period of time" + } ], + "labels" : [ { + "label" : "Type-Defect", + "description" : "Report of a software defect" + }, { + "label" : "Type-Enhancement", + "description" : "Request for enhancement" + }, { + "label" : "Type-Task", + "description" : "Work item that does not change the code" + }, { + "label" : "Type-Review", + "description" : "Request for a source code review" + }, { + "label" : "Type-Other", + "description" : "Some other kind of issue" + }, { + "label" : "Milestone-0.12", + "description" : "Fix should be included in release 0.12" + }, { + "label" : "Priority-Critical", + "description" : "Must resolve in the specified milestone" + }, { + "label" : "Priority-High", + "description" : "Strongly want to resolve in the specified milestone" + }, { + "label" : "Priority-Medium", + "description" : "Normal priority" + }, { + "label" : "Priority-Low", + "description" : "Might slip to later milestone" + }, { + "label" : "OpSys-All", + "description" : "Affects all operating systems" + }, { + "label" : "OpSys-Windows", + "description" : "Affects Windows users" + }, { + "label" : "OpSys-Linux", + "description" : "Affects Linux users" + }, { + "label" : "OpSys-OSX", + "description" : "Affects Mac OS X users" + }, { + "label" : "Security", + "description" : "Security risk to users" + }, { + "label" : "Performance", + "description" : "Performance issue" + }, { + "label" : "Usability", + "description" : "Affects program usability" + }, { + "label" : "Maintainability", + "description" : "Hinders future changes" + }, { + "label" : "Component-Panel", + "description" : "Issue relates to the panel (e.g. positioning, hiding, transparency)" + }, { + "label" : "Component-Taskbar", + "description" : "Issue relates to the taskbar (e.g. tasks, multiple desktops)" + }, { + "label" : "Component-Battery", + "description" : "Issue relates to the battery" + }, { + "label" : "Component-Systray", + "description" : "Issue relates to the system tray" + }, { + "label" : "Component-Clock", + "description" : "Issue relates to the clock" + }, { + "label" : "Component-Launcher", + "description" : "Issue relates to the launcher" + }, { + "label" : "Component-Tint2conf", + "description" : "Issue relates to the configuration GUI (tint2conf)" + }, { + "label" : "Component-Docs", + "description" : "Issue relates to end-user documentation" + }, { + "label" : "Component-New", + "description" : "Issue describes a new component proposal" + } ], + "prompts" : [ { + "name" : "Defect report from user", + "title" : "Enter one-line summary", + "description" : "What steps will reproduce the problem?\n1.\n2.\n3.\n\nWhat is the expected output? What do you see instead?\n\n\nWhat version of the product are you using? On what operating system?\n\n\nWhich window manager (e.g. openbox, xfwm, metacity, mutter, kwin) or\nwhich desktop environment (e.g. Gnome 2, Gnome 3, LXDE, XFCE, KDE)\nare you using?\n\n\nPlease provide any additional information below. It might be helpful\nto attach your tint2rc file (usually located at ~/.config/tint2/tint2rc).", + "titleMustBeEdited" : true, + "status" : "New", + "labels" : [ "Priority-Medium" ], + "defaultToMember" : true + }, { + "name" : "Defect report from developer", + "title" : "Enter one-line summary", + "description" : "What steps will reproduce the problem?\n1.\n2.\n3.\n\nWhat is the expected output? What do you see instead?\n\n\nPlease use labels and text to provide additional information.", + "titleMustBeEdited" : true, + "status" : "Accepted", + "labels" : [ "Type-Defect", "Priority-Medium" ], + "membersOnly" : true, + "defaultToMember" : true + }, { + "name" : "Review request", + "title" : "Code review request", + "description" : "Purpose of code changes on this branch:\n\n\nWhen reviewing my code changes, please focus on:\n\n\nAfter the review, I'll merge this branch into:\n/trunk", + "status" : "New", + "labels" : [ "Type-Review", "Priority-Medium" ], + "membersOnly" : true, + "defaultToMember" : true + } ], + "defaultPromptForMembers" : 1, + "defaultPromptForNonMembers" : 0, + "usersCanSetLabels" : false + }, + "role" : "owner", + "issues" : { + "kind" : "projecthosting#issueList", + "totalResults" : 473, + "items" : [ { + "kind" : "projecthosting#issue", + "id" : 169, + "title" : "Scrolling through tasks", + "summary" : "Scrolling through tasks", + "stars" : 1, + "starred" : false, + "status" : "Fixed", + "state" : "closed", + "labels" : [ "Type-Enhancement", "Priority-Medium" ], + "author" : { + "kind" : "projecthosting#issuePerson", + "name" : "schattenpr...", + "htmlLink" : "https://code.google.com/u/106498139506637530000/" + }, + "owner" : { + "kind" : "projecthosting#issuePerson", + "name" : "thilo...", + "htmlLink" : "https://code.google.com/u/104224918623172014000/" + }, + "updated" : "2009-11-18T05:14:58.000Z", + "published" : "2009-11-18T00:20:19.000Z", + "closed" : "2009-11-18T05:14:58.000Z", + "projectId" : "tint2", + "canComment" : true, + "canEdit" : true, + "comments" : { + "kind" : "projecthosting#issueCommentList", + "totalResults" : 2, + "items" : [ { + "id" : 0, + "kind" : "projecthosting#issueComment", + "author" : { + "kind" : "projecthosting#issuePerson", + "name" : "schattenpr...", + "htmlLink" : "https://code.google.com/u/10649813950663753000/" + }, + "content" : "I like to scroll through the tasks with my scrollwheel (like in fluxbox). \r\n\r\nPatch is attached that adds two new mouse-actions (next_task+prev_task) \r\nthat can be used for exactly that purpose. \r\n\r\nall the best!", + "published" : "2009-11-18T00:20:19.000Z", + "updates" : { + "kind" : "projecthosting#issueCommentUpdate" + }, + "canDelete" : true, + "attachments" : [ { + "attachmentId" : "8901002890399325565", + "fileName" : "tint2_task_scrolling.diff", + "fileSize" : 3059, + "mimetype" : "text/x-c++; charset=us-ascii" + }, { + "attachmentId" : "000", + "fileName" : "screenshot.png", + "fileSize" : 0, + "mimetype" : "image/png" + } ] + }, { + "id" : 1, + "kind" : "projecthosting#issueComment", + "author" : { + "kind" : "projecthosting#issuePerson", + "name" : "thilo...", + "htmlLink" : "https://code.google.com/u/104224918623172014000/" + }, + "content" : "applied, thanks.\r\n", + "published" : "2009-11-18T05:14:58.000Z", + "updates" : { + "kind" : "projecthosting#issueCommentUpdate", + "status" : "Fixed", + "labels" : [ "-Type-Defect", "Type-Enhancement" ] + }, + "canDelete" : true + } ] + } + } ] + } + } ] +} diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 4c11709ed6e..015a66f7fa0 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -225,25 +225,29 @@ describe ApplicationHelper do end describe 'link_to' do - it 'should not include rel=nofollow for internal links' do - expect(link_to('Home', root_path)).to eq("<a href=\"/\">Home</a>") + expect(link_to('Home', root_path)).to eq('<a href="/">Home</a>') end it 'should include rel=nofollow for external links' do - expect(link_to('Example', 'http://www.example.com')).to eq("<a href=\"http://www.example.com\" rel=\"nofollow\">Example</a>") + expect(link_to('Example', 'http://www.example.com')). + to eq '<a href="http://www.example.com" rel="nofollow">Example</a>' + end + + it 'should include rel=nofollow for external links and honor existing html_options' do + expect(link_to('Example', 'http://www.example.com', class: 'toggle', data: {toggle: 'dropdown'})) + .to eq '<a class="toggle" data-toggle="dropdown" href="http://www.example.com" rel="nofollow">Example</a>' end - it 'should include re=nofollow for external links and honor existing html_options' do - expect( - link_to('Example', 'http://www.example.com', class: 'toggle', data: {toggle: 'dropdown'}) - ).to eq("<a class=\"toggle\" data-toggle=\"dropdown\" href=\"http://www.example.com\" rel=\"nofollow\">Example</a>") + it 'should include rel=nofollow for external links and preserve other rel values' do + expect(link_to('Example', 'http://www.example.com', rel: 'noreferrer')) + .to eq '<a href="http://www.example.com" rel="noreferrer nofollow">Example</a>' end - it 'should include rel=nofollow for external links and preserver other rel values' do - expect( - link_to('Example', 'http://www.example.com', rel: 'noreferrer') - ).to eq("<a href=\"http://www.example.com\" rel=\"noreferrer nofollow\">Example</a>") + it 'should not include rel=nofollow for external links on the same host as GitLab' do + expect(Gitlab.config.gitlab).to receive(:host).and_return('example.foo') + expect(link_to('Example', 'http://example.foo/bar')). + to eq '<a href="http://example.foo/bar">Example</a>' end end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index c631acc591d..944e743675c 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -4,6 +4,11 @@ describe GitlabMarkdownHelper do include ApplicationHelper include IssuesHelper + # TODO: Properly test this + def can?(*) + true + end + let!(:project) { create(:project) } let(:empty_project) { create(:empty_project) } @@ -15,6 +20,9 @@ describe GitlabMarkdownHelper do let(:snippet) { create(:project_snippet, project: project) } let(:member) { project.project_members.where(user_id: user).first } + # Helper expects a current_user method. + let(:current_user) { user } + def url_helper(image_name) File.join(root_url, 'assets', image_name) end @@ -651,7 +659,7 @@ describe GitlabMarkdownHelper do end it "should leave ref-like href of 'manual' links untouched" do - expect(markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})")).to eq("<p>why not <a href=\"http://example.tld/#!#{merge_request.iid}\">inspect </a><a class=\"gfm gfm-merge_request \" href=\"#{namespace_project_merge_request_url(project.namespace, project, merge_request)}\" title=\"Merge Request: #{merge_request.title}\">!#{merge_request.iid}</a><a href=\"http://example.tld/#!#{merge_request.iid}\"></a></p>\n") + expect(markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})")).to eq("<p>why not <a href=\"http://example.tld/#!#{merge_request.iid}\">inspect </a><a class=\"gfm gfm-merge_request \" href=\"#{namespace_project_merge_request_path(project.namespace, project, 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 diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb new file mode 100644 index 00000000000..c052981fe73 --- /dev/null +++ b/spec/helpers/icons_helper_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +describe IconsHelper do + describe 'file_type_icon_class' do + it 'returns folder class' do + expect(file_type_icon_class('folder', 0, 'folder_name')).to eq 'folder' + end + + it 'returns share class' do + expect(file_type_icon_class('file', '120000', 'link')).to eq 'share' + end + + it 'returns file-pdf-o class with .pdf' do + expect(file_type_icon_class('file', 0, 'filename.pdf')).to eq 'file-pdf-o' + end + + it 'returns file-image-o class with .jpg' do + expect(file_type_icon_class('file', 0, 'filename.jpg')).to eq 'file-image-o' + end + + it 'returns file-image-o class with .JPG' do + expect(file_type_icon_class('file', 0, 'filename.JPG')).to eq 'file-image-o' + end + + it 'returns file-image-o class with .png' do + expect(file_type_icon_class('file', 0, 'filename.png')).to eq 'file-image-o' + end + + it 'returns file-archive-o class with .tar' do + expect(file_type_icon_class('file', 0, 'filename.tar')).to eq 'file-archive-o' + end + + it 'returns file-archive-o class with .TAR' do + expect(file_type_icon_class('file', 0, 'filename.TAR')).to eq 'file-archive-o' + end + + it 'returns file-archive-o class with .tar.gz' do + expect(file_type_icon_class('file', 0, 'filename.tar.gz')).to eq 'file-archive-o' + end + + it 'returns file-audio-o class with .mp3' do + expect(file_type_icon_class('file', 0, 'filename.mp3')).to eq 'file-audio-o' + end + + it 'returns file-audio-o class with .MP3' do + expect(file_type_icon_class('file', 0, 'filename.MP3')).to eq 'file-audio-o' + end + + it 'returns file-audio-o class with .wav' do + expect(file_type_icon_class('file', 0, 'filename.wav')).to eq 'file-audio-o' + end + + it 'returns file-video-o class with .avi' do + expect(file_type_icon_class('file', 0, 'filename.avi')).to eq 'file-video-o' + end + + it 'returns file-video-o class with .AVI' do + expect(file_type_icon_class('file', 0, 'filename.AVI')).to eq 'file-video-o' + end + + it 'returns file-video-o class with .mp4' do + expect(file_type_icon_class('file', 0, 'filename.mp4')).to eq 'file-video-o' + end + + it 'returns file-word-o class with .doc' do + expect(file_type_icon_class('file', 0, 'filename.doc')).to eq 'file-word-o' + end + + it 'returns file-word-o class with .DOC' do + expect(file_type_icon_class('file', 0, 'filename.DOC')).to eq 'file-word-o' + end + + it 'returns file-word-o class with .docx' do + expect(file_type_icon_class('file', 0, 'filename.docx')).to eq 'file-word-o' + end + + it 'returns file-excel-o class with .xls' do + expect(file_type_icon_class('file', 0, 'filename.xls')).to eq 'file-excel-o' + end + + it 'returns file-excel-o class with .XLS' do + expect(file_type_icon_class('file', 0, 'filename.XLS')).to eq 'file-excel-o' + end + + it 'returns file-excel-o class with .xlsx' do + expect(file_type_icon_class('file', 0, 'filename.xlsx')).to eq 'file-excel-o' + end + + it 'returns file-excel-o class with .ppt' do + expect(file_type_icon_class('file', 0, 'filename.ppt')).to eq 'file-powerpoint-o' + end + + it 'returns file-excel-o class with .PPT' do + expect(file_type_icon_class('file', 0, 'filename.PPT')).to eq 'file-powerpoint-o' + end + + it 'returns file-excel-o class with .pptx' do + expect(file_type_icon_class('file', 0, 'filename.pptx')).to eq 'file-powerpoint-o' + end + + it 'returns file-text-o class with .unknow' do + expect(file_type_icon_class('file', 0, 'filename.unknow')).to eq 'file-text-o' + end + + it 'returns file-text-o class with no extension' do + expect(file_type_icon_class('file', 0, 'CHANGELOG')).to eq 'file-text-o' + end + end +end diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb index aef1108e333..e99c3f5bc11 100644 --- a/spec/helpers/submodule_helper_spec.rb +++ b/spec/helpers/submodule_helper_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe SubmoduleHelper do + include RepoHelpers + describe 'submodule links' do let(:submodule_item) { double(id: 'hash', path: 'rack') } let(:config) { Gitlab.config.gitlab } @@ -111,6 +113,39 @@ describe SubmoduleHelper do expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) end end + + context 'submodules with relative links' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + + before do + self.instance_variable_set(:@project, project) + end + + it 'one level down' do + commit_id = sample_commit[:id] + result = relative_self_links('../test.git', commit_id) + expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"]) + end + + it 'two levels down' do + commit_id = sample_commit[:id] + result = relative_self_links('../../test.git', commit_id) + expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"]) + end + + it 'one level down with namespace and repo' do + commit_id = sample_commit[:id] + result = relative_self_links('../foobar/test.git', commit_id) + expect(result).to eq(["/foobar/test", "/foobar/test/tree/#{commit_id}"]) + end + + it 'two levels down with namespace and repo' do + commit_id = sample_commit[:id] + result = relative_self_links('../foobar/baz/test.git', commit_id) + expect(result).to eq(["/baz/test", "/baz/test/tree/#{commit_id}"]) + end + end end def stub_url(url) diff --git a/spec/lib/file_size_validator_spec.rb b/spec/lib/file_size_validator_spec.rb new file mode 100644 index 00000000000..5c89c854714 --- /dev/null +++ b/spec/lib/file_size_validator_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe 'Gitlab::FileSizeValidatorSpec' do + let(:validator) { FileSizeValidator.new(options) } + let(:attachment) { AttachmentUploader.new } + let(:note) { create(:note) } + + describe 'options uses an integer' do + let(:options) { { maximum: 10, attributes: { attachment: attachment } } } + + it 'attachment exceeds maximum limit' do + allow(attachment).to receive(:size) { 100 } + validator.validate_each(note, :attachment, attachment) + expect(note.errors).to have_key(:attachment) + end + + it 'attachment under maximum limit' do + allow(attachment).to receive(:size) { 1 } + validator.validate_each(note, :attachment, attachment) + expect(note.errors).not_to have_key(:attachment) + end + end + + describe 'options uses a symbol' do + let(:options) { { maximum: :test, + attributes: { attachment: attachment } } } + before do + allow(note).to receive(:test) { 10 } + end + + it 'attachment exceeds maximum limit' do + allow(attachment).to receive(:size) { 100 } + validator.validate_each(note, :attachment, attachment) + expect(note.errors).to have_key(:attachment) + end + + it 'attachment under maximum limit' do + allow(attachment).to receive(:size) { 1 } + validator.validate_each(note, :attachment, attachment) + expect(note.errors).not_to have_key(:attachment) + end + end +end diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb index f5523105848..0ec6a43f681 100644 --- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb @@ -8,8 +8,12 @@ describe Gitlab::BitbucketImport::ProjectCreator do is_private: true, owner: "asd"}.with_indifferent_access } - let(:namespace){ create(:namespace) } + let(:namespace){ create(:group, owner: user) } + before do + namespace.add_owner(user) + end + it 'creates project' do allow_any_instance_of(Project).to receive(:add_import_job) diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index c96ee78e5fd..cb7b0fbb890 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -5,126 +5,128 @@ describe Gitlab::ClosingIssueExtractor do let(:issue) { create(:issue, project: project) } let(:iid1) { issue.iid } - describe :closed_by_message_in_project do + subject { described_class.new(project, project.creator) } + + describe "#closed_by_message" do context 'with a single reference' do it do message = "Awesome commit (Closes ##{iid1})" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "Awesome commit (closes ##{iid1})" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "Closed ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "closed ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "Closing ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "closing ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "Close ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "close ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "Awesome commit (Fixes ##{iid1})" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "Awesome commit (fixes ##{iid1})" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "Fixed ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "fixed ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "Fixing ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "fixing ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "Fix ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "fix ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "Awesome commit (Resolves ##{iid1})" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "Awesome commit (resolves ##{iid1})" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "Resolved ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "resolved ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "Resolving ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "resolving ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "Resolve ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end it do message = "resolve ##{iid1}" - expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) + expect(subject.closed_by_message(message)).to eq([issue]) end end @@ -137,28 +139,28 @@ describe Gitlab::ClosingIssueExtractor do it 'fetches issues in single line message' do message = "Closes ##{iid1} and fix ##{iid2}" - expect(subject.closed_by_message_in_project(message, project)). + expect(subject.closed_by_message(message)). to eq([issue, other_issue]) end it 'fetches comma-separated issues references in single line message' do message = "Closes ##{iid1}, closes ##{iid2}" - expect(subject.closed_by_message_in_project(message, project)). + expect(subject.closed_by_message(message)). to eq([issue, other_issue]) end it 'fetches comma-separated issues numbers in single line message' do message = "Closes ##{iid1}, ##{iid2} and ##{iid3}" - expect(subject.closed_by_message_in_project(message, project)). + expect(subject.closed_by_message(message)). to eq([issue, other_issue, third_issue]) end it 'fetches issues in multi-line message' do message = "Awesome commit (closes ##{iid1})\nAlso fixes ##{iid2}" - expect(subject.closed_by_message_in_project(message, project)). + expect(subject.closed_by_message(message)). to eq([issue, other_issue]) end @@ -166,7 +168,7 @@ describe Gitlab::ClosingIssueExtractor do message = "Awesome commit (closes ##{iid1})\n"\ "Also fixing issues ##{iid2}, ##{iid3} and #4" - expect(subject.closed_by_message_in_project(message, project)). + expect(subject.closed_by_message(message)). to eq([issue, other_issue, third_issue]) end end diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb index 8d594a112d4..3bf52cb685e 100644 --- a/spec/lib/gitlab/github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/github_import/project_creator_spec.rb @@ -10,7 +10,11 @@ describe Gitlab::GithubImport::ProjectCreator do clone_url: "https://gitlab.com/asd/vim.git", owner: OpenStruct.new(login: "john")) } - let(:namespace){ create(:namespace) } + let(:namespace){ create(:group, owner: user) } + + before do + namespace.add_owner(user) + end it 'creates project' do allow_any_instance_of(Project).to receive(:add_import_job) diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb index 4c0d64ed138..3cefe4ea8e2 100644 --- a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb +++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb @@ -10,7 +10,11 @@ describe Gitlab::GitlabImport::ProjectCreator do http_url_to_repo: "https://gitlab.com/asd/vim.git", owner: {name: "john"}}.with_indifferent_access } - let(:namespace){ create(:namespace) } + let(:namespace){ create(:group, owner: user) } + + before do + namespace.add_owner(user) + end it 'creates project' do allow_any_instance_of(Project).to receive(:add_import_job) diff --git a/spec/lib/gitlab/gitorious_import/project_creator.rb b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb index cf2318bb3a2..c1125ca6357 100644 --- a/spec/lib/gitlab/gitorious_import/project_creator.rb +++ b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb @@ -3,14 +3,17 @@ require 'spec_helper' describe Gitlab::GitoriousImport::ProjectCreator do let(:user) { create(:user) } let(:repo) { Gitlab::GitoriousImport::Repository.new('foo/bar-baz-qux') } - let(:namespace){ create(:namespace) } + let(:namespace){ create(:group, owner: user) } + + before do + namespace.add_owner(user) + end it 'creates project' do allow_any_instance_of(Project).to receive(:add_import_job) project_creator = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, user) - project_creator.execute - project = Project.last + project = project_creator.execute expect(project.name).to eq("Bar Baz Qux") expect(project.path).to eq("bar-baz-qux") diff --git a/spec/lib/gitlab/google_code_import/client_spec.rb b/spec/lib/gitlab/google_code_import/client_spec.rb new file mode 100644 index 00000000000..d2bf871daa8 --- /dev/null +++ b/spec/lib/gitlab/google_code_import/client_spec.rb @@ -0,0 +1,34 @@ +require "spec_helper" + +describe Gitlab::GoogleCodeImport::Client do + let(:raw_data) { JSON.parse(File.read(Rails.root.join("spec/fixtures/GoogleCodeProjectHosting.json"))) } + subject { described_class.new(raw_data) } + + describe "#valid?" do + context "when the data is valid" do + it "returns true" do + expect(subject).to be_valid + end + end + + context "when the data is invalid" do + let(:raw_data) { "No clue" } + + it "returns true" do + expect(subject).to_not be_valid + end + end + end + + describe "#repos" do + it "returns only Git repositories" do + expect(subject.repos.length).to eq(1) + end + end + + describe "#repo" do + it "returns the referenced repository" do + expect(subject.repo("tint2").name).to eq("tint2") + end + end +end diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb new file mode 100644 index 00000000000..1c4503ae0ef --- /dev/null +++ b/spec/lib/gitlab/google_code_import/importer_spec.rb @@ -0,0 +1,84 @@ +require "spec_helper" + +describe Gitlab::GoogleCodeImport::Importer do + let(:mapped_user) { create(:user, username: "thilo123") } + let(:raw_data) { JSON.parse(File.read(Rails.root.join("spec/fixtures/GoogleCodeProjectHosting.json"))) } + let(:client) { Gitlab::GoogleCodeImport::Client.new(raw_data) } + let(:import_data) { + { + "repo" => client.repo("tint2").raw_data, + "user_map" => { + "thilo..." => "@#{mapped_user.username}" + } + } + } + let(:project) { create(:project) } + subject { described_class.new(project) } + + before do + project.create_import_data(data: import_data) + end + + describe "#execute" do + + it "imports status labels" do + subject.execute + + %w(New NeedInfo Accepted Wishlist Started Fixed Invalid Duplicate WontFix Incomplete).each do |status| + expect(project.labels.find_by(name: "Status: #{status}")).to_not be_nil + end + end + + it "imports labels" do + subject.execute + + %w( + Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical + Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security + Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery + Component-Systray Component-Clock Component-Launcher Component-Tint2conf Component-Docs Component-New + ).each do |label| + label.sub!("-", ": ") + expect(project.labels.find_by(name: label)).to_not be_nil + end + end + + it "imports issues" do + subject.execute + + issue = project.issues.first + expect(issue).to_not be_nil + expect(issue.iid).to eq(169) + expect(issue.author).to eq(project.creator) + expect(issue.assignee).to eq(mapped_user) + expect(issue.state).to eq("closed") + expect(issue.label_names).to include("Priority: Medium") + expect(issue.label_names).to include("Status: Fixed") + expect(issue.label_names).to include("Type: Enhancement") + expect(issue.title).to eq("Scrolling through tasks") + expect(issue.state).to eq("closed") + expect(issue.description).to include("schattenpr...") + expect(issue.description).to include("November 18, 2009 00:20") + expect(issue.description).to include('I like to scroll through the tasks with my scrollwheel \(like in fluxbox\).') + expect(issue.description).to include('Patch is attached that adds two new mouse\-actions \(next\_taskprev\_task\)') + expect(issue.description).to include('that can be used for exactly that purpose.') + expect(issue.description).to include('all the best!') + expect(issue.description).to include('[tint2_task_scrolling.diff](https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/tint2_task_scrolling.diff)') + expect(issue.description).to include('') + end + + it "imports issue comments" do + subject.execute + + note = project.issues.first.notes.first + expect(note).to_not be_nil + expect(note.note).to include("Comment 1") + expect(note.note).to include("@#{mapped_user.username}") + expect(note.note).to include("November 18, 2009 05:14") + expect(note.note).to include("applied, thanks.") + expect(note.note).to include("Status: Fixed") + expect(note.note).to include("~~Type: Defect~~") + expect(note.note).to include("Type: Enhancement") + end + end +end diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb new file mode 100644 index 00000000000..7a224538b8b --- /dev/null +++ b/spec/lib/gitlab/google_code_import/project_creator_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::GoogleCodeImport::ProjectCreator do + let(:user) { create(:user) } + let(:repo) { + Gitlab::GoogleCodeImport::Repository.new( + "name" => 'vim', + "summary" => 'VI Improved', + "repositoryUrls" => [ "https://vim.googlecode.com/git/" ] + ) + } + let(:namespace){ create(:group, owner: user) } + + before do + namespace.add_owner(user) + end + + it 'creates project' do + allow_any_instance_of(Project).to receive(:add_import_job) + + project_creator = Gitlab::GoogleCodeImport::ProjectCreator.new(repo, namespace, user) + project = project_creator.execute + + expect(project.import_url).to eq("https://vim.googlecode.com/git/") + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end +end diff --git a/spec/lib/gitlab/key_fingerprint_spec.rb b/spec/lib/gitlab/key_fingerprint_spec.rb new file mode 100644 index 00000000000..266eab6e793 --- /dev/null +++ b/spec/lib/gitlab/key_fingerprint_spec.rb @@ -0,0 +1,12 @@ +require "spec_helper" + +describe Gitlab::KeyFingerprint do + let(:key) { "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" } + let(:fingerprint) { "3f:a2:ee:de:b5:de:53:c3:aa:2f:9c:45:24:4c:47:7b" } + + describe "#fingerprint" do + it "generates the key's fingerprint" do + expect(Gitlab::KeyFingerprint.new(key).fingerprint).to eq(fingerprint) + end + end +end diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index 2df2beca7a6..00e9076c787 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -16,19 +16,5 @@ describe Gitlab::LDAP::Config do it "raises an error if a unknow provider is used" do expect{ Gitlab::LDAP::Config.new 'unknown' }.to raise_error end - - context "if 'ldap' is the provider name" do - let(:provider) { 'ldap' } - - context "and 'ldap' is not in defined as a provider" do - before { Gitlab::LDAP::Config.stub(providers: %w{ldapmain}) } - - it "uses the first provider" do - # Fetch the provider_name attribute from 'options' so that we know - # that the 'options' Hash is not empty/nil. - expect(config.options['provider_name']).to eq('ldapmain') - end - end - end end end diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 4f93545feb6..42015c28c81 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe Gitlab::LDAP::User do - let(:gl_user) { Gitlab::LDAP::User.new(auth_hash) } + let(:ldap_user) { Gitlab::LDAP::User.new(auth_hash) } + let(:gl_user) { ldap_user.gl_user } let(:info) do { name: 'John', @@ -16,17 +17,17 @@ describe Gitlab::LDAP::User do describe :changed? do it "marks existing ldap user as changed" do existing_user = create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') - expect(gl_user.changed?).to be_truthy + expect(ldap_user.changed?).to be_truthy end it "marks existing non-ldap user if the email matches as changed" do existing_user = create(:user, email: 'john@example.com') - expect(gl_user.changed?).to be_truthy + expect(ldap_user.changed?).to be_truthy end it "dont marks existing ldap user as changed" do existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain') - expect(gl_user.changed?).to be_falsey + expect(ldap_user.changed?).to be_falsey end end @@ -34,12 +35,12 @@ describe Gitlab::LDAP::User do it "finds the user if already existing" do existing_user = create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') - expect{ gl_user.save }.to_not change{ User.count } + expect{ ldap_user.save }.to_not change{ User.count } end it "connects to existing non-ldap user if the email matches" do existing_user = create(:omniauth_user, email: 'john@example.com', provider: "twitter") - expect{ gl_user.save }.to_not change{ User.count } + expect{ ldap_user.save }.to_not change{ User.count } existing_user.reload expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid' @@ -47,7 +48,59 @@ describe Gitlab::LDAP::User do end it "creates a new user if not found" do - expect{ gl_user.save }.to change{ User.count }.by(1) + expect{ ldap_user.save }.to change{ User.count }.by(1) + end + end + + + describe 'blocking' do + context 'signup' do + context 'dont block on create' do + before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: false } + + it do + ldap_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create' do + before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: true } + + it do + ldap_user.save + expect(gl_user).to be_valid + expect(gl_user).to be_blocked + end + end + end + + context 'sign-in' do + before do + ldap_user.save + ldap_user.gl_user.activate + end + + context 'dont block on create' do + before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: false } + + it do + ldap_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create' do + before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: true } + + it do + ldap_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end end end end diff --git a/spec/lib/gitlab/oauth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb index 5eb77b492b2..5eb77b492b2 100644 --- a/spec/lib/gitlab/oauth/auth_hash_spec.rb +++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb diff --git a/spec/lib/gitlab/oauth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 44cdd1e4fab..44cdd1e4fab 100644 --- a/spec/lib/gitlab/oauth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index b3f4bb5aeda..c9fb62b61ae 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -1,73 +1,76 @@ require 'spec_helper' describe Gitlab::ReferenceExtractor do + let(:project) { create(:project) } + subject { Gitlab::ReferenceExtractor.new(project, project.creator) } + it 'extracts username references' do - subject.analyze('this contains a @user reference', nil) - expect(subject.users).to eq([{ project: nil, id: 'user' }]) + subject.analyze('this contains a @user reference') + expect(subject.references[:user]).to eq([[project, 'user']]) end it 'extracts issue references' do - subject.analyze('this one talks about issue #1234', nil) - expect(subject.issues).to eq([{ project: nil, id: '1234' }]) + subject.analyze('this one talks about issue #1234') + expect(subject.references[:issue]).to eq([[project, '1234']]) end it 'extracts JIRA issue references' do - subject.analyze('this one talks about issue JIRA-1234', nil) - expect(subject.issues).to eq([{ project: nil, id: 'JIRA-1234' }]) + subject.analyze('this one talks about issue JIRA-1234') + expect(subject.references[:issue]).to eq([[project, 'JIRA-1234']]) end it 'extracts merge request references' do - subject.analyze("and here's !43, a merge request", nil) - expect(subject.merge_requests).to eq([{ project: nil, id: '43' }]) + subject.analyze("and here's !43, a merge request") + expect(subject.references[:merge_request]).to eq([[project, '43']]) end it 'extracts snippet ids' do - subject.analyze('snippets like $12 get extracted as well', nil) - expect(subject.snippets).to eq([{ project: nil, id: '12' }]) + subject.analyze('snippets like $12 get extracted as well') + expect(subject.references[:snippet]).to eq([[project, '12']]) end it 'extracts commit shas' do - subject.analyze('commit shas 98cf0ae3 are pulled out as Strings', nil) - expect(subject.commits).to eq([{ project: nil, id: '98cf0ae3' }]) + subject.analyze('commit shas 98cf0ae3 are pulled out as Strings') + expect(subject.references[:commit]).to eq([[project, '98cf0ae3']]) end it 'extracts commit ranges' do - subject.analyze('here you go, a commit range: 98cf0ae3...98cf0ae4', nil) - expect(subject.commit_ranges).to eq([{ project: nil, id: '98cf0ae3...98cf0ae4' }]) + subject.analyze('here you go, a commit range: 98cf0ae3...98cf0ae4') + expect(subject.references[:commit_range]).to eq([[project, '98cf0ae3...98cf0ae4']]) end it 'extracts multiple references and preserves their order' do - subject.analyze('@me and @you both care about this', nil) - expect(subject.users).to eq([ - { project: nil, id: 'me' }, - { project: nil, id: 'you' } + subject.analyze('@me and @you both care about this') + expect(subject.references[:user]).to eq([ + [project, 'me'], + [project, 'you'] ]) end it 'leaves the original note unmodified' do text = 'issue #123 is just the worst, @user' - subject.analyze(text, nil) + subject.analyze(text) expect(text).to eq('issue #123 is just the worst, @user') end it 'extracts no references for <pre>..</pre> blocks' do - subject.analyze("<pre>def puts '#1 issue'\nend\n</pre>```", nil) + subject.analyze("<pre>def puts '#1 issue'\nend\n</pre>```") expect(subject.issues).to be_blank end it 'extracts no references for <code>..</code> blocks' do - subject.analyze("<code>def puts '!1 request'\nend\n</code>```", nil) + subject.analyze("<code>def puts '!1 request'\nend\n</code>```") expect(subject.merge_requests).to be_blank end it 'extracts no references for code blocks with language' do - subject.analyze("this code:\n```ruby\ndef puts '#1 issue'\nend\n```", nil) + subject.analyze("this code:\n```ruby\ndef puts '#1 issue'\nend\n```") expect(subject.issues).to be_blank end it 'extracts issue references for invalid code blocks' do - subject.analyze('test: ```this one talks about issue #1234```', nil) - expect(subject.issues).to eq([{ project: nil, id: '1234' }]) + subject.analyze('test: ```this one talks about issue #1234```') + expect(subject.references[:issue]).to eq([[project, '1234']]) end it 'handles all possible kinds of references' do @@ -75,83 +78,79 @@ describe Gitlab::ReferenceExtractor do expect(subject).to respond_to(*accessors) end - context 'with a project' do - let(:project) { create(:project) } - - 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') + it 'accesses valid user objects' do + @u_foo = create(:user, username: 'foo') + @u_bar = create(:user, username: 'bar') + @u_offteam = create(:user, username: 'offteam') - project.team << [@u_foo, :reporter] - project.team << [@u_bar, :guest] + project.team << [@u_foo, :reporter] + project.team << [@u_bar, :guest] - subject.analyze('@foo, @baduser, @bar, and @offteam', project) - expect(subject.users_for(project)).to eq([@u_foo, @u_bar]) - end + subject.analyze('@foo, @baduser, @bar, and @offteam') + expect(subject.users).to eq([@u_foo, @u_bar, @u_offteam]) + end - it 'accesses valid issue objects' do - @i0 = create(:issue, project: project) - @i1 = create(:issue, project: project) + it 'accesses valid issue objects' do + @i0 = create(:issue, project: project) + @i1 = create(:issue, project: project) - subject.analyze("##{@i0.iid}, ##{@i1.iid}, and #999.", project) - expect(subject.issues_for(project)).to eq([@i0, @i1]) - end + subject.analyze("##{@i0.iid}, ##{@i1.iid}, and #999.") + expect(subject.issues).to eq([@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') + 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}.", project) - expect(subject.merge_requests_for(project)).to eq([@m1, @m0]) - end + subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.") + expect(subject.merge_requests).to eq([@m1, @m0]) + end - it 'accesses valid snippets' do - @s0 = create(:project_snippet, project: project) - @s1 = create(:project_snippet, project: project) - @s2 = create(:project_snippet) + 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}", project) - expect(subject.snippets_for(project)).to eq([@s0, @s1]) - end + subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}") + expect(subject.snippets).to eq([@s0, @s1]) + end - it 'accesses valid commits' do - commit = project.repository.commit('master') + it 'accesses valid commits' do + commit = project.repository.commit('master') - subject.analyze("this references commits #{commit.sha[0..6]} and 012345", - project) - extracted = subject.commits_for(project) - expect(extracted.size).to eq(1) - expect(extracted[0].sha).to eq(commit.sha) - expect(extracted[0].message).to eq(commit.message) - end + subject.analyze("this references commits #{commit.sha[0..6]} and 012345") + extracted = subject.commits + expect(extracted.size).to eq(1) + expect(extracted[0].sha).to eq(commit.sha) + expect(extracted[0].message).to eq(commit.message) + end - it 'accesses valid commit ranges' do - commit = project.repository.commit('master') - earlier_commit = project.repository.commit('master~2') + it 'accesses valid commit ranges' do + commit = project.repository.commit('master') + earlier_commit = project.repository.commit('master~2') - subject.analyze("this references commits #{earlier_commit.sha[0..6]}...#{commit.sha[0..6]}", - project) - extracted = subject.commit_ranges_for(project) - expect(extracted.size).to eq(1) - expect(extracted[0][0].sha).to eq(earlier_commit.sha) - expect(extracted[0][0].message).to eq(earlier_commit.message) - expect(extracted[0][1].sha).to eq(commit.sha) - expect(extracted[0][1].message).to eq(commit.message) - end + subject.analyze("this references commits #{earlier_commit.sha[0..6]}...#{commit.sha[0..6]}") + extracted = subject.commit_ranges + expect(extracted.size).to eq(1) + expect(extracted[0][0].sha).to eq(earlier_commit.sha) + expect(extracted[0][0].message).to eq(earlier_commit.message) + expect(extracted[0][1].sha).to eq(commit.sha) + expect(extracted[0][1].message).to eq(commit.message) end context 'with a project with an underscore' do - let(:project) { create(:project, path: 'test_project') } - let(:issue) { create(:issue, project: project) } + let(:other_project) { create(:project, path: 'test_project') } + let(:issue) { create(:issue, project: other_project) } + + before do + other_project.team << [project.creator, :developer] + end it 'handles project issue references' do - subject.analyze("this refers issue #{project.path_with_namespace}##{issue.iid}", - project) - extracted = subject.issues_for(project) + subject.analyze("this refers issue #{other_project.path_with_namespace}##{issue.iid}") + extracted = subject.issues expect(extracted.size).to eq(1) expect(extracted).to eq([issue]) end - end end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 1db9f15b790..727884c41c5 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -1,14 +1,14 @@ require 'spec_helper' describe Gitlab::Regex do - describe 'path regex' do - it { expect('gitlab-ce').to match(Gitlab::Regex.path_regex) } - it { expect('gitlab_git').to match(Gitlab::Regex.path_regex) } - it { expect('_underscore.js').to match(Gitlab::Regex.path_regex) } - it { expect('100px.com').to match(Gitlab::Regex.path_regex) } - it { expect('?gitlab').not_to match(Gitlab::Regex.path_regex) } - it { expect('git lab').not_to match(Gitlab::Regex.path_regex) } - it { expect('gitlab.git').not_to match(Gitlab::Regex.path_regex) } + describe 'project path regex' do + it { expect('gitlab-ce').to match(Gitlab::Regex.project_path_regex) } + it { expect('gitlab_git').to match(Gitlab::Regex.project_path_regex) } + it { expect('_underscore.js').to match(Gitlab::Regex.project_path_regex) } + it { expect('100px.com').to match(Gitlab::Regex.project_path_regex) } + it { expect('?gitlab').not_to match(Gitlab::Regex.project_path_regex) } + it { expect('git lab').not_to match(Gitlab::Regex.project_path_regex) } + it { expect('gitlab.git').not_to match(Gitlab::Regex.project_path_regex) } end describe 'project name regex' do diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index ba42f9e5c70..b297fbd5119 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -7,9 +7,12 @@ describe Notify do let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name } let(:gitlab_sender) { Gitlab.config.gitlab.email_from } + let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to } let(:recipient) { create(:user, email: 'recipient@example.com') } let(:project) { create(:project) } + around(:each) { ActionMailer::Base.deliveries.clear } + before(:each) do email = recipient.emails.create(email: "notifications@example.com") recipient.update_attribute(:notification_email, email.email) @@ -27,6 +30,11 @@ describe Notify do expect(sender.display_name).to eq(gitlab_sender_display_name) expect(sender.address).to eq(gitlab_sender) end + + it 'has a Reply-To address' do + reply_to = subject.header[:reply_to].addresses + expect(reply_to).to eq([gitlab_sender_reply_to]) + end end shared_examples 'an email starting a new thread' do |message_id_prefix| @@ -183,13 +191,6 @@ describe Notify do context 'for issues' do let(:issue) { create(:issue, author: current_user, assignee: assignee, project: project) } let(:issue_with_description) { create(:issue, author: current_user, assignee: assignee, project: project, description: Faker::Lorem.sentence) } - let(:issue_with_image) do - create(:issue, - author: current_user, - assignee: assignee, - project: project, - description: "") - end describe 'that are new' do subject { Notify.new_issue_email(issue.assignee_id, issue.id) } @@ -214,22 +215,6 @@ describe Notify do end end - describe 'that contain images' do - let(:png) { File.read("#{Rails.root}/spec/fixtures/dk.png") } - let(:png_encoded) { Base64::encode64(png) } - - before :each do - file_path = File.join(Rails.root, 'public', 'uploads', issue_with_image.project.path_with_namespace, '12345/test.jpg') - allow(File).to receive(:file?).with(file_path).and_return(true) - allow(File).to receive(:read).with(file_path).and_return(png) - end - - subject { Notify.new_issue_email(issue_with_image.assignee_id, issue_with_image.id) } - it 'replaces attached images with inline images' do - is_expected.to have_body_text URI.encode(png_encoded) - end - end - describe 'that have been reassigned' do subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user) } @@ -294,14 +279,6 @@ describe Notify do let(:merge_author) { create(:user) } let(:merge_request) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project) } let(:merge_request_with_description) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project, description: Faker::Lorem.sentence) } - let(:merge_request_with_image) do - create(:merge_request, - author: current_user, - assignee: assignee, - source_project: project, - target_project: project, - description: "") - end describe 'that are new' do subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) } @@ -338,22 +315,6 @@ describe Notify do end end - describe 'that are new and contain contain images in the description' do - let(:png) {File.read("#{Rails.root}/spec/fixtures/dk.png")} - let(:png_encoded) { Base64::encode64(png) } - - before :each do - file_path = File.join(Rails.root, 'public', 'uploads', merge_request_with_image.project.path_with_namespace, '/12345/test.jpg') - allow(File).to receive(:file?).with(file_path).and_return(true) - allow(File).to receive(:read).with(file_path).and_return(png) - end - - subject { Notify.new_merge_request_email(merge_request_with_image.assignee_id, merge_request_with_image.id) } - it 'replaces attached images with inline images' do - is_expected.to have_body_text URI.encode(png_encoded) - end - end - describe 'that are reassigned' do subject { Notify.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) } @@ -462,12 +423,9 @@ describe Notify do describe 'project access changed' do let(:project) { create(:project) } let(:user) { create(:user) } - let(:project_member) do - create(:project_member, - project: project, - user: user) - end - + let(:project_member) { create(:project_member, + project: project, + user: user) } subject { Notify.project_access_granted_email(project_member.id) } it_behaves_like 'an email sent from GitLab' @@ -507,32 +465,6 @@ describe Notify do end end - describe 'on a commit that contains an image' do - let(:commit) { project.repository.commit } - let(:note_with_image) do - create(:note, - project: project, - author: note_author, - note: "") - end - - let(:png) {File.read("#{Rails.root}/spec/fixtures/dk.png")} - let(:png_encoded) { Base64::encode64(png) } - - before :each do - file_path = File.join(Rails.root, 'public', 'uploads', note_with_image.project.path_with_namespace, '12345/test.jpg') - allow(File).to receive(:file?).with(file_path).and_return(true) - allow(File).to receive(:read).with(file_path).and_return(png) - allow(Note).to receive(:find).with(note_with_image.id).and_return(note_with_image) - allow(note_with_image).to receive(:noteable).and_return(commit) - end - - subject { Notify.note_commit_email(recipient.id, note_with_image.id) } - it 'replaces attached images with inline images' do - is_expected.to have_body_text URI.encode(png_encoded) - end - end - describe 'on a commit' do let(:commit) { project.repository.commit } @@ -793,6 +725,11 @@ describe Notify do sender = subject.header[:from].addrs[0] expect(sender.address).to eq(user.email) end + + it "is set to reply to the committer email" do + sender = subject.header[:reply_to].addrs[0] + expect(sender.address).to eq(user.email) + end end context "when the committer email domain is not completely within the GitLab domain" do @@ -806,6 +743,11 @@ describe Notify do sender = subject.header[:from].addrs[0] expect(sender.address).to eq(gitlab_sender) end + + it "is set to reply to the default email" do + sender = subject.header[:reply_to].addrs[0] + expect(sender.address).to eq(gitlab_sender_reply_to) + end end context "when the committer email domain is outside the GitLab domain" do @@ -819,6 +761,11 @@ describe Notify do sender = subject.header[:from].addrs[0] expect(sender.address).to eq(gitlab_sender) end + + it "is set to reply to the default email" do + sender = subject.header[:reply_to].addrs[0] + expect(sender.address).to eq(gitlab_sender_reply_to) + end end end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 8b3d88640da..11cc7762ce4 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -14,7 +14,7 @@ describe Commit do message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.' allow(commit).to receive(:safe_message).and_return(message) - expect(commit.title).to eq("#{message[0..79]}…") + expect(commit.title).to eq("#{message[0..79]}…") end it "truncates a message with a newline before 80 characters at the newline" do diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb index f351aab9238..7032b777144 100644 --- a/spec/models/deploy_keys_project_spec.rb +++ b/spec/models/deploy_keys_project_spec.rb @@ -28,17 +28,32 @@ describe DeployKeysProject do let(:deploy_key) { subject.deploy_key } context "when the deploy key is only used by this project" do - it "destroys the deploy key" do - subject.destroy + context "when the deploy key is public" do + before do + deploy_key.update_attribute(:public, true) + end - expect { - deploy_key.reload - }.to raise_error(ActiveRecord::RecordNotFound) + it "doesn't destroy the deploy key" do + subject.destroy + + expect { + deploy_key.reload + }.not_to raise_error(ActiveRecord::RecordNotFound) + end + end + + context "when the deploy key is private" do + it "destroys the deploy key" do + subject.destroy + + expect { + deploy_key.reload + }.to raise_error(ActiveRecord::RecordNotFound) + end end end context "when the deploy key is used by more than one project" do - let!(:other_project) { create(:project) } before do diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index a212b95a7d6..2fb651bef1b 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -58,12 +58,17 @@ describe Key do expect(build(:key)).to be_valid end - it "rejects the unfingerprintable key (contains space in middle)" do - expect(build(:key_with_a_space_in_the_middle)).not_to be_valid + it 'rejects an unfingerprintable key that contains a space' do + key = build(:key) + + # Not always the middle, but close enough + key.key = key.key[0..100] + ' ' + key.key[100..-1] + + expect(key).not_to be_valid end - it "rejects the unfingerprintable key (not a key)" do - expect(build(:invalid_key)).not_to be_valid + it 'rejects the unfingerprintable key (not a key)' do + expect(build(:key, key: 'ssh-rsa an-invalid-key==')).not_to be_valid end end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb new file mode 100644 index 00000000000..56d030a03b3 --- /dev/null +++ b/spec/models/member_spec.rb @@ -0,0 +1,148 @@ +require 'spec_helper' + +describe Member do + describe "Associations" do + it { is_expected.to belong_to(:user) } + end + + describe "Validation" do + subject { Member.new(access_level: Member::GUEST) } + + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:source) } + it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) } + + context "when an invite email is provided" do + let(:member) { build(:project_member, invite_email: "user@example.com", user: nil) } + + it "doesn't require a user" do + expect(member).to be_valid + end + + it "requires a valid invite email" do + member.invite_email = "nope" + + expect(member).not_to be_valid + end + + it "requires a unique invite email scoped to this source" do + create(:project_member, source: member.source, invite_email: member.invite_email) + + expect(member).not_to be_valid + end + + it "is valid otherwise" do + expect(member).to be_valid + end + end + + context "when an invite email is not provided" do + let(:member) { build(:project_member) } + + it "requires a user" do + member.user = nil + + expect(member).not_to be_valid + end + + it "is valid otherwise" do + expect(member).to be_valid + end + end + end + + describe "Delegate methods" do + it { is_expected.to respond_to(:user_name) } + it { is_expected.to respond_to(:user_email) } + end + + describe ".add_user" do + let!(:user) { create(:user) } + let(:project) { create(:project) } + + context "when called with a user id" do + it "adds the user as a member" do + Member.add_user(project.project_members, user.id, ProjectMember::MASTER) + + expect(project.users).to include(user) + end + end + + context "when called with a user object" do + it "adds the user as a member" do + Member.add_user(project.project_members, user, ProjectMember::MASTER) + + expect(project.users).to include(user) + end + end + + context "when called with a known user email" do + it "adds the user as a member" do + Member.add_user(project.project_members, user.email, ProjectMember::MASTER) + + expect(project.users).to include(user) + end + end + + context "when called with an unknown user email" do + it "adds a member invite" do + Member.add_user(project.project_members, "user@example.com", ProjectMember::MASTER) + + expect(project.project_members.invite.pluck(:invite_email)).to include("user@example.com") + end + end + end + + describe "#accept_invite!" do + let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) } + let(:user) { create(:user) } + + it "resets the invite token" do + member.accept_invite!(user) + + expect(member.invite_token).to be_nil + end + + it "sets the invite accepted timestamp" do + member.accept_invite!(user) + + expect(member.invite_accepted_at).not_to be_nil + end + + it "sets the user" do + member.accept_invite!(user) + + expect(member.user).to eq(user) + end + + it "calls #after_accept_invite" do + expect(member).to receive(:after_accept_invite) + + member.accept_invite!(user) + end + end + + describe "#decline_invite!" do + let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) } + + it "destroys the member" do + member.decline_invite! + + expect(member).to be_destroyed + end + + it "calls #after_decline_invite" do + expect(member).to receive(:after_decline_invite) + + member.decline_invite! + end + end + + describe "#generate_invite_token" do + let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) } + + it "sets the invite token" do + expect { member.generate_invite_token }.to change { member.invite_token} + end + end +end diff --git a/spec/models/members_spec.rb b/spec/models/members_spec.rb deleted file mode 100644 index dfd3f7feb6b..00000000000 --- a/spec/models/members_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe Member do - describe "Associations" do - it { is_expected.to belong_to(:user) } - end - - describe "Validation" do - subject { Member.new(access_level: Member::GUEST) } - - it { is_expected.to validate_presence_of(:user) } - it { is_expected.to validate_presence_of(:source) } - it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) } - end - - describe "Delegate methods" do - it { is_expected.to respond_to(:user_name) } - it { is_expected.to respond_to(:user_email) } - end -end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index ed6845c82cc..e87432fdf62 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -33,8 +33,6 @@ describe Namespace do it { is_expected.to respond_to(:to_param) } end - it { expect(Namespace.global_id).to eq('GLN') } - describe :to_param do it { expect(namespace.to_param).to eq(namespace.path) } end @@ -85,4 +83,14 @@ describe Namespace do it { expect(Namespace.find_by_path_or_name('WOW')).to eq(@namespace) } it { expect(Namespace.find_by_path_or_name('unknown')).to eq(nil) } end + + describe ".clean_path" do + + let!(:user) { create(:user, username: "johngitlab-etc") } + let!(:namespace) { create(:namespace, path: "JohnGitLab-etc1") } + + it "cleans the path and makes sure it's available" do + expect(Namespace.clean_path("-john+gitlab-ETC%.git@gmail.com")).to eq("johngitlab-ETC2") + end + end end diff --git a/spec/models/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb index 13c8d54a2af..13c8d54a2af 100644 --- a/spec/models/asana_service_spec.rb +++ b/spec/models/project_services/asana_service_spec.rb diff --git a/spec/models/project_services/buildbox_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb index 9f29fbe12b0..e987241f3ca 100644 --- a/spec/models/project_services/buildbox_service_spec.rb +++ b/spec/models/project_services/buildkite_service_spec.rb @@ -19,7 +19,7 @@ require 'spec_helper' -describe BuildboxService do +describe BuildkiteService do describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } @@ -32,7 +32,7 @@ describe BuildboxService do default_branch: 'default-brancho' ) - @service = BuildboxService.new + @service = BuildkiteService.new @service.stub( project: @project, service_hook: true, diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb index 610f33c5823..6a557d839ca 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -46,4 +46,25 @@ describe GitlabCiService do it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/2ab7834c")} end end + + describe "Fork registration" do + before do + @old_project = create(:empty_project) + @project = create(:empty_project) + @user = create(:user) + + @service = GitlabCiService.new + @service.stub( + service_hook: true, + project_url: 'http://ci.gitlab.org/projects/2', + token: 'verySecret', + project: @old_project + ) + end + + it "performs http reuquest to ci" do + stub_request(:post, "http://ci.gitlab.org/api/v1/forks") + @service.fork_registration(@project, @user.private_token) + end + end end diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb index 959044dc727..f94bef5c365 100644 --- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb +++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb @@ -31,6 +31,7 @@ describe GitlabIssueTrackerService do context 'with absolute urls' do before do + GitlabIssueTrackerService.default_url_options[:script_name] = "/gitlab/root" @service = project.create_gitlab_issue_tracker_service(active: true) end @@ -39,15 +40,15 @@ describe GitlabIssueTrackerService do end it 'should give the correct path' do - expect(@service.project_url).to eq("/#{project.path_with_namespace}/issues") - expect(@service.new_issue_url).to eq("/#{project.path_with_namespace}/issues/new") - expect(@service.issue_url(432)).to eq("/#{project.path_with_namespace}/issues/432") + expect(@service.project_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues") + expect(@service.new_issue_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/new") + expect(@service.issue_url(432)).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/432") end end - context 'with enabled relative urls' do + context 'with relative urls' do before do - Settings.gitlab.stub(:relative_url_root).and_return("/gitlab/root") + GitlabIssueTrackerService.default_url_options[:script_name] = "/gitlab/root" @service = project.create_gitlab_issue_tracker_service(active: true) end @@ -56,9 +57,9 @@ describe GitlabIssueTrackerService do end it 'should give the correct path' do - expect(@service.project_url).to eq("/gitlab/root/#{project.path_with_namespace}/issues") - expect(@service.new_issue_url).to eq("/gitlab/root/#{project.path_with_namespace}/issues/new") - expect(@service.issue_url(432)).to eq("/gitlab/root/#{project.path_with_namespace}/issues/432") + expect(@service.project_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues") + expect(@service.new_issue_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues/new") + expect(@service.issue_path(432)).to eq("/gitlab/root/#{project.path_with_namespace}/issues/432") end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 10e90cae143..24384e8bf22 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -307,16 +307,6 @@ describe User do end end - describe ".clean_username" do - - let!(:user) { create(:user, username: "johngitlab-etc") } - let!(:namespace) { create(:namespace, path: "JohnGitLab-etc1") } - - it "cleans a username and makes sure it's available" do - expect(User.clean_username("-john+gitlab-ETC%.git@gmail.com")).to eq("johngitlab-ETC2") - end - end - describe 'all_ssh_keys' do it { is_expected.to have_many(:keys).dependent(:destroy) } diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index f28dfea3ccf..cc387378d3a 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -57,7 +57,14 @@ describe API::API, api: true do expect(json_response.first['name']).to eq(project.name) expect(json_response.first['owner']['username']).to eq(user.username) end - + + it 'should include the project labels as the tag_list' do + get api('/projects', user) + response.status.should == 200 + json_response.should be_an Array + json_response.first.keys.should include('tag_list') + end + context 'and using search' do it 'should return searched project' do get api('/projects', user), { search: project.name } @@ -247,12 +254,12 @@ describe API::API, api: true do expect(json_response['message']['name']).to eq([ 'can\'t be blank', 'is too short (minimum is 0 characters)', - Gitlab::Regex.project_regex_message + Gitlab::Regex.project_name_regex_message ]) expect(json_response['message']['path']).to eq([ 'can\'t be blank', 'is too short (minimum is 0 characters)', - Gitlab::Regex.send(:default_regex_message) + Gitlab::Regex.send(:project_path_regex_message) ]) end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 729970153d1..09a79553f72 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -11,8 +11,6 @@ describe API::API, api: true do let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } - before { project.team << [user, :reporter] } - describe "GET /projects/:id/repository/tags" do it "should return an array of project tags" do get api("/projects/#{project.id}/repository/tags", user) diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 081400cdedd..e6d5545f812 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -140,7 +140,7 @@ describe API::API, api: true do expect(json_response['message']['projects_limit']). to eq(['must be greater than or equal to 0']) expect(json_response['message']['username']). - to eq([Gitlab::Regex.send(:default_regex_message)]) + to eq([Gitlab::Regex.send(:namespace_regex_message)]) end it "shouldn't available for non admin users" do @@ -266,7 +266,7 @@ describe API::API, api: true do expect(json_response['message']['projects_limit']). to eq(['must be greater than or equal to 0']) expect(json_response['message']['username']). - to eq([Gitlab::Regex.send(:default_regex_message)]) + to eq([Gitlab::Regex.send(:namespace_regex_message)]) end context "with existing user" do diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index d9bd91f5990..042352311da 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -168,12 +168,11 @@ 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 -# edit_project_deploy_key GET /:project_id/deploy_keys/:id/edit(.:format) deploy_keys#edit # 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 Projects::DeployKeysController, 'routing' do it_behaves_like 'RESTful project resources' do + let(:actions) { [:index, :show, :new, :create] } let(:controller) { 'deploy_keys' } end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index d4915b51952..e219a57c29e 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -64,50 +64,35 @@ describe SnippetsController, "routing" do end end -# help GET /help(.:format) help#index -# help_permissions GET /help/permissions(.:format) help#permissions -# help_workflow GET /help/workflow(.:format) help#workflow -# help_api GET /help/api(.:format) help#api -# help_web_hooks GET /help/web_hooks(.:format) help#web_hooks -# help_system_hooks GET /help/system_hooks(.:format) help#system_hooks -# help_markdown GET /help/markdown(.:format) help#markdown -# help_ssh GET /help/ssh(.:format) help#ssh -# help_raketasks GET /help/raketasks(.:format) help#raketasks +# help GET /help(.:format) help#index +# help_page GET /help/:category/:file(.:format) help#show {:category=>/.*/, :file=>/[^\/\.]+/} +# help_shortcuts GET /help/shortcuts(.:format) help#shortcuts +# help_ui GET /help/ui(.:format) help#ui describe HelpController, "routing" do it "to #index" do expect(get("/help")).to route_to('help#index') end - it "to #permissions" do - expect(get("/help/permissions/permissions")).to route_to('help#show', category: "permissions", file: "permissions") - end - - it "to #workflow" do - expect(get("/help/workflow/README")).to route_to('help#show', category: "workflow", file: "README") - end - - it "to #api" do - expect(get("/help/api/README")).to route_to('help#show', category: "api", file: "README") - end - - it "to #web_hooks" do - expect(get("/help/web_hooks/web_hooks")).to route_to('help#show', category: "web_hooks", file: "web_hooks") - end - - it "to #system_hooks" do - expect(get("/help/system_hooks/system_hooks")).to route_to('help#show', category: "system_hooks", file: "system_hooks") - end + it 'to #show' do + path = '/help/markdown/markdown.md' + expect(get(path)).to route_to('help#show', + category: 'markdown', + file: 'markdown', + format: 'md') - it "to #markdown" do - expect(get("/help/markdown/markdown")).to route_to('help#show',category: "markdown", file: "markdown") + path = '/help/workflow/protected_branches/protected_branches1.png' + expect(get(path)).to route_to('help#show', + category: 'workflow/protected_branches', + file: 'protected_branches1', + format: 'png') end - it "to #ssh" do - expect(get("/help/ssh/README")).to route_to('help#show', category: "ssh", file: "README") + it 'to #shortcuts' do + expect(get('/help/shortcuts')).to route_to('help#shortcuts') end - it "to #raketasks" do - expect(get("/help/raketasks/README")).to route_to('help#show', category: "raketasks", file: "README") + it 'to #ui' do + expect(get('/help/ui')).to route_to('help#ui') end end diff --git a/spec/services/archive_repository_service_spec.rb b/spec/services/archive_repository_service_spec.rb new file mode 100644 index 00000000000..f168a913976 --- /dev/null +++ b/spec/services/archive_repository_service_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +describe ArchiveRepositoryService do + let(:project) { create(:project) } + subject { ArchiveRepositoryService.new(project, "master", "zip") } + + describe "#execute" do + it "cleans old archives" do + expect(project.repository).to receive(:clean_old_archives) + + subject.execute(timeout: 0.0) + end + + context "when the repository doesn't have an archive file path" do + before do + allow(project.repository).to receive(:archive_file_path).and_return(nil) + end + + it "raises an error" do + expect { + subject.execute(timeout: 0.0) + }.to raise_error + end + end + + context "when the repository has an archive file path" do + let(:file_path) { "/archive.zip" } + let(:pid_file_path) { "/archive.zip.pid" } + + before do + allow(project.repository).to receive(:archive_file_path).and_return(file_path) + allow(project.repository).to receive(:archive_pid_file_path).and_return(pid_file_path) + end + + context "when the archive file already exists" do + before do + allow(File).to receive(:exist?).with(file_path).and_return(true) + end + + it "returns the file path" do + expect(subject.execute(timeout: 0.0)).to eq(file_path) + end + end + + context "when the archive file doesn't exist yet" do + before do + allow(File).to receive(:exist?).with(file_path).and_return(false) + allow(File).to receive(:exist?).with(pid_file_path).and_return(true) + end + + context "when the archive pid file doesn't exist yet" do + before do + allow(File).to receive(:exist?).with(pid_file_path).and_return(false) + end + + it "queues the RepositoryArchiveWorker" do + expect(RepositoryArchiveWorker).to receive(:perform_async) + + subject.execute(timeout: 0.0) + end + end + + context "when the archive pid file already exists" do + it "doesn't queue the RepositoryArchiveWorker" do + expect(RepositoryArchiveWorker).not_to receive(:perform_async) + + subject.execute(timeout: 0.0) + end + end + + context "when the archive file exists after a little while" do + before do + Thread.new do + sleep 0.1 + allow(File).to receive(:exist?).with(file_path).and_return(true) + end + end + + it "returns the file path" do + expect(subject.execute(timeout: 0.2)).to eq(file_path) + end + end + + context "when the archive file doesn't exist after the timeout" do + it "returns nil" do + expect(subject.execute(timeout: 0.0)).to eq(nil) + end + end + end + end + end +end + diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index e55a2e3f8a0..c9025bdf133 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -40,6 +40,17 @@ describe Projects::ForkService do expect(@to_project.errors[:base]).not_to include("Fork transaction failed.") end end + + context 'GitLab CI is enabled' do + it "calls fork registrator for CI" do + @from_project.build_missing_services + @from_project.gitlab_ci_service.update_attributes(active: true) + + expect(ForkRegistrationWorker).to receive(:perform_async) + + fork_project(@from_project, @to_user) + end + end end describe :fork_to_namespace do @@ -89,7 +100,8 @@ describe Projects::ForkService do def fork_project(from_project, user, fork_success = true, params = {}) context = Projects::ForkService.new(from_project, user, params) - shell = double('gitlab_shell').stub(fork_repository: fork_success) + shell = double('gitlab_shell') + shell.stub(fork_repository: fork_success) context.stub(gitlab_shell: shell) context.execute end diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb index fc34b456482..e5c47015a03 100644 --- a/spec/services/projects/upload_service_spec.rb +++ b/spec/services/projects/upload_service_spec.rb @@ -67,6 +67,16 @@ describe Projects::UploadService do it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file['url']).to match('doc_sample.txt') } end + + context 'for too large a file' do + before do + txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') + allow(txt).to receive(:size) { 1000.megabytes.to_i } + @link_to_file = upload_file(@project.repository, txt) + end + + it { expect(@link_to_file).to eq(nil) } + end end def upload_file(repository, file) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index eaec2198dc8..53ccaa4fd67 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -44,3 +44,5 @@ RSpec.configure do |config| TestEnv.init end end + +ActiveRecord::Migration.maintain_test_schema! diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb index c7cf109a7bb..691f84f39d4 100644 --- a/spec/support/select2_helper.rb +++ b/spec/support/select2_helper.rb @@ -17,9 +17,9 @@ module Select2Helper selector = options[:from] if options[:multiple] - execute_script("$('#{selector}').select2('val', ['#{value}']);") + execute_script("$('#{selector}').select2('val', ['#{value}'], true);") else - execute_script("$('#{selector}').select2('val', '#{value}');") + execute_script("$('#{selector}').select2('val', '#{value}', true);") end end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index f869488d8d8..44d70e741b2 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -85,7 +85,7 @@ module TestEnv end # We must copy bare repositories because we will push to them. - system(*%W(git clone -q --bare #{factory_repo_path} #{factory_repo_path_bare})) + system(git_env, *%W(git clone -q --bare #{factory_repo_path} #{factory_repo_path_bare})) end def copy_repo(project) @@ -113,4 +113,10 @@ module TestEnv def factory_repo_name 'gitlab-test' end + + # Prevent developer git configurations from being persisted to test + # repositories + def git_env + {'GIT_TEMPLATE_DIR' => ''} + end end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 8a411b7720a..a59f74c2121 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -87,7 +87,7 @@ describe 'gitlab:app namespace rake task' do expect(tar_contents).to match('db/') expect(tar_contents).to match('uploads/') expect(tar_contents).to match('repositories/') - expect(tar_contents).not_to match(/^.{4,9}[rwx]/) + expect(tar_contents).not_to match(/^.{4,9}[rwx].* (db|uploads|repositories)\/$/) end it 'should delete temp directories' do @@ -98,4 +98,55 @@ describe 'gitlab:app namespace rake task' do expect(temp_dirs).to be_empty end end # backup_create task + + describe "Skipping items" do + def tars_glob + Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar')) + end + + before :all do + @origin_cd = Dir.pwd + + Rake::Task["gitlab:backup:db:create"].reenable + Rake::Task["gitlab:backup:repo:create"].reenable + Rake::Task["gitlab:backup:uploads:create"].reenable + + # Record the existing backup tars so we don't touch them + existing_tars = tars_glob + + # Redirect STDOUT and run the rake task + orig_stdout = $stdout + $stdout = StringIO.new + ENV["SKIP"] = "repositories" + run_rake_task('gitlab:backup:create') + $stdout = orig_stdout + + @backup_tar = (tars_glob - existing_tars).first + end + + after :all do + FileUtils.rm(@backup_tar) + Dir.chdir @origin_cd + end + + it "does not contain skipped item" do + tar_contents, exit_status = Gitlab::Popen.popen( + %W{tar -tvf #{@backup_tar} db uploads repositories} + ) + + expect(tar_contents).to match('db/') + expect(tar_contents).to match('uploads/') + expect(tar_contents).not_to match('repositories/') + end + + it 'does not invoke repositories restore' do + Rake::Task["gitlab:shell:setup"].stub invoke: true + allow($stdout).to receive :write + + expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke + expect(Rake::Task["gitlab:backup:repo:restore"]).not_to receive :invoke + expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke + expect { run_rake_task('gitlab:backup:restore') }.to_not raise_error + end + end end # gitlab:app namespace diff --git a/spec/workers/fork_registration_worker_spec.rb b/spec/workers/fork_registration_worker_spec.rb new file mode 100644 index 00000000000..cc6f574b29c --- /dev/null +++ b/spec/workers/fork_registration_worker_spec.rb @@ -0,0 +1,10 @@ + +require 'spec_helper' + +describe ForkRegistrationWorker do + context "as a resque worker" do + it "reponds to #perform" do + expect(ForkRegistrationWorker.new).to respond_to(:perform) + end + end +end diff --git a/spec/workers/repository_archive_worker_spec.rb b/spec/workers/repository_archive_worker_spec.rb new file mode 100644 index 00000000000..c2362058cfd --- /dev/null +++ b/spec/workers/repository_archive_worker_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe RepositoryArchiveWorker do + let(:project) { create(:project) } + subject { RepositoryArchiveWorker.new } + + before do + allow(Project).to receive(:find).and_return(project) + end + + describe "#perform" do + it "cleans old archives" do + expect(project.repository).to receive(:clean_old_archives) + + subject.perform(project.id, "master", "zip") + end + + context "when the repository doesn't have an archive file path" do + before do + allow(project.repository).to receive(:archive_file_path).and_return(nil) + end + + it "doesn't archive the repo" do + expect(project.repository).not_to receive(:archive_repo) + + subject.perform(project.id, "master", "zip") + end + end + + context "when the repository has an archive file path" do + let(:file_path) { "/archive.zip" } + let(:pid_file_path) { "/archive.zip.pid" } + + before do + allow(project.repository).to receive(:archive_file_path).and_return(file_path) + allow(project.repository).to receive(:archive_pid_file_path).and_return(pid_file_path) + end + + context "when the archive file already exists" do + before do + allow(File).to receive(:exist?).with(file_path).and_return(true) + end + + it "doesn't archive the repo" do + expect(project.repository).not_to receive(:archive_repo) + + subject.perform(project.id, "master", "zip") + end + end + + context "when the archive file doesn't exist yet" do + before do + allow(File).to receive(:exist?).with(file_path).and_return(false) + allow(File).to receive(:exist?).with(pid_file_path).and_return(true) + end + + context "when the archive pid file doesn't exist yet" do + before do + allow(File).to receive(:exist?).with(pid_file_path).and_return(false) + end + + it "archives the repo" do + expect(project.repository).to receive(:archive_repo) + + subject.perform(project.id, "master", "zip") + end + end + + context "when the archive pid file already exists" do + it "doesn't archive the repo" do + expect(project.repository).not_to receive(:archive_repo) + + subject.perform(project.id, "master", "zip") + end + end + end + end + end +end + diff --git a/vendor/assets/javascripts/chart-lib.min.js b/vendor/assets/javascripts/chart-lib.min.js index 626e6c3cdb9..3a0a2c87345 100644 --- a/vendor/assets/javascripts/chart-lib.min.js +++ b/vendor/assets/javascripts/chart-lib.min.js @@ -1,11 +1,11 @@ /*! * Chart.js * http://chartjs.org/ - * Version: 1.0.1-beta.4 + * Version: 1.0.2 * - * Copyright 2014 Nick Downie + * Copyright 2015 Nick Downie * Released under the MIT license * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md */ -(function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;this.width=t.canvas.width,this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n<t.length;n++)i.apply(e,[t[n],n].concat(s))}else for(var o in t)i.apply(e,[t[o],o].concat(s))},o=s.clone=function(t){var i={};return n(t,function(e,s){t.hasOwnProperty(s)&&(i[s]=e)}),i},a=s.extend=function(t){return n(Array.prototype.slice.call(arguments,1),function(i){n(i,function(e,s){i.hasOwnProperty(s)&&(t[s]=e)})}),t},h=s.merge=function(){var t=Array.prototype.slice.call(arguments,0);return t.unshift({}),a.apply(null,t)},l=s.indexOf=function(t,i){if(Array.prototype.indexOf)return t.indexOf(i);for(var e=0;e<t.length;e++)if(t[e]===i)return e;return-1},r=(s.where=function(t,i){var e=[];return s.each(t,function(t){i(t)&&e.push(t)}),e},s.findNextWhere=function(t,i,e){e||(e=-1);for(var s=e+1;s<t.length;s++){var n=t[s];if(i(n))return n}},s.findPreviousWhere=function(t,i,e){e||(e=t.length);for(var s=e-1;s>=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof t.define&&t.define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),x=s.radians=function(t){return t*(Math.PI/180)},S=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),x=Math.round(f/v);(x>a||a>2*x)&&!h;)if(x>a)v*=2,x=Math.round(f/v),x%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,x=Math.round(f/v)}else v/=2,x=Math.round(f/v);return h&&(x=o,v=f/x),{steps:x,stepValue:v,min:p,max:p+x*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),b=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),-(s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)))},easeOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),s*Math.pow(2,-10*t)*Math.sin(2*(1*t-i)*Math.PI/e)+1)},easeInOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:2==(t/=.5)?1:(e||(e=.3*1.5),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),1>t?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-b.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*b.easeInBounce(2*t):.5*b.easeOutBounce(2*t-1)+.5}}),w=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=(s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),s.animationLoop=function(t,i,e,s,n,o){var a=0,h=b[e]||b.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=w(l):n.apply(o)};w(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),L=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},k=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},P(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){L(t.chart.canvas,e,i)})}),F=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},R=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),T=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},M=s.fontString=function(t,i,e){return i+" "+t+"px "+e},W=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},z=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return T(this.chart),this},stop:function(){return s.cancelAnimFrame.call(t,this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=F(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:R(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),k(this,this.events),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)<Math.pow(e,2)},draw:function(){if(this.display){var t=this.ctx;t.beginPath(),t.arc(this.x,this.y,this.radius,0,2*Math.PI),t.closePath(),t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.fillStyle=this.fillColor,t.fill(),t.stroke()}}}),e.Arc=e.Element.extend({inRange:function(t,i){var e=s.getAngleFromPoint(this,{x:t,y:i}),n=e.angle>=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=M(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;switch(t.fillStyle=this.fillColor,this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}z(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=M(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=M(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=W(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){z(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?W(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),t<this.yLabelWidth&&this.calculateXLabelRotation()},calculateXLabelRotation:function(){this.ctx.font=this.font;var t,i,e=this.ctx.measureText(this.xLabels[0]).width,s=this.ctx.measureText(this.xLabels[this.xLabels.length-1]).width;if(this.xScalePaddingRight=s/2+3,this.xScalePaddingLeft=e/2>this.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=W(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(x(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(x(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/(this.valuesCount-(this.offsetGridLines?0:1)),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a);t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath(),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+S(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+S(this.lineWidth),o=this.xLabelRotation>0;t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath(),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*x(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=M(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;i<this.valuesCount;i++)t=this.getPointPosition(i,d),e=this.ctx.measureText(C(this.templateString,{value:this.labels[i]})).width+5,0===i||i===this.valuesCount/2?(s=e/2,t.x+s>p&&(p=t.x+s,n=i),t.x-s<g&&(g=t.x-s,a=i)):i<this.valuesCount/2?t.x+e>p&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e<g&&(g=t.x-e,a=i);l=g,r=Math.ceil(p-this.width),o=this.getIndexAngle(n),h=this.getIndexAngle(a),c=r/Math.sin(o+Math.PI/2),u=l/Math.sin(h+Math.PI/2),c=f(c)?c:0,u=f(u)?u:0,this.drawingArea=d-(u+c)/2,this.setCenterPoint(u,c)},setCenterPoint:function(t,i){var e=this.width-i-this.drawingArea,s=t+this.drawingArea;this.xCenter=(s+e)/2,this.yCenter=this.height/2},getIndexAngle:function(t){var i=2*Math.PI/this.valuesCount;return t*i-Math.PI/2},getPointPosition:function(t,i){var e=this.getIndexAngle(t);return{x:Math.cos(e)*i+this.xCenter,y:Math.sin(e)*i+this.yCenter}},draw:function(){if(this.display){var t=this.ctx;if(n(this.yLabels,function(i,e){if(e>0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a<this.valuesCount;a++)s=this.getPointPosition(a,this.calculateCenterOffset(this.min+e*this.stepValue)),0===a?t.moveTo(s.x,s.y):t.lineTo(s.x,s.y);t.closePath(),t.stroke()}if(this.showLabels){if(t.font=M(this.fontSize,this.fontStyle,this.fontFamily),this.showLabelBackdrop){var h=t.measureText(i).width;t.fillStyle=this.backdropColor,t.fillRect(this.xCenter-h/2-this.backdropPaddingX,o-this.fontSize/2-this.backdropPaddingY,h+2*this.backdropPaddingX,this.fontSize+2*this.backdropPaddingY)}t.textAlign="center",t.textBaseline="middle",t.fillStyle=this.fontColor,t.fillText(i,this.xCenter,o)}}},this),!this.lineArc){t.lineWidth=this.angleLineWidth,t.strokeStyle=this.angleLineColor;for(var i=this.valuesCount-1;i>=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=M(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].fillColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<this.datasets.length;a++)for(i=0;i<this.datasets[a].bars.length;i++)if(this.datasets[a].bars[i].inRange(n.x,n.y))return e.each(this.datasets,o),s;return s},buildScale:function(t){var i=this,s=function(){var t=[];return i.eachBars(function(i){t.push(i.value)}),t},n={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(s(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.barShowStroke?this.options.barStrokeWidth:0,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(n,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new this.ScaleClass(n)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].bars.push(new this.BarClass({value:t,label:i,x:this.scale.calculateBarX(this.datasets.length,e,this.scale.valuesCount+1),y:this.scale.endPoint,width:this.scale.calculateBarWidth(this.datasets.length),base:this.scale.endPoint,strokeColor:this.datasets[e].strokeColor,fillColor:this.datasets[e].fillColor}))},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.bars.shift()},this),this.update()},reflow:function(){e.extend(this.BarClass.prototype,{y:this.scale.endPoint,base:this.scale.endPoint});var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();this.chart.ctx;this.scale.draw(i),e.each(this.datasets,function(t,s){e.each(t.bars,function(t,e){t.hasValue()&&(t.base=this.scale.endPoint,t.transition({x:this.scale.calculateBarX(this.datasets.length,s,e),y:this.scale.calculateY(t.value),width:this.scale.calculateBarWidth(this.datasets.length)},i).draw())},this)},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'}; -i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(t/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle)},this)}}),i.types.Doughnut.extend({name:"Pie",defaults:e.merge(s,{percentageInnerCutout:0})})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0,bezierCurveTension:.4,pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)<Math.pow(this.radius+this.hitDetectionRadius,2)}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this),this.buildScale(t.labels),this.eachPoints(function(t,i){e.extend(t,{x:this.scale.calculateX(i),y:this.scale.endPoint}),t.save()},this)},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachPoints(function(t){t.save()}),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.datasets,function(t){e.each(t.points,function(t){t.inRange(s.x,s.y)&&i.push(t)})},this),i},buildScale:function(t){var s=this,n=function(){var t=[];return s.eachPoints(function(i){t.push(i.value)}),t},o={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(n(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.pointDotRadius+this.options.pointDotStrokeWidth,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(o,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new i.Scale(o)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:this.scale.calculateX(this.scale.valuesCount+1),y:this.scale.endPoint,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.points.shift()},this),this.update()},reflow:function(){var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();var s=this.chart.ctx,n=function(t){return null!==t.value},o=function(t,i,s){return e.findNextWhere(i,n,s)||t},a=function(t,i,s){return e.findPreviousWhere(i,n,s)||t};this.scale.draw(i),e.each(this.datasets,function(t){var h=e.where(t.points,n);e.each(t.points,function(t,e){t.hasValue()&&t.transition({y:this.scale.calculateY(t.value),x:this.scale.calculateX(e)},i)},this),this.options.bezierCurve&&e.each(h,function(t,i){var s=i>0&&i<h.length-1?this.options.bezierCurveTension:0;t.controlPoints=e.splineCurve(a(t,h,i),t,o(t,h,i),s),t.controlPoints.outer.y>this.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.y<this.scale.startPoint&&(t.controlPoints.outer.y=this.scale.startPoint),t.controlPoints.inner.y>this.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y<this.scale.startPoint&&(t.controlPoints.inner.y=this.scale.startPoint)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(h,function(t,i){if(0===i)s.moveTo(t.x,t.y);else if(this.options.bezierCurve){var e=a(t,h,i);s.bezierCurveTo(e.controlPoints.outer.x,e.controlPoints.outer.y,t.controlPoints.inner.x,t.controlPoints.inner.y,t.x,t.y)}else s.lineTo(t.x,t.y)},this),s.stroke(),this.options.datasetFill&&h.length>0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle),t.draw()},this),this.scale.draw()}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers;i.Type.extend({name:"Radar",defaults:{scaleShowLine:!0,angleShowLineOut:!0,scaleShowLabels:!1,scaleBeginAtZero:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:10,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this);
\ No newline at end of file +(function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;var i=function(t,i){return t["offset"+i]?t["offset"+i]:document.defaultView.getComputedStyle(t).getPropertyValue(i)},e=this.width=i(t.canvas,"Width"),n=this.height=i(t.canvas,"Height");t.canvas.width=e,t.canvas.height=n;var e=this.width=t.canvas.width,n=this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,customTooltips:!1,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n<t.length;n++)i.apply(e,[t[n],n].concat(s))}else for(var o in t)i.apply(e,[t[o],o].concat(s))},o=s.clone=function(t){var i={};return n(t,function(e,s){t.hasOwnProperty(s)&&(i[s]=e)}),i},a=s.extend=function(t){return n(Array.prototype.slice.call(arguments,1),function(i){n(i,function(e,s){i.hasOwnProperty(s)&&(t[s]=e)})}),t},h=s.merge=function(){var t=Array.prototype.slice.call(arguments,0);return t.unshift({}),a.apply(null,t)},l=s.indexOf=function(t,i){if(Array.prototype.indexOf)return t.indexOf(i);for(var e=0;e<t.length;e++)if(t[e]===i)return e;return-1},r=(s.where=function(t,i){var e=[];return s.each(t,function(t){i(t)&&e.push(t)}),e},s.findNextWhere=function(t,i,e){e||(e=-1);for(var s=e+1;s<t.length;s++){var n=t[s];if(i(n))return n}},s.findPreviousWhere=function(t,i,e){e||(e=t.length);for(var s=e-1;s>=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof define&&define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),S=s.radians=function(t){return t*(Math.PI/180)},x=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),S=Math.round(f/v);(S>a||a>2*S)&&!h;)if(S>a)v*=2,S=Math.round(f/v),S%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,S=Math.round(f/v)}else v/=2,S=Math.round(f/v);return h&&(S=o,v=f/S),{steps:S,stepValue:v,min:p,max:p+S*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),w=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),-(s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)))},easeOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),s*Math.pow(2,-10*t)*Math.sin(2*(1*t-i)*Math.PI/e)+1)},easeInOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:2==(t/=.5)?1:(e||(e=.3*1.5),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),1>t?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-w.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*w.easeInBounce(2*t):.5*w.easeOutBounce(2*t-1)+.5}}),b=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),L=(s.animationLoop=function(t,i,e,s,n,o){var a=0,h=w[e]||w.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=b(l):n.apply(o)};b(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),k=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},F=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},L(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){k(t.chart.canvas,e,i)})}),R=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},T=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),M=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},W=s.fontString=function(t,i,e){return i+" "+t+"px "+e},z=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},B=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return M(this.chart),this},stop:function(){return P(this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=R(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:T(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),F(this,this.events);var t=this.chart.canvas;t.width=this.chart.width,t.height=this.chart.height,t.style.removeProperty?(t.style.removeProperty("width"),t.style.removeProperty("height")):(t.style.removeAttribute("width"),t.style.removeAttribute("height")),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),this.options.customTooltips&&this.options.customTooltips(!1),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx,custom:this.options.customTooltips}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart,custom:this.options.customTooltips}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)<Math.pow(e,2)},draw:function(){if(this.display){var t=this.ctx;t.beginPath(),t.arc(this.x,this.y,this.radius,0,2*Math.PI),t.closePath(),t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.fillStyle=this.fillColor,t.fill(),t.stroke()}}}),e.Arc=e.Element.extend({inRange:function(t,i){var e=s.getAngleFromPoint(this,{x:t,y:i}),n=e.angle>=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=this.caretPadding=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;if(t.fillStyle=this.fillColor,this.custom)this.custom(this);else{switch(this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}B(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=W(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=z(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){if(this.custom)this.custom(this);else{B(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?z(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),t<this.yLabelWidth&&this.calculateXLabelRotation()},calculateXLabelRotation:function(){this.ctx.font=this.font;var t,i,e=this.ctx.measureText(this.xLabels[0]).width,s=this.ctx.measureText(this.xLabels[this.xLabels.length-1]).width;if(this.xScalePaddingRight=s/2+3,this.xScalePaddingLeft=e/2>this.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=z(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(S(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(S(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/Math.max(this.valuesCount-(this.offsetGridLines?0:1),1),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a),l=this.showHorizontalLines;t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),0!==o||l||(l=!0),l&&t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),l&&(t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+x(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+x(this.lineWidth),o=this.xLabelRotation>0,a=this.showVerticalLines;0!==e||a||(a=!0),a&&t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),a&&(t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*S(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;i<this.valuesCount;i++)t=this.getPointPosition(i,d),e=this.ctx.measureText(C(this.templateString,{value:this.labels[i]})).width+5,0===i||i===this.valuesCount/2?(s=e/2,t.x+s>p&&(p=t.x+s,n=i),t.x-s<g&&(g=t.x-s,a=i)):i<this.valuesCount/2?t.x+e>p&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e<g&&(g=t.x-e,a=i);l=g,r=Math.ceil(p-this.width),o=this.getIndexAngle(n),h=this.getIndexAngle(a),c=r/Math.sin(o+Math.PI/2),u=l/Math.sin(h+Math.PI/2),c=f(c)?c:0,u=f(u)?u:0,this.drawingArea=d-(u+c)/2,this.setCenterPoint(u,c)},setCenterPoint:function(t,i){var e=this.width-i-this.drawingArea,s=t+this.drawingArea;this.xCenter=(s+e)/2,this.yCenter=this.height/2},getIndexAngle:function(t){var i=2*Math.PI/this.valuesCount;return t*i-Math.PI/2},getPointPosition:function(t,i){var e=this.getIndexAngle(t);return{x:Math.cos(e)*i+this.xCenter,y:Math.sin(e)*i+this.yCenter}},draw:function(){if(this.display){var t=this.ctx;if(n(this.yLabels,function(i,e){if(e>0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a<this.valuesCount;a++)s=this.getPointPosition(a,this.calculateCenterOffset(this.min+e*this.stepValue)),0===a?t.moveTo(s.x,s.y):t.lineTo(s.x,s.y);t.closePath(),t.stroke()}if(this.showLabels){if(t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.showLabelBackdrop){var h=t.measureText(i).width;t.fillStyle=this.backdropColor,t.fillRect(this.xCenter-h/2-this.backdropPaddingX,o-this.fontSize/2-this.backdropPaddingY,h+2*this.backdropPaddingX,this.fontSize+2*this.backdropPaddingY)}t.textAlign="center",t.textBaseline="middle",t.fillStyle=this.fontColor,t.fillText(i,this.xCenter,o)}}},this),!this.lineArc){t.lineWidth=this.angleLineWidth,t.strokeStyle=this.angleLineColor;for(var i=this.valuesCount-1;i>=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].fillColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<this.datasets.length;a++)for(i=0;i<this.datasets[a].bars.length;i++)if(this.datasets[a].bars[i].inRange(n.x,n.y))return e.each(this.datasets,o),s;return s},buildScale:function(t){var i=this,s=function(){var t=[];return i.eachBars(function(i){t.push(i.value)}),t},n={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(s(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,showHorizontalLines:this.options.scaleShowHorizontalLines,showVerticalLines:this.options.scaleShowVerticalLines,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.barShowStroke?this.options.barStrokeWidth:0,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(n,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new this.ScaleClass(n)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].bars.push(new this.BarClass({value:t,label:i,x:this.scale.calculateBarX(this.datasets.length,e,this.scale.valuesCount+1),y:this.scale.endPoint,width:this.scale.calculateBarWidth(this.datasets.length),base:this.scale.endPoint,strokeColor:this.datasets[e].strokeColor,fillColor:this.datasets[e].fillColor})) +},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.bars.shift()},this),this.update()},reflow:function(){e.extend(this.BarClass.prototype,{y:this.scale.endPoint,base:this.scale.endPoint});var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();this.chart.ctx;this.scale.draw(i),e.each(this.datasets,function(t,s){e.each(t.bars,function(t,e){t.hasValue()&&(t.base=this.scale.endPoint,t.transition({x:this.scale.calculateBarX(this.datasets.length,s,e),y:this.scale.calculateY(t.value),width:this.scale.calculateBarWidth(this.datasets.length)},i).draw())},this)},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(Math.abs(t)/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=Math.abs(t.value)},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle)},this)}}),i.types.Doughnut.extend({name:"Pie",defaults:e.merge(s,{percentageInnerCutout:0})})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,bezierCurve:!0,bezierCurveTension:.4,pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)<Math.pow(this.radius+this.hitDetectionRadius,2)}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this),this.buildScale(t.labels),this.eachPoints(function(t,i){e.extend(t,{x:this.scale.calculateX(i),y:this.scale.endPoint}),t.save()},this)},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachPoints(function(t){t.save()}),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.datasets,function(t){e.each(t.points,function(t){t.inRange(s.x,s.y)&&i.push(t)})},this),i},buildScale:function(t){var s=this,n=function(){var t=[];return s.eachPoints(function(i){t.push(i.value)}),t},o={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(n(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,showHorizontalLines:this.options.scaleShowHorizontalLines,showVerticalLines:this.options.scaleShowVerticalLines,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.pointDotRadius+this.options.pointDotStrokeWidth,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(o,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new i.Scale(o)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:this.scale.calculateX(this.scale.valuesCount+1),y:this.scale.endPoint,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.points.shift()},this),this.update()},reflow:function(){var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();var s=this.chart.ctx,n=function(t){return null!==t.value},o=function(t,i,s){return e.findNextWhere(i,n,s)||t},a=function(t,i,s){return e.findPreviousWhere(i,n,s)||t};this.scale.draw(i),e.each(this.datasets,function(t){var h=e.where(t.points,n);e.each(t.points,function(t,e){t.hasValue()&&t.transition({y:this.scale.calculateY(t.value),x:this.scale.calculateX(e)},i)},this),this.options.bezierCurve&&e.each(h,function(t,i){var s=i>0&&i<h.length-1?this.options.bezierCurveTension:0;t.controlPoints=e.splineCurve(a(t,h,i),t,o(t,h,i),s),t.controlPoints.outer.y>this.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.y<this.scale.startPoint&&(t.controlPoints.outer.y=this.scale.startPoint),t.controlPoints.inner.y>this.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y<this.scale.startPoint&&(t.controlPoints.inner.y=this.scale.startPoint)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(h,function(t,i){if(0===i)s.moveTo(t.x,t.y);else if(this.options.bezierCurve){var e=a(t,h,i);s.bezierCurveTo(e.controlPoints.outer.x,e.controlPoints.outer.y,t.controlPoints.inner.x,t.controlPoints.inner.y,t.x,t.y)}else s.lineTo(t.x,t.y)},this),s.stroke(),this.options.datasetFill&&h.length>0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle),t.draw()},this),this.scale.draw()}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers;i.Type.extend({name:"Radar",defaults:{scaleShowLine:!0,angleShowLineOut:!0,scaleShowLabels:!1,scaleBeginAtZero:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:10,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this);
\ No newline at end of file |