From 5f682094d9b7c985ad62ebe29664bb6fe87b54be Mon Sep 17 00:00:00 2001
From: Matthew Monaco
Date: Wed, 18 Jun 2014 11:49:39 -0600
Subject: [PATCH 001/109] Add 'confirm' option to users api
---
CHANGELOG | 1 +
doc/api/users.md | 1 +
lib/api/users.rb | 5 ++++-
3 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG b/CHANGELOG
index 5dab8c864e..5298e137d6 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -8,6 +8,7 @@ v 7.5.0
- Tie up loose ends with annotated tags: API & UI (Sean Edge)
- Return valid json for deleting branch via API (sponsored by O'Reilly Media)
- Expose username in project events API (sponsored by O'Reilly Media)
+ - Allow user confirmation to be skipped for new users via API
v 7.4.2
- Fix internal snippet exposing for unauthenticated users
diff --git a/doc/api/users.md b/doc/api/users.md
index 3fdd3a75e8..fec5deebee 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -168,6 +168,7 @@ Parameters:
- `bio` (optional) - User's biography
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
+- `confirm` (optional) - Require confirmation - true (default) or false
## User modification
diff --git a/lib/api/users.rb b/lib/api/users.rb
index d07815a8a9..1a4a8535d4 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -54,15 +54,18 @@ module API
# bio - Bio
# admin - User is admin - true or false (default)
# can_create_group - User can create groups - true or false
+ # confirm - Require user confirmation - true (default) or false
# Example Request:
# POST /users
post do
authenticated_as_admin!
required_attributes! [:email, :password, :name, :username]
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :extern_uid, :provider, :bio, :can_create_group, :admin]
+ attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :extern_uid, :provider, :bio, :can_create_group, :confirm, :admin]
user = User.build_user(attrs)
admin = attrs.delete(:admin)
user.admin = admin unless admin.nil?
+ confirm = ! (attrs.delete(:confirm) =~ (/(false|f|no|0)$/i))
+ user.skip_confirmation! unless confirm
if user.save
present user, with: Entities::UserFull
else
From d0f6654e9ced38d03f5946fde10c3f0e3732a5d7 Mon Sep 17 00:00:00 2001
From: Alexander Balashov
Date: Tue, 18 Nov 2014 10:12:05 +0300
Subject: [PATCH 002/109] Remove useless `assets.compress` option, Rails 4 uses
only `assets.js_compressor`
> The config.assets.compress option should be changed to config.assets.js_compressor like so for instance
---
config/environments/production.rb | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 78bf543402..3316ece387 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -11,8 +11,9 @@ Gitlab::Application.configure do
# Disable Rails's static asset server (Apache or nginx will already do this)
config.serve_static_assets = false
- # Compress JavaScripts and CSS
- config.assets.compress = true
+ # Compress JavaScripts and CSS.
+ config.assets.js_compressor = :uglifier
+ # config.assets.css_compressor = :sass
# Don't fallback to assets pipeline if a precompiled asset is missed
config.assets.compile = true
@@ -74,7 +75,6 @@ Gitlab::Application.configure do
config.action_mailer.raise_delivery_errors = true
config.eager_load = true
- config.assets.js_compressor = :uglifier
config.allow_concurrency = false
end
From d37cf2a23da0fcbaca695092822e9cd2af4b1bdb Mon Sep 17 00:00:00 2001
From: Ciro Santilli
Date: Sun, 19 Oct 2014 10:02:13 +0200
Subject: [PATCH 003/109] Factor permission check in issuable finder
---
app/finders/issuable_finder.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index e147751006..088a766ed3 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -44,7 +44,7 @@ class IssuableFinder
table_name = klass.table_name
if project
- if project.public? || (current_user && current_user.can?(:read_project, project))
+ if Ability.abilities.allowed?(current_user, :read_project, project)
project.send(table_name)
else
[]
From a3a66939cacd7ffd6aa806fdb0d973ab7c1eb09c Mon Sep 17 00:00:00 2001
From: Ben Bodenmiller
Date: Sun, 8 Feb 2015 23:27:51 -0800
Subject: [PATCH 004/109] formatting cleanup on mysql_to_postgresql.md
---
doc/update/mysql_to_postgresql.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md
index 229689392b..8e65cb3390 100644
--- a/doc/update/mysql_to_postgresql.md
+++ b/doc/update/mysql_to_postgresql.md
@@ -75,7 +75,7 @@ test -e /opt/gitlab/embedded/service/gitlab-rails/db/schema.rb.bundled || sudo /
## Converting a GitLab backup file from MySQL to Postgres
**Note:** Please make sure to have Python 2.7.x (or higher) installed.
-GitLab backup files (_gitlab_backup.tar) contain a SQL dump. Using the lanyrd database converter we can replace a MySQL database dump inside the tar file with a Postgres database dump. This can be useful if you are moving to another server.
+GitLab backup files (`_gitlab_backup.tar`) contain a SQL dump. Using the lanyrd database converter we can replace a MySQL database dump inside the tar file with a Postgres database dump. This can be useful if you are moving to another server.
```
# Stop GitLab
From 692bb679d0ade7401e6a422e2faeb04b63bd23c2 Mon Sep 17 00:00:00 2001
From: Ben Bodenmiller
Date: Sun, 8 Feb 2015 23:51:37 -0800
Subject: [PATCH 005/109] use gitlab branch of mysql-postgresql-converter
---
doc/update/mysql_to_postgresql.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md
index 229689392b..ba8259d582 100644
--- a/doc/update/mysql_to_postgresql.md
+++ b/doc/update/mysql_to_postgresql.md
@@ -11,7 +11,7 @@ sudo service gitlab stop
# Update /home/git/gitlab/config/database.yml
-git clone https://github.com/gitlabhq/mysql-postgresql-converter.git
+git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab
cd mysql-postgresql-converter
mysqldump --compatible=postgresql --default-character-set=utf8 -r databasename.mysql -u root gitlabhq_production -p
python db_converter.py databasename.mysql databasename.psql
@@ -38,7 +38,7 @@ On non-omnibus installations (distributed using Git) we retrieve the index decla
```
# Clone the database converter on your Postgres-backed GitLab server
cd /tmp
-git clone https://github.com/gitlabhq/mysql-postgresql-converter.git
+git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab
cd /home/git/gitlab
@@ -59,7 +59,7 @@ On omnibus-gitlab we need to get the index declarations from a file called `sche
```
# Clone the database converter on your Postgres-backed GitLab server
cd /tmp
-/opt/gitlab/embedded/bin/git clone https://github.com/gitlabhq/mysql-postgresql-converter.git
+/opt/gitlab/embedded/bin/git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab
cd /tmp/mysql-postgresql-converter
# Download schema.rb.bundled if necessary
@@ -97,7 +97,7 @@ cd tmp/backups/postgresql
sudo -u git -H mysqldump --compatible=postgresql --default-character-set=utf8 -r gitlabhq_production.mysql -u root gitlabhq_production -p
# Clone the database converter
-sudo -u git -H git clone https://github.com/gitlabhq/mysql-postgresql-converter.git
+sudo -u git -H git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab
# Convert gitlabhq_production.mysql
sudo -u git -H mkdir db
From 252ee4e7e51ae18f30f4b9089be850daaca958ac Mon Sep 17 00:00:00 2001
From: Aaron Stone
Date: Mon, 11 Aug 2014 11:06:15 -0700
Subject: [PATCH 006/109] Improve login screen when only OmniAuth providers are
enabled
Avoids an empty Sign in box when signup_enabled? is false, and avoids
showing "No authentication methods configured" unless there really are none.
OmniAuth signin gets its own file for consistency with signin and signup and LDAP.
---
app/views/devise/sessions/new.html.haml | 16 ++++++++++++++--
app/views/devise/shared/_omniauth_box.html.haml | 10 ++++++++++
app/views/devise/shared/_signin_box.html.haml | 16 ----------------
3 files changed, 24 insertions(+), 18 deletions(-)
create mode 100644 app/views/devise/shared/_omniauth_box.html.haml
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index fa2460518f..89e4e229ac 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -1,6 +1,18 @@
%div
- = render 'devise/shared/signin_box'
+ - if signin_enabled? || ldap_enabled?
+ = render 'devise/shared/signin_box'
- - if signup_enabled?
+ -# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box
+ - if Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?
+ .clearfix.prepend-top-20
+ = render 'devise/shared/omniauth_box'
+
+ -# Signup only makes sense if you can also sign-in
+ - if signin_enabled? && signup_enabled?
.prepend-top-20
= render 'devise/shared/signup_box'
+
+ -# Show a message if none of the mechanisms above are enabled
+ - if !signin_enabled? && !ldap_enabled? && !(Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?)
+ %div
+ No authentication methods configured.
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
new file mode 100644
index 0000000000..4cd1c303b2
--- /dev/null
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -0,0 +1,10 @@
+%p
+ %span.light
+ Sign in with
+ - providers = additional_providers
+ - providers.each do |provider|
+ %span.light
+ - if default_providers.include?(provider)
+ = link_to authbutton(provider, 32), omniauth_authorize_path(resource_name, provider)
+ - else
+ = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), class: "btn"
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index 805cf81623..8faa6398a6 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -24,19 +24,3 @@
- elsif signin_enabled?
= render 'devise/sessions/new_base'
- - else
- %div
- No authentication methods configured.
-
-- if Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?
- .clearfix.prepend-top-20
- %p
- %span.light
- Sign in with
- - providers = additional_providers
- - providers.each do |provider|
- %span.light
- - if default_providers.include?(provider)
- = link_to authbutton(provider, 32), omniauth_authorize_path(resource_name, provider)
- - else
- = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), class: "btn"
\ No newline at end of file
From b86caf0de317cfc4012f10d30a2e15eef471948e Mon Sep 17 00:00:00 2001
From: Bugagazavr
Date: Mon, 16 Feb 2015 02:10:47 +0300
Subject: [PATCH 007/109] Correct json payload [ci skip]
---
doc/api/repositories.md | 61 ++++++++++++++++++++---------------------
1 file changed, 29 insertions(+), 32 deletions(-)
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index 8acf85d21c..3316745380 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -15,24 +15,21 @@ Parameters:
```json
[
{
- "name": "v1.0.0",
"commit": {
- "id": "2695effb5807a22ff3d138d593fd856244e155e7",
- "parents": [],
- "tree": "38017f2f189336fe4497e9d230c5bb1bf873f08d",
- "message": "Initial commit",
- "author": {
- "name": "John Smith",
- "email": "john@example.com"
- },
- "committer": {
- "name": "Jack Smith",
- "email": "jack@example.com"
- },
+ "author_name": "John Smith",
+ "author_email": "john@example.com",
"authored_date": "2012-05-28T04:42:42-07:00",
- "committed_date": "2012-05-28T04:42:42-07:00"
+ "committed_date": "2012-05-28T04:42:42-07:00",
+ "committer_name": "Jack Smith",
+ "committer_email": "jack@example.com",
+ "id": "2695effb5807a22ff3d138d593fd856244e155e7",
+ "message": "Initial commit",
+ "parents_ids": [
+ "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
+ ]
},
- "protected": null
+ "name": "v1.0.0",
+ "message": null
}
]
```
@@ -53,23 +50,23 @@ Parameters:
- `message` (optional) - Creates annotated tag.
```json
-[
- {
- "name": "v1.0.0",
- "message": "Release 1.0.0",
- "commit": {
- "id": "2695effb5807a22ff3d138d593fd856244e155e7",
- "parents": [],
- "message": "Initial commit",
- "authored_date": "2012-05-28T04:42:42-07:00",
- "author_name": "John Smith",
- "author email": "john@example.com",
- "committer_name": "Jack Smith",
- "committed_date": "2012-05-28T04:42:42-07:00",
- "committer_email": "jack@example.com"
- },
- }
-]
+{
+ "commit": {
+ "author_name": "John Smith",
+ "author_email": "john@example.com",
+ "authored_date": "2012-05-28T04:42:42-07:00",
+ "committed_date": "2012-05-28T04:42:42-07:00",
+ "committer_name": "Jack Smith",
+ "committer_email": "jack@example.com",
+ "id": "2695effb5807a22ff3d138d593fd856244e155e7",
+ "message": "Initial commit",
+ "parents_ids": [
+ "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
+ ]
+ },
+ "name": "v1.0.0",
+ "message": null
+}
```
The message will be `nil` when creating a lightweight tag otherwise
it will contain the annotation.
From 9bf8480b4a0d3ea6e284c4bd8bf26243f3f3f6f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com>
Date: Sat, 14 Feb 2015 16:04:45 +0100
Subject: [PATCH 008/109] Generalize the image upload in markdown
This commit generalizes the image upload via drag and drop so it supports all files.
It also adds access control for these files.
---
CHANGELOG | 1 +
.../javascripts/dropzone_input.js.coffee | 21 +++--
app/controllers/files_controller.rb | 29 ++++++-
app/controllers/projects_controller.rb | 18 ++---
app/services/projects/file_service.rb | 55 +++++++++++++
app/services/projects/image_service.rb | 39 ---------
app/uploaders/file_uploader.rb | 19 ++++-
app/views/projects/_issuable_form.html.haml | 2 +-
app/views/projects/issues/_form.html.haml | 2 +-
.../projects/merge_requests/_form.html.haml | 2 +-
.../merge_requests/_new_submit.html.haml | 5 +-
app/views/projects/milestones/_form.html.haml | 4 +-
app/views/projects/notes/_edit_form.html.haml | 2 +-
app/views/projects/notes/_form.html.haml | 6 +-
app/views/projects/wikis/_form.html.haml | 5 +-
config/routes.rb | 5 +-
spec/controllers/projects_controller_spec.rb | 48 ++++++-----
spec/services/projects/file_service_spec.rb | 81 +++++++++++++++++++
spec/services/projects/image_service_spec.rb | 62 --------------
19 files changed, 247 insertions(+), 159 deletions(-)
create mode 100644 app/services/projects/file_service.rb
delete mode 100644 app/services/projects/image_service.rb
create mode 100644 spec/services/projects/file_service_spec.rb
delete mode 100644 spec/services/projects/image_service_spec.rb
diff --git a/CHANGELOG b/CHANGELOG
index 05d1e7bdb4..9e50178d8b 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,6 @@
v 7.8.0 (unreleased)
- Fix broken access control for note attachments (Hannes Rosenögger)
+ - Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger)
- Replace highlight.js with rouge-fork rugments (Stefan Tatschner)
- Make project search case insensitive (Hannes Rosenögger)
- Include issue/mr participants in list of recipients for reassign/close/reopen emails
diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee
index d98d548293..bed8471b39 100644
--- a/app/assets/javascripts/dropzone_input.js.coffee
+++ b/app/assets/javascripts/dropzone_input.js.coffee
@@ -9,7 +9,7 @@ class @DropzoneInput
iconPicture = ""
iconSpinner = ""
btnAlert = ""
- project_image_path_upload = window.project_image_path_upload or null
+ project_file_path_upload = window.project_file_path_upload or null
form_textarea = $(form).find("textarea.markdown-area")
form_textarea.wrap ""
@@ -72,13 +72,12 @@ class @DropzoneInput
form.find(".md-preview-holder").hide()
dropzone = form_dropzone.dropzone(
- url: project_image_path_upload
+ url: project_file_path_upload
dictDefaultMessage: ""
clickable: true
- paramName: "markdown_img"
+ paramName: "markdown_file"
maxFilesize: 10
uploadMultiple: false
- acceptedFiles: "image/jpg,image/jpeg,image/gif,image/png"
headers:
"X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
@@ -133,7 +132,10 @@ class @DropzoneInput
child = $(dropzone[0]).children("textarea")
formatLink = (str) ->
- ""
+ text = "[" + str.alt + "](" + str.url + ")"
+ if str.is_image is true
+ text = "!" + text
+ text
handlePaste = (event) ->
pasteEvent = event.originalEvent
@@ -177,9 +179,9 @@ class @DropzoneInput
uploadFile = (item, filename) ->
formData = new FormData()
- formData.append "markdown_img", item, filename
+ formData.append "markdown_file", item, filename
$.ajax
- url: project_image_path_upload
+ url: project_file_path_upload
type: "POST"
data: formData
dataType: "json"
@@ -234,4 +236,7 @@ class @DropzoneInput
return
formatLink: (str) ->
- ""
+ text = "[" + str.alt + "](" + str.url + ")"
+ if str.is_image is true
+ text = "!" + text
+ text
diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb
index 15523cbc2e..a86340dd9b 100644
--- a/app/controllers/files_controller.rb
+++ b/app/controllers/files_controller.rb
@@ -1,5 +1,5 @@
class FilesController < ApplicationController
- def download
+ def download_notes
note = Note.find(params[:id])
uploader = note.attachment
@@ -14,7 +14,32 @@ class FilesController < ApplicationController
not_found!
end
else
- redirect_to uploader.url
+ not_found!
end
end
+
+ def download_files
+ namespace_id = params[:namespace]
+ project_id = params[:project]
+ folder_id = params[:folder_id]
+ filename = params[:filename]
+ project_with_namespace="#{namespace_id}/#{project_id}"
+ filename_with_id="#{folder_id}/#{filename}"
+
+ project = Project.find_with_namespace(project_with_namespace)
+
+ uploader = FileUploader.new("#{Rails.root}/uploads","#{project_with_namespace}/#{folder_id}")
+ uploader.retrieve_from_store!(filename)
+
+ if can?(current_user, :read_project, project)
+ download(uploader)
+ else
+ not_found!
+ end
+ end
+
+ def download(uploader)
+ disposition = uploader.image? ? 'inline' : 'attachment'
+ send_file uploader.file.path, disposition: disposition
+ end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 462ab3d474..b430278903 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -134,12 +134,13 @@ class ProjectsController < ApplicationController
end
end
- def upload_image
- link_to_image = ::Projects::ImageService.new(repository, params, root_url).execute
+ def upload_file
+ link_to_file = ::Projects::FileService.new(repository, params, root_url).
+ execute
respond_to do |format|
- if link_to_image
- format.json { render json: { link: link_to_image } }
+ if link_to_file
+ format.json { render json: { link: link_to_file } }
else
format.json { render json: 'Invalid file.', status: :unprocessable_entity }
end
@@ -158,13 +159,8 @@ class ProjectsController < ApplicationController
private
- def upload_path
- base_dir = FileUploader.generate_dir
- File.join(repository.path_with_namespace, base_dir)
- end
-
- def accepted_images
- %w(png jpg jpeg gif)
+ def invalid_file(error)
+ render json: { message: error.message }, status: :internal_server_error
end
def set_title
diff --git a/app/services/projects/file_service.rb b/app/services/projects/file_service.rb
new file mode 100644
index 0000000000..8c149bf53a
--- /dev/null
+++ b/app/services/projects/file_service.rb
@@ -0,0 +1,55 @@
+module Projects
+ class FileService < BaseService
+ include Rails.application.routes.url_helpers
+ def initialize(repository, params, root_url)
+ @repository, @params, @root_url = repository, params.dup, root_url
+ end
+
+ def execute
+ uploader = FileUploader.new("#{Rails.root}/uploads", upload_path, accepted_files)
+ file = @params['markdown_file']
+
+ if file
+ alt = file.original_filename
+ uploader.store!(file)
+ filename = nil
+ if image?(file)
+ filename=File.basename(alt, '.*')
+ else
+ filename=File.basename(alt)
+ end
+ link = {
+ 'alt' => filename,
+ 'url' => uploader.secure_url,
+ 'is_image' => image?(file)
+ }
+ else
+ link = nil
+ end
+ end
+
+ protected
+
+ def accepted_files
+ # insert accepted mime types here (e.g %w(jpg jpeg gif png))
+ nil
+ end
+
+ def accepted_images
+ %w(jpg jpeg gif png)
+ end
+
+ def image?(file)
+ accepted_images.map { |format| file.content_type.include? format }.any?
+ end
+
+ def upload_path
+ base_dir = FileUploader.generate_dir
+ File.join(@repository.path_with_namespace, base_dir)
+ end
+
+ def correct_mime_type?(file)
+ accepted_files.map { |format| image.content_type.include? format }.any?
+ end
+ end
+end
diff --git a/app/services/projects/image_service.rb b/app/services/projects/image_service.rb
deleted file mode 100644
index 7ca7e82c4a..0000000000
--- a/app/services/projects/image_service.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-module Projects
- class ImageService < BaseService
- include Rails.application.routes.url_helpers
- def initialize(repository, params, root_url)
- @repository, @params, @root_url = repository, params.dup, root_url
- end
-
- def execute
- uploader = FileUploader.new('uploads', upload_path, accepted_images)
- image = @params['markdown_img']
-
- if image && correct_mime_type?(image)
- alt = image.original_filename
- uploader.store!(image)
- link = {
- 'alt' => File.basename(alt, '.*'),
- 'url' => File.join(@root_url, uploader.url)
- }
- else
- link = nil
- end
- end
-
- protected
-
- def upload_path
- base_dir = FileUploader.generate_dir
- File.join(@repository.path_with_namespace, base_dir)
- end
-
- def accepted_images
- %w(png jpg jpeg gif)
- end
-
- def correct_mime_type?(image)
- accepted_images.map{ |format| image.content_type.include? format }.any?
- end
- end
-end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 0fa987c93f..ac7bd5b27e 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -21,7 +21,7 @@ class FileUploader < CarrierWave::Uploader::Base
end
def extension_white_list
- @allowed_extensions
+ @allowed_extensions || super
end
def store!(file)
@@ -38,4 +38,21 @@ class FileUploader < CarrierWave::Uploader::Base
def self.generate_dir
SecureRandom.hex(5)
end
+
+ def secure_url
+ Gitlab.config.gitlab.relative_url_root + "/files/#{@path}/#{@filename}"
+ end
+
+ def image?
+ img_ext = %w(png jpg jpeg gif bmp tiff)
+ if file.respond_to?(:extension)
+ img_ext.include?(file.extension.downcase)
+ else
+ # Not all CarrierWave storages respond to :extension
+ ext = file.path.split('.').last.downcase
+ img_ext.include?(ext)
+ end
+ rescue
+ false
+ end
end
diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml
index 5a57673b58..18897b055a 100644
--- a/app/views/projects/_issuable_form.html.haml
+++ b/app/views/projects/_issuable_form.html.haml
@@ -23,7 +23,7 @@
Parsed with
#{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}.
.pull-right
- Attach images (JPG, PNG, GIF) by dragging & dropping
+ Attach files by dragging & dropping
or #{link_to 'selecting them', '#', class: 'markdown-selector' }.
.clearfix
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index 2a7b44955c..975980bd6b 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -11,4 +11,4 @@
e.preventDefault();
});
- window.project_image_path_upload = "#{upload_image_project_path @project}";
+ window.project_file_path_upload = "#{upload_file_project_path @project}";
diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml
index d52e64666a..28c4734e14 100644
--- a/app/views/projects/merge_requests/_form.html.haml
+++ b/app/views/projects/merge_requests/_form.html.haml
@@ -9,4 +9,4 @@
e.preventDefault();
});
- window.project_image_path_upload = "#{upload_image_project_path @project}";
+ window.project_file_path_upload = "#{upload_file_project_path @project}";
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index bca3e45bf1..0653b30fcc 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -27,7 +27,7 @@
Parsed with
#{link_to 'Gitlab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}.
.pull-right
- Attach images (JPG, PNG, GIF) by dragging & dropping
+ Attach files by dragging & dropping
or #{link_to 'selecting them', '#', class: 'markdown-selector'}.
.clearfix
@@ -113,10 +113,11 @@
e.preventDefault();
});
- window.project_image_path_upload = "#{upload_image_project_path @project}";
+ window.project_file_path_upload = "#{upload_file_project_path @project}";
:javascript
var merge_request
merge_request = new MergeRequest({
action: 'commits'
});
+
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index b3b170d711..5fbb668570 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -25,7 +25,7 @@
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
.hint
.pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}.
- .pull-left Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
+ .pull-left Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
.clearfix
.error-alert
.col-md-6
@@ -51,4 +51,4 @@
onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) }
}).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val()));
- window.project_image_path_upload = "#{upload_image_project_path @project}";
+ window.project_file_path_upload = "#{upload_file_project_path @project}";
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index cdc76f5d96..4ba5907831 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -6,7 +6,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 images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }.
+ .pull-right Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }.
.note-form-actions
.buttons
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 1a4e06289f..fe3dab569f 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f|
+= form_for [@project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form" }, authenticity_token: true do |f|
= note_target_fields
= f.hidden_field :commit_id
= f.hidden_field :line_code
@@ -11,7 +11,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 images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }.
+ .pull-right Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }.
.note-form-actions
@@ -29,4 +29,4 @@
= f.file_field :attachment, class: "js-note-attachment-input hidden"
:javascript
- window.project_image_path_upload = "#{upload_image_project_path @project}";
+ window.project_file_path_upload = "#{upload_file_project_path @project}";
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 84731e43e9..0afee138c8 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -26,7 +26,7 @@
= render 'projects/zen', f: f, attr: :content, classes: 'description form-control'
.col-sm-12.hint
.pull-left Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}
- .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
+ .pull-right Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
.clearfix
.error-alert
@@ -43,5 +43,6 @@
= link_to "Cancel", project_wiki_path(@project, :home), class: "btn btn-cancel"
:javascript
- window.project_image_path_upload = "#{upload_image_project_path @project}";
+ window.project_file_path_upload = "#{upload_file_project_path @project}";
+
diff --git a/config/routes.rb b/config/routes.rb
index 65786d8356..bd659ddeab 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -93,7 +93,8 @@ Gitlab::Application.routes.draw do
#
# Attachments serving
#
- get 'files/:type/:id/:filename' => 'files#download', constraints: { id: /\d+/, type: /[a-z]+/, filename: /.+/ }
+ get 'files/:type/:id/:filename' => 'files#download_notes', constraints: { id: /\d+/, type: /[a-z]+/, filename: /.+/ }
+ get 'files/:namespace/:project/:folder_id/:filename' => 'files#download_files', constraints: { namespace: /[^\/]+/, project: /[a-zA-Z.\/0-9_\-]+/, filename: /.+/ }
#
# Admin Area
@@ -220,7 +221,7 @@ Gitlab::Application.routes.draw do
put :transfer
post :archive
post :unarchive
- post :upload_image
+ post :upload_file
post :toggle_star
post :markdown_preview
get :autocomplete_sources
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index ef786ccd32..039751a41e 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -7,43 +7,49 @@ describe ProjectsController do
let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
- describe "POST #upload_image" do
+ describe 'POST #upload_file' do
before do
sign_in(user)
project.team << [user, :developer]
end
- context "without params['markdown_img']" do
- it "returns an error" do
- post :upload_image, id: project.to_param, format: :json
+ context "without params['markdown_file']" do
+ it 'returns an error' do
+ post :upload_file, id: project.to_param, format: :json
expect(response.status).to eq(422)
end
end
- context "with invalid file" do
+ context 'with valid image' do
before do
- post :upload_image, id: project.to_param, markdown_img: txt, format: :json
+ post :upload_file,
+ id: project.to_param,
+ markdown_file: jpg,
+ format: :json
end
- it "returns an error" do
- expect(response.status).to eq(422)
- end
- end
-
- context "with valid file" do
- before do
- post :upload_image, id: project.to_param, markdown_img: jpg, format: :json
- end
-
- it "returns a content with original filename and new link." do
- expect(response.body).to match "\"alt\":\"rails_sample\""
+ it 'returns a content with original filename, new link, and correct type.' do
+ expect(response.body).to match '\"alt\":\"rails_sample\"'
expect(response.body).to match "\"url\":\"http://test.host/uploads/#{project.path_with_namespace}"
+ expect(response.body).to match '\"is_image\":true'
+ end
+ end
+
+ context 'with valid non-image file' do
+ before do
+ post :upload_file, id: project.to_param, markdown_file: txt, format: :json
+ end
+
+ it 'returns a content with original filename, new link, and correct type.' do
+ expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
+ expect(response.body).to match "\"url\":\"http://test.host/uploads/#{project.path_with_namespace}"
+ expect(response.body).to match '\"is_image\":false'
end
end
end
- describe "POST #toggle_star" do
- it "toggles star if user is signed in" do
+ describe 'POST #toggle_star' do
+ it 'toggles star if user is signed in' do
sign_in(user)
expect(user.starred?(public_project)).to be_falsey
post :toggle_star, id: public_project.to_param
@@ -52,7 +58,7 @@ describe ProjectsController do
expect(user.starred?(public_project)).to be_falsey
end
- it "does nothing if user is not signed in" do
+ it 'does nothing if user is not signed in' do
post :toggle_star, id: public_project.to_param
expect(user.starred?(public_project)).to be_falsey
post :toggle_star, id: public_project.to_param
diff --git a/spec/services/projects/file_service_spec.rb b/spec/services/projects/file_service_spec.rb
new file mode 100644
index 0000000000..38ab4a467b
--- /dev/null
+++ b/spec/services/projects/file_service_spec.rb
@@ -0,0 +1,81 @@
+require 'spec_helper'
+
+describe Projects::FileService do
+ describe 'File service' do
+ before do
+ @user = create :user
+ @project = create :project, creator_id: @user.id, namespace: @user.namespace
+ end
+
+ context 'for valid gif file' do
+ before do
+ gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
+ @link_to_file = upload_file(@project.repository,
+ { 'markdown_file' => gif },
+ 'http://test.example/')
+ end
+
+ it { expect(@link_to_file).to have_key('alt') }
+ it { expect(@link_to_file).to have_key('url') }
+ it { expect(@link_to_file).to have_key('is_image') }
+ it { expect(@link_to_file).to have_value('banana_sample') }
+ it { expect(@link_to_file['is_image']).to equal(true) }
+ it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file['url']).to match('banana_sample.gif') }
+ end
+
+ context 'for valid png file' do
+ before do
+ png = fixture_file_upload(Rails.root + 'spec/fixtures/dk.png',
+ 'image/png')
+ @link_to_file = upload_file(@project.repository,
+ { 'markdown_file' => png },
+ 'http://test.example/')
+ end
+
+ it { expect(@link_to_file).to have_key('alt') }
+ it { expect(@link_to_file).to have_key('url') }
+ it { expect(@link_to_file).to have_value('dk') }
+ it { expect(@link_to_file).to have_key('is_image') }
+ it { expect(@link_to_file['is_image']).to equal(true) }
+ it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file['url']).to match('dk.png') }
+ end
+
+ context 'for valid jpg file' do
+ before do
+ jpg = fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg')
+ @link_to_file = upload_file(@project.repository, { 'markdown_file' => jpg }, 'http://test.example/')
+ end
+
+ it { expect(@link_to_file).to have_key('alt') }
+ it { expect(@link_to_file).to have_key('url') }
+ it { expect(@link_to_file).to have_key('is_image') }
+ it { expect(@link_to_file).to have_value('rails_sample') }
+ it { expect(@link_to_file['is_image']).to equal(true) }
+ it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file['url']).to match('rails_sample.jpg') }
+ end
+
+ context 'for txt file' do
+ before do
+ txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain')
+ @link_to_file = upload_file(@project.repository,
+ { 'markdown_file' => txt },
+ 'http://test.example/')
+ end
+
+ it { expect(@link_to_file).to have_key('alt') }
+ it { expect(@link_to_file).to have_key('url') }
+ it { expect(@link_to_file).to have_key('is_image') }
+ it { expect(@link_to_file).to have_value('doc_sample.txt') }
+ it { expect(@link_to_file['is_image']).to equal(false) }
+ it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file['url']).to match('doc_sample.txt') }
+ end
+ end
+
+ def upload_file(repository, params, root_url)
+ Projects::FileService.new(repository, params, root_url).execute
+ end
+end
diff --git a/spec/services/projects/image_service_spec.rb b/spec/services/projects/image_service_spec.rb
deleted file mode 100644
index 23c4e227ae..0000000000
--- a/spec/services/projects/image_service_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-require 'spec_helper'
-
-describe Projects::ImageService do
- describe 'Image service' do
- before do
- @user = create :user
- @project = create :project, creator_id: @user.id, namespace: @user.namespace
- end
-
- context 'for valid gif file' do
- before do
- gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
- @link_to_image = upload_image(@project.repository, { 'markdown_img' => gif }, "http://test.example/")
- end
-
- it { expect(@link_to_image).to have_key("alt") }
- it { expect(@link_to_image).to have_key("url") }
- it { expect(@link_to_image).to have_value("banana_sample") }
- it { expect(@link_to_image["url"]).to match("http://test.example/uploads/#{@project.path_with_namespace}") }
- it { expect(@link_to_image["url"]).to match("banana_sample.gif") }
- end
-
- context 'for valid png file' do
- before do
- png = fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png')
- @link_to_image = upload_image(@project.repository, { 'markdown_img' => png }, "http://test.example/")
- end
-
- it { expect(@link_to_image).to have_key("alt") }
- it { expect(@link_to_image).to have_key("url") }
- it { expect(@link_to_image).to have_value("dk") }
- it { expect(@link_to_image["url"]).to match("http://test.example/uploads/#{@project.path_with_namespace}") }
- it { expect(@link_to_image["url"]).to match("dk.png") }
- end
-
- context 'for valid jpg file' do
- before do
- jpg = fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg')
- @link_to_image = upload_image(@project.repository, { 'markdown_img' => jpg }, "http://test.example/")
- end
-
- it { expect(@link_to_image).to have_key("alt") }
- it { expect(@link_to_image).to have_key("url") }
- it { expect(@link_to_image).to have_value("rails_sample") }
- it { expect(@link_to_image["url"]).to match("http://test.example/uploads/#{@project.path_with_namespace}") }
- it { expect(@link_to_image["url"]).to match("rails_sample.jpg") }
- end
-
- context 'for txt file' do
- before do
- txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain')
- @link_to_image = upload_image(@project.repository, { 'markdown_img' => txt }, "http://test.example/")
- end
-
- it { expect(@link_to_image).to be_nil }
- end
- end
-
- def upload_image(repository, params, root_url)
- Projects::ImageService.new(repository, params, root_url).execute
- end
-end
From ca504a77fe994da893cd0632de5e0e7ea5b729fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com>
Date: Sat, 14 Feb 2015 16:45:22 +0100
Subject: [PATCH 009/109] Fix tests
---
spec/services/projects/file_service_spec.rb | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/spec/services/projects/file_service_spec.rb b/spec/services/projects/file_service_spec.rb
index 38ab4a467b..e2d6766735 100644
--- a/spec/services/projects/file_service_spec.rb
+++ b/spec/services/projects/file_service_spec.rb
@@ -20,7 +20,7 @@ describe Projects::FileService do
it { expect(@link_to_file).to have_key('is_image') }
it { expect(@link_to_file).to have_value('banana_sample') }
it { expect(@link_to_file['is_image']).to equal(true) }
- it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") }
it { expect(@link_to_file['url']).to match('banana_sample.gif') }
end
@@ -38,7 +38,7 @@ describe Projects::FileService do
it { expect(@link_to_file).to have_value('dk') }
it { expect(@link_to_file).to have_key('is_image') }
it { expect(@link_to_file['is_image']).to equal(true) }
- it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") }
it { expect(@link_to_file['url']).to match('dk.png') }
end
@@ -53,7 +53,7 @@ describe Projects::FileService do
it { expect(@link_to_file).to have_key('is_image') }
it { expect(@link_to_file).to have_value('rails_sample') }
it { expect(@link_to_file['is_image']).to equal(true) }
- it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") }
it { expect(@link_to_file['url']).to match('rails_sample.jpg') }
end
@@ -70,7 +70,7 @@ describe Projects::FileService do
it { expect(@link_to_file).to have_key('is_image') }
it { expect(@link_to_file).to have_value('doc_sample.txt') }
it { expect(@link_to_file['is_image']).to equal(false) }
- it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") }
it { expect(@link_to_file['url']).to match('doc_sample.txt') }
end
end
From 9729cc584f5758395960416f308a9c45f698cdee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com>
Date: Sat, 14 Feb 2015 19:52:45 +0100
Subject: [PATCH 010/109] implement Project::UploadsController
---
app/controllers/files_controller.rb | 29 ++-----------------
.../projects/uploads_controller.rb | 16 ++++++++++
app/uploaders/file_uploader.rb | 4 ++-
config/routes.rb | 5 ++--
spec/services/projects/file_service_spec.rb | 8 ++---
5 files changed, 28 insertions(+), 34 deletions(-)
create mode 100644 app/controllers/projects/uploads_controller.rb
diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb
index a86340dd9b..15523cbc2e 100644
--- a/app/controllers/files_controller.rb
+++ b/app/controllers/files_controller.rb
@@ -1,5 +1,5 @@
class FilesController < ApplicationController
- def download_notes
+ def download
note = Note.find(params[:id])
uploader = note.attachment
@@ -14,32 +14,7 @@ class FilesController < ApplicationController
not_found!
end
else
- not_found!
+ redirect_to uploader.url
end
end
-
- def download_files
- namespace_id = params[:namespace]
- project_id = params[:project]
- folder_id = params[:folder_id]
- filename = params[:filename]
- project_with_namespace="#{namespace_id}/#{project_id}"
- filename_with_id="#{folder_id}/#{filename}"
-
- project = Project.find_with_namespace(project_with_namespace)
-
- uploader = FileUploader.new("#{Rails.root}/uploads","#{project_with_namespace}/#{folder_id}")
- uploader.retrieve_from_store!(filename)
-
- if can?(current_user, :read_project, project)
- download(uploader)
- else
- not_found!
- end
- end
-
- def download(uploader)
- disposition = uploader.image? ? 'inline' : 'attachment'
- send_file uploader.file.path, disposition: disposition
- end
end
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
new file mode 100644
index 0000000000..1c9fb1c86f
--- /dev/null
+++ b/app/controllers/projects/uploads_controller.rb
@@ -0,0 +1,16 @@
+class Projects::UploadsController < Projects::ApplicationController
+ layout 'project'
+
+ before_filter :project
+
+ def show
+ folder_id = params[:folder_id]
+ filename = params[:filename]
+
+ uploader = FileUploader.new("#{Rails.root}/uploads","#{@project.path_with_namespace}/#{folder_id}")
+ uploader.retrieve_from_store!(filename)
+
+ disposition = uploader.image? ? 'inline' : 'attachment'
+ send_file uploader.file.path, disposition: disposition
+ end
+end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index ac7bd5b27e..51ae8040e5 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -40,7 +40,9 @@ class FileUploader < CarrierWave::Uploader::Base
end
def secure_url
- Gitlab.config.gitlab.relative_url_root + "/files/#{@path}/#{@filename}"
+ path_array = @path.split('/')
+ path = File.join(path_array[0],path_array[1],'uploads',path_array[2])
+ Gitlab.config.gitlab.relative_url_root + "/#{path}/#{@filename}"
end
def image?
diff --git a/config/routes.rb b/config/routes.rb
index bd659ddeab..d29ad8db63 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -93,8 +93,7 @@ Gitlab::Application.routes.draw do
#
# Attachments serving
#
- get 'files/:type/:id/:filename' => 'files#download_notes', constraints: { id: /\d+/, type: /[a-z]+/, filename: /.+/ }
- get 'files/:namespace/:project/:folder_id/:filename' => 'files#download_files', constraints: { namespace: /[^\/]+/, project: /[a-zA-Z.\/0-9_\-]+/, filename: /.+/ }
+ get 'files/:type/:id/:filename' => 'files#download', constraints: { id: /\d+/, type: /[a-z]+/, filename: /.+/ }
#
# Admin Area
@@ -257,6 +256,8 @@ Gitlab::Application.routes.draw do
end
end
+ get '/uploads/:folder_id/:filename' => 'uploads#show', constraints: { filename: /.+/ }
+
get '/compare/:from...:to' => 'compare#show', :as => 'compare',
:constraints => { from: /.+/, to: /.+/ }
diff --git a/spec/services/projects/file_service_spec.rb b/spec/services/projects/file_service_spec.rb
index e2d6766735..7bbe5b575c 100644
--- a/spec/services/projects/file_service_spec.rb
+++ b/spec/services/projects/file_service_spec.rb
@@ -20,7 +20,7 @@ describe Projects::FileService do
it { expect(@link_to_file).to have_key('is_image') }
it { expect(@link_to_file).to have_value('banana_sample') }
it { expect(@link_to_file['is_image']).to equal(true) }
- it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") }
it { expect(@link_to_file['url']).to match('banana_sample.gif') }
end
@@ -38,7 +38,7 @@ describe Projects::FileService do
it { expect(@link_to_file).to have_value('dk') }
it { expect(@link_to_file).to have_key('is_image') }
it { expect(@link_to_file['is_image']).to equal(true) }
- it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") }
it { expect(@link_to_file['url']).to match('dk.png') }
end
@@ -53,7 +53,7 @@ describe Projects::FileService do
it { expect(@link_to_file).to have_key('is_image') }
it { expect(@link_to_file).to have_value('rails_sample') }
it { expect(@link_to_file['is_image']).to equal(true) }
- it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") }
it { expect(@link_to_file['url']).to match('rails_sample.jpg') }
end
@@ -70,7 +70,7 @@ describe Projects::FileService do
it { expect(@link_to_file).to have_key('is_image') }
it { expect(@link_to_file).to have_value('doc_sample.txt') }
it { expect(@link_to_file['is_image']).to equal(false) }
- it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") }
it { expect(@link_to_file['url']).to match('doc_sample.txt') }
end
end
From 192e7306626e073a5e6fb3b41d69f0e3fddb0821 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com>
Date: Sun, 15 Feb 2015 18:48:32 +0100
Subject: [PATCH 011/109] Fix tests
---
spec/controllers/projects_controller_spec.rb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 039751a41e..2d52e3fd91 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -30,7 +30,7 @@ describe ProjectsController do
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"rails_sample\"'
- expect(response.body).to match "\"url\":\"http://test.host/uploads/#{project.path_with_namespace}"
+ expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads"
expect(response.body).to match '\"is_image\":true'
end
end
@@ -42,7 +42,7 @@ describe ProjectsController do
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
- expect(response.body).to match "\"url\":\"http://test.host/uploads/#{project.path_with_namespace}"
+ expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads"
expect(response.body).to match '\"is_image\":false'
end
end
From d2ebdf664b42d4fac6b2e060ef79aa9fe0b0e72d Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Mon, 16 Feb 2015 19:58:40 +0100
Subject: [PATCH 012/109] Refactor.
---
.gitignore | 1 +
Gemfile | 1 +
Gemfile.lock | 6 ++
.../javascripts/dropzone_input.js.coffee | 26 ++++-----
app/controllers/files_controller.rb | 17 +++---
.../projects/uploads_controller.rb | 39 ++++++++++---
app/controllers/projects_controller.rb | 17 ------
app/services/projects/file_service.rb | 55 -------------------
app/services/projects/upload_service.rb | 22 ++++++++
app/uploaders/attachment_uploader.rb | 2 +-
app/uploaders/file_uploader.rb | 38 ++++---------
app/views/projects/issues/_form.html.haml | 2 +-
.../projects/merge_requests/_form.html.haml | 2 +-
.../merge_requests/_new_submit.html.haml | 2 +-
app/views/projects/milestones/_form.html.haml | 2 +-
app/views/projects/notes/_form.html.haml | 2 +-
app/views/projects/wikis/_form.html.haml | 2 +-
config/routes.rb | 7 ++-
db/schema.rb | 1 -
.../projects/uploads_controller_spec.rb | 49 +++++++++++++++++
spec/controllers/projects_controller_spec.rb | 45 +--------------
...service_spec.rb => upload_service_spec.rb} | 20 +++----
22 files changed, 164 insertions(+), 194 deletions(-)
delete mode 100644 app/services/projects/file_service.rb
create mode 100644 app/services/projects/upload_service.rb
create mode 100644 spec/controllers/projects/uploads_controller_spec.rb
rename spec/services/projects/{file_service_spec.rb => upload_service_spec.rb} (80%)
diff --git a/.gitignore b/.gitignore
index 7a7b5c9393..89fe301ee8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,6 +36,7 @@ nohup.out
public/assets/
public/uploads.*
public/uploads/
+uploads/
rails_best_practices_output.html
tags
tmp/
diff --git a/Gemfile b/Gemfile
index c3d8299e94..3f0eae8ef4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -205,6 +205,7 @@ 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'
diff --git a/Gemfile.lock b/Gemfile.lock
index 3283da40f8..2f5ebf80b1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -61,6 +61,9 @@ GEM
sass (~> 3.2)
browser (0.7.2)
builder (3.2.2)
+ byebug (3.2.0)
+ columnize (~> 0.8)
+ debugger-linecache (~> 1.2)
cal-heatmap-rails (0.0.1)
capybara (2.2.1)
mime-types (>= 1.16)
@@ -88,6 +91,7 @@ GEM
coffee-script-source (1.6.3)
colored (1.2)
colorize (0.5.8)
+ columnize (0.9.0)
connection_pool (2.1.0)
coveralls (0.7.0)
multi_json (~> 1.3)
@@ -103,6 +107,7 @@ GEM
daemons (1.1.9)
database_cleaner (1.3.0)
debug_inspector (0.0.2)
+ debugger-linecache (1.2.0)
default_value_for (3.0.0)
activerecord (>= 3.2.0, < 5.0)
descendants_tracker (0.0.3)
@@ -646,6 +651,7 @@ DEPENDENCIES
binding_of_caller
bootstrap-sass (~> 3.0)
browser
+ byebug
cal-heatmap-rails (~> 0.0.1)
capybara (~> 2.2.1)
carrierwave
diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee
index bed8471b39..2d9b496b13 100644
--- a/app/assets/javascripts/dropzone_input.js.coffee
+++ b/app/assets/javascripts/dropzone_input.js.coffee
@@ -9,7 +9,7 @@ class @DropzoneInput
iconPicture = ""
iconSpinner = ""
btnAlert = ""
- project_file_path_upload = window.project_file_path_upload or null
+ project_uploads_path = window.project_uploads_path or null
form_textarea = $(form).find("textarea.markdown-area")
form_textarea.wrap ""
@@ -72,10 +72,10 @@ class @DropzoneInput
form.find(".md-preview-holder").hide()
dropzone = form_dropzone.dropzone(
- url: project_file_path_upload
+ url: project_uploads_path
dictDefaultMessage: ""
clickable: true
- paramName: "markdown_file"
+ paramName: "file"
maxFilesize: 10
uploadMultiple: false
headers:
@@ -131,10 +131,9 @@ class @DropzoneInput
child = $(dropzone[0]).children("textarea")
- formatLink = (str) ->
- text = "[" + str.alt + "](" + str.url + ")"
- if str.is_image is true
- text = "!" + text
+ formatLink = (link) ->
+ text = "[#{link.alt}](#{link.url})"
+ text = "!#{text}" if link.is_image
text
handlePaste = (event) ->
@@ -179,9 +178,9 @@ class @DropzoneInput
uploadFile = (item, filename) ->
formData = new FormData()
- formData.append "markdown_file", item, filename
+ formData.append "file", item, filename
$.ajax
- url: project_file_path_upload
+ url: project_uploads_path
type: "POST"
data: formData
dataType: "json"
@@ -235,8 +234,7 @@ class @DropzoneInput
$(@).closest('.gfm-form').find('.div-dropzone').click()
return
- formatLink: (str) ->
- text = "[" + str.alt + "](" + str.url + ")"
- if str.is_image is true
- text = "!" + text
- text
+ formatLink: (link) ->
+ text = "[#{link.alt}](#{link.url})"
+ text = "!#{text}" if link.is_image
+ text
\ No newline at end of file
diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb
index 15523cbc2e..267239b7b8 100644
--- a/app/controllers/files_controller.rb
+++ b/app/controllers/files_controller.rb
@@ -3,18 +3,21 @@ class FilesController < ApplicationController
note = Note.find(params[:id])
uploader = note.attachment
- if uploader.file_storage?
- if can?(current_user, :read_project, note.project)
- # Replace old notes location in /public with the new one in / and send the file
+ if can?(current_user, :read_project, note.project)
+ if uploader.file_storage?
path = uploader.file.path.gsub("#{Rails.root}/public", Rails.root.to_s)
- disposition = uploader.image? ? 'inline' : 'attachment'
- send_file path, disposition: disposition
+ if File.exist?(path)
+ disposition = uploader.image? ? 'inline' : 'attachment'
+ send_file path, disposition: disposition
+ else
+ not_found!
+ end
else
- not_found!
+ redirect_to uploader.url
end
else
- redirect_to uploader.url
+ not_found!
end
end
end
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index 1c9fb1c86f..355163ac87 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -3,14 +3,37 @@ class Projects::UploadsController < Projects::ApplicationController
before_filter :project
- def show
- folder_id = params[:folder_id]
- filename = params[:filename]
-
- uploader = FileUploader.new("#{Rails.root}/uploads","#{@project.path_with_namespace}/#{folder_id}")
- uploader.retrieve_from_store!(filename)
+ def create
+ link_to_file = ::Projects::UploadService.new(repository, params[:file]).
+ execute
- disposition = uploader.image? ? 'inline' : 'attachment'
- send_file uploader.file.path, disposition: disposition
+ respond_to do |format|
+ if link_to_file
+ format.json do
+ render json: { link: link_to_file }
+ end
+ else
+ format.json do
+ render json: 'Invalid file.', status: :unprocessable_entity
+ end
+ end
+ end
+ end
+
+ def show
+ uploader = FileUploader.new(project, params[:secret])
+
+ if uploader.file_storage?
+ uploader.retrieve_from_store!(params[:filename])
+
+ if uploader.file.exists?
+ disposition = uploader.image? ? 'inline' : 'attachment'
+ send_file uploader.file.path, disposition: disposition
+ else
+ not_found!
+ end
+ else
+ redirect_to uploader.url
+ end
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index b430278903..9be66b6b9f 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -134,19 +134,6 @@ class ProjectsController < ApplicationController
end
end
- def upload_file
- link_to_file = ::Projects::FileService.new(repository, params, root_url).
- execute
-
- respond_to do |format|
- if link_to_file
- format.json { render json: { link: link_to_file } }
- else
- format.json { render json: 'Invalid file.', status: :unprocessable_entity }
- end
- end
- end
-
def toggle_star
current_user.toggle_star(@project)
@project.reload
@@ -159,10 +146,6 @@ class ProjectsController < ApplicationController
private
- def invalid_file(error)
- render json: { message: error.message }, status: :internal_server_error
- end
-
def set_title
@title = 'New Project'
end
diff --git a/app/services/projects/file_service.rb b/app/services/projects/file_service.rb
deleted file mode 100644
index 8c149bf53a..0000000000
--- a/app/services/projects/file_service.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-module Projects
- class FileService < BaseService
- include Rails.application.routes.url_helpers
- def initialize(repository, params, root_url)
- @repository, @params, @root_url = repository, params.dup, root_url
- end
-
- def execute
- uploader = FileUploader.new("#{Rails.root}/uploads", upload_path, accepted_files)
- file = @params['markdown_file']
-
- if file
- alt = file.original_filename
- uploader.store!(file)
- filename = nil
- if image?(file)
- filename=File.basename(alt, '.*')
- else
- filename=File.basename(alt)
- end
- link = {
- 'alt' => filename,
- 'url' => uploader.secure_url,
- 'is_image' => image?(file)
- }
- else
- link = nil
- end
- end
-
- protected
-
- def accepted_files
- # insert accepted mime types here (e.g %w(jpg jpeg gif png))
- nil
- end
-
- def accepted_images
- %w(jpg jpeg gif png)
- end
-
- def image?(file)
- accepted_images.map { |format| file.content_type.include? format }.any?
- end
-
- def upload_path
- base_dir = FileUploader.generate_dir
- File.join(@repository.path_with_namespace, base_dir)
- end
-
- def correct_mime_type?(file)
- accepted_files.map { |format| image.content_type.include? format }.any?
- end
- end
-end
diff --git a/app/services/projects/upload_service.rb b/app/services/projects/upload_service.rb
new file mode 100644
index 0000000000..a186c97628
--- /dev/null
+++ b/app/services/projects/upload_service.rb
@@ -0,0 +1,22 @@
+module Projects
+ class UploadService < BaseService
+ def initialize(project, file)
+ @project, @file = project, file
+ end
+
+ def execute
+ return nil unless @file
+
+ uploader = FileUploader.new(@project)
+ uploader.store!(@file)
+
+ filename = uploader.image? ? uploader.file.basename : uploader.file.filename
+
+ {
+ 'alt' => filename,
+ 'url' => uploader.secure_url,
+ 'is_image' => uploader.image?
+ }
+ end
+ end
+end
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index 22742d287a..58dc6e90c1 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -21,7 +21,7 @@ class AttachmentUploader < CarrierWave::Uploader::Base
end
def secure_url
- Gitlab.config.gitlab.relative_url_root + "/files/#{model.class.to_s.underscore}/#{model.id}/#{file.filename}"
+ File.join(Gitlab.config.gitlab.relative_url_root, "files", model.class.to_s.underscore, model.id.to_s, file.filename)
end
def file_storage?
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 51ae8040e5..c040f6bbe9 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -2,47 +2,33 @@
class FileUploader < CarrierWave::Uploader::Base
storage :file
- def initialize(base_dir, path = '', allowed_extensions = nil)
- @base_dir = base_dir
- @path = path
- @allowed_extensions = allowed_extensions
+ def initialize(project, secret = self.class.generate_secret)
+ @project = project
+ @secret = secret
end
def base_dir
- @base_dir
+ "#{Rails.root}/uploads"
end
def store_dir
- File.join(@base_dir, @path)
+ File.join(base_dir, @project.path_with_namespace, @secret)
end
def cache_dir
- File.join(@base_dir, 'tmp', @path)
+ File.join(base_dir, 'tmp', @project.path_with_namespace, @secret)
end
- def extension_white_list
- @allowed_extensions || super
- end
-
- def store!(file)
- @filename = self.class.generate_filename(file)
- super
- end
-
- def self.generate_filename(file)
- original_filename = File.basename(file.original_filename, '.*')
- extension = File.extname(file.original_filename)
- new_filename = Digest::MD5.hexdigest(original_filename) + extension
- end
-
- def self.generate_dir
+ def self.generate_secret
SecureRandom.hex(5)
end
def secure_url
- path_array = @path.split('/')
- path = File.join(path_array[0],path_array[1],'uploads',path_array[2])
- Gitlab.config.gitlab.relative_url_root + "/#{path}/#{@filename}"
+ File.join(Gitlab.config.gitlab.relative_url_root, @project.path_with_namespace, "uploads", @secret, file.filename)
+ end
+
+ def file_storage?
+ self.class.storage == CarrierWave::Storage::File
end
def image?
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index 975980bd6b..afeeed6edf 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -11,4 +11,4 @@
e.preventDefault();
});
- window.project_file_path_upload = "#{upload_file_project_path @project}";
+ window.project_uploads_path = "#{project_uploads_path @project}";
diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml
index 28c4734e14..c1a05e4586 100644
--- a/app/views/projects/merge_requests/_form.html.haml
+++ b/app/views/projects/merge_requests/_form.html.haml
@@ -9,4 +9,4 @@
e.preventDefault();
});
- window.project_file_path_upload = "#{upload_file_project_path @project}";
+ window.project_uploads_path = "#{project_uploads_path @project}";
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 0653b30fcc..4cf2a05b1a 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -113,7 +113,7 @@
e.preventDefault();
});
- window.project_file_path_upload = "#{upload_file_project_path @project}";
+ window.project_uploads_path = "#{project_uploads_path @project}";
:javascript
var merge_request
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 5fbb668570..dbcd23eee0 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -51,4 +51,4 @@
onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) }
}).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val()));
- window.project_file_path_upload = "#{upload_file_project_path @project}";
+ window.project_uploads_path = "#{project_uploads_path @project}";
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index fe3dab569f..9f9efc782d 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -29,4 +29,4 @@
= f.file_field :attachment, class: "js-note-attachment-input hidden"
:javascript
- window.project_file_path_upload = "#{upload_file_project_path @project}";
+ window.project_uploads_path = "#{project_uploads_path @project}";
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 0afee138c8..b1579878ed 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -43,6 +43,6 @@
= link_to "Cancel", project_wiki_path(@project, :home), class: "btn btn-cancel"
:javascript
- window.project_file_path_upload = "#{upload_file_project_path @project}";
+ window.project_uploads_path = "#{project_uploads_path @project}";
diff --git a/config/routes.rb b/config/routes.rb
index d29ad8db63..f0a7cf1e8a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -220,7 +220,6 @@ Gitlab::Application.routes.draw do
put :transfer
post :archive
post :unarchive
- post :upload_file
post :toggle_star
post :markdown_preview
get :autocomplete_sources
@@ -256,7 +255,11 @@ Gitlab::Application.routes.draw do
end
end
- get '/uploads/:folder_id/:filename' => 'uploads#show', constraints: { filename: /.+/ }
+ resources :uploads, only: [:create] do
+ collection do
+ get ":secret/:filename", action: :show, constraints: { filename: /.+/ }
+ end
+ end
get '/compare/:from...:to' => 'compare#show', :as => 'compare',
:constraints => { from: /.+/, to: /.+/ }
diff --git a/db/schema.rb b/db/schema.rb
index e11a068c9c..be3d35a431 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -26,7 +26,6 @@ ActiveRecord::Schema.define(version: 20150213121042) do
t.datetime "updated_at"
t.string "home_page_url"
t.integer "default_branch_protection", default: 2
- t.boolean "twitter_sharing_enabled", default: true
end
create_table "broadcast_messages", force: true do |t|
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
new file mode 100644
index 0000000000..8c99b5ca52
--- /dev/null
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -0,0 +1,49 @@
+require('spec_helper')
+
+describe Projects::UploadsController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
+ let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
+
+ describe 'POST #create' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ context "without params['file']" do
+ it 'returns an error' do
+ post :create, project_id: project.to_param, format: :json
+ expect(response.status).to eq(422)
+ end
+ end
+
+ context 'with valid image' do
+ before do
+ post :create,
+ project_id: project.to_param,
+ file: jpg,
+ format: :json
+ end
+
+ it 'returns a content with original filename, new link, and correct type.' do
+ expect(response.body).to match '\"alt\":\"rails_sample\"'
+ expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads"
+ expect(response.body).to match '\"is_image\":true'
+ end
+ end
+
+ context 'with valid non-image file' do
+ before do
+ post :create, project_id: project.to_param, file: txt, format: :json
+ end
+
+ it 'returns a content with original filename, new link, and correct type.' do
+ expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
+ expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads"
+ expect(response.body).to match '\"is_image\":false'
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 2d52e3fd91..9be4c2e505 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -4,50 +4,7 @@ describe ProjectsController do
let(:project) { create(:project) }
let(:public_project) { create(:project, :public) }
let(:user) { create(:user) }
- let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
- let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
-
- describe 'POST #upload_file' do
- before do
- sign_in(user)
- project.team << [user, :developer]
- end
-
- context "without params['markdown_file']" do
- it 'returns an error' do
- post :upload_file, id: project.to_param, format: :json
- expect(response.status).to eq(422)
- end
- end
-
- context 'with valid image' do
- before do
- post :upload_file,
- id: project.to_param,
- markdown_file: jpg,
- format: :json
- end
-
- it 'returns a content with original filename, new link, and correct type.' do
- expect(response.body).to match '\"alt\":\"rails_sample\"'
- expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads"
- expect(response.body).to match '\"is_image\":true'
- end
- end
-
- context 'with valid non-image file' do
- before do
- post :upload_file, id: project.to_param, markdown_file: txt, format: :json
- end
-
- it 'returns a content with original filename, new link, and correct type.' do
- expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
- expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads"
- expect(response.body).to match '\"is_image\":false'
- end
- end
- end
-
+
describe 'POST #toggle_star' do
it 'toggles star if user is signed in' do
sign_in(user)
diff --git a/spec/services/projects/file_service_spec.rb b/spec/services/projects/upload_service_spec.rb
similarity index 80%
rename from spec/services/projects/file_service_spec.rb
rename to spec/services/projects/upload_service_spec.rb
index 7bbe5b575c..fc34b45648 100644
--- a/spec/services/projects/file_service_spec.rb
+++ b/spec/services/projects/upload_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Projects::FileService do
+describe Projects::UploadService do
describe 'File service' do
before do
@user = create :user
@@ -10,9 +10,7 @@ describe Projects::FileService do
context 'for valid gif file' do
before do
gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
- @link_to_file = upload_file(@project.repository,
- { 'markdown_file' => gif },
- 'http://test.example/')
+ @link_to_file = upload_file(@project.repository, gif)
end
it { expect(@link_to_file).to have_key('alt') }
@@ -28,9 +26,7 @@ describe Projects::FileService do
before do
png = fixture_file_upload(Rails.root + 'spec/fixtures/dk.png',
'image/png')
- @link_to_file = upload_file(@project.repository,
- { 'markdown_file' => png },
- 'http://test.example/')
+ @link_to_file = upload_file(@project.repository, png)
end
it { expect(@link_to_file).to have_key('alt') }
@@ -45,7 +41,7 @@ describe Projects::FileService do
context 'for valid jpg file' do
before do
jpg = fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg')
- @link_to_file = upload_file(@project.repository, { 'markdown_file' => jpg }, 'http://test.example/')
+ @link_to_file = upload_file(@project.repository, jpg)
end
it { expect(@link_to_file).to have_key('alt') }
@@ -60,9 +56,7 @@ describe Projects::FileService do
context 'for txt file' do
before do
txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain')
- @link_to_file = upload_file(@project.repository,
- { 'markdown_file' => txt },
- 'http://test.example/')
+ @link_to_file = upload_file(@project.repository, txt)
end
it { expect(@link_to_file).to have_key('alt') }
@@ -75,7 +69,7 @@ describe Projects::FileService do
end
end
- def upload_file(repository, params, root_url)
- Projects::FileService.new(repository, params, root_url).execute
+ def upload_file(repository, file)
+ Projects::UploadService.new(repository, file).execute
end
end
From ab401a6132411294cee03cf4e0902ec75c2c42dc Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Mon, 16 Feb 2015 22:08:44 +0100
Subject: [PATCH 013/109] Remove note attachment file selector.
---
app/assets/javascripts/notes.js.coffee | 13 -------------
app/views/projects/notes/_edit_form.html.haml | 10 +---------
app/views/projects/notes/_form.html.haml | 8 --------
3 files changed, 1 insertion(+), 30 deletions(-)
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index 1c090bd06d..90e6fd6d15 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -39,9 +39,6 @@ class @Notes
# reset main target form after submit
$(document).on "ajax:complete", ".js-main-target-form", @resetMainTargetForm
- # attachment button
- $(document).on "click", ".js-choose-note-attachment-button", @chooseNoteAttachment
-
# update the file name when an attachment is selected
$(document).on "change", ".js-note-attachment-input", @updateFormAttachment
@@ -73,7 +70,6 @@ class @Notes
$(document).off "click", ".js-note-delete"
$(document).off "click", ".js-note-attachment-delete"
$(document).off "ajax:complete", ".js-main-target-form"
- $(document).off "click", ".js-choose-note-attachment-button"
$(document).off "click", ".js-discussion-reply-button"
$(document).off "click", ".js-add-diff-note-button"
$(document).off "visibilitychange"
@@ -173,15 +169,6 @@ class @Notes
form.find(".js-note-text").data("autosave").reset()
- ###
- Called when clicking the "Choose File" button.
-
- Opens the file selection dialog.
- ###
- chooseNoteAttachment: ->
- form = $(this).closest("form")
- form.find(".js-note-attachment-input").click()
-
###
Shows the main form and does some setup on it.
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index 4ba5907831..ca097f3d55 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -11,12 +11,4 @@
.note-form-actions
.buttons
= f.submit 'Save Comment', class: "btn btn-primary btn-save btn-grouped js-comment-button"
- = link_to 'Cancel', "#", class: "btn btn-cancel note-edit-cancel"
-
- .note-form-option.hidden-xs
- %a.choose-btn.btn.js-choose-note-attachment-button
- %i.fa.fa-paperclip
- %span Choose File ...
-
- %span.file_name.js-attachment-filename
- = f.file_field :attachment, class: "js-note-attachment-input hidden"
+ = link_to 'Cancel', "#", class: "btn btn-cancel note-edit-cancel"
\ No newline at end of file
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 9f9efc782d..8b331ef819 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -20,13 +20,5 @@
= yield(:note_actions)
%a.btn.grouped.js-close-discussion-note-form Cancel
- .note-form-option.hidden-xs
- %a.choose-btn.btn.js-choose-note-attachment-button
- %i.fa.fa-paperclip
- %span Choose File ...
-
- %span.file_name.js-attachment-filename
- = f.file_field :attachment, class: "js-note-attachment-input hidden"
-
:javascript
window.project_uploads_path = "#{project_uploads_path @project}";
From ab65be7a2f5d46a681d882f4f90d1f0438c64b02 Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Mon, 16 Feb 2015 22:08:55 +0100
Subject: [PATCH 014/109] Use longer upload secret.
---
app/uploaders/file_uploader.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index c040f6bbe9..bdfbfc668b 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -20,7 +20,7 @@ class FileUploader < CarrierWave::Uploader::Base
end
def self.generate_secret
- SecureRandom.hex(5)
+ SecureRandom.hex
end
def secure_url
From 99fb4d387fbcddc56292b788a84b1e62d27765dd Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Mon, 16 Feb 2015 22:09:17 +0100
Subject: [PATCH 015/109] Add paperclip icon to links to uploads in notes.
---
app/assets/stylesheets/sections/notes.scss | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/sections/notes.scss
index 5494845eb8..40adc8b3ba 100644
--- a/app/assets/stylesheets/sections/notes.scss
+++ b/app/assets/stylesheets/sections/notes.scss
@@ -66,6 +66,22 @@ ul.notes {
overflow: auto;
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;
+ }
+ }
}
}
.note-header {
From 4036fb167ddce47db8ad4d885000b2c9db96595d Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Mon, 16 Feb 2015 22:09:52 +0100
Subject: [PATCH 016/109] Change textarea upload hover icon from picture to
paperclip.
---
app/assets/javascripts/dropzone_input.js.coffee | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee
index 2d9b496b13..06e9f0001a 100644
--- a/app/assets/javascripts/dropzone_input.js.coffee
+++ b/app/assets/javascripts/dropzone_input.js.coffee
@@ -6,7 +6,7 @@ class @DropzoneInput
divHover = ""
divSpinner = ""
divAlert = ""
- iconPicture = ""
+ iconPaperclip = ""
iconSpinner = ""
btnAlert = ""
project_uploads_path = window.project_uploads_path or null
@@ -19,7 +19,7 @@ class @DropzoneInput
form_dropzone = $(form).find('.div-dropzone')
form_dropzone.parent().addClass "div-dropzone-wrapper"
form_dropzone.append divHover
- $(".div-dropzone-hover").append iconPicture
+ $(".div-dropzone-hover").append iconPaperclip
form_dropzone.append divSpinner
$(".div-dropzone-spinner").append iconSpinner
$(".div-dropzone-spinner").css
From 65b125a5035cb021aeb81e168fd4ae1ad6c74c11 Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Wed, 18 Feb 2015 08:16:42 +0100
Subject: [PATCH 017/109] Update schema.
---
db/schema.rb | 1 +
1 file changed, 1 insertion(+)
diff --git a/db/schema.rb b/db/schema.rb
index be3d35a431..e11a068c9c 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -26,6 +26,7 @@ ActiveRecord::Schema.define(version: 20150213121042) do
t.datetime "updated_at"
t.string "home_page_url"
t.integer "default_branch_protection", default: 2
+ t.boolean "twitter_sharing_enabled", default: true
end
create_table "broadcast_messages", force: true do |t|
From 008e3d66e9649079037260c072020340c69f3eec Mon Sep 17 00:00:00 2001
From: Standa Opichal
Date: Wed, 18 Feb 2015 15:47:10 +0100
Subject: [PATCH 018/109] Fix for TeamCity buildQueue REST API
build/@branchName.
Strips the refs/heads/ if any to get just the branchName out of a :ref
e.g.: `refs/heads/feature/newProfile` -> `feature/newProfile`
The TeamCity buildQueue POST data require the branchName attribute
to contain just the branch name (just xyzbranch and not the full
ref in the refs/heads/xyzbranch). With the refs/heads/xyzbranch the
build is triggered and looks like it is working however it always
ompiles the default branch (or master).
---
app/models/project_services/teamcity_service.rb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index c4b6ef5d9a..b6932f1c77 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -115,13 +115,13 @@ class TeamcityService < CiService
end
end
- def execute(data)
+ def execute(push)
auth = {
username: username,
password: password,
}
- branch = data[:ref]
+ branch = push[:ref].gsub('refs/heads/', '')
self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue",
body: ""\
From 5493e27d53d157c1d43dca6c29815f4bc3a39c5c Mon Sep 17 00:00:00 2001
From: krolik
Date: Fri, 20 Feb 2015 13:00:42 +0200
Subject: [PATCH 019/109] Fixed merge request diff page after back browser
button is pressed.
---
app/assets/javascripts/merge_request.js.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
index 757592842e..b891c1352b 100644
--- a/app/assets/javascripts/merge_request.js.coffee
+++ b/app/assets/javascripts/merge_request.js.coffee
@@ -123,7 +123,7 @@ class @MergeRequest
loadDiff: (event) ->
$.ajax
type: 'GET'
- url: this.$('.merge-request-tabs .diffs-tab a').attr('href')
+ url: this.$('.merge-request-tabs .diffs-tab a').attr('href') + ".json";
beforeSend: =>
this.$('.mr-loading-status .loading').show()
complete: =>
From 51b234cfb6582650f362cf2339d0f8b0b5a10345 Mon Sep 17 00:00:00 2001
From: krolik
Date: Fri, 20 Feb 2015 13:03:28 +0200
Subject: [PATCH 020/109] Removed exceeding semicolon
---
app/assets/javascripts/merge_request.js.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
index b891c1352b..09a7b4b310 100644
--- a/app/assets/javascripts/merge_request.js.coffee
+++ b/app/assets/javascripts/merge_request.js.coffee
@@ -123,7 +123,7 @@ class @MergeRequest
loadDiff: (event) ->
$.ajax
type: 'GET'
- url: this.$('.merge-request-tabs .diffs-tab a').attr('href') + ".json";
+ url: this.$('.merge-request-tabs .diffs-tab a').attr('href') + ".json"
beforeSend: =>
this.$('.mr-loading-status .loading').show()
complete: =>
From 874640123b9b508fef40d4285a7c28d7e4653dd7 Mon Sep 17 00:00:00 2001
From: Derek Campbell
Date: Fri, 20 Feb 2015 09:20:42 -0400
Subject: [PATCH 021/109] To close an issue you must set 'state_event' to
'close'. I cannot set 'closed' to '1'.
---
doc/api/issues.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 5a2f6a4c22..a7dd8b74c3 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -208,7 +208,7 @@ If an error occurs, an error number and a message explaining the reason is retur
## Delete existing issue (**Deprecated**)
-The function is deprecated and returns a `405 Method Not Allowed` error if called. An issue gets now closed and is done by calling `PUT /projects/:id/issues/:issue_id` with parameter `closed` set to 1.
+The function is deprecated and returns a `405 Method Not Allowed` error if called. An issue gets now closed and is done by calling `PUT /projects/:id/issues/:issue_id` with parameter `state_event` set to `close`.
```
DELETE /projects/:id/issues/:issue_id
From c801df81fb48272b670b7448e3898a98cdb8b742 Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Fri, 20 Feb 2015 14:39:35 +0100
Subject: [PATCH 022/109] Satisfy Rubocop.
---
app/controllers/projects/uploads_controller.rb | 2 +-
config/initializers/static_files.rb | 2 +-
config/routes.rb | 4 ++--
lib/gitlab/middleware/static.rb | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index b922b56418..2b4da35bc7 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -16,4 +16,4 @@ class Projects::UploadsController < Projects::ApplicationController
not_found!
end
end
-end
\ No newline at end of file
+end
diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb
index e04c29cee4..2a6eaec0cc 100644
--- a/config/initializers/static_files.rb
+++ b/config/initializers/static_files.rb
@@ -10,4 +10,4 @@ begin
rescue
# If ActionDispatch::Static wasn't loaded onto the stack (like in production),
# an exception is raised.
-end
\ No newline at end of file
+end
diff --git a/config/routes.rb b/config/routes.rb
index 0e7f7d893d..ca56c2d268 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -76,11 +76,11 @@ Gitlab::Application.routes.draw do
scope path: :uploads do
# Note attachments and User/Group/Project avatars
get ":model/:mounted_as/:id/:filename", to: "uploads#show",
- constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ }
+ constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ }
# Project markdown uploads
get ":id/:secret/:filename", to: "projects/uploads#show",
- constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ }
+ constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ }
end
#
diff --git a/lib/gitlab/middleware/static.rb b/lib/gitlab/middleware/static.rb
index b92319c95d..85ffa8aca6 100644
--- a/lib/gitlab/middleware/static.rb
+++ b/lib/gitlab/middleware/static.rb
@@ -10,4 +10,4 @@ module Gitlab
end
end
end
-end
\ No newline at end of file
+end
From 4ef6ffaad3e9b7a29b438722e5e101de78521ec7 Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Fri, 20 Feb 2015 15:19:50 +0100
Subject: [PATCH 023/109] Split up AttachmentUploader.
---
app/models/group.rb | 2 +-
app/models/project.rb | 2 +-
app/models/user.rb | 2 +-
app/uploaders/attachment_uploader.rb | 10 --------
app/uploaders/avatar_uploader.rb | 32 ++++++++++++++++++++++++
app/views/events/event/_note.html.haml | 6 ++---
app/views/projects/notes/_note.html.haml | 6 ++---
features/steps/groups.rb | 2 +-
features/steps/profile/profile.rb | 2 +-
features/steps/project/project.rb | 2 +-
10 files changed, 44 insertions(+), 22 deletions(-)
create mode 100644 app/uploaders/avatar_uploader.rb
diff --git a/app/models/group.rb b/app/models/group.rb
index d6ec0be608..da9621a2a1 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -23,7 +23,7 @@ class Group < Namespace
validate :avatar_type, if: ->(user) { user.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
- mount_uploader :avatar, AttachmentUploader
+ mount_uploader :avatar, AvatarUploader
after_create :post_create_hook
after_destroy :post_destroy_hook
diff --git a/app/models/project.rb b/app/models/project.rb
index 56e1aa2904..e2c7f76eb0 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -138,7 +138,7 @@ class Project < ActiveRecord::Base
if: ->(project) { project.avatar && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
- mount_uploader :avatar, AttachmentUploader
+ mount_uploader :avatar, AvatarUploader
# Scopes
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
diff --git a/app/models/user.rb b/app/models/user.rb
index 21ccc76978..a723b1289b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -177,7 +177,7 @@ class User < ActiveRecord::Base
end
end
- mount_uploader :avatar, AttachmentUploader
+ mount_uploader :avatar, AvatarUplaoder
# Scopes
scope :admins, -> { where(admin: true) }
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index b122b6c865..a9691bee46 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -3,8 +3,6 @@
class AttachmentUploader < CarrierWave::Uploader::Base
storage :file
- after :store, :reset_events_cache
-
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
@@ -22,15 +20,7 @@ class AttachmentUploader < CarrierWave::Uploader::Base
false
end
- def secure_url
- Gitlab.config.gitlab.relative_url_root + "/files/#{model.class.to_s.underscore}/#{model.id}/#{file.filename}"
- end
-
def file_storage?
self.class.storage == CarrierWave::Storage::File
end
-
- def reset_events_cache(file)
- model.reset_events_cache if model.is_a?(User)
- end
end
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
new file mode 100644
index 0000000000..7cad044555
--- /dev/null
+++ b/app/uploaders/avatar_uploader.rb
@@ -0,0 +1,32 @@
+# encoding: utf-8
+
+class AvatarUploader < CarrierWave::Uploader::Base
+ storage :file
+
+ after :store, :reset_events_cache
+
+ def store_dir
+ "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
+ end
+
+ def image?
+ img_ext = %w(png jpg jpeg gif bmp tiff)
+ if file.respond_to?(:extension)
+ img_ext.include?(file.extension.downcase)
+ else
+ # Not all CarrierWave storages respond to :extension
+ ext = file.path.split('.').last.downcase
+ img_ext.include?(ext)
+ end
+ rescue
+ false
+ end
+
+ def file_storage?
+ self.class.storage == CarrierWave::Storage::File
+ end
+
+ def reset_events_cache(file)
+ model.reset_events_cache if model.is_a?(User)
+ end
+end
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index 0acb853877..4ef18c0906 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -18,9 +18,9 @@
- note = event.target
- if note.attachment.url
- if note.attachment.image?
- = link_to note.attachment.secure_url, target: '_blank' do
- = image_tag note.attachment.secure_url, class: 'note-image-attach'
+ = link_to note.attachment.url, target: '_blank' do
+ = image_tag note.attachment.url, class: 'note-image-attach'
- else
- = link_to note.attachment.secure_url, target: "_blank", class: 'note-file-attach' do
+ = link_to note.attachment.url, target: "_blank", class: 'note-file-attach' do
%i.fa.fa-paperclip
= note.attachment_identifier
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 88c7b7ccf1..cfeba00d27 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -57,10 +57,10 @@
- if note.attachment.url
.note-attachment
- if note.attachment.image?
- = link_to note.attachment.secure_url, target: '_blank' do
- = image_tag note.attachment.secure_url, class: 'note-image-attach'
+ = link_to note.attachment.url, target: '_blank' do
+ = image_tag note.attachment.url, class: 'note-image-attach'
.attachment
- = link_to note.attachment.secure_url, target: "_blank" do
+ = link_to note.attachment.url, target: "_blank" do
%i.fa.fa-paperclip
= note.attachment_identifier
= link_to delete_attachment_project_note_path(@project, note),
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 610e7fd3a4..0a9b4ccba5 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -110,7 +110,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
end
step 'I should see new group "Owned" avatar' do
- Group.find_by(name: "Owned").avatar.should be_instance_of AttachmentUploader
+ Group.find_by(name: "Owned").avatar.should be_instance_of AvatarUploader
Group.find_by(name: "Owned").avatar.url.should == "/uploads/group/avatar/#{ Group.find_by(name:"Owned").id }/gitlab_logo.png"
end
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index a907b0b7dc..4efd217678 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -29,7 +29,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step 'I should see new avatar' do
- @user.avatar.should be_instance_of AttachmentUploader
+ @user.avatar.should be_instance_of AvatarUploader
@user.avatar.url.should == "/uploads/user/avatar/#{ @user.id }/gitlab_logo.png"
end
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 033d45e025..d39c8e7d2d 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -35,7 +35,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I should see new project avatar' do
- @project.avatar.should be_instance_of AttachmentUploader
+ @project.avatar.should be_instance_of AvatarUploader
url = @project.avatar.url
url.should == "/uploads/project/avatar/#{ @project.id }/gitlab_logo.png"
end
From 7f1adc3d9cdc5c3f1c0fcbf6c72d89b8ee062af5 Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Fri, 20 Feb 2015 15:56:12 +0100
Subject: [PATCH 024/109] Fix URL to uploaded file.
---
app/controllers/projects/uploads_controller.rb | 2 +-
app/services/projects/upload_service.rb | 3 +--
app/uploaders/file_uploader.rb | 4 ++++
config/routes.rb | 6 +++---
4 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index 53b92d8643..9020e86c44 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -4,7 +4,7 @@ class Projects::UploadsController < Projects::ApplicationController
before_filter :project
def create
- link_to_file = ::Projects::UploadService.new(repository, params[:file]).
+ link_to_file = ::Projects::UploadService.new(project, params[:file]).
execute
respond_to do |format|
diff --git a/app/services/projects/upload_service.rb b/app/services/projects/upload_service.rb
index b2466b52ad..a186c97628 100644
--- a/app/services/projects/upload_service.rb
+++ b/app/services/projects/upload_service.rb
@@ -1,6 +1,5 @@
module Projects
class UploadService < BaseService
- include Rails.application.routes.url_helpers
def initialize(project, file)
@project, @file = project, file
end
@@ -15,7 +14,7 @@ module Projects
{
'alt' => filename,
- 'url' => project_upload_url(@project, secret: uploader.secret, filename: uploader.file.filename),
+ 'url' => uploader.secure_url,
'is_image' => uploader.image?
}
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 36a28f93c4..f9673abbfe 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -25,6 +25,10 @@ class FileUploader < CarrierWave::Uploader::Base
SecureRandom.hex
end
+ def secure_url
+ File.join(Gitlab.config.gitlab.url, @project.path_with_namespace, "uploads", @secret, file.filename)
+ end
+
def file_storage?
self.class.storage == CarrierWave::Storage::File
end
diff --git a/config/routes.rb b/config/routes.rb
index 498716b12e..b6f58acf1a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -79,8 +79,8 @@ Gitlab::Application.routes.draw do
constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ }
# Project markdown uploads
- get ":id/:secret/:filename", to: "projects/uploads#show",
- constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ }
+ get ":project_id/:secret/:filename", to: "projects/uploads#show",
+ constraints: { project_id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ }
end
#
@@ -264,7 +264,7 @@ Gitlab::Application.routes.draw do
resources :uploads, only: [:create] do
collection do
- get ":secret/:filename", action: :show, constraints: { filename: /.+/ }
+ get ":secret/:filename", action: :show, as: :show, constraints: { filename: /.+/ }
end
end
From 2570e4df79fa09d3c4abc1d0ec82c67a322b249e Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Fri, 20 Feb 2015 16:55:38 +0100
Subject: [PATCH 025/109] Fix specs.
---
config/routes.rb | 10 ++++++----
spec/controllers/projects/uploads_controller_spec.rb | 4 ++--
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/config/routes.rb b/config/routes.rb
index b6f58acf1a..3d826bf559 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -75,12 +75,14 @@ Gitlab::Application.routes.draw do
scope path: :uploads 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: /.+/ }
+ get ":model/:mounted_as/:id/:filename",
+ to: "uploads#show",
+ constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ }
# Project markdown uploads
- get ":project_id/:secret/:filename", to: "projects/uploads#show",
- constraints: { project_id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ }
+ get ":project_id/:secret/:filename",
+ to: "projects/uploads#show",
+ constraints: { project_id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ }
end
#
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index 8c99b5ca52..28d313b7e9 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -29,7 +29,7 @@ describe Projects::UploadsController do
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"rails_sample\"'
- expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads"
+ expect(response.body).to match "\"url\":\"http://localhost/#{project.path_with_namespace}/uploads"
expect(response.body).to match '\"is_image\":true'
end
end
@@ -41,7 +41,7 @@ describe Projects::UploadsController do
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
- expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads"
+ expect(response.body).to match "\"url\":\"http://localhost/#{project.path_with_namespace}/uploads"
expect(response.body).to match '\"is_image\":false'
end
end
From b19b8c679a136234094443e2d4a345f136a0bcc1 Mon Sep 17 00:00:00 2001
From: Marco Wessel
Date: Mon, 23 Feb 2015 02:31:12 +0100
Subject: [PATCH 026/109] Give last_activity_at a default value so it will
always be set
---
CHANGELOG | 1 +
app/models/project.rb | 6 ++++++
.../20150223022001_set_missing_last_activity_at.rb | 9 +++++++++
db/schema.rb | 4 ++--
4 files changed, 18 insertions(+), 2 deletions(-)
create mode 100644 db/migrate/20150223022001_set_missing_last_activity_at.rb
diff --git a/CHANGELOG b/CHANGELOG
index 6702ba2ba4..5bdcc53540 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,6 +3,7 @@ v 7.9.0 (unreleased)
- Upgrade Rails gem to version 4.1.9.
- Improve UI for commits, issues and merge request lists
- Fix commit comments on first line of diff not rendering in Merge Request Discussion view.
+ - Fix ordering of imported but unchanged projects (Marco Wessel)
v 7.8.0
- Fix access control and protection against XSS for note attachments and other uploads.
diff --git a/app/models/project.rb b/app/models/project.rb
index 91ab788083..04189839d6 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -48,6 +48,12 @@ class Project < ActiveRecord::Base
default_value_for :wall_enabled, false
default_value_for :snippets_enabled, gitlab_config_features.snippets
+ # set last_activity_at to the same as updated_at
+ before_create :set_last_activity_at
+ def set_last_activity_at
+ self.last_activity_at = self.updated_at
+ end
+
ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :tags
diff --git a/db/migrate/20150223022001_set_missing_last_activity_at.rb b/db/migrate/20150223022001_set_missing_last_activity_at.rb
new file mode 100644
index 0000000000..3a3adf1887
--- /dev/null
+++ b/db/migrate/20150223022001_set_missing_last_activity_at.rb
@@ -0,0 +1,9 @@
+class SetMissingLastActivityAt < ActiveRecord::Migration
+ def up
+ execute "UPDATE projects SET last_activity_at = updated_at WHERE last_activity_at IS NULL"
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e11a068c9c..d34eab7508 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: 20150213121042) do
+ActiveRecord::Schema.define(version: 20150223022001) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -334,12 +334,12 @@ ActiveRecord::Schema.define(version: 20150213121042) do
t.string "import_url"
t.integer "visibility_level", default: 0, null: false
t.boolean "archived", default: false, null: false
- t.string "avatar"
t.string "import_status"
t.float "repository_size", default: 0.0
t.integer "star_count", default: 0, null: false
t.string "import_type"
t.string "import_source"
+ t.string "avatar"
end
add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree
From d4bfdd34baa9eea2e89223b039f15cd4d4b3e5ae Mon Sep 17 00:00:00 2001
From: Marco Wessel
Date: Mon, 23 Feb 2015 04:03:12 +0100
Subject: [PATCH 027/109] use update_column and set to created_at like
elsewhere
---
app/models/project.rb | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/models/project.rb b/app/models/project.rb
index 04189839d6..967e4de22a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -48,10 +48,10 @@ class Project < ActiveRecord::Base
default_value_for :wall_enabled, false
default_value_for :snippets_enabled, gitlab_config_features.snippets
- # set last_activity_at to the same as updated_at
- before_create :set_last_activity_at
+ # set last_activity_at to the same as created_at
+ after_create :set_last_activity_at
def set_last_activity_at
- self.last_activity_at = self.updated_at
+ update_column(:last_activity_at, self.created_at)
end
ActsAsTaggableOn.strict_case_match = true
From 846f83177448832bd04ea0167b34541b5d3b71c6 Mon Sep 17 00:00:00 2001
From: Sabba Petri
Date: Mon, 23 Feb 2015 10:40:06 -0800
Subject: [PATCH 028/109] Fixes grammatical consistency and small changes
This commit adds consistency to small things like periods, commas,
etc. Also gives additional information to buttons and headers.
Fixes #2002, #2005, #2003
---
app/views/profiles/accounts/show.html.haml | 8 ++++----
app/views/profiles/applications.html.haml | 4 +++-
app/views/profiles/design.html.haml | 4 ++--
app/views/profiles/emails/index.html.haml | 4 ++--
app/views/profiles/groups/index.html.haml | 4 ++--
app/views/profiles/history.html.haml | 4 ++--
app/views/profiles/keys/index.html.haml | 6 +++---
app/views/profiles/notifications/show.html.haml | 7 +++----
app/views/profiles/passwords/edit.html.haml | 4 ++--
app/views/profiles/show.html.haml | 4 ++--
10 files changed, 25 insertions(+), 24 deletions(-)
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 53a50f6796..f124637c07 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -1,5 +1,5 @@
%h3.page-title
- Account settings
+ Account Settings
%p.light
You can change your username and private token here.
- if current_user.ldap_user?
@@ -10,7 +10,7 @@
.account-page
%fieldset.update-token
%legend
- Private token
+ Reset Private token
%div
= form_for @user, url: reset_private_token_profile_path, method: :put do |f|
.data
@@ -25,7 +25,7 @@
- if current_user.private_token
= text_field_tag "token", current_user.private_token, class: "form-control"
%div
- = f.submit 'Reset', data: { confirm: "Are you sure?" }, class: "btn btn-primary btn-build-token"
+ = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-primary btn-build-token"
- else
%span You don`t have one yet. Click generate to fix it.
= f.submit 'Generate', class: "btn success btn-build-token"
@@ -43,7 +43,7 @@
- if show_profile_username_tab?
%fieldset.update-username
%legend
- Username
+ Change Username
= form_for @user, url: update_username_profile_path, method: :put, remote: true do |f|
%p
Changing your username will change path to all personal projects!
diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml
index 4b5817e10b..c8c522e981 100644
--- a/app/views/profiles/applications.html.haml
+++ b/app/views/profiles/applications.html.haml
@@ -1,5 +1,7 @@
%h3.page-title
- OAuth2
+ Application Settings
+%p.light
+ OAuth2 protocol settings below.
%fieldset.oauth-applications
%legend Your applications
diff --git a/app/views/profiles/design.html.haml b/app/views/profiles/design.html.haml
index 0d8075b7d4..8d09595fd4 100644
--- a/app/views/profiles/design.html.haml
+++ b/app/views/profiles/design.html.haml
@@ -1,7 +1,7 @@
%h3.page-title
- My appearance settings
+ Design Settings
%p.light
- Appearance settings saved to your profile and available across all devices
+ Appearance settings will be saved to your profile and made available across all devices.
%hr
= form_for @user, url: profile_path, remote: true, method: :put do |f|
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 0b30e77233..3bbad6fdf7 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -1,5 +1,5 @@
%h3.page-title
- My email addresses
+ Email Settings
%p.light
Your
%b Primary Email
@@ -34,4 +34,4 @@
.col-sm-10
= f.text_field :email, class: 'form-control'
.form-actions
- = f.submit 'Add', class: 'btn btn-create'
+ = f.submit 'Add email address', class: 'btn btn-create'
diff --git a/app/views/profiles/groups/index.html.haml b/app/views/profiles/groups/index.html.haml
index e9ffca8faf..daf76636ff 100644
--- a/app/views/profiles/groups/index.html.haml
+++ b/app/views/profiles/groups/index.html.haml
@@ -1,12 +1,12 @@
%h3.page-title
- Group membership
+ Group Membership
- if current_user.can_create_group?
%span.pull-right
= link_to new_group_path, class: "btn btn-new" do
%i.fa.fa-plus
New Group
%p.light
- Group members have access to all a group's projects
+ Group members have access to all group projects.
%hr
.panel.panel-default
.panel-heading
diff --git a/app/views/profiles/history.html.haml b/app/views/profiles/history.html.haml
index 3951c47b5f..9cafe03b8b 100644
--- a/app/views/profiles/history.html.haml
+++ b/app/views/profiles/history.html.haml
@@ -1,7 +1,7 @@
%h3.page-title
- Account history
+ My Account History
%p.light
- All events created by your account are listed here
+ All events created by your account are listed below.
%hr
.profile_history
= render @events
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index c83c73ffcf..965d5e032f 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -1,12 +1,12 @@
%h3.page-title
- My SSH keys (#{@keys.count})
+ SSH Keys Settings
.pull-right
= link_to "Add SSH Key", new_profile_key_path, class: "btn btn-new"
%p.light
- SSH keys allow you to establish a secure connection between your computer and GitLab
+ My SSH keys: #{@keys.count}
%br
Before you can add an SSH key you need to
- = link_to "generate it", help_page_path("ssh", "README")
+ = link_to "generate it.", help_page_path("ssh", "README")
%hr
= render 'key_table'
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 28bc5a426a..e3cd323927 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -1,10 +1,9 @@
%h3.page-title
- Notifications settings
+ Notifications Settings
%p.light
These are your global notification settings.
%hr
-
= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications form-horizontal global-notifications-form' } do |f|
-if @user.errors.any?
%div.alert.alert-danger
@@ -60,7 +59,7 @@
%p
You can also specify notification level per group or per project.
%br
- By default all projects and groups uses notification level set above.
+ By default, all projects and groups will use the notification level set above.
%h4 Groups:
%ul.bordered-list
- @group_members.each do |users_group|
@@ -69,7 +68,7 @@
.col-md-6
%p
- To specify notification level per project of a group you belong to,
+ To specify the notification level per project of a group you belong to,
%br
you need to be a member of the project itself, not only its group.
%h4 Projects:
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index 6b19db4eb5..3b1ebbfaf5 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -1,4 +1,4 @@
-%h3.page-title Password
+%h3.page-title Password Settings
%p.light
- if @user.password_automatically_set?
Set your password.
@@ -12,7 +12,7 @@
- unless @user.password_automatically_set?
You must provide current password in order to change it.
%br
- After a successful password update you will be redirected to login page where you should login with your new password
+ After a successful password update, you will be redirected to the login page where you can log in with your new password.
-if @user.errors.any?
.alert.alert-danger
%ul
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 640104fdad..b2808c46c0 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -1,7 +1,7 @@
%h3.page-title
- Profile settings
+ Profile Settings
%p.light
- This information appears on your profile.
+ This information will appear on your profile.
- if current_user.ldap_user?
Some options are unavailable for LDAP accounts
%hr
From 5d2dda9744eb902e397410f8f4b94af3a0acb1df Mon Sep 17 00:00:00 2001
From: Sabba Petri
Date: Mon, 23 Feb 2015 11:39:08 -0800
Subject: [PATCH 029/109] Added information to tooltips
Tooltips now have meaning by mentioning their function ("Filter by..."). Fixes #1992
---
app/helpers/events_helper.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 063916a8df..d38b546e1b 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -30,7 +30,7 @@ module EventsHelper
end
content_tag :li, class: "filter_icon #{active}" do
- link_to request.path, class: 'has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => tooltip do
+ link_to request.path, class: 'has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => 'Filter by ' + tooltip.downcase do
icon(icon_for_event[key]) + content_tag(:span, ' ' + tooltip)
end
end
From 5ce2d44b136aae8e9e42397474a0e75bc6b32ded Mon Sep 17 00:00:00 2001
From: Sabba Petri
Date: Mon, 23 Feb 2015 11:51:18 -0800
Subject: [PATCH 030/109] Added Profile tooltip
For consistency sake, the profile in the navbar has a tooltip.
---
app/views/layouts/_head_panel.html.haml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml
index 77bfe4f996..d5928d2ed2 100644
--- a/app/views/layouts/_head_panel.html.haml
+++ b/app/views/layouts/_head_panel.html.haml
@@ -42,7 +42,7 @@
= link_to destroy_user_session_path, class: "logout", method: :delete, title: "Logout", class: 'has_bottom_tooltip', 'data-original-title' => 'Logout' do
%i.fa.fa-sign-out
%li.hidden-xs
- = link_to current_user, class: "profile-pic", id: 'profile-pic' do
+ = link_to current_user, class: "profile-pic has_bottom_tooltip", id: 'profile-pic', 'data-original-title' => 'Your profile' do
= image_tag avatar_icon(current_user.email, 60), alt: 'User activity'
= render 'shared/outdated_browser'
From e35fe204795bbb19d46f34af49c1ad4f7148e68f Mon Sep 17 00:00:00 2001
From: Sabba Petri
Date: Mon, 23 Feb 2015 13:38:04 -0800
Subject: [PATCH 031/109] Filter icons look like proper buttons
The filter icons (Push events, Merge events, Comments, Team) now have a border
that signifies they can be pushed. This not only keeps it in line with the rest
of the application buttons, but it makes it more obvious, especially for the
Merge events button with a checkbox icon.
---
app/helpers/events_helper.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 063916a8df..db0d4a2661 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -30,7 +30,7 @@ module EventsHelper
end
content_tag :li, class: "filter_icon #{active}" do
- link_to request.path, class: 'has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => tooltip do
+ link_to request.path, class: 'btn has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => tooltip do
icon(icon_for_event[key]) + content_tag(:span, ' ' + tooltip)
end
end
From b3fd0ca04d498504e93894378be98dc1bda7e259 Mon Sep 17 00:00:00 2001
From: Sabba Petri
Date: Mon, 23 Feb 2015 14:27:52 -0800
Subject: [PATCH 032/109] Toggle sidebar button more obvious
The toggle is now at the top of the sidebar because it is not noticeable
near the bottom. By placing it at the top, users will immediately know that
they can have more space if they desire versus on the bottom, they will have
to search for it and that's not desired.
Fixes #2044
---
.../stylesheets/sections/nav_sidebar.scss | 18 +++++++++++++-----
app/views/layouts/_collapse_button.html.haml | 4 ++--
2 files changed, 15 insertions(+), 7 deletions(-)
diff --git a/app/assets/stylesheets/sections/nav_sidebar.scss b/app/assets/stylesheets/sections/nav_sidebar.scss
index 17923ca499..8841068b6a 100644
--- a/app/assets/stylesheets/sections/nav_sidebar.scss
+++ b/app/assets/stylesheets/sections/nav_sidebar.scss
@@ -108,7 +108,7 @@
width: $sidebar_width;
.nav-sidebar {
- margin-top: 20px;
+ margin-top: 29px;
position: fixed;
top: 45px;
width: $sidebar_width;
@@ -127,7 +127,7 @@
width: 52px;
.nav-sidebar {
- margin-top: 20px;
+ margin-top: 29px;
position: fixed;
top: 45px;
width: 52px;
@@ -144,14 +144,22 @@
}
}
}
+
+ .collapse-nav a {
+ left: 0px;
+ padding: 5px 23px 3px 22px;
+ }
}
}
.collapse-nav a {
position: fixed;
- bottom: 15px;
- padding: 10px;
- background: #DDD;
+ top: 47px;
+ padding: 5px 13px 3px 13px;
+ left: 197px;
+ background: #EEE;
+ color: black;
+ border: 1px solid rgba(0,0,0,0.035);
}
@media (max-width: $screen-md-max) {
diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml
index b3b338b55b..2ed51d87ca 100644
--- a/app/views/layouts/_collapse_button.html.haml
+++ b/app/views/layouts/_collapse_button.html.haml
@@ -1,4 +1,4 @@
- if nav_menu_collapsed?
- = link_to icon('angle-right'), '#', class: 'toggle-nav-collapse'
+ = link_to icon('angle-right'), '#', class: 'toggle-nav-collapse', title: "Open/Close"
- else
- = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse'
+ = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Open/Close"
From 746dd89ab010299f731c195082087d32f25698df Mon Sep 17 00:00:00 2001
From: DJ Mountney
Date: Mon, 23 Feb 2015 16:19:03 -0800
Subject: [PATCH 033/109] Fix 404 when deleting a project
The deletion from the admin section was redirecting to the wrong address.
---
app/controllers/projects_controller.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 38341b1c8c..d1583e6ebf 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -102,7 +102,7 @@ class ProjectsController < ApplicationController
flash[:alert] = 'Project deleted.'
if request.referer.include?('/admin')
- redirect_to admin_namespace_projects_path
+ redirect_to admin_namespaces_projects_path
else
redirect_to projects_dashboard_path
end
From f6add983d332758cd1ae22732da762cff608af1e Mon Sep 17 00:00:00 2001
From: Marcin Kulik
Date: Tue, 24 Feb 2015 09:42:07 +0000
Subject: [PATCH 034/109] Use proper Gitorious icons on import pages
---
app/assets/images/gitorious-logo-black.png | Bin 0 -> 809 bytes
app/assets/images/gitorious-logo-blue.png | Bin 0 -> 495 bytes
app/assets/stylesheets/sections/import.scss | 18 ++++++++++++++++++
app/views/import/gitorious/status.html.haml | 2 +-
app/views/projects/new.html.haml | 2 +-
5 files changed, 20 insertions(+), 2 deletions(-)
create mode 100644 app/assets/images/gitorious-logo-black.png
create mode 100644 app/assets/images/gitorious-logo-blue.png
create mode 100644 app/assets/stylesheets/sections/import.scss
diff --git a/app/assets/images/gitorious-logo-black.png b/app/assets/images/gitorious-logo-black.png
new file mode 100644
index 0000000000000000000000000000000000000000..78f17a9af79dc2395acd9bcc2a391589dd6b6ed8
GIT binary patch
literal 809
zcmV+^1J?YBP)G*X{kliLPwpCH_Ux^-m~D{fnn~wbKd#g`|kapbKi6DEOLf+
zN&$y=oMu=FqBx%JuhX%pIz@#hg8*wUQgI3eX`+i+7xFy6Z;2lf|+O3eT=?{Zlr#Q=P_NZ+T60H7bTbD9FnDV;VH
zA`f-gCtb7}g3r*Sy@opADbZ*TJ;hVP&=#Ra2}uMH0sJEX7DqETm;o1l*~3KvX!H+o
zQ0i^HOB?%bV!@DH<~e@foyBO$F^fs>*LGt_2#fNb_bvDo%C3kzo=3CYJPiS$A6~ShAh|z})|$Rug%3K+rg~Bm!|iNR1|hf}VBDE?U{j
nDy!$>sq%+$UhqH%QaR#Z&H$a*+H(Ht00000NkvXXu0mjfir-pq
literal 0
HcmV?d00001
diff --git a/app/assets/images/gitorious-logo-blue.png b/app/assets/images/gitorious-logo-blue.png
new file mode 100644
index 0000000000000000000000000000000000000000..4962cffba3153927ddd8cd5ff295fe4ca142bfa8
GIT binary patch
literal 495
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Y)RhkE)4%caKYZ?lYt_f1s;*b
z3=De8Ak0{?)V>TT$X?><>&kwgNu1AE_|Z?p9}EnPHJ&bxAr-gY&e|w=$biT7sZ8;T
z6d|?~LR)eirt)S)HZdA`dg%&j74B4Skjc^Z`dKOmc^xlq#JR8X;C&kdvr$f8mXRJ2aS*_MNDR*_yoTOyvF(6<;Njo*gv=n{#vX#_O67_!?$*J9p?F
z<+tp;ShYZ<FVdQ
I&MBb@0GSiJ)c^nh
literal 0
HcmV?d00001
diff --git a/app/assets/stylesheets/sections/import.scss b/app/assets/stylesheets/sections/import.scss
new file mode 100644
index 0000000000..3df4bb84bd
--- /dev/null
+++ b/app/assets/stylesheets/sections/import.scss
@@ -0,0 +1,18 @@
+i.icon-gitorious {
+ display: inline-block;
+ background-position: 0px 0px;
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+i.icon-gitorious-small {
+ background-image: image-url('gitorious-logo-blue.png');
+ width: 13px;
+ height: 13px;
+}
+
+i.icon-gitorious-big {
+ background-image: image-url('gitorious-logo-black.png');
+ width: 18px;
+ height: 18px;
+}
diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml
index 35ed0a717d..8ede5c3e84 100644
--- a/app/views/import/gitorious/status.html.haml
+++ b/app/views/import/gitorious/status.html.haml
@@ -1,5 +1,5 @@
%h3.page-title
- %i.fa.fa-gitorious
+ %i.icon-gitorious.icon-gitorious-big
Import repositories from Gitorious.org
%p.light
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 33162ded4a..c37ae8d31d 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -70,7 +70,7 @@
.col-sm-2
.col-sm-10
= link_to new_import_gitorious_path do
- %i.fa.fa-heart
+ %i.icon-gitorious.icon-gitorious-small
Import projects from Gitorious.org
%hr.prepend-botton-10
From 71a844cdaee129d4e300c20cbb27db009cf81b73 Mon Sep 17 00:00:00 2001
From: Valery Sizov
Date: Sat, 14 Feb 2015 18:18:05 +0200
Subject: [PATCH 035/109] Web Editor: save to new branch
---
app/controllers/projects/blob_controller.rb | 26 ++++++++++++++++---
app/services/files/create_service.rb | 3 ++-
app/services/files/update_service.rb | 3 ++-
app/views/projects/blob/edit.html.haml | 7 +++++
app/views/projects/blob/new.html.haml | 7 +++++
.../satellite/files/edit_file_action.rb | 6 +++--
lib/gitlab/satellite/files/new_file_action.rb | 10 +++++--
7 files changed, 53 insertions(+), 9 deletions(-)
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 1207548eae..4b7eb4df29 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -1,6 +1,7 @@
# Controller for viewing a file's blame
class Projects::BlobController < Projects::ApplicationController
include ExtractsPath
+ include ActionView::Helpers::SanitizeHelper
# Raised when given an invalid file path
class InvalidPathError < StandardError; end
@@ -21,11 +22,18 @@ class Projects::BlobController < Projects::ApplicationController
def create
file_path = File.join(@path, File.basename(params[:file_name]))
- result = Files::CreateService.new(@project, current_user, params, @ref, file_path).execute
+ result = Files::CreateService.new(
+ @project,
+ current_user,
+ params.merge(new_branch: sanitized_new_branch_name),
+ @ref,
+ file_path
+ ).execute
if result[:status] == :success
flash[:notice] = "Your changes have been successfully committed"
- redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@ref, file_path))
+ ref = sanitized_new_branch_name.presence || @ref
+ redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(ref, file_path))
else
flash[:alert] = result[:message]
render :new
@@ -41,7 +49,13 @@ class Projects::BlobController < Projects::ApplicationController
def update
result = Files::UpdateService.
- new(@project, current_user, params, @ref, @path).execute
+ new(
+ @project,
+ current_user,
+ params.merge(new_branch: sanitized_new_branch_name),
+ @ref,
+ @path
+ ).execute
if result[:status] == :success
flash[:notice] = "Your changes have been successfully committed"
@@ -131,6 +145,8 @@ class Projects::BlobController < Projects::ApplicationController
if from_merge_request
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
"#file-path-#{hexdigest(@path)}"
+ elsif sanitized_new_branch_name.present?
+ namespace_project_blob_path(@project.namespace, @project, File.join(sanitized_new_branch_name, @path))
else
namespace_project_blob_path(@project.namespace, @project, @id)
end
@@ -140,4 +156,8 @@ class Projects::BlobController < Projects::ApplicationController
# If blob edit was initiated from merge request page
@from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id])
end
+
+ def sanitized_new_branch_name
+ @new_branch ||= sanitize(strip_tags(params[:new_branch]))
+ end
end
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index 2c457ef2ce..de5322e990 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -38,7 +38,8 @@ module Files
created_successfully = new_file_action.commit!(
params[:content],
params[:commit_message],
- params[:encoding]
+ params[:encoding],
+ params[:new_branch]
)
if created_successfully
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index bcf0e7f3ce..328cf3a4b0 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -23,7 +23,8 @@ module Files
edit_file_action.commit!(
params[:content],
params[:commit_message],
- params[:encoding]
+ params[:encoding],
+ params[:new_branch]
)
success
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 6884ad1f2f..1f61a0b940 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -14,6 +14,13 @@
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/commit_message_container', params: params,
placeholder: "Update #{@blob.name}"
+
+ .form-group.branch
+ = label_tag 'branch', class: 'control-label' do
+ Branch
+ .col-sm-10
+ = text_field_tag 'new_branch', @ref, class: "form-control"
+
= hidden_field_tag 'last_commit', @last_commit
= hidden_field_tag 'content', '', id: "file-content"
= hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 45865d552a..d78a01f642 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -4,6 +4,13 @@
= render 'projects/blob/editor', ref: @ref
= render 'shared/commit_message_container', params: params,
placeholder: 'Add new file'
+
+ .form-group.branch
+ = label_tag 'branch', class: 'control-label' do
+ Branch
+ .col-sm-10
+ = text_field_tag 'new_branch', @ref, class: "form-control"
+
= hidden_field_tag 'content', '', id: 'file-content'
= render 'projects/commit_button', ref: @ref,
cancel_path: namespace_project_tree_path(@project.namespace, @project, @id)
diff --git a/lib/gitlab/satellite/files/edit_file_action.rb b/lib/gitlab/satellite/files/edit_file_action.rb
index 82d71ab990..3cb9c0b5ec 100644
--- a/lib/gitlab/satellite/files/edit_file_action.rb
+++ b/lib/gitlab/satellite/files/edit_file_action.rb
@@ -10,7 +10,7 @@ module Gitlab
# Returns false if committing the change fails
# Returns false if pushing from the satellite to bare repo failed or was rejected
# Returns true otherwise
- def commit!(content, commit_message, encoding)
+ def commit!(content, commit_message, encoding, new_branch = nil)
in_locked_and_timed_satellite do |repo|
prepare_satellite!(repo)
@@ -42,10 +42,12 @@ module Gitlab
end
+ target_branch = new_branch.present? ? "#{ref}:#{new_branch}" : ref
+
# push commit back to bare repo
# will raise CommandFailed when push fails
begin
- repo.git.push({ raise: true, timeout: true }, :origin, ref)
+ repo.git.push({ raise: true, timeout: true }, :origin, target_branch)
rescue Grit::Git::CommandFailed => ex
log_and_raise(PushFailed, ex.message)
end
diff --git a/lib/gitlab/satellite/files/new_file_action.rb b/lib/gitlab/satellite/files/new_file_action.rb
index 69f7ffa94e..724dfa0d04 100644
--- a/lib/gitlab/satellite/files/new_file_action.rb
+++ b/lib/gitlab/satellite/files/new_file_action.rb
@@ -9,7 +9,7 @@ module Gitlab
# Returns false if committing the change fails
# Returns false if pushing from the satellite to bare repo failed or was rejected
# Returns true otherwise
- def commit!(content, commit_message, encoding)
+ def commit!(content, commit_message, encoding, new_branch = nil)
in_locked_and_timed_satellite do |repo|
prepare_satellite!(repo)
@@ -45,9 +45,15 @@ module Gitlab
# will raise CommandFailed when commit fails
repo.git.commit(raise: true, timeout: true, a: true, m: commit_message)
+ target_branch = if new_branch.present? && !@project.empty_repo?
+ "#{ref}:#{new_branch}"
+ else
+ "#{current_ref}:#{ref}"
+ end
+
# push commit back to bare repo
# will raise CommandFailed when push fails
- repo.git.push({ raise: true, timeout: true }, :origin, "#{current_ref}:#{ref}")
+ repo.git.push({ raise: true, timeout: true }, :origin, target_branch)
# everything worked
true
From 3177693c6f47fda14f84abf0b8f694aec3e076fc Mon Sep 17 00:00:00 2001
From: Valery Sizov
Date: Mon, 23 Feb 2015 21:00:30 +0200
Subject: [PATCH 036/109] WebEditor: save to new branch: spinach
---
features/project/source/browse_files.feature | 22 +++++++++++++++++++
features/steps/project/source/browse_files.rb | 15 ++++++++++++-
2 files changed, 36 insertions(+), 1 deletion(-)
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index ee8d0bffa9..90b966dd64 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -34,6 +34,17 @@ Feature: Project Source Browse Files
Then I am redirected to the new file
And I should see its new content
+ @javascript
+ Scenario: I can create and commit file and specify new branch
+ Given I click on "new file" link in repo
+ And I edit code
+ And I fill the new file name
+ And I fill the commit message
+ And I fill the new branch name
+ And I click on "Commit Changes"
+ Then I am redirected to the new file on new branch
+ And I should see its new content
+
@javascript @tricky
Scenario: I can create file in empty repo
Given I own an empty project
@@ -83,6 +94,17 @@ Feature: Project Source Browse Files
Then I am redirected to the ".gitignore"
And I should see its new content
+ @javascript
+ Scenario: I can edit and commit file to new branch
+ Given I click on ".gitignore" file in repo
+ And I click button "Edit"
+ And I edit code
+ And I fill the commit message
+ And I fill the new branch name
+ And I click on "Commit Changes"
+ Then I am redirected to the ".gitignore" on new branch
+ And I should see its new content
+
@javascript @wip
Scenario: If I don't change the content of the file I see an error message
Given I click on ".gitignore" file in repo
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 98d8a60e1a..557555aee5 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -69,6 +69,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
fill_in :file_name, with: new_file_name
end
+ step 'I fill the new branch name' do
+ fill_in :new_branch, with: 'new_branch_name'
+ end
+
step 'I fill the new file name with an illegal name' do
fill_in :file_name, with: '.git'
end
@@ -148,6 +152,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'master/.gitignore'))
end
+ step 'I am redirected to the ".gitignore" on new branch' do
+ expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'new_branch_name/.gitignore'))
+ end
+
step 'I am redirected to the permalink URL' do
expect(current_path).to(
eq(namespace_project_blob_path(@project.namespace, @project,
@@ -161,6 +169,11 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
@project.namespace, @project, 'master/' + new_file_name))
end
+ step 'I am redirected to the new file on new branch' do
+ expect(current_path).to eq(namespace_project_blob_path(
+ @project.namespace, @project, 'new_branch_name/' + new_file_name))
+ end
+
step "I don't see the permalink link" do
expect(page).not_to have_link('permalink')
end
@@ -177,7 +190,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
click_link 'add a file'
# Remove pre-receive hook so we can push without auth
- FileUtils.rm(File.join(@project.repository.path, 'hooks', 'pre-receive'))
+ FileUtils.rm_f(File.join(@project.repository.path, 'hooks', 'pre-receive'))
end
private
From 51b18fe6f6558f14e5a8123d27f65f3557134822 Mon Sep 17 00:00:00 2001
From: Igor Bogoslavskyi
Date: Tue, 24 Feb 2015 10:50:18 +0100
Subject: [PATCH 037/109] typo fixed
fixed a typo in an url to gitlab-ssl. Now it does not return the 404 error
---
doc/update/6.x-or-7.x-to-7.8.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/doc/update/6.x-or-7.x-to-7.8.md b/doc/update/6.x-or-7.x-to-7.8.md
index 5884312c47..673d9253d6 100644
--- a/doc/update/6.x-or-7.x-to-7.8.md
+++ b/doc/update/6.x-or-7.x-to-7.8.md
@@ -179,7 +179,7 @@ sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab
### Change Nginx settings
* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/lib/support/nginx/gitlab but with your settings.
-* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stablef/lib/support/nginx/gitlab-ssl but with your settings.
+* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/lib/support/nginx/gitlab-ssl but with your settings.
* A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section.
## 9. Start application
From 63c4f3ca7665d076ea143991823ec99c8f996a73 Mon Sep 17 00:00:00 2001
From: Valery Sizov
Date: Tue, 24 Feb 2015 13:20:04 +0200
Subject: [PATCH 038/109] update gitlab-grack to 2.0.0.rc2
---
Gemfile | 2 +-
Gemfile.lock | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Gemfile b/Gemfile
index 093f7bacc0..00d8f4429f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -41,7 +41,7 @@ gem "browser"
gem "gitlab_git", '7.0.0.rc14'
# Ruby/Rack Git Smart-HTTP Server Handler
-gem 'gitlab-grack', '~> 2.0.0.pre', require: 'grack'
+gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack'
# LDAP Auth
gem 'gitlab_omniauth-ldap', '1.2.0', require: "omniauth-ldap"
diff --git a/Gemfile.lock b/Gemfile.lock
index 4bc47836e7..07cdbc2d75 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -183,7 +183,7 @@ GEM
gitlab-flowdock-git-hook (0.4.2.2)
gitlab-grit (>= 2.4.1)
multi_json
- gitlab-grack (2.0.0.pre)
+ gitlab-grack (2.0.0.rc2)
rack (~> 1.5.1)
gitlab-grit (2.7.2)
charlock_holmes (~> 0.6)
@@ -668,7 +668,7 @@ DEPENDENCIES
gemnasium-gitlab-service (~> 0.2)
github-markup
gitlab-flowdock-git-hook (~> 0.4.2)
- gitlab-grack (~> 2.0.0.pre)
+ gitlab-grack (~> 2.0.0.rc2)
gitlab-linguist (~> 3.0.0)
gitlab_emoji (~> 0.0.1.1)
gitlab_git (= 7.0.0.rc14)
From 9338c6325263d950966e87ddb23095075f18558e Mon Sep 17 00:00:00 2001
From: kfei
Date: Wed, 17 Dec 2014 00:53:17 -0800
Subject: [PATCH 039/109] Gracefully shutdown services in Docker container
The problem is `docker stop` only sends SIGTERM to the PID 1 inside the
container, and the PID 1 (`/bin/sh -c ...`) does not take care of
signals. Hence the services (e.g., postgresql, redis, sidekiq, etc)
never have chances to graceful shutdown. Docker just kills the container
after its 10 seconds timeout by default.
What this commit does:
1) Add a wrapper as the default executable of Docker container. Which
starts services through `runit`, reconfigure Gitlab by `gitlab-ctl`
and gracefully shutdown all services when a SIGTERM is received.
2) Create an `assets` directory for assets.
3) Add `.dockerignore` file.
Now you'll see the following log messages after `docker stop`:
```
SIGTERM signal received, try to gracefully shutdown all services...
ok: down: logrotate: 1s, normally up
ok: down: nginx: 0s, normally up
ok: down: postgresql: 1s, normally up
ok: down: redis: 0s, normally up
ok: down: sidekiq: 0s, normally up
ok: down: unicorn: 0s, normally up
```
Signed-off-by: kfei
---
docker/.dockerignore | 1 +
docker/Dockerfile | 11 +++++++----
docker/{ => assets}/gitlab.rb | 0
docker/assets/wrapper | 17 +++++++++++++++++
4 files changed, 25 insertions(+), 4 deletions(-)
create mode 100644 docker/.dockerignore
rename docker/{ => assets}/gitlab.rb (100%)
create mode 100755 docker/assets/wrapper
diff --git a/docker/.dockerignore b/docker/.dockerignore
new file mode 100644
index 0000000000..dd449725e1
--- /dev/null
+++ b/docker/.dockerignore
@@ -0,0 +1 @@
+*.md
diff --git a/docker/Dockerfile b/docker/Dockerfile
index cfb89357a6..3a0a55e18e 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -26,9 +26,12 @@ RUN mkdir -p /opt/gitlab/sv/sshd/supervise \
# Expose web & ssh
EXPOSE 80 22
-# Volume & configuration
+# Declare volumes
VOLUME ["/var/opt/gitlab", "/var/log/gitlab", "/etc/gitlab"]
-ADD gitlab.rb /etc/gitlab/
-# Default is to run runit & reconfigure
-CMD gitlab-ctl reconfigure & /opt/gitlab/embedded/bin/runsvdir-start
\ No newline at end of file
+# Copy assets
+COPY assets/gitlab.rb /etc/gitlab/
+COPY assets/wrapper /usr/local/bin/
+
+# Wrapper to handle signal, trigger runit and reconfigure GitLab
+CMD ["/usr/local/bin/wrapper"]
diff --git a/docker/gitlab.rb b/docker/assets/gitlab.rb
similarity index 100%
rename from docker/gitlab.rb
rename to docker/assets/gitlab.rb
diff --git a/docker/assets/wrapper b/docker/assets/wrapper
new file mode 100755
index 0000000000..9e6e7a0590
--- /dev/null
+++ b/docker/assets/wrapper
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+function sigterm_handler() {
+ echo "SIGTERM signal received, try to gracefully shutdown all services..."
+ gitlab-ctl stop
+}
+
+trap "sigterm_handler; exit" TERM
+
+function entrypoint() {
+ # Default is to run runit and reconfigure GitLab
+ gitlab-ctl reconfigure &
+ /opt/gitlab/embedded/bin/runsvdir-start &
+ wait
+}
+
+entrypoint
From 1e1a95f5c36357b03df9420903d421edf7ac08c4 Mon Sep 17 00:00:00 2001
From: Bugagazavr
Date: Tue, 24 Feb 2015 15:12:25 +0300
Subject: [PATCH 040/109] Update Changelog
---
CHANGELOG | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG b/CHANGELOG
index d879ee8572..deda7ffc32 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -6,6 +6,7 @@ v 7.9.0 (unreleased)
- Improve error messages for file edit failures
- Improve UI for commits, issues and merge request lists
- Fix commit comments on first line of diff not rendering in Merge Request Discussion view.
+ - Improve trigger merge request hook when source project branch has been updated (Kirill Zaitsev)
v 7.8.0
- Fix access control and protection against XSS for note attachments and other uploads.
From ad6d6232342558705c54ba70a94f9d7ddbd00f8c Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Tue, 17 Feb 2015 16:59:50 +0100
Subject: [PATCH 041/109] Add Bitbucket importer.
---
CHANGELOG | 2 +
Gemfile | 1 +
Gemfile.lock | 5 +
.../images/authbuttons/bitbucket_32.png | Bin 0 -> 2713 bytes
.../images/authbuttons/bitbucket_64.png | Bin 0 -> 2163 bytes
.../import/bitbucket_controller.rb | 74 +++++++++++++++
app/helpers/oauth_helper.rb | 4 +-
app/helpers/projects_helper.rb | 4 +
app/models/project.rb | 2 +-
app/models/user.rb | 1 +
app/views/import/base/create.js.haml | 7 ++
app/views/import/bitbucket/status.html.haml | 44 +++++++++
app/views/import/github/status.html.haml | 2 +-
app/views/import/gitlab/status.html.haml | 2 +-
.../_bitbucket_import_modal.html.haml | 9 ++
app/views/projects/new.html.haml | 13 +++
app/workers/repository_import_worker.rb | 2 +
config/gitlab.yml.example | 18 ++--
config/routes.rb | 5 +
...tbucket_access_token_and_secret_to_user.rb | 6 ++
db/schema.rb | 40 ++++----
doc/integration/bitbucket.m | 1 +
lib/gitlab/bitbucket_import/client.rb | 88 ++++++++++++++++++
lib/gitlab/bitbucket_import/importer.rb | 52 +++++++++++
lib/gitlab/bitbucket_import/key_adder.rb | 22 +++++
.../bitbucket_import/project_creator.rb | 39 ++++++++
lib/gitlab/github_import/client.rb | 6 +-
lib/gitlab/github_import/importer.rb | 2 +-
lib/gitlab/gitlab_import/client.rb | 10 +-
lib/gitlab/gitlab_import/importer.rb | 2 +-
lib/gitlab/import_formatter.rb | 2 +-
.../import/bitbucket_controller_spec.rb | 77 +++++++++++++++
.../bitbucket_import/project_creator_spec.rb | 22 +++++
.../project_creator_spec.rb} | 7 +-
...ect_creator.rb => project_creator_spec.rb} | 5 +-
35 files changed, 522 insertions(+), 54 deletions(-)
create mode 100644 app/assets/images/authbuttons/bitbucket_32.png
create mode 100644 app/assets/images/authbuttons/bitbucket_64.png
create mode 100644 app/controllers/import/bitbucket_controller.rb
create mode 100644 app/views/import/bitbucket/status.html.haml
create mode 100644 app/views/projects/_bitbucket_import_modal.html.haml
create mode 100644 db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb
create mode 100644 doc/integration/bitbucket.m
create mode 100644 lib/gitlab/bitbucket_import/client.rb
create mode 100644 lib/gitlab/bitbucket_import/importer.rb
create mode 100644 lib/gitlab/bitbucket_import/key_adder.rb
create mode 100644 lib/gitlab/bitbucket_import/project_creator.rb
create mode 100644 spec/controllers/import/bitbucket_controller_spec.rb
create mode 100644 spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
rename spec/lib/gitlab/{github/project_creator.rb => github_import/project_creator_spec.rb} (77%)
rename spec/lib/gitlab/gitlab_import/{project_creator.rb => project_creator_spec.rb} (91%)
diff --git a/CHANGELOG b/CHANGELOG
index d879ee8572..5a3a2ca2e2 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -72,6 +72,8 @@ v 7.8.0
- Improve database performance for GitLab
- Add Asana service (Jeremy Benoist)
- Improve project web hooks with extra data
+ - Add Bitbucket omniauth provider.
+ - Add Bitbucket importer.
v 7.7.2
- Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch
diff --git a/Gemfile b/Gemfile
index 093f7bacc0..ad01b2b43e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -30,6 +30,7 @@ gem 'omniauth-github'
gem 'omniauth-shibboleth'
gem 'omniauth-kerberos'
gem 'omniauth-gitlab'
+gem 'omniauth-bitbucket'
gem 'doorkeeper', '2.1.0'
gem "rack-oauth2", "~> 1.0.5"
diff --git a/Gemfile.lock b/Gemfile.lock
index 4bc47836e7..226591a50a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -338,6 +338,10 @@ GEM
omniauth (1.1.4)
hashie (>= 1.2, < 3)
rack
+ omniauth-bitbucket (0.0.2)
+ multi_json (~> 1.7)
+ omniauth (~> 1.1)
+ omniauth-oauth (~> 1.0)
omniauth-github (1.1.1)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
@@ -701,6 +705,7 @@ DEPENDENCIES
nprogress-rails
octokit (= 3.7.0)
omniauth (~> 1.1.3)
+ omniauth-bitbucket
omniauth-github
omniauth-gitlab
omniauth-google-oauth2
diff --git a/app/assets/images/authbuttons/bitbucket_32.png b/app/assets/images/authbuttons/bitbucket_32.png
new file mode 100644
index 0000000000000000000000000000000000000000..27702eb973d1c3c0b9a39eee082ca72d38339cc2
GIT binary patch
literal 2713
zcmZ`*3p~^78y~tb)c!~k#|$NxjV;v7!WfgG5i4^UCKKD*9CHv4hY%>T%6zJQtvd&<}w?H)3
zN>;-BU|cc-W(YNa{Yb>7`hQ>E|ENTkpUNm)>Q7`M%U9WQF}|)5C9EhCPxqtxhgy-@
z6g1qx&`93^u5W1KVu(N*z>&LmqhM<|-!oiRW0({&o6dBh(}U1XR4yebSTqGApm3<+
zSM6%XDzPXJq$86`5f)ffMzoOrf7n_+h91mdQdlez-)M!u8e7XJP+0UJj>!E%8kPMm
z#2R?5>d<#P+62B5W;M80K>U$lHMmM3+6d%wrHEG4UC7Pe
zQbfrtOIAT^*~?)L{VdEX!-^0E6P@K%XZ!2LSw@972MrYN=)boR4V?6NKMeqEIEAya
za3M;iyHSH&tW}m;!fReeSH{}}mI6~jcys)iB0=5BGF}sl`3n+YZ;O$J@IZ?nes^n&
zVMxiPE5+>C5hK?=kso^kSgz2{+>*@LD!t`J?5}4`hUotDW^;O+)GEYu?$U29S8D?<
z&^S^XE;#2|fhsd3Vs*^%8y;{|c`}5CvW+J=>n%AJr!;a*o!-?%&EW@RNc8OHb%h>o
zF-;RBIoOUa75KSzwkaCpU9?ze^Y-rg-dD
zdg^!*2o}?-_JX99D2`I3ROIS!vhg)dzustZps>=aIeNjk7FO-#(sOdosaH#d^M@i3
z%(x?7xusy-$fcl_oAgLhtDb*Wt+A%^eTUrftQ1d_8z?}sF(-y`rY5kiS#3V_Wht~Z
zW^^0L$cr;%)yi4Wj*aWNE2sO5SG3vem(qeBLtqA`$=1i=pDd!g*M;h$Ka!2Z1{L&P
z$u@NZf_5eq0f1l&S{OV&mPl?C(mI7m8cL$%eTDg+^U9PnzA3Ov%|6d%0Te
zS`g9Ebc(KeWu~qhn80iD`wLe%tnqNS4AuJ37Q^mnLuQ@?+4*KJ3&7V?|8P_DC_b$<
z)l}<&HKw6rB+^z7o3{z8gEn~)+z)CXDQ-szq7~_J4>^Lc6gxr8-#mEC)9O*nUX|p#
zwgm!cSI^}R}dZwyi
z#iBa|exJX#W=
zLLDN=Kk`Mj1-!TVKv|et&L`n#Zn}Gx#C>?x3=Az0H(Q^Gwah`NHwnB5Q#a~Huv|`u
zo=y!vPMb(PGn9>V>bS{&iFvSV|CYhxUF(vNBkylbFlzz&mjxaW{;Ai+_+#<+x*Nm0
zb95wMS+D2c2;irTLG8T`B+0*Y=qPQRQcQgt+~X+~*gpo;Ucw(fTO-F7x9ESfSzpn4
zuVdVBXC6#9c9(YL`OSHFsKk$JY9IL3bz!MWY=XoF^(CCcN0_I=@Gf_-u4~C1jW(~8ArF3J!^MM7`oRR
z?RXrp&BGCP9J5u=V*YK!vCxHVy|T=)zK%DSPWwo=ayN5vqvzYA%8pCl&i(Tw;%!4Y
zO(RNHZLr(*T#FZ%-HE@(ypyuW`WCf9{N2@)xgTc(5OTQjLU<8u-9Gb#A
z4a$S!Qja;_`E37OgJ@^{=J0>gyas2C&zVv+s>GpoEssJ9AMEsl3-Xw03~Y)D&Oe~Z
zLAt9`tN}|=9jv$`-&j?CQx7rH3v?-#X`iBw_AZpEbAPke0*>q0K3in!#2R!oI4ck`
zytT2Px~ex*`oQCIL1<6NuUj4HxU;>92OmdrO&n++RkFo9@oJ^y1-r-YV?7@a{1Q+f
zDSMkuzWLUw)J*Auw;Ks5i4)@nR46UA2
zmwM&%krW?sUNxhQeWFV(Zm8x&$|(57B*?obtFGUmCSPH|eB1aZ-I-RD%%KGDB5+zJ
z@3l-Df*N8T1$_f&iZy>$j_z@Upo;^dL5P;d$2s(SrrMKzma{Ei%zdX$N7R5c?nwXWa;9p*_m)j*51XX05;U$nbepp!G-TG;r^8FVk9I3cu(*80QB)VcObzvy~
zft2JC1X3qrqa`mXGxAE6qZ1gHe-wRu(deHgt{}OJ`kdWr-RvBqd?~f>w(eZo-=6l$
gqZTZLJY{B5E#UAGZ=PVPRP?`yvv#m5vGk7mFT{djfB*mh
literal 0
HcmV?d00001
diff --git a/app/assets/images/authbuttons/bitbucket_64.png b/app/assets/images/authbuttons/bitbucket_64.png
new file mode 100644
index 0000000000000000000000000000000000000000..4b90a57bc7de93163275f99ea2971e2740af98af
GIT binary patch
literal 2163
zcmV-(2#oiMP)2uhW|vhI6clA;HOc~sCx{2egDXMtKom7mKv^T8ct;fPlX#%Ui}gx8f}-Mq
z8i53&iTVRjz=KFcSXNM@D}srF8g{ySd}H3pR^Ig1Oi%ahB0rw&W_G&jtEa!JuIj3F
z^7%Yyqwm?bInLn?j(6--#~D1u#044R2)luLa0a**G=ha-71#u}0UzW`Amn$q@!C}^
zYc{xsb@XMOj*ua){w;gl<&IN7-g#rmivQY~^Mn6>mLkB7X!IUn2)G{11FIQuzJvIa
zb-uwEhJdaSGTbBrwBcRBncxMm-V$5@a_!(71cG0{a(!T_cO7^RoCbDR5pe~0Mh4V@
zyI4U~s%itn3f}pU^%SdfEw}^J8X4e~D!^j~RN=xIV6)s^pFcxOgSS)Ir(iyK7R&@U
zf@xqfm;lCui@+sd3b+c~0%n1izyh!gYy>+ZvyCxGYI5QLI4}i
z!iiM)3K(9bu^z}qkP)(ij^;{tj!EySgorMahk!K@(2D}RNCE5uFG4^b0$MHoFaVe7
zsA~l70hc7s0OA>Sc=Da;Ahcv@D+J^r;AwUN&o%}$LqGrlL3;wo$cVd~u{1nQ5t<>V
zNERpo1OyQ9GX!*J7jWAXu$~!U6Hs^--wgT`NqtRs&xUpjx#P;doCjT&b2WoT{#MvMFE*80a*%|Dgt65laicFd&5$%C%(@3tYs9>fCdW4+A?4y1+b!u
z5egNphXRJ$GQgvNdIO%xF90s;aG_!a`HD8MbY
z7FbLH{w^TEr+`J)wE)RbKqCd@b^!r73V56XvNi!(3YgXb84wDU&Vb1xz?uQ0DmV!G
z#Qu5ugWw1X$k+sU6mSRx{9_i70zpKB{~*531JZ0f*bV`8G6q-&!5RoyD}g6YL$a7l
z>A|3m4CZGaim2l3c#`nWA1l(7SfD8pRBoL4T1En!H^Y6YT;A`ZXqk*45zyuKxs|yTa
zr6hL$8x36cYnCTlZMw%tX~tM^wv-w7)!g7D0&2mQIQFHFKL8xrEqc+i0D4NE0uFdo
z%8YXZeg*qcfE!1EO95H%wG^?3$+f{h94Uj@9_0{9p#
z+<|%0r{V^2lccmm>cKd0E;yv{xpbM^<2GJmeU;^Kd%QdW_sGZ{w-5dWPBbKV#1TAF
zX#6nnx4cd-l^n0RBMku;h}ClD11(^L>?Bu#YEmV~aYliyQbsu$FgA$*m+9C8d|xn|
zU`?=tgW>^}#ZVvc5NIu@@e<>D(2Z?y%RMFL8Qag>l1d<(**C8S=YX;NeJ=P-v!_&~
zoBnBZJ1^NWITpYI*V#5D<7}oX3CqpN0jU!t_9OxT`zO0YBU4n*4~4Nr06iiXvkW~4
zw1d0Rdr8d61*lDM4Ac0pi2mo>grv0LswdK3VN4QhiURbF4H*D#3l(IC-CV^El=B%ZZR5Cy%EHMV5Ku&7k3kJ2*&5D@|U@co?6_fFw(WGy(AWqzjh5i_QTB<55nLX%`_t8aAj
z!fy^Hoh=4n
zkPLvSjsZPEOR>d@3K4>04Hd!|i>;r)Ua5}(#HH1xZE4u~CqyKcPvo$6DgtyMd|m}~
zA}V0EblG$SNTJhqVgXY+Lcqx~2J8%kzT(Ii9Kvd)ZB3oHV}T0(4N^hf~*P
zThOszblPQZ;9c(_v2>^at++clAIxprM@Wn|dJgY+H;th)1laz&d~Yxu%m8nKFTs`=
puf?};oj;Fd45Q%4{|-+g;9pZ(FBz?c<5BAccess denied! Please verify you can add deploy keys to this repository.
"")
- else
:plain
job = $("tr#repo_#{@repo_id}")
job.attr("id", "project_#{@project.id}")
+ target_field = job.find(".import-target")
+ target_field.empty()
+ target_field.append('#{link_to @project.path_with_namespace, @project}')
$("table.import-jobs tbody").prepend(job)
job.addClass("active").find(".import-actions").html(" started")
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
new file mode 100644
index 0000000000..7a2613e4b0
--- /dev/null
+++ b/app/views/import/bitbucket/status.html.haml
@@ -0,0 +1,44 @@
+%h3.page-title
+ %i.fa.fa-bitbucket
+ Import repositories from Bitbucket
+
+%p.light
+ Select projects you want to import.
+%hr
+%p
+ = button_tag 'Import all projects', class: "btn btn-success js-import-all"
+
+%table.table.import-jobs
+ %thead
+ %tr
+ %th From Bitbucket
+ %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= project.import_source
+ %td
+ %strong= link_to project.path_with_namespace, project
+ %td.job-status
+ - if project.import_status == 'finished'
+ %span.cgreen
+ %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["owner"]}___#{repo["slug"]}"}
+ %td= "#{repo["owner"]}/#{repo["slug"]}"
+ %td.import-target
+ = "#{repo["owner"]}/#{repo["slug"]}"
+ %td.import-actions.job-status
+ = button_tag "Import", class: "btn js-add-to-import"
+
+:coffeescript
+ $ ->
+ new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}")
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 84d9903fe1..b1538b1a41 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -1,6 +1,6 @@
%h3.page-title
%i.fa.fa-github
- Import repositories from GitHub.com
+ Import repositories from GitHub
%p.light
Select projects you want to import.
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index d1e48dfad2..43db102994 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -1,5 +1,5 @@
%h3.page-title
- %i.fa.fa-github
+ %i.fa.fa-heart
Import repositories from GitLab.com
%p.light
diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml
new file mode 100644
index 0000000000..dd7aacc7e6
--- /dev/null
+++ b/app/views/projects/_bitbucket_import_modal.html.haml
@@ -0,0 +1,9 @@
+%div#bitbucket_import_modal.modal.hide
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %h3 GitHub OAuth import
+ .modal-body
+ You need to setup integration with Bitbucket first.
+ = link_to 'How to setup integration with Bitbucket', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/bitbucket.md'
\ No newline at end of file
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 5216f30811..875c092fd1 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -53,6 +53,19 @@
Import projects from GitHub
= render 'github_import_modal'
+ .project-import.form-group
+ .col-sm-2
+ .col-sm-10
+ - if bitbucket_import_enabled?
+ = link_to status_import_bitbucket_path do
+ %i.fa.fa-bitbucket
+ Import projects from Bitbucket
+ - else
+ = link_to '#', class: 'how_to_import_link light' do
+ %i.fa.fa-bitbucket
+ Import projects from Bitbucket
+ = render 'bitbucket_import_modal'
+
- unless request.host == 'gitlab.com'
.project-import.form-group
.col-sm-2
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 5f9970d379..d7e759fb47 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -14,6 +14,8 @@ class RepositoryImportWorker
Gitlab::GithubImport::Importer.new(project).execute
elsif project.import_type == 'gitlab'
Gitlab::GitlabImport::Importer.new(project).execute
+ elsif project.import_type == 'bitbucket'
+ Gitlab::BitbucketImport::Importer.new(project).execute
else
true
end
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 044b1f66b2..6dff07cf9d 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -207,17 +207,19 @@ production: &base
# arguments, followed by optional 'args' which can be either a hash or an array.
# Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html
providers:
- # - { name: 'google_oauth2', app_id: 'YOUR APP ID',
- # app_secret: 'YOUR APP SECRET',
+ # - { name: 'google_oauth2', app_id: 'YOUR_APP_ID',
+ # app_secret: 'YOUR_APP_SECRET',
# args: { access_type: 'offline', approval_prompt: '' } }
- # - { name: 'twitter', app_id: 'YOUR APP ID',
- # app_secret: 'YOUR APP SECRET'}
- # - { name: 'github', app_id: 'YOUR APP ID',
- # app_secret: 'YOUR APP SECRET',
+ # - { name: 'twitter', app_id: 'YOUR_APP_ID',
+ # app_secret: 'YOUR_APP_SECRET'}
+ # - { name: 'github', app_id: 'YOUR_APP_ID',
+ # app_secret: 'YOUR_APP_SECRET',
# args: { scope: 'user:email' } }
- # - { name: 'gitlab', app_id: 'YOUR APP ID',
- # app_secret: 'YOUR APP SECRET',
+ # - { name: 'gitlab', app_id: 'YOUR_APP_ID',
+ # app_secret: 'YOUR_APP_SECRET',
# args: { scope: 'api' } }
+ # - { name: 'bitbucket', app_id: 'YOUR_APP_ID',
+ # app_secret: 'YOUR_APP_SECRET'}
diff --git a/config/routes.rb b/config/routes.rb
index ecd439aece..57964bdc3b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -68,6 +68,11 @@ Gitlab::Application.routes.draw do
get :jobs
end
+ resource :bitbucket, only: [:create, :new], controller: :bitbucket do
+ get :status
+ get :callback
+ get :jobs
+ end
resource :gitorious, only: [:create, :new], controller: :gitorious do
get :status
get :callback
diff --git a/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb b/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb
new file mode 100644
index 0000000000..23ac1b399e
--- /dev/null
+++ b/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb
@@ -0,0 +1,6 @@
+class AddBitbucketAccessTokenAndSecretToUser < ActiveRecord::Migration
+ def change
+ add_column :users, :bitbucket_access_token, :string
+ add_column :users, :bitbucket_access_token_secret, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e11a068c9c..8069d95c69 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: 20150213121042) do
+ActiveRecord::Schema.define(version: 20150217123345) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -410,12 +410,12 @@ ActiveRecord::Schema.define(version: 20150213121042) do
end
create_table "users", force: true do |t|
- t.string "email", default: "", null: false
- t.string "encrypted_password", default: "", null: false
+ t.string "email", default: "", null: false
+ t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
- t.integer "sign_in_count", default: 0
+ t.integer "sign_in_count", default: 0
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
@@ -423,22 +423,22 @@ ActiveRecord::Schema.define(version: 20150213121042) do
t.datetime "created_at"
t.datetime "updated_at"
t.string "name"
- t.boolean "admin", default: false, null: false
- t.integer "projects_limit", default: 10
- t.string "skype", default: "", null: false
- t.string "linkedin", default: "", null: false
- t.string "twitter", default: "", null: false
+ t.boolean "admin", default: false, null: false
+ t.integer "projects_limit", default: 10
+ t.string "skype", default: "", null: false
+ t.string "linkedin", default: "", null: false
+ t.string "twitter", default: "", null: false
t.string "authentication_token"
- t.integer "theme_id", default: 1, null: false
+ t.integer "theme_id", default: 1, null: false
t.string "bio"
- t.integer "failed_attempts", default: 0
+ t.integer "failed_attempts", default: 0
t.datetime "locked_at"
t.string "username"
- t.boolean "can_create_group", default: true, null: false
- t.boolean "can_create_team", default: true, null: false
+ t.boolean "can_create_group", default: true, null: false
+ t.boolean "can_create_team", default: true, null: false
t.string "state"
- t.integer "color_scheme_id", default: 1, null: false
- t.integer "notification_level", default: 1, null: false
+ t.integer "color_scheme_id", default: 1, null: false
+ t.integer "notification_level", default: 1, null: false
t.datetime "password_expires_at"
t.integer "created_by_id"
t.datetime "last_credential_check_at"
@@ -447,13 +447,15 @@ ActiveRecord::Schema.define(version: 20150213121042) do
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
- t.boolean "hide_no_ssh_key", default: false
- t.string "website_url", default: "", null: false
+ t.boolean "hide_no_ssh_key", default: false
+ t.string "website_url", default: "", null: false
t.string "github_access_token"
t.string "gitlab_access_token"
t.string "notification_email"
- t.boolean "hide_no_password", default: false
- t.boolean "password_automatically_set", default: false
+ t.boolean "hide_no_password", default: false
+ t.boolean "password_automatically_set", default: false
+ t.string "bitbucket_access_token"
+ t.string "bitbucket_access_token_secret"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
diff --git a/doc/integration/bitbucket.m b/doc/integration/bitbucket.m
new file mode 100644
index 0000000000..30404ce4c5
--- /dev/null
+++ b/doc/integration/bitbucket.m
@@ -0,0 +1 @@
+TODO
\ No newline at end of file
diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb
new file mode 100644
index 0000000000..3d2ef78ee7
--- /dev/null
+++ b/lib/gitlab/bitbucket_import/client.rb
@@ -0,0 +1,88 @@
+module Gitlab
+ module BitbucketImport
+ class Client
+ attr_reader :consumer, :api
+
+ def initialize(access_token = nil, access_token_secret = nil)
+ @consumer = ::OAuth::Consumer.new(
+ config.app_id,
+ config.app_secret,
+ bitbucket_options
+ )
+
+ if access_token && access_token_secret
+ @api = ::OAuth::AccessToken.new(@consumer, access_token, access_token_secret)
+ end
+ end
+
+ def request_token(redirect_uri)
+ request_token = consumer.get_request_token(oauth_callback: redirect_uri)
+
+ {
+ oauth_token: request_token.token,
+ oauth_token_secret: request_token.secret,
+ oauth_callback_confirmed: request_token.callback_confirmed?.to_s
+ }
+ end
+
+ def authorize_url(request_token, redirect_uri)
+ request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash)
+
+ if request_token.callback_confirmed?
+ request_token.authorize_url
+ else
+ request_token.authorize_url(oauth_callback: redirect_uri)
+ end
+ end
+
+ def get_token(request_token, oauth_verifier, redirect_uri)
+ request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash)
+
+ if request_token.callback_confirmed?
+ request_token.get_access_token(oauth_verifier: oauth_verifier)
+ else
+ request_token.get_access_token(oauth_callback: redirect_uri)
+ end
+ end
+
+ def user
+ JSON.parse(api.get("/api/1.0/user").body)
+ end
+
+ def issues(project_identifier)
+ JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues").body)
+ end
+
+ def issue_comments(project_identifier, issue_id)
+ JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body)
+ end
+
+ def project(project_identifier)
+ JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}").body)
+ end
+
+ def deploy_key(project_identifier)
+ JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find { |key| key["label"] =~ /GitLab/ }
+ end
+
+ def add_deploy_key(project_identifier, key)
+ JSON.parse(api.post("/api/1.0/repositories/#{project_identifier}/deploy-keys", key: key, label: "GitLab import key").body)
+ end
+
+ def projects
+ JSON.parse(api.get("/api/1.0/user/repositories").body).
+ select { |repo| repo["scm"] == "git" }
+ end
+
+ private
+
+ def config
+ Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket"}
+ end
+
+ def bitbucket_options
+ OmniAuth::Strategies::Bitbucket.default_options[:client_options]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
new file mode 100644
index 0000000000..42c93707ca
--- /dev/null
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -0,0 +1,52 @@
+module Gitlab
+ module BitbucketImport
+ class Importer
+ attr_reader :project, :client
+
+ def initialize(project)
+ @project = project
+ @client = Client.new(project.creator.bitbucket_access_token, project.creator.bitbucket_access_token_secret)
+ @formatter = Gitlab::ImportFormatter.new
+ end
+
+ def execute
+ project_identifier = project.import_source
+
+ return true unless client.project(project_identifier)["has_issues"]
+
+ #Issues && Comments
+ issues = client.issues(project_identifier)
+
+ issues["issues"].each do |issue|
+ body = @formatter.author_line(issue["reported_by"]["username"], issue["content"])
+
+ comments = client.issue_comments(project_identifier, issue["local_id"])
+
+ if comments.any?
+ body += @formatter.comments_header
+ end
+
+ comments.each do |comment|
+ body += @formatter.comment(comment["author_info"]["username"], comment["utc_created_on"], comment["content"])
+ end
+
+ project.issues.create!(
+ description: body,
+ title: issue["title"],
+ state: %w(resolved invalid duplicate wontfix).include?(issue["status"]) ? 'closed' : 'opened',
+ author_id: gl_user_id(project, issue["reported_by"]["username"])
+ )
+ end
+
+ true
+ end
+
+ private
+
+ def gl_user_id(project, bitbucket_id)
+ user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s)
+ (user && user.id) || project.creator_id
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb
new file mode 100644
index 0000000000..207811237b
--- /dev/null
+++ b/lib/gitlab/bitbucket_import/key_adder.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module BitbucketImport
+ class KeyAdder
+ attr_reader :repo, :current_user, :client
+
+ def initialize(repo, current_user)
+ @repo, @current_user = repo, current_user
+ @client = Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret)
+ end
+
+ def execute
+ project_identifier = "#{repo["owner"]}/#{repo["slug"]}"
+ return true if client.deploy_key(project_identifier)
+
+ # TODO: Point to actual public key.
+ client.add_deploy_key(project_identifier, File.read("/Users/douwemaan/.ssh/id_rsa.pub"))
+
+ true
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb
new file mode 100644
index 0000000000..db33af2c2d
--- /dev/null
+++ b/lib/gitlab/bitbucket_import/project_creator.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module BitbucketImport
+ class ProjectCreator
+ attr_reader :repo, :namespace, :current_user
+
+ def initialize(repo, namespace, current_user)
+ @repo = repo
+ @namespace = namespace
+ @current_user = current_user
+ end
+
+ def execute
+ @project = Project.new(
+ name: repo["name"],
+ path: repo["slug"],
+ description: repo["description"],
+ namespace: namespace,
+ creator: current_user,
+ 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
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index c9904fe877..676d226bdd 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -46,11 +46,7 @@ module Gitlab
end
def github_options
- {
- site: 'https://api.github.com',
- authorize_url: 'https://github.com/login/oauth/authorize',
- token_url: 'https://github.com/login/oauth/access_token'
- }
+ OmniAuth::Strategies::GitHub.default_options[:client_options]
end
end
end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index bc2b645b2d..23832b3233 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -20,7 +20,7 @@ module Gitlab
body += @formatter.comments_header
client.issue_comments(project.import_source, issue.number).each do |c|
- body += @formatter.comment_to_md(c.user.login, c.created_at, c.body)
+ body += @formatter.comment(c.user.login, c.created_at, c.body)
end
end
diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb
index 2206b68da9..ecf4ff94e3 100644
--- a/lib/gitlab/gitlab_import/client.rb
+++ b/lib/gitlab/gitlab_import/client.rb
@@ -9,7 +9,7 @@ module Gitlab
@client = ::OAuth2::Client.new(
config.app_id,
config.app_secret,
- github_options
+ gitlab_options
)
if access_token
@@ -70,12 +70,8 @@ module Gitlab
Gitlab.config.omniauth.providers.find{|provider| provider.name == "gitlab"}
end
- def github_options
- {
- site: 'https://gitlab.com/',
- authorize_url: 'oauth/authorize',
- token_url: 'oauth/token'
- }
+ def gitlab_options
+ OmniAuth::Strategies::GitLab.default_options[:client_options]
end
end
end
diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb
index 5f9b14399a..c5304a0699 100644
--- a/lib/gitlab/gitlab_import/importer.rb
+++ b/lib/gitlab/gitlab_import/importer.rb
@@ -25,7 +25,7 @@ module Gitlab
end
comments.each do |comment|
- body += @formatter.comment_to_md(comment["author"]["name"], comment["created_at"], comment["body"])
+ body += @formatter.comment(comment["author"]["name"], comment["created_at"], comment["body"])
end
project.issues.create!(
diff --git a/lib/gitlab/import_formatter.rb b/lib/gitlab/import_formatter.rb
index ebb4b87f7e..72e041a90b 100644
--- a/lib/gitlab/import_formatter.rb
+++ b/lib/gitlab/import_formatter.rb
@@ -1,6 +1,6 @@
module Gitlab
class ImportFormatter
- def comment_to_md(author, date, body)
+ def comment(author, date, body)
"\n\n*By #{author} on #{date}*\n\n#{body}"
end
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
new file mode 100644
index 0000000000..84e37ae560
--- /dev/null
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Import::BitbucketController do
+ let(:user) { create(:user, bitbucket_access_token: 'asd123', bitbucket_access_token_secret: "sekret") }
+
+ before do
+ sign_in(user)
+ end
+
+ describe "GET callback" do
+ before do
+ session[:oauth_request_token] = {}
+ end
+
+ it "updates access token" do
+ token = "asdasd12345"
+ secret = "sekrettt"
+ access_token = double(token: token, secret: secret)
+ Gitlab::BitbucketImport::Client.any_instance.stub(:get_token).and_return(access_token)
+ Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket")
+
+ get :callback
+
+ expect(user.reload.bitbucket_access_token).to eq(token)
+ expect(user.reload.bitbucket_access_token_secret).to eq(secret)
+ expect(controller).to redirect_to(status_import_bitbucket_url)
+ end
+ end
+
+ describe "GET status" do
+ before do
+ @repo = OpenStruct.new(slug: 'vim', owner: 'asd')
+ end
+
+ it "assigns variables" do
+ @project = create(:project, import_type: 'bitbucket', creator_id: user.id)
+ controller.stub_chain(:client, :projects).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: 'bitbucket', creator_id: user.id, import_source: 'asd/vim')
+ controller.stub_chain(:client, :projects).and_return([@repo])
+
+ get :status
+
+ expect(assigns(:already_added_projects)).to eq([@project])
+ expect(assigns(:repos)).to eq([])
+ end
+ end
+
+ describe "POST create" do
+ before do
+ @repo = {
+ slug: 'vim',
+ owner: "john"
+ }.with_indifferent_access
+ end
+
+ it "takes already existing namespace" do
+ namespace = create(:namespace, name: "john", owner: user)
+ expect(Gitlab::BitbucketImport::KeyAdder).
+ to receive(:new).with(@repo, user).
+ and_return(double(execute: true))
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).with(@repo, namespace, user).
+ and_return(double(execute: true))
+ controller.stub_chain(:client, :project).and_return(@repo)
+
+ post :create, format: :js
+ 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
new file mode 100644
index 0000000000..f552310584
--- /dev/null
+++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Gitlab::BitbucketImport::ProjectCreator do
+ let(:user) { create(:user, bitbucket_access_token: "asdffg", bitbucket_access_token_secret: "sekret") }
+ let(:repo) { {
+ name: 'Vim',
+ slug: 'vim',
+ is_private: true,
+ owner: "asd"}.with_indifferent_access
+ }
+ let(:namespace){ create(:namespace) }
+
+ it 'creates project' do
+ allow_any_instance_of(Project).to receive(:add_import_job)
+
+ project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, user)
+ project = project_creator.execute
+
+ expect(project.import_url).to eq("ssh://git@bitbucket.org/asd/vim.git")
+ expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+end
diff --git a/spec/lib/gitlab/github/project_creator.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb
similarity index 77%
rename from spec/lib/gitlab/github/project_creator.rb
rename to spec/lib/gitlab/github_import/project_creator_spec.rb
index 3686ddbf17..8d594a112d 100644
--- a/spec/lib/gitlab/github/project_creator.rb
+++ b/spec/lib/gitlab/github_import/project_creator_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Github::ProjectCreator do
+describe Gitlab::GithubImport::ProjectCreator do
let(:user) { create(:user, github_access_token: "asdffg") }
let(:repo) { OpenStruct.new(
login: 'vim',
@@ -15,9 +15,8 @@ describe Gitlab::Github::ProjectCreator do
it 'creates project' do
allow_any_instance_of(Project).to receive(:add_import_job)
- project_creator = Gitlab::Github::ProjectCreator.new(repo, namespace, user)
- project_creator.execute
- project = Project.last
+ project_creator = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, user)
+ project = project_creator.execute
expect(project.import_url).to eq("https://asdffg@gitlab.com/asd/vim.git")
expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
diff --git a/spec/lib/gitlab/gitlab_import/project_creator.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
similarity index 91%
rename from spec/lib/gitlab/gitlab_import/project_creator.rb
rename to spec/lib/gitlab/gitlab_import/project_creator_spec.rb
index e5d917830b..4c0d64ed13 100644
--- a/spec/lib/gitlab/gitlab_import/project_creator.rb
+++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::GitlabImport::ProjectCreator do
let(:user) { create(:user, gitlab_access_token: "asdffg") }
- let(:repo) {{
+ let(:repo) { {
name: 'vim',
path: 'vim',
visibility_level: Gitlab::VisibilityLevel::PRIVATE,
@@ -16,8 +16,7 @@ describe Gitlab::GitlabImport::ProjectCreator do
allow_any_instance_of(Project).to receive(:add_import_job)
project_creator = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, user)
- project_creator.execute
- project = Project.last
+ project = project_creator.execute
expect(project.import_url).to eq("https://oauth2:asdffg@gitlab.com/asd/vim.git")
expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
From 448817c4de965bf7286f33a3447937987a8864a1 Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Tue, 17 Feb 2015 22:52:32 +0100
Subject: [PATCH 042/109] Load public key in initializer.
---
app/controllers/application_controller.rb | 13 +++++++++++++
app/controllers/import/bitbucket_controller.rb | 5 +++++
app/controllers/import/github_controller.rb | 5 +++++
app/controllers/import/gitlab_controller.rb | 5 +++++
app/helpers/oauth_helper.rb | 2 ++
app/helpers/projects_helper.rb | 12 ------------
config/initializers/public_key.rb | 2 ++
lib/gitlab/bitbucket_import.rb | 6 ++++++
lib/gitlab/bitbucket_import/key_adder.rb | 9 ++++++---
9 files changed, 44 insertions(+), 15 deletions(-)
create mode 100644 config/initializers/public_key.rb
create mode 100644 lib/gitlab/bitbucket_import.rb
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index eb3be08df5..7940b5cb3f 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -16,6 +16,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :abilities, :can?, :current_application_settings
+ helper_method :github_import_enabled?, :gitlab_import_enabled?, :bitbucket_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
@@ -313,4 +314,16 @@ class ApplicationController < ActionController::Base
set_filter_values(merge_requests)
merge_requests
end
+
+ def github_import_enabled?
+ OauthHelper.enabled_oauth_providers.include?(:github)
+ end
+
+ def gitlab_import_enabled?
+ OauthHelper.enabled_oauth_providers.include?(:gitlab)
+ end
+
+ def bitbucket_import_enabled?
+ OauthHelper.enabled_oauth_providers.include?(:bitbucket) && Gitlab::BitbucketImport.public_key.present?
+ end
end
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 27e91f49f2..89de5c5205 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -1,4 +1,5 @@
class Import::BitbucketController < Import::BaseController
+ before_filter :verify_bitbucket_import_enabled
before_filter :bitbucket_auth, except: :callback
# rescue_from OAuth::Error, with: :bitbucket_unauthorized
@@ -55,6 +56,10 @@ class Import::BitbucketController < Import::BaseController
@client ||= Gitlab::BitbucketImport::Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret)
end
+ def verify_bitbucket_import_enabled
+ not_found! unless bitbucket_import_enabled?
+ end
+
def bitbucket_auth
if current_user.bitbucket_access_token.blank?
go_to_bitbucket_for_permissions
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index c869c7c86f..dc7668ee6f 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -1,4 +1,5 @@
class Import::GithubController < Import::BaseController
+ before_filter :verify_github_import_enabled
before_filter :github_auth, except: :callback
rescue_from Octokit::Unauthorized, with: :github_unauthorized
@@ -44,6 +45,10 @@ class Import::GithubController < Import::BaseController
@client ||= Gitlab::GithubImport::Client.new(current_user.github_access_token)
end
+ def verify_github_import_enabled
+ not_found! unless github_import_enabled?
+ end
+
def github_auth
if current_user.github_access_token.blank?
go_to_github_for_permissions
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index a51ea36aff..74f992b469 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -1,4 +1,5 @@
class Import::GitlabController < Import::BaseController
+ before_filter :verify_gitlab_import_enabled
before_filter :gitlab_auth, except: :callback
rescue_from OAuth2::Error, with: :gitlab_unauthorized
@@ -41,6 +42,10 @@ class Import::GitlabController < Import::BaseController
@client ||= Gitlab::GitlabImport::Client.new(current_user.gitlab_access_token)
end
+ def verify_gitlab_import_enabled
+ not_found! unless gitlab_import_enabled?
+ end
+
def gitlab_auth
if current_user.gitlab_access_token.blank?
go_to_gitlab_for_permissions
diff --git a/app/helpers/oauth_helper.rb b/app/helpers/oauth_helper.rb
index 848d74c18c..1a0ad17b60 100644
--- a/app/helpers/oauth_helper.rb
+++ b/app/helpers/oauth_helper.rb
@@ -20,4 +20,6 @@ module OauthHelper
def additional_providers
enabled_oauth_providers.reject{|provider| provider.to_s.starts_with?('ldap')}
end
+
+ extend self
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 8a48a9d394..c85ad12634 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -265,16 +265,4 @@ module ProjectsHelper
"success"
end
end
-
- def github_import_enabled?
- enabled_oauth_providers.include?(:github)
- end
-
- def gitlab_import_enabled?
- enabled_oauth_providers.include?(:gitlab)
- end
-
- def bitbucket_import_enabled?
- enabled_oauth_providers.include?(:bitbucket)
- end
end
diff --git a/config/initializers/public_key.rb b/config/initializers/public_key.rb
new file mode 100644
index 0000000000..d27e6519d1
--- /dev/null
+++ b/config/initializers/public_key.rb
@@ -0,0 +1,2 @@
+path = File.expand_path("~/.ssh/id_rsa.pub")
+Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path)
\ No newline at end of file
diff --git a/lib/gitlab/bitbucket_import.rb b/lib/gitlab/bitbucket_import.rb
new file mode 100644
index 0000000000..0e53972ac5
--- /dev/null
+++ b/lib/gitlab/bitbucket_import.rb
@@ -0,0 +1,6 @@
+module Gitlab
+ module BitbucketImport
+ mattr_accessor :public_key
+ @public_key = nil
+ end
+end
\ No newline at end of file
diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb
index 207811237b..7d0b5fbc8a 100644
--- a/lib/gitlab/bitbucket_import/key_adder.rb
+++ b/lib/gitlab/bitbucket_import/key_adder.rb
@@ -9,13 +9,16 @@ module Gitlab
end
def execute
+ return false unless BitbucketImport.public_key.present?
+
project_identifier = "#{repo["owner"]}/#{repo["slug"]}"
return true if client.deploy_key(project_identifier)
-
- # TODO: Point to actual public key.
- client.add_deploy_key(project_identifier, File.read("/Users/douwemaan/.ssh/id_rsa.pub"))
+
+ client.add_deploy_key(project_identifier, BitbucketImport.public_key)
true
+ rescue
+ false
end
end
end
From f2b37de54ba3cb0a375fb3a03e7ffd1f18444c39 Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Wed, 18 Feb 2015 08:21:30 +0100
Subject: [PATCH 043/109] Fix specs.
---
config/initializers/public_key.rb | 2 +-
lib/gitlab/bitbucket_import.rb | 2 +-
spec/controllers/import/bitbucket_controller_spec.rb | 1 +
spec/controllers/import/github_controller_spec.rb | 1 +
spec/controllers/import/gitlab_controller_spec.rb | 1 +
5 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/config/initializers/public_key.rb b/config/initializers/public_key.rb
index d27e6519d1..75d74e3625 100644
--- a/config/initializers/public_key.rb
+++ b/config/initializers/public_key.rb
@@ -1,2 +1,2 @@
path = File.expand_path("~/.ssh/id_rsa.pub")
-Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path)
\ No newline at end of file
+Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path)
diff --git a/lib/gitlab/bitbucket_import.rb b/lib/gitlab/bitbucket_import.rb
index 0e53972ac5..7298152e7e 100644
--- a/lib/gitlab/bitbucket_import.rb
+++ b/lib/gitlab/bitbucket_import.rb
@@ -3,4 +3,4 @@ module Gitlab
mattr_accessor :public_key
@public_key = nil
end
-end
\ No newline at end of file
+end
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 84e37ae560..5dd4124061 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -5,6 +5,7 @@ describe Import::BitbucketController do
before do
sign_in(user)
+ controller.stub(:bitbucket_import_enabled?).and_return(true)
end
describe "GET callback" do
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 3b779855d3..b882041340 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -5,6 +5,7 @@ describe Import::GithubController do
before do
sign_in(user)
+ controller.stub(:github_import_enabled?).and_return(true)
end
describe "GET callback" do
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index 287aa315db..b6b86b1bce 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -5,6 +5,7 @@ describe Import::GitlabController do
before do
sign_in(user)
+ controller.stub(:gitlab_import_enabled?).and_return(true)
end
describe "GET callback" do
From 6979b3afd50f86550e523ed66ef22fd153e6cbc8 Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Wed, 18 Feb 2015 17:00:26 +0100
Subject: [PATCH 044/109] Delete deploy key from Bitbucket after importing.
---
app/workers/repository_import_worker.rb | 36 +++++++++++-----------
lib/gitlab/bitbucket_import/client.rb | 19 +++++++++---
lib/gitlab/bitbucket_import/key_adder.rb | 2 --
lib/gitlab/bitbucket_import/key_deleter.rb | 23 ++++++++++++++
4 files changed, 56 insertions(+), 24 deletions(-)
create mode 100644 lib/gitlab/bitbucket_import/key_deleter.rb
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index d7e759fb47..437640d230 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -6,27 +6,27 @@ class RepositoryImportWorker
def perform(project_id)
project = Project.find(project_id)
- result = gitlab_shell.send(:import_repository,
+
+ import_result = gitlab_shell.send(:import_repository,
project.path_with_namespace,
project.import_url)
+ return project.import_fail unless import_result
- result_of_data_import = if project.import_type == 'github'
- Gitlab::GithubImport::Importer.new(project).execute
- elsif project.import_type == 'gitlab'
- Gitlab::GitlabImport::Importer.new(project).execute
- elsif project.import_type == 'bitbucket'
- Gitlab::BitbucketImport::Importer.new(project).execute
- else
- true
- end
+ data_import_result = if project.import_type == 'github'
+ Gitlab::GithubImport::Importer.new(project).execute
+ elsif project.import_type == 'gitlab'
+ Gitlab::GitlabImport::Importer.new(project).execute
+ elsif project.import_type == 'bitbucket'
+ Gitlab::BitbucketImport::Importer.new(project).execute
+ else
+ true
+ end
+ return project.import_fail unless data_import_result
- if result && result_of_data_import
- project.import_finish
- project.save
- project.satellite.create unless project.satellite.exists?
- project.update_repository_size
- else
- project.import_fail
- end
+ project.import_finish
+ project.save
+ project.satellite.create unless project.satellite.exists?
+ project.update_repository_size
+ Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket'
end
end
diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb
index 3d2ef78ee7..5095e592ab 100644
--- a/lib/gitlab/bitbucket_import/client.rb
+++ b/lib/gitlab/bitbucket_import/client.rb
@@ -61,17 +61,28 @@ module Gitlab
JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}").body)
end
- def deploy_key(project_identifier)
- JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find { |key| key["label"] =~ /GitLab/ }
+ def find_deploy_key(project_identifier, key)
+ JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find { |deploy_key|
+ deploy_key["key"].chomp == key.chomp
+ }
end
def add_deploy_key(project_identifier, key)
+ deploy_key = find_deploy_key(project_identifier, key)
+ return if deploy_key
+
JSON.parse(api.post("/api/1.0/repositories/#{project_identifier}/deploy-keys", key: key, label: "GitLab import key").body)
end
+ def delete_deploy_key(project_identifier, key)
+ deploy_key = find_deploy_key(project_identifier, key)
+ return unless deploy_key
+
+ api.delete("/api/1.0/repositories/#{project_identifier}/deploy-keys/#{deploy_key["pk"]}").code == "204"
+ end
+
def projects
- JSON.parse(api.get("/api/1.0/user/repositories").body).
- select { |repo| repo["scm"] == "git" }
+ JSON.parse(api.get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" }
end
private
diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb
index 7d0b5fbc8a..9931aa7e02 100644
--- a/lib/gitlab/bitbucket_import/key_adder.rb
+++ b/lib/gitlab/bitbucket_import/key_adder.rb
@@ -12,8 +12,6 @@ module Gitlab
return false unless BitbucketImport.public_key.present?
project_identifier = "#{repo["owner"]}/#{repo["slug"]}"
- return true if client.deploy_key(project_identifier)
-
client.add_deploy_key(project_identifier, BitbucketImport.public_key)
true
diff --git a/lib/gitlab/bitbucket_import/key_deleter.rb b/lib/gitlab/bitbucket_import/key_deleter.rb
new file mode 100644
index 0000000000..1a24a86fc3
--- /dev/null
+++ b/lib/gitlab/bitbucket_import/key_deleter.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module BitbucketImport
+ class KeyDeleter
+ attr_reader :project, :current_user, :client
+
+ def initialize(project)
+ @project = project
+ @current_user = project.creator
+ @client = Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret)
+ end
+
+ def execute
+ return false unless BitbucketImport.public_key.present?
+
+ client.delete_deploy_key(project.import_source, BitbucketImport.public_key)
+
+ true
+ rescue
+ false
+ end
+ end
+ end
+end
From a938b3eeb4684f5747f63c23385f870ccbf43d2d Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Wed, 18 Feb 2015 17:00:39 +0100
Subject: [PATCH 045/109] Link to original repo on import status pages.
---
app/views/import/bitbucket/status.html.haml | 6 ++++--
app/views/import/github/status.html.haml | 9 +++++++--
app/views/import/gitlab/status.html.haml | 11 ++++++++---
3 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index 7a2613e4b0..cb8c29259c 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -17,7 +17,8 @@
%tbody
- @already_added_projects.each do |project|
%tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
- %td= project.import_source
+ %td
+ = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank"
%td
%strong= link_to project.path_with_namespace, project
%td.job-status
@@ -33,7 +34,8 @@
- @repos.each do |repo|
%tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
- %td= "#{repo["owner"]}/#{repo["slug"]}"
+ %td
+ = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
%td.import-target
= "#{repo["owner"]}/#{repo["slug"]}"
%td.import-actions.job-status
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index b1538b1a41..dc8ec5e7ae 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -17,7 +17,8 @@
%tbody
- @already_added_projects.each do |project|
%tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
- %td= project.import_source
+ %td
+ = link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank"
%td
%strong= link_to project.path_with_namespace, project
%td.job-status
@@ -25,12 +26,16 @@
%span.cgreen
%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= repo.full_name
+ %td
+ = link_to repo.full_name, "https://github.com/#{repo.full_name}", target: "_blank"
%td.import-target
= repo.full_name
%td.import-actions.job-status
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index 43db102994..841e660b08 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -12,12 +12,13 @@
%thead
%tr
%th From GitLab.com
- %th To GitLab private instance
+ %th To this GitLab instance
%th Status
%tbody
- @already_added_projects.each do |project|
%tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
- %td= project.import_source
+ %td
+ = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank"
%td
%strong= link_to project.path_with_namespace, project
%td.job-status
@@ -25,12 +26,16 @@
%span.cgreen
%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= repo["path_with_namespace"]
+ %td
+ = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank"
%td.import-target
= repo["path_with_namespace"]
%td.import-actions.job-status
From 3fde1dce1f9058d4b57d17eac55051fb174c6aa4 Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Wed, 18 Feb 2015 18:10:22 +0100
Subject: [PATCH 046/109] Satisfy Rubocop.
---
lib/gitlab/bitbucket_import/client.rb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb
index 5095e592ab..c907bebaef 100644
--- a/lib/gitlab/bitbucket_import/client.rb
+++ b/lib/gitlab/bitbucket_import/client.rb
@@ -62,9 +62,9 @@ module Gitlab
end
def find_deploy_key(project_identifier, key)
- JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find { |deploy_key|
+ JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key|
deploy_key["key"].chomp == key.chomp
- }
+ end
end
def add_deploy_key(project_identifier, key)
From 20691df230332022cb4d5008d84c7ee6e6c8dbfd Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Wed, 18 Feb 2015 22:42:52 +0100
Subject: [PATCH 047/109] Add Bitbucket integration docs.
---
app/views/import/bitbucket/status.html.haml | 2 +-
app/views/import/github/status.html.haml | 2 +-
app/views/import/gitlab/status.html.haml | 2 +-
.../_bitbucket_import_modal.html.haml | 10 +-
.../projects/_github_import_modal.html.haml | 10 +-
.../projects/_gitlab_import_modal.html.haml | 10 +-
doc/integration/bitbucket.m | 1 -
doc/integration/bitbucket.md | 97 +++++++++++++++++++
doc/integration/github.md | 29 +++---
doc/integration/gitlab.md | 18 ++--
doc/integration/google.md | 14 +--
doc/integration/omniauth.md | 1 +
doc/integration/twitter.md | 14 +--
13 files changed, 163 insertions(+), 47 deletions(-)
delete mode 100644 doc/integration/bitbucket.m
create mode 100644 doc/integration/bitbucket.md
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index cb8c29259c..90c97393b5 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -1,6 +1,6 @@
%h3.page-title
%i.fa.fa-bitbucket
- Import repositories from Bitbucket
+ Import projects from Bitbucket
%p.light
Select projects you want to import.
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index dc8ec5e7ae..957022f382 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -1,6 +1,6 @@
%h3.page-title
%i.fa.fa-github
- Import repositories from GitHub
+ Import projects from GitHub
%p.light
Select projects you want to import.
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index 841e660b08..db16168120 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -1,6 +1,6 @@
%h3.page-title
%i.fa.fa-heart
- Import repositories from GitLab.com
+ Import projects from GitLab.com
%p.light
Select projects you want to import.
diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml
index dd7aacc7e6..5c52f91927 100644
--- a/app/views/projects/_bitbucket_import_modal.html.haml
+++ b/app/views/projects/_bitbucket_import_modal.html.haml
@@ -3,7 +3,11 @@
.modal-content
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3 GitHub OAuth import
+ %h3 Import projects from Bitbucket
.modal-body
- You need to setup integration with Bitbucket first.
- = link_to 'How to setup integration with Bitbucket', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/bitbucket.md'
\ No newline at end of file
+ To enable importing projects from Bitbucket,
+ - if current_user.admin?
+ you need to
+ - else
+ your GitLab administrator needs to
+ == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/butbucket.md'}.
\ No newline at end of file
diff --git a/app/views/projects/_github_import_modal.html.haml b/app/views/projects/_github_import_modal.html.haml
index 99325e6611..e88a0f7d68 100644
--- a/app/views/projects/_github_import_modal.html.haml
+++ b/app/views/projects/_github_import_modal.html.haml
@@ -3,7 +3,11 @@
.modal-content
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3 GitHub OAuth import
+ %h3 Import projects from GitHub
.modal-body
- You need to setup integration with GitHub first.
- = link_to 'How to setup integration with GitHub', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/github.md'
\ No newline at end of file
+ To enable importing projects from GitHub,
+ - if current_user.admin?
+ you need to
+ - else
+ your GitLab administrator needs to
+ == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/github.md'}.
\ No newline at end of file
diff --git a/app/views/projects/_gitlab_import_modal.html.haml b/app/views/projects/_gitlab_import_modal.html.haml
index e7503f023b..52212b6ae0 100644
--- a/app/views/projects/_gitlab_import_modal.html.haml
+++ b/app/views/projects/_gitlab_import_modal.html.haml
@@ -3,7 +3,11 @@
.modal-content
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3 GitLab OAuth import
+ %h3 Import projects from GitLab.com
.modal-body
- You need to setup integration with GitLab first.
- = link_to 'How to setup integration with GitLab', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/gitlab.md'
\ No newline at end of file
+ To enable importing projects from GitLab.com,
+ - if current_user.admin?
+ you need to
+ - else
+ your GitLab administrator needs to
+ == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/gitlab.md'}.
\ No newline at end of file
diff --git a/doc/integration/bitbucket.m b/doc/integration/bitbucket.m
deleted file mode 100644
index 30404ce4c5..0000000000
--- a/doc/integration/bitbucket.m
+++ /dev/null
@@ -1 +0,0 @@
-TODO
\ No newline at end of file
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
new file mode 100644
index 0000000000..9f24ad8c58
--- /dev/null
+++ b/doc/integration/bitbucket.md
@@ -0,0 +1,97 @@
+# Integrate your server with Bitbucket
+
+Import projects from Bitbucket and login to your GitLab instance with your Bitbucket account.
+
+To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket.
+Bitbucket will generate an application ID and secret key for you to use.
+
+1. Sign in to Bitbucket.
+
+1. Navigate to your individual user settings or a team's settings, depending on how you want the application registered. It does not matter if the application is registered as an individual or a team - that is entirely up to you.
+
+1. Select "OAuth" in the left menu.
+
+1. Select "Add consumer".
+
+1. Provide the required details.
+ - Name: This can be anything. Consider something like "\'s GitLab" or "\'s GitLab" or something else descriptive.
+ - Application description: Fill this in if you wish.
+ - URL: The URL to your GitLab installation. 'https://gitlab.company.com'
+1. Select "Save".
+
+1. You should now see a Key and Secret in the list of OAuth customers.
+ Keep this page open as you continue configuration.
+
+1. On your GitLab server, open the configuration file.
+
+ For omnibus package:
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For instalations from source:
+
+ ```sh
+ cd /home/git/gitlab
+
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+
+1. Add the provider configuration:
+
+ For omnibus package:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "bitbucket",
+ "app_id" => "YOUR_KEY",
+ "app_secret" => "YOUR_APP_SECRET",
+ "url" => "https://bitbucket.org/"
+ }
+ ]
+ ```
+
+ For installation from source:
+
+ ```
+ - { name: 'bitbucket', app_id: 'YOUR_KEY',
+ app_secret: 'YOUR_APP_SECRET' }
+ ```
+
+1. Change 'YOUR_APP_ID' to the key from the Bitbucket application page from step 7.
+
+1. Change 'YOUR_APP_SECRET' to the secret from the Bitbucket application page from step 7.
+
+1. Save the configuration file.
+
+1. Restart GitLab for the changes to take effect.
+
+On the sign in page there should now be a Bitbucket icon below the regular sign in form.
+Click the icon to begin the authentication process. Bitbucket will ask the user to sign in and authorize the GitLab application.
+If everything goes well the user will be returned to GitLab and will be signed in.
+
+## Bitbucket project import
+
+To allow projects to be imported directly into GitLab, Bitbucket requires one extra setup step compared to GitHub and GitLab.com.
+
+Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and instead requires GitLab to use SSH and identify itself using your GitLab server's SSH key.
+
+GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/id_rsa.pub`, which will expand to `/home/git/.ssh/id_rsa.pub` in most configurations.
+
+If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following:
+
+1. Create a new SSH key:
+
+ ```sh
+ sudo -u git -H ssh-keygen
+ ```
+
+ Make sure to use an **empty passphrase**.
+
+2. Restart GitLab to allow it to find the new public key.
+
+You should now see the "Import projects from Bitbucket" option on the New Project page enabled.
\ No newline at end of file
diff --git a/doc/integration/github.md b/doc/integration/github.md
index 137d7e9d63..a9f1bc31bb 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -1,6 +1,9 @@
-# GitHub OAuth2 OmniAuth Provider
+# Integrate your server with GitHub
-To enable the GitHub OmniAuth provider you must register your application with GitHub. GitHub will generate a client ID and secret key for you to use.
+Import projects from GitHub and login to your GitLab instance with your GitHub account.
+
+To enable the GitHub OmniAuth provider you must register your application with GitHub.
+GitHub will generate an application ID and secret key for you to use.
1. Sign in to GitHub.
@@ -17,7 +20,9 @@ To enable the GitHub OmniAuth provider you must register your application with G
- Authorization callback URL: 'https://gitlab.company.com/'
1. Select "Register application".
-1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). Keep this page open as you continue configuration. 
+1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
+ Keep this page open as you continue configuration.
+ 
1. On your GitLab server, open the configuration file.
@@ -35,7 +40,7 @@ To enable the GitHub OmniAuth provider you must register your application with G
sudo -u git -H editor config/gitlab.yml
```
-1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for inital settings.
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
1. Add the provider configuration:
@@ -45,8 +50,8 @@ To enable the GitHub OmniAuth provider you must register your application with G
gitlab_rails['omniauth_providers'] = [
{
"name" => "github",
- "app_id" => "YOUR APP ID",
- "app_secret" => "YOUR APP SECRET",
+ "app_id" => "YOUR_APP_ID",
+ "app_secret" => "YOUR_APP_SECRET",
"url" => "https://github.com/",
"args" => { "scope" => "user:email" } }
}
@@ -56,17 +61,19 @@ To enable the GitHub OmniAuth provider you must register your application with G
For installation from source:
```
- - { name: 'github', app_id: 'YOUR APP ID',
- app_secret: 'YOUR APP SECRET',
+ - { name: 'github', app_id: 'YOUR_APP_ID',
+ app_secret: 'YOUR_APP_SECRET',
args: { scope: 'user:email' } }
```
-1. Change 'YOUR APP ID' to the client ID from the GitHub application page from step 7.
+1. Change 'YOUR_APP_ID' to the client ID from the GitHub application page from step 7.
-1. Change 'YOUR APP SECRET' to the client secret from the GitHub application page from step 7.
+1. Change 'YOUR_APP_SECRET' to the client secret from the GitHub application page from step 7.
1. Save the configuration file.
1. Restart GitLab for the changes to take effect.
-On the sign in page there should now be a GitHub icon below the regular sign in form. Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
+On the sign in page there should now be a GitHub icon below the regular sign in form.
+Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application.
+If everything goes well the user will be returned to GitLab and will be signed in.
\ No newline at end of file
diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md
index 87400bed5b..49ffaa62af 100644
--- a/doc/integration/gitlab.md
+++ b/doc/integration/gitlab.md
@@ -3,7 +3,7 @@
Import projects from GitLab.com and login to your GitLab instance with your GitLab.com account.
To enable the GitLab.com OmniAuth provider you must register your application with GitLab.com.
-GitLab.com will generate a application ID and secret key for you to use.
+GitLab.com will generate an application ID and secret key for you to use.
1. Sign in to GitLab.com
@@ -46,7 +46,7 @@ GitLab.com will generate a application ID and secret key for you to use.
sudo -u git -H editor config/gitlab.yml
```
-1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for inital settings.
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
1. Add the provider configuration:
@@ -56,8 +56,8 @@ GitLab.com will generate a application ID and secret key for you to use.
gitlab_rails['omniauth_providers'] = [
{
"name" => "gitlab",
- "app_id" => "YOUR APP ID",
- "app_secret" => "YOUR APP SECRET",
+ "app_id" => "YOUR_APP_ID",
+ "app_secret" => "YOUR_APP_SECRET",
"args" => { "scope" => "api" } }
}
]
@@ -66,14 +66,14 @@ GitLab.com will generate a application ID and secret key for you to use.
For installations from source:
```
- - { name: 'gitlab', app_id: 'YOUR APP ID',
- app_secret: 'YOUR APP SECRET',
+ - { name: 'gitlab', app_id: 'YOUR_APP_ID',
+ app_secret: 'YOUR_APP_SECRET',
args: { scope: 'api' } }
```
-1. Change 'YOUR APP ID' to the Application ID from the GitLab application page.
+1. Change 'YOUR_APP_ID' to the Application ID from the GitLab.com application page.
-1. Change 'YOUR APP SECRET' to the secret from the GitLab application page.
+1. Change 'YOUR_APP_SECRET' to the secret from the GitLab.com application page.
1. Save the configuration file.
@@ -81,4 +81,4 @@ GitLab.com will generate a application ID and secret key for you to use.
On the sign in page there should now be a GitLab.com icon below the regular sign in form.
Click the icon to begin the authentication process. GitLab.com will ask the user to sign in and authorize the GitLab application.
-If everything goes well the user will be returned to your GitLab instance and will be signed in.
+If everything goes well the user will be returned to your GitLab instance and will be signed in.
\ No newline at end of file
diff --git a/doc/integration/google.md b/doc/integration/google.md
index 168077c277..d7b741ece6 100644
--- a/doc/integration/google.md
+++ b/doc/integration/google.md
@@ -43,7 +43,7 @@ To enable the Google OAuth2 OmniAuth provider you must register your application
sudo -u git -H editor config/gitlab.yml
```
-1. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) for inital settings.
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
1. Add the provider configuration:
@@ -53,8 +53,8 @@ To enable the Google OAuth2 OmniAuth provider you must register your application
gitlab_rails['omniauth_providers'] = [
{
"name" => "google_oauth2",
- "app_id" => "YOUR APP ID",
- "app_secret" => "YOUR APP SECRET",
+ "app_id" => "YOUR_APP_ID",
+ "app_secret" => "YOUR_APP_SECRET",
"args" => { "access_type" => "offline", "approval_prompt" => '' } }
}
]
@@ -63,14 +63,14 @@ To enable the Google OAuth2 OmniAuth provider you must register your application
For installations from source:
```
- - { name: 'google_oauth2', app_id: 'YOUR APP ID',
- app_secret: 'YOUR APP SECRET',
+ - { name: 'google_oauth2', app_id: 'YOUR_APP_ID',
+ app_secret: 'YOUR_APP_SECRET',
args: { access_type: 'offline', approval_prompt: '' } }
```
-1. Change 'YOUR APP ID' to the client ID from the Google Developer page from step 10.
+1. Change 'YOUR_APP_ID' to the client ID from the Google Developer page from step 10.
-1. Change 'YOUR APP SECRET' to the client secret from the Google Developer page from step 10.
+1. Change 'YOUR_APP_SECRET' to the client secret from the Google Developer page from step 10.
1. Save the configuration file.
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index c92fa3ee4b..24f7b4bb4b 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -70,6 +70,7 @@ Now we can choose one or more of the Supported Providers below to continue confi
## Supported Providers
- [GitHub](github.md)
+- [Bitbucket](bitbucket.md)
- [GitLab.com](gitlab.md)
- [Google](google.md)
- [Shibboleth](shibboleth.md)
diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md
index 2d517b2fbc..fe9091ad9a 100644
--- a/doc/integration/twitter.md
+++ b/doc/integration/twitter.md
@@ -47,7 +47,7 @@ To enable the Twitter OmniAuth provider you must register your application with
sudo -u git -H editor config/gitlab.yml
```
-1. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) for inital settings.
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
1. Add the provider configuration:
@@ -57,8 +57,8 @@ To enable the Twitter OmniAuth provider you must register your application with
gitlab_rails['omniauth_providers'] = [
{
"name" => "twitter",
- "app_id" => "YOUR APP ID",
- "app_secret" => "YOUR APP SECRET"
+ "app_id" => "YOUR_APP_ID",
+ "app_secret" => "YOUR_APP_SECRET"
}
]
```
@@ -66,13 +66,13 @@ To enable the Twitter OmniAuth provider you must register your application with
For installations from source:
```
- - { name: 'twitter', app_id: 'YOUR APP ID',
- app_secret: 'YOUR APP SECRET' }
+ - { name: 'twitter', app_id: 'YOUR_APP_ID',
+ app_secret: 'YOUR_APP_SECRET' }
```
-1. Change 'YOUR APP ID' to the API key from Twitter page in step 11.
+1. Change 'YOUR_APP_ID' to the API key from Twitter page in step 11.
-1. Change 'YOUR APP SECRET' to the API secret from the Twitter page in step 11.
+1. Change 'YOUR_APP_SECRET' to the API secret from the Twitter page in step 11.
1. Save the configuration file.
From bc80efb1fd2978a328fdc89a3164f60bcf7ed604 Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Sat, 21 Feb 2015 11:07:35 +0100
Subject: [PATCH 048/109] Bring Gitorious import page in line with others.
---
app/views/import/gitorious/status.html.haml | 11 ++++++++---
app/views/projects/new.html.haml | 2 +-
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml
index 35ed0a717d..c7617ca43d 100644
--- a/app/views/import/gitorious/status.html.haml
+++ b/app/views/import/gitorious/status.html.haml
@@ -1,6 +1,6 @@
%h3.page-title
%i.fa.fa-gitorious
- Import repositories from Gitorious.org
+ Import projects from Gitorious
%p.light
Select projects you want to import.
@@ -17,7 +17,8 @@
%tbody
- @already_added_projects.each do |project|
%tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
- %td= project.import_source
+ %td
+ = link_to project.import_source, "https://gitorious.org/#{project.import_source}", target: "_blank"
%td
%strong= link_to project.path_with_namespace, project
%td.job-status
@@ -25,12 +26,16 @@
%span.cgreen
%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= repo.full_name
+ %td
+ = link_to repo.full_name, "https://gitorious.org/#{repo.full_name}", target: "_blank"
%td.import-target
= repo.full_name
%td.import-actions.job-status
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 875c092fd1..f3d166ffb8 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -85,7 +85,7 @@
.col-sm-10
= link_to new_import_gitorious_path do
%i.fa.fa-heart
- Import projects from Gitorious.org
+ Import projects from Gitorious
%hr.prepend-botton-10
From 16c767814a921ab0d7ad3c551bb439a9e270f7b7 Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Sat, 21 Feb 2015 11:08:05 +0100
Subject: [PATCH 049/109] Re-enable rescuing from Bitbucket OAuth errors.
---
app/controllers/import/bitbucket_controller.rb | 2 +-
config/routes.rb | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 89de5c5205..83ebc5fddc 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -2,7 +2,7 @@ class Import::BitbucketController < Import::BaseController
before_filter :verify_bitbucket_import_enabled
before_filter :bitbucket_auth, except: :callback
- # rescue_from OAuth::Error, with: :bitbucket_unauthorized
+ rescue_from OAuth::Error, with: :bitbucket_unauthorized
def callback
request_token = session.delete(:oauth_request_token)
diff --git a/config/routes.rb b/config/routes.rb
index 57964bdc3b..450895cbdb 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -73,6 +73,7 @@ Gitlab::Application.routes.draw do
get :callback
get :jobs
end
+
resource :gitorious, only: [:create, :new], controller: :gitorious do
get :status
get :callback
From fcd3e626b085f6d16ff2c780093bafa898647a85 Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Sat, 21 Feb 2015 11:08:30 +0100
Subject: [PATCH 050/109] Move CHANGELOG entry.
---
CHANGELOG | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG b/CHANGELOG
index 5a3a2ca2e2..726a415e80 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -6,6 +6,8 @@ v 7.9.0 (unreleased)
- Improve error messages for file edit failures
- Improve UI for commits, issues and merge request lists
- Fix commit comments on first line of diff not rendering in Merge Request Discussion view.
+ - Add Bitbucket omniauth provider.
+ - Add Bitbucket importer.
v 7.8.0
- Fix access control and protection against XSS for note attachments and other uploads.
@@ -72,8 +74,6 @@ v 7.8.0
- Improve database performance for GitLab
- Add Asana service (Jeremy Benoist)
- Improve project web hooks with extra data
- - Add Bitbucket omniauth provider.
- - Add Bitbucket importer.
v 7.7.2
- Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch
From 46bcf40b2cbc818837c855bebaef1b621e7c5283 Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Tue, 24 Feb 2015 15:08:42 +0100
Subject: [PATCH 051/109] Add ".org" back to Gitorious mentions.
---
app/views/import/gitorious/status.html.haml | 4 ++--
app/views/projects/new.html.haml | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml
index c7617ca43d..7f1456fef5 100644
--- a/app/views/import/gitorious/status.html.haml
+++ b/app/views/import/gitorious/status.html.haml
@@ -1,6 +1,6 @@
%h3.page-title
%i.fa.fa-gitorious
- Import projects from Gitorious
+ Import projects from Gitorious.org
%p.light
Select projects you want to import.
@@ -11,7 +11,7 @@
%table.table.import-jobs
%thead
%tr
- %th From Gitorious
+ %th From Gitorious.org
%th To GitLab
%th Status
%tbody
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index f3d166ffb8..875c092fd1 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -85,7 +85,7 @@
.col-sm-10
= link_to new_import_gitorious_path do
%i.fa.fa-heart
- Import projects from Gitorious
+ Import projects from Gitorious.org
%hr.prepend-botton-10
From 769d1ce64bdb7c9fd3981c65bc25dd14eb176c1e Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Tue, 24 Feb 2015 16:10:55 +0100
Subject: [PATCH 052/109] Fix spec.
---
spec/controllers/projects/uploads_controller_spec.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index 8774ee0a84..029f48b2d7 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -25,7 +25,7 @@ describe Projects::UploadsController do
context 'with valid image' do
before do
post :create,
- namespace_id: project.namespace.to_param
+ namespace_id: project.namespace.to_param,
project_id: project.to_param,
file: jpg,
format: :json
From 1bf9fa8c7fc027b5273c143949e57eac9bef52a4 Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Tue, 24 Feb 2015 16:28:23 +0100
Subject: [PATCH 053/109] Exclude forks from profile contributions list.
---
app/controllers/users_controller.rb | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 4c2fe4c3c8..8a13394dba 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -6,7 +6,9 @@ class UsersController < ApplicationController
def show
@contributed_projects = Project.
where(id: authorized_projects_ids & @user.contributed_projects_ids).
- in_group_namespace.includes(:namespace)
+ in_group_namespace.
+ includes(:namespace).
+ reject(&:forked?)
@projects = @user.personal_projects.
where(id: authorized_projects_ids).includes(:namespace)
From ea31726781296efa9c2493c3f01aa62ca77b45fe Mon Sep 17 00:00:00 2001
From: Sabba Petri
Date: Tue, 24 Feb 2015 09:06:59 -0800
Subject: [PATCH 054/109] Added a margin and a couple styles
Added a background, rather than an outline, which should reduce
clutter on the screen.
---
app/assets/stylesheets/sections/events.scss | 10 ++++++++--
app/helpers/events_helper.rb | 2 +-
2 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/app/assets/stylesheets/sections/events.scss b/app/assets/stylesheets/sections/events.scss
index b761451321..a477359dc8 100644
--- a/app/assets/stylesheets/sections/events.scss
+++ b/app/assets/stylesheets/sections/events.scss
@@ -184,6 +184,12 @@
}
}
-.event_filter li a {
- padding: 5px 10px;
+.event_filter {
+
+ li a {
+ padding: 5px 10px;
+ background: rgba(0,0,0,0.045);
+ margin-left: 4px;
+ }
+
}
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index db0d4a2661..063916a8df 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -30,7 +30,7 @@ module EventsHelper
end
content_tag :li, class: "filter_icon #{active}" do
- link_to request.path, class: 'btn has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => tooltip do
+ link_to request.path, class: 'has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => tooltip do
icon(icon_for_event[key]) + content_tag(:span, ' ' + tooltip)
end
end
From f7c948223d000e4488d864d50f3292a6c7afeaf7 Mon Sep 17 00:00:00 2001
From: Dmitriy Zaporozhets
Date: Tue, 24 Feb 2015 09:08:34 -0800
Subject: [PATCH 055/109] Fix access to attachments uploaded with 'Choose file'
button for public access
---
app/controllers/files_controller.rb | 2 ++
1 file changed, 2 insertions(+)
diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb
index 9671245d3f..a130bcba9c 100644
--- a/app/controllers/files_controller.rb
+++ b/app/controllers/files_controller.rb
@@ -1,4 +1,6 @@
class FilesController < ApplicationController
+ skip_before_filter :authenticate_user!, :reject_blocked
+
def download
note = Note.find(params[:id])
uploader = note.attachment
From 5179c5830bb3d8602eb25cc00f53be697c6010ac Mon Sep 17 00:00:00 2001
From: Douwe Maan
Date: Tue, 24 Feb 2015 16:28:40 +0100
Subject: [PATCH 056/109] Contributed projects either have user pushes or
created MRs.
---
app/models/user.rb | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/app/models/user.rb b/app/models/user.rb
index 21ccc76978..0c133f0e1e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -618,9 +618,10 @@ class User < ActiveRecord::Base
def contributed_projects_ids
Event.where(author_id: self).
where("created_at > ?", Time.now - 1.year).
- code_push.
+ where("action = :pushed OR (target_type = 'MergeRequest' AND action = :created)",
+ pushed: Event::PUSHED, created: Event::CREATED).
reorder(project_id: :desc).
- select('DISTINCT(project_id)').
- map(&:project_id)
+ select(:project_id).
+ uniq
end
end
From 5b6d6bbc195c438de0babb9a1f0b6971bbd5673e Mon Sep 17 00:00:00 2001
From: Sabba Petri
Date: Tue, 24 Feb 2015 09:42:35 -0800
Subject: [PATCH 057/109] Added square caret to Back to Settings button
This was done to show difference between Open/Close toggle, which
was using the same arrow as Back to Settings button previously.
---
app/views/layouts/nav/_project.html.haml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 15b489c7d9..ef31537b84 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -2,7 +2,7 @@
- if @project_settings_nav
= nav_link do
= link_to namespace_project_path(@project.namespace, @project), title: 'Back to project', class: "" do
- %i.fa.fa-angle-left
+ %i.fa.fa-caret-square-o-left
%span
Back to project
From 9c4337e58bf8a5e2c9085a1f4eff0e942d659d2d Mon Sep 17 00:00:00 2001
From: Sabba Petri
Date: Tue, 24 Feb 2015 10:01:41 -0800
Subject: [PATCH 058/109] Fixed tests
Tests expected specific string capitalization for headers.
---
features/steps/profile/notifications.rb | 2 +-
features/steps/profile/profile.rb | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb
index df96dddd06..13e93618eb 100644
--- a/features/steps/profile/notifications.rb
+++ b/features/steps/profile/notifications.rb
@@ -7,6 +7,6 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps
end
step 'I should see global notifications settings' do
- page.should have_content "Notifications settings"
+ page.should have_content "Notifications Settings"
end
end
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index a907b0b7dc..3cba2ae100 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -3,7 +3,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
include SharedPaths
step 'I should see my profile info' do
- page.should have_content "Profile settings"
+ page.should have_content "Profile Settings"
end
step 'I change my profile info' do
From 53e404144c8ce9f602ec1e3a642d5d3971dc8217 Mon Sep 17 00:00:00 2001
From: Valery Sizov
Date: Tue, 24 Feb 2015 20:24:30 +0200
Subject: [PATCH 059/109] changelog && documentation
---
CHANGELOG | 1 +
doc/workflow/web_editor.md | 4 ++--
doc/workflow/web_editor/edit_file.png | Bin 99624 -> 89039 bytes
doc/workflow/web_editor/new_file.png | Bin 100516 -> 85526 bytes
4 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG b/CHANGELOG
index d879ee8572..0186c03469 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -6,6 +6,7 @@ v 7.9.0 (unreleased)
- Improve error messages for file edit failures
- Improve UI for commits, issues and merge request lists
- Fix commit comments on first line of diff not rendering in Merge Request Discussion view.
+ - Save web edit in new branch
v 7.8.0
- Fix access control and protection against XSS for note attachments and other uploads.
diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md
index bcadf5e8c0..7fc8f96b9e 100644
--- a/doc/workflow/web_editor.md
+++ b/doc/workflow/web_editor.md
@@ -10,7 +10,7 @@ to create the first file and open it in the web editor.

-Fill in a file name, some content, a commit message and press the commit button.
+Fill in a file name, some content, a commit message, branch name and press the commit button.
The file will be saved to the repository.

@@ -21,6 +21,6 @@ viewing the file.

Editing a file is almost the same as creating a new file,
-with as addition the ability to preview your changes in a separate tab.
+with as addition the ability to preview your changes in a separate tab. Also you can save your change to another branch by filling out field `branch`

diff --git a/doc/workflow/web_editor/edit_file.png b/doc/workflow/web_editor/edit_file.png
index 1522c50b62fcf5d4fd1c3d0abdc1232e8e09ca98..f480c69ac3eafbbfd85b30ca0b1324d824f89409 100644
GIT binary patch
literal 89039
zcmZs?b9|*gx4>K5+O=)lwr$%sr7VRm3GipvfYH5EGl4xj-(5
zM!`^r(rffKg4AJ&W5CB4U%}VGl%AV{s;EBzwCi5Cd|tP+vVT4wOy%$$dL3sofgIG!
zhY;hWff9%3F(TY}7D`DB&$x7hVWT7x;i6>xVGprWP851tX7xZ8-s@eb+|&L%d+3HX
zU(II60#Qa`#UEBQ`%UZ*SH?jf!b||7j$yiNn9hjehSc|0)I1F43d^mJe>sZBF?apg
zzfW`nHO~;_+MH4?0|ew0)U?E^1}%}h$mZMRlRY63NF8XOB0Xbr95Y~dkcP%?P(QXt
zWJ73={4MO~1hG#1tEP^pljH!0P8VlG?NL#w~rH7;30#HatRpihld}yZf814@j0A)0H
zKiu#KTX7Ky+ox{&;G18iyJN<5XwKx#wZ1WukbWJJNLv$
z+kChQnjh)BR(7@9N9Y~#XbuP=42XP>?6Eni4^;50u1H#9dXGX$J1J9`nANpTam*=}
z<&h>x)b`I6=-|hO)<^K!s!s>G3y7X7kmuq(jFOm65Mx_#=Oajvg|cpkwW~x7;jO;b
zz%=O$ma9lCFKl6mIblL-aF1Vz2*I#5AWJ=R}paiW)N=~5ZT`*NNV>G)om1I`arcaAl
z99}aHsF__62l@h$fFkzrP3U~V1Vf5@&fFH+EK{g}D6_pem**DnmckbJ7QGhK4ffn`
z%RPEKo!2bhf`RZ|5Zh5g3EkmDgVQMHFl`WiqQoHzBcwpm1r%NwE?BZ}I0f`7c_-2c
zWRz%J=yKmD2gV1e1|G@LWMa&v;3#jTv!s6)?H5%Pk<3xm;jcyk6%A#ulDvMLkK`Og
z9z0^eNhIuve$Ovb^Hjy9PN$xxo>x_;QKN~#(6cR6Z&j~YU?}67gRn8M(K=n6Tb-kr
zqx$V6+nq)-ax&U7&TQdoF=jC}vXTBX<@Z?TIMi72$Yr8KN+%^BWn}Vv@@~=`mPGuX=b+`FUNliE7ELR)OA1_yZptNXfMzkBA1xKlm5QTU
zV0ow}iz=+9PKm4ZVcpod#NQZvtXeEgdfwWTB-%f=MYsiRhWAmn)n`SSZJEuP!WY47
z6phPg>>hx7@q5$z+p~RKbwnz}T*=xY$hdTO$Tm8GpN6tm#Y)!YwlCJv>=o><>{bnT
zjeU(x){WKzilG&mJRKaDxUo1#-2S1ZA^!caA@CTM
zmRIJDD_H#O8Lcku
zx&}M?-*Uo}AjbrA8=
zq>9Z__kW>p(kz|U%>Sw{W}9zadv1+sf?xl=WU;U~!!dhbdXC4%#+AQr)0}YmaEZ6U
zC!kEYMF>e4kn5Ven`@Cf-O`ZL}
zPylpPH9WS~KDWUxOftGk&7>ru-0H@5a<$5RuaXAdxdR;A(Xb0QNv}DeLlgk+jZ1>JLc_#9)L!N
zE<_1QxkxcdDMi^_w4-LKHg$dYB=dIJcxK18j06)4`5fA-{<;uQhAt{(;((jdL|Z9f
zK#0oTNeVveRgjNR68ksy)C2Wg(Q?f4FPmR|Y+aL8oTZ<|XA5CxZetgn6q}XX)C|-z
zqDHH-qrT&^grLo?CJM`f^qbLY&TnxgA*q^0XQ%*qpA1{KnxXsD!
z0H=i8+!xZ-0${;zC#auZ%;_NHAkjnRs#m4Cjm%--Uf}FUuj=RA12`|VC()DY5JS7(
zrRJ7uzHV*j-oD5~uU@1+5{Fi~29{=^-~`{>ud(*>M&0VR1DFTtp(FqYDu;m3+Q*7=
zg{q$RYh%0-mo|~=_NMXt%uMN8O9iYW8b=(FfltF{#&GR}W%5xumag2ci?hD_+o4je
z&?eumSHXGF#psb*TM4bAni8Nvv{I)M-C?7t_u=Frgssb}jTfI+@zegrZBu8m{_q%M
z>Vj8^KhG=hLG&SfR8i#kJO&mJ*}mj1(1qb~s_5nHwQbQou2t=A(7Ld{=C)hDzgWKv
ztj=k5^y|EcxQ|;htk|0G)_!+wn{KPVFP@xy2yFj|53vA0cH-G?^IqS7Nac&=}LYx{Mh_`p1sh$fBYLiZ~Z0wrQE4j
ze>K7OTbIC3g`YW?%;6I}48fwh+=7I`ULGLI1*jJ@$RH}VhS1YK!nbP#Cbba&EthAe
z+~qMhvL%p(RuGgfSddRe5F`=}fC=kd424Ptd@ut81!irOoNmYOL409z5>ka?E354a
z(BN~BoeF19&!Zh6a2Z#;S_tA7n}=agOlK`C2!0y6TD5PL_UotAGrFnW+cXg$^fCwt
zXs(UAw!5~1Jg=FP1EYz#lc@!xw}bQ7Nf!i!-<$U<>0se*LhS8e@94(sEkOED4c@Qx
zzp|M~iT|nMZYMyht)NUS=HzNY%)!Xc$V@5-M@&r2?`m$zt12$}-{N0S0;JaN?#{eS
zOkQ4Ij9zSvPOer=EId3sOw6oItgH-QH5lA{9NkU4864fn{;QGyX-C|`&CJ!t+1l@c$|LUzPtMs`J01Jj^WrDfu5I|1HVS^sf>8$B6#R
zT>s>L*-H?PpXuMW7lfM&S!e_S5eAVF7g6^H1?IpCt1o^GGu8LkIE4XY{zU&Wt_ruY
zwoTGw>|~vkJ=a;Puj~8+e3o6GXs~a5OvhTMXW$6fa}0*VCZ>KRC4NO1|MPj(35YPk
z1jW4bs^8tHf1BdT!-=@5@v;NdhKK?|ZFzt=
zGs-eqYQqE@LP!rWL2)uhu#EjV^=tX$o|LUuxpluYdPiB#N-iHeTz0Wsh}Jc*Ekmcc
zjLQtcl#K-L4AHk$xP!tuI!3&T4O<+n_hBv;F`Nq0(((yfja2eFbooT@g1-?a3szMF
zkT))<{@X!(Wd$XPMf|_@|8HlY;6U|~8dR#o3Ya;u!vf#f%*d|68jYC&QDg^5UcAq7
z^`4mU-=u@3g;A}(`{$1*mOTiz)7Cv`9UXEli4v9Gt%XPDhZUtkg@L+)4VLgd0jox2
ztE(~Cz{-9)|MnxNW4Pmm!4@zRhWcQ&0TVmxd-7ENUpxHM4+LzmWP5>O?e@h}cG1{N
zp6ULn=-bGOn2B)GtpHGxUE&`M$`F#e$fDH^z5<53Nx|eZCmtwxifA!BiXl+o_4j=^
z;Ms&w^~CUg2O;*sOktQ*qP39=XZ8WV*M8mp&>{QUN+hr&^~47<3>G+rw}?w?y+r^3fl>-*M=ufP+bZJXv?Wn=olE!TmX&$^
zP%vR|L+N7qqxu1`azaKmbrgbB8X^SeR`2;4hAwiL+-hBm<)9xGdZkGU?!4`~DJ=l>sL{Kj1@{}aaKmw;ooA2Emt6_JNEC<}j=Z)2IF
z3=ugP|Mzy-HkzywgAM;(N}FbZ_k!v0_)+tqXenJxLK}_($WAq6OoBr2XH2M&9?#^$
zl=&pwGW2R}%5Y&J3ts%%Ot?Z?p#fYRs$7Ide}+WTwumv7n);vD@;_{W0Q!V}cDKgm
zn$Qj8uuO@ofO-jS_uhsK-4J!~JaMA>v3SZ6qh&LuNT1`GW1lW<_YLq#yWKh9_sHRw>ICTT&Xks+G
zxv1Vs<XxPMlzBL-r*?FL3?w;FJ)(O6yX?cU+a%&V}mOgvBnqb104D4s@nlXOUyRf`9vty>Y#JHOe
zy-`2$YQF-DoBz1bpGHu!z=3YbYn_Q!&u?*>=qo#4AM1T8RqU_Lw>gf0H|cEry&Z~p
z=?x^bu{ox%bnsaPAu*YlS}vcrxv-~C6TCYqOQ!mKx{YE<{cktqypPgiV_sA*YBD7V
zf4-x#5Z52#yG8}U?2ksRKNLq0aBy-X$RaHn)zd@TgLJ2Z98sfL?Zb8jF;$kx>Bqd2
z9^8O!b`fo($URcvbHTyHFe7L7OCbSq#b!*FdXXjKRX<|q2_E6(c~53+&WuCijTAAY
z_fPLHMCN8XTXmA2y!u|eGg*U`wG|kNzOrVet^nCPXd=h8_hXT=iEO5!M~D~DX#{uP
zMs<3hqfx1&>Acs8dZx?GAZ^mr8?dnHAx@&_hPd0K0sa>AT4Cai##^tHA}dXYE%`Qz
ze+_X11_D1PTSj*S#_4YHluQ!$iF;Sf!=t=VH%eHDe1@D^n;~DOq9VAhhZpWGa<)ab
z1jpfmcbTWgYH~;yX+Cs2HM+*NUzdFG)TJl_$Gt}^EP|k%_U-puw){!~jjm{M__K3>
zSYbuQI>0FK{>^Q=D&Y41d-J8wai_xKn53eLbtS{3Q#3*XKI-NE`Tp+F<;lS})lb9Q
zwYQ?8jg5_O14ZcHzc4S^AY|eSF+*9n_iMbx!g$)tO2-hmoT;|?d6X+YWNEGjCXT?hz8O1zv-Gp4q89#J|
zhrbp2+~9KAt4uDWLMC7}_noj;QmPLB%)&R=G-N
zPeV^Jg~lpWyO!O7V45P+Eex@Pl9>Y$lxQ(^XDFOT5hoMM+@qZ^0b2p{rR0mw
zpjK}rIl-q^O@r^)CkdN=k7l|HyMnyQ6j+I)Z&)t&jX)``0_z5OQreIL<6J~{z~6JM{-|7_I%c*G2}
z9%m!787(_J1B$=TQ@vM6xh*u#tV^EzQ35SYD99c??HuxQ_mr_XTG-QOcpJ`gR`}@g
z_t@6`_QN16J?^LJ6~MvA#OHCV7#Uri7zc6g>%WbraQZf}n9riMhFf_vgQHzxOm!D0{?Ii_&+m(_Ht8n5Yr>
z|B*neG`wj%-UtCkFYL6X)!37v0!`j1B-3%yy-6@B;Ga*5QfME$O<
z0Qw#b@AJf?QamYvkoV2L!)m|F@3YqGYHK^_^Svq~gX{B_(4X7c>)J;iUiGOQ?fSdE
zf4$vHKEZHZwQZw1&cFBPnMepq#cXp2hsFHf%*?g-*GG?xh32IE{s;}t6f=;M9pk+0
z?2YE3QC%yDOAn-um|mE_hb^(GKQGV@kGX6N-=f
zwfPJk6e35S&1*i!jAg&9qXJh*wPcj4QAzV-XoYZ*%?MPz`l=Mi9Fo
z6A@8xVDLC1Y(UDSL&LzZuHlS^4^FHnDs^wFtsqU^3g1owt=iJvdYcHmR@vk~dKHi>
zW6nFI4~8z3o>{^xj{op4`4n@~#4OwA0&HE3C;J)=fMpl;&g-x9^ziXR{4XL|dz8&d
zazr?zDMLYTVxcnTUZDbvE44R+34*Ll%DMWh1|BA|#uGX4!)-uXQh9v+P9ln>f*aJ7$Z4<{OAd%X>2TpOU1tZljEv%y4iU)kofwuZ7
zcXNg6Ws@#ZDVk*^{0J69Lqo#cTusfi6Agz-Rv{*2yhY=gIjWov*9RY)+WlpMCuf|j
zXeFJA9G09iB(tV2IBgx>m*JGG=ltQLOB4XoY)3QvQOqVt2MGC02V
zCY6h;9EVk?sCBB0fmmEVw=?ZxasSn`f?-GnS}s_{929D+1)l8>TeM}1nH1rL1sn7Y
zoEsTMgQ!DenpW{G^oYc%yA4p4p4kC#$@HB(!|JtzAWV
z4CV`oHgNSqF4={`|2B9Vvdngv4XKu04;!^?I&+pC6(=$FHfh*qGcY|(A
z@F-0yEgb;BM8stu0oB@GmFoXR;k(gs2^dhbEUo0bYymeL+YWrkeDAYQ$#QHPotpYs
zVR?8qF+Y&r__$v-+H78xP&6dCZwU7MdDljcq}IyYxK
zl86ET0E~Ov%&$dgVhhyf=Y5?z`k~CZmO2a)Ybx0Ty4;LYDybq~y@AMaE?rf7k#!T`
z8qck9iyv5?)a$R)%-bMQ(BFkqwzY3>2PY!+`)kruMkWwmw;a1Odj0SjS=geTBHQvm
zdZ91DDL3|72Ormi-j1@g-P~?ZPEK0AwjCT9!_tDkF>~S)u}a6(Td{S$yqu>Lkt;D(
zTjd7AA%i#IA|B~5==g8%fBG8ICy@x*drFs=B$>=hws532-sSmFC9j;e=*)-FE_JiSI;PYyUr^+EQqNv`K=2D-7!^PDVv
zW2ZiAQbgCIiZYf=_&EktWNcYk*Ic&9u+^wq=K@>bxl!reoSL#4aEFiJt9oP|M@$4_
zCtlBig(cTpO=DqT?5Q2|j#yZqFrOfj->Vv5KX~|V=J!W3kKJBh_h|ETa9|F-4g@Y|
zQDH_g8^Af7SEz6`m*YjAv$$wL&J(=V5HQ?HnqGu|TSKEu5+6pqTK6w&
zEEpS~7Y@oBiIScoAmR5vIO%-+5Zhb{rVM<&;L>aIZ#yka5EYB;sSKgOIw55A-`NVd
zdgt2mE^I4@qcYq16&D*DyB+wdL65czRoub95iL9RTAs|nkn63-Oc<+``@uhW8ltxm
zp`hFM_VSbEzLw*>P;9fsO7ZM1#ce232{obPV~o6{@KrTZ0oQJNP%}I3m2}Y5Wye_heM5iKjlL#p;$4kzRM6i9Z&-Utswg
zFz>OWCKac4XH&`%Ueg(Q8OK9HXKgr*VV|(k%x`73`lGV#u+{pw;PX;#
z(!x&N>_}rTDjlGNYtOVx&t`p58`NE6lj!gXjv)KbVAxD#TU3tE6MUUJz!Q1~>);U)
z?{`rPuZdAAxd7jxUZEG4)B%7aJxIjU79rqB`YlLQwm=Th*5UceP{HM;?3RLqnluF&
zmo+pCzNm6ST9KyGf+#8D`vp{FBprQ5cJgpT6{x((U&z#X1r?KfNSd-^xfR$hOV;II
zyIoT?l*S0Dr=yW{w5ugqoP@jiU6r|efIJ>=5j0l*9;o7S3#8EKJ=>;^0Ri`v=1!N)
zG)lT%xKrC2z*w6frddGNbc?9R^V&I``hib|3NGc8z~tnni6GNT3X|@rw+@^%4L-!
zmTL@1P*B$|A%L@o+pF7lSFPDlEDj&mO$N@QR(#C3tzpmzLbU{*O>Z-t+r>}dW*)1Z;u$lx`v!y7)NRI+3!#0yK5g&Yz$xYviMueXQ}I|Y957`OaE-IB=_cJZV^
zPfoQKlyUEQw{w(BqdjvLn@zHLuhs$$)=wQ0uBfQUn!^?vdYJ3;ooGUfFYxW8f$x&r
z{fXI9o&=nUA=k%pF#?%OR2Kn{6PR@snD}#CF^4yNH0DWJ)Ij?8kFTvFeCnaK&6}PQ
zLV#N4`L^AoOxC~fH9mWNqi}fZ)t;A%AthBqwsC-rT|6Y8?=?LvPgkeFJ
z0?&d7yU`ViZ>G9by|%fFj3_9O7OV@7O45{W_zzX>#*ZWjrRazTV?@%)IhvQ*MpPJX
zJ{oc~KTjl$m7(LSy&^~_Dbp*$j}1RLa#NT2nykvTC8ilY(&ogKaQ>d9sK1LkQ`1%1
zxS_S}x@G0Y>tRch0?nj#0V4GL+H6+zbPNmnir@L4()_x7!|$W(a!l<@gxC@1z~1Rc
zr$o#v>;}mCcPQwjU%1;f*d5clHv44b)&e>iDA2;&xqf8Az)-Yg#>mj;7!0#-?;OW!
zan_eoAVlISBq3)t?5v^=;kyb+!*})CFBddd>KMzP6YtcUzrxS{Y1oOD25!7zxPJeg
zT+LQ^?b6q7Y(?iJRt!#tA@Oy<6(&>GQ6Xt=U^TEN!TA2?k3-7X+Jgb=DlM}&lPr%r
z(0i}-Psd-}#b1e0AX=JfDcL-(UE-%eA$#eXbZL!je}0AWlPLlv`pVQNgzwl`xW@Tq
z6BE=!-^@M4`orr-0l0q*-ZmN}N>r!PN3y|0BO}|_Y-8r)Ef$S`#R;V&VtR0fnCuE7
z)t$YuS!@s`9X(jv(k*!CdXBX>i5TcXd
z!fO}CrCMlqiA6#{OLH8a;sA*8GUZR1a%D$L>mOj^uur*0>e{$D-bv}mV$<71D#{qN
z#`q%;5}cdS_4)HMeDeth@z^|D)y}3{4v{4oR&!vV
z{Tj-MD0oskm4$CMb{~`Zwh#-4IIoP7my-X32CSd-Z4+1ym%Q(GciBZ~Ur~v%bvb1a
zhZh@%XASzNaVHCS883C~IQtbXP|S|XfFJahS{cs>>F(Un!T`h!C{jX4n>FQ0780Ve
z^lc#pCHUEOpFbGSPeq}k!W|nDSveZR#lWd757xy%?;-6FfweHuGWNAP$o^&}5d{52
zIcXC}mmk!Hk0SkGwRDWn2vIjPPXHeT$nNB_Vo71M46PGFGq)T{{Z%Vb;NZ+?9)d@h
z7&)uGse(7fKoWw=#>6&R|GuirEEJtbws0)wXbltlJQ!=NB%#<^{2}h(B54qgk~Eq8
zgYkwTe|`ZP3M&7520Elz^OkRgzN3zo7MI+9WmifV^8na
ziXklv$w$-4iAtf=F1esbq3_v+8nFa-K;H|OV<$yqvAvg?x7QRZ<^^xsXB7hsJlGu=Mxc-)SNMocZhkAThkXMNLVSW~T5yUjEj_C>5#
z0TX?Gmqv9j#IJfq&X2=%k2{gC#GFbHvPXzR_ND@%XeH8l#qup->A7r1jX&$%UNZ}+
zX|{FYIra7qps9p(O7O1f_s-I2X(bh^VBV;k@l1&B|g3MU%?`=`5l=A
zBU}codRoX*{-EQXn+Dg9L_U>ol8~+KD?ENkno$Ut*_Jk(;j9r65m1%VEZ*Flk&lku
z>IVGQ=H9<@8gm|NSQt%N}{10*jbqT#^xVbilw2Wu9z5QWlqaaIw>PKLv&Z;))~v$faZh22^l2j0<_D;
z;^u3}ikrrU-9UheM941%n#MahL~c`IBT
z>w@|^Ig7w7dL@qb
z{%(_*=|+J#a!goTUQSQfyH1_&oRm5y(HPy^>OK~S6F6R%8v!Ws*_iE`H
zFXEHeCbaPCu{j%N3+zUeBBg09
z_10If(~@ydlyg%{SBjK5l@ThI7w=SDD$(t10pFt|pp=i_78E_6wt$nm3^LvqT2*E8
zeG%_Ji3V%-%J(ZSn4~rIflR|01#f+&gUCn^*Ed8%U3A6iwpXu2*N@-zjQD(Sxy&&C
zny@5Iyuud$%^VC1rMoSUoRZ)zIqExYi7dnv7~s9Kc8=n})|YMJLy$IMeuWP0HnHTjD2_r_6J
z#b@y+{n6xHgE@o$ft}Ozfb&;Px`?Ke-T$G%<~3UqX$c3y!5t^go$Ec
zAPVdB9TT}_4PkheKzuJ~U|+*_Hs$B^`VXBQZhm%R1`aZ|GJ5o&rr(MpyYX1XK@F>%
zbeUuWit#_-0k{pZi1ur(xa-r*SSz==-2;jbBmK)0!^FRL?cef?-QScoZGk4sW7PZE
zP?4@dk?hnFholTV)#u8h{{9*ssm;xRC4)N?sm{*M8~x9XR=1E4AT%C)`|FIiRdJ00
z6=A_`naQpR5cnt85WAK$AT73AaI%=0;y2Fs9I
zSz9=GxY|C#0#Z^|uqg_D=!BS<^WX1;>}5*PJtFx_b^;VjDx8EyY1v8eU!kmR-2l^e
zF|w89F=L7pf1STv>TxRwRWWH}nceBUwA5Iz9Uc5CM~^}uEp$hU7~o#^goj6GA7^Ka
z>rL-k6^lgt*d_C}yRc#Ep;^d!%_7_mZzqL$LC=G0W467a&(&Qo(S>1TaCNw26TsbOeVf~JqeJj;74#J{yEhZ*Xs=xd3>*g@4O5*@~Yo`al3W%
zdFoDMQi$#_YzZce(U?WP-udk}A;%VZ%_x>Rf@>k+qAM%8_Os(ADVO00GPbH*f13MmaSDfRgn{p*0cC(m0|VnP
z*#j0#K6;W!RF!^)7SXY}4%?70l=@o_;1I#v)32wAn;5K+{H`~*oHWg5_Wd>(lMhZ#
zBvwz0#B<^|co-OFq>RMgjJ2@f*+A)$MBU$L{W|s%7Pckn4+;v6uvgpN+t_9)I71oZ
zaKN{V<((ZAV;o*{>Z?X&DPGMcG6aI2Fh)eD15
zP%^!NRKnWAK6U7|nUIoNX9ab|CcEHr&Gs)n%ga{FSKa#^@KF%@=u}(LL00qA7
zw(^UctTlOew+&Y
z#)s6hErv#1G5uN-nN?NlX6xdN_NI-8B?hq;`SdwMbl
z$8F+eCNapt#Ep$not@p0@LbT5+JmclFes&@jziz@5g&fYDj*@jHr6|3R);FCw)!z2
zK9?lq{YCCUAWAAOCw})wB*Ql+b8&Artz$!XBR8o^lNYh|SE-zsb7P(ssi>-ER8XRu
zXYDJV<$_wJ
zP+RQNn0jh8%?#)e4dbUk^Gtsnvb^XyHvKH;I->eL9u`1gd#hTn5xX&MFtCtC4N@CZ
z;b9XfsT$EUy;;n8<@9u4`Nl6HPw3P;wV6dTB`q_BfOL(qaFDGy4rn(~(u7pg%=nQF
zz%80dvCL-D(p@cZ#Y(8k|I;=#G_?2E`r70ERx8`iq?~cC#jJbRgqaf$Cw7+*BKCph
zyV+f-N`{i5fBs7EmI^hRZbnwWG1JB4FBt*q(8ZRwH7_ZuVOSOPSgZ~G%^KVfxEwE&
zKfkNNR-jwp)eK5JRT7o@V2Hr&5qyKT7g@*lBnX%oss@@=N;8wOp!o9bi}#J)^1bVF
z9=FS5q6RE6((P|8ER2L5?1WO`icfXMh)B4wZ6|YJk6MD{?w)c-lJjI&48mnf1wqA4d%FB`PUgoX0E$OeH
z7voA$z7HZ(WR+tqOB?Ff1G8xN+Q{G6JLyOhZswyY1qIUxd@BATQ?RfM0WZJ=6WO3E%Xq_kw=TEM(7iwd?9{Mc1~8tGFJdT5`>#vc;i-Qm2a5
z$+i>}xHR8z&Ptreo7VGdE%^1d(<~AySa84Iz)b7hR-d>G@K7PuN$ym)tP^{VmL@UwW0Y6bue&Hm`i6=8q6nKhB)j*2?va=_D!U?b}40uW8m^Y
zY-2g@lG%OA(m(Na1uOJCD4?!r4la+f){}FD0p&RZ3YjbHJ)Im+UY0*;y25b0s>r1RPMpw$`P!U&6;td22{DGNtWkovnbDsB
zvbV+WKL<}sVpdr-qZ66E=qhIo%yw}ujdD^})x%eY=fl&MWwW%H+nkTUZ@
zSnlpSH_$WC$ji)ZxyxA}Zx7frfZy~b`8ZQviyc2)FwxSAMm*eXCa^RnxFm%YhBd|^
zQtlcThTd=`48tuNu3ZJn`KHbrQ`6GBKVJ7Z#~TGF{Y*MNi$Ju6CnF=Pp78;6+B!>x
zv#fU)o)ouKskKv&P#Ls1_YMzp@$tn3)H%5t=-BkuD2DmwcvdA}5tm{5!+ia3@1lWc`ZrnArvo%CV5o?JQ|j3x~E
z)ui}(6trb{`*Jqj{j|4oV`e6q5Rw>d9pbtpC-{ChzY?E@Ec80TCViMZqY;=#4Wtft
z*mNEmGaLVW{##L9KPO5p%xHena5*j$g=C)BNOi@Qgw^s%BKY}%f;{9Pd9qYUk%W%%
z_e4wRR$)|Qy%nmROCW1lxuvqCzB+d#o4UWbxzpe&-r;=ON=q)g^Nri4*SCLQBO2lH
z%LIS>OusJ?RPK={klU{NN_aNrC`CbY={{z>{H^~@^oPM(N#
zRRV=G#DYQ-%)CEiNmAj;k{%y6*(NfOCC(l;?`g++R6Blg$pq5E)T(*vh}$KbT*XfC
zR^atmD9C|x<^bsCcV5`2lN#$p-5ed3B){F&b#SWv@x_bWy{`*J+sahRU+e*I8`qO5
z`B~=XxO|7Tb}4M(6N)bavMSg<*hP4cKRBi1e*O#k>gl00zS_aYXQdb5z{!#_Vfkfa
zMXw(!)e6y1kiQ5oj%HIY-!rmFX?-UyS>0=?SYYYIV<|x+Z`xoR2U7)mye^L*yBQJZ
zB}T|VmKgOc(>^}ZS0>6xn0}*%xQ(Oii|_F=s*9ac7)GA#gozP
z##(Cza3A+(T(TZp6L8P=D>bZ`g=f}iZS}uZdJ_INB|;X4->8ddHZjxWASyAV5KYXF
z1(8|YbzSUj6!+WignM~L6N@=#LcqSG9clQDr2s1fNu+QXnqxU&^L-!~no1X2X+Fzk
z*(L+$9oop?M6X;_>fT!&kaEXf+nOgf*~)qzgOLAE(a3U*3=O#*a)9v
zImPK&$$p25^J4wXd&_utNTjO>!@b^izZjh_2h92M>OE3XKmr=cq0&?UORnrEjm&to
zUTs>a__n99&stG=*2m9M5>rY5w(*7gmD(mck6zu~_`R+7Xc
z0o=7^qZsLx~Em;v?+wPjs_9g=HjD1
z+U<0hP31C5k<^cYCdYSGi(dF6YursJd1*K|@chF$1$QWQY?@a6NtAO$fSK6ghTUwOk`P8y<44~;
zh@Q)3)`vF4{MJ&Q&fsKc)ME@PX0*y>P#Wy{;2I#YEO6LN@#Ha#B#6gE?c6iCQfpQ{
z-~(`ITk=?MOO6&p2on>1iVI#p7#5VYDJydzWje>2Dn+N#rL@1cP|6YX(2->1V$0?K
zH?RAY0n7ZxiCz2;u*p>I2Tt_fu1I@M=xp
zGi4md{usFBuB>Gr{}6hnEPHTuuzc-nFx{FUzr^NJiNj&>IE8Tp)5hyRs}oKAT*z`j
z-wL{jv1c;M+xP>lVOY<*O)T}S(w^I$sN#e=NrOQN`H%q-BWeW>;kFun=MwcHxh35{
zyO_H{Yge?hrxr+8b38n&s7dq8v;v_l#dQy8Jo5s1=yFK@ik`3R=E_}z+&vp_YI7;_
z^!uW1tnsZp2CC^6>cF`x2;aWYBV5`woIOHq^9~JZL=L}XxAg$?m+KPnOBqXNIQdTr
zeY(d{CM@>ug_oiDo#tV0P9XHoV$66gLOB@SU
zHADQ3AE|oYkBWF%;X(N6+k&`R$C?CR9E-j!Dgk=<;LV$D8mG?bzibF99=tFJ;#&_T
zt0rTBkm41+w%QEA?a@o^k@>wTx7s@aGYfX7rN@}BJOAj$?bXiXOxB?vJs_WPfwxke
z`lW^1#%=c0M^Pjh$SJin4>tPXOId_xe;l(-cxlz%%)(XmmCvc&C=N~xKQWz
zp?Ucfy$b)onKogFDn2k^ngZBtZ}}VBSRLvTV+QZH-;3L+Ii00t!#)@Rjpca#eu->=
zDCVwhmB34550Ro&(}==P7&^#r!(ZI@?vivRQ)E-G&+euwfEl_Gr2Sa}01$W@DaqoZ
z*KKfq=ez29>brgW9@gsftgRN}qnu2+d3Le`@)#~n?t8rHH5`w#4?MX}cqOWGI^aq$
zrGPeg|JhTkP(5h>H2q_}U65_fk5|F={$Bp)Fi5
z2VBv{uQ1?09O4%*`Rfz4N3j?Cd7q<+U0`AkXKwFL`JT7}3zo6*AlKe&%_s_TjV0ax
z;m2v`7xJpBZP`x_L|NJgvRfC*9p|BLzzVBG9up+wwx6f%;E$%)>nz5AQF)KV3sCr;wY^&a$2Sh^NY52QofSfAwzOAD;XE~>e>@gGuIKy
zZ_ws_7Ofmld&)>BoSHg2?(ntB%2qRkl{BIVjtr1!MW&CmvCr-2?K`M{;d*;uhzx4a
zo;8#Z9+02R$4e6E{jjC_ck)ngow_g7D;5FcV|l(08bFkA?;lu_+!$Xr`{I>%Ag-wp
z0j~{ljHELFh?Qw{u9H)Ev#)FAU(u*1-oZzj_>QOFz>dNZQzwh~-@>t**)>R6dF9_I
zmL1aFjgA1!M)>u)+Jk);aPTEyX+EW^x+`jaE1OQctvg*SOiBjWK5`aVPM!5OYbm8W
zJ-tDfuiHi6n_Z!^@W|0@TxDfNOacu9Z?--Oc7KQ0VnF`S%?N#Z22@&ob2NUz_K9w%
zfq0k(G7yW|UrT_)<7T;h7*c0&)E=f#ZR0d`t)?pCF8%f3O0#787UgnfHO{ige>O+C<%dlT(vIA6k2QPF+>bI1Q$Q
z$rh;|st*_%O@I+k8o3n3|GN0Vg%1i7i&r(hVDhi^)00XboW#T5hh?cy7^EKV!u^Jj
zF=fus_sj`JLRPxSp;&wS1wrFEofBU9eEk=7J_)>wl+`{Pk9?oOa^cXtWy
z?iSqL-6goYyStp~y?1w~`}=+Ga}K|#nyfKp4S9#mK_|^F3+UpC{|t7j@3%ZfVU^~V
zO=cJdnAHy=;aY|8gd{a`rpV++waZP2`b>Xu+@Gb_4VzH+fv<_oeh1R|Z}STR%YJzr
ztt`uv&Y`u~XtFw6iwoi4yAbqy@w`po^pTi}>)RAj2_ji6CTuR9%NHqI@KwO`@OSu+PiyTL9cDzlgnPc2v$?rSuPmT-+3NHO==s;$~+glh+3&z2|4!qy6
zdh)BJ{YCFFVP0yDmpC{)wN;yzhGM7PyLLVl_GhHvT67c>@e%!8j-O*$mWi~%?bigEFsbHRBGl?cr`nEoeiogb
zfH0Bh^0F6cEv~V#;+aBgC766EX6h{?cODp8#>MKx6kOw_rM&;$EBIR=$`HYwKCkA3&mJy
zK6oBigC6ihQZGTL#zFoZda?CZdreSLQL%Ye4(fNI`-8Elz#Cy{p3bv4=jdnghqc3p@+xz^7Bevw}-kd>!mJJVyCg!&zCEt
ziQB`<%~6z+lA6oZD~kTNYD-7SOp@g$7T)=%$b)?}lJuGuG{v(M{`a*sY1qi7eR6h+
z|1)3;kxQd?mC5-#SN!poT*N?>qz7f$|2?4giNO9>^1GFPzDrQZQXWLfMJ#;i{|qF9
z6^6`m?T7#0U4SUb+bH~>fmtXK&6-_4$G?Ei{}kJGEQnHibdJrx2Wm)o53`w(nh5_X
z;sTikAWGM*|!s=a8m(t~HyX
zA-v<_;sOH$6)4Ot6$_?jW@e0Q7jH#BPa(ku34DczhtHCkM_Lk+{Q4I~3uFk^_$IF!
z5ou{hYwL^K+di3N80U!QeUjL3&d$yzCMF^xBG}&G#^4AfF@xJS>;fX_LS%&iWG^+P
z;u&UU=Goa9DTU*-}>z8W(X^Owz;<^xlm3|>SSE!?vRl~>Cx#2^?L@$YyuU4ia
zMA0l!c1z>1>(n_(W-rg&*}H$f%7!-BWtBsSYT*IXU$2k|B)Db#U&;ktm0q|4R;xT-
zVPvoVWJHZSC<`ik>y`GjX}-Va{10BWLIAN~L%?~V$-lnW{77JLt=XD3jrp&E|4%)D
ztf}1E2$5!NWW?rps)60@;NSqN=5}etD?~#>GnYH4OM$|=41dE&7<})q;ep#d;2s!(
zja#yBc)_xubMoXaxP>-y8CNTMaAW>$HKNk`R@PB^-*u|h0vnUyFNO-@_53P8DPom)xf*vHUXQ&G-q83V$dyIE=f)
zNm_-DlYfYbg@r-RxH#3VFgq5~^_GYM{L|_ChWi;yAOs0jgDu(BQD7ye?^nDwpif-k
zfd{e>>(KCUoLC;{$Jh5;AU8i_?Tqt-pdf!^ZR!kqQ*tsWl{rGSlkF;WE26{n88yyO
z$+VpJt#(E=PY|IfL}PXG{#rxx^XJdUOZDaX`7mJ_^!hMbp05vk
zB_$<{z%hwrrook!mqq)cag*2UDQUpN_NSluENDkZM{DkPD@lyNw&%-X+P3>m)-|`O
z)GT`I?sk3Kl(S?{nhdVyuqHCVlfIeRW+o>d-cYU^2KF!=Vl_~B+sySD|BUGM=Nv$>smc?SjV;pDw&N(BSN-|Ox}6u?2T~L)A*r)YGA%SZ
z(-|@9sVix_yl3C0b?Fs1U*iL2Ccx9(&W?o+I*2-INv0?+rm4gYpw;2%i(VmaO
zv)s72xK@1^Ic#&4BQX*o-mJDeCt{?JS(A-+g4WQ&*`@;uKf>B!t
z*)@2CrJORKl98*(Mc_lZMr4G7y
z4Vm3D<57j_pC(lf7ON|4XfcVP}=0Tz`>Ek>tS&^Z!I
zRn~!Im#4ikZkQyNOHC@jh-lJldo*W^*UXoaG`r*Dn*+u*=z0-2f^>hNcsyGPx4~7)
z%^8QB!3)j|h2rZw8Yp>jd`%-=ZPm$5akz_@zB1zybWX}E0XKFU*(C2Yqnx@mUy6#1
z@S0duEu%n%?&o6SR!Oqwy6ta(E6aM7tu_o3iYdA?Uy6?$|KTvZTLMkU?MhHPKWX=;
zP6bKz`r)Vj!6J(=A;@w;US2SvNMsD7YiYm>l%TGqWrVuGS6Ix*lf%0`vAOru;KsV9
zrr7xSNVEueBWu?ExF?i1=`A{9SN3~pjsYB1`mZ76?KEj9!KOo3+IW&wo8rvzq_3_w
zhtiv`tF0aflSrzl7lT8t@oh~h*p*ePHV)<5&s}&>Hu?!XH#@&Jd)Ac7UmZ1ss8eQb
zm`Prn@apK;-U~-r&4U1H&O9hQEZRp6A#T)pQnPvhycE-pvG|lxf;^DFEMG6eY4R?5J_^$@d2re
zC9!U}TkB>VQKjccqnuTy+fh@7eG)i}GIo#cDUO7s)BZ(dYwR{QBika*`oGH33NZ3F
z`bPl!R*WoKOALN~$O-0~MGh`Wo
zQdiS){Ea^q)R^q9?yON+UVlB6rk$^
z+8z(kf?6&Id9a^I>{M8+Z}g8C`z}RmB|0wS9Mw=y+CZ(NwwWKoJF4;9^i#R>XaMnv
z0~$DvS82X2)Jc~%R8v-|W(*N<7H|@NT|4Wj8y!CzkAu<2l%1;sFGINXlT=eIR2wsK=4T!zFw3mAuj0to(#K`9;L1s6UbI(UPVzfE
zXuB+#LN>|igJxt-A|uv|72dqWEPSgOjEd*zvsWA4J^Lh8P?#N{4kXFlq5o9G$gWv1
zH1=(Gifr@K!R)1arUa+=b`~SLG@MsIL5Ero4!MmW!*qoEwohXW*Mgy8#$r^G>Zo6U
zuwc`oKsaKga>BlupM<2d7Bge)=pmQMLkaA_e$KFmr?67IT#|cBP-VEA(J53S+CZ+>_w}-c;_*
zdeSYW+}NKd9L)z&GiLo)IzxdUnR_>1O^))eov%4zcQ@&B0
z9?vg$kFu2YNh>QW?~9t{`5U|>G8(E8>z&hg6hJ#mY&}%GZ6|Hmj5&7(X}G&nXCbNJ
z`!X@bsxOU}=E@kHE)19i=2Mo}a2UpVE=wmBh}D!8$YW#yijmjv)@j{-Vl%#(%4Kpj
zPumVzk$j<{rj(9b*=Mw_GI>j9#B`o)5i0a#MkAbOZN@T$0F{xl@a0U
z@|}mWJL;gG&qaPO3$RZgH?Vl9**Z%9TU7lgB${Z4y%x~a3H+&AZJ4DEcpV5&wAS@O
zh6(3%I9|7WnC5o9)sQ~i?C^%1=Q;#kyUYN6ak(>0&t6nff$VGCHf*j%81MPqnVltZ
zbxm>%5!Wpex%bNXwIQ{v8b3Vf#ZfOsE;KpJ$gC#<&AwB}`a(pf`-JAkpD{J72mSTq
zRP?;{YWy`1l4J&t``%dEb2K&^Jsl&QoIFWPrvdm*u+#ew+s4Y}(*U;2oG`80mi3My
z%UMih`?JkoI3@2!oEUnBT?!}RbCyZ47St&TJ9&p@)X
z={71^kKlE&k>3PX&%A@QCS~&I|1YX{>&7zbQH^XxR5Ob1YDhXbOog
zL>1^0Ae{A%GaozRE2P65-|lejTE>;WB^edS1M-@K7*3$1q~tU&75Hl4%%nNcXAH9&
zxHe`bXcA#zN=mI}R>_h3l+Z)$6+;!%e!mvw=jZok{)W-KL-U?lQ+&c*pL;y`4r>#<
ze5uad_oqso8742aX47Zg9t`;<#fg%y93t!4-(NRwC5)Hvn&qpC#H$?o^wOs=Q90+8
zLJ{kg3^K?Kwzr%&eh~>%5q?{M&_3f)l+&~^0UoFOO*xm$R)ykNlouCzed-h@x`eei
zn5WyjrYl^W1bmi{k?QeClT*=5v|cZ0aEntS+j2)HS(qm%{x&b3MT$}
z0Fyq#;Z;|-6uq>ou&sm#N}!={N?&vpwQlY_kOjs*wJTHTvy
z2?hhI2dwZT`09h(x@I#>y}kTI^38DzKO0|uGay-_FlCp|8H(UkG$ri%Kt3f^sk=swBst|#c{K7m&p6LGq_wC5
zj{UV>6h3=Me@n`9oLS)zr%k7+oS&WXWH=+4bC?Y&4UK)>Gc45Vx6U6LZ%`6}d@G04
zsVEq>39+#lwm=~8s=g0()eQw^EuRKa{}ueRN~K=!JAAi^*Afe=eu<3t~dvHT+UHN?gNk;W78`4Wh-dGh#_$c}DjX
zIfDAaR%Fg_pW5_htY%X$r(Ln{*q$ojd$@(iHJR0hXNjYknWq`Nj?BLKDlHXN3+4Pb
zKZFeal@|)sx`sq2U#^n#-tn#L%2>kraWjN=G4*Pq4??P1tHp_a4EHFhgSVK@$5Oxg
zu3ku3;7synnqS|XTB^9hMD`sw^K_erzJ96NXnN`|$gPt>omKR+XcO&^y&5#$Es)wh
z=^Z#UKqOaLV-HzEecbBJkE=U2jrlZ58LJKAU$Z~_C14$KgcFwp4Jr1#)S$43FrKpj
zfstCZH--io;)Zufy>Cvk^#)O`MRKcdm8dJz{TUiw;WXTE|G{A(6c;I#RxTS}LZp&r
zsogm&KC8KyF#N)kZg-*RU*tgz(zqZJN@;ZPsz6=E$~6^w$-|Gn(=O69iHZtjra*n}
z%-Mr+!V~Y)^Bbx=uAOZ
zjF3TLt`9-=!C0cdJBhqdFkqlC7v#ERHp#z;f%Abf)Lu?0KcjwIGsvjwfFTpu(}S%N
znSA|iS^s~VLZEsVfc!L*|2nG_^^-I(=&oK%#`QOg1KMo|f}j^}4{gI+6u`mF_M3eQ
zhxkAD9K@UPsi9H>>h|{5UF!UiZhvP76t^>2M2CkDVWH12{Crvu1#04JU|`U0j>jDO
zS5R^v3Chjgy%1H69c1ODQU$OL~Ku~z{
zXs3q4}eAec=(FNVc2m-Ph?zaoICm1vTOYZ%}
z8y$)y6Ymbc^6Y!Y9nmhc+oTv$?eSCeF?DQ`-d!eDB-H0S(O&|j;{>k7S_M(}fY^o_W-Iv3Q
ze*AY>BJc5#TQEU!wMaB3*~6yi9+`rFPQeR=a>=8q2wfukau0VOEN41bwnHootd;w{
zbm2db%;9@$6^5{|H6@bGknGhqNlHkxC@IRf{>*
zr+6vm?E<;|`DJrHpk^*@ZEbU=XFs)nd#s@o_Pc?BCOd?CAqIr*&&u`wzwYCoh73waR;cJ>>W+Up7rLmI1G3js&psPtu@2;r_S}
z+Nfdn^o?KeBmeJDgGhA{!%mF+{D(UIA9cGBkN{ZdW=s5IC7=%)(V&6Zx=48!|MxgTS(bz(OKJ*Meh$(P_GJe<)kt@HGOku
z=aaayT+5R^(lgaNt}<3u_vSmd;C#!l`bxw6K~D;f$HFw|{0mjXHVe20X$++e*03JM|%wV8fpnSS6j9BZLf
z9A(iV&^YWFMdGTsJXyI=!%iu}2e_Lqv^Fc?v{FtxdGm%KS?7%rATzlqLN8u3TR!iz
z-yI+U9=>1g#gwLO{Q}$m^|iW!xH@Utv)gg)<$TA)HGuxpk1(M!8Ph?k-*kAHB?J>=
zQSqJQK;)OJ-BE0S2N%PIo<8W;;dts4fM;8$&VJGNwY;R^5{$l~AGn#?d04Jf4w@UQ
zK+gf75Le|@&ciKC_Yv)`+l?#?=l8h-7yY`BDnmJP&WynlK~huk>Pw8>q@?ODZacM8
z6LOl1WvhF-)#m(0866Y4c@EAHfn=}%at8WIIY0h?@W%>CM^VUpIg6r_l0)-=;*b##
z=on{m*ph6$)z=HU{}4Cc6%v+W+17Z45*EnH_40gI5Y_21y<*FK#p3DN77f|0!PcU?(DX@3
zDREK$Vt%u~CGpbctv92UL3GG~a(ub|_F#sT!b;!sj?$8ci`*{)h-J%4rHX$!4LPJe
zTe=oF#D)K~?%UjbVf6Y#8(D{Z%}mghr7R7&Fh8F;2M<+tN98zW#N=}0Z<(PDOnke3
z7J**OB43z(jWd9b>yWWT&LUa(qYm3uk2
zx60`?02@)}sk=)TAWM4Y=DFkQip*#|*7NBDV=86lAfUo-N2z+qZMn5pXEog_f7xQA
zpH>37y3h0NAT-NXMh3a3kNa+UN_8o+;BeWv&*Sk(
zTCMGRvOa}zuk|(hPQ3ih^$xOr(tB44v?koTKJ&K=w0RN;!`|n22OsX)p>20D=BREL|R0ws3Ff9+_Sh?Oib*j{vOqVok4I0
z9&(A(MXeW~5+>)G`&(o0A(G<=Jk#Fe*NHfim;1gU{G<&^EfsS`>;14boAgxNvd6Wi
zxU(0_`xYQtc)Z)oHnv%Hk2)C0Sgtz@fjq#77
zT;{X4v*pMp=a1Cxf>&0x%dGyf005n(J`dxaf30lzP^om=#r^X0X!Gjzr~2`QGBUy6
zFvYqMF)w3Ilgr)%AL(0iT-?pWUdGeILuX<3(f4#Dm?}fm(7A7S(&=XR2YHsm_{rQ4
zh~3iVo~HSoW}d|loDmoLri|4eEltBm-A@uAMwfLewPgA^!tpfNJYx&+L|NAy@Q;sf
zLi6^{m)!h(xOPmj84aZ(Zl{lap%7sv_7UP+>LeZj%pd0n2GWDF%g;(WHSJ$7r$$C{XMNX+DnK>d2m#OAekcyKB%(6gbm|VF
z<7H+>kH&o*YTr|vB0se44MYhx2u6MUrUhs_dgTVf@4b82LQ8^v8((b%6HwfVcuV9m
z1T%>I%+%A_gMotCWAiKw@i0ulik}1*?_=J?L-9+ZvW86Zq7wT%d5VRPg^y(ovkW5v
zc_0erB+lo!kFqcbnA`Sg-p`(AtyhLHK&N}JvxODTz4R;YwJYunTkG+=^!~h%;5s~U
zWVVjQqBnR~Tr}pDM_qX|%!=)+#Nay57iOwa9yUE_T}1b$qxm;~beP2tO5Tnao}aW4
zFZU4<0+T-VeX(+Juxkh#o!AOq(*}-!y<{$G7&XO1mi5lZz0(tF_TEO$kDp>V6t%qh
z9M$NP7!DN)hQbRW0tQRqT_yq68sd8Qda>2dYPsrmy3EeX$~wROg8t!I9dW4H^ThyO
zJms;8HDDwi)5ccgsq^JWAz@u;I>VRs1Ft>b^zTaR
zyn-@CQH5n$ZH2!AR-3@cBMOJYr$T-rO5<23D)wkhQO3rkFP+{OhF50Z+c(!V6;>B5%rBzr1#O$IL
zFkzq~$r6=yWsWSowwetf->;7R$h0P*JSOWVB*+HvwOhJ*ZLKl)792aF^3p@PxlLS$o#T-9_#t7
zh$U_I+lS@ak~O~5TbI{@GsltyF8T
zq#U~Yc!8N2TR*QiLC!Di8Kcja%Hwx$H?>B*+D{gF!R2Fs@;z|goU~rI4qgBJWTRNe~{OOdv)Cu3_Z9L3pWfSFXU{#`DJZhCg(zRF5cGl
zbQ3q0>a*Z~xCi{dr!?`A?oCXUbZSQ5vh`*b^e==+C6jx0Y-;tFUHNq#Vb@y4Ibo40
zXkOrQW)wqSSq7%U_ByJI8$0s5QH5Gp(zu1Bowjm%LO(ShXhL1OJaV@#=$zp#hiF92
zSmCZW5OQ3ft~5*IzjMKjWm%@L6<1AJlKsfJMo##C;pU3iWeR!()v9^wlrnSxG+D!k
zGA#5QDH@<8*jcug6iiPE_FIYN+3FY<5PLlvcc*x?rFhSPFIuy_vsL8az=q|{Vl-}Z
zSCAmWntvLr1~BsOs>aYLCOBT
zhSUE;g8p{~ybu8(E}`4jb-Ej-ZNCOx2X1RicXzbd7^o5|_QN|ZUh8hY4D2wv8BS~0
zXm+N4Q|Ve`)%+zUTXlx+UUQ`IYo}P5HtmrZd-CKY4DC)^RN`@Cxi`d9LpN}mTZkG*
zyk|HE1u;RRSxL@jcfL#!{{&DDXapAvwfK^k13Aq`W`C~d-U*$%#wEn$hT9*dq`bzlZ1tooN1p@l-f!M6!(dVSCV)6hyI5jf*p
z4$;UhlRcVjz_~okwE9#jf%17X#r|?uf@LDF4WPaE$~87w?pL*@b0Nhh98Td)22o}elL
z;O+4Ookf6-(lnj#cuQ6wJG|jzM;UF5=^kdA#o+~UFgz(~*NazB5c<^Q^w|*p);v1C
z_SKkmZ9F=+cRp@0~z@+%3^;
z#(Ol_Z%8ZcJg8)~Wf>9GL2UJJ!RHCdH~mE7-tNr}d05_VCgd6Y+z-eGXjdXL?7F8t
z_&H`EMG$dPhiKK4Vae@-?tT7P2pC)4K>dxqL^9dEEs(kw%11{bignn8ndyng)YM}FcroMZ
zd3AVo6C#p0G2M({*=+y%lG=(_5mj|@b;G&tzI<2*ZC~9;qp4{t@tM(IgTdxJZj*>+
zb$Q*|wY3$ALv=uDL#rWn;BsNtf0^-=_0lp&_sIY_#Iyt4NLylC&e3|+8U
zw?6qOntc`EB_-n5fnJ9bRH^2Jv8g>UqnBz58R*F+f
z@%Sh*oo^cAg~y_q(uL+tnF7&R5(@e~d!`SoWX9{`@22Ai6vuRPN1Uu7KI{v4vBL)w
zCAFBWI3~MW%OvI=tU`3JkUe-&<)+^D{a+sEQ?~LJD_wU%SQWVs|v9f(4hSWqoI7=jdq3
zV>gTBw_OqZK@krVqV$5MBWhq|1WFiUY@3{ta;mPjt`6-sNTb1OqZeHp)IWMUUmxn9
z^MMctZ)V&obS61Dxwq+fI=65b7Ia3cFE!k`e^qKKP6HmNgBd6%++dtV$;CS_^v`KC
zi6@fX(ditssFAeu&TnsbZ5q!v8Vk~0C^$G42M1wsLT&fPj17KgkpJjsUS)y_tTTuP
zE4FLpaRQAdJJ?&Dj>;_1RU$7+w}=n<3l6}su>hTC|nhMOfCm1?)AX2uNEV
z=Xok|_&A-$>8Jw<^S)z_t_Cki36Jm})(G^$P7k8=j^^^}%JX3dJy_s!*>=2B3*L&Z
zOtU#YB?Vr)_5AwU?eU->B{`Xcp3MyFpIhLC5Q0)W)4YFrQdUstNRkzfz?)HqD-%>v
zNqp`5@S+h)_`kjW7f6B=O{l`!6&DwqYA&sh`~(@43{b1Cp*Ttq2UQ@#-Ku@SVtQ{#
z-BD|l;|Q+QnZTq5RDsXaY0LB17~%gNOz=Wzdh_b&=)7aDzOhALm+Dw7`0_2_{^6mr
z{~I4%%775Zz$Q!4&V`ft^u*yN4Gqm=RYmrPP5vyI6raCEFVh}s9p{rVw1^Z!=Ctur;gAIw{w5((+t26Tj&G$$t~%
zuw1j`>i%K>TC&Lc>nEe5<=&Ti3{$srC@jnL`-|_jX$Z1Sq3`h)mf9rI$W4oij<9iE
zt{v=*V$$Q`SXV#qX&qy;+3+Hz#cs-V9$cpQHQWcMAC$gu{%oY+c`6uZ1m}|cvDr14
zQ|-7XHWU>}iyaBWahyuNaYxO&(x%)LUQWKM0vKrexOYb1
zr_Q%}m@>oSqOdy#JxTKM^flkp+K
z{BcU2)_x+ZEBm8owpTHQA&c1(
zieIFLnkOv5+__G7x!-JLL>QYudVEJGWsI{!(+Qz4dlpmY`_T>
z`_1|E*eV7qFP#A0P@~x+kDBLE+cnD?)14W=djzMQ75Z_bcdMa>YRw)p8f;DMto1>!V$D-3
zZ-esbH(F2jTUm62k~9XD5{S%X;_VE
zi}Ut*IydXKS%^=Ib}-z9eweHl^pk-;)Tt^^I*z~D#orRlnLl@7?YUYd|0@gOqf4ys
zi^Pp`RlAnzh+dT2by#joF#6LY*P>#xe9OktG~fi!vCh?PQO7%{Q(Gvtalli`$%CqSVH$4FAD*{lQr9!%yv
z)-N`gBbC$P_fyXF_88wUzy6@j{Xzdt>te$OkM+$*!+N9MrYjiEBhPJo;j?ik)dogs
z-EI_zao=oJRb_TkRvF7qf^>RWz>IK>qpasQizQOK-blW&6;xr*<&U0Z_T43zh>~y^
zZo=GmV!y`Mc4iQR5pILWKkjL{U*z#96h>1h&yKlUpKh^Al~6rf7~F1AejDNVV2^%F
zI}mm6yAXQw_@
z;d`!T%%&Uy>0#pmk44L&xGE+5-A|{}D-3k;yb@|l3UPMIcb+6alv7-@V7MX=wcCSf
z(pQ1-R%%RHz*kFQbR;{%V@RNnG>eG-HJ#s%I|l3?SZCVQu0AtAOCP(dj(
zr|Y}iiAat5zJ1WQJID-1T?0S}e~*Cj5d^Ue9@nUOSZKs$T?n4SIFTa+n=p&RV&l7h
z?|HU{TYM}gVh)iLln?KnZLN{D^CIJvv&CsAL}{&N64k31@<)mm(FBfu
zz)@dJkxOH{FL!)2;%Wn
z51>*kp6By#K`FBydxw>&-JfN(>jGtVfn3&cc&9Z$H|z309mvq@*Z__kZz#
zg^BT}AM4AP=PHAH_v;R0@C%zwGaw@3*o25GYKP9kGb?pdzMV<|b4jHb&tCBzZ_l?;
zWQsK<^0en0%Q>Uc6@Nw7NxMYg*gkBhUoyzYz+l0fj+KZkLBeNnrE#S;vv#eoPC#zl
zJ6qk{_)-9oz}0JU(YbJgwJZp?Q!9Kmp=eyDQM>hS~yw>NueNII^
zYBfCsn5DLJ5c6k5RSSz3@^bYcRYAw42P=pV^TkzJL)BBW5E+gSJP^}AQ@SC3F
zg;0ZNU=R!G>JnLDPLj9{JjNO!{EYrh1Dtj*1-iN#tt3}+4JbehXD+(tT`mFZ)BBie
z?3uz~FoJP~A1;%YFCB6joOZd}LSDTmnaqt|=Q}4xLvOjBq9{eW9E-E)emOFl46}qM
zb*+WyP+>9dTPwX2*UNg=vG_BF(laKBsyO3)&Pfh?Av&e0g%;amZt(ja)h%8UELpOZR}RIToiL0(u~
z9xbMu^%>hNRhLZL;Zo0_dlY`Hd(3jS*=mOF3;Tv9?&o(l|mrD`wy*`V4_GnGwj#{I+=b@+*tCRU?e
z4bik420CC7Q
z*}S=McvEV8q{$>StGM%+^BfTN#~eodwA2=(E6pg)rZ3n%76(M4A~Pgrll4Kvqgz7%
zie7YUzCga}kf_xr$o1ssIyr@3$2X3+7GrC&^UA#L={R0q!}y^O^#(o}PqC7IXK&QigJgtBytLYo2kHk0cC0^?4Y{MOTaruh;H*CO7vk&2
zyV|vWjL%HYBVoAewsqd3mF1oUR_-Jh{(W%(L#n0mM5%%IeEn{PzAOlkzkz>{XSZ^A
zIAS^FZdD3a>~fbs&StU}nUOKR^~*FFPsGalS6Vtz)P&`fKfi9Enw+wHjvPvQan?JV
z0zO&s1&&6xy5mVN1IOGm3uy!w-G$E)LjIEhbKN@nzN*96*q>)&spkfNW%=i6HBAmZ
zCO^XH9Zh$4;4Kaay_>@m5^p~=DN^v$FC?y@3ypW&yr~#KNs~nDKcpxQ!tMXX&wy4e
z$oF4RUh$&?)E(pT`aoat<7XiV&7?mt{vV#hM>>zKCBB)=#drK2m4R8>omjf#I}7T&
zq;#%yhbdUW?ntZLnjg*K%^SFshp!n0k8xb)4=m3nFbijXhOpgEI2$!7T+y5EjG8i}
zZS9++ap%|SQ2cg49Pr@RG#uj64g#D_oMBd*Pi)p4{H%upe?v)-+vo;Dy!&qMiyqWf
zXKyasDJKPp%wK+Yne1#JoD3C6&veZ2exnWYI2$aXnwFP)w<5Yb^N~o{I2ncM>EqhZ
zyWo-f-k1Ct65=f&%#y62Z7dOYFMzxU3+L%o(=x?ecpegG`>G-pq6u&&g2hFYytWhi|
z^Cv~;4c(sJbF?Sz8sP*9eCI-e>cnK!F~^%g5MS}pgWN|sL4O&M);E0$PPfVPYMAnDuc?MYqO
za7JU(8@{Dy0V_&HWMjI@O=Ij5Gv15Yu+$C(n}x+4TW2xbH?ausvGgGW1-lWMZ{r;1
z*M;c2y{%lUqONOtxrc{m6qBYeKD6!Xk2_TD>C{7NX{wIgfa(SrV^MZ&b^eArl6EK~
z+o$!Lwch&B+IiWktvOrE#H)+YUUmC5m8yf-k-9ZD0Acd(q@DcmSQgG7tIs#H3}XZthevpnbxkFpYP17Q^9xLcRe8wb70
z&vNPLZS~v4v_Yxtmp2@PXQ5ng-O78xFdP+^B;`Z!u5TU$<9R?@bl@*}54__@73Yoh
z=D&OLqefKP<6aQ9Qq-~1#Du!38lTY@J3R8Wh9+F^>N4-z1?n)oJ
z{PxIH7@X@e)Wx@Q83Ws250B60c$1OKJ-*-zwZVAI?GR$4iBOoqdU4=Z@`)wa3}LcQ
zB_vPdVvj9H7S;&K_2PpXSQ+qVJ|wNR)AH~i;`r8$yN`xjkQ`Mrdqm#zL@+ryjjLZ#
zFctCWFAb&Hb8G%IC6@G?v+YSvn&M7GEQ&TH#$k7QL|j3LY}1{!3$s;u>#n-f$;2`TfHt9GVZIFqO*X#Rc
z)6uu~#6b$bGAi{;mZ#~3$o%DA?cIcF=EzfNDvbK|x8wy&qq@V94fCThhH;dGk2hUD
zbLrOaycdT_XV2E`n_#n-e|53yX%CtshZ0R-kW}S)5$vK%MV_pghW|r|69%v~9;Xn`@6IVf
z)o$x>_cdcM4LBv$zZQ~a(eNI^t~0icTTQ{gA8Q1T!C`GdboZz8i9=b?&EvAV9X@Yl
z^X#s)T6g<8)klz=0aWw=X0shyet4}>i;Qa{ql2~=$u-P2e6gn8`DcLFVBENGN}4;h
z>dwKv2xk#V^)3RD`h?uaY}K9%@1kvUghrO|URJs4e~jO{x#jnw?m4^&h--Q1e{OVm
zEIQTrZZLIXDp?<{e64~1uuHT33Z`IREZ++ZZ7{4nBoC7+P
zL(S&atP?tX($c7Y$B?eIGxqICYAFO|O{L9w89OIc!5#~U
zBo?>aV1F`JI{YYFQr5RwPBdrsBp9?j?>aie)@r7CS@$VywV1TO?3N_3Oq;ThOn*f@
zX5|+Hrar`a_H>+PvuqcI0*j)SX|yp_o+gh$c|yI=7sAS_v^*@YFxE1R
zEWKe_EOIhjofEmAM=#fq5KBzHYH87WussU`Snqx1$cs#+(D9cgJ<0g3I@huG8p~^Ek?WDg)=LXD#CNn$Unqe
z2A*|H%R4(O5^#|BK8iFn5jrX-13efbZNv%P-ml>rDfm#ex?JV9#V9ne)kRfdFlkLU
zwZ%mY)NSp~bPCABH!#*>n{2|D-+%ZJA`E^1=r1r5cmRF4Jf`Oad&oReyJ2%s%Jb3!
zYdjEH)UTT?7tk(+=i%|%z3hTPGY^J&ddObw&ei&AxoJvA!tEJ?dCuPW^x^i#7J5TV
z8sYZ#;>tefU>Pl8dj2+pow8@nyn@W|_&xvk0@~se){@Xx+xHo@RFg}!n@XRbQT+E8{y143e;@b}-Y!TI5b#|fG=tjs#<19$WgZNc)V0k#o
zw(!G`u+KRRLy(gnChPcqwc77#gDr}W!pwLcjlph|$tm6qD4A{|ZjnZ&6tOi^^$1Ydh%ZKvb{Y@Cq_nxZhQs0X
z94)332LFWq0O!P#4c;ugUVKGjPfAL(046ab&?W2#GwIiuBlvp#O-{M+hNgsIB8_+L
z0J_g3{4Ba2{hqx@Ozg)IPHRAiQA4b?J(Eq0mKH89%KjEQhbI=*c`@J%Ims;_y*}g$
z%nu%%cSyN>4ds0@{+yPPq%8%3PtMLz0wuQr?YJcK*VnGQ&iIo?N8HA-1K6+6Ia+wM
zaL%i~NkW#{G7o_5^YSo^9-lPOFPi-m_@5Oyu3V>KU(CSjR60)2hR;n^GCN-OgVX}{}AS%9&_ON+Uv7G_B+y^3~Oc_zcd
zaKzn=cbfEvn)*^7@3=Z|EONS4j#D8amLgtCVUM8^^YO8
zHhUyHUEJl$69GvdljCITJZL2Y!|4!@u`ga3A(OzfY3A%WSStWarFHH)Oy+Fu-z8KE
zLK2Df1$gCqkO^y2i#-x?n8)ITsvce#l~v2EM7b7FOD+qUiGJ3TXZ=HB1;Po5{q-e=dVeX7=~T1DRA^Ja0csmK#O
z%t5{l-u}VCpa}BIG*kTq%7lDQK_TPAYMQxtv5aJ+PgqenoDdXCOB%%`68W^o-2<5Wp#Fa@M7mn$*b_Sk!7
z^jP4_%8wcOKhdxQ7f9-dh=st&L>5n0TAI3+)_R@@?Bed)5)hwg)*gFz_By+^W>7m)
z`(%cp&LU#<#Uki%9F>jbw3V;z8L|zN36#U$V`_18m*tJaXjLsg@#yHlsP3YrkXOIY
zMQZkzW@d$23qC8nfJH|IQO#BQFvbmvA++)u4RkcI7vkgO;iN!a|}
zmG$>;f$D)>Hj);}#`~4cF%8W9_Rx~D7%N7Wa|s2%$>bndrf#uvkA|05l4Q<2cxaq1
zQj!iJese-&QHRx0FU0r=@I$oJKuK=HyKduKS!7d(glIBntVeq(?vZ=~DaB|xt`|y6
zDIPNQ3HM1oH+ul;(IsXwd@}pa9)z7qpF^nyp>FY)7QjmyRbL}!2B?1FWGhp$&I!M0
z$L%tuPb0g})79lQE)ji%R}MR@+ivU0o_$jw1FS>h9N6L7u|w16vI?|4p@E0QXcB}C
zb>`?V@+sB#JMO~S0b#kfPDD8@l4K?H9yKr4-#!h|RO(Fd6qhOSoJ2-eZjx&dtkOgJl;Z4qVFkxvZ+)T?F>S{3lh(r&2H*
zOG)_c{pEr@ft-=i0oeC!e0)4(r8Ro8vQP0Nc~6FXuR82)yHCzF!MDDvA1EcwBC--k
z0tP>&AJ`=^pc-Q>+{wNkptr{c!$TSlstzR%x)TbdTk*Byfd=oSdTaNU1KlqK~x)SxbjP{5=h?LP05s
z5NCpDH)IOrj(1fBg_!7Yp>fw83Q+uRch8d5^_9@bCXYmwGBHsxe%PF>o$(~EXku4M
zBTiYvFR9}>XW#!(qp!X;)a;HGW4^RRyrJv4X4yO-%@ZsrIt3XmlDXf%?O(UxtOY+tLY-Q~0mYw1Vz3cYHK9T6R{A&-$
zMq^j;c+%dPz$>GOgt=0JRGL`rRp
ztT?NjzLb`I6|Ujh+(Wtfq}N>Aa9y+(G7xb<9Zv4_$kRzOh;^0YOK)f?HZ1EG0&U)s
z#}&Ty7$jj`|(=R+-3
z^Zj{ZyCHJp79Lw%M&UWkjKL_PPU%9WivD?aRmHoLfF_*RztaG+B@iZILew^%KOR2b
zH-G}cz>b(SmlTWUC%?E(i4v4)G{G^T5!cg7nCn0K!*`X^(S?e;!#NDvCiC#EA%I#4
zTAtu-n}E6U30<=W-HqjzV50%e4LFh|2Y!B%DC`a$&Q?d?4*8KuSt-c$_ak2{41}n6
z2BD1YjKyh!pPfzvr5?wIR@2}^IDP~G*LPQ=Z2{%cL1qrU$L%;c+1+h1b!H3p+biOh
zX>4d>YMpgDa|`=OALVzkRFYv$T5Bo#_Kfu0`bI&TA7*bk&Z3YQb!r@%*+VSP
zO!|ZaA8}g(s9DPoD9-u@D#z0-OIuZOIKE&NS@mUF(V2p*T5acSEnk9d}l{Q9OHjuCnwc^or^V^=1Oe45}xz=
zcYm3I7&PZ3J!KYutI^Ak88cYT%g_ulN*Qq1GR*3r-2u5ni6=im_-XQj?O2
zpA&F-qoa6>rk#ONLa4^V!i>N6IDZ+E*pxNM{Ju=fGSx&o#53n%umRn0is72i4jB#F
zwNvnPQ(3j@z{tHvT2p_)CmI2LGjN9+Uh_SR`(ji%ZCfr!Xcx*2!)_okC9n8{&vj&k
zCR$^J=d(=Y)-W~Vdg{u}$^a8JH^AD;j(Ls$IlKwphDoF!^K&67M-{k^s>C59#mAb?
z+~$99yB1O(t6#sqP!LlNe1Z`8)2cCvM%(jxh$PGF-DS~yDzXkmM-eEhL&eh1KLjkg
z<@5WZO1gTkM_V&<&OZ0m+**0A#3Dm8<9x5+a0pTTATx2eF=(xRE`fY_erhb2hWvLZ
zfio=?k`?U9=uxrEdiCz(Zx;@&yUJ1%+J>OgstPOI$VzGAKsVQ~CHD3REORbn%XEF(
zGzq_rRA-L9@R#m>hlX_-U$$15=Pg8LDJ>0MF~UYuwCuCgyQ=HGkFPM7@dJhWaZ*N(
zVS(kw&PHby!Un1?;$2!*YQ=aCz4#Lb7EM-$D5dNtPe#ne5&wmbQMVscVea)hY;4D!
z(RO`yP|q*D{TTrM{vaCSBnmBR&PPV`#)nmM9r#R)H@r`1=vNk&_PJu=C1)U1>5cap
z4WWlkGSKwQ(fCzqg1oPKVS9{aLsClW>{K2_N!A6fTbfH#4SVwkx?x3|?3L0Qdsi$;
z*ct_E*wA<>O8qcl*jJ_)cGuQ`3W*zQ2OG?E)-P2zQWU$+RW3V->MY7T8umxG=p7^6
zq`xuoy>+E`%8fXg~i4+jIn3<9WU8nO}Nb)Q=)d
z5Kr##ZVv14cxl+_1z!ZkV8g^pyZErkC}@$}gaJaj5>8%T`k0X=bW?BjpUvwMf1gd?
zP=gBjU>mW7s$^GlG#2?~giuNEPlv4OuVGjvWVL&itQLM?KJw@O)@vLEF~=s32@3r!
zU^v<{D`5hS;EV+MI~JnIZIpoL>liOa%ulcXfiRK-Er>#M2+aVVi`|saA{TuZLW+@$NI&6>k75)Pq$Mty;2SU&{pK=2-}ykR52s{eq|eVRlA5
z&;+GlP&^|ERnEC?IX=Ff|F~8&<&U`!o)sE)m4MRiMn$NAck4Gq
zP3RLSJ*dB<^giQapW30zUKCx*Z02fzovw!+yh5mIVngHpoi)JqkroHC_O&hgXPny0
zZkYW2JO@wKt_Npf;$=)a|F{yEl0^mug#C}L>Tf)VYeLRk`)wJwW3c2e<9lcHkr4G$a_72Nz?p*jpymn{ZXkAK}leVjcrQ|Fh41v`p>~j
z0|$TNwm3dM1oQUwq*?d>%#HLXr(aI@*!}nacbEJjDqe(soctSK_^+KA8Ng}JXb-CK
z_{VC9ZhFG=UTk~D^t=8u(T~@d#Cu)H_(InL=bxLYN&8Rszjl5N_TTLV&g~ME%7LcC
z$-L!Ge}v81sY><#dD15+6{+857a(SnuBhS#*TCY}vXYXT7Bxy69w^dKdIq9+a3+*D
z=flmY11h*S9Q+CfVvdYKO4H*vlh5-$|2tkj)FMF01pE*%lA1$EFRd|K+CF
z!#7N7yoUP+q*fk~3c5f6cgUQyu>ZW}{ym|ad~hZOPO@5BnQ3WdOe!Zjz_xD(lQ{y$
z#s}rvjKTCD3aO$
zSClwzqlFL(HASH#bzz2XW+1dBaTr+3XM22@+gF}x%(GZIpEqu962}Qk_Q>F1liNdOhhn*3joZU{nQ|5X
za{`63jzJmCr0RD4%erUP@tl{m>-@|vpzkHH%e`#xiu&`&?i4uD9yq+ges^!Y0~>*)
z;r{9LIp$A;bMZI^eOqxYLR%TqXeC8OvASGT>2IQN=0-uJeU<6;TRB&{s^D^_N?=im
z6Om3SP2mQj65_Evv8aW~EIqgbJSH!VIRf8f6%=&GGCMCYl($;9PG8%sJ=VxBsgI$x
zH&{kA;~x&bE-f7eeJL)TJLk{SbK1$Iv5H`C<#*(?`F&z4gJpmjl6y82gt&pLJ8at?YOxC_xXkf
zW@Z}6uvu`PYGy6Cv2)Yc2|EnBU*nk}8*bO);RDKyIl@lS@GVXuF!WAiILx60M(4Uaj|$8^}||HO08v^Tim@Na!f
zYaEzzbPIn;^1+txH#wgC7z2yaBM(X8dXjwBCHOWdT~cizL%
z@X@5-`Z$d6uE(9D642H0P6-0~rnELFM!TcNix)Y*f-huRy(Cyf+M87!$J;^}{*QM%
zfW>EiR8z$S6q97{=*Se{J=|RjTa+wUax9}Es?wCtIrbho~q0pIF
zHj_On$~^3P062jKbOINWo?~Xt(7*%n#TvB8752p9moWc*%Mn^TDiwi?C7Vc@!5{qi
zJF$fHF0w{)yVPR_U_m`3)qx`A*`$}GiKek@YXO@hNXUG(_#A9TImBy>s4zUPpO=tR
z2xH{qB{4D`Jtxnc)Usqz2v$A+hqPxfo8%*jN%Ig@~_F2%2
z0dQ9C00o4z9wT|ULm7qY9Rp4HOIzJS&w)Ehi*NRz|qUvhg3s
zt5!u-9UN)ZejC}i%$SjWId6BjYGl$fk?VpG@rU`<_oqQ_vu>*C@AQPd|oE?$v*XOnc$ER
zA447#_HVdGIr*7lnnY^xn0TW5Mg74@OSbm*o3ErD4SSbw96dhG>_MQFzDk3>kU;}=
zyrl{Fo1Ba{5a)1q*pcOXw~UHaph3pdUe$Y-8Wm=VP=2N#U4rynH~LugapJW9sD1cN
z4>Et&s?!ecfJUkB2218Nl|H6R$UsGvCTJj5ws>Q3A-j(^u@B>Ylf5d{0tef?XY!A1
z8eTWJYS(iR&9M95(kGiw{-Yt^Q)aY|u|c>nt7OYIydY0`5&z)`V#9p~juksPMuqC{
z%lRi*iTVT#6R>@x`4Ru==Kdz&1NxvMxoMN4|8x;AA>n|&Cj22G<3Gmq35pW|=u;oCJ!T84gBFm+2KOPTs^956y@-0BGlU3!+4xQe6W^wp`BlH(IqJV(i=5qm
zZuaM>!jhsH9!DBUg~gd;`YB{et>w`d+Mpb&wdjcu
zNf7LK)z&9zMIQDzTpYpNGi1@!Z*ehUTpTSLt?*(YWb3ifB=Uqz@zz@UhAEL*C`eET
zc#YF60ms76PS5M$Bx=)>Kkev4qaQ8uhM%rVWXZO~?CKPCzx;P$Cij48O}d6j6bb&}
zs&zmS2e0X^nFG!WN-vx~w$S_l`@Y2QZ>(K}i=!eHWU596Fl=ym3Td7@K@T15T7_mU
z(YDT0Ueana*q3C!9*^fO7t!*uhaEYsyh2-QK8NqHa)wYj=5IVvsK@SDHx;sT^JPEA
zj5ne%v#OoFYMFD+?7=ay9U92OC_I-tJ*KV+BJv%M22K{vI7?ExHfGw@Zs@l
zQkG1o!ry^2ZP{L)eU1{o`z1fR*f+5CI}!SOfc=ZGNw_Q$(ZKN!BBLU0QdM_BQHSv5
z+eOA2rG#8k`IT(Ow}+E>P6oU{IWyC0qq-Z$=&(vT6itNjcbj
z3q^yH4WQHo6VY)%8blZt1@!t{-Bd#9<7P8HAGu0IAt6}i`PZ0+V`q}O)OqPS`;H=T
zprJFYea^sQ6S0((Y$%+D4stBoSCk1xvJN>SqQnhpnf`+_NoV-yoQsxJ
zTau*Fi3*Ad#zGX`;CQ#^kdZIx`G9=?CLR97LSmqPYC)fWaRz7r;V6+#r8_^Lm{0Nw6pAgje&m1p{8Irx0#R1TRo|800BS4bJ&PpFIsCMp8fNvJ
zS>we`Y>HXff7~I8%KJPT#o&@`o(_L=YHv4xP{?*~Z`$&;p5Udwb}?)kA8bo(N_D!l
zG&US+J4Q(+$R^6wkC%-S>&BD~02_?jzp;t4^B)rt?DYwi%Tp;2|*i;LrmX?<2yY}T2YNOLI
zDaImkr`Eu7eFNFtZCR2g%S&5FwIIcbb4125#S!4x?b85w%R#@0Lh)Omx*Zw;YObsp
zV)@@q&ES72DyTuSaOd*Ui$Frf66*5GN%|3{PVpNPjro*%%+_A!wK%^1j_G+~QDDxm
ziFxE^aaDU{sBCcH4!>*m9aJzC@UH|R=MrY5tAVrQnII2=+M2PW`Xc5_tD`H*CzcMY
zeG)!dO3>sBXTs4$;2*rx8X+lR87(;&zAr6##q%?IwCET4HYk4>H%)Xeq?9VZL^WR`
z`VKNRnaQA0ys2E;TWRG-qFs5iP3s1`wj94JIQO39kJR)mm`u*s5Tja2Uw%plbo5v?
zIF_!UBK6Pxda$NV96`*QG2cBzoDj89dXNcYnZT!ML8JrtG2z&q#)e8&;31MlVrK(S
zAq%0umBtbVfQ=$2u1alU5TUFW0>t!u|LtpV6qB(!
z>0+rk>6#SVM7OrSWE*RTXmbZ@yj3Cr3)h`DgY|svr^5_{wvkj$a_X-k
z3s#ar`z9i%oKDHfIiTP>*E%_@-wRD}^KX)}HUQHT|0$?>DeXx&&D&N1%W!rvXe6V8Ow3j`t9>4=EBxJY9G?`ukkVy>8I5;9;!lC*LnuRY=B*=gbbLud{|!5l7#TlG#>7LE2TJurT$sHD?`
z5;N@R?Nv{cDPrSbq^0b1?h~@2n%d)w&H4ly82uIrSs?a((1ioBnXEghH61tS@-RnK;e8NY-{U$-I|uC8TE9l4Re@UCK&L06@HNAkrqy{bLQlh
zK}u?KvE+^Y=17EQc%s-Z%;qlZe!u3-KNQwm<2!i>pmmh%)0BzLU63$Mhx8P5cTd(L
z#ws{m#SBR|V{Ra>z4aGF(14SYnLUJ1b+nT+f}bGy#w=RRyzClC83Oe=1-f;?+5)rP
zB(dkXd;}j+U#%f#% |