Commit 371e8505 authored by Douwe Maan's avatar Douwe Maan Committed by Robert Speicher

Merge branch 'ce-mirror-backport' into 'master'

Backport relevant changes from gitlab-org/gitlab-ee!51

To do:

- [x] Update gitlab-shell

See merge request !1822
parent 8382fac7
...@@ -328,6 +328,10 @@ table { ...@@ -328,6 +328,10 @@ table {
} }
} }
.well {
margin-bottom: 0;
}
.search_box { .search_box {
@extend .well; @extend .well;
text-align: center; text-align: center;
......
...@@ -172,7 +172,7 @@ ...@@ -172,7 +172,7 @@
} }
.panel-body { .panel-body {
form { form, pre {
margin: 0; margin: 0;
} }
......
...@@ -28,8 +28,8 @@ class Projects::ImportsController < Projects::ApplicationController ...@@ -28,8 +28,8 @@ class Projects::ImportsController < Projects::ApplicationController
if @project.import_finished? if @project.import_finished?
redirect_to(project_path(@project)) and return redirect_to(project_path(@project)) and return
else else
redirect_to new_namespace_project_import_path(@project.namespace, redirect_to(new_namespace_project_import_path(@project.namespace,
@project) && return @project)) and return
end end
end end
end end
......
...@@ -253,14 +253,6 @@ module ProjectsHelper ...@@ -253,14 +253,6 @@ module ProjectsHelper
filename_path(project, :version) filename_path(project, :version)
end end
def hidden_pass_url(original_url)
result = URI(original_url)
result.password = '*****' unless result.password.nil?
result
rescue
original_url
end
def project_wiki_path_with_version(proj, page, version, is_newest) def project_wiki_path_with_version(proj, page, version, is_newest)
url_params = is_newest ? {} : { version_id: version } url_params = is_newest ? {} : { version_id: version }
namespace_project_wiki_path(proj.namespace, proj, page, url_params) namespace_project_wiki_path(proj.namespace, proj, page, url_params)
......
...@@ -310,15 +310,17 @@ class Project < ActiveRecord::Base ...@@ -310,15 +310,17 @@ class Project < ActiveRecord::Base
def add_import_job def add_import_job
if forked? if forked?
unless RepositoryForkWorker.perform_async(id, forked_from_project.path_with_namespace, self.namespace.path) RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path)
import_fail
end
else else
RepositoryImportWorker.perform_async(id) RepositoryImportWorker.perform_async(self.id)
end end
end end
def clear_import_data def clear_import_data
update(import_error: nil)
ProjectCacheWorker.perform_async(self.id)
self.import_data.destroy if self.import_data self.import_data.destroy if self.import_data
end end
...@@ -346,6 +348,14 @@ class Project < ActiveRecord::Base ...@@ -346,6 +348,14 @@ class Project < ActiveRecord::Base
import_status == 'finished' import_status == 'finished'
end end
def safe_import_url
result = URI.parse(self.import_url)
result.password = '*****' unless result.password.nil?
result.to_s
rescue
original_url
end
def check_limit def check_limit
unless creator.can_create_project? or namespace.kind == 'group' unless creator.can_create_project? or namespace.kind == 'group'
errors[:limit_reached] << ("Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it") errors[:limit_reached] << ("Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it")
......
...@@ -6,7 +6,7 @@ class Repository ...@@ -6,7 +6,7 @@ class Repository
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
attr_accessor :raw_repository, :path_with_namespace, :project attr_accessor :path_with_namespace, :project
def self.clean_old_archives def self.clean_old_archives
repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path
...@@ -19,15 +19,19 @@ class Repository ...@@ -19,15 +19,19 @@ class Repository
def initialize(path_with_namespace, default_branch = nil, project = nil) def initialize(path_with_namespace, default_branch = nil, project = nil)
@path_with_namespace = path_with_namespace @path_with_namespace = path_with_namespace
@project = project @project = project
if path_with_namespace
@raw_repository = Gitlab::Git::Repository.new(path_to_repo)
@raw_repository.autocrlf = :input
end end
def raw_repository
return nil unless path_with_namespace
@raw_repository ||= begin
repo = Gitlab::Git::Repository.new(path_to_repo)
repo.autocrlf = :input
repo
rescue Gitlab::Git::Repository::NoRepository rescue Gitlab::Git::Repository::NoRepository
nil nil
end end
end
# Return absolute path to repository # Return absolute path to repository
def path_to_repo def path_to_repo
...@@ -105,29 +109,25 @@ class Repository ...@@ -105,29 +109,25 @@ class Repository
end end
def add_branch(branch_name, ref) def add_branch(branch_name, ref)
cache.expire(:branch_names) expire_branches_cache
@branches = nil
gitlab_shell.add_branch(path_with_namespace, branch_name, ref) gitlab_shell.add_branch(path_with_namespace, branch_name, ref)
end end
def add_tag(tag_name, ref, message = nil) def add_tag(tag_name, ref, message = nil)
cache.expire(:tag_names) expire_tags_cache
@tags = nil
gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message) gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
end end
def rm_branch(branch_name) def rm_branch(branch_name)
cache.expire(:branch_names) expire_branches_cache
@branches = nil
gitlab_shell.rm_branch(path_with_namespace, branch_name) gitlab_shell.rm_branch(path_with_namespace, branch_name)
end end
def rm_tag(tag_name) def rm_tag(tag_name)
cache.expire(:tag_names) expire_tags_cache
@tags = nil
gitlab_shell.rm_tag(path_with_namespace, tag_name) gitlab_shell.rm_tag(path_with_namespace, tag_name)
end end
...@@ -169,6 +169,16 @@ class Repository ...@@ -169,6 +169,16 @@ class Repository
end end
end end
def expire_tags_cache
cache.expire(:tag_names)
@tags = nil
end
def expire_branches_cache
cache.expire(:branch_names)
@branches = nil
end
def expire_cache def expire_cache
cache_keys.each do |key| cache_keys.each do |key|
cache.expire(key) cache.expire(key)
...@@ -484,7 +494,7 @@ class Repository ...@@ -484,7 +494,7 @@ class Repository
root_ref_commit = commit(root_ref) root_ref_commit = commit(root_ref)
if branch_commit if branch_commit
rugged.merge_base(root_ref_commit.id, branch_commit.id) == branch_commit.id is_ancestor?(branch_commit.id, root_ref_commit.id)
else else
nil nil
end end
...@@ -494,6 +504,11 @@ class Repository ...@@ -494,6 +504,11 @@ class Repository
rugged.merge_base(first_commit_id, second_commit_id) rugged.merge_base(first_commit_id, second_commit_id)
end end
def is_ancestor?(ancestor_id, descendant_id)
merge_base(ancestor_id, descendant_id) == ancestor_id
end
def search_files(query, ref) def search_files(query, ref)
offset = 2 offset = 2
args = %W(#{Gitlab.config.git.bin_path} grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref}) args = %W(#{Gitlab.config.git.bin_path} grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref})
......
...@@ -55,7 +55,9 @@ module Projects ...@@ -55,7 +55,9 @@ module Projects
@project.save @project.save
if @project.persisted? && !@project.import? if @project.persisted? && !@project.import?
raise 'Failed to create repository' unless @project.create_repository unless @project.create_repository
raise 'Failed to create repository'
end
end end
end end
...@@ -94,9 +96,7 @@ module Projects ...@@ -94,9 +96,7 @@ module Projects
@project.team << [current_user, :master, current_user] @project.team << [current_user, :master, current_user]
end end
if @project.import? @project.import_start if @project.import?
@project.import_start
end
end end
end end
end end
...@@ -18,17 +18,12 @@ ...@@ -18,17 +18,12 @@
.project-repo-buttons .project-repo-buttons
.split-one .split-one
= render 'projects/buttons/star' = render 'projects/buttons/star'
- unless empty_repo
= render 'projects/buttons/fork' = render 'projects/buttons/fork'
= render "shared/clone_panel" = render "shared/clone_panel"
.split-repo-buttons
- unless empty_repo
- if can? current_user, :download_code, @project
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn', rel: 'nofollow' do
= icon('download fw')
.split-repo-buttons
= render "projects/buttons/download"
= render 'projects/buttons/dropdown' = render 'projects/buttons/dropdown'
= render 'projects/buttons/notifications'
= render 'projects/buttons/notifications'
- unless @project.empty_repo?
- if can? current_user, :download_code, @project
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', rel: 'nofollow', title: "Download ZIP" do
= icon('download')
...@@ -32,5 +32,3 @@ ...@@ -32,5 +32,3 @@
= link_to new_namespace_project_tag_path(@project.namespace, @project) do = link_to new_namespace_project_tag_path(@project.namespace, @project) do
= icon('tags fw') = icon('tags fw')
New tag New tag
- if current_user && can?(current_user, :fork_project, @project) - unless @project.empty_repo?
- if current_user && can?(current_user, :fork_project, @project)
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn' do = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has_tooltip' do
= icon('code-fork fw') = icon('code-fork fw')
Fork Fork
%span.count %span.count
= @project.forks_count = @project.forks_count
- else - else
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn' do = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has_tooltip' do
= icon('code-fork fw') = icon('code-fork fw')
%span.count %span.count
= @project.forks_count = @project.forks_count
- if current_user - if current_user
= link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star', method: :post, remote: true do = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star has_tooltip', method: :post, remote: true, title: "Star project" do
= icon('star fw') = icon('star fw')
%span.count %span.count
= @project.star_count = @project.star_count
......
- page_title "Import repository" - page_title "Import repository"
%h3.page-title %h3.page-title
- if @project.import_failed?
Import failed. Retry?
- else
Import repository Import repository
%hr %hr
- if @project.import_failed?
.panel.panel-danger
.panel-heading The repository could not be imported.
.panel-body
%pre
:preserve
#{@project.import_error.try(:strip)}
= form_for @project, url: namespace_project_import_path(@project.namespace, @project), method: :post, html: { class: 'form-horizontal' } do |f| = form_for @project, url: namespace_project_import_path(@project.namespace, @project), method: :post, html: { class: 'form-horizontal' } do |f|
.form-group.import-url-data = render "shared/import_form", f: f
= f.label :import_url, class: 'control-label' do
%span Import existing git repo
.col-sm-10
= f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git'
.well.prepend-top-20
This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git.
%br
The import will time out after 4 minutes. For big repositories, use a clone/push combination.
For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}
.form-actions .form-actions
= f.submit 'Start import', class: "btn btn-create", tabindex: 4 = f.submit 'Start import', class: "btn btn-create", tabindex: 4
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- else - else
Import in progress. Import in progress.
- unless @project.forked? - unless @project.forked?
%p.monospace git clone --bare #{hidden_pass_url(@project.import_url)} %p.monospace git clone --bare #{@project.safe_import_url}
%p Please wait while we import the repository for you. Refresh at will. %p Please wait while we import the repository for you. Refresh at will.
:javascript :javascript
new ProjectImport(); new ProjectImport();
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
= f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'select2', tabindex: 2} = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'select2', tabindex: 2}
- if import_sources_enabled? - if import_sources_enabled?
.project-import.js-toggle-container .project-import.js-toggle-container
.form-group .form-group
%label.control-label Import project from %label.control-label Import project from
...@@ -82,19 +81,7 @@ ...@@ -82,19 +81,7 @@
%span Any repo by URL %span Any repo by URL
.js-toggle-content.hide .js-toggle-content.hide
.form-group.import-url-data = render "shared/import_form", f: f
= f.label :import_url, class: 'control-label' do
%span Git repository URL
.col-sm-10
= f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
.well.prepend-top-20
%ul
%li
The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
%li
The import will time out after 4 minutes. For big repositories, use a clone/push combination.
%li
To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}.
.prepend-botton-10 .prepend-botton-10
......
...@@ -21,7 +21,6 @@ ...@@ -21,7 +21,6 @@
= gitlab_config.protocol.upcase = gitlab_config.protocol.upcase
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true
- if project.kind_of?(Project) - if project.kind_of?(Project)
.input-group-addon .input-group-addon.has_tooltip{title: "#{visibility_level_label(project.visibility_level)} project", data: { container: "body" } }
.visibility-level-label.has_tooltip{'data-title' => "#{visibility_level_label(project.visibility_level)} project" } .visibility-level-label
= visibility_level_icon(project.visibility_level) = visibility_level_icon(project.visibility_level)
.form-group.import-url-data
= f.label :import_url, class: 'control-label' do
%span Git repository URL
.col-sm-10
= f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
.well.prepend-top-20
%ul
%li
The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.
%li
If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
%li
The import will time out after 4 minutes. For big repositories, use a clone/push combination.
%li
To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}.
...@@ -13,22 +13,20 @@ class RepositoryForkWorker ...@@ -13,22 +13,20 @@ class RepositoryForkWorker
end end
result = gitlab_shell.fork_repository(source_path, target_path) result = gitlab_shell.fork_repository(source_path, target_path)
unless result unless result
logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}") logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}")
project.update(import_error: "The project could not be forked.")
project.import_fail project.import_fail
project.save
return return
end end
if project.valid_repo? unless project.valid_repo?
ProjectCacheWorker.perform_async(project.id)
project.import_finish
else
project.import_fail
logger.error("Project #{id} had an invalid repository after fork") logger.error("Project #{id} had an invalid repository after fork")
project.update(import_error: "The forked repository is invalid.")
project.import_fail
return
end end
project.save project.import_finish
end end
end end
...@@ -7,18 +7,25 @@ class RepositoryImportWorker ...@@ -7,18 +7,25 @@ class RepositoryImportWorker
def perform(project_id) def perform(project_id)
project = Project.find(project_id) project = Project.find(project_id)
unless project.import_url == Project::UNKNOWN_IMPORT_URL if project.import_url == Project::UNKNOWN_IMPORT_URL
import_result = gitlab_shell.send(:import_repository, # In this case, we only want to import issues, not a repository.
project.path_with_namespace,
project.import_url)
return project.import_fail unless import_result
else
unless project.create_repository unless project.create_repository
return project.import_fail project.update(import_error: "The repository could not be created.")
project.import_fail
return
end
else
begin
gitlab_shell.import_repository(project.path_with_namespace, project.import_url)
rescue Gitlab::Shell::Error => e
project.update(import_error: e.message)
project.import_fail
return
end end
end end
data_import_result = case project.import_type data_import_result =
case project.import_type
when 'github' when 'github'
Gitlab::GithubImport::Importer.new(project).execute Gitlab::GithubImport::Importer.new(project).execute
when 'gitlab' when 'gitlab'
...@@ -32,12 +39,20 @@ class RepositoryImportWorker ...@@ -32,12 +39,20 @@ class RepositoryImportWorker
else else
true true
end end
return project.import_fail unless data_import_result
Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket' unless data_import_result
project.update(import_error: "The remote issue data could not be imported.")
project.import_fail
return
end
if project.import_type == 'bitbucket'
Gitlab::BitbucketImport::KeyDeleter.new(project).execute
end
project.import_finish project.import_finish
project.save
ProjectCacheWorker.perform_async(project.id) # Explicitly update mirror so that upstream remote is created and fetched
project.update_mirror
end end
end end
class AddImportErrorToProject < ActiveRecord::Migration
def change
add_column :projects, :import_error, :text
end
end
...@@ -641,6 +641,9 @@ ActiveRecord::Schema.define(version: 20151116144118) do ...@@ -641,6 +641,9 @@ ActiveRecord::Schema.define(version: 20151116144118) do
t.string "import_type" t.string "import_type"
t.string "import_source" t.string "import_source"
t.integer "commit_count", default: 0 t.integer "commit_count", default: 0
t.boolean "merge_requests_ff_only_enabled", default: false
t.text "issues_template"
t.text "import_error"
end end
add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree
......
...@@ -44,7 +44,6 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps ...@@ -44,7 +44,6 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
git_import_instructions = first('.js-toggle-content') git_import_instructions = first('.js-toggle-content')
expect(git_import_instructions).to be_visible expect(git_import_instructions).to be_visible
expect(git_import_instructions).to have_content "Git repository URL" expect(git_import_instructions).to have_content "Git repository URL"
expect(git_import_instructions).to have_content "The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL:"
end end
step 'I click on "Google Code"' do step 'I click on "Google Code"' do
......
module Gitlab module Gitlab
class Shell class Shell
class AccessDenied < StandardError; end class Error < StandardError; end
class KeyAdder < Struct.new(:io) class KeyAdder < Struct.new(:io)
def add_key(id, key) def add_key(id, key)
...@@ -36,8 +36,9 @@ module Gitlab ...@@ -36,8 +36,9 @@ module Gitlab
# import_repository("gitlab/gitlab-ci", "https://github.com/randx/six.git") # import_repository("gitlab/gitlab-ci", "https://github.com/randx/six.git")
# #
def import_repository(name, url) def import_repository(name, url)
Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'import-project', output, status = Popen::popen([gitlab_shell_projects_path, 'import-project', "#{name}.git", url, '240'])
"#{name}.git", url, '240']) raise Error, output unless status.zero?
true
end end
# Move repository # Move repository
......
...@@ -25,13 +25,6 @@ describe Projects::ForkService do ...@@ -25,13 +25,6 @@ describe Projects::ForkService do
end end
end end
context 'fork project failure' do
it "fails due to transaction failure" do
@to_project = fork_project(@from_project, @to_user, false)
expect(@to_project.import_failed?)
end
end
context 'project already exists' do context 'project already exists' do
it "should fail due to validation, not transaction failure" do it "should fail due to validation, not transaction failure" do
@existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) @existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
...@@ -66,7 +59,7 @@ describe Projects::ForkService do ...@@ -66,7 +59,7 @@ describe Projects::ForkService do
context 'fork project for group' do context 'fork project for group' do
it 'group owner successfully forks project into the group' do it 'group owner successfully forks project into the group' do
to_project = fork_project(@project, @group_owner, true, @opts) to_project = fork_project(@project, @group_owner, @opts)
expect(to_project.owner).to eq(@group) expect(to_project.owner).to eq(@group)
expect(to_project.namespace).to eq(@group) expect(to_project.namespace).to eq(@group)
expect(to_project.name).to eq(@project.name) expect(to_project.name).to eq(@project.name)
...@@ -78,7 +71,7 @@ describe Projects::ForkService do ...@@ -78,7 +71,7 @@ describe Projects::ForkService do
context 'fork project for group when user not owner' do context 'fork project for group when user not owner' do
it 'group developer should fail to fork project into the group' do it 'group developer should fail to fork project into the group' do
to_project = fork_project(@project, @developer, true, @opts) to_project = fork_project(@project, @developer, @opts)
expect(to_project.errors[:namespace]).to eq(['is not valid']) expect(to_project.errors[:namespace]).to eq(['is not valid'])
end end
end end
...@@ -87,7 +80,7 @@ describe Projects::ForkService do ...@@ -87,7 +80,7 @@ describe Projects::ForkService do
it 'should fail due to validation, not transaction failure' do it 'should fail due to validation, not transaction failure' do
existing_project = create(:project, name: @project.name, existing_project = create(:project, name: @project.name,
namespace: @group) namespace: @group)
to_project = fork_project(@project, @group_owner, true, @opts) to_project = fork_project(@project, @group_owner, @opts)
expect(existing_project.persisted?).to be_truthy expect(existing_project.persisted?).to be_truthy
expect(to_project.errors[:name]).to eq(['has already been taken']) expect(to_project.errors[:name]).to eq(['has already been taken'])
expect(to_project.errors[:path]).to eq(['has already been taken']) expect(to_project.errors[:path]).to eq(['has already been taken'])
...@@ -95,8 +88,8 @@ describe Projects::ForkService do ...@@ -95,8 +88,8 @@ describe Projects::ForkService do
end end
end end
def fork_project(from_project, user, fork_success = true, params = {}) def fork_project(from_project, user, params = {})
allow(RepositoryForkWorker).to receive(:perform_async).and_return(fork_success) allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
Projects::ForkService.new(from_project, user, params).execute Projects::ForkService.new(from_project, user, params).execute
end end
end end
...@@ -12,7 +12,6 @@ describe RepositoryForkWorker do ...@@ -12,7 +12,6 @@ describe RepositoryForkWorker do
project.path_with_namespace, project.path_with_namespace,
fork_project.namespace.path). fork_project.namespace.path).
and_return(true) and_return(true)
expect(ProjectCacheWorker).to receive(:perform_async)
subject.perform(project.id, subject.perform(project.id,
project.path_with_namespace, project.path_with_namespace,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment