diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b501ea9a58..d08f0d19f3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,13 +1,5 @@ +redmine.orgのチケットURL: https://www.redmine.org/issues/xxxx -____________________________________________________________________ - -Your contributions to Redmine are welcome! - -Please **open an issue on the [official website]** instead of sending pull requests. - -Since the development of Redmine is not conducted on GitHub but on the [official website] and core developers are not monitoring the GitHub repo, pull requests might not get reviewed. - -For more detail about how to contribute, please see the wiki page [Contribute] on the [official website]. - -[official website]: https://www.redmine.org/ -[Contribute]: https://www.redmine.org/projects/redmine/wiki/Contribute +TODO: +- [ ] 単体テストかく +- [ ] ... diff --git a/.github/actions/comment_patch_url.rb b/.github/actions/comment_patch_url.rb new file mode 100755 index 0000000000..6483c20e59 --- /dev/null +++ b/.github/actions/comment_patch_url.rb @@ -0,0 +1,76 @@ +#!/usr/bin/env ruby + +require 'json' + +require 'faraday' + +REPO = 'redmine-patch-meetup/redmine-dev-mirror' + +WORKFLOW_RUN = JSON.parse ENV['WORKFLOW_RUN_JSON'] + +CONNECTION = Faraday.new('https://api.github.com/') do |conn| + conn.response :raise_error + conn.adapter Faraday.default_adapter +end + +def repo_resource(resource) + "repos/#{REPO}/#{resource}" +end + +def get_repo_resource(resource) + response = CONNECTION.get repo_resource(resource) + JSON.parse response.body +end + +def post_to_repo_resource(resource, body) + response = CONNECTION.post repo_resource(resource), + body.to_json, + "Content-Type" => "application/json", + "Authorization" => "token #{ENV['GITHUB_TOKEN']}" + JSON.parse response.body +end + +def patch_artifact_id + response = JSON.parse CONNECTION.get(WORKFLOW_RUN['artifacts_url']).body + patch_artifact = response['artifacts'].find { |artifact| artifact['name'] == 'patch' } + patch_artifact['id'] +end + +def get_suite_id + suite_url = WORKFLOW_RUN['check_suite_url'] + id_start_index = suite_url.rindex('/') + 1 + suite_url[id_start_index..-1] +end + +def patch_artifact_download_url + "https://github.com/#{REPO}/suites/#{get_suite_id}/artifacts/#{patch_artifact_id}" +end + +def pull_request_number + WORKFLOW_RUN.dig('pull_requests', 0, 'number') +end + +def post_pr_comment(pr_number, comment) + post_to_repo_resource "issues/#{pr_number}/comments", { body: comment } +end + +def find_previous_comment_id(pr_number) + comments = get_repo_resource "issues/#{pr_number}/comments" + previous_comment = comments.find { |comment| + comment['body'].include?('Patch can be downloaded [here]') && comment['user']['login'].include?('github-actions') + } + previous_comment['id'] if previous_comment +end + +def delete_comment(comment_id) + CONNECTION.delete repo_resource("issues/comments/#{comment_id}"), nil, "Authorization" => "token #{ENV['GITHUB_TOKEN']}" +end + +def main + existing_comment_id = find_previous_comment_id(pull_request_number) + delete_comment(existing_comment_id) if existing_comment_id + + post_pr_comment pull_request_number, "Patch can be downloaded [here](#{patch_artifact_download_url})" if pull_request_number +end + +main if __FILE__ == $0 diff --git a/.github/actions/test-with-db.sh b/.github/actions/test-with-db.sh new file mode 100755 index 0000000000..2aac7b33a7 --- /dev/null +++ b/.github/actions/test-with-db.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -x + +database=$1 + +cp ./config/database.$database.yml ./config/database.yml +bundle install --path vendor/bundle --without minimagick +bundle update +bundle exec rake db:create db:migrate +bundle exec rake test diff --git a/.github/workflows/comment-patch.yml b/.github/workflows/comment-patch.yml new file mode 100644 index 0000000000..0bdcb1d602 --- /dev/null +++ b/.github/workflows/comment-patch.yml @@ -0,0 +1,24 @@ +name: Comment Patch + +on: + workflow_run: + workflows: + - Create Patch + types: + - completed + branches-ignore: + - master + - develop + +jobs: + create-patch: + runs-on: ubuntu-latest + steps: + - name: Install gems + run: sudo gem install faraday + - uses: actions/checkout@v2 + - name: Comment Patch URL + run: ./.github/actions/comment_patch_url.rb + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WORKFLOW_RUN_JSON: ${{ toJSON(github.event.workflow_run) }} diff --git a/.github/workflows/create-patch.yml b/.github/workflows/create-patch.yml new file mode 100644 index 0000000000..87acdd8e8c --- /dev/null +++ b/.github/workflows/create-patch.yml @@ -0,0 +1,35 @@ +name: Create Patch + +on: + push: + branches-ignore: + - master + - develop + +jobs: + create-patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Create Patch + run: | + git config user.email "$AUTHOR_EMAIL" + git config user.name "$AUTHOR_NAME" + git rev-parse --abbrev-ref HEAD > .branch-name + git checkout develop + git checkout -b patch + git merge --squash `cat .branch-name` + git commit -m "Patch for `cat .branch-name`" + PATCH_FILE=`cat .branch-name | sed 's/\//_/g'`.patch # Replace forward slash in branch name with _ + git format-patch develop..HEAD --stdout -k > $PATCH_FILE + echo "::set-output name=PATCH_FILE::$PATCH_FILE" + id: patch-creator + env: + AUTHOR_EMAIL: ${{ github.event.head_commit.author.email }} + AUTHOR_NAME: ${{ github.event.head_commit.author.name }} + - uses: actions/upload-artifact@v2 + with: + name: patch + path: ${{ steps.patch-creator.outputs.PATCH_FILE }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..58e4270fc6 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,93 @@ +name: Test + +on: + push: + branches-ignore: + - master + - develop + +jobs: + test-with-mysql: + strategy: + fail-fast: false + matrix: + ruby: [2.5, 2.6, 2.7] + db_version: [5.7] + runs-on: ubuntu-latest + container: + image: ruby:${{ matrix.ruby }} + services: + db: + image: mysql:${{ matrix.db_version }} + env: + MYSQL_ROOT_PASSWORD: password + ports: + - 3306:3306 + steps: + - uses: actions/checkout@v2 + - name: Cache gems + uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ matrix.ruby }}-mysql-${{ hashFiles('**/Gemfile') }} + restore-keys: | + ${{ matrix.ruby }}-mysql- + ${{ matrix.ruby }}- + - name: Install & run tests + run: ./.github/actions/test-with-db.sh mysql + env: + DB_HOST: db + test-with-postgres: + strategy: + fail-fast: false + matrix: + ruby: [2.5, 2.6, 2.7] + db_version: [9.5] + runs-on: ubuntu-latest + container: + image: ruby:${{ matrix.ruby }} + services: + db: + image: postgres:${{ matrix.db_version }} + env: + LANG: C.UTF-8 + POSTGRES_INITDB_ARGS: --locale=C.UTF-8 + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + # needed because the postgres container does not provide a healthcheck + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - uses: actions/checkout@v2 + - name: Cache gems + uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ matrix.ruby }}-postgres-${{ hashFiles('**/Gemfile') }} + restore-keys: | + ${{ matrix.ruby }}-postgres- + ${{ matrix.ruby }}- + - name: Install & run tests + run: ./.github/actions/test-with-db.sh postgres + env: + DB_HOST: db + test-with-sqlite: + strategy: + fail-fast: false + matrix: + ruby: [2.5, 2.6, 2.7] + runs-on: ubuntu-latest + container: + image: ruby:${{ matrix.ruby }} + steps: + - uses: actions/checkout@v2 + - name: Cache gems + uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ matrix.ruby }}-sqlite-${{ hashFiles('**/Gemfile') }} + restore-keys: | + ${{ matrix.ruby }}-sqlite- + ${{ matrix.ruby }}- + - name: Install & run tests + run: ./.github/actions/test-with-db.sh sqlite diff --git a/.gitignore b/.gitignore index ebc25cbd86..d90a44f348 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ /node_modules yarn-error.log + +docker-compose.yml diff --git a/Gemfile b/Gemfile index 949fe86d2a..5c756894e0 100644 --- a/Gemfile +++ b/Gemfile @@ -88,6 +88,10 @@ end group :development do gem "yard" + gem "guard" + gem "guard-minitest" + gem "pry", "<= 0.12.2" if RUBY_VERSION < '2.4' + gem "pry-byebug" end group :test do diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000000..2ee21bd185 --- /dev/null +++ b/Guardfile @@ -0,0 +1,3 @@ +guard :minitest, all_on_start: false do + watch(%r{^test/(.*)test(.*)\.rb$}) +end diff --git a/README.md b/README.md new file mode 100644 index 0000000000..2e0ca56282 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Redmineパッチ会 + +Redmine本体の改善をできる人を増やし、Redmineパッチ会を継続的に行っていけることを目的として、2020年7月から基本毎月オンライン開催しています。 + +基本は複数人のチームに分かれて、相談しながらパッチを書いています。 +(一人でもOK、今後参加者を中心にやり方が変わっていくかも) + +## このリポジトリ + +オープンソースのプロジェクト管理システム、[Redmine](https://redmine.org/projects/redmine)のフォークリポジトリです。 +このリポジトリでRedmineのバグ修正や機能改善を行い、Redmine本体に取り込んでもらうことでRedmineをより良くしていけるよう活動しています。 + +## Redmineパッチ会に参加したい + +Redmineの改善に興味ある方であればどなたでも。 +プログラミングせずに画面の文言変更でもパッチは送れます。 +一緒に仕様を考えて、本家にチケットを作成するだけでもやれることはあります。 Ruby・Railsのプログラミング経験があると更に幅は広がります。 + +初参加の場合、見学からでもお気軽にどうぞ(^^ + +### 1. Connpassでイベントを公開しているので、参加申し込みをしてみましょう! + +https://redmine-patch.connpass.com/ + +### 2. 参加登録をしたら、オンラインのやりとり・当日の会場として利用しているDiscordに参加しよう! + +イベントに参加登録をした方にのみ参加用URLが確認可能です。 +参加の上で不安な点、わからない点があったらテキストチャンネルで気軽に相談してください👍 + +### 3. チーム開発に参加できる環境を整えよう!(プログラミング以外での参加の場合は不要) + +主に通話にDiscord、複数人でのコーディングにVisual Studio CodeのLive Share拡張を利用しています。 +**VSCodeのLive Shareでモブプロのように参加できるため、Redmineが動く開発環境がなくても参加できます。** + +* [Visual Studio Code](https://code.visualstudio.com/)をインストール +* Visual Studio Codeを開いて、拡張機能 [Live Share](https://marketplace.visualstudio.com/items?itemName=MS-vsliveshare.vsliveshare)をインストール +* (ない人は)Githubのアカウントを作成 + +### 4. イベントの時間になったらDiscordを開いて、ボイスチャンネルに参加しよう! + +(時間になったら他の参加者も参加しているはず) \ No newline at end of file diff --git a/README.rdoc b/README.rdoc deleted file mode 100644 index 4132fb86e7..0000000000 --- a/README.rdoc +++ /dev/null @@ -1,5 +0,0 @@ -= Redmine - -Redmine is a flexible project management web application written using Ruby on Rails framework. - -More details can be found in the doc directory or on the official website http://www.redmine.org diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 15aa665a0a..d6694c815a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -28,9 +28,11 @@ class ApplicationController < ActionController::Base include Redmine::Hook::Helper include RoutesHelper include AvatarsHelper + include IconsHelper helper :routes helper :avatars + helper :icons class_attribute :accept_api_auth_actions class_attribute :accept_rss_auth_actions diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 03fb26d4ca..b0210c2169 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -126,9 +126,14 @@ def link_to_issue(issue, options={}) # * :download - Force download (default: false) def link_to_attachment(attachment, options={}) text = options.delete(:text) || attachment.filename + icon = 'paperclip' + icon_only = false + if options.delete(:download) route_method = :download_named_attachment_url options[:filename] = attachment.filename + icon = 'download' + icon_only = true else route_method = :attachment_url # make sure we don't have an extraneous :filename in the options @@ -137,7 +142,7 @@ def link_to_attachment(attachment, options={}) html_options = options.slice!(:only_path, :filename) options[:only_path] = true unless options.key?(:only_path) url = send(route_method, attachment, options) - link_to text, url, html_options + link_to labeled_icon(text, icon, icon_only: icon_only, size: 12), url, html_options end # Generates a link to a SCM revision @@ -797,7 +802,7 @@ def actions_dropdown(&block) content = capture(&block) if content.present? trigger = - content_tag('span', l(:button_actions), :class => 'icon-only icon-actions', + content_tag('span', labeled_icon(l(:button_actions), 'ellipsis-h', icon_only: true), :class => 'icon icon-actions icon-svg', :title => l(:button_actions)) trigger = content_tag('span', trigger, :class => 'drdn-trigger') content = content_tag('div', content, :class => 'drdn-items') @@ -1832,8 +1837,8 @@ def heads_for_auto_complete(project) def copy_object_url_link(url) link_to_function( - l(:button_copy_link), 'copyTextToClipboard(this);', - class: 'icon icon-copy-link', + labeled_icon(l(:button_copy_link), 'link'), 'copyTextToClipboard(this);', + class: 'icon icon-copy-link icon-svg', data: {'clipboard-text' => url} ) end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb new file mode 100644 index 0000000000..5a9bce5d6b --- /dev/null +++ b/app/helpers/icons_helper.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module IconsHelper + DEFAULT_ICON_SIZE = 14 + DEFAULT_ICON_PATH = 'public/svgs' + + def labeled_icon(label, icon_name = nil, size: DEFAULT_ICON_SIZE, css_class: nil, icon_only: false, path: DEFAULT_ICON_PATH) + css_classes = [] + css_classes << "s#{size}" if size + css_classes << "#{css_class}" unless css_class.blank? + + svg = svg_file(icon_name, css_classes, path) + + if icon_only + svg + else + svg + label + end + end + + private + + def svg_file(icon, css_class, path) + file_path = "#{Rails.root}/#{path}/#{icon}.svg" + + if File.exists?(file_path) + doc = content_tag(:svg,:class => css_class, :xmlns => "http://www.w3.org/2000/svg") do + content_tag(:use, '', :href => "/svgs/#{icon}.svg#icon") + end + else + doc = "" + end + + raw doc + end +end diff --git a/app/helpers/journals_helper.rb b/app/helpers/journals_helper.rb index d95d2b77b1..067e5a106d 100644 --- a/app/helpers/journals_helper.rb +++ b/app/helpers/journals_helper.rb @@ -34,27 +34,27 @@ def render_journal_actions(issue, journal, options={}) dropbown_links << copy_object_url_link(issue_url(issue, anchor: "note-#{indice}", only_path: false)) if journal.notes.present? if options[:reply_links] - links << link_to(l(:button_quote), + links << link_to(labeled_icon(l(:button_quote), 'quote-left', icon_only: true), quoted_issue_path(issue, :journal_id => journal, :journal_indice => indice), :remote => true, :method => 'post', :title => l(:button_quote), - :class => 'icon-only icon-comment' + :class => 'icon-only icon-comment icon-svg' ) end if journal.editable_by?(User.current) - links << link_to(l(:button_edit), + links << link_to(labeled_icon(l(:button_edit), 'pen-alt', icon_only: true), edit_journal_path(journal), :remote => true, :method => 'get', :title => l(:button_edit), - :class => 'icon-only icon-edit' + :class => 'icon-only icon-edit icon-svg' ) - dropbown_links << link_to(l(:button_delete), + dropbown_links << link_to(labeled_icon(l(:button_delete), 'trash-alt'), journal_path(journal, :journal => {:notes => ""}), :remote => true, :method => 'put', :data => {:confirm => l(:text_are_you_sure)}, - :class => 'icon icon-del' + :class => 'icon icon-del icon-svg' ) end end diff --git a/app/helpers/watchers_helper.rb b/app/helpers/watchers_helper.rb index d00c5b776c..f4a891a05d 100644 --- a/app/helpers/watchers_helper.rb +++ b/app/helpers/watchers_helper.rb @@ -26,15 +26,16 @@ def watcher_link(objects, user) return '' unless objects.any? watched = Watcher.any_watched?(objects, user) - css = [watcher_css(objects), watched ? 'icon icon-fav' : 'icon icon-fav-off'].join(' ') + css = [watcher_css(objects), watched ? 'icon icon-fav' : 'icon icon-fav-off icon-svg'].join(' ') text = watched ? l(:button_unwatch) : l(:button_watch) url = watch_path( :object_type => objects.first.class.to_s.underscore, :object_id => (objects.size == 1 ? objects.first.id : objects.map(&:id).sort) ) method = watched ? 'delete' : 'post' + icon = watched ? 'star' : 'star-off' - link_to text, url, :remote => true, :method => method, :class => css + link_to labeled_icon(text, icon), url, :remote => true, :method => method, :class => css end # Returns the css class used to identify watch links for a given +object+ diff --git a/app/views/attachments/_links.html.erb b/app/views/attachments/_links.html.erb index 25d022029f..dbb777b449 100644 --- a/app/views/attachments/_links.html.erb +++ b/app/views/attachments/_links.html.erb @@ -1,23 +1,23 @@