diff --git a/app/assets/javascripts/new_application.js b/app/assets/javascripts/new_application.js index 2444e8d68..a81dfeba4 100644 --- a/app/assets/javascripts/new_application.js +++ b/app/assets/javascripts/new_application.js @@ -89,4 +89,5 @@ $(document).ready(function() { $(this).button('loading'); }); + $('[data-toggle="tooltip"]').tooltip(); }); diff --git a/app/controllers/api/v1/projects_controller.rb b/app/controllers/api/v1/projects_controller.rb index bf67f75b4..d984bdcd5 100644 --- a/app/controllers/api/v1/projects_controller.rb +++ b/app/controllers/api/v1/projects_controller.rb @@ -70,7 +70,7 @@ class Api::V1::ProjectsController < Api::V1::BaseController def fork owner = (Group.find params[:group_id] if params[:group_id].present?) || current_user authorize! :write, owner if owner.class == Group - if forked = @project.fork(owner, params[:fork_name]) and forked.valid? + if forked = @project.fork(owner, new_name: params[:fork_name]) and forked.valid? render_json_response forked, 'Project has been forked successfully' else render_validation_error forked, 'Project has not been forked' diff --git a/app/controllers/projects/projects_controller.rb b/app/controllers/projects/projects_controller.rb index 12a183d58..9a5f5a23f 100644 --- a/app/controllers/projects/projects_controller.rb +++ b/app/controllers/projects/projects_controller.rb @@ -48,6 +48,9 @@ class Projects::ProjectsController < Projects::BaseController end def edit + @project_aliases = Project.where.not(id: @project.id). + where('alias_from_id IN (:ids) OR id IN (:ids)', { ids: [@project.alias_from_id, @project.id] }). + paginate(page: current_page) end def create @@ -114,7 +117,9 @@ class Projects::ProjectsController < Projects::BaseController def fork owner = (Group.find params[:group] if params[:group].present?) || current_user authorize! :write, owner if owner.class == Group - if forked = @project.fork(owner, params[:fork_name]) and forked.valid? + + is_alias = params[:alias] == 'true' + if forked = @project.fork(owner, new_name: params[:fork_name], is_alias: is_alias) and forked.valid? redirect_to forked, notice: t("flash.project.forked") else flash[:warning] = t("flash.project.fork_error") diff --git a/app/models/concerns/git.rb b/app/models/concerns/git.rb index 5c097374a..680efcdbd 100644 --- a/app/models/concerns/git.rb +++ b/app/models/concerns/git.rb @@ -134,6 +134,14 @@ module Git protected + def aliases_path + File.join(APP_CONFIG['git_path'], 'git_projects', '.aliases') + end + + def alias_path + File.join(aliases_path, "#{alias_from_id}.git") + end + def build_path(dir) File.join(APP_CONFIG['git_path'], 'git_projects', "#{dir}.git") end @@ -152,13 +160,36 @@ module Git end end + # Creates fork/alias for GIT repo def fork_git_repo - dummy = Grit::Repo.new(path) rescue parent.repo.fork_bare(path, shared: false) + dummy = Grit::Repo.new(path) rescue nil + # Do nothing if GIT repo already exist + unless dummy + if alias_from_id + FileUtils.mkdir_p(aliases_path) + if !Dir.exists?(alias_path) && alias_from + # Move GIT repo into aliases + FileUtils.mv(alias_from.path, alias_path, force: true) + # Create link for GIT + FileUtils.ln_sf alias_path, alias_from.path + end + # Create folder + FileUtils.mkdir_p File.join(APP_CONFIG['git_path'], 'git_projects', owner_uname || owner.uname) + # Create link for GIT + FileUtils.ln_sf alias_path, path + else + parent.repo.fork_bare(path, shared: false) + end + end write_hook end def destroy_git_repo FileUtils.rm_rf path + return unless alias_from_id + unless alias_from || Project.where.not(id: id).where(alias_from_id: alias_from_id).exists? + FileUtils.rm_rf alias_path + end end def write_hook diff --git a/app/models/project.rb b/app/models/project.rb index 3784265d6..0d33a5720 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -17,6 +17,9 @@ class Project < ActiveRecord::Base belongs_to :owner, polymorphic: true, counter_cache: :own_projects_count belongs_to :maintainer, class_name: 'User' + belongs_to :alias_from, class_name: 'Project' + has_many :aliases, class_name: 'Project', foreign_key: 'alias_from_id' + has_many :issues, dependent: :destroy has_many :pull_requests, dependent: :destroy, foreign_key: 'to_project_id' has_many :labels, dependent: :destroy @@ -216,13 +219,14 @@ class Project < ActiveRecord::Base end end - def fork(new_owner, new_name = name) + def fork(new_owner, new_name: nil, is_alias: false) new_name = new_name.presence || name dup.tap do |c| - c.name = new_name - c.parent_id = id - c.owner = new_owner - c.updated_at = nil; c.created_at = nil # :id = nil + c.name = new_name + c.parent_id = id + c.alias_from_id = alias_from_id || id if is_alias + c.owner = new_owner + c.updated_at = nil; c.created_at = nil # :id = nil # Hack to call protected method :) c.send :set_maintainer c.save diff --git a/app/views/projects/base/_submenu.html.haml b/app/views/projects/base/_submenu.html.haml deleted file mode 100644 index 32cc85a78..000000000 --- a/app/views/projects/base/_submenu.html.haml +++ /dev/null @@ -1,44 +0,0 @@ -- content_for :submenu do - - act = action_name.to_sym; contr = controller_name.to_sym; treeish = @project.default_head(params[:treeish]) - %nav.navbar.navbar-default{ role: 'navigation' } - .container-fluid - / Brand and toggle get grouped for better mobile display - .navbar-header - %button.navbar-toggle{ 'data-target' => '#submenu-navbar-collapse', 'data-toggle' => 'collapse', type: 'button' } - %span.sr-only Toggle navigation - %span.icon-bar - %span.icon-bar - %span.icon-bar - .navbar-brand - = fa_visibility_icon @project - = link_to @project.owner.uname, @project.owner - \/ - = link_to @project.name, project_path(@project) - / Collect the nav links, forms, and other content for toggling - #submenu-navbar-collapse.collapse.navbar-collapse - %ul.nav.navbar-nav.left-border - - if @project.parent - %li - = link_to project_path(@project.parent), class: 'small' do - = fa_icon 'code-fork' - = @project.parent.name_with_owner - %li{ class: ('active' if act.in?([:show, :edit, :branches, :tags]) && contr.in?([:trees, :blobs]) || contr == :commits) } - = link_to t('project_menu.code'), tree_path(@project, treeish) - - if @project.is_package and can?(:read, @project => BuildList) - %li{ class: ('active' if contr == :build_lists) } - = link_to t('project_menu.builds'), project_build_lists_path(@project) - - - if @project.has_issues - %li{ class: ('active' if contr == :issues && act == :index) } - = link_to t('project_menu.tracker', count: @opened_issues_count), project_issues_path(@project) - %li{ class: ('active' if contr == :issues && act == :pull_requests) } - =link_to t('project_menu.pull_requests', count: @opened_pull_requests_count), project_pull_requests_path(@project) - - if @project.has_wiki - %li{ class: ('active' if contr == :wiki) } - = link_to t('project_menu.wiki'), project_wiki_index_path(@project) - %li=# link_to t('project_menu.readme'), '#' #pending - - if can? :update, @project - %li{ class: ('active' if act.in?(%i[edit update sections]) && contr == :projects) } - = link_to t('project_menu.settings'), edit_project_path(@project) - / /.navbar-collapse - / /.container-fluid diff --git a/app/views/projects/base/_submenu.html.slim b/app/views/projects/base/_submenu.html.slim new file mode 100644 index 000000000..d13399a8b --- /dev/null +++ b/app/views/projects/base/_submenu.html.slim @@ -0,0 +1,50 @@ +- content_for :submenu do + - act = action_name.to_sym; contr = controller_name.to_sym; treeish = @project.default_head(params[:treeish]) + nav.navbar.navbar-default role='navigation' + .container-fluid + / Brand and toggle get grouped for better mobile display + .navbar-header + button.navbar-toggle data-target='#submenu-navbar-collapse' data-toggle='collapse' type='button' + span.sr-only Toggle navigation + span.icon-bar + span.icon-bar + span.icon-bar + .navbar-brand + = fa_visibility_icon @project + = link_to @project.owner.uname, @project.owner + | / + = link_to @project.name, project_path(@project) + / Collect the nav links, forms, and other content for toggling + #submenu-navbar-collapse.collapse.navbar-collapse + ul.nav.navbar-nav.left-border + - if @project.parent + - is_alias = @project.alias_from_id.present? + - tooltip_title = t(".tooltips.#{is_alias ? 'alias' : 'fork' }", name: @project.parent.name_with_owner) + li data-toggle='tooltip' data-placement='bottom' title=tooltip_title + = link_to project_path(@project.parent), class: 'small' do + - if is_alias + = fa_icon 'share-alt' + - else + = fa_icon 'code-fork' + =< @project.parent.name_with_owner + li class=('active' if act.in?([:show, :edit, :branches, :tags]) && contr.in?([:trees, :blobs]) || contr == :commits) + = link_to t('project_menu.code'), tree_path(@project, treeish) + - if @project.is_package and can?(:read, @project => BuildList) + li class=('active' if contr == :build_lists) + = link_to t('project_menu.builds'), project_build_lists_path(@project) + + - if @project.has_issues + li class=('active' if contr == :issues && act == :index) + = link_to t('project_menu.tracker', count: @opened_issues_count), project_issues_path(@project) + li class=('active' if contr == :issues && act == :pull_requests) + = link_to t('project_menu.pull_requests', count: @opened_pull_requests_count), project_pull_requests_path(@project) + - if @project.has_wiki + li class=('active' if contr == :wiki) + = link_to t('project_menu.wiki'), project_wiki_index_path(@project) + li + / = link_to t('project_menu.readme'), '#' #pending + - if can? :update, @project + li class=('active' if act.in?(%i[edit update sections]) && contr == :projects) + = link_to t('project_menu.settings'), edit_project_path(@project) + / /.navbar-collapse + / /.container-fluid diff --git a/app/views/projects/git/base/_choose_fork.html.haml b/app/views/projects/git/base/_choose_fork.html.haml deleted file mode 100644 index 4c216ccb6..000000000 --- a/app/views/projects/git/base/_choose_fork.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- is_group = owner.class == Group ? "(#{t 'activerecord.models.group'})" : '' -- full_name = "#{owner.uname}/#{name} #{is_group}" - -- if owner.own_projects.exists? name: name - %p.text-center - =t 'layout.projects.already_exists' - =link_to full_name, project_path("#{owner.uname}/#{name}")#, class: 'center-block' -- else - = form_for @project, url: fork_project_path(@project), html: { class: :form, multipart: true, method: :post } do |f| - = hidden_field_tag :group, owner.id if owner.class == Group - = hidden_field_tag :fork_name, name, name: 'fork_name' - =f.submit t('layout.projects.fork_to', to: full_name), class: 'btn btn-primary center-block', - 'data-loading-text' => t('layout.processing'), id: 'create_fork' diff --git a/app/views/projects/git/base/_choose_fork.html.slim b/app/views/projects/git/base/_choose_fork.html.slim new file mode 100644 index 000000000..68527a2da --- /dev/null +++ b/app/views/projects/git/base/_choose_fork.html.slim @@ -0,0 +1,22 @@ +- is_group = owner.class == Group ? "(#{t 'activerecord.models.group'})" : '' +- full_name = "#{owner.uname}/#{name} #{is_group}" + +- if owner.own_projects.exists? name: name + p.text-center + => t('layout.projects.already_exists') + = link_to full_name, project_path("#{owner.uname}/#{name}") +- else + = form_for @project, url: fork_project_path(@project), html: { class: :form, multipart: true, method: :post } do |f| + = hidden_field_tag :group, owner.id if owner.class == Group + = hidden_field_tag :fork_name, name, name: 'fork_name' + = hidden_field_tag :alias, '{{create_alias}}' + .btn-group.btn-group-justified ng-init='create_alias = false' + .btn-group + = f.submit t('layout.projects.fork_to', to: full_name), + class: 'btn btn-primary center-block', + 'data-loading-text' => t('layout.processing'), id: 'create_fork' + .btn-group + = f.submit t('layout.projects.create_alias_for', for: full_name), + class: 'btn btn-primary center-block', + ng_click: 'create_alias = true', + 'data-loading-text' => t('layout.processing'), id: 'create_fork' \ No newline at end of file diff --git a/app/views/projects/git/base/_forks.html.haml b/app/views/projects/git/base/_forks.html.haml deleted file mode 100644 index 7e32ebc6e..000000000 --- a/app/views/projects/git/base/_forks.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -=render 'projects/git/base/choose_fork', owner: current_user, name: name -%hr -- Group.can_own_project(current_user).each do |group| - =render 'projects/git/base/choose_fork', owner: group, name: name - %hr \ No newline at end of file diff --git a/app/views/projects/git/base/_forks.html.slim b/app/views/projects/git/base/_forks.html.slim new file mode 100644 index 000000000..c75ee407f --- /dev/null +++ b/app/views/projects/git/base/_forks.html.slim @@ -0,0 +1,5 @@ +== render 'projects/git/base/choose_fork', owner: current_user, name: name +hr +- Group.can_own_project(current_user).each do |group| + == render 'projects/git/base/choose_fork', owner: group, name: name + hr \ No newline at end of file diff --git a/app/views/projects/projects/_aliases.html.slim b/app/views/projects/projects/_aliases.html.slim new file mode 100644 index 000000000..e0eee2f9a --- /dev/null +++ b/app/views/projects/projects/_aliases.html.slim @@ -0,0 +1,15 @@ +.col-sm-9.col-sm-offset-3 + + h3= t('.header') + + table.table.table-condensed.offset20 + thead + tr + th= t('.name') + tbody + - @project_aliases.each do |project| + tr + td + = fa_visibility_icon @project + = link_to project.name_with_owner, project_path(project) + = will_paginate @project_aliases diff --git a/app/views/projects/projects/edit.html.slim b/app/views/projects/projects/edit.html.slim index 0d7ea9aae..6089156f4 100644 --- a/app/views/projects/projects/edit.html.slim +++ b/app/views/projects/projects/edit.html.slim @@ -1,16 +1,19 @@ -set_meta_tags title: [title_object(@project), t('layout.projects.edit')] -= render 'submenu' +== render 'submenu' .container.col-md-offset-2.col-md-8 .row - = render 'settings_menu' + == render 'settings_menu' = simple_form_for @project, html: { class: 'form-horizontal' }, wrapper: :horizontal_form, wrapper_mappings: { boolean: :horizontal_boolean } do |f| - = render "form", f: f + == render "form", f: f - = render 'build_schedule' + == render 'build_schedule' + - if @project_aliases.present? + hr.col-sm-12 + == render 'aliases' hr.col-sm-12 .col-sm-9.col-sm-offset-3 diff --git a/config/locales/models/project.en.yml b/config/locales/models/project.en.yml index 7c2ee4368..0666784c5 100644 --- a/config/locales/models/project.en.yml +++ b/config/locales/models/project.en.yml @@ -1,4 +1,17 @@ en: + + projects: + projects: + aliases: + header: "Aliases" + name: "Project name" + + base: + submenu: + tooltips: + alias: "Alias of %{name}" + fork: "Fork of %{name}" + layout: projects: placeholder: @@ -21,6 +34,7 @@ en: edit: Settings fork_and_edit: Fork fork_to: Fork to %{to} + create_alias_for: Create alias for %{for} fork_modal_header: Where do you want to fork this project? already_exists: Project already exists unexisted_project: Project not exists diff --git a/config/locales/models/project.ru.yml b/config/locales/models/project.ru.yml index 87a988194..357299af9 100644 --- a/config/locales/models/project.ru.yml +++ b/config/locales/models/project.ru.yml @@ -1,4 +1,18 @@ ru: + + projects: + projects: + aliases: + header: "Ссылки" + name: "Название проекта" + + + base: + submenu: + tooltips: + alias: "Ссылка %{name}" + fork: "Клонирование %{name}" + layout: projects: placeholder: @@ -21,6 +35,7 @@ ru: edit: Настройки fork_and_edit: Клонировать fork_to: Клонировать в %{to} + create_alias_for: Создать ссылку для %{for} fork_modal_header: Куда Вы хотите клонировать проект? already_exists: Проект уже существует unexisted_project: Проект не существует diff --git a/db/migrate/20150112204757_add_alias_from_to_projects.rb b/db/migrate/20150112204757_add_alias_from_to_projects.rb new file mode 100644 index 000000000..a0ebc11ad --- /dev/null +++ b/db/migrate/20150112204757_add_alias_from_to_projects.rb @@ -0,0 +1,6 @@ +class AddAliasFromToProjects < ActiveRecord::Migration + def change + add_column :projects, :alias_from_id, :integer + add_index :projects, :alias_from_id + end +end diff --git a/db/schema.rb b/db/schema.rb index e93e250ca..3a59b0325 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: 20141015193923) do +ActiveRecord::Schema.define(version: 20150112204757) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -173,6 +173,8 @@ ActiveRecord::Schema.define(version: 20141015193923) do t.string "owner_uname", null: false t.boolean "architecture_dependent", default: false, null: false t.integer "autostart_status" + t.integer "alias_from_id" + t.index ["alias_from_id"], :name => "index_projects_on_alias_from_id" t.index ["name", "owner_id", "owner_type"], :name => "index_projects_on_name_and_owner_id_and_owner_type", :unique => true, :case_sensitive => false end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index b3c2daf17..e37f649fc 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -3,43 +3,95 @@ require 'spec_helper' describe Project do before { stub_symlink_methods } - context 'creation' do - let(:root_project) { FactoryGirl.create(:project) } - let(:child_project) { root_project.fork(FactoryGirl.create(:user)) } + context '#fork' do + let(:root_project) { FactoryGirl.create(:project) } + let(:child_project) { root_project.fork(FactoryGirl.create(:user)) } let(:child_child_project) { child_project.fork(FactoryGirl.create(:user)) } + let(:alias_project) { root_project.fork(FactoryGirl.create(:user), is_alias: true) } + let(:alias_alias_project) { alias_project.fork(FactoryGirl.create(:user), is_alias: true) } - it { root_project } - it { child_project } - it { child_child_project } - end + context 'creation' do - context 'for destroy' do - let!(:root_project) { FactoryGirl.create(:project) } - let!(:child_project) { root_project.fork(FactoryGirl.create(:user)) } - let!(:child_child_project) { child_project.fork(FactoryGirl.create(:user)) } + it { root_project } - context 'root project' do - before { root_project.destroy } - - it "should not be delete child" do - Project.where(id: child_project).count.should == 1 + it 'creates child project' do + expect(child_project).to be_valid + expect(child_project.parent).to eq(root_project) + expect(child_project.alias_from).to be_nil + expect{ Grit::Repo.new(child_project.path) }.to_not raise_exception end - it "should not be delete child of the child" do - Project.where(id: child_child_project).count.should == 1 + it 'creates child-child project' do + expect(child_child_project).to be_valid + expect(child_child_project.parent).to eq(child_project) + expect(child_child_project.alias_from).to be_nil + expect{ Grit::Repo.new(child_child_project.path) }.to_not raise_exception + end + + it 'creates alias project' do + expect(alias_project).to be_valid + expect(alias_project.parent).to eq(root_project) + expect(alias_project.alias_from).to eq(root_project) + expect{ Grit::Repo.new(alias_project.path) }.to_not raise_exception + end + + it 'creates alias-alias project' do + expect(alias_alias_project).to be_valid + expect(alias_alias_project.parent).to eq(alias_project) + expect(alias_alias_project.alias_from).to eq(root_project) + expect{ Grit::Repo.new(alias_alias_project.path) }.to_not raise_exception end end - pending 'when will be available orphan_strategy: :adopt' do - context 'middle node' do - before{ child_project.destroy } + context 'for destroy' do - it "should set root project as a parent for orphan child" do - Project.find(child_child_project).ancestry == root_project - end + it 'root project' do + child_child_project # init chain of projects + expect do + root_project.destroy + end.to change(Project, :count).by(-1) + end - it "should not be delete child of the child" do - Project.where(id: child_child_project).count.should == 1 + it 'middle child node' do + child_child_project # init chain of projects + expect do + child_project.destroy + end.to change(Project, :count).by(-1) + end + + it 'alias nodes' do + alias_alias_project # init chain of projects + expect do + alias_project.destroy + end.to change(Project, :count).by(-1) + expect{ Grit::Repo.new(root_project.path) }.to_not raise_exception + expect{ Grit::Repo.new(alias_alias_project.path) }.to_not raise_exception + expect{ Grit::Repo.new(alias_project.path) }.to raise_exception + + expect do + alias_alias_project.destroy + end.to change(Project, :count).by(-1) + expect{ Grit::Repo.new(root_project.path) }.to_not raise_exception + expect{ Grit::Repo.new(alias_alias_project.path) }.to raise_exception + + expect do + root_project.destroy + end.to change(Project, :count).by(-1) + expect{ Grit::Repo.new(root_project.path) }.to raise_exception + expect{ Grit::Repo.new(root_project.send(:alias_path)) }.to raise_exception + end + + pending 'when will be available orphan_strategy: :adopt' do + context 'middle node' do + before{ child_project.destroy } + + it "should set root project as a parent for orphan child" do + Project.find(child_child_project).ancestry == root_project + end + + it "should not be delete child of the child" do + Project.where(id: child_child_project).count.should == 1 + end end end end