├── .devcontainer ├── Dockerfile ├── devcontainer.json └── docker-compose.yml ├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .ruby-version ├── Capfile ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ ├── favicon.ico │ │ ├── rails.png │ │ └── rails_contributors_logo.gif │ ├── javascripts │ │ └── application.js │ └── stylesheets │ │ ├── application.css │ │ ├── reset.css │ │ ├── screen.scss │ │ └── turbolinks.scss ├── controllers │ ├── application_controller.rb │ ├── commits_controller.rb │ ├── contributors_controller.rb │ ├── faqs_controller.rb │ └── releases_controller.rb ├── helpers │ └── application_helper.rb ├── models │ ├── application_record.rb │ ├── commit.rb │ ├── contribution.rb │ ├── contributor.rb │ ├── names_manager.rb │ ├── names_manager │ │ ├── canonical_names.rb │ │ ├── false_positives.rb │ │ └── hard_coded_authors.rb │ ├── release.rb │ ├── repo.rb │ ├── repo_update.rb │ └── time_constraints.rb └── views │ ├── commits │ ├── _commit.html.erb │ └── index.html.erb │ ├── contributors │ ├── _contributor.html.erb │ └── index.html.erb │ ├── faqs │ └── show.html.erb │ ├── layouts │ ├── _top_bar_content.html.erb │ └── application.html.erb │ ├── releases │ ├── _release.html.erb │ └── index.html.erb │ └── shared │ └── _window_sidebar.html.erb ├── bin ├── bundle ├── cap ├── new-aliases ├── rails ├── rake ├── setup └── yarn ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── database.yml ├── deploy.rb ├── deploy │ └── production.rb ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ ├── staging.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── content_security_policy.rb │ ├── core_extensions.rb │ ├── ensure_rails_git_is_cloned.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── nfc_attribute_normalization.rb │ ├── permissions_policy.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── logrotate ├── nginx.conf └── routes.rb ├── db ├── migrate │ ├── 20141101094005_initial_schema.rb │ ├── 20150326181907_add_first_contribution_at_to_contributors.rb │ └── 20160512095609_rename_nmupdated.rb ├── schema.rb └── seeds.rb ├── dc ├── bash ├── deploy ├── exec ├── psql ├── rails ├── server └── sync ├── doc ├── design.md ├── docker.md ├── fix_credit.md └── production.md ├── docker-compose.yml ├── lib ├── application_utils.rb ├── bot_killer.rb ├── nfc_attribute_normalizer.rb └── tasks │ └── .gitkeep ├── public ├── 404.html ├── 406-unsupported-browser.html ├── 422.html ├── 500.html ├── favicon.ico ├── robots.txt └── system │ └── maintenance.html.deleteme ├── test ├── application_system_test_case.rb ├── controllers │ ├── commits_controller_test.rb │ ├── contributors_controller_test.rb │ └── releases_controller_test.rb ├── credits │ ├── canonical_names_test.rb │ ├── cleanup_test.rb │ ├── disambiguation_test.rb │ ├── false_positives_test.rb │ ├── hard_coded_authors_test.rb │ ├── heuristics_test.rb │ └── wanted_aliases_test.rb ├── fixtures │ ├── commits.yml │ ├── contributions.yml │ ├── contributors.yml │ ├── diff_more_than_changelogs_69edebf.log │ ├── diff_only_changelogs_e3a39ca.log │ ├── releases.yml │ └── repo_updates.yml ├── helpers │ └── application_helper_test.rb ├── integration │ └── bot_killer_test.rb ├── models │ ├── commit_test.rb │ ├── contributor_test.rb │ ├── names_manager_test.rb │ ├── nfc_attribute_normalizer_test.rb │ ├── parameterize_test.rb │ ├── release_test.rb │ ├── repo_test.rb │ └── time_constraints_test.rb ├── support │ └── assert_contributor_names.rb └── test_helper.rb └── tmp └── .keep /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Ruby version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.1, 3.0, 2, 2.7, 2.6, 3-bullseye, 3.1-bullseye, 3.0-bullseye, 2-bullseye, 2.7-bullseye, 2.6-bullseye, 3-buster, 3.1-buster, 3.0-buster, 2-buster, 2.7-buster, 2.6-buster 2 | ARG VARIANT="3.3.4" 3 | FROM ghcr.io/rails/devcontainer/images/ruby:${VARIANT} 4 | 5 | # Default value to allow debug server to serve content over GitHub Codespace's port forwarding service 6 | # The value is a comma-separated list of allowed domains 7 | ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev" 8 | 9 | # [Optional] Uncomment this section to install additional OS packages. 10 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 11 | && apt-get -y install --no-install-recommends cmake pkg-config 12 | 13 | # [Optional] Uncomment this line to install additional gems. 14 | # RUN gem install 15 | 16 | # [Optional] Uncomment this line to install global node packages. 17 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 18 | 19 | # Donwload rails repository locally to do calculations on the repository 20 | RUN git clone --mirror https://github.com/rails/rails.git 21 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/ruby-rails-postgres 3 | // Update the VARIANT arg in docker-compose.yml to pick a Ruby version 4 | { 5 | "name": "Ruby on Rails & Postgres", 6 | "dockerComposeFile": "docker-compose.yml", 7 | "service": "app", 8 | "workspaceFolder": "/workspace", 9 | 10 | // Configure tool-specific properties. 11 | "customizations": { 12 | }, 13 | 14 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 15 | // This can be used to network with other containers or the host. 16 | // "forwardPorts": [3000, 5432], 17 | 18 | // Use 'postCreateCommand' to run commands after the container is created. 19 | // "postCreateCommand": "bundle install && rake db:setup", 20 | 21 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 22 | "remoteUser": "vscode", 23 | "features": { 24 | "github-cli": "latest", 25 | "ghcr.io/devcontainers/features/node:1": {}, 26 | "ghcr.io/rails/devcontainer/features/postgres-client": {} 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | build: 6 | context: .. 7 | dockerfile: .devcontainer/Dockerfile 8 | args: 9 | # Update 'VARIANT' to pick a version of Ruby: 3, 3.1, 3.0, 2, 2.7, 2.6 10 | # Append -bullseye or -buster to pin to an OS version. 11 | # Use -bullseye variants on local arm64/Apple Silicon. 12 | VARIANT: "3.3.4" 13 | 14 | volumes: 15 | - ..:/workspaces:cached 16 | 17 | # Overrides default command so things don't shut down after the process ends. 18 | command: sleep infinity 19 | 20 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. 21 | network_mode: service:db 22 | # Uncomment the next line to use a non-root user for all processes. 23 | # user: vscode 24 | 25 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. 26 | # (Adding the "ports" property to this file will not forward from a Codespace.) 27 | 28 | db: 29 | image: postgres:14.13 30 | restart: unless-stopped 31 | volumes: 32 | - postgres-data:/var/lib/postgresql/data 33 | environment: 34 | POSTGRES_USER: postgres 35 | POSTGRES_DB: postgres 36 | POSTGRES_PASSWORD: postgres 37 | # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally. 38 | # (Adding the "ports" property to this file will not forward from a Codespace.) 39 | 40 | volumes: 41 | postgres-data: null 42 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | .circleci 3 | .git 4 | vendor/bundle 5 | log/* 6 | !/log/.keep 7 | tmp/* 8 | !/tmp/.keep 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "bundler" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | services: 14 | postgres: 15 | image: postgres:14.13 16 | env: 17 | POSTGRES_DB: rails_contributors_test 18 | POSTGRES_PASSWORD: postgres 19 | ports: 20 | - 5432:5432 21 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 22 | 23 | env: 24 | BUNDLE_PATH: vendor/bundle 25 | DATABASE_URL: postgres://postgres:postgres@localhost:5432/rails_contributors_test 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | 30 | - name: Install system dependencies 31 | run: | 32 | sudo apt-get update 33 | sudo apt-get install -y git cmake pkg-config curl libpq-dev tzdata 34 | 35 | - name: Clone rails.git 36 | run: git clone --mirror https://github.com/rails/rails.git 37 | 38 | - name: Set up Ruby 39 | uses: ruby/setup-ruby@v1 40 | with: 41 | ruby-version: 3.3.4 42 | bundler-cache: true 43 | 44 | - name: Run tests 45 | run: bin/rails test 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .byebug_history 2 | /log 3 | /tmp 4 | /rails.git 5 | config/master.key 6 | public/assets 7 | public/cache 8 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.4 2 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | require 'capistrano/setup' 2 | require 'capistrano/deploy' 3 | 4 | require "capistrano/scm/git" 5 | install_plugin Capistrano::SCM::Git 6 | 7 | require 'capistrano/rvm' 8 | require 'capistrano/rails' 9 | require 'capistrano/puma' 10 | install_plugin Capistrano::Puma 11 | install_plugin Capistrano::Puma::Systemd 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # If we change the Ruby version, we need to keep in sync: 2 | # 3 | # * The constraint in the Gemfile. 4 | # * .ruby-version (used by CircleCI). 5 | # * Install the version by hand in the server (uses RVM). 6 | # 7 | FROM ruby:3.3.4-alpine3.20 8 | 9 | # LANG as recommended in the Encoding section of https://hub.docker.com/_/ruby/. 10 | ENV LANG C.UTF-8 11 | ENV PS1 '\w\$ ' 12 | ENV BUNDLE_JOBS 4 13 | 14 | # openssh-client is handy to check SSH access from within the container. 15 | # curl is used in deployments. 16 | # cmake is needed to compile rugged. 17 | # gcompat is needed by nokogiri. 18 | # git is used by the application itself. 19 | # tzdata is needed by the TZinfo gem. 20 | RUN apk --no-cache add --update \ 21 | build-base \ 22 | bash \ 23 | openssh-client \ 24 | cmake \ 25 | curl \ 26 | gcompat \ 27 | git \ 28 | vim \ 29 | postgresql-client \ 30 | postgresql-dev \ 31 | tzdata \ 32 | nodejs \ 33 | yarn 34 | 35 | RUN gem update --system 36 | RUN gem install bundler -v '2.3.17' 37 | 38 | WORKDIR /rails-contributors 39 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | ruby '3.3.4' 2 | 3 | source 'https://rubygems.org' 4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 5 | 6 | gem 'rails', '~> 7.2.1' 7 | 8 | gem 'sprockets-rails' 9 | 10 | gem 'rack', '< 3.0' 11 | gem 'pg' 12 | gem 'puma', '~> 5.6' 13 | gem 'rugged', '1.7.2' 14 | gem 'unf' 15 | gem 'turbolinks', '~> 5' 16 | gem 'actionpack-page_caching', '1.2.4' 17 | 18 | gem 'sass-rails', '~> 6.0' 19 | gem 'uglifier', '>= 1.3.0' 20 | gem 'coffee-rails', '~> 5.0.0' 21 | 22 | group :development do 23 | gem 'web-console', '>= 3.3.0' 24 | end 25 | 26 | group :test do 27 | gem 'rails-controller-testing' 28 | end 29 | 30 | group :deployment do 31 | gem 'capistrano', '~> 3.19', require: false 32 | gem 'capistrano-rails', '~> 1.6', require: false 33 | gem 'capistrano-rvm', '0.1.2', require: false 34 | gem 'capistrano3-puma', '~> 5.2', require: false 35 | gem 'rbnacl', '>= 3.2', '< 8.0' 36 | gem 'rbnacl-libsodium' 37 | gem 'bcrypt_pbkdf', '>= 1.0', '< 2.0' 38 | gem 'ed25519' 39 | end 40 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (7.2.1) 5 | actionpack (= 7.2.1) 6 | activesupport (= 7.2.1) 7 | nio4r (~> 2.0) 8 | websocket-driver (>= 0.6.1) 9 | zeitwerk (~> 2.6) 10 | actionmailbox (7.2.1) 11 | actionpack (= 7.2.1) 12 | activejob (= 7.2.1) 13 | activerecord (= 7.2.1) 14 | activestorage (= 7.2.1) 15 | activesupport (= 7.2.1) 16 | mail (>= 2.8.0) 17 | actionmailer (7.2.1) 18 | actionpack (= 7.2.1) 19 | actionview (= 7.2.1) 20 | activejob (= 7.2.1) 21 | activesupport (= 7.2.1) 22 | mail (>= 2.8.0) 23 | rails-dom-testing (~> 2.2) 24 | actionpack (7.2.1) 25 | actionview (= 7.2.1) 26 | activesupport (= 7.2.1) 27 | nokogiri (>= 1.8.5) 28 | racc 29 | rack (>= 2.2.4, < 3.2) 30 | rack-session (>= 1.0.1) 31 | rack-test (>= 0.6.3) 32 | rails-dom-testing (~> 2.2) 33 | rails-html-sanitizer (~> 1.6) 34 | useragent (~> 0.16) 35 | actionpack-page_caching (1.2.4) 36 | actionpack (>= 4.0.0) 37 | actiontext (7.2.1) 38 | actionpack (= 7.2.1) 39 | activerecord (= 7.2.1) 40 | activestorage (= 7.2.1) 41 | activesupport (= 7.2.1) 42 | globalid (>= 0.6.0) 43 | nokogiri (>= 1.8.5) 44 | actionview (7.2.1) 45 | activesupport (= 7.2.1) 46 | builder (~> 3.1) 47 | erubi (~> 1.11) 48 | rails-dom-testing (~> 2.2) 49 | rails-html-sanitizer (~> 1.6) 50 | activejob (7.2.1) 51 | activesupport (= 7.2.1) 52 | globalid (>= 0.3.6) 53 | activemodel (7.2.1) 54 | activesupport (= 7.2.1) 55 | activerecord (7.2.1) 56 | activemodel (= 7.2.1) 57 | activesupport (= 7.2.1) 58 | timeout (>= 0.4.0) 59 | activestorage (7.2.1) 60 | actionpack (= 7.2.1) 61 | activejob (= 7.2.1) 62 | activerecord (= 7.2.1) 63 | activesupport (= 7.2.1) 64 | marcel (~> 1.0) 65 | activesupport (7.2.1) 66 | base64 67 | bigdecimal 68 | concurrent-ruby (~> 1.0, >= 1.3.1) 69 | connection_pool (>= 2.2.5) 70 | drb 71 | i18n (>= 1.6, < 2) 72 | logger (>= 1.4.2) 73 | minitest (>= 5.1) 74 | securerandom (>= 0.3) 75 | tzinfo (~> 2.0, >= 2.0.5) 76 | airbrussh (1.5.2) 77 | sshkit (>= 1.6.1, != 1.7.0) 78 | base64 (0.2.0) 79 | bcrypt_pbkdf (1.1.1) 80 | bigdecimal (3.1.8) 81 | bindex (0.8.1) 82 | builder (3.3.0) 83 | capistrano (3.19.1) 84 | airbrussh (>= 1.0.0) 85 | i18n 86 | rake (>= 10.0.0) 87 | sshkit (>= 1.9.0) 88 | capistrano-bundler (2.1.0) 89 | capistrano (~> 3.1) 90 | capistrano-rails (1.6.3) 91 | capistrano (~> 3.1) 92 | capistrano-bundler (>= 1.1, < 3) 93 | capistrano-rvm (0.1.2) 94 | capistrano (~> 3.0) 95 | sshkit (~> 1.2) 96 | capistrano3-puma (5.2.0) 97 | capistrano (~> 3.7) 98 | capistrano-bundler 99 | puma (>= 4.0, < 6.0) 100 | coffee-rails (5.0.0) 101 | coffee-script (>= 2.2.0) 102 | railties (>= 5.2.0) 103 | coffee-script (2.4.1) 104 | coffee-script-source 105 | execjs 106 | coffee-script-source (1.12.2) 107 | concurrent-ruby (1.3.4) 108 | connection_pool (2.4.1) 109 | crass (1.0.6) 110 | date (3.3.4) 111 | drb (2.2.1) 112 | ed25519 (1.3.0) 113 | erubi (1.13.0) 114 | execjs (2.7.0) 115 | ffi (1.15.5) 116 | globalid (1.2.1) 117 | activesupport (>= 6.1) 118 | i18n (1.14.5) 119 | concurrent-ruby (~> 1.0) 120 | io-console (0.7.2) 121 | irb (1.14.0) 122 | rdoc (>= 4.0.0) 123 | reline (>= 0.4.2) 124 | logger (1.6.0) 125 | loofah (2.22.0) 126 | crass (~> 1.0.2) 127 | nokogiri (>= 1.12.0) 128 | mail (2.8.1) 129 | mini_mime (>= 0.1.1) 130 | net-imap 131 | net-pop 132 | net-smtp 133 | marcel (1.0.4) 134 | mini_mime (1.1.5) 135 | mini_portile2 (2.8.7) 136 | minitest (5.25.1) 137 | net-imap (0.4.14) 138 | date 139 | net-protocol 140 | net-pop (0.1.2) 141 | net-protocol 142 | net-protocol (0.2.2) 143 | timeout 144 | net-scp (4.0.0) 145 | net-ssh (>= 2.6.5, < 8.0.0) 146 | net-sftp (4.0.0) 147 | net-ssh (>= 5.0.0, < 8.0.0) 148 | net-smtp (0.5.0) 149 | net-protocol 150 | net-ssh (7.2.3) 151 | nio4r (2.7.3) 152 | nokogiri (1.16.7) 153 | mini_portile2 (~> 2.8.2) 154 | racc (~> 1.4) 155 | pg (1.5.7) 156 | psych (5.1.2) 157 | stringio 158 | puma (5.6.8) 159 | nio4r (~> 2.0) 160 | racc (1.8.1) 161 | rack (2.2.9) 162 | rack-session (1.0.2) 163 | rack (< 3) 164 | rack-test (2.1.0) 165 | rack (>= 1.3) 166 | rackup (1.0.0) 167 | rack (< 3) 168 | webrick 169 | rails (7.2.1) 170 | actioncable (= 7.2.1) 171 | actionmailbox (= 7.2.1) 172 | actionmailer (= 7.2.1) 173 | actionpack (= 7.2.1) 174 | actiontext (= 7.2.1) 175 | actionview (= 7.2.1) 176 | activejob (= 7.2.1) 177 | activemodel (= 7.2.1) 178 | activerecord (= 7.2.1) 179 | activestorage (= 7.2.1) 180 | activesupport (= 7.2.1) 181 | bundler (>= 1.15.0) 182 | railties (= 7.2.1) 183 | rails-controller-testing (1.0.5) 184 | actionpack (>= 5.0.1.rc1) 185 | actionview (>= 5.0.1.rc1) 186 | activesupport (>= 5.0.1.rc1) 187 | rails-dom-testing (2.2.0) 188 | activesupport (>= 5.0.0) 189 | minitest 190 | nokogiri (>= 1.6) 191 | rails-html-sanitizer (1.6.0) 192 | loofah (~> 2.21) 193 | nokogiri (~> 1.14) 194 | railties (7.2.1) 195 | actionpack (= 7.2.1) 196 | activesupport (= 7.2.1) 197 | irb (~> 1.13) 198 | rackup (>= 1.0.0) 199 | rake (>= 12.2) 200 | thor (~> 1.0, >= 1.2.2) 201 | zeitwerk (~> 2.6) 202 | rake (13.2.1) 203 | rbnacl (7.1.1) 204 | ffi 205 | rbnacl-libsodium (1.0.16) 206 | rbnacl (>= 3.0.1) 207 | rdoc (6.7.0) 208 | psych (>= 4.0.0) 209 | reline (0.5.9) 210 | io-console (~> 0.5) 211 | rugged (1.7.2) 212 | sass-rails (6.0.0) 213 | sassc-rails (~> 2.1, >= 2.1.1) 214 | sassc (2.4.0) 215 | ffi (~> 1.9) 216 | sassc-rails (2.1.2) 217 | railties (>= 4.0.0) 218 | sassc (>= 2.0) 219 | sprockets (> 3.0) 220 | sprockets-rails 221 | tilt 222 | securerandom (0.3.1) 223 | sprockets (4.2.1) 224 | concurrent-ruby (~> 1.0) 225 | rack (>= 2.2.4, < 4) 226 | sprockets-rails (3.5.2) 227 | actionpack (>= 6.1) 228 | activesupport (>= 6.1) 229 | sprockets (>= 3.0.0) 230 | sshkit (1.23.0) 231 | base64 232 | net-scp (>= 1.1.2) 233 | net-sftp (>= 2.1.2) 234 | net-ssh (>= 2.8.0) 235 | stringio (3.1.1) 236 | thor (1.3.1) 237 | tilt (2.0.11) 238 | timeout (0.4.1) 239 | turbolinks (5.2.1) 240 | turbolinks-source (~> 5.2) 241 | turbolinks-source (5.2.0) 242 | tzinfo (2.0.6) 243 | concurrent-ruby (~> 1.0) 244 | uglifier (4.2.0) 245 | execjs (>= 0.3.0, < 3) 246 | unf (0.2.0) 247 | useragent (0.16.10) 248 | web-console (4.2.1) 249 | actionview (>= 6.0.0) 250 | activemodel (>= 6.0.0) 251 | bindex (>= 0.4.0) 252 | railties (>= 6.0.0) 253 | webrick (1.8.1) 254 | websocket-driver (0.7.6) 255 | websocket-extensions (>= 0.1.0) 256 | websocket-extensions (0.1.5) 257 | zeitwerk (2.6.17) 258 | 259 | PLATFORMS 260 | ruby 261 | 262 | DEPENDENCIES 263 | actionpack-page_caching (= 1.2.4) 264 | bcrypt_pbkdf (>= 1.0, < 2.0) 265 | capistrano (~> 3.19) 266 | capistrano-rails (~> 1.6) 267 | capistrano-rvm (= 0.1.2) 268 | capistrano3-puma (~> 5.2) 269 | coffee-rails (~> 5.0.0) 270 | ed25519 271 | pg 272 | puma (~> 5.6) 273 | rack (< 3.0) 274 | rails (~> 7.2.1) 275 | rails-controller-testing 276 | rbnacl (>= 3.2, < 8.0) 277 | rbnacl-libsodium 278 | rugged (= 1.7.2) 279 | sass-rails (~> 6.0) 280 | sprockets-rails 281 | turbolinks (~> 5) 282 | uglifier (>= 1.3.0) 283 | unf 284 | web-console (>= 3.3.0) 285 | 286 | RUBY VERSION 287 | ruby 3.3.4p94 288 | 289 | BUNDLED WITH 290 | 2.5.17 291 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-ω Xavier Noria 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rails Contributors 2 | 3 | [![CircleCI](https://circleci.com/gh/rails/rails-contributors.svg?style=svg)](https://circleci.com/gh/rails/rails-contributors) 4 | 5 | This is the application behind https://contributors.rubyonrails.org. 6 | 7 | ## Documentation 8 | 9 | * [*doc/fix_credit.md*](doc/fix_credit.md) explains how to fix credits. 10 | * [*doc/docker.md*](doc/docker.md) covers development with Docker. 11 | * [*doc/design.md*](doc/design.md) documents the design of the application. 12 | * [*doc/production.md*](doc/production.md) notes for production servers. 13 | 14 | ## License 15 | 16 | Released under the MIT License, Copyright (c) 2012–ω Xavier Noria. 17 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link application.js 3 | //= link application.css 4 | -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails/rails-contributors/3b875dfb13a6c3b11674157474d83b6400da2194/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /app/assets/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails/rails-contributors/3b875dfb13a6c3b11674157474d83b6400da2194/app/assets/images/rails.png -------------------------------------------------------------------------------- /app/assets/images/rails_contributors_logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails/rails-contributors/3b875dfb13a6c3b11674157474d83b6400da2194/app/assets/images/rails_contributors_logo.gif -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require turbolinks 14 | //= require_tree . 15 | 16 | window.addEventListener("turbolinks:load", () => { 17 | if (window.matchMedia("(min-width: 768px)").matches) { 18 | document.querySelector("#sidebar details:not([open]) summary")?.click(); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require reset 14 | *= require screen 15 | *= require turbolinks 16 | */ 17 | -------------------------------------------------------------------------------- /app/assets/stylesheets/reset.css: -------------------------------------------------------------------------------- 1 | html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}:focus{outline:0}ins{text-decoration:none}del{text-decoration:line-through}table{border-collapse:collapse;border-spacing:0} 2 | -------------------------------------------------------------------------------- /app/assets/stylesheets/screen.scss: -------------------------------------------------------------------------------- 1 | $lead_color: #C52F24; 2 | $dark_lead_color: #AF2A20; 3 | $light_lead_color: #FFF0F5; 4 | 5 | body { 6 | background-color: #eaeaea; 7 | font-family: Helvetica, Arial, sans-serif; 8 | font-size: 87.5%; 9 | line-height: 1.5em; 10 | color: #333333; 11 | } 12 | 13 | /* 14 | --------------------------------- 15 | HTML TAGS 16 | --------------------------------- 17 | */ 18 | 19 | a { 20 | color: $lead_color; 21 | } 22 | 23 | h1 { 24 | font-size:2em; 25 | color:#000000; 26 | margin:10px 0; 27 | } 28 | 29 | h2 { 30 | font-size:1.5em; 31 | color:#000000; 32 | margin:10px 0; 33 | } 34 | 35 | p { 36 | margin-bottom:1.5em; 37 | } 38 | 39 | strong { 40 | font-weight:bold; 41 | } 42 | 43 | tt { 44 | font-family:monaco,"Bitstream Vera Sans Mono","Courier New",courier,monospace; 45 | } 46 | 47 | /* 48 | --------------------------------- 49 | SYNTAX FORMAT 50 | --------------------------------- 51 | */ 52 | 53 | pre,code { 54 | margin: 1.5em 0; 55 | overflow: auto; 56 | white-space: pre; 57 | } 58 | 59 | pre,code,tt { 60 | font: 1em 'andale mono', 'lucida console', monospace; 61 | line-height: 1.5; 62 | } 63 | 64 | code { 65 | background-color:#FFFAFA; 66 | border: none; 67 | display: block; 68 | font-family: monaco, "Bitstream Vera Sans Mono", "Courier New", courier, monospace; 69 | margin: 0.25em 0 1.5em 0; 70 | } 71 | 72 | .ruby .keywords { 73 | color : #EE3F3F; 74 | } 75 | 76 | .ruby .ivar { 77 | color : blue; 78 | } 79 | 80 | .ruby .comment { 81 | color: #708090; 82 | } 83 | 84 | .ruby .symbol { 85 | color: green; 86 | } 87 | 88 | 89 | /* 90 | --------------------------------- 91 | SPECIFIC TAGS 92 | --------------------------------- 93 | */ 94 | 95 | h1#title { 96 | float:left; 97 | } 98 | 99 | h1#title span.listing-total { 100 | color:$lead_color; 101 | font-size:0.5em; 102 | font-style:italic; 103 | font-weight:normal; 104 | } 105 | 106 | .clearfix { 107 | clear:both; 108 | } 109 | 110 | .sha1 { 111 | font-family:Consolas,Monaco,monospace; 112 | color:#980905; 113 | } 114 | 115 | /* 116 | --------------------------------- 117 | TOP HEADER LINKS 118 | --------------------------------- 119 | */ 120 | 121 | #topBar { 122 | background-color:#000000; 123 | color:#565656; 124 | padding:1em; 125 | } 126 | 127 | @media (min-width: 768px) { 128 | #topBar { 129 | padding: 1em 0; 130 | } 131 | 132 | #topBar ul { 133 | display: flex; 134 | } 135 | 136 | #topBar li:not(:last-child, :first-child) { 137 | margin-right:6px; 138 | padding-right:6px; 139 | border-right: 1px solid #565656; 140 | } 141 | } 142 | 143 | #topBar a.home { 144 | color:white; 145 | } 146 | 147 | #topBar a { 148 | color:#EE3F3F; 149 | } 150 | 151 | #topBar strong { 152 | color:#999999; 153 | margin-right:0.5em; 154 | } 155 | 156 | /* 157 | --------------------------------- 158 | LAYOUT GENERALS 159 | --------------------------------- 160 | */ 161 | 162 | #header { 163 | background-color:$lead_color; 164 | color:#FFFFFF; 165 | overflow:hidden; 166 | padding:1em 0; 167 | } 168 | 169 | #logo { 170 | img { 171 | width: 100%; 172 | max-width: 492px; 173 | } 174 | } 175 | 176 | #container { 177 | overflow:hidden; 178 | position:relative; 179 | max-width:850px; 180 | 181 | display: flex; 182 | flex-wrap: wrap-reverse; 183 | align-items: flex-end; 184 | overflow: visible; 185 | } 186 | 187 | #content { 188 | background-color:#FFFFFF; 189 | min-height:500px; 190 | max-width:630px; 191 | width: 100%; 192 | z-index: 1; 193 | box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2), 0 4px 6px -4px rgba(0, 0, 0, 0.2); 194 | } 195 | 196 | #content-in { 197 | border-top:2px solid #EBEBEB; 198 | padding:20px; 199 | } 200 | 201 | @media (min-width: 768px) { 202 | .wrapper { 203 | margin:0 8em; 204 | text-align:left; 205 | max-width:69em; 206 | width: 100%; 207 | } 208 | } 209 | 210 | /* 211 | --------------------------------- 212 | SIDEBAR NAVIGATION 213 | --------------------------------- 214 | */ 215 | #sidebar { 216 | background-color:$lead_color; 217 | color:#FFFFFF; 218 | margin:20px 0; 219 | width: 100%; 220 | padding: 0 20px; 221 | 222 | summary { 223 | cursor: pointer; 224 | margin: 5px 0; 225 | } 226 | } 227 | 228 | @media (min-width: 768px) { 229 | #sidebar { 230 | width:160px; 231 | border-bottom-right-radius:10px; 232 | border-top-right-radius:10px; 233 | 234 | details[open] summary { 235 | visibility: hidden; 236 | margin: 0; 237 | } 238 | } 239 | } 240 | 241 | #sidebar h4 { 242 | color:#F1938C; 243 | display:block; 244 | padding:0 0 0.5em 1em; 245 | } 246 | 247 | #sidebar hr { 248 | color:$dark_lead_color; 249 | background-color: $dark_lead_color; 250 | border:1px; 251 | height:1px; 252 | } 253 | 254 | #sidebar li { 255 | background-color: #E6E6E6; 256 | border-bottom:3px solid $dark_lead_color; 257 | display:block; 258 | font-size: 16px; 259 | height:30px; 260 | margin-bottom:10px; 261 | text-align: center; 262 | } 263 | 264 | #sidebar li a { 265 | color:#999999; 266 | display:block; 267 | text-decoration: none; 268 | padding:5px 10px 5px 15px; 269 | } 270 | 271 | #sidebar li a:hover { 272 | color:#666666; 273 | } 274 | 275 | #sidebar li.current a { 276 | background:#FFF ; 277 | color:#333; 278 | font-weight:bold; 279 | } 280 | 281 | 282 | #sidebar ul#common-links li { 283 | font-weight: normal; 284 | font-size:14px; 285 | margin:0; 286 | } 287 | 288 | /* 289 | --------------------------------- 290 | TABLES GENERALS 291 | --------------------------------- 292 | */ 293 | 294 | #table-wrap { 295 | clear:both; 296 | color:#555555; 297 | font-size: 1em; 298 | overflow-x: auto; 299 | } 300 | 301 | #table-wrap table { 302 | margin-top:20px; 303 | width:100%; 304 | } 305 | 306 | #table-wrap table tr.header { 307 | background-color:#333333; 308 | color:#EEEEEE; 309 | font-size: 1.2em; 310 | } 311 | 312 | #table-wrap table tr.header th { 313 | padding:5px; 314 | text-align: left; 315 | } 316 | 317 | #table-wrap table tr.even { 318 | background-color:#ffffff; 319 | } 320 | 321 | #table-wrap table tr.odd { 322 | background-color: $light_lead_color; 323 | } 324 | 325 | #table-wrap table td { 326 | padding:5px; 327 | } 328 | 329 | #table-wrap table td.contributor-rank { 330 | text-align: center; 331 | } 332 | 333 | #table-wrap table td.contributor-name { 334 | width: 75%; 335 | font-weight: bold; 336 | font-size: 1.1em; 337 | } 338 | 339 | #table-wrap table td.contributor-since { 340 | white-space: nowrap; 341 | text-align: left; 342 | } 343 | 344 | #table-wrap table td.no-commits { 345 | text-align: center; 346 | } 347 | 348 | td.commit-sha1, td.commit-date { 349 | white-space: nowrap; 350 | } 351 | 352 | /* 353 | --------------------------------- 354 | RELEASES LIST TABLE SPECIFICS 355 | --------------------------------- 356 | */ 357 | 358 | td.release-tag, td.release-date, td.release-name { 359 | text-align: left; 360 | white-space: nowrap; 361 | } 362 | 363 | td.release-name { 364 | width: 50%; 365 | font-weight: bold; 366 | font-size: 1.1em; 367 | } 368 | 369 | #table-wrap table td.release-counter { 370 | text-align: center; 371 | white-space: nowrap; 372 | } 373 | -------------------------------------------------------------------------------- /app/assets/stylesheets/turbolinks.scss: -------------------------------------------------------------------------------- 1 | .turbolinks-progress-bar { 2 | background-color: #c52f24; 3 | } 4 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | before_action :trace_user_agent 3 | 4 | private 5 | def set_contributor 6 | @contributor = Contributor.find_by_param(params[:contributor_id]) 7 | head :not_found unless @contributor 8 | end 9 | 10 | def set_release 11 | @release = Release.find_by_param(params[:release_id]) 12 | head :not_found unless @release 13 | end 14 | 15 | def set_time_constraints 16 | @time_window = params[:time_window].presence || 'all-time' 17 | 18 | if TimeConstraints.valid_time_window?(@time_window) 19 | @since, @upto = TimeConstraints.time_window_for(@time_window).values_at(:since, :upto) 20 | else 21 | head :not_found 22 | end 23 | end 24 | 25 | BOTS_REGEXP = %r{ 26 | Baidu | 27 | Gigabot | 28 | Openbot | 29 | Google | 30 | libwww-perl | 31 | lwp-trivial | 32 | msnbot | 33 | SiteUptime | 34 | Slurp | 35 | WordPress | 36 | ZIBB | 37 | ZyBorg | 38 | Yahoo | 39 | Lycos_Spider | 40 | Infoseek | 41 | ia_archiver | 42 | scoutjet | 43 | nutch | 44 | nuhk | 45 | dts\ agent | 46 | twiceler | 47 | ask\ jeeves | 48 | Webspider | 49 | Daumoa | 50 | MEGAUPLOAD | 51 | Yammybot | 52 | yacybot | 53 | GingerCrawler | 54 | Yandex | 55 | Gaisbot | 56 | TweetmemeBot | 57 | HttpClient | 58 | DotBot | 59 | 80legs | 60 | MLBot | 61 | wasitup | 62 | ichiro | 63 | discobot | 64 | bingbot | 65 | FAST | 66 | MauiBot | 67 | yrspider | 68 | SemrushBot 69 | }xi 70 | def trace_user_agent 71 | if request.user_agent =~ BOTS_REGEXP 72 | logger.info("(BOT) #{request.user_agent}") 73 | else 74 | logger.info("(BROWSER) #{request.user_agent}") 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /app/controllers/commits_controller.rb: -------------------------------------------------------------------------------- 1 | class CommitsController < ApplicationController 2 | caches_page :index, :in_time_window, :in_release, :in_edge 3 | 4 | before_action :set_target, only: %w(index in_release) 5 | before_action :set_contributor, only: %w(in_edge in_time_window) 6 | before_action :set_time_constraints, only: 'in_time_window' 7 | 8 | def index 9 | @commits = @target.commits.sorted 10 | end 11 | 12 | def in_time_window 13 | commits = @contributor.commits 14 | commits = commits.in_time_window(@since, @upto) if @since 15 | @commits = commits.sorted 16 | render 'index' 17 | end 18 | 19 | def in_release 20 | @commits = @contributor.commits.release(@release).sorted 21 | render 'index' 22 | end 23 | 24 | def in_edge 25 | @edge = true 26 | @commits = @contributor.commits.edge.sorted 27 | render 'index' 28 | end 29 | 30 | private 31 | 32 | def set_target 33 | if params[:contributor_id].present? 34 | set_contributor 35 | end 36 | 37 | if params[:release_id].present? 38 | set_release 39 | end 40 | 41 | @target = @contributor || @release 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/controllers/contributors_controller.rb: -------------------------------------------------------------------------------- 1 | class ContributorsController < ApplicationController 2 | caches_page :index, :in_time_window, :in_edge 3 | 4 | def index 5 | @contributors = if params[:release_id].present? 6 | set_release 7 | Contributor.all_with_ncommits_by_release(@release) 8 | else 9 | Contributor.all_with_ncommits 10 | end 11 | end 12 | 13 | def in_time_window 14 | set_time_constraints 15 | 16 | @contributors = if @since 17 | Contributor.all_with_ncommits_by_time_window(@since, @upto) 18 | else 19 | Contributor.all_with_ncommits 20 | end 21 | 22 | render 'index' 23 | end 24 | 25 | def in_edge 26 | @edge = true 27 | @contributors = Contributor.all_with_ncommits_in_edge 28 | 29 | render 'index' 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/controllers/faqs_controller.rb: -------------------------------------------------------------------------------- 1 | class FaqsController < ApplicationController 2 | caches_page :show 3 | 4 | def show 5 | end 6 | end -------------------------------------------------------------------------------- /app/controllers/releases_controller.rb: -------------------------------------------------------------------------------- 1 | class ReleasesController < ApplicationController 2 | caches_page :index 3 | 4 | def index 5 | @releases = Release.all_with_ncommits_and_ncontributors 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # Methods added to this helper will be available to all templates in the application. 2 | module ApplicationHelper 3 | def github_url_for_sha1(sha1) 4 | "https://github.com/rails/rails/commit/#{sha1}" 5 | end 6 | 7 | def github_url_for_tag(tag) 8 | "https://github.com/rails/rails/tree/#{tag}" 9 | end 10 | 11 | def link_to_commit_in_github(commit) 12 | link_to content_tag(:span, commit.short_sha1, class: 'sha1'), github_url_for_sha1(commit.sha1), class: 'commit' 13 | end 14 | 15 | def link_to_release_in_github(release) 16 | link_to content_tag(:span, release.tag, class: 'tag'), github_url_for_tag(release.tag), class: 'tag' 17 | end 18 | 19 | def link_to_contributor(contributor) 20 | link_to contributor.name, contributor_commits_path(contributor) 21 | end 22 | 23 | def link_to_release(release) 24 | link_to release.name, release_contributors_path(release) 25 | end 26 | 27 | def add_window_to(title, time_window) 28 | time_window ||= 'all-time' 29 | "#{title} - #{TimeConstraints.label_for(time_window)}".html_safe 30 | end 31 | 32 | def sidebar_tab(name, current, options={}, html_options={}) 33 | li_options = current ? {class: 'current'} : {} 34 | content_tag :li, li_options do 35 | link_to name, options, html_options 36 | end 37 | end 38 | 39 | def normalize_title(title) 40 | title = title.starts_with?('Rails Contributors') ? title : "Rails Contributors - #{title}" 41 | strip_tags(title) 42 | end 43 | 44 | def date(timestamp) 45 | timestamp.strftime('%d %b %Y') 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/commit.rb: -------------------------------------------------------------------------------- 1 | class Commit < ApplicationRecord 2 | has_many :contributions, dependent: :destroy 3 | has_many :contributors, through: :contributions 4 | belongs_to :release, optional: true 5 | 6 | scope :with_no_contributors, -> { 7 | select('commits.*'). # otherwise we get read-only records 8 | left_joins(:contributions). 9 | where(contributions: { commit_id: nil }) 10 | } 11 | 12 | scope :in_time_window, ->(since, upto) { 13 | if upto 14 | where(committer_date: since..upto) 15 | else 16 | where('commits.committer_date >= ?', since) 17 | end 18 | } 19 | 20 | scope :release, ->(release) { 21 | where(release_id: release.id) 22 | } 23 | 24 | scope :edge, -> { 25 | where(release_id: nil) 26 | } 27 | 28 | scope :sorted, -> { 29 | order(committer_date: :desc) 30 | } 31 | 32 | validates :sha1, presence: true, uniqueness: true 33 | 34 | nfc :author_name, :committer_name, :message, :diff 35 | 36 | # Constructor that initializes the object from a Rugged commit. 37 | def self.import!(rugged_commit) 38 | commit = new_from_rugged_commit(rugged_commit) 39 | commit.save! 40 | commit 41 | end 42 | 43 | def self.new_from_rugged_commit(rugged_commit) 44 | new( 45 | sha1: rugged_commit.oid, 46 | author_name: rugged_commit.author[:name].force_encoding('UTF-8'), 47 | author_email: rugged_commit.author[:email].force_encoding('UTF-8'), 48 | author_date: rugged_commit.author[:time], 49 | committer_name: rugged_commit.committer[:name].force_encoding('UTF-8'), 50 | committer_email: rugged_commit.committer[:email].force_encoding('UTF-8'), 51 | committer_date: rugged_commit.committer[:time], 52 | message: rugged_commit.message.force_encoding('UTF-8'), 53 | merge: rugged_commit.parents.size > 1 54 | ) 55 | end 56 | 57 | # Returns a shortened sha1 for this commit. Length is 7 by default. 58 | def short_sha1(length=7) 59 | sha1[0, length] 60 | end 61 | 62 | # Returns the URL of this commit in GitHub. 63 | def github_url 64 | "https://github.com/rails/rails/commit/#{sha1}" 65 | end 66 | 67 | def short_message 68 | @short_message ||= message ? message.split("\n").first : nil 69 | end 70 | 71 | # Returns the list of canonical contributor names of this commit. 72 | def extract_contributor_names(repo) 73 | names = extract_candidates(repo) 74 | names = canonicalize(names) 75 | names.uniq 76 | end 77 | 78 | protected 79 | 80 | def imported_from_svn? 81 | message.include?('git-svn-id: http://svn-commit.rubyonrails.org/rails') 82 | end 83 | 84 | # Both svn and git may have the name of the author in the message using the [...] 85 | # convention. If none is found we check the changelog entry for svn commits. 86 | # If that fails as well the contributor is the git author by definition. 87 | def extract_candidates(repo) 88 | names = hard_coded_authors 89 | if names.nil? 90 | names = extract_contributor_names_from_message 91 | if names.empty? 92 | names = extract_svn_contributor_names_diffing(repo) if imported_from_svn? 93 | if names.empty? 94 | sanitized = sanitize([author_name]) 95 | names = handle_false_positives(sanitized) 96 | if names.empty? 97 | # This is an edge case, some rare commits have an empty author name. 98 | names = sanitized 99 | end 100 | end 101 | end 102 | end 103 | 104 | # In modern times we are counting merge commits, because behind a merge 105 | # commit there is work by the core team member in the pull request. To 106 | # be fair, we are going to do what would be analogous for commits in 107 | # Subversion, where each commit has to be considered a merge commit. 108 | # 109 | # Note we do a uniq later in case normalization yields a clash. 110 | if imported_from_svn? && !names.include?(author_name) 111 | names << author_name 112 | end 113 | 114 | names.map(&:nfc) 115 | end 116 | 117 | def hard_coded_authors 118 | NamesManager.hard_coded_authors(self) 119 | end 120 | 121 | def sanitize(names) 122 | names.map {|name| NamesManager.sanitize(name)} 123 | end 124 | 125 | def handle_false_positives(names) 126 | names.map {|name| NamesManager.handle_false_positives(name)}.flatten.compact 127 | end 128 | 129 | def canonicalize(names) 130 | names.map {|name| NamesManager.canonical_name_for(name, author_email)}.flatten 131 | end 132 | 133 | def extract_contributor_names_from_message 134 | subject, body = message.split("\n\n", 2) 135 | 136 | # Check the end of subject first. 137 | contributor_names = extract_contributor_names_from_text(subject) 138 | return contributor_names if contributor_names.any? 139 | return [] if body.nil? 140 | 141 | # Some modern commits have an isolated body line with the credit. 142 | body.scan(/^\[[^\]]+\]$/) do |match| 143 | contributor_names = extract_contributor_names_from_text(match) 144 | return contributor_names if contributor_names.any? 145 | end 146 | 147 | # See https://help.github.com/en/articles/creating-a-commit-with-multiple-authors. 148 | co_authors = body.scan(/^Co-authored-by:(.*)$/i) 149 | return sanitize([author_name] + co_authors.flatten) if co_authors.any? 150 | 151 | # Check the end of the body as last option. 152 | if imported_from_svn? 153 | return extract_contributor_names_from_text(body.sub(/git-svn-id:.*/m, '')) 154 | end 155 | 156 | [] 157 | end 158 | 159 | # When Rails had a svn repo there was a convention for authors: the committer 160 | # put their name between brackets at the end of the commit or changelog message. 161 | # For example: 162 | # 163 | # Fix case-sensitive validates_uniqueness_of. Closes #11366 [miloops] 164 | # 165 | # Sometimes there's a "Closes #tiquet" after it, as in: 166 | # 167 | # Correct documentation for dom_id [jbarnette] Closes #10775 168 | # Models with no attributes should just have empty hash fixtures [Sam] (Closes #3563) 169 | # 170 | # Of course this is not robust, but it is the best we can get. 171 | def extract_contributor_names_from_text(text) 172 | names = text =~ /\[([^\]]+)\](?:\s+\(?Closes\s+#\d+\)?)?\s*\z/ ? [$1] : [] 173 | names = sanitize(names) 174 | handle_false_positives(names) 175 | end 176 | 177 | # Looks for contributor names in changelogs. There are +1600 commits with credit here. 178 | def extract_svn_contributor_names_diffing(repo) 179 | cache_diff(repo) unless diff 180 | return [] if only_modifies_changelogs? 181 | extract_changelog.split("\n").map do |line| 182 | extract_contributor_names_from_text(line) 183 | end.flatten 184 | rescue 185 | # There are 10 diffs that have invalid UTF-8 and we get an exception, just 186 | # ignore them. See f0753992ab8cc9bbbf9b047fdc56f8899df5635e for example. 187 | update_column(:diff, $!.message) 188 | [] 189 | end 190 | 191 | def cache_diff(repo) 192 | update(diff: repo.diff(sha1).force_encoding('UTF-8')) 193 | end 194 | 195 | # Extracts any changelog entry for this commit. This is done by diffing with 196 | # git show, and is an expensive operation. So, we do this only for those 197 | # commits where this is needed, and cache the result in the database. 198 | def extract_changelog 199 | changelog = '' 200 | in_changelog = false 201 | diff.each_line do |line| 202 | if line =~ /^diff --git/ 203 | in_changelog = false 204 | elsif line =~ /^\+\+\+.*changelog$/i 205 | in_changelog = true 206 | elsif in_changelog && line =~ /^\+\s*\*/ 207 | changelog << line 208 | end 209 | end 210 | changelog 211 | end 212 | 213 | # Some commits only touch CHANGELOGs, for example 214 | # 215 | # https://github.com/rails/rails/commit/f18356edb728522fcd3b6a00f11b29fd3bff0577 216 | # 217 | # Note we need this only for commits coming from Subversion, where 218 | # CHANGELOGs had no extension. 219 | def only_modifies_changelogs? 220 | diff.scan(/^diff --git(.*)$/) do |fname| 221 | return false unless fname.first.strip.ends_with?('CHANGELOG') 222 | end 223 | true 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /app/models/contribution.rb: -------------------------------------------------------------------------------- 1 | class Contribution < ApplicationRecord 2 | belongs_to :contributor 3 | belongs_to :commit 4 | end 5 | -------------------------------------------------------------------------------- /app/models/contributor.rb: -------------------------------------------------------------------------------- 1 | class Contributor < ApplicationRecord 2 | has_many :contributions, dependent: :destroy 3 | has_many :commits, through: :contributions 4 | 5 | validates :name, presence: true, uniqueness: true 6 | validates :url_id, presence: true, uniqueness: true 7 | 8 | nfc :name 9 | 10 | scope :with_no_commits, -> { 11 | left_joins(:contributions).where(contributions: { commit_id: nil }) 12 | } 13 | 14 | def self.all_with_ncommits 15 | _all_with_ncommits(:contributions) 16 | end 17 | 18 | def self.all_with_ncommits_by_time_window(since, upto) 19 | if upto 20 | _all_with_ncommits(:commits, ['commits.committer_date BETWEEN ? AND ?', since, upto]) 21 | else 22 | _all_with_ncommits(:commits, ['commits.committer_date >= ?', since]) 23 | end 24 | end 25 | 26 | def self.all_with_ncommits_by_release(release) 27 | _all_with_ncommits(:commits, 'commits.release_id' => release.id) 28 | end 29 | 30 | def self.all_with_ncommits_in_edge 31 | _all_with_ncommits(:commits, 'commits.release_id' => nil) 32 | end 33 | 34 | def self._all_with_ncommits(joins, where=nil) 35 | select('contributors.*, COUNT(*) AS ncommits'). 36 | joins(joins). 37 | where(where). 38 | group('contributors.id'). 39 | order('ncommits DESC, contributors.url_id ASC') 40 | end 41 | 42 | def self.set_first_contribution_timestamps(only_new) 43 | scope = only_new ? 'first_contribution_at IS NULL' : '1 = 1' 44 | 45 | connection.execute(<<-SQL) 46 | UPDATE contributors 47 | SET first_contribution_at = first_contributions.committer_date 48 | FROM ( 49 | SELECT contributor_id, MIN(commits.committer_date) AS committer_date 50 | FROM contributions 51 | INNER JOIN commits ON commits.id = commit_id 52 | GROUP BY contributor_id 53 | ) AS first_contributions 54 | WHERE id = first_contributions.contributor_id 55 | AND #{scope} 56 | SQL 57 | end 58 | 59 | # The contributors table may change if new name equivalences are added and IDs 60 | # in particular are expected to move. So, we just put the parameterized name 61 | # in URLs, which is unique anyway. 62 | def to_param 63 | url_id 64 | end 65 | 66 | def self.find_by_param(param) 67 | find_by_url_id(param) 68 | end 69 | 70 | def name=(name) 71 | super 72 | set_url_id 73 | end 74 | 75 | private 76 | 77 | def set_url_id 78 | self.url_id = name.parameterize 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /app/models/names_manager.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module NamesManager 4 | extend HardCodedAuthors 5 | extend FalsePositives 6 | extend CanonicalNames 7 | 8 | # Determines whether any heuristic has been updated since +ts+. 9 | def self.updated_since?(ts) 10 | [__FILE__, *Dir.glob("#{__dir__}/names_manager/*.rb")].any? do |filename| 11 | File.mtime(filename) > ts 12 | end 13 | end 14 | 15 | # Removes email addresses (anything between <...>), and strips whitespace. 16 | def self.sanitize(name) 17 | name.sub(/<[^>]+>/, '').strip 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/models/names_manager/false_positives.rb: -------------------------------------------------------------------------------- 1 | module NamesManager 2 | module FalsePositives 3 | CONNECTORS_REGEXP = %r{(?:[,/&+]|\band\b)} 4 | 5 | # Returns +nil+ if +name+ is known *not* to correspond to an author, the 6 | # author name(s) if special handling applies, like multiple authors separated 7 | # by a connector. If nothing applies, returns +name+ back. 8 | # 9 | # Note that this method is responsible for extracting names as they appear 10 | # in the original string. Canonicalization is done elsewhere. 11 | def handle_false_positives(name) 12 | case name 13 | when /\A\d+\b/ 14 | when /#\d+/ 15 | when /GH-\d+/ 16 | when /\A\s*\z/ 17 | when /RAILS_ENV/ 18 | when /^See rails ML/ 19 | when /RubyConf/ 20 | when 'update from Trac' 21 | when /\A['":]/ 22 | when 'RC1' 23 | when %r{https://} 24 | when '\\x00-\\x1f' 25 | when /\ACVE-[\d-]+\z/i 26 | when 'and' 27 | when 'options' 28 | when 'API DOCS' 29 | when 'hat-tip to anathematic' 30 | when 'props to Zarathu in #rubyonrails' 31 | when 'thanks Pratik!' 32 | when 'multiple=true' 33 | when /ci[\s_-]s?kip/i 34 | when 'ci skp' 35 | when 'ci ski' 36 | when /skip[ -]ci/i 37 | when 'key' 38 | when '.lock' 39 | when "{ :ca => :'es-ES' }" 40 | when 'fixes 5f5e6d924973003c105feb711cefdb726f312768' 41 | when '79990505e5080804b53d81fec059136afa2237d7' 42 | when 'direct_upload_xls_in_chrome' 43 | when "schoenm\100earthlink.net sandra.metz\100duke.edu" 44 | name.split 45 | when '=?utf-8?q?Adam=20Cig=C3=A1nek?=' 46 | 'Adam Cigánek'.nfc 47 | when '=?utf-8?q?Mislav=20Marohni=C4=87?=' 48 | 'Mislav Marohnić'.nfc 49 | when 'nik.wakelin Koz' 50 | ['nik.wakelin', 'Koz'] 51 | when "me\100jonnii.com rails\100jeffcole.net Marcel Molina Jr." 52 | ["me\100jonnii.com", "rails\100jeffcole.net", 'Marcel Molina Jr.'] 53 | when "jeremy\100planetargon.com Marcel Molina Jr." 54 | ["jeremy\100planetargon.com", 'Marcel Molina Jr.'] 55 | when "matt\100mattmargolis.net Marcel Molina Jr." 56 | ["matt\100mattmargolis.net", 'Marcel Molina Jr.'] 57 | when "doppler\100gmail.com phil.ross\100gmail.com" 58 | ["doppler\100gmail.com", "phil.ross\100gmail.com"] 59 | when 'After much pestering from Dave Thomas' 60 | 'Dave Thomas' 61 | when "jon\100blankpad.net)" 62 | ["jon\100blankpad.net"] 63 | when 'Jose and Yehuda' 64 | ['José Valim'.nfc, 'Yehuda Katz'] 65 | when /\A(?:Suggested|Aggregated)\s+by\s+(.*)/i 66 | $1 67 | when /\A(?:DHH\s*)?via\s+(.*)/i 68 | $1 69 | # These are present in some of Ben Sheldon commits. This pattern needs 70 | # ad-hoc handling because "/" is considered to be a name connector. 71 | when %r{\[he/him\]} 72 | $`.strip 73 | when /\b\w+\+\w+@/ 74 | # The plus sign is taken to be a connector below, this catches some known 75 | # addresses that use a plus sign in the username, see unit tests for examples. 76 | # We know there's no case where the plus sign acts as well as a connector in 77 | # the same string. 78 | name.split(/\s*,\s*/).map(&:strip) 79 | when CONNECTORS_REGEXP # There are lots of these, even with a combination of connectors. 80 | name.split(CONNECTORS_REGEXP).map(&:strip).reject do |part| 81 | part == 'others' || 82 | part == '?' 83 | end 84 | else 85 | # just return the candidate back 86 | name 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /app/models/names_manager/hard_coded_authors.rb: -------------------------------------------------------------------------------- 1 | module NamesManager 2 | module HardCodedAuthors 3 | # Sometimes the information to give credit for some commits is just not 4 | # present in the repository, or is present in a way the application does 5 | # not understand. This method has a hard-coded list of contributor names 6 | # for those commits. 7 | # 8 | # See the comment in each case for their justification. 9 | # 10 | # Please add commits here very exceptionally, only when there's no way 11 | # we can extract the candidates from the commit. False positives are 12 | # handled by NamesManager::FalsePositives, and regular aliases are managed 13 | # by NamesManager::CanonicalNames. 14 | def hard_coded_authors(commit) 15 | case commit.sha1 16 | when '1382f4de1f9b0e443e7884bd4da53c20f0754568' 17 | # This was a commit backported from 2.3 that missed Dana in the way. 18 | ['David Burger', 'Dana Jones'] 19 | when '882dd4e6054470ee56c46ab1432861952c81b633' 20 | # The following patch comes from this ticket https://rails.lighthouseapp.com/projects/8994/tickets/2856 21 | # but Yehuda told me credit for that commit was screwed up. 22 | ['David Calavera'] 23 | when 'f9a02b12d15bdbd3c2ed18b16b31b712a77027bc' 24 | # The attribution is done with parens in a way we do not extract. 25 | ['Juan Lupión'] 26 | when '4b4aa8f6e08ba2aa2ddce56f1d5b631a78eeef6c' 27 | # parens again 28 | ['Jesper Hvirring Henriksen'] 29 | when '945d999aadc9df41487e675fa0a863396cb54e31' 30 | # Author is "pivotal", but Adam Milligan told me Chris is the author (via github). 31 | ['Chris Heisterkamp'] 32 | when 'eb457ceee13779ade67e1bdebd2919d476148277' 33 | # Author is "pivotal", but Adam Milligan told me Joseph is the author (via github). 34 | ['Joseph Palermo'] 35 | when '6f2c4991efbbc5f567a6df36ca78de4f3ca24ee2', '9dbde4f5cbd0617ee6cce3e41d41335f9c9ce3fd' 36 | # Author is "pivotal", but Adam Milligan told me he himself is the author (via github). 37 | ['Adam Milligan'] 38 | when 'ddf2b4add33d5e54c5f5e7adacadbb50d3fa7b52' 39 | # These were Xavier's edits that for some reason were committed by Mikel. 40 | ['Xavier Noria'] 41 | when '3b1c69d2dd2b24a29e4443d7dc481589320a3f3e' 42 | # This was a patch from a bugmash that was applied by Xavier directly. 43 | # See https://rails.lighthouseapp.com/projects/8994/tickets/4284. 44 | ['Kieran Pilkington'] 45 | when 'a4041c5392457448cfdfef2e1b24007cfa46948b' 46 | # Vishnu forked using a different email address, and credit goes in the git commit 47 | # to Vishnu K. Sharma because of that, but the commit is his. 48 | ['Vishnu Atrai'] 49 | when 'ec44763f03b49e8c6e3bff71772ba32863a01306' 50 | # Mohammad El-Abid asked for this fix on Twitter (see https://twitter.com/The_Empty/status/73864303123496960). 51 | ['Mohammad Typaldos'] 52 | when '99dd117d6292b66a60567fd950c7ca2bda7e01f3' 53 | # Same here. 54 | ['Mohammad Typaldos'] 55 | when '3582bce6fdb30730b34b91a17b8bb33066eed7b8' 56 | # The attribution was wrong, and amended later in 33736bc18a9733d95953f6eaec924db10badc908. 57 | ['Juanjo Bazán', 'Tarmo Tänav', 'BigTitus'] 58 | when '7e8e91c439c1a877f867cd7ba634f7297ccef04b' 59 | # Credit was given in a non-conventional manner. 60 | ['Godfrey Chan', 'Philippe Creux'] 61 | when '798881ecd4510b9e1e5e10529fc2d81b9deb961e', '134c1156dd5713da41c62ff798fe3979723e64cc' 62 | # Idem. 63 | ['Godfrey Chan', 'Sergio Campamá'] 64 | when 'b23ffd0dac895aa3fd3afd8d9be36794941731b2' 65 | # See https://github.com/rails/rails/pull/13692#issuecomment-33617156. 66 | ['Łukasz Sarnacki', 'Matt Aimonetti'] 67 | when '1240338a02e5decab2a94b651fff78889d725e31' 68 | # Blake and Arthur paired on this YAML compatibility backport. 69 | ['Blake Mesdag', 'Arthur Neves'] 70 | when 'd318badc269358c53d9dfb4000e8c4c21a94b578' 71 | # Adrien worked on the fix, Grey on a regression spec, but only Grey's PR 72 | # was merged. See https://github.com/fxn/rails-contributors/pull/59. 73 | ['Grey Baker', 'Adrien Siami'] 74 | when '41adf8710e695f01a8100a87c496231d29e57cf2' 75 | # This commit uses non-conventional notation for multiple credits. 76 | ['Mislav Marohnić', 'Geoff Buesing'] 77 | when '6ddde027c4e51262e58f67438672d66a2b278f43' 78 | # Idem. 79 | ['Arthur Zapparoli', 'Michael Koziarski'] 80 | when '063c393bf0a2eb762770c97f925b7c2867361ad4' 81 | ['ivanvr'] 82 | when '872e22c60391dc45b7551cc0698d5530bb310611' 83 | # This patch comes from https://github.com/rails/web-console/pull/91, 84 | # originally authored by Daniel, but ported to upstream Rails by Genadi. 85 | ['Daniel Rikowski', 'Genadi Samokovarov'] 86 | when '92209356c310caabda8665d6369d3b1e4a1800d1' 87 | # Tsukuru Tanimichi originally worked on the PR, but Aaron Patterson and 88 | # Eileen Uchitelle changed the implementation on a separate branch. 89 | ['Eileen M. Uchitelle', 'Aaron Patterson', 'Tsukuru Tanimichi'] 90 | when '9668cc3bb03740b13477df0832332eec71563bc5' 91 | # Backport of the above commit. 92 | ['Eileen M. Uchitelle', 'Aaron Patterson', 'Tsukuru Tanimichi'] 93 | when '4f1472d4de37f1f77195c36390ce8bb65bb61e71' 94 | # The metadata for this commit completely lacks the name of the original 95 | # author. You'll see there only stuff related to ImgBot. 96 | ['John Bampton'] 97 | when '903dcefbaff8cf3a0e9db61048aebd9e753835ea' 98 | ['Josh Peek', 'David Heinemeier Hansson'] 99 | when 'fdbc55b9d55ae9d2b5b39be3ef4af5f60f6954a3', '6c6c3fa166975e5aebbe444605d736909e9eb75b' 100 | ['Yasuo Honda'] 101 | when '83c6ba18899a9f797d79726ca0078bdf618ec3d4' 102 | # This is a Git commit, but credit was still given in the CHANGELOG. 103 | ['S. Brent Faulkner'] 104 | else 105 | nil 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /app/models/release.rb: -------------------------------------------------------------------------------- 1 | class Release < ApplicationRecord 2 | include Comparable 3 | 4 | has_many :commits, :dependent => :nullify 5 | has_many :contributors, :through => :commits 6 | 7 | scope :sorted, -> { 8 | order('releases.major DESC, releases.minor DESC, releases.tiny DESC, releases.patch DESC') 9 | } 10 | 11 | validates :tag, presence: true, uniqueness: true 12 | 13 | # Computes the commits of every release, imports any of them missing, and 14 | # associates them. 15 | def self.process_commits(repo, new_releases) 16 | new_releases.sort.each do |release| 17 | release.process_commits(repo) 18 | end 19 | end 20 | 21 | # Imports a rugged object that was returned as a reference to a tag. 22 | # 23 | # Some tags are full objects (annotated tags), and other tags are kinda 24 | # symlinks to commits (lightweight tags). The method understands both. 25 | def self.import!(tag, rugged_object) 26 | case rugged_object 27 | when Rugged::Tag 28 | date = rugged_object.tagger[:time] 29 | target = rugged_object.target 30 | when Rugged::Commit 31 | date = rugged_object.author[:time] 32 | target = rugged_object 33 | end 34 | 35 | # Some tags, like v1.2.0, do not have their target imported. Ensure the 36 | # target of a tag is in the database. 37 | unless commit = Commit.find_by_sha1(target.oid) 38 | commit = Commit.import!(target) 39 | end 40 | Release.create!(tag: tag, date: date) 41 | end 42 | 43 | # Releases are ordered by their version, numerically. Note this is not 44 | # chronological, since it is customary that stable branches have maintenance 45 | # releases in parallel. 46 | def <=>(other) 47 | (major <=> other.major).nonzero? || 48 | (minor <=> other.minor).nonzero? || 49 | (tiny <=> other.tiny).nonzero? || 50 | (patch <=> other.patch).nonzero? || 51 | 0 52 | end 53 | 54 | # Writes the tag and sets major, minor, tiny, and patch from the new value. 55 | def tag=(str) 56 | write_attribute(:tag, str) 57 | split_version 58 | end 59 | 60 | # Sets the date, correcting it for releases whose Git date we know is wrong. 61 | def date=(date) 62 | date = actual_date_for_release(date) 63 | write_attribute(:date, date) 64 | end 65 | 66 | # The name of a release is the tag name except for the leading "v". 67 | def name 68 | tag[1..-1] 69 | end 70 | 71 | # Returns the name with dots converted to hyphens. 72 | def to_param 73 | name.tr('.', '-') 74 | end 75 | 76 | # Encapsulates what needs to be queried given a param. 77 | def self.find_by_param(param) 78 | find_by_tag('v' + param.tr('-', '.')) 79 | end 80 | 81 | # Computes the commits in this release, imports any of them missing, and 82 | # associates them. 83 | def process_commits(repo) 84 | released_sha1s = repo.rev_list(prev.try(:tag), tag) 85 | 86 | released_sha1s.each_slice(1024) do |sha1s| 87 | import_missing_commits(repo, sha1s) 88 | associate_commits(sha1s) 89 | end 90 | end 91 | 92 | # About a thousand of commits from the Subversion times are not reachable 93 | # from branch tips, which is what the main importer walks back. When a 94 | # release is detected we make sure the commits reported by rev-list are 95 | # present in the database. 96 | def import_missing_commits(repo, released_sha1s) 97 | existing_sha1s = Commit.where(sha1: released_sha1s).pluck(:sha1) 98 | (released_sha1s - existing_sha1s).each do |sha1| 99 | logger.debug("importing #{sha1} for #{tag}") 100 | Commit.import!(repo.repo.lookup(sha1)) 101 | end 102 | end 103 | 104 | # Associate the given SHA1s to this release, assuming they exist in the 105 | # commits table. 106 | def associate_commits(sha1s) 107 | # We force release_id to be NULL because rev-list in svn yields some 108 | # repeated commits in several releases. 109 | Commit.where(sha1: sha1s, release_id: nil).update_all(release_id: id) 110 | end 111 | 112 | # Computes the previous release as of today from the database. The previous 113 | # release may change with time. For example, the previous release of 3.2.0 114 | # may be 3.1.5 one day, and 3.1.6 if it gets later released. 115 | def prev 116 | Release.where(<<-SQL).sorted.first 117 | (major = #{major} AND minor = #{minor} AND tiny = #{tiny} AND patch < #{patch}) OR 118 | (major = #{major} AND minor = #{minor} AND tiny < #{tiny}) OR 119 | (major = #{major} AND minor < #{minor}) OR 120 | (major < #{major}) 121 | SQL 122 | end 123 | 124 | # Returns all releases, ordered by version, with virtual attributes ncommits 125 | # and ncontributors. 126 | def self.all_with_ncommits_and_ncontributors 127 | # Outer joins, because 2.0.1 according to git rev-list v2.0.0..v2.0.1 was a 128 | # release with no commits. 129 | select(<<-SELECT).joins(<<-JOINS).group('releases.id').sorted.to_a 130 | releases.*, 131 | COUNT(DISTINCT(commits.id)) AS ncommits, 132 | COUNT(DISTINCT(contributions.contributor_id)) AS ncontributors 133 | SELECT 134 | LEFT OUTER JOIN commits ON commits.release_id = releases.id 135 | LEFT OUTER JOIN contributions ON commits.id = contributions.commit_id 136 | JOINS 137 | end 138 | 139 | # Returns the URL of this commit in GitHub. 140 | def github_url 141 | "https://github.com/rails/rails/tree/#{tag}" 142 | end 143 | 144 | private 145 | 146 | def split_version 147 | numbers = name.split('.') 148 | 149 | self.major = numbers[0].to_i 150 | self.minor = numbers[1].to_i 151 | self.tiny = numbers[2].to_i 152 | self.patch = numbers[3].to_i 153 | end 154 | 155 | # Releases coming from Subversion were tagged in 2008 when the repo was 156 | # imported into git. I have scrapped 157 | # 158 | # http://rubyforge.org/frs/?group_id=307 159 | # 160 | # to generate this case statement. 161 | def actual_date_for_release(date) 162 | case tag 163 | when 'v0.5.0' 164 | DateTime.new(2004, 7, 24) 165 | when 'v0.5.5' 166 | DateTime.new(2004, 7, 28) 167 | when 'v0.5.6' 168 | DateTime.new(2004, 7, 29) 169 | when 'v0.5.7' 170 | DateTime.new(2004, 8, 1) 171 | when 'v0.6.0' 172 | DateTime.new(2004, 8, 6) 173 | when 'v0.6.5' 174 | DateTime.new(2004, 8, 20) 175 | when 'v0.7.0' 176 | DateTime.new(2004, 9, 5) 177 | when 'v0.8.0' 178 | DateTime.new(2004, 10, 25) 179 | when 'v0.8.5' 180 | DateTime.new(2004, 11, 17) 181 | when 'v0.9.0' 182 | DateTime.new(2004, 12, 16) 183 | when 'v0.9.1' 184 | DateTime.new(2004, 12, 17) 185 | when 'v0.9.2' 186 | DateTime.new(2004, 12, 23) 187 | when 'v0.9.3' 188 | DateTime.new(2005, 1, 4) 189 | when 'v0.9.4' 190 | DateTime.new(2005, 1, 17) 191 | when 'v0.9.4.1' 192 | DateTime.new(2005, 1, 18) 193 | when 'v0.9.5' 194 | DateTime.new(2005, 1, 25) 195 | when 'v0.10.0' 196 | DateTime.new(2005, 2, 24) 197 | when 'v0.10.1' 198 | DateTime.new(2005, 3, 7) 199 | when 'v0.11.0' 200 | DateTime.new(2005, 3, 22) 201 | when 'v0.11.1' 202 | DateTime.new(2005, 3, 27) 203 | when 'v0.12.0' 204 | DateTime.new(2005, 4, 19) 205 | when 'v0.12.1' 206 | DateTime.new(2005, 4, 19) 207 | when 'v0.13.0' 208 | DateTime.new(2005, 7, 6) 209 | when 'v0.13.1' 210 | DateTime.new(2005, 7, 11) 211 | when 'v0.14.1' 212 | DateTime.new(2005, 10, 19) 213 | when 'v0.14.2' 214 | DateTime.new(2005, 10, 26) 215 | when 'v0.14.3' 216 | DateTime.new(2005, 11, 7) 217 | when 'v0.14.4' 218 | DateTime.new(2005, 12, 8) 219 | when 'v1.0.0' 220 | DateTime.new(2005, 12, 13) 221 | when 'v1.1.0' 222 | DateTime.new(2006, 3, 28) 223 | when 'v1.1.1' 224 | DateTime.new(2006, 4, 6) 225 | when 'v1.1.2' 226 | DateTime.new(2006, 4, 9) 227 | when 'v1.1.3' 228 | DateTime.new(2006, 6, 27) 229 | when 'v1.1.4' 230 | DateTime.new(2006, 6, 29) 231 | when 'v1.1.5' 232 | DateTime.new(2006, 8, 8) 233 | when 'v1.1.6' 234 | DateTime.new(2006, 8, 10) 235 | when 'v1.2.0' 236 | DateTime.new(2007, 1, 18) 237 | when 'v1.2.1' 238 | DateTime.new(2007, 1, 18) 239 | when 'v1.2.2' 240 | DateTime.new(2007, 2, 6) 241 | when 'v1.2.3' 242 | DateTime.new(2007, 3, 13) 243 | when 'v1.2.4' 244 | DateTime.new(2007, 10, 4) 245 | when 'v1.2.5' 246 | DateTime.new(2007, 10, 12) 247 | when 'v1.2.6' 248 | DateTime.new(2007, 11, 24) 249 | when 'v2.0.0' 250 | DateTime.new(2007, 12, 6) 251 | when 'v2.0.1' 252 | DateTime.new(2007, 12, 7) 253 | when 'v2.0.2' 254 | DateTime.new(2007, 12, 16) 255 | when 'v2.0.4' 256 | DateTime.new(2008, 9, 4) 257 | when 'v2.1.0' 258 | DateTime.new(2008, 5, 31) 259 | else 260 | date 261 | end 262 | end 263 | end 264 | -------------------------------------------------------------------------------- /app/models/repo.rb: -------------------------------------------------------------------------------- 1 | require 'application_utils' 2 | 3 | class Repo 4 | attr_reader :logger, :path, :heads, :tags, :repo, :rebuild_all 5 | 6 | # Clone with --mirror: 7 | # 8 | # git clone --mirror git://github.com/rails/rails.git 9 | # 10 | PATH = "#{Rails.root}/rails.git" 11 | HEADS = %r{\Arefs/heads/(main|[\d\-]+(-stable)?)\z} 12 | TAGS = %r{\Arefs/tags/v[\d.]+\z} 13 | 14 | # This is the entry point to sync the database from cron jobs etc: 15 | # 16 | # bundle exec rails runner Repo.sync 17 | # 18 | # is the intended usage. This fetches new stuff, imports new commits if any, 19 | # imports new releases if any, assigns contributors and updates ranks. 20 | # 21 | # If the names manager has been updated since the previous execution special 22 | # code detects names that are gone and recomputes the contributors for their 23 | # commits. This can be forced by passing rebuild_all: true. 24 | def self.sync(path: PATH, heads: HEADS, tags: TAGS, rebuild_all: false) 25 | new(path: path, heads: heads, tags: tags, rebuild_all: rebuild_all).sync 26 | end 27 | 28 | def initialize(path: PATH, heads: HEADS, tags: TAGS, rebuild_all: false) 29 | @logger = Rails.logger 30 | @path = path 31 | @heads = heads 32 | @tags = tags 33 | @rebuild_all = rebuild_all || names_mapping_updated? 34 | @repo = Rugged::Repository.new(path) 35 | end 36 | 37 | # Executes a git command, optionally capturing its output. 38 | # 39 | # If the execution is not successful +StandardError+ is raised. 40 | def git(args, capture=false) 41 | cmd = "git #{args}" 42 | logger.info(cmd) 43 | Dir.chdir(path) do 44 | if capture 45 | out = `#{cmd}` 46 | return out if $?.success? 47 | raise "git error: #{$?}" 48 | else 49 | system(cmd) or raise "git error: #{$?}" 50 | end 51 | end 52 | end 53 | 54 | # Issues a git fetch. 55 | def fetch 56 | git 'fetch --quiet --prune' 57 | end 58 | 59 | # Returns the patch of the given commit. 60 | def diff(sha1) 61 | git "diff --no-color #{sha1}^!", true 62 | end 63 | 64 | # Returns the commits between +from+ and +to+. That is, that a reachable from 65 | # +to+, but not from +from+. 66 | # 67 | # We use this method to determine which commits belong to a release. 68 | def rev_list(from, to) 69 | arg = from ? "#{from}..#{to}" : to 70 | lines = git "rev-list #{arg}", true 71 | lines.split("\n") 72 | end 73 | 74 | # This method does the actual work behind Repo.sync. 75 | def sync 76 | ApplicationUtils.acquiring_lock_file('updating') do 77 | started_at = Time.current 78 | 79 | fetch 80 | 81 | ActiveRecord::Base.transaction do 82 | ncommits = sync_commits 83 | nreleases = sync_releases 84 | 85 | if ncommits > 0 || nreleases > 0 || rebuild_all 86 | sync_names 87 | sync_ranks 88 | sync_first_contribution_timestamps 89 | end 90 | 91 | RepoUpdate.create!( 92 | ncommits: ncommits, 93 | nreleases: nreleases, 94 | started_at: started_at, 95 | ended_at: Time.current, 96 | rebuild_all: rebuild_all 97 | ) 98 | 99 | ApplicationUtils.expire_cache if cache_needs_expiration?(ncommits, nreleases) 100 | end 101 | end 102 | end 103 | 104 | protected 105 | 106 | def refs(regexp) 107 | repo.refs.select do |ref| 108 | ref.name =~ regexp 109 | end 110 | end 111 | 112 | # Imports those commits in the Git repo that do not yet exist in the database 113 | # by walking the main and stable branches backwards starting at the tips 114 | # and following parents. 115 | def sync_commits 116 | ncommits = 0 117 | 118 | ActiveRecord::Base.logger.silence do 119 | refs(heads).each do |ref| 120 | to_visit = [repo.lookup(ref.target.oid)] 121 | while commit = to_visit.shift 122 | unless Commit.exists?(sha1: commit.oid) 123 | ncommits += 1 124 | Commit.import!(commit) 125 | to_visit.concat(commit.parents) 126 | end 127 | end 128 | end 129 | end 130 | 131 | ncommits 132 | end 133 | 134 | # Imports new releases, if any, determines which commits belong to them, and 135 | # associates them. By definition, a release corresponds to a stable tag, one 136 | # that matches \Av[\d.]+\z. 137 | def sync_releases 138 | new_releases = [] 139 | 140 | refs(tags).each do |ref| 141 | tag = ref.name[%r{[^/]+\z}] 142 | unless Release.exists?(tag: tag) 143 | target = ref.target 144 | commit = target.is_a?(Rugged::Commit) ? target : target.target 145 | new_releases << Release.import!(tag, commit) 146 | end 147 | end 148 | 149 | Release.process_commits(self, new_releases) 150 | 151 | new_releases.size 152 | end 153 | 154 | # Computes the name of the contributors and adjusts associations and the 155 | # names table. If some names are gone due to new mappings collapsing two 156 | # names into one, for example, the credit for commits of gone names is 157 | # revised, resulting in the canonical name being associated. 158 | def sync_names 159 | Contribution.delete_all if rebuild_all 160 | assign_contributors 161 | Contributor.with_no_commits.delete_all if rebuild_all 162 | end 163 | 164 | # Once all tables have been updated we compute the rank of each contributor. 165 | def sync_ranks 166 | i = 0 167 | prev_ncommits = nil 168 | new_rank = 0 169 | ranks_to_update = Hash.new {|h, k| h[k] = []} 170 | 171 | # Compute new ranks, and store those which need to be updated. 172 | Contributor.all_with_ncommits.each do |contributor| 173 | i += 1 174 | 175 | if contributor.ncommits != prev_ncommits 176 | new_rank = i 177 | prev_ncommits = contributor.ncommits 178 | end 179 | 180 | if contributor.rank != new_rank 181 | ranks_to_update[new_rank] << contributor.id 182 | end 183 | end 184 | 185 | # Update new ranks, if any. 186 | ranks_to_update.each do |rank, contributor_ids| 187 | Contributor.where(id: contributor_ids).update_all("rank = #{rank}") 188 | end 189 | end 190 | 191 | def sync_first_contribution_timestamps 192 | Contributor.set_first_contribution_timestamps(!rebuild_all) 193 | end 194 | 195 | # Determines whether the names mapping has been updated. This is useful because 196 | # if the names mapping is up to date we only need to assign contributors for 197 | # new commits. 198 | def names_mapping_updated? 199 | @nmu ||= begin 200 | lastru = RepoUpdate.last 201 | # Use started_at in case a revised names manager is deployed while an update 202 | # is running. 203 | lastru ? NamesManager.updated_since?(lastru.started_at) : true 204 | end 205 | end 206 | 207 | # Goes over all or new commits in the database and builds a hash that maps 208 | # each sha1 to the array of the canonical names of their contributors. 209 | # 210 | # This computation ignores the current contributions table altogether, it 211 | # only takes into account the current mapping rules for name resolution. 212 | def compute_contributor_names_per_commit 213 | Hash.new {|h, sha1| h[sha1] = []}.tap do |contributor_names_per_commit| 214 | Commit.with_no_contributors.find_each do |commit| 215 | commit.extract_contributor_names(self).each do |contributor_name| 216 | contributor_names_per_commit[commit.sha1] << contributor_name 217 | end 218 | end 219 | end 220 | end 221 | 222 | # Iterates over all commits with no contributors and assigns to them the ones 223 | # in the previously computed contributor_names_per_commit. 224 | def assign_contributors 225 | contributor_names_per_commit = compute_contributor_names_per_commit 226 | contributors = Hash.new {|h, name| h[name] = Contributor.find_or_create_by(name: name)} 227 | 228 | data = [] 229 | Commit.with_no_contributors.find_each do |commit| 230 | contributor_names_per_commit[commit.sha1].each do |contributor_name| 231 | # FIXME: This check is needed because creation in a few exceptional 232 | # cases fails due to url_id collisions (Geoffrey ROGUELON, Adam), or 233 | # due blank url_ids (प्रथमेश). 234 | if contributors[contributor_name].id 235 | data << "#{contributors[contributor_name].id},#{commit.id}\n" 236 | end 237 | end 238 | end 239 | 240 | conn = ActiveRecord::Base.connection.raw_connection 241 | conn.copy_data('COPY contributions (contributor_id, commit_id) FROM STDIN CSV') do 242 | data.each do |row| 243 | conn.put_copy_data(row) 244 | end 245 | end 246 | end 247 | 248 | # Do we need to expire the cached pages? 249 | def cache_needs_expiration?(ncommits, nreleases) 250 | ncommits > 0 || nreleases > 0 || rebuild_all 251 | end 252 | end 253 | -------------------------------------------------------------------------------- /app/models/repo_update.rb: -------------------------------------------------------------------------------- 1 | class RepoUpdate < ApplicationRecord 2 | end 3 | -------------------------------------------------------------------------------- /app/models/time_constraints.rb: -------------------------------------------------------------------------------- 1 | module TimeConstraints 2 | ALL = { 3 | 'all-time' => 'All time', 4 | 'today' => 'Today', 5 | 'this-week' => 'This week', 6 | 'this-month' => 'This month', 7 | 'this-year' => 'This year', 8 | } 9 | 10 | DATE_RANGE = /\A(\d+)(?:-(\d+))?\z/ 11 | 12 | def self.all 13 | ALL 14 | end 15 | 16 | def self.label_for(key) 17 | ALL[key] || 'Date Range' 18 | end 19 | 20 | def self.valid_time_window?(key) 21 | ALL[key] || key =~ DATE_RANGE 22 | end 23 | 24 | # These date objects have to be computed per call, they can't be associated 25 | # to the keys. 26 | def self.time_window_for(key) 27 | case key 28 | when 'all-time' 29 | {} 30 | when 'today' 31 | { since: Date.current.beginning_of_day } 32 | when 'this-week' 33 | { since: Date.current.beginning_of_week } 34 | when 'this-month' 35 | { since: Date.current.beginning_of_month } 36 | when 'this-year' 37 | { since: Date.current.beginning_of_year } 38 | when DATE_RANGE 39 | { since: parse_time($1, false), upto: parse_time($2, true) } 40 | else 41 | raise ArgumentError, "Unknown time window key #{key}" 42 | end 43 | end 44 | 45 | def self.parse_time(str, end_of_day_if_date) 46 | if str 47 | time = Time.zone.parse(str) 48 | str.length == 8 && end_of_day_if_date ? time.end_of_day : time 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/views/commits/_commit.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= link_to_commit_in_github commit %> 3 | <%= date commit.committer_date %> 4 | <%= truncate(commit.short_message, length: 59) %> 5 | 6 | -------------------------------------------------------------------------------- /app/views/commits/index.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | if @contributor && @release 3 | @title = %(##{@contributor.rank} #{h(@contributor.name)} - Rails #{@release.name}).html_safe 4 | @description = "Commits contributed by #{@contributor.name} in the #{@release.name} release of Ruby on Rails" 5 | @keywords = @contributor.name 6 | elsif @contributor && @edge 7 | @title = %(##{@contributor.rank} #{h(@contributor.name)} - Edge).html_safe 8 | @description = "Unreleased commits contributed by #{@contributor.name} to Ruby on Rails" 9 | @keywords = @contributor.name 10 | elsif @contributor 11 | @title = add_window_to(%(##{@contributor.rank} #{h(@contributor.name)}), @time_window) 12 | @description = "Commits contributed by #{@contributor.name} to Ruby on Rails" 13 | @keywords = @contributor.name 14 | else 15 | @title = %(Rails #{@release.name}) 16 | @description = "Commits in the #{@release.name} release of Ruby on Rails" 17 | @keywords = @release.name 18 | end 19 | %> 20 | 21 | 22 |
23 |

24 | <%= @title %>
25 | Showing <%= pluralize @commits.size, 'commit' %> 26 |

27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | <%= render @commits %> 36 |
HashDateMessage
37 |
38 |
39 | 40 | <%= render 'shared/window_sidebar' %> 41 | -------------------------------------------------------------------------------- /app/views/contributors/_contributor.html.erb: -------------------------------------------------------------------------------- 1 | 2 | #<%= contributor.rank %> 3 | <%= link_to_contributor contributor %> 4 | <%= date contributor.first_contribution_at %> 5 | 6 | <% path = if @time_window 7 | contributor_commits_in_time_window_path(contributor, @time_window) 8 | elsif @release 9 | contributor_commits_in_release_path(contributor, @release) 10 | elsif @edge 11 | contributor_commits_in_edge_path(contributor) 12 | else 13 | contributor_commits_path(contributor) 14 | end 15 | %> 16 | <%= link_to contributor.ncommits, path %> 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/views/contributors/index.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | @title = @release ? "Rails Contributors - #{@release.name}" : @edge ? 'Rails Contributors - Edge' : add_window_to("Rails Contributors", @time_window) 3 | @description = 'Ruby on Rails contributors' 4 | %> 5 | 6 |

7 | <%= @title %>
8 | Showing <%= pluralize @contributors.length, 'person' %> 9 |

10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <%= render @contributors %> 20 |
NameSinceCommits
21 |
22 | 23 | <%= render 'shared/window_sidebar' %> 24 | -------------------------------------------------------------------------------- /app/views/faqs/show.html.erb: -------------------------------------------------------------------------------- 1 | <% @title = 'Frequently Asked Questions' %> 2 | 3 | <%# TODO: Move style to CSS. %> 4 |

<%= @title %>

5 | 6 | <%# TODO: Move style to CSS. %> 7 | 16 | 17 |

What is Rails Contributors?

18 |

Rails Contributors gives credit to all people who have contributed to the Rails code base.

19 | 20 |

Why, isn't this information already provided by Git?

21 |

Not really.

22 |

Rails Contributors is tailor-made for Ruby on Rails and, albeit not perfect, tries really hard to be as accurate as possible. You need to go far beyond Git metadata to accomplish what this application does.

23 | 24 |

Subversion

25 |

Ruby on Rails used Subversion for approximately the first four years. Since Subversion does not have the concept of a commit author, the author's name is not available.

26 | 27 |

Some conventions emerged though, like putting the author name in square brackets <%= link_to 'in the commit message', github_url_for_sha1('03d37a2d68c1940f65d5a65a51ae747955a5b075') %>, or <%= link_to 'in CHANGELOG entries', github_url_for_sha1('c95365a00eb086f61cb5e73e9e6d5dae54325b60') %>. Rails Contributors processes those credits.

28 | 29 |

Canonical names

30 |

Over the years people get mentioned by their name, or nickname, there are abbreviations, typos, emails.... For example, Marcel Molina Jr. has commits credited to "Marcel Molina" (no "Jr."), "Marcel", "Marcel Molina Jr" (lacks a dot), "marcel", "noradio" (nickname), and "Marcel Mollina Jr." (typo, double "l"). The application knows all of them refer to the same person, and it is able to consolidate all of Marcel's commits into one single canonical name.

31 |

Rails Contributors knows about +1100 name variations.

32 | 33 |

Multiple authors

34 |

Git does not support multiple authors per commit. Rails developed custom conventions for that, and over time GitHub did too. The application understands them all.

35 | 36 |

Some examples: <%= link_to 'ef2b139', github_url_for_sha1('9e9793b440c044b765f2d1f702feeb92aef2b139') %> (credit via [...] in the commit message), <%= link_to '65b4aae', github_url_for_sha1('ec20838381dc26b68142c84c0e03f523765b4aae') %> (credit given in the CHANGELOG), <%= link_to '63efb49', github_url_for_sha1("99e52ae7b1c621069f2971d2f352822f263efb49") %> (credit via Co-authored-by annotations).

37 | 38 |

Plural nicknames

39 |

A couple of nicknames refer to several people. For example, a commit by "Carlhuda" (like <%= link_to 'this one', github_url_for_sha1('c102db9367690af992786d2a62bbf8caeec88742') %>) has to be credited to Yehuda Katz and Carl Lerche.

40 | 41 |

Manual credit

42 |

Finally, occasionally the information to give credit is just missing. For example, Dana Jones should have given credit for <%= link_to 'this commit', github_url_for_sha1('1382f4de1f9b0e443e7884bd4da53c20f0754568') %>, but the attribution was not included. Rails Contributors has support also for these exceptional hard-coded fixes, and Dana has gotten <%= link_to 'her due credit', 'http://contributors.rubyonrails.org/contributors/dana-jones/commits' %>.

43 | 44 |

My contributions are split, can they be merged?

45 |

Yes, please have a look at the maps in <%= link_to 'canonical_names.rb'.html_safe, 'https://github.com/rails/rails-contributors/blob/master/app/models/names_manager/canonical_names.rb' %> and feel free to submit a pull request. Please take into account only maps for nicknames that exist in the repository, typos, etc., are accepted. That is not a generic database of aliases.

46 | 47 |

Can several people get credit for one commit?

48 |

Yes, you can place their name at the end of the first line of the commit message in square brackets:

49 |
50 |     awesome API refinement [DHH & Jeremy Kemper]
51 | 
52 |

or in the rest of the message, in a line by itself:

53 |
54 |     first line of commit message
55 | 
56 |     message body line 1
57 |     message body line 2
58 | 
59 |     [Author 1, Author 2, Author 3]
60 | 
61 |

A few list separators are supported, but you can just stick with "&" or "," for example.

62 |

or you can use Co-authored-by with their name and email in individual lines:

63 |
64 |     first line of commit message
65 | 
66 |     message body line 1
67 |     message body line 2
68 | 
69 |     Co-authored-by: Author 1 <author1@example.com>
70 |     Co-authored-by: Author 2 <author2@example.com>
71 | 
72 | 73 |

The home page has a huge table, no pagination?

74 |

That's by design.

75 |

If you contributed anything to Rails, even a comma in a comment, your name deserves to be in the home page.

76 | 77 |

Why does it count commits?

78 |

Generally speaking, commits, LOCs, etc. are not a meaningful metric of the amount or quality of people's contributions. We all know that, and that's why this website is not a ranking.

79 |

But commits are the natural unit of data we have as input, what can be grouped and listed. That's about it.

80 | 81 |

Is this gamification?

82 |

No.

83 |

While Rails Contributors may encourage some people to partake in the project, there is no gamification in the motivation behind this application.

84 |

The motivation for Rails Contributors is to give well-deserved public recognition to all people who have contributed to the code base over the history of the project, and to say a big "Thank You!"

85 | 86 | <%= render 'shared/window_sidebar' %> 87 | -------------------------------------------------------------------------------- /app/views/layouts/_top_bar_content.html.erb: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | <%= normalize_title(@title) %> 13 | <%= stylesheet_link_tag 'application' %> 14 | <%= javascript_include_tag 'application' %> 15 | <%= favicon_link_tag %> 16 | 17 | 18 | 23 | 30 |
31 |
32 |
33 |
34 |
35 | <%= yield %> 36 |
37 |
38 | 44 |
45 |
46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /app/views/releases/_release.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= link_to_release_in_github release %> 3 | <%= date release.date %> 4 | <%= link_to_release release %> 5 | <%= link_to release.ncontributors, release_contributors_path(release) %> 6 | <%= link_to release.ncommits, release_commits_path(release) %> 7 | 8 | -------------------------------------------------------------------------------- /app/views/releases/index.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | @title = 'Rails Releases' 3 | @description = 'Listing of Ruby on Rails releases' 4 | @keywords = 'releases' 5 | %> 6 | 7 | 8 |
9 |

10 | <%= @title %>
11 | Showing <%= pluralize @releases.size, 'releases' %> 12 |

13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | <%= render @releases %> 24 |
TagDateReleaseContributorsCommits
25 |
26 |
27 | 28 | <%= render 'shared/window_sidebar' %> 29 | -------------------------------------------------------------------------------- /app/views/shared/_window_sidebar.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :sidebar do %> 2 | 17 | <% end %> 18 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 || ">= 0.a" 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_version 64 | @bundler_version ||= begin 65 | env_var_version || cli_arg_version || 66 | lockfile_version || "#{Gem::Requirement.default}.a" 67 | end 68 | end 69 | 70 | def load_bundler! 71 | ENV["BUNDLE_GEMFILE"] ||= gemfile 72 | 73 | # must dup string for RG < 1.8 compatibility 74 | activate_bundler(bundler_version.dup) 75 | end 76 | 77 | def activate_bundler(bundler_version) 78 | if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0") 79 | bundler_version = "< 2" 80 | end 81 | gem_error = activation_error_handling do 82 | gem "bundler", bundler_version 83 | end 84 | return if gem_error.nil? 85 | require_error = activation_error_handling do 86 | require "bundler/version" 87 | end 88 | return if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 89 | warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`" 90 | exit 42 91 | end 92 | 93 | def activation_error_handling 94 | yield 95 | nil 96 | rescue StandardError, LoadError => e 97 | e 98 | end 99 | end 100 | 101 | m.load_bundler! 102 | 103 | if m.invoked_as_script? 104 | load Gem.bin_path("bundler", "bundle") 105 | end 106 | -------------------------------------------------------------------------------- /bin/cap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'cap' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("capistrano", "cap") 30 | -------------------------------------------------------------------------------- /bin/new-aliases: -------------------------------------------------------------------------------- 1 | #!bin/rails runner 2 | 3 | require 'set' 4 | 5 | known = Set.new 6 | 7 | # These handlers match a GitHub account, but we know they are not them. 8 | known += %w( 9 | adam 10 | alex 11 | Andreas 12 | Caleb 13 | dan 14 | jamesh 15 | jonathan 16 | Kent 17 | mat 18 | Scott 19 | seth 20 | steve 21 | trevor 22 | ) 23 | 24 | # These handlers match a GitHub account, asked for confirmation via internal 25 | # messages but got no response so far. 26 | known += %w( 27 | blackanger 28 | ian 29 | jerome 30 | mark 31 | Paul 32 | robby 33 | shane 34 | tom 35 | xavier 36 | ) 37 | 38 | # I've sent an email to these addresses and there's no response so far. 39 | known += %W( 40 | agkr\100pobox.com 41 | alec+rails\100veryclever.net 42 | alex.r.moon\100gmail.com 43 | david.a.williams\100gmail.com 44 | dwlt\100dwlt.net 45 | edward.frederick\100revolution.com 46 | eli.gordon\100gmail.com 47 | eugenol\100gmail.com 48 | fhanshaw\100vesaria.com 49 | gaetanot\100comcast.net 50 | gnuman1\100gmail.com 51 | imbcmdth\100hotmail.com 52 | info\100loobmedia.com 53 | jan\100ulbrich-boerwang.de 54 | jhahn\100niveon.com 55 | jonrailsdev\100shumi.org 56 | junk\100miriamtech.com 57 | justin\100textdrive.com 58 | machomagna\100gmail.com 59 | me\100jonnii.com 60 | nick+rails\100ag.arizona.edu 61 | rails.20.clarry\100spamgourmet.com 62 | rails-bug\100owl.me.uk 63 | s.brink\100web.de 64 | schultzr\100gmail.com 65 | seattle\100rootimage.msu.edu 66 | yanowitz-rubyonrails\100quantumfoam.org 67 | ) 68 | 69 | # I've sent an email to these addresses, and got some sort of error back. 70 | known += %W( 71 | altano\100bigfoot.com 72 | asnem\100student.ethz.ch 73 | damn_pepe\100gmail.com 74 | dev.rubyonrails\100maxdunn.com 75 | kdole\100tamu.edu 76 | kevin-temp\100writesoon.com 77 | mklame\100atxeu.com 78 | nbpwie102\100sneakemail.com 79 | nkriege\100hotmail.com 80 | nwoods\100mail.com 81 | pfc.pille\100gmx.net 82 | rails\100cogentdude.com 83 | rcolli2\100tampabay.rr.com 84 | rubyonrails\100atyp.de 85 | solo\100gatelys.com 86 | starr\100starrnhorne.com 87 | zachary\100panandscan.com 88 | ) 89 | 90 | # These people declined having their full name in the listing. 91 | known += %W( 92 | okkez 93 | maiha 94 | burningTyger 95 | lagroue\100free.fr 96 | ) 97 | 98 | # We know "todd" is not Todd Hanson, but don't know who he is. 99 | known << 'todd' 100 | 101 | Contributor.where("position(' ' in name) = 0").pluck(:name).each do |name| 102 | puts name unless known.member?(name) 103 | end 104 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | APP_ROOT = File.expand_path("..", __dir__) 5 | APP_NAME = "rails-contributors" 6 | 7 | def system!(*args) 8 | system(*args, exception: true) 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?("config/database.yml") 22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! "bin/rails db:prepare" 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! "bin/rails log:clear tmp:clear" 30 | 31 | puts "\n== Restarting application server ==" 32 | system! "bin/rails restart" 33 | 34 | # puts "\n== Configuring puma-dev ==" 35 | # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}" 36 | # system "curl -Is https://#{APP_NAME}.test/up | head -n 1" 37 | end 38 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | yarn = ENV["PATH"].split(File::PATH_SEPARATOR). 5 | select { |dir| File.expand_path(dir) != __dir__ }. 6 | product(["yarn", "yarn.cmd", "yarn.ps1"]). 7 | map { |dir, file| File.expand_path(file, dir) }. 8 | find { |file| File.executable?(file) } 9 | 10 | if yarn 11 | exec yarn, *ARGV 12 | else 13 | $stderr.puts "Yarn executable was not detected in the system." 14 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 15 | exit 1 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | # require "active_job/railtie" 7 | require "active_record/railtie" 8 | # require "active_storage/engine" 9 | require "action_controller/railtie" 10 | # require "action_mailer/railtie" 11 | # require "action_mailbox/engine" 12 | # require "action_text/engine" 13 | require "action_view/railtie" 14 | # require "action_cable/engine" 15 | require "rails/test_unit/railtie" 16 | 17 | # Require the gems listed in Gemfile, including any gems 18 | # you've limited to :test, :development, or :production. 19 | Bundler.require(*Rails.groups) 20 | 21 | require_relative "../lib/bot_killer" 22 | 23 | module RailsContributors 24 | class Application < Rails::Application 25 | # Initialize configuration defaults for originally generated Rails version. 26 | config.load_defaults 7.2 27 | 28 | config.middleware.insert 0, BotKiller 29 | 30 | # Please, add to the `ignore` list any other `lib` subdirectories that do 31 | # not contain `.rb` files, or that should not be reloaded or eager loaded. 32 | # Common ones are `templates`, `generators`, or `middleware`, for example. 33 | # config.autoload_lib(ignore: %w[assets tasks]) 34 | 35 | # Configuration for the application, engines, and railties goes here. 36 | # 37 | # These settings can be overridden in specific environments using the files 38 | # in config/environments, which are processed later. 39 | # 40 | # config.time_zone = "Central Time (US & Canada)" 41 | # config.eager_load_paths << Rails.root.join("extras") 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: sample_production 11 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | LwzsHbqWiwntNYJtlmzViZhsOdMSaOkb5oEnU8QIHx2b+GSYMIBxBlDxv6SjZJTgxLuyXTlTNq9+LQ/TYX9qYGwoYW2yO5NLpDPmpCzVNLJJnNWR37wbAO98E+rKgSAH8whHWbDPgb1poRt25mYhzGYfUVLfOWBwy5bE9ShL3HeqwvirE/NpQVy9zY38c4hzIPuhqzplO7ludVWmDKd59fFpXlr9VkEcl0iq8GPxHrHB9maB7EWrrHuPyePcg3nKlgHbQa+e3m4bNOE3UD6CDiDwWYkU9K3v1BszvO6jBi4QFteFi8IsChPJHS178ZW4OH+LQkDp4ECWDgOGLc0SOCn9V0s6zp2F6EkFFjaZlAib+dB/N2gMOm43zoDaQcthCB/M+ERBCVp+4ZYl2Y4bCfsxDlCRevzdO+vQ--bsE3WapjhHaNn90c--V5fM1ulmCIrSxNuH/f0xzg== -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | adapter: postgresql 3 | url: <%= ENV['DATABASE_URL'] %> 4 | pool: 5 5 | min_messages: error 6 | 7 | development: 8 | <<: *defaults 9 | host: localhost 10 | user: postgres 11 | database: rails_contributors_development 12 | 13 | test: 14 | <<: *defaults 15 | host: localhost 16 | user: postgres 17 | database: rails_contributors_test 18 | 19 | staging: 20 | <<: *defaults 21 | host: localhost 22 | user: postgres 23 | database: rails_contributors_development 24 | -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | lock '~> 3.10' 2 | 3 | set :application, 'rails-contributors' 4 | set :branch, 'main' 5 | set :repo_url, 'https://github.com/rails/rails-contributors.git' 6 | 7 | set :deploy_to, '/home/rails/rails-contributors' 8 | 9 | set :log_level, :info 10 | 11 | set :linked_files, %w{config/database.yml config/master.key} 12 | set :linked_dirs, %w{log tmp/pids tmp/cache tmp/sockets public/system public/assets rails.git} 13 | 14 | set :keep_releases, 5 15 | 16 | set :bundle_without, %w{development test deployment}.join(' ') 17 | set :bundle_gemfile, -> { release_path.join('Gemfile') } 18 | -------------------------------------------------------------------------------- /config/deploy/production.rb: -------------------------------------------------------------------------------- 1 | DOCS_SERVER_IP = '138.197.6.175' 2 | 3 | set :ssh_options, port: 987 4 | server DOCS_SERVER_IP, user: 'rails', roles: %w(web app db) 5 | 6 | set :puma_bind, 'unix:/tmp/rails-contributors.sock' 7 | set :puma_preload_app, false 8 | set :puma_workers, 1 9 | set :puma_phased_restart, true 10 | 11 | set :rvm_ruby_version, '3.3.4' 12 | set :rvm_custom_path, '/home/rails/.rvm' 13 | 14 | namespace :deploy do 15 | after :normalize_assets, :gzip_assets do 16 | on release_roles(fetch(:assets_roles)) do 17 | assets_path = release_path.join('public', fetch(:assets_prefix)) 18 | within assets_path do 19 | execute :find, ". \\( -name '*.js' -o -name '*.css' \\) -print0 | xargs -0 gzip --keep --best --quiet --force" 20 | end 21 | end 22 | end 23 | end 24 | 25 | after 'deploy:finished', :trigger_webhook do 26 | run_locally do 27 | execute "curl -X POST http://#{DOCS_SERVER_IP}:9292/rails-master-hook" 28 | end 29 | end 30 | 31 | after 'deploy:finished', 'deploy:cleanup_assets' 32 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded any time 7 | # it changes. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.enable_reloading = true 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable server timing. 18 | config.server_timing = true 19 | 20 | # Enable/disable caching. By default caching is disabled. 21 | # Run rails dev:cache to toggle caching. 22 | if Rails.root.join("tmp/caching-dev.txt").exist? 23 | config.action_controller.perform_caching = true 24 | config.action_controller.enable_fragment_cache_logging = true 25 | 26 | config.cache_store = :memory_store 27 | config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } 28 | else 29 | config.action_controller.perform_caching = false 30 | 31 | config.cache_store = :null_store 32 | end 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise exceptions for disallowed deprecations. 38 | config.active_support.disallowed_deprecation = :raise 39 | 40 | # Tell Active Support which deprecation messages to disallow. 41 | config.active_support.disallowed_deprecation_warnings = [] 42 | 43 | # Raise an error on page load if there are pending migrations. 44 | config.active_record.migration_error = :page_load 45 | 46 | # Highlight code that triggered database queries in logs. 47 | config.active_record.verbose_query_logs = true 48 | 49 | # Debug mode disables concatenation and preprocessing of assets. 50 | # This option may cause significant delays in view rendering with a large 51 | # number of complex assets. 52 | config.assets.debug = true 53 | 54 | # Suppress logger output for asset requests. 55 | config.assets.quiet = true 56 | 57 | # Raises error for missing translations. 58 | # config.i18n.raise_on_missing_translations = true 59 | 60 | # Annotate rendered view with file names. 61 | config.action_view.annotate_rendered_view_with_filenames = true 62 | 63 | # Raise error when a before_action's only/except options reference missing actions. 64 | config.action_controller.raise_on_missing_callback_actions = true 65 | end 66 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.enable_reloading = false 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | config.action_controller.page_cache_directory = "#{Rails.root}/public/cache" 19 | 20 | # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment 21 | # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). 22 | # config.require_master_key = true 23 | 24 | # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. 25 | # config.public_file_server.enabled = false 26 | 27 | # Compress CSS using a preprocessor. 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fall back to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 34 | # config.asset_host = "http://assets.example.com" 35 | 36 | # Specifies the header that your server uses for sending files. 37 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 38 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX 39 | 40 | # Assume all access to the app is happening through a SSL-terminating reverse proxy. 41 | # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. 42 | # config.assume_ssl = true 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Skip http-to-https redirect for the default health check endpoint. 48 | # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } 49 | 50 | # Log to STDOUT by default 51 | # config.logger = ActiveSupport::Logger.new(STDOUT) 52 | # .tap { |logger| logger.formatter = ::Logger::Formatter.new } 53 | # .then { |logger| ActiveSupport::TaggedLogging.new(logger) } 54 | 55 | # Use default logging formatter so that PID and timestamp are not suppressed. 56 | config.log_formatter = ::Logger::Formatter.new 57 | 58 | # Prepend all log lines with the following tags. 59 | config.log_tags = [ :request_id ] 60 | 61 | # "info" includes generic and useful information about system operation, but avoids logging too much 62 | # information to avoid inadvertent exposure of personally identifiable information (PII). If you 63 | # want to log everything, set the level to "debug". 64 | config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") 65 | 66 | # Use a different cache store in production. 67 | # config.cache_store = :mem_cache_store 68 | 69 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 70 | # the I18n.default_locale when a translation cannot be found). 71 | config.i18n.fallbacks = true 72 | 73 | # Don't log any deprecations. 74 | config.active_support.report_deprecations = false 75 | 76 | # Do not dump schema after migrations. 77 | config.active_record.dump_schema_after_migration = false 78 | 79 | # Enable DNS rebinding protection and other `Host` header attacks. 80 | # config.hosts = [ 81 | # "example.com", # Allow requests from example.com 82 | # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` 83 | # ] 84 | # Skip DNS rebinding protection for the default health check endpoint. 85 | # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } 86 | end 87 | -------------------------------------------------------------------------------- /config/environments/staging.rb: -------------------------------------------------------------------------------- 1 | production.rb -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | # While tests run files are not watched, reloading is not necessary. 12 | config.enable_reloading = false 13 | 14 | # Eager loading loads your entire application. When running a single test locally, 15 | # this is usually not necessary, and can slow down your test suite. However, it's 16 | # recommended that you enable it in continuous integration systems to ensure eager 17 | # loading is working properly before deploying your code. 18 | config.eager_load = ENV["CI"].present? 19 | 20 | # Configure public file server for tests with Cache-Control for performance. 21 | config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{1.hour.to_i}" } 22 | 23 | # Show full error reports and disable caching. 24 | config.consider_all_requests_local = true 25 | config.action_controller.perform_caching = false 26 | config.cache_store = :null_store 27 | 28 | # Render exception templates for rescuable exceptions and raise for other exceptions. 29 | config.action_dispatch.show_exceptions = :rescuable 30 | 31 | # Disable request forgery protection in test environment. 32 | config.action_controller.allow_forgery_protection = false 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | 37 | # Raise exceptions for disallowed deprecations. 38 | config.active_support.disallowed_deprecation = :raise 39 | 40 | # Tell Active Support which deprecation messages to disallow. 41 | config.active_support.disallowed_deprecation_warnings = [] 42 | 43 | # Raises error for missing translations. 44 | # config.i18n.raise_on_missing_translations = true 45 | 46 | # Annotate rendered view with file names. 47 | # config.action_view.annotate_rendered_view_with_filenames = true 48 | 49 | # Raise error when a before_action's only/except options reference missing actions. 50 | config.action_controller.raise_on_missing_callback_actions = true 51 | end 52 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w[ admin.js admin.css ] 13 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] 9 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles. 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src style-src) 22 | # 23 | # # Report violations without enforcing the policy. 24 | # # config.content_security_policy_report_only = true 25 | # end 26 | -------------------------------------------------------------------------------- /config/initializers/core_extensions.rb: -------------------------------------------------------------------------------- 1 | UNF_NORMALIZER = UNF::Normalizer.new 2 | 3 | module StringExtensions 4 | def parameterize 5 | super.gsub('ß', 'ss').gsub('ø', 'o') 6 | end 7 | 8 | def nfc 9 | UNF_NORMALIZER.normalize(self, :nfc) 10 | end 11 | end 12 | 13 | String.prepend(StringExtensions) 14 | -------------------------------------------------------------------------------- /config/initializers/ensure_rails_git_is_cloned.rb: -------------------------------------------------------------------------------- 1 | RAILS_GIT = "#{Rails.root}/rails.git" 2 | 3 | unless Dir.exist?(RAILS_GIT) 4 | puts <<~EOS 5 | Please, mirror the Rails repository using the command 6 | 7 | git clone --mirror https://github.com/rails/rails.git 8 | 9 | from the host computer. 10 | 11 | Once that is done, if you want to run the website please 12 | populate the database with 13 | 14 | dc/sync 15 | 16 | This takes a while and it is not necessary if you only 17 | want to run the test suite. 18 | EOS 19 | 20 | exit 1 21 | end 22 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 8 | ] 9 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/nfc_attribute_normalization.rb: -------------------------------------------------------------------------------- 1 | require 'nfc_attribute_normalizer' 2 | 3 | class ActiveRecord::Base 4 | extend NFCAttributeNormalizer 5 | end -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide HTTP permissions policy. For further 4 | # information see: https://developers.google.com/web/updates/2018/06/feature-policy 5 | 6 | # Rails.application.config.permissions_policy do |policy| 7 | # policy.camera :none 8 | # policy.gyroscope :none 9 | # policy.microphone :none 10 | # policy.usb :none 11 | # policy.fullscreen :self 12 | # policy.payment :self, "https://secure.example.com" 13 | # end 14 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /config/logrotate: -------------------------------------------------------------------------------- 1 | # sudo ln -s /home/rails/rails-contributors/config/logrotate /etc/logrotate.d/rails-contributors 2 | # 3 | # logrotate is not a daemon, but a cron job, there is nothing to restart. 4 | 5 | /home/rails/rails-contributors/shared/log/*.log { 6 | compress 7 | copytruncate 8 | daily 9 | dateext 10 | delaycompress 11 | missingok 12 | rotate 30 13 | } 14 | -------------------------------------------------------------------------------- /config/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream rails-contributors { 2 | server unix:/tmp/rails-contributors.sock fail_timeout=0; 3 | } 4 | 5 | server { 6 | server_name contributors.rubyonrails.org; 7 | 8 | # ~2 seconds is often enough for most folks to parse HTML/CSS and 9 | # retrieve needed images/icons/frames, connections are cheap in 10 | # nginx so increasing this is generally safe... 11 | keepalive_timeout 5; 12 | 13 | gzip_static on; 14 | gzip_vary on; 15 | 16 | # Path for static files 17 | root /home/rails/rails-contributors/current/public; 18 | 19 | if (-f $document_root/system/maintenance.html) { 20 | return 503; 21 | } 22 | 23 | # Serve static files, including those generated by page caching. 24 | try_files $uri /cache/$uri /cache/$uri/index.html /cache/$uri.html @app; 25 | 26 | location ^~ /assets/ { 27 | expires max; 28 | add_header Cache-Control "public"; 29 | break; 30 | } 31 | 32 | location @app { 33 | # an HTTP header important enough to have its own Wikipedia entry: 34 | # http://en.wikipedia.org/wiki/X-Forwarded-For 35 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 36 | 37 | # pass the Host: header from the client right along so redirects 38 | # can be set properly within the Rack application 39 | proxy_set_header Host $http_host; 40 | 41 | # we don't want nginx trying to do something clever with 42 | # redirects, we set the Host: header above already. 43 | proxy_redirect off; 44 | 45 | proxy_pass http://rails-contributors; 46 | } 47 | 48 | # Rails error pages 49 | error_page 500 502 504 /500.html; 50 | location = /500.html { 51 | root /home/rails/rails-contributors/current/public; 52 | } 53 | 54 | error_page 503 @maintenance; 55 | location @maintenance { 56 | rewrite ^(.*)$ /system/maintenance.html break; 57 | } 58 | 59 | listen 443 ssl; # managed by Certbot 60 | ssl_certificate /etc/letsencrypt/live/api.rubyonrails.org/fullchain.pem; # managed by Certbot 61 | ssl_certificate_key /etc/letsencrypt/live/api.rubyonrails.org/privkey.pem; # managed by Certbot 62 | include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 63 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot 64 | 65 | add_header Strict-Transport-Security "max-age=63072000; includeSubdomains;" always; # enable HSTS 66 | } 67 | 68 | server { 69 | if ($host = contributors.rubyonrails.org) { 70 | return 301 https://$host$request_uri; 71 | } # managed by Certbot 72 | 73 | server_name contributors.rubyonrails.org; 74 | listen 80; 75 | return 404; # managed by Certbot 76 | } 77 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | get 'contributors/in-time-window/:time_window' => 'contributors#in_time_window', as: 'contributors_in_time_window' 3 | 4 | resources :contributors, only: 'index' do 5 | get 'commits/in-time-window/:time_window' => 'commits#in_time_window', as: 'commits_in_time_window' 6 | get 'commits/in-release/:release_id' => 'commits#in_release', as: 'commits_in_release' 7 | get 'commits/in-edge' => 'commits#in_edge', as: 'commits_in_edge' 8 | 9 | resources :commits, only: 'index' 10 | end 11 | 12 | resources :releases, only: 'index' do 13 | resources :commits, only: 'index' 14 | resources :contributors, only: 'index' 15 | end 16 | 17 | get 'edge/contributors' => 'contributors#in_edge', as: 'contributors_in_edge' 18 | 19 | resource :faq, only: :show 20 | 21 | root to: 'contributors#index' 22 | end 23 | -------------------------------------------------------------------------------- /db/migrate/20141101094005_initial_schema.rb: -------------------------------------------------------------------------------- 1 | class InitialSchema < ActiveRecord::Migration[4.2] 2 | def up 3 | create_table :commits do |t| 4 | t.string :sha1, null: false 5 | t.string :author_email, null: false 6 | t.string :author_name, null: false 7 | t.datetime :author_date, null: false 8 | t.string :committer_email, null: false 9 | t.string :committer_name, null: false 10 | t.datetime :committer_date, null: false 11 | t.text :message, null: false 12 | t.text :diff 13 | t.integer :release_id 14 | t.boolean :merge, null: false 15 | 16 | t.index :sha1, unique: true 17 | t.index :release_id 18 | end 19 | 20 | create_table :contributions do |t| 21 | t.integer :contributor_id, null: false 22 | t.integer :commit_id, null: false 23 | 24 | t.index :contributor_id 25 | t.index :commit_id 26 | end 27 | 28 | create_table :contributors do |t| 29 | t.string :name, null: false 30 | t.string :url_id, null: false 31 | t.integer :rank 32 | 33 | t.index :name, unique: true 34 | t.index :url_id, unique: true 35 | end 36 | 37 | create_table :releases do |t| 38 | t.string :tag, null: false 39 | t.datetime :date, null: false 40 | t.integer :major, null: false 41 | t.integer :minor, null: false 42 | t.integer :tiny, null: false 43 | t.integer :patch, null: false 44 | 45 | t.index :tag, unique: true 46 | end 47 | 48 | create_table :repo_updates do |t| 49 | t.integer :ncommits, null: false 50 | t.datetime :started_at, null: false 51 | t.datetime :ended_at, null: false 52 | t.integer :nreleases, null: false 53 | t.boolean :nmupdated, null: false 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /db/migrate/20150326181907_add_first_contribution_at_to_contributors.rb: -------------------------------------------------------------------------------- 1 | class AddFirstContributionAtToContributors < ActiveRecord::Migration[4.2] 2 | def up 3 | add_column :contributors, :first_contribution_at, :datetime 4 | Contributor.try(:fill_missing_first_contribution_timestamps) 5 | end 6 | 7 | def down 8 | remove_column :contributors, :first_contribution_at 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20160512095609_rename_nmupdated.rb: -------------------------------------------------------------------------------- 1 | class RenameNmupdated < ActiveRecord::Migration[4.2] 2 | def change 3 | rename_column :repo_updates, :nmupdated, :rebuild_all 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema[7.0].define(version: 2016_05_12_095609) do 14 | # These are extensions that must be enabled in order to support this database 15 | enable_extension "plpgsql" 16 | 17 | create_table "commits", id: :serial, force: :cascade do |t| 18 | t.string "sha1", null: false 19 | t.string "author_email", null: false 20 | t.string "author_name", null: false 21 | t.datetime "author_date", precision: nil, null: false 22 | t.string "committer_email", null: false 23 | t.string "committer_name", null: false 24 | t.datetime "committer_date", precision: nil, null: false 25 | t.text "message", null: false 26 | t.text "diff" 27 | t.integer "release_id" 28 | t.boolean "merge", null: false 29 | t.index ["release_id"], name: "index_commits_on_release_id" 30 | t.index ["sha1"], name: "index_commits_on_sha1", unique: true 31 | end 32 | 33 | create_table "contributions", id: :serial, force: :cascade do |t| 34 | t.integer "contributor_id", null: false 35 | t.integer "commit_id", null: false 36 | t.index ["commit_id"], name: "index_contributions_on_commit_id" 37 | t.index ["contributor_id"], name: "index_contributions_on_contributor_id" 38 | end 39 | 40 | create_table "contributors", id: :serial, force: :cascade do |t| 41 | t.string "name", null: false 42 | t.string "url_id", null: false 43 | t.integer "rank" 44 | t.datetime "first_contribution_at", precision: nil 45 | t.index ["name"], name: "index_contributors_on_name", unique: true 46 | t.index ["url_id"], name: "index_contributors_on_url_id", unique: true 47 | end 48 | 49 | create_table "releases", id: :serial, force: :cascade do |t| 50 | t.string "tag", null: false 51 | t.datetime "date", precision: nil, null: false 52 | t.integer "major", null: false 53 | t.integer "minor", null: false 54 | t.integer "tiny", null: false 55 | t.integer "patch", null: false 56 | t.index ["tag"], name: "index_releases_on_tag", unique: true 57 | end 58 | 59 | create_table "repo_updates", id: :serial, force: :cascade do |t| 60 | t.integer "ncommits", null: false 61 | t.datetime "started_at", precision: nil, null: false 62 | t.datetime "ended_at", precision: nil, null: false 63 | t.integer "nreleases", null: false 64 | t.boolean "rebuild_all", null: false 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) 7 | # Mayor.create(:name => 'Daley', :city => cities.first) 8 | -------------------------------------------------------------------------------- /dc/bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec dc/exec bash "$@" 4 | -------------------------------------------------------------------------------- /dc/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec dc/exec cap production deploy 4 | -------------------------------------------------------------------------------- /dc/exec: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec docker-compose exec app "$@" 4 | -------------------------------------------------------------------------------- /dc/psql: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec dc/rails db -p 4 | -------------------------------------------------------------------------------- /dc/rails: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec dc/exec bin/rails "$@" 4 | -------------------------------------------------------------------------------- /dc/server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ $1 = 'staging' ]]; then 4 | docker-compose exec app \ 5 | rm -rf tmp/cache 6 | docker-compose exec -e RAILS_ENV=staging app \ 7 | bin/rails assets:precompile 8 | docker-compose exec -e RAILS_SERVE_STATIC_FILES=1 app \ 9 | bin/rails server -b 0.0.0.0 -e staging 10 | else 11 | dc/rails server -b 0.0.0.0 12 | fi 13 | -------------------------------------------------------------------------------- /dc/sync: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ ! -d rails.git ]]; then 4 | echo Mirroring the Rails repository, takes a while, please wait... 5 | git clone --mirror https://github.com/rails/rails.git 6 | fi 7 | 8 | if [[ $1 = 'all' ]]; then 9 | exec dc/rails runner "Repo.sync(rebuild_all: true)" 10 | else 11 | exec dc/rails runner Repo.sync 12 | fi 13 | -------------------------------------------------------------------------------- /doc/design.md: -------------------------------------------------------------------------------- 1 | # Rails Contributors design 2 | 3 | ## Main design guidelines 4 | 5 | * Rails Contributors is not about Rails, the project, it is about Rails contributors. People are first and this has to be reflected in the interface. 6 | 7 | * All contributors are credited in the home page. In particular, listings are not paginated. 8 | 9 | * Rails Contributors is not meant to be a ranking, but the interface revolves around commits because that's the most natural unit of work you can credit for. 10 | 11 | * Rails Contributors is not meant to be a ranking, but there is a number that gives context about how active someone has been when browsing listings others than the global one. 12 | 13 | * Auto-healing: Fixing credits and adding new mappings is very common, the application should handle this gracefully, detecting and merging accounts as needed automatically on deploy. 14 | 15 | * The application updates the database on every Rails push via a webhook in GitHub. Should detect which commits are new since the last run and import them efficiently. 16 | 17 | * Contributors are identified preferably by their real name. 18 | 19 | * At any point in time, the application should be able to credit all people from a local Rails checkout and an empty database. 20 | 21 | * It is impossible to give credit with 100% accuracy on the entire Rails history due to a number of reasons. Accept that, and do your best. 22 | 23 | * Page cache everything. 24 | 25 | ## Credit algorithm 26 | 27 | The logic used to give credit to a given commit is in the method `Commit#extract_candidates`. 28 | 29 | ## Main entities 30 | 31 | * [`Repo`](../app/models/repo.rb) knows how to update the local Rails checkout and update the database crediting people. New mappings are handled automatically. `Repo.sync` is the external entry point that needs to be executed in production with the `runner` command when the webhook is triggered. 32 | 33 | * [`Contributor`](../app/models/contributor.rb) and [`Commit`](../app/models/commit.rb) represent contributors and commits. Since the application supports multiple authors for a commit, they are connected by the [`Contribution`](../app/models/contribution.rb) join model. 34 | 35 | * [`NamesManager`](../app/models/names_manager.rb) is responsible for handling false positives, mappings, etc. This module is split in three for code organization. 36 | 37 | * [`Release`](../app/models/release.rb) represent Rails releases and has the code that detects and imports them. 38 | -------------------------------------------------------------------------------- /doc/docker.md: -------------------------------------------------------------------------------- 1 | # Development with Docker 2 | 3 | ## Bootstrap project 4 | 5 | If you just cloned the project: 6 | 7 | ``` 8 | $ docker-compose up -d 9 | $ dc/exec bundle install 10 | $ dc/rails db:setup 11 | $ dc/sync 12 | ``` 13 | 14 | The last command is going to clone the Rails repository and populate the database, it takes several minutes. 15 | 16 | ## Rails repository mirror 17 | 18 | The application assumes `rails.git` is present in the root directory with a mirror of the repository: 19 | 20 | ``` 21 | git clone --mirror https://github.com/rails/rails.git 22 | ``` 23 | 24 | That clone is performed by `dc/sync` automatically, but it does not update itself. To do so, please run 25 | 26 | ``` 27 | $ cd rails.git 28 | $ git fetch 29 | ``` 30 | 31 | whenever you need to get the most recent commits. 32 | 33 | ## Development 34 | 35 | To develop you need to start the services: 36 | 37 | ``` 38 | docker-compose start 39 | ``` 40 | 41 | When done, stop them: 42 | 43 | ``` 44 | docker-compose stop 45 | ``` 46 | 47 | A number of convenience scripts are located in the `dc` directory, all of them operate in the main `app` container: 48 | 49 | | Command | Description | 50 | | --------------- | -------------------------- | 51 | | `dc/bash` | Gets a Bash shell | 52 | | `dc/psql` | Gets a `psql` shell | 53 | | `dc/rails` | Runs `bin/rails` | 54 | | `dc/server` | Launches Puma in port 3000 | 55 | | `dc/sync` | Syncs the database | 56 | | `dc/deploy` | Deploys the application | 57 | 58 | The commands `dc/server` and `dc/psql` are convenience wrappers around `dc/rails`. In general `dc/rails` is the main command: 59 | 60 | ``` 61 | $ dc/rails test 62 | $ dc/rails console 63 | $ dc/rails db:migrate 64 | $ dc/rails runner 'p Commit.count' 65 | $ ... 66 | ``` 67 | 68 | The command `dc/server staging` runs the application in `staging` mode. That is a production-like environment (`config/environments/staging.rb` is a symlink to `config/environments/production.rb`), but it uses the development database and serves static files. If there are changes that act differently in `development` and `production`, this execution mode may be useful for checking things up before deployment. 69 | 70 | To update the local Rails checkout and update the credits run `dc/sync`. The command also accepts an optional argument `all`, which forces the recomputation of all assignments. This is handy when you've changed the heuristics, and it takes less than a full database rebuild, since it does not reimport the commits themselves (which is costly). 71 | 72 | ## Database persistence 73 | 74 | The database is stored in a volume and persists even if you take the containers down with `docker-compose down`. If you want to remove the volume and start from scratch please pass `-v` to the command. 75 | 76 | ## Production is not affected 77 | 78 | The production server does not run Docker, you can safely tweak the Docker setup without fear of breaking production. 79 | -------------------------------------------------------------------------------- /doc/fix_credit.md: -------------------------------------------------------------------------------- 1 | # How to fix credits 2 | 3 | ## Logic 4 | 5 | * Mappings are the most common way to fix a credit. These are appropriate when you want a handler, email, typo, etc. to be mapped to one single canonical real name. These correspondences go in [*app/models/names_manager/canonical_names.rb*](../app/models/names_manager/canonical_names.rb) and the application merges automatically the commits credited to the mapped string in production when deployed. Please take into account that these mappings are meant to connect handlers and friends which are *present* in the history. Add mappings only if the application has found the mapped string and you can write a test that fails for a particular commit otherwise, this is not a database of people's handlers. 6 | 7 | * Occasionally heuristics may give a false positive. Sometimes these are not authors ("Closes #1234"), sometimes they are indeed two people ("Jose and Yehuda"). These are managed in [*app/models/names_manager/false_positives.rb*](../app/models/names_manager/false_positives.rb). 8 | 9 | * In rare cases you need to force the credit. For example, the commit was sent with a generic company account, attribution is given in the commit message with a convention that is not supported, multiple authorship with some people missing, etc. These kind of fixes are implemented in [*app/models/names_manager/hard_coded_authors.rb*](../app/models/names_manager/hard_coded_authors.rb). These are really exceptional, please use this as a last resort. 10 | 11 | ## Tests 12 | 13 | Please, always add a test covering the fixed credit. Suites are found in the respective files in the [*test/credits*](../test/credits) directory. 14 | -------------------------------------------------------------------------------- /doc/production.md: -------------------------------------------------------------------------------- 1 | # Production setup 2 | 3 | ## Ruby 4 | 5 | Needs Ruby 2.7.7. 6 | 7 | ## System dependencies 8 | 9 | ``` 10 | # ExecJS runtime 11 | $ sudo apt-get install nodejs 12 | 13 | # PostgreSQL 14 | $ sudo apt-get install postgresql postgresql-contrib libpq-dev 15 | 16 | # rugged 17 | $ sudo apt-get install cmake pkg-config 18 | 19 | # Yarn 20 | $ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo -E apt-key add - 21 | $ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list 22 | $ sudo apt-get update && sudo apt-get install yarn 23 | ``` 24 | 25 | ## Rails repository mirror 26 | 27 | ``` 28 | git clone --mirror https://github.com/rails/rails.git 29 | ``` 30 | 31 | ## Ruby dependencies 32 | 33 | ``` 34 | bundle install 35 | ``` 36 | 37 | ## Database setup 38 | 39 | Assuming your user is able to create databases, for example by running 40 | 41 | ``` 42 | $ sudo -u postgres createuser --superuser $USER 43 | ``` 44 | 45 | just execute 46 | 47 | ``` 48 | $ bin/rails db:setup 49 | ``` 50 | 51 | to create the databases, and 52 | 53 | ``` 54 | $ bin/rails runner Repo.sync 55 | ``` 56 | 57 | to populate the default one. 58 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: . 4 | command: /bin/bash -c 'while true; do sleep 60; done' 5 | ports: 6 | - '3000:3000' 7 | depends_on: 8 | - db 9 | volumes: 10 | - .:/rails-contributors 11 | - ~/.ssh:/root/.ssh 12 | environment: 13 | DATABASE_URL: postgres://postgres:postgres@db 14 | db: 15 | image: postgres:14.13-alpine 16 | environment: 17 | POSTGRES_PASSWORD: postgres 18 | # A subdirectory, as recommended in https://hub.docker.com/_/postgres/. 19 | PGDATA: /var/lib/postgresql/data/pgdata 20 | volumes: 21 | - pgdata:/var/lib/postgresql/data/pgdata 22 | 23 | volumes: 24 | pgdata: 25 | -------------------------------------------------------------------------------- /lib/application_utils.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module ApplicationUtils 4 | def self.acquiring_lock_file(basename) 5 | abs_name = File.join(tmpdir, basename) 6 | lock_file = File.open(abs_name, File::CREAT | File::EXCL | File::WRONLY) 7 | Rails.logger.info("acquired lock file #{basename}") 8 | lock_file.write("#{$$}\n") 9 | begin 10 | yield 11 | ensure 12 | Rails.logger.info("releasing lock file #{basename}") 13 | lock_file.close 14 | FileUtils.rm_f(abs_name) 15 | end 16 | rescue Errno::EEXIST 17 | Rails.logger.info("couldn't acquire lock file #{basename}") 18 | end 19 | 20 | # Returns the name of the tmp directory under Rails.root. It creates 21 | # it if needed. 22 | def self.tmpdir 23 | tmpdir = File.join(Rails.root, 'tmp') 24 | Dir.mkdir(tmpdir) unless File.exist?(tmpdir) 25 | tmpdir 26 | end 27 | 28 | # Expires the page caches in the public directory. 29 | # 30 | # We move the cache directory first out to a temporary place, and then 31 | # recursively delete that one. It is done that way because if we rm -rf 32 | # directly and at the same time requests come and create new cache files a 33 | # black hole can be created and handling that is beyond the legendary 34 | # robustness of this website. 35 | # 36 | # On the other hand, moving is atomic. Atomic is good. 37 | def self.expire_cache 38 | cache_dir = Rails.application.config.action_controller.page_cache_directory 39 | 40 | if Dir.exist?(cache_dir) 41 | expired_cache = "#{tmpdir}/expired_cache.#{Time.now.to_f}" 42 | FileUtils.mv(cache_dir, expired_cache, force: true) 43 | FileUtils.rm_rf(expired_cache) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/bot_killer.rb: -------------------------------------------------------------------------------- 1 | class BotKiller 2 | BLACKLISTED_BOTS = %r{ 3 | FAST | 4 | MauiBot 5 | }xi 6 | 7 | def initialize(app) 8 | @app = app 9 | end 10 | 11 | def call(env) 12 | request = Rack::Request.new(env) 13 | 14 | if request.user_agent =~ BLACKLISTED_BOTS 15 | [404, {}, ["Not found"]] 16 | else 17 | @app.call(env) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/nfc_attribute_normalizer.rb: -------------------------------------------------------------------------------- 1 | # Not very clever, but will do for what we need. 2 | module NFCAttributeNormalizer 3 | def nfc(*attribute_names) 4 | include Module.new { 5 | attribute_names.each do |name| 6 | module_eval <<-EOS 7 | def #{name}=(value) 8 | value = value.to_s.scrub.nfc unless value.nil? 9 | write_attribute(#{name.inspect}, value) 10 | end 11 | EOS 12 | end 13 | } 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails/rails-contributors/3b875dfb13a6c3b11674157474d83b6400da2194/lib/tasks/.gitkeep -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/406-unsupported-browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Your browser is not supported (406) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

Your browser is not supported.

62 |

Please upgrade your browser to continue.

63 |
64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails/rails-contributors/3b875dfb13a6c3b11674157474d83b6400da2194/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | 3 | User-Agent: Google 4 | Allow: / 5 | 6 | User-Agent: DuckDuckBot 7 | Allow: / 8 | 9 | User-Agent: bingbot 10 | Crawl-delay: 5 11 | Allow: / 12 | 13 | User-Agent: * 14 | Disallow: / 15 | -------------------------------------------------------------------------------- /public/system/maintenance.html.deleteme: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Rails Contributors: Maintenance 7 | 8 | 9 | 10 | 11 |
12 |
13 |

Rails Contributors

14 |

15 | We are doing some maintenance to Rails Contributors. The site should 16 | be back soon. 17 |

18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 4 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400] 5 | end 6 | -------------------------------------------------------------------------------- /test/controllers/commits_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CommitsControllerTest < ActionController::TestCase 4 | def test_index_for_releases 5 | cases = [ 6 | [releases(:v3_2_0), Array(commits(:commit_6c65676, :commit_5b90635))], 7 | [releases(:v2_3_2), []], 8 | [releases(:v0_14_4), Array(commits(:commit_e0ef631))], 9 | ] 10 | 11 | cases.each do |release, commits| 12 | get :index, params: { release_id: release } 13 | 14 | assert_response :success 15 | assert_equal commits, assigns(:commits) 16 | 17 | label = commits.size == 1 ? 'commit' : 'commits' 18 | assert_select 'span.listing-total', "Showing #{commits.size} #{label}" 19 | assert_select 'li.current', 'Releases' 20 | end 21 | end 22 | 23 | def test_index_for_contributors 24 | cases = [ 25 | [:david, [:commit_339e4e8, :commit_e0ef631]], 26 | [:jeremy, [:commit_b821094, :commit_7cdfd91, :commit_5b90635]], 27 | [:jose, [:commit_5b90635]], 28 | ].map {|a, b| [contributors(a), Array(commits(*b))]} 29 | 30 | cases.each do |contributor, commits| 31 | get :index, params: { contributor_id: contributor } 32 | 33 | assert_response :success 34 | assert_equal commits, assigns(:commits) 35 | 36 | label = commits.size == 1 ? 'commit' : 'commits' 37 | assert_select 'span.listing-total', "Showing #{commits.size} #{label}" 38 | assert_select 'li.current', 'All time' 39 | end 40 | end 41 | 42 | def test_commits_in_time_window 43 | since = '20121219' 44 | date_range = '20121201-20121231' 45 | 46 | cases = { 47 | contributors(:david) => [ 48 | ['all-time', Array(commits(:commit_339e4e8, :commit_e0ef631))], 49 | ['today', []], 50 | ['this-week', []], 51 | ['this-year', Array(commits(:commit_339e4e8))], 52 | [since, []], 53 | [date_range, Array(commits(:commit_339e4e8))], 54 | ], 55 | 56 | contributors(:jeremy) => [ 57 | ['all-time', Array(commits(:commit_b821094, :commit_7cdfd91, :commit_5b90635))], 58 | ['today', Array(commits(:commit_b821094))], 59 | ['this-week', Array(commits(:commit_b821094))], 60 | ['this-year', Array(commits(:commit_b821094, :commit_7cdfd91, :commit_5b90635))], 61 | [since, Array(commits(:commit_b821094))], 62 | [date_range, Array(commits(:commit_b821094))], 63 | ], 64 | 65 | contributors(:vijay) => [ 66 | ['all-time', Array(commits(:commit_6c65676))], 67 | ['today', []], 68 | ['this-week', []], 69 | ['this-year', Array(commits(:commit_6c65676))], 70 | [since, []], 71 | [date_range, []], 72 | ], 73 | } 74 | 75 | time_travel do 76 | cases.each do |contributor, commits_per_time_window| 77 | commits_per_time_window.each do |time_window, commits| 78 | get :in_time_window, params: { contributor_id: contributor, time_window: time_window } 79 | 80 | assert_response :success 81 | assert_equal commits, assigns(:commits) 82 | 83 | label = commits.size == 1 ? 'commit' : 'commits' 84 | assert_select 'span.listing-total', "Showing #{commits.size} #{label}" 85 | if time_window == since || time_window == date_range 86 | assert_select 'li.current', false 87 | else 88 | assert_select 'li.current', TimeConstraints.label_for(time_window) 89 | end 90 | end 91 | end 92 | end 93 | end 94 | 95 | def test_in_release 96 | cases = { 97 | contributors(:jeremy) => {releases('v3_2_0') => [commits(:commit_5b90635)], 98 | contributors(:xavier) => {}}, 99 | contributors(:david) => {releases('v0_14_4') => [commits(:commit_e0ef631)]}, 100 | } 101 | 102 | all_releases = Release.all 103 | cases.each do |contributor, releases| 104 | all_releases.each do |release| 105 | commits = releases[release] || [] 106 | 107 | get :in_release, params: { contributor_id: contributor, release_id: release } 108 | 109 | assert_response :success 110 | assert_equal commits, assigns(:commits) 111 | 112 | label = commits.size == 1 ? 'commit' : 'commits' 113 | assert_select 'span.listing-total', "Showing #{commits.size} #{label}" 114 | assert_select 'li.current', 'Releases' 115 | end 116 | end 117 | end 118 | 119 | def test_in_edge 120 | cases = [ 121 | [:david, [:commit_339e4e8]], 122 | [:jeremy, [:commit_b821094, :commit_7cdfd91]], 123 | [:xavier, [:commit_26c024e]], 124 | ].map {|a, b| [contributors(a), Array(commits(*b))]} 125 | 126 | cases.each do |contributor, commits| 127 | get :in_edge, params: { contributor_id: contributor } 128 | 129 | assert_response :success 130 | assert_equal commits, assigns(:commits) 131 | 132 | label = commits.size == 1 ? 'commit' : 'commits' 133 | assert_select 'span.listing-total', "Showing #{commits.size} #{label}" 134 | assert_select 'li.current', 'Edge' 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /test/controllers/contributors_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ContributorsControllerTest < ActionController::TestCase 4 | def test_index_main_listing 5 | # Order by ncommits DESC, url_id ASC. 6 | expected = [[:jeremy, 3], [:david, 2], [:jose, 1], [:vijay, 1], [:xavier, 1]] 7 | 8 | get :index 9 | 10 | assert_response :success 11 | 12 | actual = assigns(:contributors) 13 | assert_equal expected.size, actual.size 14 | 15 | expected.zip(actual).each do |e, a| 16 | assert_equal contributors(e.first).name, a.name 17 | assert_equal e.second, a.ncommits 18 | end 19 | end 20 | 21 | def test_index_by_release 22 | releases = { 23 | 'v3.2.0' => [[:jeremy, 1], [:jose, 1], [:vijay, 1]], 24 | 'v0.14.4' => [[:david, 1]] 25 | } 26 | 27 | Release.all.each do |release| 28 | get :index, params: { release_id: release } 29 | 30 | assert_response :success 31 | 32 | actual = assigns(:contributors) 33 | 34 | if expected = releases[release.tag] 35 | assert_equal expected.size, actual.size 36 | expected.zip(actual) do |e, a| 37 | assert_equal contributors(e.first).name, a.name 38 | assert_equal e.second, a.ncommits 39 | end 40 | else 41 | assert_equal [], actual 42 | end 43 | end 44 | end 45 | 46 | def test_in_time_window 47 | since = '20121219' 48 | date_range = '20121201-20121231' 49 | 50 | time_windows = { 51 | 'all-time' => [[:jeremy, 3], [:david, 2], [:jose, 1], [:vijay, 1], [:xavier, 1]], 52 | 'today' => [[:jeremy, 1]], 53 | 'this-week' => [[:jeremy, 1], [:xavier, 1]], 54 | 'this-month' => [[:david, 1], [:jeremy, 1], [:xavier, 1]], 55 | 'this-year' => [[:jeremy, 3], [:david, 1], [:jose, 1], [:vijay, 1], [:xavier, 1]], 56 | since => [[:jeremy, 1], [:xavier, 1]], 57 | date_range => [[:david, 1], [:jeremy, 1], [:xavier, 1]], 58 | } 59 | 60 | time_travel do 61 | time_windows.each do |time_window, expected| 62 | get :in_time_window, params: { time_window: time_window } 63 | 64 | assert_response :success 65 | 66 | actual = assigns(:contributors) 67 | assert_equal expected.size, actual.size 68 | 69 | expected.zip(actual).each do |e, a| 70 | assert_equal contributors(e.first).name, a.name 71 | assert_equal e.second, a.ncommits 72 | end 73 | end 74 | end 75 | end 76 | 77 | def test_in_edge 78 | # Order by ncommits DESC, url_id ASC. 79 | expected = [[:jeremy, 2], [:david, 1], [:xavier, 1]] 80 | 81 | get :in_edge 82 | 83 | assert_response :success 84 | 85 | actual = assigns(:contributors) 86 | assert_equal expected.size, actual.size 87 | 88 | expected.zip(actual).each do |e, a| 89 | assert_equal contributors(e.first).name, a.name 90 | assert_equal e.second, a.ncommits 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /test/controllers/releases_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ReleasesControllerTest < ActionController::TestCase 4 | def test_index 5 | get :index 6 | assert_response :success 7 | 8 | assert_select 'span.listing-total', 'Showing 5 releases' 9 | 10 | expected = %w( 11 | v3_2_0 12 | v2_3_2_1 13 | v2_3_2 14 | v1_0_0 15 | v0_14_4 16 | ).map {|_| releases(_)} 17 | 18 | actual = assigns(:releases) 19 | 20 | assert_equal expected.size, actual.size 21 | expected.zip(assigns(:releases)).each do |e, a| 22 | assert_equal e.tag, a.tag 23 | assert_equal e.commits.count, a.ncommits 24 | assert_equal e.contributors.count, a.ncontributors 25 | end 26 | 27 | assert_select 'span.listing-total', 'Showing 5 releases' 28 | assert_select 'li.current', 'Releases' 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/credits/cleanup_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Credits 4 | class CleanupTest < ActiveSupport::TestCase 5 | include AssertContributorNames 6 | 7 | # CHANGELOG has "[François Beausoleil <...>, Blair Zajac <...>]". 8 | test 'names get email addresses stripped' do 9 | assert_contributor_names 'dfda57a', 'François Beausoleil', 'Blair Zajac' 10 | end 11 | 12 | # Commit message has "[*Godfrey Chan*, *Aaron Patterson*]". 13 | test 'removes Markdown emphasis' do 14 | assert_contributor_names '28d52c5', 'Godfrey Chan', 'Aaron Patterson' 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/credits/disambiguation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | # Some commits need disambiguation to be credited correctly. 4 | module Credits 5 | class DisambiguationTest < ActiveSupport::TestCase 6 | include AssertContributorNames 7 | 8 | test 'disambiguates abhishek' do 9 | assert_contributor_names '21f0c580', 'Abhishek Jain' 10 | assert_contributor_names '00e30b8f', 'Abhishek Yadav' 11 | end 12 | 13 | test 'disambiguates Sam' do 14 | assert_contributor_names 'b37399a', 'Sam Saffron' 15 | assert_contributor_names '0a57f34', 'Sam Saffron' 16 | assert_contributor_names '44fb54f', 'Sam' 17 | end 18 | 19 | test 'disambiguates James' do 20 | assert_contributor_names '63d7fd6', 'James Bowles' 21 | end 22 | 23 | test 'disambiguates root' do 24 | assert_contributor_names 'a9d3b77', 'Mohamed Osama' 25 | end 26 | 27 | test 'disambiguates unknown' do 28 | assert_contributor_names 'e813ad2a', 'Andrew Grimm' 29 | assert_contributor_names '2414fdb2', 'Jens Kolind' 30 | assert_contributor_names '3833d45', 'Manish Puri' 31 | end 32 | 33 | test 'disambiguates David' do 34 | assert_contributor_names '5d5f0bad', 'David Heinemeier Hansson', 'Sam Stephenson' 35 | assert_contributor_names 'bc437632', 'David Wang' 36 | end 37 | 38 | test 'disambiguates Jan' do 39 | assert_contributor_names '4942b41', 'Jan Habermann' 40 | assert_contributor_names 'f294540', 'Jan Xie' 41 | end 42 | 43 | test 'empty author' do 44 | assert_contributor_names '4e873ff', 'Jarek Radosz' 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/credits/hard_coded_authors_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Credits 4 | class SpecialCasedCommitsTest < ActiveSupport::TestCase 5 | include AssertContributorNames 6 | 7 | test 'special-cased commits' do 8 | assert_contributor_names '1382f4d', 'David Burger', 'Dana Jones' 9 | assert_contributor_names '882dd4e', 'David Calavera' 10 | assert_contributor_names 'f9a02b1', 'Juan Lupión' 11 | assert_contributor_names '4b4aa8f', 'Jesper Hvirring Henriksen' 12 | assert_contributor_names '945d999', 'Chris Heisterkamp' 13 | assert_contributor_names 'eb457ce', 'Joseph Palermo' 14 | assert_contributor_names '6f2c499', 'Adam Milligan' 15 | assert_contributor_names '9dbde4f', 'Adam Milligan' 16 | assert_contributor_names 'ddf2b4a', 'Xavier Noria' 17 | assert_contributor_names '3b1c69d', 'Kieran Pilkington' 18 | assert_contributor_names 'a4041c5', 'Vishnu Atrai' 19 | assert_contributor_names 'ec44763', 'Mohammad Typaldos' 20 | assert_contributor_names '99dd117', 'Mohammad Typaldos' 21 | assert_contributor_names '3582bce', 'Juanjo Bazán', 'Tarmo Tänav', 'BigTitus' 22 | assert_contributor_names '7e8e91c', 'Godfrey Chan', 'Philippe Creux' 23 | assert_contributor_names '798881e', 'Godfrey Chan', 'Sergio Campamá' 24 | assert_contributor_names '134c115', 'Godfrey Chan', 'Sergio Campamá' 25 | assert_contributor_names 'b23ffd0', 'Łukasz Sarnacki', 'Matt Aimonetti' 26 | assert_contributor_names '1240338', 'Blake Mesdag', 'Arthur Neves' 27 | assert_contributor_names 'd318bad', 'Grey Baker', 'Adrien Siami' 28 | assert_contributor_names '41adf87', 'Mislav Marohnić', 'Geoff Buesing' 29 | assert_contributor_names '6ddde02', 'Arthur Zapparoli', 'Michael Koziarski' 30 | assert_contributor_names '063c393', 'Iván Vega' 31 | assert_contributor_names '872e22c', 'Daniel Rikowski', 'Genadi Samokovarov' 32 | assert_contributor_names '9220935', 'Eileen M. Uchitelle', 'Aaron Patterson', 'Tsukuru Tanimichi' 33 | assert_contributor_names '9668cc3', 'Eileen M. Uchitelle', 'Aaron Patterson', 'Tsukuru Tanimichi' 34 | assert_contributor_names '4f1472d', 'John Bampton' 35 | assert_contributor_names 'fdbc55b', 'Yasuo Honda' 36 | assert_contributor_names '6c6c3fa', 'Yasuo Honda' 37 | assert_contributor_names '83c6ba1', 'S. Brent Faulkner' 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/credits/heuristics_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Credits 4 | class HeuristicsTest < ActiveSupport::TestCase 5 | include AssertContributorNames 6 | 7 | # Example from 1c47d04: 8 | # 9 | # Added Object#presence that returns the object if it's #present? otherwise returns nil [DHH/Colin Kelley] 10 | # 11 | test 'captures credit at the end of the subject' do 12 | assert_contributor_names '1c47d04', 'David Heinemeier Hansson', 'Colin Kelley' 13 | end 14 | 15 | # Example from 9e9793b: 16 | # 17 | # do not trigger AR lazy load hook before initializers ran. 18 | # 19 | # [Rafael Mendonça França & Yves Senn] 20 | # 21 | # This require caused the `active_record.set_configs` initializer to 22 | # ... 23 | # 24 | test 'captures credit in an isolated line' do 25 | # First line in body. 26 | assert_contributor_names '9e9793b', 'Rafael Mendonça França', 'Yves Senn' 27 | 28 | # Line in the middle. 29 | assert_contributor_names '2a67e12', 'Matthew Draper', 'Yves Senn' 30 | 31 | # There are multiple lines with [...], only one of them has credits. 32 | assert_contributor_names '84c0f73', 'Godfrey Chan', 'Xavier Noria' 33 | end 34 | 35 | # Example from ec20838: 36 | # 37 | # diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG 38 | # index b793bda..b76db7c 100644 39 | # --- a/activerecord/CHANGELOG 40 | # +++ b/activerecord/CHANGELOG 41 | # @@ -1,5 +1,7 @@ 42 | # *SVN* 43 | # 44 | # +* ActiveRecord::Base.remove_connection explicitly ... welcome. #3591 [Simon Stapleton, Tom Ward] 45 | # + 46 | # ... 47 | # 48 | test 'captures credit from CHANGELOGs in commits imported from Subversion' do 49 | assert_contributor_names 'ec20838', 'Simon Stapleton', 'Tom Ward' 50 | assert_contributor_names 'cf656ec', 'Christopher Cotton' 51 | assert_contributor_names '532d4e8', 'Michael Schoen' 52 | assert_contributor_names 'c95365a', 'Geoff Buesing' 53 | assert_contributor_names '8dea60b', 'Vitaly Kushner' 54 | end 55 | 56 | # Example from 71528b1: 57 | # 58 | # Previously we only added the "lib" subdirectory to the load path when 59 | # setting up gem dependencies for frozen gems. Now we add the "ext" 60 | # subdirectory as well for those gems which have compiled C extensions 61 | # as well. [Wincent Colaiuta] 62 | # 63 | # [#268 state:resolved] 64 | # 65 | test 'subject is defined by two consecutive newlines' do 66 | assert_contributor_names '71528b1', 'Wincent Colaiuta' 67 | end 68 | 69 | # Example from bf0f145: 70 | # 71 | # let the rails command recurse upwards looking for script/rails, and exec ruby on it for better portability [Xavier Noria] (Closes #4008) 72 | # 73 | # These were common in the SVN days. 74 | test 'eventual trailing "(Closes #nnn)" are ignored' do 75 | assert_contributor_names 'bf0f145', 'Xavier Noria' 76 | end 77 | 78 | # Example from 41bfede: 79 | # 80 | # Tidy up framework initialization code to ensure that it doesn't add folders to the load path that it doesn't intend to require. 81 | # 82 | # Work around mongrel swallowing LoadErrors to ensure that users get more helpful errors if active_resource is required but not missing. [mislav] Closes #9 83 | # 84 | # 85 | # git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@7738 5ecf4fe2-1ee6-0310-87b1-e25e094e27de 86 | # 87 | test 'captures credit at the end of the body message of commits imported from Subversion' do 88 | assert_contributor_names '03d37a2', 'Tobias Lütke' 89 | assert_contributor_names '41bfede', 'Mislav Marohnić' 90 | end 91 | 92 | # Example from 4e873ff: 93 | # 94 | # commit 4e873ffcdab0c445e2211db1d27ddd5b349f7913 95 | # Author: 96 | # Date: Tue Apr 12 00:59:55 2011 -0700 97 | # ... 98 | # 99 | test 'captures credit from email if author name is empty' do 100 | assert_contributor_names '4e873ff', 'Jarek Radosz' 101 | end 102 | 103 | test 'fallback to the author name' do 104 | assert_contributor_names 'f756bfb', 'Jeremy Daer' 105 | assert_contributor_names 'cf6b13b', 'Carlos Antonio da Silva' 106 | assert_contributor_names '6033d2c', 'Iñigo Solano Pàez' 107 | end 108 | 109 | test 'committers get credit for commits imported from Subversion' do 110 | assert_contributor_names 'cf656ec', 'Christopher Cotton', 'Marcel Molina Jr.', equal: true 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /test/credits/wanted_aliases_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | # These are aliases the application should not map to real names because 4 | # authors requested it. This suite prevents future passes resolving aliases 5 | # from forgetting this commitment. 6 | module Credits 7 | class WantedAliasesTest < ActiveSupport::TestCase 8 | include AssertContributorNames 9 | 10 | test 'bogdanvlviv' do 11 | assert_contributor_names 'f2489f4', 'bogdanvlviv' 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/fixtures/commits.yml: -------------------------------------------------------------------------------- 1 | # WARNING: contributors.yml has first contribution timestamps that depend on 2 | # these fixtures. Keep them in sync as needed. 3 | # 4 | # In the following comments we assumed today is 26 December 2012. 5 | # 6 | # This is a commit from today, but with an author date in the past, 7 | # which allows us to test that time windows consider the committer 8 | # date. (The real commit does not have these differences.) 9 | # 10 | # It has no release associated. 11 | commit_b821094: 12 | sha1: 'b82109495b2e910f05922b05a2df69d42f3635a9' 13 | author_email: 'jeremy@bitsweat.net' 14 | author_name: 'Jeremy Daer' 15 | author_date: '2012-12-19 21:02:24' 16 | committer_email: 'jeremy@bitsweat.net' 17 | committer_name: 'Jeremy Daer' 18 | committer_date: '2012-12-26 21:16:05' 19 | message: | 20 | Make test logs easier to read. 21 | 22 | Tagging every message in tests makes the logs really wide. It's great 23 | for grepping, but annoying to open in an editor or a narrow terminal. 24 | Try out a different approach: spit out a heading before each test. 25 | merge: false 26 | 27 | # This commit belongs to this week, no release. 28 | commit_26c024e: 29 | sha1: '26c024e95999e545fbef85a25165234af77ea605' 30 | author_email: 'fxn@hashref.com' 31 | author_name: 'Xavier Noria' 32 | author_date: '2012-12-24 21:14:02' 33 | committer_email: 'fxn@hashref.com' 34 | committer_name: 'Xavier Noria' 35 | committer_date: '2012-12-24 21:16:16' 36 | message: | 37 | silences "possibly useless use of :: in void context" warnings 38 | 39 | The AS utility silence_warnings does not really silence this 40 | one, because it is issued at parse-time. It seemed to in 41 | some places because the constant was the only expression in 42 | the block and therefore it was its return value, that could 43 | potentially be used by silence_warnings are return value of 44 | the yield call. 45 | 46 | To bypass the warning we assign to a variable. The chosen 47 | variable is "_" because it is special-cased in parse.c not 48 | to issue an "assigned but unused variable" warning in turn. 49 | merge: false 50 | 51 | # This commit belongs to this month, no release. 52 | commit_339e4e8: 53 | sha1: '339e4e80d514bd94fcb0e15689db43e5de83642a' 54 | author_email: 'david@loudthinking.com' 55 | author_name: 'David Heinemeier Hansson' 56 | author_date: '2012-12-07 20:37:25' 57 | committer_email: 'david@loudthinking.com' 58 | committer_name: 'David Heinemeier Hansson' 59 | committer_date: '2012-12-07 20:38:53' 60 | message: | 61 | Let the scaffold example use the "set shared record" pattern to explain callbacks 62 | merge: false 63 | 64 | # This commit belongs to this year, release 3.2.0. 65 | commit_6c65676: 66 | sha1: '6c656761ec951f74dcb844f46fe6839af3baa1d4' 67 | author_email: 'vijaydev.cse@gmail.com' 68 | author_name: 'Vijay Dev' 69 | author_date: '2012-01-20 00:46:37' 70 | committer_email: 'vijaydev.cse@gmail.com' 71 | committer_name: 'Vijay Dev' 72 | committer_date: '2012-01-20 00:47:14' 73 | message: | 74 | update release notes [ci skip] 75 | release: 'v3_2_0' 76 | merge: false 77 | 78 | # This commit belongs to this year, release 3.2.0, has two contributors to test 79 | # commit counters. 80 | commit_5b90635: 81 | sha1: '5b906355f122e36f9fa071c639ae6f6b869d2981' 82 | author_email: 'jose.valim@gmail.com' 83 | author_name: 'José Valim' 84 | author_date: '2012-01-19 19:49:13' 85 | committer_email: 'jose.valim@gmail.com' 86 | committer_name: 'José Valim' 87 | committer_date: '2012-01-19 19:49:13' 88 | message: | 89 | Update CHANGELOG 90 | release: 'v3_2_0' 91 | merge: false 92 | 93 | # This commit is seven years old, release v0.14.4. 94 | commit_e0ef631: 95 | sha1: 'e0ef63105538f8d97faa095234f069913dd5229c' 96 | author_email: 'david@loudthinking.com' 97 | author_name: 'David Heinemeier Hansson' 98 | author_date: '2005-11-11 10:07:24' 99 | committer_email: 'david@loudthinking.com' 100 | committer_name: 'David Heinemeier Hansson' 101 | committer_date: '2005-11-11 10:07:24' 102 | message: | 103 | Added stable branch to prepare for 1.0 release 104 | 105 | git-svn-id: http://svn-commit.rubyonrails.org/rails/branches/stable@2980 5ecf4fe2-1ee6-0310-87b1-e25e094e27de 106 | release: 'v0_14_4' 107 | merge: false 108 | 109 | # This commit has a committer_date that is more recent the author_date and 110 | # should appear in the contributions list before 5b90635. 111 | commit_7cdfd91: 112 | sha1: '7cdfd910b7cdd398f8b54542c5a6a17966a5c8f3' 113 | author_email: 'jeremy@bitsweat.net' 114 | author_name: 'Jeremy Daer' 115 | author_date: '2011-12-11 22:30:05' 116 | committer_email: 'kennyj@gmail.com' 117 | committer_name: 'kennyj' 118 | committer_date: '2012-03-07 04:01:00' 119 | message: | 120 | Use 1.9 native XML escaping to speed up html_escape and shush regexp warnings 121 | 122 | length user system total real 123 | before 6 0.010000 0.000000 0.010000 ( 0.012378) 124 | after 6 0.010000 0.000000 0.010000 ( 0.012866) 125 | before 60 0.040000 0.000000 0.040000 ( 0.046273) 126 | after 60 0.040000 0.000000 0.040000 ( 0.036421) 127 | before 600 0.390000 0.000000 0.390000 ( 0.390670) 128 | after 600 0.210000 0.000000 0.210000 ( 0.209094) 129 | before 6000 3.750000 0.000000 3.750000 ( 3.751008) 130 | after 6000 1.860000 0.000000 1.860000 ( 1.857901) 131 | merge: false 132 | -------------------------------------------------------------------------------- /test/fixtures/contributions.yml: -------------------------------------------------------------------------------- 1 | <% n = 0 %> 2 | 3 | contribution_<%= n += 1 %>: 4 | commit: commit_b821094 5 | contributor: jeremy 6 | 7 | contribution_<%= n += 1 %>: 8 | commit: commit_26c024e 9 | contributor: xavier 10 | 11 | contribution_<%= n += 1 %>: 12 | commit: commit_339e4e8 13 | contributor: david 14 | 15 | contribution_<%= n += 1 %>: 16 | commit: commit_6c65676 17 | contributor: vijay 18 | 19 | contribution_<%= n += 1 %>: 20 | commit: commit_5b90635 21 | contributor: jose 22 | 23 | contribution_<%= n += 1 %>: 24 | commit: commit_5b90635 25 | contributor: jeremy 26 | 27 | contribution_<%= n += 1 %>: 28 | commit: commit_e0ef631 29 | contributor: david 30 | 31 | contribution_<%= n += 1 %>: 32 | commit: commit_7cdfd91 33 | contributor: jeremy 34 | -------------------------------------------------------------------------------- /test/fixtures/contributors.yml: -------------------------------------------------------------------------------- 1 | david: 2 | name: 'David Heinemeier Hansson' 3 | url_id: 'david-heinemeier-hansson' 4 | first_contribution_at: '2005-11-11 10:07:24' 5 | 6 | jeremy: 7 | name: 'Jeremy Daer' 8 | url_id: 'jeremy-kemper' 9 | first_contribution_at: '2012-01-19 19:49:13' 10 | 11 | jose: 12 | name: 'José Valim' 13 | url_id: 'jose-valim' 14 | first_contribution_at: '2012-01-19 19:49:13' 15 | 16 | xavier: 17 | name: 'Xavier Noria' 18 | url_id: 'xavier-noria' 19 | first_contribution_at: '2012-12-24 21:16:16' 20 | 21 | vijay: 22 | name: 'Vijay Dev' 23 | url_id: 'vijay-dev' 24 | first_contribution_at: '2012-01-20 00:47:14' 25 | -------------------------------------------------------------------------------- /test/fixtures/diff_more_than_changelogs_69edebf.log: -------------------------------------------------------------------------------- 1 | diff --git a/actionmailer/CHANGELOG b/actionmailer/CHANGELOG 2 | index 2d1019e..8ba3ccd 100644 3 | --- a/actionmailer/CHANGELOG 4 | +++ b/actionmailer/CHANGELOG 5 | @@ -1,3 +1,8 @@ 6 | +*2.0.2* (December 16th, 2007) 7 | + 8 | +* Included in Rails 2.0.2 9 | + 10 | + 11 | *2.0.1* (December 7th, 2007) 12 | 13 | * Update ActionMailer so it treats ActionView the same way that ActionController does. Closes #10244 [rick] 14 | diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile 15 | index b87361b..4622871 100755 16 | --- a/actionmailer/Rakefile 17 | +++ b/actionmailer/Rakefile 18 | @@ -55,7 +55,7 @@ spec = Gem::Specification.new do |s| 19 | s.rubyforge_project = "actionmailer" 20 | s.homepage = "http://www.rubyonrails.org" 21 | 22 | - s.add_dependency('actionpack', '= 2.0.1' + PKG_BUILD) 23 | + s.add_dependency('actionpack', '= 2.0.2' + PKG_BUILD) 24 | 25 | s.has_rdoc = true 26 | s.requirements << 'none' 27 | diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb 28 | index 82136b6..954a472 100644 29 | --- a/actionmailer/lib/action_mailer/version.rb 30 | +++ b/actionmailer/lib/action_mailer/version.rb 31 | @@ -2,7 +2,7 @@ module ActionMailer 32 | module VERSION #:nodoc: 33 | MAJOR = 2 34 | MINOR = 0 35 | - TINY = 1 36 | + TINY = 2 37 | 38 | STRING = [MAJOR, MINOR, TINY].join('.') 39 | end 40 | diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG 41 | index 8e77c59..bf8b538 100644 42 | --- a/actionpack/CHANGELOG 43 | +++ b/actionpack/CHANGELOG 44 | @@ -1,4 +1,4 @@ 45 | -*SVN* 46 | +*2.0.2* (December 16th, 2007) 47 | 48 | * Fixed that ActionView#file_exists? would be incorrect if @first_render is set #10569 [dbussink] 49 | 50 | diff --git a/actionpack/Rakefile b/actionpack/Rakefile 51 | index 7c3757e..1b1d9a1 100644 52 | --- a/actionpack/Rakefile 53 | +++ b/actionpack/Rakefile 54 | @@ -76,7 +76,7 @@ spec = Gem::Specification.new do |s| 55 | s.has_rdoc = true 56 | s.requirements << 'none' 57 | 58 | - s.add_dependency('activesupport', '= 2.0.1' + PKG_BUILD) 59 | + s.add_dependency('activesupport', '= 2.0.2' + PKG_BUILD) 60 | 61 | s.require_path = 'lib' 62 | s.autorequire = 'action_controller' 63 | diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb 64 | index 1f978de..7aa6a5d 100644 65 | --- a/actionpack/lib/action_pack/version.rb 66 | +++ b/actionpack/lib/action_pack/version.rb 67 | @@ -2,7 +2,7 @@ module ActionPack #:nodoc: 68 | module VERSION #:nodoc: 69 | MAJOR = 2 70 | MINOR = 0 71 | - TINY = 1 72 | + TINY = 2 73 | 74 | STRING = [MAJOR, MINOR, TINY].join('.') 75 | end 76 | diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG 77 | index b168b07..fe44b1b 100644 78 | --- a/activerecord/CHANGELOG 79 | +++ b/activerecord/CHANGELOG 80 | @@ -1,4 +1,4 @@ 81 | -*SVN* 82 | +*2.0.2* (December 16th, 2007) 83 | 84 | * Ensure optimistic locking handles nil #lock_version values properly. Closes #10510 [rick] 85 | 86 | diff --git a/activerecord/Rakefile b/activerecord/Rakefile 87 | index 54d03af..c22934e 100755 88 | --- a/activerecord/Rakefile 89 | +++ b/activerecord/Rakefile 90 | @@ -172,7 +172,7 @@ spec = Gem::Specification.new do |s| 91 | s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) } 92 | end 93 | 94 | - s.add_dependency('activesupport', '= 2.0.1' + PKG_BUILD) 95 | + s.add_dependency('activesupport', '= 2.0.2' + PKG_BUILD) 96 | 97 | s.files.delete "test/fixtures/fixture_database.sqlite" 98 | s.files.delete "test/fixtures/fixture_database_2.sqlite" 99 | diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb 100 | index 41b234f..a8ee7db 100644 101 | --- a/activerecord/lib/active_record/version.rb 102 | +++ b/activerecord/lib/active_record/version.rb 103 | @@ -2,7 +2,7 @@ module ActiveRecord 104 | module VERSION #:nodoc: 105 | MAJOR = 2 106 | MINOR = 0 107 | - TINY = 1 108 | + TINY = 2 109 | 110 | STRING = [MAJOR, MINOR, TINY].join('.') 111 | end 112 | diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG 113 | index d3d20a6..243511c 100644 114 | --- a/activeresource/CHANGELOG 115 | +++ b/activeresource/CHANGELOG 116 | @@ -1,4 +1,4 @@ 117 | -*SVN* 118 | +*2.0.2* (December 16th, 2007) 119 | 120 | * Added more specific exceptions for 400, 401, and 403 (all descending from ClientError so existing rescues will work) #10326 [trek] 121 | 122 | diff --git a/activeresource/Rakefile b/activeresource/Rakefile 123 | index 7b0b8fd..3434864 100644 124 | --- a/activeresource/Rakefile 125 | +++ b/activeresource/Rakefile 126 | @@ -62,7 +62,7 @@ spec = Gem::Specification.new do |s| 127 | s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) } 128 | end 129 | 130 | - s.add_dependency('activesupport', '= 2.0.1' + PKG_BUILD) 131 | + s.add_dependency('activesupport', '= 2.0.2' + PKG_BUILD) 132 | 133 | s.require_path = 'lib' 134 | s.autorequire = 'active_resource' 135 | diff --git a/activeresource/lib/active_resource/version.rb b/activeresource/lib/active_resource/version.rb 136 | index a59c9d2..4f7de5e 100644 137 | --- a/activeresource/lib/active_resource/version.rb 138 | +++ b/activeresource/lib/active_resource/version.rb 139 | @@ -2,7 +2,7 @@ module ActiveResource 140 | module VERSION #:nodoc: 141 | MAJOR = 2 142 | MINOR = 0 143 | - TINY = 1 144 | + TINY = 2 145 | STRING = [MAJOR, MINOR, TINY].join('.') 146 | end 147 | diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb 148 | index 913198f..83fbaec 100644 149 | --- a/activesupport/lib/active_support/version.rb 150 | +++ b/activesupport/lib/active_support/version.rb 151 | @@ -2,7 +2,7 @@ module ActiveSupport 152 | module VERSION #:nodoc: 153 | MAJOR = 2 154 | MINOR = 0 155 | - TINY = 1 156 | + TINY = 2 157 | 158 | STRING = [MAJOR, MINOR, TINY].join('.') 159 | end 160 | diff --git a/railties/CHANGELOG b/railties/CHANGELOG 161 | index 1a74229..9d6f822 100644 162 | --- a/railties/CHANGELOG 163 | +++ b/railties/CHANGELOG 164 | @@ -1,4 +1,4 @@ 165 | -*SVN* 166 | +*2.0.2* (December 16th, 2007) 167 | 168 | * Changed the default database from mysql to sqlite3, so now running "rails myapp" will have a config/database.yml that's setup for SQLite3 (which in OS X Leopard is installed by default, so is the gem, so everything Just Works with no database configuration at all). To get a Rails application preconfigured for MySQL, just run "rails -d mysql myapp" [DHH] 169 | 170 | diff --git a/railties/Rakefile b/railties/Rakefile 171 | index fc32f57..0504d6c 100644 172 | --- a/railties/Rakefile 173 | +++ b/railties/Rakefile 174 | @@ -312,11 +312,11 @@ spec = Gem::Specification.new do |s| 175 | EOF 176 | 177 | s.add_dependency('rake', '>= 0.7.2') 178 | - s.add_dependency('activesupport', '= 2.0.1' + PKG_BUILD) 179 | - s.add_dependency('activerecord', '= 2.0.1' + PKG_BUILD) 180 | - s.add_dependency('actionpack', '= 2.0.1' + PKG_BUILD) 181 | - s.add_dependency('actionmailer', '= 2.0.1' + PKG_BUILD) 182 | - s.add_dependency('activeresource', '= 2.0.1' + PKG_BUILD) 183 | + s.add_dependency('activesupport', '= 2.0.2' + PKG_BUILD) 184 | + s.add_dependency('activerecord', '= 2.0.2' + PKG_BUILD) 185 | + s.add_dependency('actionpack', '= 2.0.2' + PKG_BUILD) 186 | + s.add_dependency('actionmailer', '= 2.0.2' + PKG_BUILD) 187 | + s.add_dependency('activeresource', '= 2.0.2' + PKG_BUILD) 188 | 189 | s.rdoc_options << '--exclude' << '.' 190 | s.has_rdoc = false 191 | diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb 192 | index 780a1d5..da90645 100644 193 | --- a/railties/lib/rails/version.rb 194 | +++ b/railties/lib/rails/version.rb 195 | @@ -2,7 +2,7 @@ module Rails 196 | module VERSION #:nodoc: 197 | MAJOR = 2 198 | MINOR = 0 199 | - TINY = 1 200 | + TINY = 2 201 | 202 | STRING = [MAJOR, MINOR, TINY].join('.') 203 | end -------------------------------------------------------------------------------- /test/fixtures/diff_only_changelogs_e3a39ca.log: -------------------------------------------------------------------------------- 1 | diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG 2 | index 610ad12..5e8cd4e 100644 3 | --- a/actionpack/CHANGELOG 4 | +++ b/actionpack/CHANGELOG 5 | @@ -1,5 +1,8 @@ 6 | *SVN* 7 | 8 | +* Make assert_routing aware of the HTTP method used. #8039 [mpalmer] 9 | + e.g. assert_routing({ :method => 'put', :path => '/product/321' }, { :controller => "product", :action => "update", :id => "321" }) 10 | + 11 | * Remove ERB trim variables from trace template in case ActionView::Base.erb_trim_mode is changed in the application. #10098 [tpope, kampers] 12 | 13 | * Fix typo in form_helper documentation. #10650 [xaviershay, kampers] 14 | diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG 15 | index 251b0c3..118e3b4 100644 16 | --- a/activerecord/CHANGELOG 17 | +++ b/activerecord/CHANGELOG 18 | @@ -1,9 +1,5 @@ 19 | *SVN* 20 | 21 | -* Make assert_routing aware of the HTTP method used. #8039 [mpalmer] 22 | - e.g. assert_routing({ :method => 'put', :path => '/product/321' }, { :controller => "product", :action => "update", :id => "321" }) 23 | - 24 | - 25 | * Ensure that modifying has_and_belongs_to_many actions clear the query cache. Closes #10840 [john.andrews] 26 | 27 | * Fix issue where Table#references doesn't pass a :null option to a *_type attribute for polymorphic associations. Closes #10753 [railsjitsu] 28 | -------------------------------------------------------------------------------- /test/fixtures/releases.yml: -------------------------------------------------------------------------------- 1 | v3_2_0: 2 | tag: 'v3.2.0' 3 | major: 3 4 | minor: 2 5 | tiny: 0 6 | patch: 0 7 | date: '2012-01-20 16:44:32' 8 | 9 | v2_3_2_1: 10 | tag: 'v2.3.2.1' 11 | major: 2 12 | minor: 3 13 | tiny: 2 14 | patch: 1 15 | date: '2009-03-17 12:26:34' 16 | 17 | v2_3_2: 18 | tag: 'v2.3.2' 19 | major: 2 20 | minor: 3 21 | tiny: 2 22 | patch: 0 23 | date: '2009-03-16 03:06:50' 24 | 25 | v1_0_0: 26 | tag: 'v1.0.0' 27 | major: 1 28 | minor: 0 29 | tiny: 0 30 | patch: 0 31 | date: '2005-12-13 00:00:00' 32 | 33 | v0_14_4: 34 | tag: 'v0.14.4' 35 | major: 0 36 | minor: 14 37 | tiny: 4 38 | patch: 0 39 | date: '2005-12-08 00:00:00' 40 | -------------------------------------------------------------------------------- /test/fixtures/repo_updates.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | # one: 4 | # column: value 5 | # 6 | # two: 7 | # column: value 8 | -------------------------------------------------------------------------------- /test/helpers/application_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ApplicationHelperTest < ActionView::TestCase 4 | include ApplicationHelper 5 | 6 | def test_github_url_for_sha1 7 | sha1 = 'd4ab03a8f3c1ea8913ed76bb8653dd9bef6a894f' 8 | assert_equal "https://github.com/rails/rails/commit/#{sha1}", github_url_for_sha1(sha1) 9 | end 10 | 11 | def test_github_url_for_tag 12 | tag = 'v4.2.1' 13 | assert_equal "https://github.com/rails/rails/tree/#{tag}", github_url_for_tag(tag) 14 | end 15 | 16 | def test_date 17 | assert_equal '07 Mar 2015', date(Time.new(2015, 3, 7)) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/integration/bot_killer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BotKillerTest < ActionDispatch::IntegrationTest 4 | test 'MauiBot is blacklisted' do 5 | get '/', headers: { 'User-Agent' => 'MauiBot (crawler.feedback+wc@gmail.com)' } 6 | assert_response :not_found 7 | end 8 | 9 | test 'FAST Enterprise is blacklisted' do 10 | get '/', headers: { 'User-Agent' => 'FAST Enterprise Crawler/5.3.4 (crawler@fast.no)' } 11 | assert_response :not_found 12 | end 13 | 14 | test 'other user agents are not blacklisted' do 15 | get '/', headers: { 'User-Agent' => 'Foo' } 16 | assert_response :ok 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/commit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'ostruct' 3 | 4 | class CommitTest < ActiveSupport::TestCase 5 | def extract_contributor_names_from_text(commit, text) 6 | commit.send(:extract_contributor_names_from_text, text) 7 | end 8 | 9 | def extract_changelog(commit) 10 | commit.send(:extract_changelog) 11 | end 12 | 13 | def only_modifies_changelogs?(commit) 14 | commit.send(:only_modifies_changelogs?) 15 | end 16 | 17 | def imported_from_svn?(commit) 18 | commit.send(:imported_from_svn?) 19 | end 20 | 21 | def test_import 22 | tcomm = Time.current 23 | tauth = 1.day.ago 24 | 25 | message = <<-MSG.strip_heredoc 26 | \u{1f4a3} 27 | 28 | We are relying on hash inequality in tests 29 | MSG 30 | 31 | [[], [1], [1, 2]].each.with_index do |parents, i| 32 | sha1 = "b5ed79468289c15a685a82694dcf1adf773c91d#{i}" 33 | rugged_commit = OpenStruct.new 34 | rugged_commit.oid = sha1 35 | rugged_commit.author = {name: 'Juanjo', email: 'juanjo@example.com', time: tauth} 36 | rugged_commit.committer = {name: 'David', email: 'david@example.com', time: tcomm} 37 | rugged_commit.message = message 38 | rugged_commit.parents = parents 39 | 40 | commit = Commit.import!(rugged_commit) 41 | 42 | assert_equal sha1, commit.sha1 43 | assert_equal 'Juanjo', commit.author_name 44 | assert_equal 'juanjo@example.com', commit.author_email 45 | assert_equal tauth, commit.author_date 46 | assert_equal 'David', commit.committer_name 47 | assert_equal 'david@example.com', commit.committer_email 48 | assert_equal tcomm, commit.committer_date 49 | assert_equal message, commit.message 50 | if parents.size > 1 51 | assert commit.merge? 52 | else 53 | assert !commit.merge? 54 | end 55 | end 56 | end 57 | 58 | def test_short_sha1 59 | commit = Commit.new(sha1: 'c0f3dc9728d8810e710d52e05bc61395297be787') 60 | assert_equal 'c0f3dc9', commit.short_sha1 61 | assert_equal 'c0f3dc9728', commit.short_sha1(10) 62 | end 63 | 64 | def test_github_url 65 | commit = Commit.new(sha1: 'c0f3dc9728d8810e710d52e05bc61395297be787') 66 | assert_equal 'https://github.com/rails/rails/commit/c0f3dc9728d8810e710d52e05bc61395297be787', commit.github_url 67 | end 68 | 69 | def test_imported_from_svn 70 | commit = Commit.new(message: <<-MSG.strip_heredoc) 71 | Added stable branch to prepare for 1.0 release 72 | 73 | git-svn-id: http://svn-commit.rubyonrails.org/rails/branches/stable@2980 5ecf4fe2-1ee6-0310-87b1-e25e094e27de 74 | MSG 75 | assert imported_from_svn?(commit) 76 | 77 | commit = Commit.new(message: 'Consistent use of single and double quotes') 78 | assert !imported_from_svn?(commit) 79 | end 80 | 81 | def test_short_message 82 | assert_nil Commit.new.short_message 83 | assert_equal 'foo', Commit.new(message: 'foo').short_message 84 | assert_equal 'foo', Commit.new(message: "foo\n").short_message 85 | assert_equal 'foo bar', Commit.new(message: "foo bar\nbar\nzoo\n").short_message 86 | end 87 | 88 | def test_extract_changelog 89 | commit = Commit.new(diff: read_fixture('diff_more_than_changelogs_69edebf.log')) 90 | assert_equal <<-CHANGELOG.strip_heredoc, extract_changelog(commit) 91 | +*2.0.2* (December 16th, 2007) 92 | +* Included in Rails 2.0.2 93 | +*2.0.2* (December 16th, 2007) 94 | +*2.0.2* (December 16th, 2007) 95 | +*2.0.2* (December 16th, 2007) 96 | +*2.0.2* (December 16th, 2007) 97 | CHANGELOG 98 | end 99 | 100 | def test_only_modifies_changelogs 101 | commit = Commit.new(diff: read_fixture('diff_only_changelogs_e3a39ca.log')) 102 | assert only_modifies_changelogs?(commit) 103 | 104 | commit = Commit.new(diff: read_fixture('diff_more_than_changelogs_69edebf.log')) 105 | assert !only_modifies_changelogs?(commit) 106 | end 107 | 108 | def test_basic_name_extraction 109 | commit = Commit.new 110 | 111 | assert_equal [], extract_contributor_names_from_text(commit, '') 112 | assert_equal [], extract_contributor_names_from_text(commit, 'nothing here to extract') 113 | assert_equal ['miloops'], extract_contributor_names_from_text(commit, 'Fix case-sensitive validates_uniqueness_of. Closes #11366 [miloops]') 114 | assert_equal ['Adam Milligan', 'Pratik'], extract_contributor_names_from_text(commit, 'Ensure methods called on association proxies respect access control. [#1083 state:resolved] [Adam Milligan, Pratik]') 115 | assert_equal ['jbarnette'], extract_contributor_names_from_text(commit, 'Correct documentation for dom_id [jbarnette] Closes #10775') 116 | assert_equal ['Sam'], extract_contributor_names_from_text(commit, 'Models with no attributes should just have empty hash fixtures [Sam] (Closes #3563)') 117 | assert_equal ['Kevin Clark', 'Jeremy Hopple'], extract_contributor_names_from_text(commit, <<-MESSAGE) 118 | * Fix pagination problems when using include 119 | * Introduce Unit Tests for pagination 120 | * Allow count to work with :include by using count distinct. 121 | 122 | [Kevin Clark & Jeremy Hopple] 123 | MESSAGE 124 | end 125 | 126 | # Message from c221b5b448569771678279216360460e066095a7. 127 | def test_extracts_co_authored_by_names 128 | commit = Commit.new( 129 | author_name: 'Joel Hawksley', 130 | message: <<~MESSAGE 131 | `RenderingHelper` supports rendering objects that `respond_to?` `:render_in` 132 | 133 | Co-authored-by: Natasha Umer 134 | Co-authored-by: Aaron Patterson 135 | Co-authored-by: Shawn Allen 136 | Co-authored-by: Emily Plummer 137 | Co-authored-by: Diana Mounter 138 | Co-authored-by: John Hawthorn 139 | Co-authored-by: Nathan Herald 140 | Co-authored-by: Zaid Zawaideh 141 | Co-authored-by: Zach Ahn 142 | MESSAGE 143 | ) 144 | 145 | expected_contributor_names = [ 146 | 'Joel Hawksley', 147 | 'Natasha Umer', 148 | 'Aaron Patterson', 149 | 'Shawn Allen', 150 | 'Emily Plummer', 151 | 'Diana Mounter', 152 | 'John Hawthorn', 153 | 'Nathan Herald', 154 | 'Zaid Zawaideh', 155 | 'Zach Ahn', 156 | ] 157 | 158 | assert_equal expected_contributor_names, commit.extract_contributor_names(Repo.new) 159 | end 160 | 161 | def test_extracts_co_authored_by_names_when_titlecase 162 | commit = Commit.new( 163 | author_name: 'Joel Hawksley', 164 | message: <<~MESSAGE 165 | `RenderingHelper` supports rendering objects that `respond_to?` `:render_in` 166 | 167 | Co-Authored-By: Natasha Umer 168 | MESSAGE 169 | ) 170 | 171 | expected_contributor_names = [ 172 | 'Joel Hawksley', 173 | 'Natasha Umer' 174 | ] 175 | 176 | assert_equal expected_contributor_names, commit.extract_contributor_names(Repo.new) 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /test/models/contributor_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'set' 3 | 4 | class ContributorTest < ActiveSupport::TestCase 5 | def test_the_name_writer_sets_url_id 6 | c = Contributor.new(name: 'Jeremy Daer') 7 | assert_equal 'jeremy-daer', c.url_id 8 | end 9 | 10 | def test_to_param_returns_the_url_id 11 | c = contributors(:jeremy) 12 | assert_equal c.url_id, c.to_param 13 | end 14 | 15 | def test_find_by_param 16 | c = contributors(:jeremy) 17 | assert_equal c, Contributor.find_by_param(c.to_param) 18 | end 19 | 20 | def test_with_no_commits 21 | jeremy = contributors(:jeremy) 22 | xavier = contributors(:xavier) 23 | 24 | jeremy.contributions.delete_all 25 | xavier.contributions.delete_all 26 | 27 | assert_equal [jeremy, xavier].to_set, Contributor.with_no_commits.to_set 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/models/names_manager_test.rb: -------------------------------------------------------------------------------- 1 | # The names manager has no unit tests. 2 | # 3 | # In general, the application does not care about the exact methods used to 4 | # give credit to contributors. Rather, we want to make sure a selected number 5 | # of commits get their credit right. 6 | # 7 | # Please check the test/credits directory for integration coverage. 8 | -------------------------------------------------------------------------------- /test/models/nfc_attribute_normalizer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class NFCAttributeNormalizerTest < ActiveSupport::TestCase 4 | A_GRAVE_NFC = "\u00c0" 5 | A_GRAVE_NON_NFC = "\u0041\u0300" 6 | NON_VALID = "\xa9" 7 | 8 | NORMALIZER = Class.new do 9 | extend NFCAttributeNormalizer 10 | 11 | nfc :title, :body 12 | 13 | def initialize 14 | @values = {} 15 | end 16 | 17 | def write_attribute(name, value) 18 | @values[name] = value 19 | end 20 | 21 | def read_attribute(name) 22 | @values[name] 23 | end 24 | end 25 | 26 | def setup 27 | @model = NORMALIZER.new 28 | end 29 | 30 | def test_normalizes_non_normalized_attributes 31 | @model.title = A_GRAVE_NON_NFC 32 | assert_equal A_GRAVE_NFC, @model.read_attribute(:title) 33 | 34 | @model.body = A_GRAVE_NON_NFC 35 | assert_equal A_GRAVE_NFC, @model.read_attribute(:body) 36 | end 37 | 38 | def test_normalizes_normalized_attributes 39 | @model.title = A_GRAVE_NFC 40 | assert_equal A_GRAVE_NFC, @model.read_attribute(:title) 41 | 42 | @model.body = A_GRAVE_NFC 43 | assert_equal A_GRAVE_NFC, @model.read_attribute(:body) 44 | end 45 | 46 | def test_ignores_nil 47 | @model.title = nil 48 | assert_nil @model.read_attribute(:title) 49 | 50 | @model.body = nil 51 | assert_nil @model.read_attribute(:body) 52 | end 53 | 54 | def test_scrubs 55 | assert !NON_VALID.valid_encoding? 56 | 57 | @model.title = NON_VALID 58 | 59 | assert_equal '�', @model.read_attribute(:title) 60 | assert @model.read_attribute(:title).valid_encoding? 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/models/parameterize_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'test_helper' 4 | 5 | class ParameterizeTest < ActiveSupport::TestCase 6 | test "normalizes special cases" do 7 | assert_equal 'sorensen', 'Sørensen'.parameterize 8 | assert_equal 'weierstrass', 'Weierstraß'.parameterize 9 | end 10 | 11 | test "delegates normalization of accented letters and friends" do 12 | assert_equal 'barca', 'Barça'.parameterize 13 | assert_equal 'campio', 'CAMPIÓ'.parameterize 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/models/release_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'set' 3 | 4 | class ReleaseTest < ActiveSupport::TestCase 5 | def check_version_split(tag, major=0, minor=0, tiny=0, patch=0) 6 | r = Release.new(tag: tag) 7 | assert_equal major, r.major 8 | assert_equal minor, r.minor 9 | assert_equal tiny, r.tiny 10 | assert_equal patch, r.patch 11 | end 12 | 13 | def ordered_releases 14 | %w( 15 | v3_2_0 16 | v2_3_2_1 17 | v2_3_2 18 | v1_0_0 19 | v0_14_4 20 | ).map {|tag| releases(tag)} 21 | end 22 | 23 | def test_the_tag_writer_splits_the_version 24 | check_version_split('v4', 4) 25 | check_version_split('v3.2', 3, 2) 26 | check_version_split('v3.2.0', 3, 2, 0) 27 | check_version_split('v3.2.1', 3, 2, 1) 28 | check_version_split('v2.3.2.1', 2, 3, 2, 1) 29 | end 30 | 31 | def test_name 32 | assert_equal '3.2.0', releases(:v3_2_0).name 33 | end 34 | 35 | def test_to_param 36 | assert_equal '3-2-0', releases(:v3_2_0).to_param 37 | assert_equal '2-3-2-1', releases(:v2_3_2_1).to_param 38 | end 39 | 40 | def test_find_by_param 41 | r = releases(:v3_2_0) 42 | assert_equal r, Release.find_by_param(r.to_param) 43 | end 44 | 45 | def test_github_url 46 | assert_equal 'https://github.com/rails/rails/tree/v3.2.0', releases(:v3_2_0).github_url 47 | end 48 | 49 | def test_the_date_writer_corrects_the_date_if_needed 50 | date = DateTime.new(2011, 8, 30, 18, 58, 35) 51 | r = Release.new(tag: 'v3.1.0', date: date) 52 | assert_equal date, r.date 53 | 54 | date = DateTime.new(2005, 12, 13) 55 | r = Release.new(tag: 'v1.0.0', date: DateTime.current) 56 | assert_equal date, r.date 57 | end 58 | 59 | def test_spaceship_operator 60 | ordered_releases.each_cons(2) do |r, t| 61 | assert r > t 62 | end 63 | end 64 | 65 | def test_prev 66 | ordered_releases.each_cons(2) do |r, t| 67 | assert_equal t, r.prev 68 | end 69 | end 70 | 71 | def test_associate_commits 72 | Commit.update_all(release_id: nil) 73 | 74 | release = releases('v3_2_0') 75 | commits = [commits('commit_b821094'), commits('commit_339e4e8'), commits('commit_6c65676')] 76 | sha1s = commits.map(&:sha1) 77 | 78 | release.associate_commits(sha1s) 79 | 80 | assert_equal sha1s.to_set, release.commits.pluck(:sha1).to_set 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /test/models/repo_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RepoTest < ActiveSupport::TestCase 4 | test '#sync_ranks' do 5 | Repo.new.send(:sync_ranks) 6 | 7 | {jeremy: 1, david: 2, jose: 3, xavier: 3, vijay: 3}.each do |c, r| 8 | assert_equal r, contributors(c).rank 9 | end 10 | end 11 | 12 | test '#sync_first_contributed_timestamps' do 13 | Contributor.update_all(first_contribution_at: nil) 14 | Repo.new.send(:sync_first_contribution_timestamps) 15 | 16 | assert_first_contribution :commit_e0ef631, :david 17 | assert_first_contribution :commit_5b90635, :jeremy 18 | assert_first_contribution :commit_5b90635, :jose 19 | assert_first_contribution :commit_26c024e, :xavier 20 | assert_first_contribution :commit_6c65676, :vijay 21 | end 22 | 23 | test '#sync_first_contributed_timestamps rebuilding all' do 24 | Contributor.update_all( 25 | first_contribution_at: Commit.minimum(:committer_date) - 1.year 26 | ) 27 | 28 | Repo.new(rebuild_all: true).send(:sync_first_contribution_timestamps) 29 | 30 | assert_first_contribution :commit_e0ef631, :david 31 | assert_first_contribution :commit_5b90635, :jeremy 32 | assert_first_contribution :commit_5b90635, :jose 33 | assert_first_contribution :commit_26c024e, :xavier 34 | assert_first_contribution :commit_6c65676, :vijay 35 | end 36 | 37 | def assert_first_contribution(commit, contributor) 38 | expected = commits(commit).committer_date 39 | actual = contributors(contributor).first_contribution_at 40 | 41 | assert_equal expected, actual 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/models/time_constraints_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TimeConstraintsTest < ActiveSupport::TestCase 4 | def assert_time_window(actual, since = nil, upto = nil) 5 | [[since, :since], [upto, :upto]].each do |expected, key| 6 | # Written this way because assert_equal(nil, nil) fails in MT6. 7 | if expected.nil? 8 | assert_nil actual[key] 9 | else 10 | assert_equal expected, actual[key] 11 | end 12 | end 13 | end 14 | 15 | def test_label_for_returns_a_label_for_a_valid_time_window 16 | assert_equal 'All time', TimeConstraints.label_for('all-time') 17 | assert_equal 'Date Range', TimeConstraints.label_for('20150101') 18 | assert_equal 'Date Range', TimeConstraints.label_for('20150101-20150318') 19 | end 20 | 21 | def test_valid_time_window_returns_true_for_valid_time_windows 22 | assert TimeConstraints.valid_time_window?('all-time') 23 | assert TimeConstraints.valid_time_window?('20150101') 24 | assert TimeConstraints.valid_time_window?('20150101-20150318') 25 | end 26 | 27 | def test_valid_time_window_returns_false_for_invalid_time_windows 28 | assert !TimeConstraints.valid_time_window?('unknown-key') 29 | end 30 | 31 | def test_time_window_for_returns_an_empty_array_for_all_time 32 | assert_time_window TimeConstraints.time_window_for('all-time') 33 | end 34 | 35 | def test_time_window_for_returns_today_for_today 36 | time_travel do 37 | assert_time_window TimeConstraints.time_window_for('today'), TODAY 38 | end 39 | end 40 | 41 | def test_time_window_for_returns_beginning_of_week_for_this_week 42 | time_travel do 43 | assert_time_window TimeConstraints.time_window_for('this-week'), Time.zone.parse('2012-12-24') 44 | end 45 | end 46 | 47 | def test_time_window_for_returns_beginning_of_month_for_this_month 48 | time_travel do 49 | assert_time_window TimeConstraints.time_window_for('this-month'), Time.zone.parse('2012-12-01') 50 | end 51 | end 52 | 53 | def test_time_window_for_returns_beginning_of_year_for_this_year 54 | time_travel do 55 | assert_time_window TimeConstraints.time_window_for('this-year'), Time.zone.parse('2012-01-01') 56 | end 57 | end 58 | 59 | def test_time_window_for_parses_dates 60 | assert_time_window TimeConstraints.time_window_for('20180318'), Time.zone.parse('2018-03-18') 61 | end 62 | 63 | def test_time_window_for_parses_timestamps 64 | assert_time_window TimeConstraints.time_window_for('201803181721'), Time.zone.parse('2018-03-18 17:21') 65 | end 66 | 67 | def test_time_window_for_parses_date_ranges 68 | assert_time_window TimeConstraints.time_window_for('20180318-20150319'), Time.zone.parse('2018-03-18'), Time.zone.parse('2015-03-19').end_of_day 69 | end 70 | 71 | def test_time_window_for_parses_timestamp_ranges 72 | assert_time_window TimeConstraints.time_window_for('201803181721-201503191807'), Time.zone.parse('2018-03-18 17:21'), Time.zone.parse('2015-03-19 18:07') 73 | end 74 | 75 | def test_time_window_for_parses_mixed_ranges 76 | assert_time_window TimeConstraints.time_window_for('20180318-201503191807'), Time.zone.parse('2018-03-18 00:00'), Time.zone.parse('2015-03-19 18:07') 77 | assert_time_window TimeConstraints.time_window_for('201803181721-20150319'), Time.zone.parse('2018-03-18 17:21'), Time.zone.parse('2015-03-19').end_of_day 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/support/assert_contributor_names.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module AssertContributorNames 4 | REPO = Repo.new 5 | 6 | def assert_contributor_names(sha1, *contributor_names, **options) 7 | begin 8 | commit = Commit.new_from_rugged_commit(REPO.repo.lookup(sha1)) 9 | rescue Rugged::OdbError 10 | raise "#{sha1} was not found, please make sure the local Rails checkout is up to date" 11 | end 12 | 13 | expected = contributor_names.to_set 14 | actual = commit.extract_contributor_names(REPO).to_set 15 | 16 | diff = expected - actual 17 | error_message = "credit for #{sha1} is #{actual.to_a.to_sentence}, expected #{diff.to_a.to_sentence} to be credited" 18 | 19 | if options[:equal] 20 | assert_equal expected, actual, error_message 21 | else 22 | assert expected.subset?(actual), error_message 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../config/environment', __FILE__) 2 | require 'rails/test_help' 3 | 4 | require_relative 'support/assert_contributor_names' 5 | 6 | class ActiveSupport::TestCase 7 | TODAY = Time.zone.parse('2012-12-26') 8 | 9 | fixtures :all 10 | 11 | def time_travel(&block) 12 | travel_to(TODAY, &block) 13 | end 14 | 15 | def read_fixture(name) 16 | File.read("#{Rails.root}/test/fixtures/#{name}") 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails/rails-contributors/3b875dfb13a6c3b11674157474d83b6400da2194/tmp/.keep --------------------------------------------------------------------------------