diff --git a/.rubocop.yml b/.rubocop.yml
index 7290d627d2..03b78d6884 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/CHANGELOG b/CHANGELOG
index ea6d6c0ae9..1999949115 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,23 +1,42 @@
Please view this file on the master branch, on stable branches it's out of date.
v 7.10.0 (unreleased)
+ - 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 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)
+ - 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
@@ -59,8 +78,31 @@ v 7.9.0 (unreleased)
- 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
+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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 42b5ce22e3..3165b7379d 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 e70b4523ae..097a15a2af 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-2.6.0
+2.6.2
diff --git a/Gemfile b/Gemfile
index 13f2e4a671..5261b0e175 100644
--- a/Gemfile
+++ b/Gemfile
@@ -39,7 +39,7 @@ gem "browser"
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem "gitlab_git", '~> 7.1.3'
+gem "gitlab_git", '~> 7.1.6'
# Ruby/Rack Git Smart-HTTP Server Handler
gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack'
@@ -95,7 +95,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'
@@ -123,7 +123,6 @@ gem 'slim'
gem 'sinatra', require: nil
gem 'sidekiq', '~> 3.3'
gem 'sidetiq', '0.6.3'
-gem 'sidekiq-limit_fetch'
# HTTP requests
gem "httparty"
@@ -210,7 +209,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'
@@ -259,6 +257,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 5a3d0bdf10..cd1887fd46 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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.3)
+ gitlab_git (7.1.6)
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)
@@ -547,8 +547,6 @@ GEM
json
redis (>= 3.0.6)
redis-namespace (>= 1.3.1)
- sidekiq-limit_fetch (2.4.1)
- sidekiq (>= 2.6.5, < 4.0)
sidetiq (0.6.3)
celluloid (>= 0.14.1)
ice_cube (= 0.11.1)
@@ -705,7 +703,7 @@ DEPENDENCIES
gitlab-grack (~> 2.0.0.rc2)
gitlab-linguist (~> 3.0.1)
gitlab_emoji (~> 0.1)
- gitlab_git (~> 7.1.3)
+ gitlab_git (~> 7.1.6)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (= 1.2.1)
gollum-lib (~> 4.0.2)
@@ -758,7 +756,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)
@@ -772,7 +770,6 @@ DEPENDENCIES
settingslogic
shoulda-matchers (~> 2.7.0)
sidekiq (~> 3.3)
- sidekiq-limit_fetch
sidetiq (= 0.6.3)
simplecov
sinatra
diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee
index 06e9f0001a..fca2a290e2 100644
--- a/app/assets/javascripts/dropzone_input.js.coffee
+++ b/app/assets/javascripts/dropzone_input.js.coffee
@@ -10,6 +10,7 @@ class @DropzoneInput
iconSpinner = ""
btnAlert = ""
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 "
"
@@ -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 0aaa62d2b8..4e2e6550eb 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -18,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 2cbe03b986..fc75f14383 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) ->
@@ -160,4 +164,3 @@ class @MergeRequest
else
setTimeout(merge_request.mergeInProgress, 3000)
dataType: 'json'
-
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee
index d522d9f3b9..4a05bdccdb 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 e592b700e7..31895fbf2b 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/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index a801a2ac37..e67012f018 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -9,6 +9,8 @@ class @UsersSelect
@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"
@@ -19,25 +21,41 @@ class @UsersSelect
data = { results: users }
if query.term.length == 0
- anyUser = {
- name: 'Any',
- avatar: null,
- username: 'none',
- id: null
- }
-
- nullUser = {
- name: 'Unassigned',
- avatar: null,
- username: 'none',
- id: 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) =>
diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss
index ccba65e3fd..216f25cdcd 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/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 83daea10ed..d4af7506d5 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/notes.scss b/app/assets/stylesheets/pages/notes.scss
index d66093bc2e..facd7e1931 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 5bd725d122..c005470355 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 {
@@ -61,13 +66,12 @@
.project-home-desc {
float: left;
- color: #666;
+ color: $gray;
}
}
.visibility-level-label {
- color: #555;
- font-weight: bold;
+ color: $gray;
i {
color: inherit;
}
@@ -115,6 +119,10 @@
background: #FAFAFA;
width: 100%;
}
+
+ .input-group-addon {
+ background: #FAFAFA;
+ }
}
.project-visibility-level-holder {
@@ -133,7 +141,7 @@
.option-descr {
margin-left: 24px;
- color: #666;
+ color: $gray;
}
}
}
@@ -212,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 72bb09153b..3f900956e7 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -39,6 +39,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:sign_in_text,
:home_page_url,
:help_text,
+ :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 0000000000..e93603bef3
--- /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 9d9adaa467..22d045fc38 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 693970e534..b4c011f213 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 b5e7d9aa1d..40f53c0766 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/audit_events_controller.rb b/app/controllers/audit_events_controller.rb
index d5fc3c0c57..3ae0c43c52 100644
--- a/app/controllers/audit_events_controller.rb
+++ b/app/controllers/audit_events_controller.rb
@@ -22,7 +22,7 @@ class AuditEventsController < ApplicationController
end
def authorize_admin_group!
- render_404 unless can?(current_user, :manage_group, group)
+ render_404 unless can?(current_user, :admin_group, group)
end
def determine_layout
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index bc98eab133..af1faca93f 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 56c427a9de..9a761d57e9 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 ae9291f178..cdb96c3daa 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
@@ -25,7 +26,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
access_level = params[:access_level]
user_ids = params[:user_ids].split(',')
- @group.add_users(user_ids, access_level)
+ @group.add_users(user_ids, access_level, current_user)
group_members = @group.group_members.where(user_id: user_ids)
group_members.each do |group_member|
@@ -52,7 +53,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
log_audit_event(@group_member, action: :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
@@ -60,6 +61,20 @@ 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
@@ -67,7 +82,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
@group_member.destroy
log_audit_event(@group_member, action: :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 c46b8fff88..546ff2cc71 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 fbd9e67e6d..0e5567c773 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -3,7 +3,7 @@ class HelpController < ApplicationController
end
def show
- @filepath = params[:filepath]
+ @filepath = clean_path_info(params[:filepath])
@format = params[:format]
respond_to do |format|
@@ -36,4 +36,35 @@ class HelpController < ApplicationController
def ui
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 edb8bd4160..93a7ace353 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/google_code_controller.rb b/app/controllers/import/google_code_controller.rb
new file mode 100644
index 0000000000..fb4ef98736
--- /dev/null
+++ b/app/controllers/import/google_code_controller.rb
@@ -0,0 +1,111 @@
+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
+
+ 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 0000000000..1f97ff16c5
--- /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/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb
index 4a65c978e5..954c98c0d9 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 9252e85e8c..7f76906066 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 93730e7121..ced8e25191 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
@@ -34,36 +41,27 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
end
- def destroy
- @key = @project.deploy_keys.find(params[:id])
- @key.destroy
-
- log_audit_event(@key.title, action: :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
+ log_audit_event(@key.title, action: :create)
redirect_to namespace_project_deploy_keys_path(@project.namespace,
@project)
end
def disable
+ @key = accessible_keys.find(params[:id])
@project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy
+ log_audit_event(@key.title, action: :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 d654b02432..ef7da47073 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
@@ -35,9 +37,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def create
- users = User.where(id: params[:user_ids].split(','))
- @project.team << [users, params[:access_level]]
- members = @project.project_members.where(user_id: users.pluck(:id))
+ @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ members = @project.project_members.where(user_id: params[:user_ids].split(','))
members.each do |member|
log_audit_event(member, action: :create)
@@ -47,7 +48,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def update
- @project_member = @project.project_members.find_by(user_id: member)
+ @project_member = @project.project_members.find(params[:id])
old_access_level = @project_member.human_access
if @project_member.update_attributes(member_params)
@@ -56,19 +57,32 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def destroy
- @project_member = @project.project_members.find_by(user_id: member)
+ @project_member = @project.project_members.find(params[:id])
@project_member.destroy
log_audit_event(@project_member, action: :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_member = @project.project_members.find_by(user_id: current_user)
@project_member.destroy
@@ -82,7 +96,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),
@@ -91,10 +105,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 67acf45ab7..ec3b2b8d75 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/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 643167947b..aeb7f0699f 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 a3284c82d3..c5828d0b2d 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 7b6982c507..3f11d7afe6 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/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 3f3509bb18..20457572a0 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
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 5aae697e2f..d13d80be29 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 b56f21c7a1..4f42972a4d 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 08476f8516..0df3ecc90b 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 7ca3f05863..aa1de2f50e 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 =~ /^\ 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
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index 241462e5e4..9954617c76 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/wiki_helper.rb b/app/helpers/wiki_helper.rb
new file mode 100644
index 0000000000..a3bc64c010
--- /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 26f43bf955..1c43f95dc8 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 ab5b076535..3a83b08310 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 3cd812825e..0dbb2939bb 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 4969cef937..d35e7de0c2 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -14,13 +14,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
@@ -58,20 +61,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
+ return unless 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
-
- 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
@@ -80,9 +87,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
@@ -155,4 +161,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 d2b39f667f..85a15596f8 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 f097e66e96..97a486c952 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -17,6 +17,7 @@
# twitter_sharing_enabled :boolean default(TRUE)
# help_text :text
# restricted_visibility_levels :text
+# max_attachment_size :integer default(10)
#
class ApplicationSetting < ActiveRecord::Base
@@ -24,7 +25,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|
@@ -50,7 +51,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 e0461809e1..006fa62c8f 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 88ac83744d..478134dff6 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 9dcce3c991..b7882a2bb1 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -42,36 +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.is_a?(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+.
@@ -97,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 570f5e91c1..85d52d558c 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 7e88903b9a..18db521741 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 57f6d5cd4e..c9a88ffa8e 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 b4670b4567..49948e9d47 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -50,19 +50,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 defef7216f..315d96af1b 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 440fcd0d05..756d19adec 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 e96f07e421..49928b5f03 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
@@ -81,20 +80,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
+ return unless self.key.present?
- if cmd_status.zero?
- cmd_output.gsub /(\h{2}:)+\h{2}/ do |match|
- self.fingerprint = match
- end
- end
+ self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint
end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index fe3d2f40e8..d151c7b239 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 906540909c..c94bfd1983 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -28,10 +28,6 @@ class GroupMember < Member
scope :with_user, ->(user) { where(user_id: user.id) }
scope :with_ldap_dn, -> { joins(user: :identities).where("identities.provider LIKE ?", 'ldap%') }
- 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
@@ -44,26 +40,37 @@ class GroupMember < Member
access_level
end
- def post_create_hook
- notification_service.new_group_member(self)
- system_hook_service.execute_hooks_for(self, :create)
+ private
+
+ def send_invite
+ notification_service.invite_group_member(self, @raw_invite_token)
+
+ super
end
- def notify_update
+ def post_create_hook
+ notification_service.new_group_member(self)
+
+ super
+ end
+
+ def post_update_hook
if access_level_changed?
notification_service.update_group_member(self)
end
+
+ super
end
- def post_destroy_hook
- system_hook_service.execute_hooks_for(self, :destroy)
+ def after_accept_invite
+ notification_service.accept_group_invite(self)
+
+ super
end
- def system_hook_service
- SystemHooksService.new
- end
+ def after_decline_invite
+ notification_service.decline_group_invite(self)
- def notification_service
- NotificationService.new
+ super
end
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 6b13e0ff30..0a3b4d2182 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)
+
+ super
+ end
+
+ def after_accept_invite
+ notification_service.accept_project_invite(self)
+
+ super
+ end
+
+ def after_decline_invite
+ notification_service.decline_project_invite(self)
+
+ super
end
def event_service
EventCreateService.new
end
-
- def notification_service
- NotificationService.new
- end
-
- def system_hook_service
- SystemHooksService.new
- end
-
- def project
- source
- end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 97f00b5f5e..b92834e5e3 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -260,11 +260,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
[]
@@ -364,6 +364,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 dd74165f88..a0d79d7e5c 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -61,8 +61,8 @@ class Namespace < ActiveRecord::Base
def clean_path(path)
path.gsub!(/@.*\z/, "")
path.gsub!(/\.git\z/, "")
- path.gsub!(/\A-/, "")
- path.gsub!(/.\z/, "")
+ path.gsub!(/\A-+/, "")
+ path.gsub!(/\.+\z/, "")
path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
counter = 0
diff --git a/app/models/note.rb b/app/models/note.rb
index 99d2f00242..4bb9117775 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' }
@@ -326,6 +328,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) ||
@@ -353,7 +359,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
@@ -363,6 +369,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
@@ -456,7 +466,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 650a9392a7..c71a4ba5ae 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -27,6 +27,7 @@
# import_type :string(255)
# import_source :string(255)
# avatar :string(255)
+# import_data :text
#
require 'carrierwave/orm/activerecord'
@@ -50,6 +51,8 @@ class Project < ActiveRecord::Base
default_value_for :wall_enabled, false
default_value_for :snippets_enabled, gitlab_config_features.snippets
+ serialize :import_data, JSON
+
# set last_activity_at to the same as created_at
after_create :set_last_activity_at
def set_last_activity_at
@@ -83,7 +86,7 @@ class Project < ActiveRecord::Base
has_one :gemnasium_service, dependent: :destroy
has_one :slack_service, dependent: :destroy
has_one :jenkins_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
@@ -143,7 +146,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
@@ -191,6 +194,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
@@ -268,6 +272,11 @@ class Project < ActiveRecord::Base
RepositoryImportWorker.perform_in(2.seconds, id)
end
+ def clear_import_data
+ self.import_data = nil
+ self.save
+ end
+
def import?
import_url.present?
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index f968afe9fa..d8aedbd2ab 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
similarity index 94%
rename from app/models/project_services/buildbox_service.rb
rename to app/models/project_services/buildkite_service.rb
index 3a381ff11b..a714bc8224 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 e521186798..a199d0e86f 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 edaeeffc22..0f9838a575 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/irker_service.rb b/app/models/project_services/irker_service.rb
index 2bddb7b881..e9e1e276e7 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/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index c26bc55135..3c002a1634 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 9755fb779d..761b240109 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,23 +43,21 @@ 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)
return false if group_member_lock
+
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)
@@ -89,7 +87,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
@@ -97,13 +95,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 7276949887..263a436d52 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
diff --git a/app/models/service.rb b/app/models/service.rb
index 2f8946af9c..25a78dfa73 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -122,26 +122,26 @@ class Service < ActiveRecord::Base
def self.available_services_names
%w(
- gitlab_ci
- campfire
- hipchat
- pivotaltracker
- flowdock
- assembla
asana
- emails_on_push
- gemnasium
- slack
- jenkins
- pushover
- buildbox
+ assembla
bamboo
- teamcity
- jira
- redmine
+ buildkite
+ campfire
custom_issue_tracker
- irker
+ emails_on_push
external_wiki
+ flowdock
+ gemnasium
+ gitlab_ci
+ hipchat
+ irker
+ jenkins
+ jira
+ pivotaltracker
+ pushover
+ redmine
+ slack
+ teamcity
)
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 1985017c24..676c9d6dca 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,6 +124,7 @@ 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,
@@ -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
@@ -442,8 +445,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
@@ -463,6 +474,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
@@ -514,13 +531,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/create_tag_service.rb b/app/services/create_tag_service.rb
index 4115d68992..25f9e20324 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/git_push_service.rb b/app/services/git_push_service.rb
index 8c4e74f072..25e5dbe619 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
@@ -91,7 +91,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?
@@ -131,6 +131,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/notification_service.rb b/app/services/notification_service.rb
index cc5853144c..cfed7964c3 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 cd7690708b..d798d3c309 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 6b0d4aca3e..1e4deb6ed3 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 bcbacbff56..ae6260bcda 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 a186c97628..992a7a7a1d 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 28bf506398..de64e2e522 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -65,5 +65,10 @@
.col-sm-10
= f.text_area :help_text, class: 'form-control', rows: 4
.help-block Markdown enabled
+ .form-group
+ = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :max_attachment_size, class: 'form-control'
+
.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 b77d188a38..fa4e6335c7 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 8db2b2a709..4ef8e878a7 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 0000000000..2ae83ab95f
--- /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 0000000000..c00049424c
--- /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 0000000000..cfa2adf92e
--- /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 8e4a31053d..46bec86f17 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -87,7 +87,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, skip_ldap: @group.ldap_synced? })
+ = users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?)
%div.prepend-top-10
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
@@ -101,13 +101,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 05372f4124..78684c692c 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 0a2934d3bd..3524f04c5e 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 165db214d7..0cb7f764fa 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/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index 670f5ac7af..f4ad2b294b 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 dcf60c9043..9dc6aeffd5 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 3c7153d235..552525f4a0 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 30e5faf822..56b1948a47 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 fb7b5a85fb..cea0bbad7d 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, skip_ldap: @group.ldap_synced?, class: 'input-large', scope: :all)
+ .col-sm-10
+ = users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true, skip_ldap: @group.ldap_synced?)
+ .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 40893df282..1e74704f40 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
- if ldap_enabled? && @group.ldap_group_links.any?
= link_to reset_access_group_ldap_path(@group), class: 'btn btn-grouped', data: { confirm: "Force GitLab to do LDAP permission checks for all group members? All members besides yourself will be reduced to 'Guest' access until their next interaction with GitLab." }, method: :put do
diff --git a/app/views/groups/milestones/_issue.html.haml b/app/views/groups/milestones/_issue.html.haml
index 27d0c62df8..09f9b4b896 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 b2d2097dfa..d0d1426762 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
index 94fc43a581..30093d2d05 100644
--- a/app/views/groups/milestones/_milestone.html.haml
+++ b/app/views/groups/milestones/_milestone.html.haml
@@ -1,6 +1,6 @@
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
.pull-right
- - if can?(current_user, :manage_group, @group)
+ - 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
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index fea70f5cbc..fb32f2caa4 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 d825215bcd..05683de503 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/import/base/create.js.haml b/app/views/import/base/create.js.haml
index 8d10722628..90a6f5f9d2 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("
Access denied! Please verify you can add deploy keys to this repository.
"")
+ job.find(".import-actions").html("
Access denied! Please verify you can add deploy keys to this repository.
")
- 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 0000000000..ce78fec205
--- /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 Select none button on the right, since we only need "Google Code Project Hosting".
+ %li
+ %p
+ Scroll down to Google Code Project Hosting and enable the switch on the right.
+ %li
+ %p
+ Choose Next at the bottom of the page.
+ %li
+ %p
+ Leave the "File type" and "Delivery method" options on their default values.
+ %li
+ %p
+ Choose Create archive and wait for archiving to complete.
+ %li
+ %p
+ Click the Download button and wait for downloading to complete.
+ %li
+ %p
+ Find the downloaded ZIP file and decompress it.
+ %li
+ %p
+ Find the newly extracted Takeout/Google Code Project Hosting/GoogleCodeProjectHosting.json file.
+ %li
+ %p
+ Upload GoogleCodeProjectHosting.json 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 0000000000..d55fcfc97a
--- /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. "johnsmith@gmail.com": "John Smith" or "johnsmith@gmail.com": "@johnsmith". 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 0000000000..2013b8c03c
--- /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 0000000000..ab0ecffe4d
--- /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 b1c2e1a7b1..d58582c107 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 aa4d0e2ea9..46ae19f840 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 32fe0e37df..f0d92b7a12 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/notify.html.haml b/app/views/layouts/notify.html.haml
index 7eec93abdf..00c7cedce4 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 e796353cec..3fd4b04ac8 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, reference_only_path: false), @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 0000000000..55efad384a
--- /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 0000000000..f8b70f7a5a
--- /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 0000000000..f9525d84fa
--- /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 0000000000..6c19a288d1
--- /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 0000000000..163e88bfea
--- /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 0000000000..28ce4819b1
--- /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 d4d413b5b4..53a068be52 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, reference_only_path: false), @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 60e33227e0..5b7dd117c1 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, reference_only_path: false), @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 0000000000..7e58d30b10
--- /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 0000000000..fcbe752114
--- /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 0000000000..c2d7e6f6e3
--- /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 0000000000..484687fa51
--- /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 0000000000..79eb89616d
--- /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 0000000000..e070627211
--- /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 bbf7004c90..a374a66233 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 9d8f33cbba..09f290429e 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 5a501e4314..6c745e69e4 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -41,6 +41,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"
diff --git a/app/views/projects/_dropdown.html.haml b/app/views/projects/_dropdown.html.haml
index 3adb308728..3036f11bb2 100644
--- a/app/views/projects/_dropdown.html.haml
+++ b/app/views/projects/_dropdown.html.haml
@@ -6,28 +6,32 @@
- if @project.issues_enabled && can?(current_user, :write_issue, @project)
%li
= 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/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 0de8c509f2..4e7415be4a 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/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml
index a2faa9d5e2..c577dfa8d5 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 c02a18146e..472a13a852 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 2b9b6599a7..b49aee504f 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 e691db9c08..e6dfbfd651 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 4a5d09b950..78b4c1923d 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 0d3028d50b..288b48f458 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 52e3805041..9228074d83 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)
- = users_select_tag('issue[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @issue.assignee_id, null_user: true)
+ = 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
diff --git a/app/views/projects/merge_requests/show/_participants.html.haml b/app/views/projects/merge_requests/show/_participants.html.haml
index 4f34af1737..9c93fa55fe 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/milestones/_issue.html.haml b/app/views/projects/milestones/_issue.html.haml
index 26c83841a2..88fccfe498 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 42fbd0cd2c..0d7a118569 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/new.html.haml b/app/views/projects/new.html.haml
index 69909a8554..a06c85b425 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 3561ca49f8..b8068835b3 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 be96c30214..2ada6cb670 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 71bdf5c8f2..0728f8fa42 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 5daae2708e..d708b01a11 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', scope: :all)
+ .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 a07d076233..635e4d7094 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/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml
index 5dd32cfcaf..792de5a3d2 100644
--- a/app/views/projects/project_members/_shared_group_members.html.haml
+++ b/app/views/projects/project_members/_shared_group_members.html.haml
@@ -8,7 +8,7 @@
group, members with
%strong #{group_links.human_access}
role (#{shared_group_users_count})
- - if current_user.can?(:manage_group, shared_group)
+ - if current_user.can?(:admin_group, shared_group)
.panel-head-actions
= link_to group_group_members_path(shared_group), class: 'btn btn-sm' do
%i.fa.fa-pencil-square-o
diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml
index 1ba7a1f206..b9486a9b49 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 btn-sm', 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-sm.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 85113ffa7e..4464c51744 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,25 +60,19 @@
- 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.append-bottom-10
%p
@@ -79,11 +85,6 @@
- else
#{link_to @project.owner_name, @project.owner}
- - unless @project.empty_repo?
- - if can? current_user, :download_code, @project
- %hr
- .prepend-top-10.append-bottom-10
- = render 'projects/repositories/download_archive', split_button: true
.prepend-top-10
- @project.ci_services.each do |ci_service|
@@ -96,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
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index f22308e54b..28ad272322 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/shared/_issuable_filter.html.haml b/app/views/shared/_issuable_filter.html.haml
index f169733f2e..83f5a3a801 100644
--- a/app/views/shared/_issuable_filter.html.haml
+++ b/app/views/shared/_issuable_filter.html.haml
@@ -24,11 +24,11 @@
.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)
+ 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)
+ 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')
diff --git a/app/views/shared/_project.html.haml b/app/views/shared/_project.html.haml
index 8746970c23..722a7f7ce0 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 55a990c94e..5204fb9a90 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 bca7144495..90d9980c85 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/workers/fork_registration_worker.rb b/app/workers/fork_registration_worker.rb
new file mode 100644
index 0000000000..fffa8b3a65
--- /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 0c3ee6ba4f..33d8cc8861 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_import_worker.rb b/app/workers/repository_import_worker.rb
index 437640d230..e6a50afedb 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/config/gitlab.yml.example b/config/gitlab.yml.example
index a05e7d99f4..66f13a329c 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
@@ -112,6 +113,15 @@ production: &base
schedule_sync_minute: 30 # Minute of the hour. Value from 0-59.
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
#
@@ -144,6 +154,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 139ea43799..7cbf635c81 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
@@ -66,18 +68,18 @@ Settings.ldap['schedule_sync_minute'] = 30 if Settings.ldap['schedule_sync_minu
# 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 = Settingslogic.new(server)
- server['label'] = 'LDAP'
- server['provider_name'] = 'ldap'
Settings.ldap['servers'] = {
- 'ldap' => server
+ 'main' => server
}
end
Settings.ldap['servers'].each do |key, server|
server = Settingslogic.new(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
@@ -87,6 +89,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'] ||= []
@@ -138,6 +141,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
@@ -154,6 +158,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 +)?)|([A-Z]*-\d*))+)' 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 655590dff0..688cdf5f4b 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/4_sidekiq.rb b/config/initializers/4_sidekiq.rb
index da2e8a08b0..e856499732 100644
--- a/config/initializers/4_sidekiq.rb
+++ b/config/initializers/4_sidekiq.rb
@@ -25,5 +25,3 @@ Sidekiq.configure_client do |config|
namespace: 'resque:gitlab'
}
end
-
-Sidekiq::Queue["archive_repo"].limit = 2
diff --git a/config/initializers/5_backend.rb b/config/initializers/5_backend.rb
index 7c2e7f3900..80d641d73a 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/devise.rb b/config/initializers/devise.rb
index 73e947c88b..94b9fd4477 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::LDAP::Config.servers.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/routes.rb b/config/routes.rb
index 8567e3b8a2..cf1b3684d9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -53,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
@@ -81,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
#
@@ -91,7 +110,7 @@ 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: /[^\/]+/ }
# Appearance
get ":model/:mounted_as/:id/:filename",
@@ -101,13 +120,13 @@ Gitlab::Application.routes.draw do
# 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
@@ -152,6 +171,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
@@ -272,6 +293,7 @@ Gitlab::Application.routes.draw do
resources :ldap_group_links, only: [:index, :create, :destroy]
resources :group_members, only: [:index, :create, :update, :destroy] do
+ post :resend_invite, on: :member
delete :leave, on: :collection
end
@@ -426,7 +448,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
@@ -508,6 +530,10 @@ Gitlab::Application.routes.draw do
get :import
post :apply_import
end
+
+ member do
+ post :resend_invite
+ end
end
resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
@@ -520,7 +546,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/20150327122227_add_public_to_key.rb b/db/migrate/20150327122227_add_public_to_key.rb
new file mode 100644
index 0000000000..6ffbf4cda1
--- /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 0000000000..12c00339ee
--- /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 0000000000..1d161674a9
--- /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 0000000000..3452fd45c4
--- /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 0000000000..d9051f9fff
--- /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 0000000000..5a0b5d07e5
--- /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 0000000000..700e9f343a
--- /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/schema.rb b/db/schema.rb
index 90be1e860b..e2a5be5733 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: 20150324223425) do
+ActiveRecord::Schema.define(version: 20150413192223) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -40,6 +40,7 @@ ActiveRecord::Schema.define(version: 20150324223425) do
t.boolean "twitter_sharing_enabled", default: true
t.text "help_text"
t.text "restricted_visibility_levels"
+ t.integer "max_attachment_size", default: 10, null: false
end
create_table "audit_events", force: true do |t|
@@ -171,6 +172,7 @@ ActiveRecord::Schema.define(version: 20150324223425) 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
@@ -210,15 +212,20 @@ ActiveRecord::Schema.define(version: 20150324223425) 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
@@ -402,6 +409,7 @@ ActiveRecord::Schema.define(version: 20150324223425) do
t.string "import_type"
t.string "import_source"
t.boolean "merge_requests_rebase_default", default: true
+ t.text "import_data"
end
add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree
@@ -536,6 +544,7 @@ ActiveRecord::Schema.define(version: 20150324223425) 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 d842245bcc..afac07a4e1 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/integration/ldap.md b/doc/integration/ldap.md
index 7c17a3a985..abd8932a9a 100644
--- a/doc/integration/ldap.md
+++ b/doc/integration/ldap.md
@@ -55,6 +55,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 965d8fc313..1d5fd4c8b0 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
@@ -441,8 +441,6 @@ Note that inline HTML is disabled in the default Gitlab configuration, although
Does *not* work **very** well. Use HTML tags.
-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/release/patch.md b/doc/release/patch.md
index 68156ae9c0..4c7b471785 100644
--- a/doc/release/patch.md
+++ b/doc/release/patch.md
@@ -35,18 +35,15 @@ 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.
+
+Create release tag and push to remotes:
```
bundle exec rake release["x.x.x"]
```
-Or if you need to release only EE:
-
-```
-CE=false be rake release['x.x.x']
-```
-
### Release
1. [Build new packages with the latest version](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md)
diff --git a/doc/update/6.6-to-6.7.md b/doc/update/6.6-to-6.7.md
index 5622a7001e..b4298c9342 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/workflow/README.md b/doc/workflow/README.md
index 948567b79f..fc45a1dd04 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)
- [Rebase before merge](rebase_before_merge.md)
diff --git a/doc/workflow/forking/branch_select.png b/doc/workflow/forking/branch_select.png
new file mode 100644
index 0000000000..275f64d113
Binary files /dev/null and b/doc/workflow/forking/branch_select.png differ
diff --git a/doc/workflow/forking/fork_button.png b/doc/workflow/forking/fork_button.png
new file mode 100644
index 0000000000..def4266476
Binary files /dev/null and b/doc/workflow/forking/fork_button.png differ
diff --git a/doc/workflow/forking/groups.png b/doc/workflow/forking/groups.png
new file mode 100644
index 0000000000..3ac64b3c8e
Binary files /dev/null and b/doc/workflow/forking/groups.png differ
diff --git a/doc/workflow/forking/merge_request.png b/doc/workflow/forking/merge_request.png
new file mode 100644
index 0000000000..2dc00ed08a
Binary files /dev/null and b/doc/workflow/forking/merge_request.png differ
diff --git a/doc/workflow/forking_workflow.md b/doc/workflow/forking_workflow.md
new file mode 100644
index 0000000000..8edf7c6ab3
--- /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 f0a8b9f53d..bb25bb677c 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.1-omnibus.1-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 0000000000..9df47eb51f
--- /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/groups.feature b/features/groups.feature
index 4e675efffd..bff158adc2 100644
--- a/features/groups.feature
+++ b/features/groups.feature
@@ -62,6 +62,13 @@ Feature: Groups
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 0000000000..dc8eefaeae
--- /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 13e3b9bbd2..a71f6124d9 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/issues.feature b/features/project/issues/issues.feature
index b9031f6f32..eb813884d1 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/team_management.feature b/features/project/team_management.feature
index 9d2359a3a7..2ecaa5d694 100644
--- a/features/project/team_management.feature
+++ b/features/project/team_management.feature
@@ -17,6 +17,12 @@ Feature: Project Team Management
And I select "Mike" as "Reporter"
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"
diff --git a/features/project/wiki.feature b/features/project/wiki.feature
index 4a8c771dda..977cd609a1 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 0000000000..fb0b611762
--- /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/groups.rb b/features/steps/groups.rb
index d0b334e1ed..ac0ed5c763 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -31,6 +31,23 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
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 0000000000..d051cc3edc
--- /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 4bf5cb5fa4..50e14513a7 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/issues.rb b/features/steps/project/issues/issues.rb
index e8ca3f7c17..b8e282b202 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 980442f45e..4ab2a928e5 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/team_management.rb b/features/steps/project/team_management.rb
index aca99d3f92..1f2e863f8c 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 cd7d5eac24..bb93e582a1 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 78a2c07370..1154240e57 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/entities.rb b/lib/api/entities.rb
index 73b410a2ed..75f2f5fa2d 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -51,7 +51,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
@@ -59,6 +59,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 ed54c7f6ff..ab9b7c602b 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 c33ac8217b..84df6d7249 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -70,7 +70,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/file_size_validator.rb b/lib/file_size_validator.rb
index 42970c1be5..2eae55e534 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 0000000000..5fc1862c3e
--- /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 aabc7f1e69..530f9d93de 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 db33af2c2d..54420e62c9 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 a9fd59f03d..ab184d95c0 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
similarity index 100%
rename from lib/gitlab/contributors.rb
rename to lib/gitlab/contributor.rb
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 0ebebfa09c..d8f696d247 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 9439ca6cbf..2723eec933 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/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb
index 6424d56f8f..f0d7141bf5 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 5043f6a2eb..8cdc3d4afa 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 3cbebe5399..cc9a91c91f 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 0000000000..f702797dc6
--- /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 0000000000..02f31e45f8
--- /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 0000000000..777472cf3c
--- /dev/null
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -0,0 +1,365 @@
+module Gitlab
+ module GoogleCodeImport
+ class Importer
+ attr_reader :project, :repo
+
+ def initialize(project)
+ @project = project
+ @repo = GoogleCodeImport::Repository.new(project.import_data["repo"])
+
+ @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| hash[user] = Client.mask_email(user) }
+
+ stored_user_map = project.import_data["user_map"]
+ 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
+
+ last_id = 0
+
+ deleted_issues = []
+
+ repo.issues.each do |raw_issue|
+ while raw_issue["id"] > last_id + 1
+ last_id += 1
+
+ issue = project.issues.create!(
+ title: "Deleted issue",
+ description: "*This issue has been deleted*",
+ author_id: project.creator_id,
+ state: "closed"
+ )
+ deleted_issues << issue
+ end
+ last_id = raw_issue["id"]
+
+ 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 = project.issues.create!(
+ 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)
+
+ import_issue_comments(issue, comments)
+ end
+
+ deleted_issues.each(&:destroy!)
+ end
+
+ def import_issue_comments(issue, comments)
+ comments.each do |raw_comment|
+ 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
+ )
+
+ issue.notes.create!(
+ project_id: project.id,
+ author_id: project.creator_id,
+ note: body
+ )
+ 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)
+ project.labels.create!(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 0000000000..7ac4387d79
--- /dev/null
+++ b/lib/gitlab/google_code_import/project_creator.rb
@@ -0,0 +1,46 @@
+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
+ import_data = {
+ "repo" => repo.raw_data,
+ "user_map" => user_map
+ }
+
+ @project = Project.new(
+ 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,
+ import_data: import_data
+ )
+
+ if @project.save!
+ @project.reload
+
+ if @project.import_failed?
+ @project.import_retry
+ else
+ @project.import_start
+ end
+ end
+
+ @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 0000000000..ad33fc2cad
--- /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 0000000000..baf52ff750
--- /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:)?(?(\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_(?\d+)\.(?\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 b3bb9d8980..94acd1c3d9 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -31,8 +31,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
@@ -91,6 +89,10 @@ module Gitlab
options['active_directory']
end
+ def block_auto_created_users
+ options['block_auto_created_users']
+ end
+
protected
def base_config
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index cfa8692659..f7f3ba9ad7 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 f1e2ae74a3..8073417a16 100644
--- a/lib/gitlab/markdown.rb
+++ b/lib/gitlab/markdown.rb
@@ -192,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
@@ -251,6 +252,7 @@ module Gitlab
elsif namespace = Namespace.find_by(path: identifier)
url =
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])
diff --git a/lib/gitlab/oauth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb
similarity index 100%
rename from lib/gitlab/oauth/auth_hash.rb
rename to lib/gitlab/o_auth/auth_hash.rb
diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/o_auth/user.rb
similarity index 100%
rename from lib/gitlab/oauth/user.rb
rename to lib/gitlab/o_auth/user.rb
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 74ef6ea9ab..40a6238e89 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -1,96 +1,96 @@
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{
.*?
|.*?}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?)
- end
-
- def labels_for(project = nil)
- labels.map do |entry|
- project.labels.where(id: entry[:id]).first
- end.reject(&:nil?)
- end
-
- def issues_for(project = nil)
- issues.uniq.map do |entry|
- if should_lookup?(project, entry[:project])
- entry[:project].issues.where(iid: entry[:id]).first
- elsif entry[:project] && entry[:project].jira_tracker?
- JiraIssue.new(entry[:id], entry[:project])
+ 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.reject(&:nil?)
+ end.flatten.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
+ def labels
+ references[:label].uniq.map do |project, identifier|
+ project.labels.where(id: identifier).first
+ end.compact.uniq
+ end
+
+ def issues
+ references[:issue].uniq.map do |project, identifier|
+ if project.default_issues_tracker?
+ project.issues.where(iid: identifier).first
+ elsif project.jira_tracker?
+ JiraIssue.new(identifier, project)
end
- end.reject(&:nil?)
+ end.compact.uniq
end
- def snippets_for(project)
- snippets.map do |entry|
- project.snippets.where(id: entry[:id]).first
- 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 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 snippets
+ references[:snippet].uniq.map do |project, identifier|
+ project.snippets.where(id: identifier).first
+ 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 commits
+ references[:commit].uniq.map do |project, identifier|
+ repo = project.repository
+ repo.commit(identifier) if repo
+ end.compact.uniq
+ end
+
+ 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/satellite/satellite.rb b/lib/gitlab/satellite/satellite.rb
index f24c6199c4..398643d68d 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/spec/controllers/import/google_code_controller_spec.rb b/spec/controllers/import/google_code_controller_spec.rb
new file mode 100644
index 0000000000..037cddb460
--- /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/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
new file mode 100644
index 0000000000..c254ab7cb6
--- /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/factories.rb b/spec/factories.rb
index 784b112bf4..759a502a97 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 c8e218d4d0..457859deda 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/users_spec.rb b/spec/features/users_spec.rb
index 21a3a4bf93..4cfaab03ca 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 0000000000..d05e77271a
--- /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). 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 4c11709ed6..015a66f7fa 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("Home")
+ expect(link_to('Home', root_path)).to eq('Home')
end
it 'should include rel=nofollow for external links' do
- expect(link_to('Example', 'http://www.example.com')).to eq("Example")
+ expect(link_to('Example', 'http://www.example.com')).
+ to eq 'Example'
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("Example")
+ 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 'Example'
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("Example")
+ 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 'Example'
+ end
+
+ 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 'Example'
end
end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index e3f87dc89f..900b67710a 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
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index aef1108e33..e99c3f5bc1 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 0000000000..5c89c85471
--- /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 f552310584..0ec6a43f68 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 c96ee78e5f..cb7b0fbb89 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 8d594a112d..3bf52cb685 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 4c0d64ed13..3cefe4ea8e 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
similarity index 85%
rename from spec/lib/gitlab/gitorious_import/project_creator.rb
rename to spec/lib/gitlab/gitorious_import/project_creator_spec.rb
index cf2318bb3a..c1125ca635 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 0000000000..d2bf871daa
--- /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 0000000000..107ba49962
--- /dev/null
+++ b/spec/lib/gitlab/google_code_import/importer_spec.rb
@@ -0,0 +1,80 @@
+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, import_data: import_data) }
+ subject { described_class.new(project) }
+
+ 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 0000000000..7a224538b8
--- /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 0000000000..266eab6e79
--- /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 2df2beca7a..00e9076c78 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 4f93545feb..42015c28c8 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
similarity index 100%
rename from spec/lib/gitlab/oauth/auth_hash_spec.rb
rename to 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
similarity index 100%
rename from spec/lib/gitlab/oauth/user_spec.rb
rename to 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 9620d90478..58bf251cd5 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
..
blocks' do
- subject.analyze("
def puts '#1 issue'\nend\n
```", nil)
+ subject.analyze("
def puts '#1 issue'\nend\n
```")
expect(subject.issues).to be_blank
end
it 'extracts no references for .. blocks' do
- subject.analyze("def puts '!1 request'\nend\n```", nil)
+ subject.analyze("def puts '!1 request'\nend\n```")
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,93 +78,89 @@ 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' do
+ @u_foo = create(:user, username: 'foo')
+ @u_bar = create(:user, username: 'bar')
+ @u_offteam = create(:user, username: 'offteam')
- it 'accesses valid user objects on the project team' do
- @u_foo = create(:user, username: 'foo')
- @u_bar = create(:user, username: 'bar')
- create(:user, username: 'offteam')
+ project.team << [@u_foo, :reporter]
+ project.team << [@u_bar, :guest]
- project.team << [@u_foo, :reporter]
- project.team << [@u_bar, :guest]
+ subject.analyze('@foo, @baduser, @bar, and @offteam')
+ expect(subject.users).to eq([@u_foo, @u_bar, @u_offteam])
+ end
- subject.analyze('@foo, @baduser, @bar, and @offteam', project)
- expect(subject.users_for(project)).to eq([@u_foo, @u_bar])
- 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.")
+ expect(subject.issues).to eq([@i0, @i1])
+ end
- subject.analyze("##{@i0.iid}, ##{@i1.iid}, and #999.", project)
- expect(subject.issues_for(project)).to eq([@i0, @i1])
- end
+ it 'returns JIRA issues for a JIRA-integrated project' do
+ project.stub(jira_tracker?: true)
+ project.stub(default_issues_tracker?: false)
- it 'returns JIRA issues for a JIRA-integrated project' do
- project.stub(jira_tracker?: true)
- project.stub(default_issues_tracker?: false)
+ subject.analyze('JIRA-123 and FOOBAR-4567')
+ subject.issues.should eq(
+ [JiraIssue.new('JIRA-123', project), JiraIssue.new('FOOBAR-4567', project)]
+ )
+ end
- subject.analyze('JIRA-123 and FOOBAR-4567', project)
- subject.issues_for(project).should eq(
- [JiraIssue.new('JIRA-123', project), JiraIssue.new('FOOBAR-4567', project)]
- )
- 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}.")
+ expect(subject.merge_requests).to eq([@m1, @m0])
+ end
- subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.", project)
- expect(subject.merge_requests_for(project)).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}")
+ expect(subject.snippets).to eq([@s0, @s1])
+ end
- subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}", project)
- expect(subject.snippets_for(project)).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")
+ 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
- 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
+ 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/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index de496d0e0b..e4900e4761 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 8b3d88640d..11cc7762ce 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 f351aab923..7032b77714 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 a212b95a7d..2fb651bef1 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 0000000000..56d030a03b
--- /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 dfd3f7feb6..0000000000
--- 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/project_services/buildbox_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
similarity index 97%
rename from spec/models/project_services/buildbox_service_spec.rb
rename to spec/models/project_services/buildkite_service_spec.rb
index 9f29fbe12b..e987241f3c 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 610f33c582..6a557d839c 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/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index e99ee93eaa..1f25136891 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 }
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 729970153d..09a79553f7 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/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index d9bd91f599..042352311d 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/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index e55a2e3f8a..c9025bdf13 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 fc34b45648..e5c47015a0 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/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 60144be551..a59f74c212 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].*(db|uploads|repositories)\/$/)
+ expect(tar_contents).not_to match(/^.{4,9}[rwx].* (db|uploads|repositories)\/$/)
end
it 'should delete temp directories' do
diff --git a/spec/workers/fork_registration_worker_spec.rb b/spec/workers/fork_registration_worker_spec.rb
new file mode 100644
index 0000000000..cc6f574b29
--- /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/vendor/assets/javascripts/chart-lib.min.js b/vendor/assets/javascripts/chart-lib.min.js
index 626e6c3cdb..3a0a2c8734 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=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),st?-.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)=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(),tthis.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;ip&&(p=t.x+s,n=i),t.x-sp&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e0){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=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:'
<%}%>'},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